diff --git a/src/Artemis.UI.Avalonia.Shared/Controls/DeviceVisualizer.cs b/src/Artemis.UI.Avalonia.Shared/Controls/DeviceVisualizer.cs index d25ce1e17..543d83156 100644 --- a/src/Artemis.UI.Avalonia.Shared/Controls/DeviceVisualizer.cs +++ b/src/Artemis.UI.Avalonia.Shared/Controls/DeviceVisualizer.cs @@ -4,6 +4,7 @@ using System.Collections.ObjectModel; using System.ComponentModel; using System.IO; using System.Linq; +using System.Threading.Tasks; using Artemis.Core; using Artemis.UI.Avalonia.Shared.Events; using Avalonia; @@ -51,7 +52,7 @@ 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); - + DrawingContext.PushedState? boundsPush = null; try { @@ -70,6 +71,9 @@ namespace Artemis.UI.Avalonia.Shared.Controls if (_deviceImage != null) drawingContext.DrawImage(_deviceImage, new Rect(0, 0, Device.RgbDevice.ActualSize.Width, Device.RgbDevice.ActualSize.Height)); + if (!ShowColors) + return; + foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds) deviceVisualizerLed.RenderGeometry(drawingContext, false); } @@ -229,6 +233,7 @@ namespace Artemis.UI.Avalonia.Shared.Controls private void SetupForDevice() { + _deviceImage?.Dispose(); _deviceImage = null; _deviceVisualizerLeds.Clear(); _highlightedLeds = new List(); @@ -254,40 +259,44 @@ namespace Artemis.UI.Avalonia.Shared.Controls foreach (ArtemisLed artemisLed in Device.Leds) _deviceVisualizerLeds.Add(new DeviceVisualizerLed(artemisLed)); - // Load the device main image - if (Device.Layout?.Image != null && File.Exists(Device.Layout.Image.LocalPath)) + // Load the device main image on a background thread + ArtemisDevice? device = Device; + Task.Run(() => { - try + if (device.Layout?.Image != null && File.Exists(device.Layout.Image.LocalPath)) { - // Create a bitmap that'll be used to render the device and LED images just once - RenderTargetBitmap renderTargetBitmap = new(new PixelSize((int) Device.RgbDevice.Size.Width * 4, (int) Device.RgbDevice.Size.Height * 4)); + try + { + // Create a bitmap that'll be used to render the device and LED images just once + RenderTargetBitmap renderTargetBitmap = new(new PixelSize((int) device.RgbDevice.Size.Width * 4, (int) device.RgbDevice.Size.Height * 4)); - using IDrawingContextImpl context = renderTargetBitmap.CreateDrawingContext(new ImmediateRenderer(this)); - using Bitmap bitmap = new(Device.Layout.Image.LocalPath); - context.DrawBitmap(bitmap.PlatformImpl, 1, new Rect(bitmap.Size), new Rect(renderTargetBitmap.Size), BitmapInterpolationMode.HighQuality); - foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds) - deviceVisualizerLed.DrawBitmap(context); + using IDrawingContextImpl context = renderTargetBitmap.CreateDrawingContext(new ImmediateRenderer(this)); + using Bitmap bitmap = new(device.Layout.Image.LocalPath); + context.DrawBitmap(bitmap.PlatformImpl, 1, new Rect(bitmap.Size), new Rect(renderTargetBitmap.Size), BitmapInterpolationMode.HighQuality); + foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds) + deviceVisualizerLed.DrawBitmap(context); - _deviceImage = renderTargetBitmap; + _deviceImage = renderTargetBitmap; + + Dispatcher.UIThread.Post(InvalidateMeasure); + } + catch + { + // ignored + } } - catch - { - // ignored - } - } - - 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)); - } + double availableWidth = double.IsInfinity(availableSize.Width) ? _deviceBounds.Width : availableSize.Width; + double availableHeight = double.IsInfinity(availableSize.Height) ? _deviceBounds.Height : availableSize.Height; + double bestRatio = Math.Min(availableWidth / _deviceBounds.Width, availableHeight / _deviceBounds.Height); - #endregion + return new Size(_deviceBounds.Width * bestRatio, _deviceBounds.Height * bestRatio); + } #endregion } diff --git a/src/Artemis.UI.Avalonia.Shared/Services/Builders/ContentDialogBuilder.cs b/src/Artemis.UI.Avalonia.Shared/Services/Builders/ContentDialogBuilder.cs index 54d17c8cc..e082e189e 100644 --- a/src/Artemis.UI.Avalonia.Shared/Services/Builders/ContentDialogBuilder.cs +++ b/src/Artemis.UI.Avalonia.Shared/Services/Builders/ContentDialogBuilder.cs @@ -76,7 +76,7 @@ namespace Artemis.UI.Avalonia.Shared.Services.Builders return this; } - public ContentDialogBuilder WithViewModel(ref T viewModel, params (string name, object value)[] parameters) + public ContentDialogBuilder WithViewModel(out T viewModel, params (string name, object value)[] parameters) { IParameter[] paramsArray = parameters.Select(kv => new ConstructorArgument(kv.name, kv.value)).Cast().ToArray(); viewModel = _kernel.Get(paramsArray); diff --git a/src/Artemis.UI.Avalonia.Shared/Styles/InfoBar.axaml b/src/Artemis.UI.Avalonia.Shared/Styles/InfoBar.axaml index c3be2accd..3f9b4a9bc 100644 --- a/src/Artemis.UI.Avalonia.Shared/Styles/InfoBar.axaml +++ b/src/Artemis.UI.Avalonia.Shared/Styles/InfoBar.axaml @@ -3,8 +3,12 @@ xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"> - - + + + + + + diff --git a/src/Artemis.UI.Avalonia/Ninject/Factories/IVMFactory.cs b/src/Artemis.UI.Avalonia/Ninject/Factories/IVMFactory.cs index 735a93e58..cef934c9a 100644 --- a/src/Artemis.UI.Avalonia/Ninject/Factories/IVMFactory.cs +++ b/src/Artemis.UI.Avalonia/Ninject/Factories/IVMFactory.cs @@ -2,8 +2,10 @@ using Artemis.Core; using Artemis.UI.Avalonia.Screens.Device; using Artemis.UI.Avalonia.Screens.Device.Tabs.ViewModels; +using Artemis.UI.Avalonia.Screens.Device.ViewModels; using Artemis.UI.Avalonia.Screens.Plugins.ViewModels; using Artemis.UI.Avalonia.Screens.Root.ViewModels; +using Artemis.UI.Avalonia.Screens.Settings.Tabs.ViewModels; using Artemis.UI.Avalonia.Screens.SurfaceEditor.ViewModels; using ReactiveUI; @@ -16,6 +18,8 @@ namespace Artemis.UI.Avalonia.Ninject.Factories public interface IDeviceVmFactory : IVmFactory { DevicePropertiesViewModel DevicePropertiesViewModel(ArtemisDevice device); + DeviceSettingsViewModel DeviceSettingsViewModel(ArtemisDevice device, DevicesTabViewModel devicesTabViewModel); + DeviceDetectInputViewModel DeviceDetectInputViewModel(ArtemisDevice device); DevicePropertiesTabViewModel DevicePropertiesTabViewModel(ArtemisDevice device); DeviceInfoTabViewModel DeviceInfoTabViewModel(ArtemisDevice device); DeviceLedsTabViewModel DeviceLedsTabViewModel(ArtemisDevice device, ObservableCollection selectedLeds); diff --git a/src/Artemis.UI.Avalonia/Screens/Device/ViewModels/DeviceDetectInputViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Device/ViewModels/DeviceDetectInputViewModel.cs new file mode 100644 index 000000000..636532a0f --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/Device/ViewModels/DeviceDetectInputViewModel.cs @@ -0,0 +1,15 @@ +using Artemis.Core; + +namespace Artemis.UI.Avalonia.Screens.Device.ViewModels +{ + public class DeviceDetectInputViewModel + { + public DeviceDetectInputViewModel(ArtemisDevice device) + { + Device = device; + } + + public ArtemisDevice Device { get; } + public bool MadeChanges { get; set; } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Device/DevicePropertiesViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Device/ViewModels/DevicePropertiesViewModel.cs similarity index 100% rename from src/Artemis.UI.Avalonia/Screens/Device/DevicePropertiesViewModel.cs rename to src/Artemis.UI.Avalonia/Screens/Device/ViewModels/DevicePropertiesViewModel.cs diff --git a/src/Artemis.UI.Avalonia/Screens/Device/ViewModels/DeviceSettingsViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Device/ViewModels/DeviceSettingsViewModel.cs new file mode 100644 index 000000000..12fbe647a --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/Device/ViewModels/DeviceSettingsViewModel.cs @@ -0,0 +1,98 @@ +using System.Threading.Tasks; +using Artemis.Core; +using Artemis.Core.Services; +using Artemis.UI.Avalonia.Ninject.Factories; +using Artemis.UI.Avalonia.Screens.Settings.Tabs.ViewModels; +using Artemis.UI.Avalonia.Shared; +using Artemis.UI.Avalonia.Shared.Services.Interfaces; +using Humanizer; +using ReactiveUI; +using RGB.NET.Core; + +namespace Artemis.UI.Avalonia.Screens.Device.ViewModels +{ + public class DeviceSettingsViewModel : ActivatableViewModelBase + { + private readonly IDeviceService _deviceService; + private readonly DevicesTabViewModel _devicesTabViewModel; + private readonly IDeviceVmFactory _deviceVmFactory; + private readonly IRgbService _rgbService; + private readonly IWindowService _windowService; + + public DeviceSettingsViewModel(ArtemisDevice device, DevicesTabViewModel devicesTabViewModel, IDeviceService deviceService, IWindowService windowService, IDeviceVmFactory deviceVmFactory, + IRgbService rgbService) + { + _devicesTabViewModel = devicesTabViewModel; + _deviceService = deviceService; + _windowService = windowService; + _deviceVmFactory = deviceVmFactory; + _rgbService = rgbService; + Device = device; + + Type = Device.DeviceType.ToString().Humanize(); + Name = Device.RgbDevice.DeviceInfo.Model; + Manufacturer = Device.RgbDevice.DeviceInfo.Manufacturer; + } + + public ArtemisDevice Device { get; } + + public string Type { get; } + public string Name { get; } + public string Manufacturer { get; } + + public bool CanDetectInput => Device.DeviceType is RGBDeviceType.Keyboard or RGBDeviceType.Mouse; + + public bool IsDeviceEnabled + { + get => Device.IsEnabled; + set => Task.Run(() => UpdateIsDeviceEnabled(value)); + } + + public void IdentifyDevice() + { + _deviceService.IdentifyDevice(Device); + } + + public void OpenPluginDirectory() + { + Utilities.OpenFolder(Device.DeviceProvider.Plugin.Directory.FullName); + } + + public async Task DetectInput() + { + if (!CanDetectInput) + return; + + await _windowService.CreateContentDialog() + .WithViewModel(out var viewModel, ("device", Device)) + .ShowAsync(); + + if (viewModel.MadeChanges) + _rgbService.SaveDevice(Device); + } + + public async Task ViewProperties() + { + await _windowService.ShowDialogAsync(_deviceVmFactory.DevicePropertiesViewModel(Device)); + } + + private async Task UpdateIsDeviceEnabled(bool value) + { + if (!value) + value = !await _devicesTabViewModel.ShowDeviceDisableDialog(); + + if (value) + _rgbService.EnableDevice(Device); + else + _rgbService.DisableDevice(Device); + + this.RaisePropertyChanged(nameof(IsDeviceEnabled)); + SaveDevice(); + } + + private void SaveDevice() + { + _rgbService.SaveDevice(Device); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Device/Views/DeviceDetectInputView.axaml b/src/Artemis.UI.Avalonia/Screens/Device/Views/DeviceDetectInputView.axaml new file mode 100644 index 000000000..71c6948f7 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/Device/Views/DeviceDetectInputView.axaml @@ -0,0 +1,8 @@ + + Welcome to Avalonia! + diff --git a/src/Artemis.UI.Avalonia/Screens/Device/Views/DeviceDetectInputView.axaml.cs b/src/Artemis.UI.Avalonia/Screens/Device/Views/DeviceDetectInputView.axaml.cs new file mode 100644 index 000000000..ed56b2222 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/Device/Views/DeviceDetectInputView.axaml.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Artemis.UI.Avalonia.Screens.Device.Views +{ + public partial class DeviceDetectInputView : UserControl + { + public DeviceDetectInputView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Artemis.UI.Avalonia/Screens/Device/DevicePropertiesView.axaml b/src/Artemis.UI.Avalonia/Screens/Device/Views/DevicePropertiesView.axaml similarity index 100% rename from src/Artemis.UI.Avalonia/Screens/Device/DevicePropertiesView.axaml rename to src/Artemis.UI.Avalonia/Screens/Device/Views/DevicePropertiesView.axaml diff --git a/src/Artemis.UI.Avalonia/Screens/Device/DevicePropertiesView.axaml.cs b/src/Artemis.UI.Avalonia/Screens/Device/Views/DevicePropertiesView.axaml.cs similarity index 100% rename from src/Artemis.UI.Avalonia/Screens/Device/DevicePropertiesView.axaml.cs rename to src/Artemis.UI.Avalonia/Screens/Device/Views/DevicePropertiesView.axaml.cs diff --git a/src/Artemis.UI.Avalonia/Screens/Device/Views/DeviceSettingsView.axaml b/src/Artemis.UI.Avalonia/Screens/Device/Views/DeviceSettingsView.axaml new file mode 100644 index 000000000..8bbc47641 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/Device/Views/DeviceSettingsView.axaml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Device/Views/DeviceSettingsView.axaml.cs b/src/Artemis.UI.Avalonia/Screens/Device/Views/DeviceSettingsView.axaml.cs new file mode 100644 index 000000000..50bb1b25a --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/Device/Views/DeviceSettingsView.axaml.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Artemis.UI.Avalonia.Screens.Device.Views +{ + public partial class DeviceSettingsView : UserControl + { + public DeviceSettingsView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/SettingsView.axaml b/src/Artemis.UI.Avalonia/Screens/Settings/SettingsView.axaml index 2ff5e265b..ddf9602d9 100644 --- a/src/Artemis.UI.Avalonia/Screens/Settings/SettingsView.axaml +++ b/src/Artemis.UI.Avalonia/Screens/Settings/SettingsView.axaml @@ -12,7 +12,7 @@ - + diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/ViewModels/DevicesTabViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/ViewModels/DevicesTabViewModel.cs index 3423b8c09..39fdafccc 100644 --- a/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/ViewModels/DevicesTabViewModel.cs +++ b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/ViewModels/DevicesTabViewModel.cs @@ -1,12 +1,87 @@ -using Artemis.UI.Avalonia.Shared; +using System; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Threading.Tasks; +using Artemis.Core; +using Artemis.Core.Services; +using Artemis.UI.Avalonia.Ninject.Factories; +using Artemis.UI.Avalonia.Screens.Device.ViewModels; +using Artemis.UI.Avalonia.Shared; +using Artemis.UI.Avalonia.Shared.Services.Interfaces; +using Avalonia.Threading; +using DynamicData; +using ReactiveUI; namespace Artemis.UI.Avalonia.Screens.Settings.Tabs.ViewModels { public class DevicesTabViewModel : ActivatableViewModelBase { - public DevicesTabViewModel() + private readonly IDeviceVmFactory _deviceVmFactory; + private readonly IRgbService _rgbService; + private readonly IWindowService _windowService; + private bool _confirmedDisable; + + public DevicesTabViewModel(IRgbService rgbService, IWindowService windowService, IDeviceVmFactory deviceVmFactory) { DisplayName = "Devices"; + + _rgbService = rgbService; + _windowService = windowService; + _deviceVmFactory = deviceVmFactory; + + Devices = new ObservableCollection(); + this.WhenActivated(disposables => + { + GetDevices(); + + Observable.FromEventPattern(x => rgbService.DeviceAdded += x, x => rgbService.DeviceAdded -= x) + .Subscribe(d => AddDevice(d.EventArgs.Device)) + .DisposeWith(disposables); + Observable.FromEventPattern(x => rgbService.DeviceRemoved += x, x => rgbService.DeviceRemoved -= x) + .Subscribe(d => RemoveDevice(d.EventArgs.Device)) + .DisposeWith(disposables); + }); + } + + private void GetDevices() + { + Devices.Clear(); + Dispatcher.UIThread.InvokeAsync(() => + { + Devices.AddRange(_rgbService.Devices.Select(d => _deviceVmFactory.DeviceSettingsViewModel(d, this))); + }, DispatcherPriority.Background); + } + + public ObservableCollection Devices { get; } + + public async Task ShowDeviceDisableDialog() + { + if (_confirmedDisable) + return true; + + bool confirmed = await _windowService.ShowConfirmContentDialog( + "Disabling device", + "Disabling a device will cause it to stop updating. " + + "\r\nSome SDKs will even go back to using manufacturer lighting (Artemis restart may be required)." + ); + if (confirmed) + _confirmedDisable = true; + + return confirmed; + } + + private void AddDevice(ArtemisDevice device) + { + Devices.Add(_deviceVmFactory.DeviceSettingsViewModel(device, this)); + } + + private void RemoveDevice(ArtemisDevice device) + { + DeviceSettingsViewModel? viewModel = Devices.FirstOrDefault(i => i.Device == device); + if (viewModel != null) + Devices.Remove(viewModel); } } -} +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/ViewModels/PluginsTabViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/ViewModels/PluginsTabViewModel.cs index bbf898563..6c2928bfe 100644 --- a/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/ViewModels/PluginsTabViewModel.cs +++ b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/ViewModels/PluginsTabViewModel.cs @@ -13,6 +13,7 @@ using Artemis.UI.Avalonia.Screens.Plugins.ViewModels; using Artemis.UI.Avalonia.Shared; using Artemis.UI.Avalonia.Shared.Services.Builders; using Artemis.UI.Avalonia.Shared.Services.Interfaces; +using Avalonia.Threading; using ReactiveUI; namespace Artemis.UI.Avalonia.Screens.Settings.Tabs.ViewModels @@ -82,12 +83,16 @@ namespace Artemis.UI.Avalonia.Screens.Settings.Tabs.ViewModels public void GetPluginInstances() { - _instances = _pluginManagementService.GetAllPlugins() - .Select(p => _settingsVmFactory.CreatePluginSettingsViewModel(p)) - .OrderBy(i => i.Plugin.Info.Name) - .ToList(); + Plugins.Clear(); + Dispatcher.UIThread.InvokeAsync(() => + { + _instances = _pluginManagementService.GetAllPlugins() + .Select(p => _settingsVmFactory.CreatePluginSettingsViewModel(p)) + .OrderBy(i => i.Plugin.Info.Name) + .ToList(); - SearchPlugins(SearchPluginInput); + SearchPlugins(SearchPluginInput); + }, DispatcherPriority.Background); } private void SearchPlugins(string? searchPluginInput) diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Views/DevicesTabView.axaml b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Views/DevicesTabView.axaml index 22fdc9002..fe66685ba 100644 --- a/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Views/DevicesTabView.axaml +++ b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Views/DevicesTabView.axaml @@ -4,5 +4,22 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Avalonia.Screens.Settings.Tabs.Views.DevicesTabView"> - Welcome to Avalonia! - + + Device management + + Below you view and manage the devices that were detected by Artemis. + + + Disabling a device will cause it to stop updating. Some SDKs will even go back to using manufacturer lighting (Artemis restart may be required). + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Views/DevicesTabView.axaml.cs b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Views/DevicesTabView.axaml.cs index 9efeb4f18..84e9a9ed7 100644 --- a/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Views/DevicesTabView.axaml.cs +++ b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Views/DevicesTabView.axaml.cs @@ -1,9 +1,10 @@ -using Avalonia.Controls; +using Artemis.UI.Avalonia.Screens.Settings.Tabs.ViewModels; using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; namespace Artemis.UI.Avalonia.Screens.Settings.Tabs.Views { - public partial class DevicesTabView : UserControl + public class DevicesTabView : ReactiveUserControl { public DevicesTabView() { @@ -15,4 +16,4 @@ namespace Artemis.UI.Avalonia.Screens.Settings.Tabs.Views AvaloniaXamlLoader.Load(this); } } -} +} \ No newline at end of file