From 4737173c2af57331905f2bcca3db6a24bf4c2454 Mon Sep 17 00:00:00 2001 From: Robert Date: Tue, 17 Oct 2023 19:54:19 +0200 Subject: [PATCH] Revert "Revert "Merge branch 'development'"" This reverts commit ebf40992bcdd75d6024d4bce25410511cbf88287. --- .../Events/Plugins/DeviceProviderEventArgs.cs | 27 + .../Models/ProfileConfiguration/Hotkey.cs | 13 +- .../Models/Surface/ArtemisDevice.cs | 77 ++- src/Artemis.Core/Models/Surface/ArtemisLed.cs | 4 +- .../Models/Surface/OriginalLed.cs | 19 + src/Artemis.Core/Services/Core/IRenderer.cs | 23 + .../Services/Core/SurfaceManager.cs | 222 +++++++ src/Artemis.Core/Services/CoreService.cs | 177 +---- src/Artemis.Core/Services/DeviceService.cs | 318 ++++++++- .../Events/ArtemisKeyboardKeyEventArgs.cs | 9 + .../Services/Input/InputService.cs | 24 +- .../Services/Interfaces/ICoreService.cs | 25 - .../Services/Interfaces/IDeviceService.cs | 98 ++- .../Services/Interfaces/IRenderService.cs | 58 ++ .../Services/Interfaces/IRgbService.cs | 168 ----- src/Artemis.Core/Services/RenderService.cs | 251 +++++++ src/Artemis.Core/Services/RgbService.cs | 613 ------------------ .../Storage/Interfaces/IProfileService.cs | 5 + .../Services/Storage/ProfileService.cs | 42 +- src/Artemis.Core/Utilities/RenderScale.cs | 35 + src/Artemis.Core/Utilities/Utilities.cs | 24 - .../Controls/DeviceVisualizer.cs | 38 +- .../Services/MainWindow/IMainWindowService.cs | 7 +- .../Services/MainWindow/MainWindowService.cs | 5 + .../Services/ProfileEditor/IToolViewModel.cs | 10 + .../ProfileEditor/ProfileEditorService.cs | 10 +- src/Artemis.UI.Shared/Utilities.cs | 12 +- .../Performance/PerformanceDebugViewModel.cs | 14 +- .../Tabs/Render/RenderDebugViewModel.cs | 14 +- .../Device/DeviceDetectInputViewModel.cs | 10 +- .../Device/DevicePropertiesViewModel.cs | 21 +- .../Screens/Device/DeviceSettingsViewModel.cs | 13 +- .../Device/Tabs/DeviceGeneralTabViewModel.cs | 29 +- .../Device/Tabs/DeviceLayoutTabViewModel.cs | 31 +- .../Device/Tabs/InputMappingsTabViewModel.cs | 8 +- .../Panels/MenuBar/MenuBarViewModel.cs | 2 +- .../Panels/Playback/PlaybackViewModel.cs | 2 +- .../Dialogs/LayerHintsDialogViewModel.cs | 8 +- .../ProfileTree/FolderTreeItemViewModel.cs | 10 +- .../ProfileTree/LayerTreeItemViewModel.cs | 12 +- .../ProfileTree/ProfileTreeViewModel.cs | 6 +- .../Panels/ProfileTree/TreeItemViewModel.cs | 8 +- .../Keyframes/TimelineEasingView.axaml | 14 +- .../Keyframes/TimelineEasingViewModel.cs | 6 +- .../Keyframes/TimelineKeyframeView.axaml | 14 +- .../Keyframes/TimelineKeyframeViewModel.cs | 9 +- .../Tools/SelectionAddToolViewModel.cs | 16 +- .../Tools/SelectionRemoveToolViewModel.cs | 7 +- .../Tools/TransformToolViewModel.cs | 7 +- .../VisualEditor/VisualEditorViewModel.cs | 4 +- .../ProfileEditor/ProfileEditorView.axaml | 12 +- .../ProfileEditor/ProfileEditorView.axaml.cs | 14 - .../ProfileEditor/ProfileEditorViewModel.cs | 34 +- .../Settings/Tabs/DevicesTabViewModel.cs | 14 +- .../StartupWizard/StartupWizardViewModel.cs | 8 +- .../SurfaceEditor/ListDeviceViewModel.cs | 8 +- .../SurfaceEditor/SurfaceDeviceViewModel.cs | 10 +- .../SurfaceEditor/SurfaceEditorView.axaml | 2 +- .../SurfaceEditor/SurfaceEditorViewModel.cs | 42 +- .../NodeScriptWindowViewModel.cs | 2 +- .../Profile/ProfilePreviewViewModel.cs | 4 +- .../Services/DeviceLayoutService.cs | 14 +- 62 files changed, 1489 insertions(+), 1254 deletions(-) create mode 100644 src/Artemis.Core/Events/Plugins/DeviceProviderEventArgs.cs create mode 100644 src/Artemis.Core/Models/Surface/OriginalLed.cs create mode 100644 src/Artemis.Core/Services/Core/IRenderer.cs create mode 100644 src/Artemis.Core/Services/Core/SurfaceManager.cs create mode 100644 src/Artemis.Core/Services/Interfaces/IRenderService.cs delete mode 100644 src/Artemis.Core/Services/Interfaces/IRgbService.cs create mode 100644 src/Artemis.Core/Services/RenderService.cs delete mode 100644 src/Artemis.Core/Services/RgbService.cs create mode 100644 src/Artemis.Core/Utilities/RenderScale.cs diff --git a/src/Artemis.Core/Events/Plugins/DeviceProviderEventArgs.cs b/src/Artemis.Core/Events/Plugins/DeviceProviderEventArgs.cs new file mode 100644 index 000000000..878946530 --- /dev/null +++ b/src/Artemis.Core/Events/Plugins/DeviceProviderEventArgs.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using Artemis.Core.DeviceProviders; + +namespace Artemis.Core; + +/// +/// Provides data about device provider related events +/// +public class DeviceProviderEventArgs : EventArgs +{ + internal DeviceProviderEventArgs(DeviceProvider deviceProvider, List devices) + { + DeviceProvider = deviceProvider; + Devices = devices; + } + + /// + /// Gets the device provider the event is related to. + /// + public DeviceProvider DeviceProvider { get; } + + /// + /// Gets a list of the affected devices. + /// + public List Devices { get; set; } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/ProfileConfiguration/Hotkey.cs b/src/Artemis.Core/Models/ProfileConfiguration/Hotkey.cs index ac2c9ed24..b7dc91525 100644 --- a/src/Artemis.Core/Models/ProfileConfiguration/Hotkey.cs +++ b/src/Artemis.Core/Models/ProfileConfiguration/Hotkey.cs @@ -1,4 +1,5 @@ -using Artemis.Core.Services; +using System; +using Artemis.Core.Services; using Artemis.Storage.Entities.Profile; namespace Artemis.Core; @@ -16,6 +17,14 @@ public class Hotkey : CorePropertyChanged, IStorageModel Entity = new ProfileConfigurationHotkeyEntity(); } + /// + public Hotkey(KeyboardKey? key, KeyboardModifierKey? modifiers) + { + Key = key; + Modifiers = modifiers; + Entity = new ProfileConfigurationHotkeyEntity(); + } + /// /// Creates a new instance of based on the provided entity /// @@ -46,7 +55,7 @@ public class Hotkey : CorePropertyChanged, IStorageModel /// if the event args match the hotkey; otherwise public bool MatchesEventArgs(ArtemisKeyboardKeyEventArgs eventArgs) { - return eventArgs.Key == Key && eventArgs.Modifiers == Modifiers; + return eventArgs.Key == Key && (eventArgs.Modifiers == Modifiers || (eventArgs.Modifiers == KeyboardModifierKey.None && Modifiers == null)); } #region Implementation of IStorageModel diff --git a/src/Artemis.Core/Models/Surface/ArtemisDevice.cs b/src/Artemis.Core/Models/Surface/ArtemisDevice.cs index bf1e6211b..ef29c01f7 100644 --- a/src/Artemis.Core/Models/Surface/ArtemisDevice.cs +++ b/src/Artemis.Core/Models/Surface/ArtemisDevice.cs @@ -15,11 +15,17 @@ namespace Artemis.Core; /// public class ArtemisDevice : CorePropertyChanged { + private readonly List _originalLeds; + private readonly Size _originalSize; private SKPath? _path; private SKRect _rectangle; internal ArtemisDevice(IRGBDevice rgbDevice, DeviceProvider deviceProvider) { + _originalLeds = new List(rgbDevice.Select(l => new OriginalLed(l))); + Rectangle ledRectangle = new(rgbDevice.Select(x => x.Boundary)); + _originalSize = ledRectangle.Size + new Size(ledRectangle.Location.X, ledRectangle.Location.Y); + Identifier = rgbDevice.GetDeviceIdentifier(); DeviceEntity = new DeviceEntity(); RgbDevice = rgbDevice; @@ -48,6 +54,10 @@ public class ArtemisDevice : CorePropertyChanged internal ArtemisDevice(IRGBDevice rgbDevice, DeviceProvider deviceProvider, DeviceEntity deviceEntity) { + _originalLeds = new List(rgbDevice.Select(l => new OriginalLed(l))); + Rectangle ledRectangle = new(rgbDevice.Select(x => x.Boundary)); + _originalSize = ledRectangle.Size + new Size(ledRectangle.Location.X, ledRectangle.Location.Y); + Identifier = rgbDevice.GetDeviceIdentifier(); DeviceEntity = deviceEntity; RgbDevice = rgbDevice; @@ -350,6 +360,40 @@ public class ArtemisDevice : CorePropertyChanged return artemisLed; } + /// + /// Returns the most preferred device layout for this device. + /// + /// The most preferred device layout for this device. + public ArtemisLayout? GetBestDeviceLayout() + { + ArtemisLayout? layout; + + // Configured layout path takes precedence over all other options + if (CustomLayoutPath != null) + { + layout = new ArtemisLayout(CustomLayoutPath, LayoutSource.Configured); + if (layout.IsValid) + return layout; + } + + // Look for a layout provided by the user + layout = DeviceProvider.LoadUserLayout(this); + if (layout.IsValid) + return layout; + + if (DisableDefaultLayout) + return null; + + // Look for a layout provided by the plugin + layout = DeviceProvider.LoadLayout(this); + if (layout.IsValid) + return layout; + + // Finally fall back to a default layout + layout = ArtemisLayout.GetDefaultLayout(this); + return layout; + } + /// /// Occurs when the underlying RGB.NET device was updated /// @@ -415,8 +459,18 @@ public class ArtemisDevice : CorePropertyChanged /// A boolean indicating whether to remove excess LEDs present in the device but missing /// in the layout /// - internal void ApplyLayout(ArtemisLayout layout, bool createMissingLeds, bool removeExcessiveLeds) + internal void ApplyLayout(ArtemisLayout? layout, bool createMissingLeds, bool removeExcessiveLeds) { + if (layout == null) + { + ClearLayout(); + UpdateLeds(); + + CalculateRenderProperties(); + OnDeviceUpdated(); + return; + } + if (createMissingLeds && !DeviceProvider.CreateMissingLedsSupported) throw new ArtemisCoreException($"Cannot apply layout with {nameof(createMissingLeds)} " + "set to true because the device provider does not support it"); @@ -424,18 +478,33 @@ public class ArtemisDevice : CorePropertyChanged throw new ArtemisCoreException($"Cannot apply layout with {nameof(removeExcessiveLeds)} " + "set to true because the device provider does not support it"); + ClearLayout(); if (layout.IsValid) layout.ApplyTo(RgbDevice, createMissingLeds, removeExcessiveLeds); - - UpdateLeds(); - + Layout = layout; Layout.ApplyDevice(this); + CalculateRenderProperties(); OnDeviceUpdated(); } + private void ClearLayout() + { + if (Layout == null) + return; + + RgbDevice.DeviceInfo.LayoutMetadata = null; + RgbDevice.Size = _originalSize; + Layout = null; + + while (RgbDevice.Any()) + RgbDevice.RemoveLed(RgbDevice.First().Id); + foreach (OriginalLed originalLed in _originalLeds) + RgbDevice.AddLed(originalLed.Id, originalLed.Location, originalLed.Size, originalLed.CustomData); + } + internal void ApplyToEntity() { // Other properties are computed diff --git a/src/Artemis.Core/Models/Surface/ArtemisLed.cs b/src/Artemis.Core/Models/Surface/ArtemisLed.cs index 48d1be479..f3d0500e1 100644 --- a/src/Artemis.Core/Models/Surface/ArtemisLed.cs +++ b/src/Artemis.Core/Models/Surface/ArtemisLed.cs @@ -59,13 +59,13 @@ public class ArtemisLed : CorePropertyChanged internal void CalculateRectangles() { - Rectangle = Utilities.CreateScaleCompatibleRect( + Rectangle = RenderScale.CreateScaleCompatibleRect( RgbLed.Boundary.Location.X, RgbLed.Boundary.Location.Y, RgbLed.Boundary.Size.Width, RgbLed.Boundary.Size.Height ); - AbsoluteRectangle = Utilities.CreateScaleCompatibleRect( + AbsoluteRectangle = RenderScale.CreateScaleCompatibleRect( RgbLed.AbsoluteBoundary.Location.X, RgbLed.AbsoluteBoundary.Location.Y, RgbLed.AbsoluteBoundary.Size.Width, diff --git a/src/Artemis.Core/Models/Surface/OriginalLed.cs b/src/Artemis.Core/Models/Surface/OriginalLed.cs new file mode 100644 index 000000000..87e3f4835 --- /dev/null +++ b/src/Artemis.Core/Models/Surface/OriginalLed.cs @@ -0,0 +1,19 @@ +using RGB.NET.Core; + +namespace Artemis.Core; + +internal class OriginalLed +{ + public OriginalLed(Led source) + { + Id = source.Id; + Location = source.Location; + Size = source.Size; + CustomData = source.CustomData; + } + + public LedId Id { get; set; } + public Point Location { get; set; } + public Size Size { get; set; } + public object? CustomData { get; set; } +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/Core/IRenderer.cs b/src/Artemis.Core/Services/Core/IRenderer.cs new file mode 100644 index 000000000..8aac001db --- /dev/null +++ b/src/Artemis.Core/Services/Core/IRenderer.cs @@ -0,0 +1,23 @@ +using SkiaSharp; + +namespace Artemis.Core.Services.Core; + +/// +/// Represents a renderer that renders to a canvas. +/// +public interface IRenderer +{ + /// + /// Renders to the provided canvas, the delta is the time in seconds since the last time was + /// called. + /// + /// The canvas to render to. + /// The time in seconds since the last time was called. + void Render(SKCanvas canvas, double delta); + + /// + /// Called after the rendering has taken place. + /// + /// The texture that the render resulted in. + void PostRender(SKTexture texture); +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/Core/SurfaceManager.cs b/src/Artemis.Core/Services/Core/SurfaceManager.cs new file mode 100644 index 000000000..fb3845e3c --- /dev/null +++ b/src/Artemis.Core/Services/Core/SurfaceManager.cs @@ -0,0 +1,222 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Artemis.Core.SkiaSharp; +using RGB.NET.Core; +using SkiaSharp; + +namespace Artemis.Core.Services.Core; + +/// +/// An engine drivers an update loop for a set of devices using a graphics context +/// +internal sealed class SurfaceManager : IDisposable +{ + private readonly IRenderer _renderer; + private readonly TimerUpdateTrigger _updateTrigger; + private readonly object _renderLock = new(); + private readonly List _devices = new(); + private readonly SKTextureBrush _textureBrush = new(null) {CalculationMode = RenderMode.Absolute}; + + private ListLedGroup? _surfaceLedGroup; + private SKTexture? _texture; + + public SurfaceManager(IRenderer renderer, IManagedGraphicsContext? graphicsContext, int targetFrameRate, float renderScale) + { + _renderer = renderer; + _updateTrigger = new TimerUpdateTrigger {UpdateFrequency = 1.0 / targetFrameRate}; + + GraphicsContext = graphicsContext; + TargetFrameRate = targetFrameRate; + RenderScale = renderScale; + Surface = new RGBSurface(); + Surface.Updating += SurfaceOnUpdating; + Surface.RegisterUpdateTrigger(_updateTrigger); + + SetPaused(true); + } + + private void SurfaceOnUpdating(UpdatingEventArgs args) + { + lock (_renderLock) + { + SKTexture? texture = _texture; + if (texture == null || texture.IsInvalid) + texture = CreateTexture(); + + // Prepare a canvas + SKCanvas canvas = texture.Surface.Canvas; + canvas.Save(); + + // Apply scaling if necessary + if (Math.Abs(texture.RenderScale - 1) > 0.001) + canvas.Scale(texture.RenderScale); + + // Fresh start! + canvas.Clear(new SKColor(0, 0, 0)); + + try + { + _renderer.Render(canvas, args.DeltaTime); + } + finally + { + canvas.RestoreToCount(-1); + canvas.Flush(); + texture.CopyPixelData(); + } + + try + { + _renderer.PostRender(texture); + } + catch + { + // ignored + } + } + } + + public IManagedGraphicsContext? GraphicsContext { get; private set; } + public int TargetFrameRate { get; private set; } + public float RenderScale { get; private set; } + public RGBSurface Surface { get; } + + public bool IsPaused { get; private set; } + + public void AddDevices(IEnumerable devices) + { + lock (_renderLock) + { + foreach (ArtemisDevice artemisDevice in devices) + { + if (_devices.Contains(artemisDevice)) + continue; + _devices.Add(artemisDevice); + Surface.Attach(artemisDevice.RgbDevice); + artemisDevice.DeviceUpdated += ArtemisDeviceOnDeviceUpdated; + } + + Update(); + } + } + + public void RemoveDevices(IEnumerable devices) + { + lock (_renderLock) + { + foreach (ArtemisDevice artemisDevice in devices) + { + artemisDevice.DeviceUpdated -= ArtemisDeviceOnDeviceUpdated; + Surface.Detach(artemisDevice.RgbDevice); + _devices.Remove(artemisDevice); + } + + Update(); + } + } + + public bool SetPaused(bool paused) + { + if (IsPaused == paused) + return false; + + if (paused) + _updateTrigger.Stop(); + else + _updateTrigger.Start(); + + IsPaused = paused; + return true; + } + + private void Update() + { + lock (_renderLock) + { + UpdateLedGroup(); + CreateTexture(); + } + } + + private void UpdateLedGroup() + { + List leds = _devices.SelectMany(d => d.Leds).Select(l => l.RgbLed).ToList(); + + if (_surfaceLedGroup == null) + { + _surfaceLedGroup = new ListLedGroup(Surface, leds) {Brush = _textureBrush}; + LedsChanged?.Invoke(this, EventArgs.Empty); + return; + } + + // Clean up the old background + _surfaceLedGroup.Detach(); + + // Apply the application wide brush and decorator + _surfaceLedGroup = new ListLedGroup(Surface, leds) {Brush = _textureBrush}; + LedsChanged?.Invoke(this, EventArgs.Empty); + } + + private SKTexture CreateTexture() + { + float evenWidth = Surface.Boundary.Size.Width; + if (evenWidth % 2 != 0) + evenWidth++; + float evenHeight = Surface.Boundary.Size.Height; + if (evenHeight % 2 != 0) + evenHeight++; + + int width = Math.Max(1, MathF.Min(evenWidth * RenderScale, 4096).RoundToInt()); + int height = Math.Max(1, MathF.Min(evenHeight * RenderScale, 4096).RoundToInt()); + + _texture?.Dispose(); + _texture = new SKTexture(GraphicsContext, width, height, RenderScale, _devices); + _textureBrush.Texture = _texture; + + return _texture; + } + + private void ArtemisDeviceOnDeviceUpdated(object? sender, EventArgs e) + { + Update(); + } + + public event EventHandler? LedsChanged; + + /// + public void Dispose() + { + SetPaused(true); + + Surface.UnregisterUpdateTrigger(_updateTrigger); + _updateTrigger.Dispose(); + _texture?.Dispose(); + Surface.Dispose(); + } + + public void UpdateTargetFrameRate(int targetFrameRate) + { + TargetFrameRate = targetFrameRate; + _updateTrigger.UpdateFrequency = 1.0 / TargetFrameRate; + } + + public void UpdateRenderScale(float renderScale) + { + lock (_renderLock) + { + RenderScale = renderScale; + _texture?.Invalidate(); + } + } + + public void UpdateGraphicsContext(IManagedGraphicsContext? graphicsContext) + { + lock (_renderLock) + { + GraphicsContext = graphicsContext; + _texture?.Dispose(); + _texture = null; + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/CoreService.cs b/src/Artemis.Core/Services/CoreService.cs index 137f339b8..89a951149 100644 --- a/src/Artemis.Core/Services/CoreService.cs +++ b/src/Artemis.Core/Services/CoreService.cs @@ -20,19 +20,11 @@ namespace Artemis.Core.Services; /// internal class CoreService : ICoreService { - private readonly Stopwatch _frameStopWatch; private readonly ILogger _logger; private readonly PluginSetting _loggingLevel; - private readonly IModuleService _moduleService; private readonly IPluginManagementService _pluginManagementService; - private readonly IProfileService _profileService; - private readonly IRgbService _rgbService; - private readonly IScriptingService _scriptingService; - private readonly List _updateExceptions = new(); + private readonly IRenderService _renderService; - private int _frames; - private DateTime _lastExceptionLog; - private DateTime _lastFrameRateSample; // ReSharper disable UnusedParameter.Local public CoreService(IContainer container, @@ -40,36 +32,49 @@ internal class CoreService : ICoreService StorageMigrationService _1, // injected to ensure migration runs early ISettingsService settingsService, IPluginManagementService pluginManagementService, - IRgbService rgbService, IProfileService profileService, IModuleService moduleService, - IScriptingService scriptingService) + IScriptingService scriptingService, + IRenderService renderService) { Constants.CorePlugin.Container = container; _logger = logger; _pluginManagementService = pluginManagementService; - _rgbService = rgbService; - _profileService = profileService; - _moduleService = moduleService; - _scriptingService = scriptingService; + _renderService = renderService; _loggingLevel = settingsService.GetSetting("Core.LoggingLevel", LogEventLevel.Debug); - _frameStopWatch = new Stopwatch(); - - _rgbService.Surface.Updating += SurfaceOnUpdating; _loggingLevel.SettingChanged += (sender, args) => ApplyLoggingLevel(); } + + public bool IsElevated { get; set; } - // ReSharper restore UnusedParameter.Local + public bool IsInitialized { get; set; } - protected virtual void OnFrameRendering(FrameRenderingEventArgs e) + public void Initialize() { - FrameRendering?.Invoke(this, e); - } + if (IsInitialized) + throw new ArtemisCoreException("Cannot initialize the core as it is already initialized."); - protected virtual void OnFrameRendered(FrameRenderedEventArgs e) - { - FrameRendered?.Invoke(this, e); + _logger.Information("Initializing Artemis Core version {CurrentVersion}", Constants.CurrentVersion); + _logger.Information("Startup arguments: {StartupArguments}", Constants.StartupArguments); + _logger.Information("Elevated permissions: {IsElevated}", IsElevated); + _logger.Information("Stopwatch high resolution: {IsHighResolution}", Stopwatch.IsHighResolution); + + ApplyLoggingLevel(); + + ProcessMonitor.Start(); + + // Don't remove even if it looks useless + // Just this line should prevent a certain someone from removing HidSharp as an unused dependency as well + Version? hidSharpVersion = Assembly.GetAssembly(typeof(HidDevice))!.GetName().Version; + _logger.Debug("Forcing plugins to use HidSharp {HidSharpVersion}", hidSharpVersion); + + // Initialize the services + _pluginManagementService.CopyBuiltInPlugins(); + _pluginManagementService.LoadPlugins(IsElevated); + _renderService.Initialize(); + + OnInitialized(); } private void ApplyLoggingLevel() @@ -98,131 +103,11 @@ internal class CoreService : ICoreService } } - private void SurfaceOnUpdating(UpdatingEventArgs args) - { - if (_rgbService.IsRenderPaused) - return; - - if (_rgbService.FlushLeds) - { - _rgbService.FlushLeds = false; - _rgbService.Surface.Update(true); - return; - } - - try - { - _frameStopWatch.Restart(); - - foreach (GlobalScript script in _scriptingService.GlobalScripts) - script.OnCoreUpdating(args.DeltaTime); - - _moduleService.UpdateActiveModules(args.DeltaTime); - SKTexture texture = _rgbService.OpenRender(); - SKCanvas canvas = texture.Surface.Canvas; - canvas.Save(); - if (Math.Abs(texture.RenderScale - 1) > 0.001) - canvas.Scale(texture.RenderScale); - canvas.Clear(new SKColor(0, 0, 0)); - - if (!ProfileRenderingDisabled) - { - _profileService.UpdateProfiles(args.DeltaTime); - _profileService.RenderProfiles(canvas); - } - - OnFrameRendering(new FrameRenderingEventArgs(canvas, args.DeltaTime, _rgbService.Surface)); - canvas.RestoreToCount(-1); - canvas.Flush(); - - OnFrameRendered(new FrameRenderedEventArgs(texture, _rgbService.Surface)); - - foreach (GlobalScript script in _scriptingService.GlobalScripts) - script.OnCoreUpdated(args.DeltaTime); - } - catch (Exception e) - { - _updateExceptions.Add(e); - } - finally - { - _rgbService.CloseRender(); - _frameStopWatch.Stop(); - _frames++; - - if ((DateTime.Now - _lastFrameRateSample).TotalSeconds >= 1) - { - FrameRate = _frames; - _frames = 0; - _lastFrameRateSample = DateTime.Now; - } - - FrameTime = _frameStopWatch.Elapsed; - - LogUpdateExceptions(); - } - } - - private void LogUpdateExceptions() - { - // Only log update exceptions every 10 seconds to avoid spamming the logs - if (DateTime.Now - _lastExceptionLog < TimeSpan.FromSeconds(10)) - return; - _lastExceptionLog = DateTime.Now; - - if (!_updateExceptions.Any()) - return; - - // Group by stack trace, that should gather up duplicate exceptions - foreach (IGrouping exceptions in _updateExceptions.GroupBy(e => e.StackTrace)) - _logger.Warning(exceptions.First(), "Exception was thrown {count} times during update in the last 10 seconds", exceptions.Count()); - - // When logging is finished start with a fresh slate - _updateExceptions.Clear(); - } + public event EventHandler? Initialized; private void OnInitialized() { IsInitialized = true; Initialized?.Invoke(this, EventArgs.Empty); } - - public int FrameRate { get; private set; } - public TimeSpan FrameTime { get; private set; } - public bool ProfileRenderingDisabled { get; set; } - public bool IsElevated { get; set; } - - public bool IsInitialized { get; set; } - - public void Initialize() - { - if (IsInitialized) - throw new ArtemisCoreException("Cannot initialize the core as it is already initialized."); - - _logger.Information("Initializing Artemis Core version {CurrentVersion}", Constants.CurrentVersion); - _logger.Information("Startup arguments: {StartupArguments}", Constants.StartupArguments); - _logger.Information("Elevated permissions: {IsElevated}", IsElevated); - _logger.Information("Stopwatch high resolution: {IsHighResolution}", Stopwatch.IsHighResolution); - - ApplyLoggingLevel(); - - ProcessMonitor.Start(); - - // Don't remove even if it looks useless - // Just this line should prevent a certain someone from removing HidSharp as an unused dependency as well - Version? hidSharpVersion = Assembly.GetAssembly(typeof(HidDevice))!.GetName().Version; - _logger.Debug("Forcing plugins to use HidSharp {HidSharpVersion}", hidSharpVersion); - - // Initialize the services - _pluginManagementService.CopyBuiltInPlugins(); - _pluginManagementService.LoadPlugins(IsElevated); - - _rgbService.ApplyPreferredGraphicsContext(Constants.StartupArguments.Contains("--force-software-render")); - _rgbService.SetRenderPaused(false); - OnInitialized(); - } - - public event EventHandler? Initialized; - public event EventHandler? FrameRendering; - public event EventHandler? FrameRendered; } \ No newline at end of file diff --git a/src/Artemis.Core/Services/DeviceService.cs b/src/Artemis.Core/Services/DeviceService.cs index f0b170289..2cddc24f4 100644 --- a/src/Artemis.Core/Services/DeviceService.cs +++ b/src/Artemis.Core/Services/DeviceService.cs @@ -1,22 +1,252 @@ -using System.Linq; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; using System.Threading.Tasks; +using Artemis.Core.DeviceProviders; +using Artemis.Core.Services.Models; +using Artemis.Storage.Entities.Surface; +using Artemis.Storage.Repositories.Interfaces; using RGB.NET.Core; +using Serilog; namespace Artemis.Core.Services; internal class DeviceService : IDeviceService { - private readonly IRgbService _rgbService; + private readonly ILogger _logger; + private readonly IPluginManagementService _pluginManagementService; + private readonly IDeviceRepository _deviceRepository; + private readonly Lazy _renderService; + private readonly List _enabledDevices = new(); + private readonly List _devices = new(); - public DeviceService(IRgbService rgbService) + public DeviceService(ILogger logger, IPluginManagementService pluginManagementService, IDeviceRepository deviceRepository, Lazy renderService) { - _rgbService = rgbService; + _logger = logger; + _pluginManagementService = pluginManagementService; + _deviceRepository = deviceRepository; + _renderService = renderService; + + EnabledDevices = new ReadOnlyCollection(_enabledDevices); + Devices = new ReadOnlyCollection(_devices); + + RenderScale.RenderScaleMultiplierChanged += RenderScaleOnRenderScaleMultiplierChanged; } + public IReadOnlyCollection EnabledDevices { get; } + public IReadOnlyCollection Devices { get; } + + /// + public void IdentifyDevice(ArtemisDevice device) + { + BlinkDevice(device, 0); + } + + /// + public void AddDeviceProvider(DeviceProvider deviceProvider) + { + _logger.Verbose("[AddDeviceProvider] Adding {DeviceProvider}", deviceProvider.GetType().Name); + IRGBDeviceProvider rgbDeviceProvider = deviceProvider.RgbDeviceProvider; + + try + { + // Can't see why this would happen, RgbService used to do this though + List toRemove = _devices.Where(a => rgbDeviceProvider.Devices.Any(d => a.RgbDevice == d)).ToList(); + _logger.Verbose("[AddDeviceProvider] Removing {Count} old device(s)", toRemove.Count); + foreach (ArtemisDevice device in toRemove) + { + _devices.Remove(device); + OnDeviceRemoved(new DeviceEventArgs(device)); + } + + List providerExceptions = new(); + + void DeviceProviderOnException(object? sender, ExceptionEventArgs e) + { + if (e.IsCritical) + providerExceptions.Add(e.Exception); + else + _logger.Warning(e.Exception, "Device provider {deviceProvider} threw non-critical exception", deviceProvider.GetType().Name); + } + + _logger.Verbose("[AddDeviceProvider] Initializing device provider"); + rgbDeviceProvider.Exception += DeviceProviderOnException; + rgbDeviceProvider.Initialize(); + _logger.Verbose("[AddDeviceProvider] Attaching devices of device provider"); + rgbDeviceProvider.Exception -= DeviceProviderOnException; + if (providerExceptions.Count == 1) + throw new ArtemisPluginException("RGB.NET threw exception: " + providerExceptions.First().Message, providerExceptions.First()); + if (providerExceptions.Count > 1) + throw new ArtemisPluginException("RGB.NET threw multiple exceptions", new AggregateException(providerExceptions)); + + if (!rgbDeviceProvider.Devices.Any()) + { + _logger.Warning("Device provider {deviceProvider} has no devices", deviceProvider.GetType().Name); + return; + } + + List addedDevices = new(); + foreach (IRGBDevice rgbDevice in rgbDeviceProvider.Devices) + { + ArtemisDevice artemisDevice = GetArtemisDevice(rgbDevice); + addedDevices.Add(artemisDevice); + _devices.Add(artemisDevice); + if (artemisDevice.IsEnabled) + _enabledDevices.Add(artemisDevice); + + _logger.Debug("Device provider {deviceProvider} added {deviceName}", deviceProvider.GetType().Name, rgbDevice.DeviceInfo.DeviceName); + } + + _devices.Sort((a, b) => a.ZIndex - b.ZIndex); + _enabledDevices.Sort((a, b) => a.ZIndex - b.ZIndex); + + OnDeviceProviderAdded(new DeviceProviderEventArgs(deviceProvider, addedDevices)); + foreach (ArtemisDevice artemisDevice in addedDevices) + OnDeviceAdded(new DeviceEventArgs(artemisDevice)); + + UpdateLeds(); + } + catch (Exception e) + { + _logger.Error(e, "Exception during device loading for device provider {deviceProvider}", deviceProvider.GetType().Name); + throw; + } + } + + /// + public void RemoveDeviceProvider(DeviceProvider deviceProvider) + { + _logger.Verbose("[RemoveDeviceProvider] Pausing rendering to remove {DeviceProvider}", deviceProvider.GetType().Name); + IRGBDeviceProvider rgbDeviceProvider = deviceProvider.RgbDeviceProvider; + List toRemove = _devices.Where(a => rgbDeviceProvider.Devices.Any(d => a.RgbDevice == d)).ToList(); + + try + { + _logger.Verbose("[RemoveDeviceProvider] Removing {Count} old device(s)", toRemove.Count); + foreach (ArtemisDevice device in toRemove) + { + _devices.Remove(device); + _enabledDevices.Remove(device); + } + + _devices.Sort((a, b) => a.ZIndex - b.ZIndex); + + OnDeviceProviderRemoved(new DeviceProviderEventArgs(deviceProvider, toRemove)); + foreach (ArtemisDevice artemisDevice in toRemove) + OnDeviceRemoved(new DeviceEventArgs(artemisDevice)); + + UpdateLeds(); + } + catch (Exception e) + { + _logger.Error(e, "Exception during device removal for device provider {deviceProvider}", deviceProvider.GetType().Name); + throw; + } + } + + /// + public void AutoArrangeDevices() + { + SurfaceArrangement surfaceArrangement = SurfaceArrangement.GetDefaultArrangement(); + surfaceArrangement.Arrange(_devices); + foreach (ArtemisDevice artemisDevice in _devices) + artemisDevice.ApplyDefaultCategories(); + + SaveDevices(); + } + + /// + public void ApplyDeviceLayout(ArtemisDevice device, ArtemisLayout? layout) + { + if (layout == null || layout.Source == LayoutSource.Default) + device.ApplyLayout(layout, false, false); + else + device.ApplyLayout(layout, device.DeviceProvider.CreateMissingLedsSupported, device.DeviceProvider.RemoveExcessiveLedsSupported); + + UpdateLeds(); + } + + /// + public void EnableDevice(ArtemisDevice device) + { + if (device.IsEnabled) + return; + + _enabledDevices.Add(device); + device.IsEnabled = true; + device.ApplyToEntity(); + _deviceRepository.Save(device.DeviceEntity); + + OnDeviceEnabled(new DeviceEventArgs(device)); + UpdateLeds(); + } + + /// + public void DisableDevice(ArtemisDevice device) + { + if (!device.IsEnabled) + return; + + _enabledDevices.Remove(device); + device.IsEnabled = false; + device.ApplyToEntity(); + _deviceRepository.Save(device.DeviceEntity); + + OnDeviceDisabled(new DeviceEventArgs(device)); + UpdateLeds(); + } + + /// + public void SaveDevice(ArtemisDevice artemisDevice) + { + artemisDevice.ApplyToEntity(); + artemisDevice.ApplyToRgbDevice(); + + _deviceRepository.Save(artemisDevice.DeviceEntity); + UpdateLeds(); + } + + /// + public void SaveDevices() + { + foreach (ArtemisDevice artemisDevice in _devices) + { + artemisDevice.ApplyToEntity(); + artemisDevice.ApplyToRgbDevice(); + } + + _deviceRepository.Save(_devices.Select(d => d.DeviceEntity)); + UpdateLeds(); + } + + private ArtemisDevice GetArtemisDevice(IRGBDevice rgbDevice) + { + string deviceIdentifier = rgbDevice.GetDeviceIdentifier(); + DeviceEntity? deviceEntity = _deviceRepository.Get(deviceIdentifier); + DeviceProvider deviceProvider = _pluginManagementService.GetDeviceProviderByDevice(rgbDevice); + + ArtemisDevice device; + if (deviceEntity != null) + device = new ArtemisDevice(rgbDevice, deviceProvider, deviceEntity); + // Fall back on creating a new device + else + { + _logger.Information("No device config found for {DeviceInfo}, device hash: {DeviceHashCode}. Adding a new entry", rgbDevice.DeviceInfo, deviceIdentifier); + device = new ArtemisDevice(rgbDevice, deviceProvider); + } + + device.ApplyToRgbDevice(); + ApplyDeviceLayout(device, device.GetBestDeviceLayout()); + return device; + } + private void BlinkDevice(ArtemisDevice device, int blinkCount) { + RGBSurface surface = _renderService.Value.Surface; + // Create a LED group way at the top - ListLedGroup ledGroup = new(_rgbService.Surface, device.Leds.Select(l => l.RgbLed)) + ListLedGroup ledGroup = new(surface, device.Leds.Select(l => l.RgbLed)) { Brush = new SolidColorBrush(new Color(255, 255, 255)), ZIndex = 999 @@ -36,9 +266,81 @@ internal class DeviceService : IDeviceService } }); } - - public void IdentifyDevice(ArtemisDevice device) + + private void CalculateRenderProperties() { - BlinkDevice(device, 0); + foreach (ArtemisDevice artemisDevice in Devices) + artemisDevice.CalculateRenderProperties(); + UpdateLeds(); } + + private void UpdateLeds() + { + OnLedsChanged(); + } + + private void RenderScaleOnRenderScaleMultiplierChanged(object? sender, EventArgs e) + { + CalculateRenderProperties(); + } + + #region Events + + /// + public event EventHandler? DeviceAdded; + + /// + public event EventHandler? DeviceRemoved; + + /// + public event EventHandler? DeviceEnabled; + + /// + public event EventHandler? DeviceDisabled; + + /// + public event EventHandler? DeviceProviderAdded; + + /// + public event EventHandler? DeviceProviderRemoved; + + /// + public event EventHandler? LedsChanged; + + protected virtual void OnDeviceAdded(DeviceEventArgs e) + { + DeviceAdded?.Invoke(this, e); + } + + protected virtual void OnDeviceRemoved(DeviceEventArgs e) + { + DeviceRemoved?.Invoke(this, e); + } + + protected virtual void OnDeviceEnabled(DeviceEventArgs e) + { + DeviceEnabled?.Invoke(this, e); + } + + protected virtual void OnDeviceDisabled(DeviceEventArgs e) + { + DeviceDisabled?.Invoke(this, e); + } + + protected virtual void OnDeviceProviderAdded(DeviceProviderEventArgs e) + { + DeviceProviderAdded?.Invoke(this, e); + } + + protected virtual void OnDeviceProviderRemoved(DeviceProviderEventArgs e) + { + DeviceProviderRemoved?.Invoke(this, e); + } + + protected virtual void OnLedsChanged() + { + LedsChanged?.Invoke(this, EventArgs.Empty); + } + + #endregion } \ No newline at end of file diff --git a/src/Artemis.Core/Services/Input/Events/ArtemisKeyboardKeyEventArgs.cs b/src/Artemis.Core/Services/Input/Events/ArtemisKeyboardKeyEventArgs.cs index e6193c515..4738bab71 100644 --- a/src/Artemis.Core/Services/Input/Events/ArtemisKeyboardKeyEventArgs.cs +++ b/src/Artemis.Core/Services/Input/Events/ArtemisKeyboardKeyEventArgs.cs @@ -34,4 +34,13 @@ public class ArtemisKeyboardKeyEventArgs : EventArgs /// Gets the modifiers that are pressed /// public KeyboardModifierKey Modifiers { get; } + + /// + /// Creates a hotkey matching the event. + /// + /// The resulting hotkey. + public Hotkey ToHotkey() + { + return new Hotkey {Key = Key, Modifiers = Modifiers}; + } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/Input/InputService.cs b/src/Artemis.Core/Services/Input/InputService.cs index a7502c6ef..179c6641d 100644 --- a/src/Artemis.Core/Services/Input/InputService.cs +++ b/src/Artemis.Core/Services/Input/InputService.cs @@ -9,19 +9,19 @@ namespace Artemis.Core.Services; internal class InputService : IInputService { private readonly ILogger _logger; - private readonly IRgbService _rgbService; + private readonly IDeviceService _deviceService; private ArtemisDevice? _firstKeyboard; private ArtemisDevice? _firstMouse; private int _keyboardCount; private int _mouseCount; - public InputService(ILogger logger, IRgbService rgbService) + public InputService(ILogger logger, IDeviceService deviceService) { _logger = logger; - _rgbService = rgbService; + _deviceService = deviceService; - _rgbService.DeviceAdded += RgbServiceOnDevicesModified; - _rgbService.DeviceRemoved += RgbServiceOnDevicesModified; + _deviceService.DeviceAdded += DeviceServiceOnDevicesModified; + _deviceService.DeviceRemoved += DeviceServiceOnDevicesModified; BustIdentifierCache(); } @@ -157,7 +157,7 @@ internal class InputService : IInputService _logger.Debug("Stop identifying device {device}", _identifyingDevice); _identifyingDevice = null; - _rgbService.SaveDevices(); + _deviceService.SaveDevices(); BustIdentifierCache(); } @@ -209,7 +209,7 @@ internal class InputService : IInputService public void BustIdentifierCache() { _deviceCache.Clear(); - _devices = _rgbService.EnabledDevices.Where(d => d.InputIdentifiers.Any()).ToList(); + _devices = _deviceService.EnabledDevices.Where(d => d.InputIdentifiers.Any()).ToList(); } private void AddDeviceToCache(ArtemisDevice match, InputProvider provider, object identifier) @@ -241,12 +241,12 @@ internal class InputService : IInputService OnDeviceIdentified(); } - private void RgbServiceOnDevicesModified(object? sender, DeviceEventArgs args) + private void DeviceServiceOnDevicesModified(object? sender, DeviceEventArgs args) { - _firstKeyboard = _rgbService.Devices.FirstOrDefault(d => d.DeviceType == RGBDeviceType.Keyboard); - _firstMouse = _rgbService.Devices.FirstOrDefault(d => d.DeviceType == RGBDeviceType.Mouse); - _keyboardCount = _rgbService.Devices.Count(d => d.DeviceType == RGBDeviceType.Keyboard); - _mouseCount = _rgbService.Devices.Count(d => d.DeviceType == RGBDeviceType.Mouse); + _firstKeyboard = _deviceService.Devices.FirstOrDefault(d => d.DeviceType == RGBDeviceType.Keyboard); + _firstMouse = _deviceService.Devices.FirstOrDefault(d => d.DeviceType == RGBDeviceType.Mouse); + _keyboardCount = _deviceService.Devices.Count(d => d.DeviceType == RGBDeviceType.Keyboard); + _mouseCount = _deviceService.Devices.Count(d => d.DeviceType == RGBDeviceType.Mouse); BustIdentifierCache(); } diff --git a/src/Artemis.Core/Services/Interfaces/ICoreService.cs b/src/Artemis.Core/Services/Interfaces/ICoreService.cs index 5830815ba..8d0d1f60b 100644 --- a/src/Artemis.Core/Services/Interfaces/ICoreService.cs +++ b/src/Artemis.Core/Services/Interfaces/ICoreService.cs @@ -12,21 +12,6 @@ public interface ICoreService : IArtemisService /// bool IsInitialized { get; } - /// - /// The time the last frame took to render - /// - TimeSpan FrameTime { get; } - - /// - /// The amount of frames rendered each second - /// - public int FrameRate { get; } - - /// - /// Gets or sets whether profiles are rendered each frame by calling their Render method - /// - bool ProfileRenderingDisabled { get; set; } - /// /// Gets a boolean indicating whether Artemis is running in an elevated environment (admin permissions) /// @@ -41,14 +26,4 @@ public interface ICoreService : IArtemisService /// Occurs the core has finished initializing /// event EventHandler Initialized; - - /// - /// Occurs whenever a frame is rendering, after modules have rendered - /// - event EventHandler FrameRendering; - - /// - /// Occurs whenever a frame is finished rendering and the render pipeline is closed - /// - event EventHandler FrameRendered; } \ No newline at end of file diff --git a/src/Artemis.Core/Services/Interfaces/IDeviceService.cs b/src/Artemis.Core/Services/Interfaces/IDeviceService.cs index c3b8c7d59..b66f14022 100644 --- a/src/Artemis.Core/Services/Interfaces/IDeviceService.cs +++ b/src/Artemis.Core/Services/Interfaces/IDeviceService.cs @@ -1,13 +1,109 @@ -namespace Artemis.Core.Services; +using System; +using System.Collections.Generic; +using Artemis.Core.DeviceProviders; + +namespace Artemis.Core.Services; /// /// A service that allows you manage an /// public interface IDeviceService : IArtemisService { + /// + /// Gets a read-only collection containing all enabled devices + /// + IReadOnlyCollection EnabledDevices { get; } + + /// + /// Gets a read-only collection containing all registered devices + /// + IReadOnlyCollection Devices { get; } + /// /// Identifies the device by making it blink white 5 times /// /// void IdentifyDevice(ArtemisDevice device); + + /// + /// Adds the given device provider and its devices. + /// + /// + void AddDeviceProvider(DeviceProvider deviceProvider); + + /// + /// Removes the given device provider and its devices. + /// + /// + void RemoveDeviceProvider(DeviceProvider deviceProvider); + + /// + /// Applies auto-arranging logic to the surface + /// + void AutoArrangeDevices(); + + /// + /// Apples the provided to the provided + /// + /// + /// + void ApplyDeviceLayout(ArtemisDevice device, ArtemisLayout? layout); + + /// + /// Enables the provided device + /// + /// The device to enable + void EnableDevice(ArtemisDevice device); + + /// + /// Disables the provided device + /// + /// The device to disable + void DisableDevice(ArtemisDevice device); + + /// + /// Saves the configuration of the provided device to persistent storage + /// + /// + void SaveDevice(ArtemisDevice artemisDevice); + + /// + /// Saves the configuration of all current devices to persistent storage + /// + void SaveDevices(); + + /// + /// Occurs when a single device was added. + /// + event EventHandler DeviceAdded; + + /// + /// Occurs when a single device was removed. + /// + event EventHandler DeviceRemoved; + + /// + /// Occurs when a single device was disabled + /// + event EventHandler DeviceEnabled; + + /// + /// Occurs when a single device was disabled. + /// + event EventHandler DeviceDisabled; + + /// + /// Occurs when a device provider was added. + /// + event EventHandler DeviceProviderAdded; + + /// + /// Occurs when a device provider was removed. + /// + event EventHandler DeviceProviderRemoved; + + /// + /// Occurs when the surface has had modifications to its LED collection + /// + event EventHandler LedsChanged; } \ No newline at end of file diff --git a/src/Artemis.Core/Services/Interfaces/IRenderService.cs b/src/Artemis.Core/Services/Interfaces/IRenderService.cs new file mode 100644 index 000000000..153f4c827 --- /dev/null +++ b/src/Artemis.Core/Services/Interfaces/IRenderService.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using Artemis.Core.Services.Core; +using Artemis.Core.SkiaSharp; +using RGB.NET.Core; + +namespace Artemis.Core.Services; + +/// +/// Represents a service that manages the render loop and renderers. +/// +public interface IRenderService : IArtemisService +{ + /// + /// Gets the graphics context to be used for rendering + /// + IManagedGraphicsContext? GraphicsContext { get; } + + /// + /// Gets the RGB surface to which is being rendered. + /// + RGBSurface Surface { get; } + + /// + /// Gets a list of registered renderers. + /// + List Renderers { get; } + + /// + /// Gets or sets a boolean indicating whether rendering is paused. + /// + bool IsPaused { get; set; } + + /// + /// The time the last frame took to render + /// + TimeSpan FrameTime { get; } + + /// + /// The amount of frames rendered each second + /// + public int FrameRate { get; } + + /// + /// Initializes the render service and starts rendering. + /// + void Initialize(); + + /// + /// Occurs whenever a frame is rendering, after modules have rendered + /// + event EventHandler FrameRendering; + + /// + /// Occurs whenever a frame is finished rendering and the render pipeline is closed + /// + event EventHandler FrameRendered; +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/Interfaces/IRgbService.cs b/src/Artemis.Core/Services/Interfaces/IRgbService.cs deleted file mode 100644 index df258f5bf..000000000 --- a/src/Artemis.Core/Services/Interfaces/IRgbService.cs +++ /dev/null @@ -1,168 +0,0 @@ -using System; -using System.Collections.Generic; -using Artemis.Core.SkiaSharp; -using RGB.NET.Core; - -namespace Artemis.Core.Services; - -/// -/// A service that allows you to manage the and its contents -/// -public interface IRgbService : IArtemisService, IDisposable -{ - /// - /// Gets a read-only collection containing all enabled devices - /// - IReadOnlyCollection EnabledDevices { get; } - - /// - /// Gets a read-only collection containing all registered devices - /// - IReadOnlyCollection Devices { get; } - - /// - /// Gets a dictionary containing all s on the surface with their corresponding RGB.NET - /// as key - /// - IReadOnlyDictionary LedMap { get; } - - /// - /// Gets or sets the RGB surface rendering is performed on - /// - RGBSurface Surface { get; set; } - - /// - /// Gets or sets whether rendering should be paused - /// - bool IsRenderPaused { get; } - - /// - /// Gets a boolean indicating whether the render pipeline is open - /// - bool RenderOpen { get; } - - /// - /// Gets or sets a boolean indicating whether to flush the RGB.NET LEDs during next update - /// - bool FlushLeds { get; set; } - - /// - /// Opens the render pipeline - /// - SKTexture OpenRender(); - - /// - /// Closes the render pipeline - /// - void CloseRender(); - - /// - /// Applies the current value of the Core.PreferredGraphicsContext setting to the graphics context. - /// - /// A boolean to indicate whether or not to force the graphics context to software mode. - void ApplyPreferredGraphicsContext(bool forceSoftware); - - /// - /// Updates the graphics context to the provided . - /// Note: The old graphics context will be used until the next frame starts rendering and is disposed afterwards. - /// - /// - /// The new managed graphics context. If , software rendering - /// is used. - /// - void UpdateGraphicsContext(IManagedGraphicsContext? managedGraphicsContext); - - /// - /// Adds the given device provider to the - /// - /// - void AddDeviceProvider(IRGBDeviceProvider deviceProvider); - - /// - /// Removes the given device provider from the - /// - /// - void RemoveDeviceProvider(IRGBDeviceProvider deviceProvider); - - /// - /// Applies auto-arranging logic to the surface - /// - void AutoArrangeDevices(); - - /// - /// Applies the best available layout for the given - /// - /// The device to apply the best available layout to - /// The layout that was applied to the device - ArtemisLayout? ApplyBestDeviceLayout(ArtemisDevice device); - - /// - /// Apples the provided to the provided - /// - /// - /// - void ApplyDeviceLayout(ArtemisDevice device, ArtemisLayout layout); - - /// - /// Attempts to retrieve the that corresponds the provided RGB.NET - /// - /// - /// - /// The RGB.NET to find the corresponding - /// for - /// - /// If found, the corresponding ; otherwise . - ArtemisDevice? GetDevice(IRGBDevice rgbDevice); - - /// - /// Attempts to retrieve the that corresponds the provided RGB.NET - /// - /// The RGB.NET to find the corresponding for - /// If found, the corresponding ; otherwise . - ArtemisLed? GetLed(Led led); - - /// - /// Saves the configuration of the provided device to persistent storage - /// - /// - void SaveDevice(ArtemisDevice artemisDevice); - - /// - /// Saves the configuration of all current devices to persistent storage - /// - void SaveDevices(); - - /// - /// Enables the provided device - /// - /// The device to enable - void EnableDevice(ArtemisDevice device); - - /// - /// Disables the provided device - /// - /// The device to disable - void DisableDevice(ArtemisDevice device); - - /// - /// Pauses or resumes rendering, method won't return until the current frame finished rendering - /// - /// - /// if the pause state was changed; otherwise . - bool SetRenderPaused(bool paused); - - /// - /// Occurs when a single device was added - /// - event EventHandler DeviceAdded; - - /// - /// Occurs when a single device was removed - /// - event EventHandler DeviceRemoved; - - /// - /// Occurs when the surface has had modifications to its LED collection - /// - event EventHandler LedsChanged; -} \ No newline at end of file diff --git a/src/Artemis.Core/Services/RenderService.cs b/src/Artemis.Core/Services/RenderService.cs new file mode 100644 index 000000000..d9653279a --- /dev/null +++ b/src/Artemis.Core/Services/RenderService.cs @@ -0,0 +1,251 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Artemis.Core.Providers; +using Artemis.Core.Services.Core; +using Artemis.Core.SkiaSharp; +using DryIoc; +using RGB.NET.Core; +using Serilog; +using SkiaSharp; + +namespace Artemis.Core.Services; + +internal class RenderService : IRenderService, IRenderer, IDisposable +{ + private readonly Stopwatch _frameStopWatch; + private readonly List _updateExceptions = new(); + + private readonly ILogger _logger; + private readonly IDeviceService _deviceService; + private readonly LazyEnumerable _graphicsContextProviders; + private readonly PluginSetting _targetFrameRateSetting; + private readonly PluginSetting _renderScaleSetting; + private readonly PluginSetting _preferredGraphicsContext; + + private SurfaceManager _surfaceManager; + private int _frames; + private DateTime _lastExceptionLog; + private DateTime _lastFrameRateSample; + private bool _initialized; + + public RenderService(ILogger logger, ISettingsService settingsService, IDeviceService deviceService, LazyEnumerable graphicsContextProviders) + { + _frameStopWatch = new Stopwatch(); + _logger = logger; + _deviceService = deviceService; + _graphicsContextProviders = graphicsContextProviders; + + _targetFrameRateSetting = settingsService.GetSetting("Core.TargetFrameRate", 30); + _renderScaleSetting = settingsService.GetSetting("Core.RenderScale", 0.25); + _preferredGraphicsContext = settingsService.GetSetting("Core.PreferredGraphicsContext", "Software"); + _targetFrameRateSetting.SettingChanged += OnRenderSettingsChanged; + _renderScaleSetting.SettingChanged += RenderScaleSettingOnSettingChanged; + _preferredGraphicsContext.SettingChanged += PreferredGraphicsContextOnSettingChanged; + + Utilities.ShutdownRequested += UtilitiesOnShutdownRequested; + _surfaceManager = new SurfaceManager(this, GraphicsContext, _targetFrameRateSetting.Value, (float) _renderScaleSetting.Value); + } + + /// + public IManagedGraphicsContext? GraphicsContext { get; private set; } + + /// + public RGBSurface Surface => _surfaceManager.Surface; + + /// + public List Renderers { get; } = new(); + + /// + public bool IsPaused + { + get => _surfaceManager.IsPaused; + set => _surfaceManager.SetPaused(value); + } + + /// + public int FrameRate { get; private set; } + + /// + public TimeSpan FrameTime { get; private set; } + + /// + public void Render(SKCanvas canvas, double delta) + { + _frameStopWatch.Restart(); + try + { + OnFrameRendering(new FrameRenderingEventArgs(canvas, delta, _surfaceManager.Surface)); + foreach (IRenderer renderer in Renderers) + renderer.Render(canvas, delta); + } + catch (Exception e) + { + _updateExceptions.Add(e); + } + } + + /// + public void PostRender(SKTexture texture) + { + try + { + foreach (IRenderer renderer in Renderers) + renderer.PostRender(texture); + OnFrameRendered(new FrameRenderedEventArgs(texture, _surfaceManager.Surface)); + } + catch (Exception e) + { + _updateExceptions.Add(e); + } + finally + { + _frameStopWatch.Stop(); + _frames++; + + if ((DateTime.Now - _lastFrameRateSample).TotalSeconds >= 1) + { + FrameRate = _frames; + _frames = 0; + _lastFrameRateSample = DateTime.Now; + } + + FrameTime = _frameStopWatch.Elapsed; + + LogUpdateExceptions(); + } + } + + private void SetGraphicsContext() + { + if (Constants.StartupArguments.Contains("--force-software-render")) + { + _logger.Warning("Startup argument '--force-software-render' is applied, forcing software rendering"); + GraphicsContext = null; + return; + } + + if (_preferredGraphicsContext.Value == "Software") + { + GraphicsContext = null; + return; + } + + List providers = _graphicsContextProviders.ToList(); + if (!providers.Any()) + { + _logger.Warning("No graphics context provider found, defaulting to software rendering"); + GraphicsContext = null; + } + else + { + IManagedGraphicsContext? context = providers.FirstOrDefault(p => p.GraphicsContextName == _preferredGraphicsContext.Value)?.GetGraphicsContext(); + if (context == null) + _logger.Warning("No graphics context named '{Context}' found, defaulting to software rendering", _preferredGraphicsContext.Value); + + GraphicsContext = context; + } + } + + private void LogUpdateExceptions() + { + // Only log update exceptions every 10 seconds to avoid spamming the logs + if (DateTime.Now - _lastExceptionLog < TimeSpan.FromSeconds(10)) + return; + _lastExceptionLog = DateTime.Now; + + if (!_updateExceptions.Any()) + return; + + // Group by stack trace, that should gather up duplicate exceptions + foreach (IGrouping exceptions in _updateExceptions.GroupBy(e => e.StackTrace)) + _logger.Warning(exceptions.First(), "Exception was thrown {count} times during update in the last 10 seconds", exceptions.Count()); + + // When logging is finished start with a fresh slate + _updateExceptions.Clear(); + } + + private void DeviceServiceOnDeviceProviderAdded(object? sender, DeviceProviderEventArgs e) + { + _surfaceManager.AddDevices(e.Devices.Where(d => d.IsEnabled)); + } + + private void DeviceServiceOnDeviceProviderRemoved(object? sender, DeviceProviderEventArgs e) + { + _surfaceManager.RemoveDevices(e.Devices); + } + + private void DeviceServiceOnDeviceEnabled(object? sender, DeviceEventArgs e) + { + _surfaceManager.AddDevices(new List {e.Device}); + } + + private void DeviceServiceOnDeviceDisabled(object? sender, DeviceEventArgs e) + { + _surfaceManager.RemoveDevices(new List {e.Device}); + } + + private void OnRenderSettingsChanged(object? sender, EventArgs e) + { + _surfaceManager.UpdateTargetFrameRate(_targetFrameRateSetting.Value); + } + + private void RenderScaleSettingOnSettingChanged(object? sender, EventArgs e) + { + RenderScale.SetRenderScaleMultiplier((int) (1 / _renderScaleSetting.Value)); + _surfaceManager.UpdateRenderScale((float) _renderScaleSetting.Value); + } + + private void PreferredGraphicsContextOnSettingChanged(object? sender, EventArgs e) + { + SetGraphicsContext(); + _surfaceManager.UpdateGraphicsContext(GraphicsContext); + } + + private void UtilitiesOnShutdownRequested(object? sender, EventArgs e) + { + IsPaused = true; + } + + /// + public void Dispose() + { + IsPaused = true; + _surfaceManager.Dispose(); + } + + /// + public event EventHandler? FrameRendering; + + /// + public event EventHandler? FrameRendered; + + /// + public void Initialize() + { + if (_initialized) + return; + + SetGraphicsContext(); + _surfaceManager.AddDevices(_deviceService.EnabledDevices); + + _deviceService.DeviceProviderAdded += DeviceServiceOnDeviceProviderAdded; + _deviceService.DeviceProviderRemoved += DeviceServiceOnDeviceProviderRemoved; + _deviceService.DeviceEnabled += DeviceServiceOnDeviceEnabled; + _deviceService.DeviceDisabled += DeviceServiceOnDeviceDisabled; + + IsPaused = false; + _initialized = true; + } + + protected virtual void OnFrameRendering(FrameRenderingEventArgs e) + { + FrameRendering?.Invoke(this, e); + } + + protected virtual void OnFrameRendered(FrameRenderedEventArgs e) + { + FrameRendered?.Invoke(this, e); + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/RgbService.cs b/src/Artemis.Core/Services/RgbService.cs deleted file mode 100644 index 753becc66..000000000 --- a/src/Artemis.Core/Services/RgbService.cs +++ /dev/null @@ -1,613 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using System.Threading; -using Artemis.Core.DeviceProviders; -using Artemis.Core.Providers; -using Artemis.Core.Services.Models; -using Artemis.Core.SkiaSharp; -using Artemis.Storage.Entities.Surface; -using Artemis.Storage.Repositories.Interfaces; -using DryIoc; -using RGB.NET.Core; -using Serilog; - -namespace Artemis.Core.Services; - -/// -/// Provides wrapped access the RGB.NET -/// -internal class RgbService : IRgbService -{ - private readonly ILogger _logger; - private readonly IPluginManagementService _pluginManagementService; - private readonly IDeviceRepository _deviceRepository; - private readonly LazyEnumerable _graphicsContextProviders; - - private readonly PluginSetting _preferredGraphicsContext; - private readonly PluginSetting _renderScaleSetting; - private readonly PluginSetting _targetFrameRateSetting; - - private readonly List _devices; - private readonly List _enabledDevices; - private readonly SKTextureBrush _textureBrush = new(null) {CalculationMode = RenderMode.Absolute}; - private Dictionary _ledMap; - private ListLedGroup? _surfaceLedGroup; - private SKTexture? _texture; - - public RgbService(ILogger logger, - ISettingsService settingsService, - IPluginManagementService pluginManagementService, - IDeviceRepository deviceRepository, - LazyEnumerable graphicsContextProviders) - { - _logger = logger; - _pluginManagementService = pluginManagementService; - _deviceRepository = deviceRepository; - _graphicsContextProviders = graphicsContextProviders; - - _targetFrameRateSetting = settingsService.GetSetting("Core.TargetFrameRate", 30); - _renderScaleSetting = settingsService.GetSetting("Core.RenderScale", 0.25); - _preferredGraphicsContext = settingsService.GetSetting("Core.PreferredGraphicsContext", "Software"); - - Surface = new RGBSurface(); - Utilities.RenderScaleMultiplier = (int) (1 / _renderScaleSetting.Value); - - // Let's throw these for now - Surface.Exception += SurfaceOnException; - Surface.SurfaceLayoutChanged += SurfaceOnLayoutChanged; - _targetFrameRateSetting.SettingChanged += TargetFrameRateSettingOnSettingChanged; - _renderScaleSetting.SettingChanged += RenderScaleSettingOnSettingChanged; - _preferredGraphicsContext.SettingChanged += PreferredGraphicsContextOnSettingChanged; - _enabledDevices = new List(); - _devices = new List(); - _ledMap = new Dictionary(); - - EnabledDevices = new ReadOnlyCollection(_enabledDevices); - Devices = new ReadOnlyCollection(_devices); - LedMap = new ReadOnlyDictionary(_ledMap); - - UpdateTrigger = new TimerUpdateTrigger {UpdateFrequency = 1.0 / _targetFrameRateSetting.Value}; - SetRenderPaused(true); - Surface.RegisterUpdateTrigger(UpdateTrigger); - - Utilities.ShutdownRequested += UtilitiesOnShutdownRequested; - } - - - public TimerUpdateTrigger UpdateTrigger { get; } - - protected virtual void OnDeviceRemoved(DeviceEventArgs e) - { - DeviceRemoved?.Invoke(this, e); - } - - protected virtual void OnLedsChanged() - { - LedsChanged?.Invoke(this, EventArgs.Empty); - _texture?.Invalidate(); - } - - private void UtilitiesOnShutdownRequested(object? sender, EventArgs e) - { - SetRenderPaused(true); - } - - private void SurfaceOnLayoutChanged(SurfaceLayoutChangedEventArgs args) - { - UpdateLedGroup(); - } - - private void UpdateLedGroup() - { - bool changedRenderPaused = SetRenderPaused(true); - try - { - _ledMap = new Dictionary(_devices.SelectMany(d => d.Leds).ToDictionary(l => l.RgbLed)); - LedMap = new ReadOnlyDictionary(_ledMap); - - if (_surfaceLedGroup == null) - { - _surfaceLedGroup = new ListLedGroup(Surface, LedMap.Select(l => l.Key)) {Brush = _textureBrush}; - OnLedsChanged(); - return; - } - - lock (_surfaceLedGroup) - { - // Clean up the old background - _surfaceLedGroup.Detach(); - - // Apply the application wide brush and decorator - _surfaceLedGroup = new ListLedGroup(Surface, LedMap.Select(l => l.Key)) {Brush = _textureBrush}; - OnLedsChanged(); - } - } - finally - { - if (changedRenderPaused) - SetRenderPaused(false); - } - } - - private void TargetFrameRateSettingOnSettingChanged(object? sender, EventArgs e) - { - UpdateTrigger.UpdateFrequency = 1.0 / _targetFrameRateSetting.Value; - } - - private void SurfaceOnException(ExceptionEventArgs args) - { - _logger.Warning(args.Exception, "Surface caught exception"); - throw args.Exception; - } - - private void OnDeviceAdded(DeviceEventArgs e) - { - DeviceAdded?.Invoke(this, e); - } - - private void RenderScaleSettingOnSettingChanged(object? sender, EventArgs e) - { - Utilities.RenderScaleMultiplier = (int) (1 / _renderScaleSetting.Value); - - _texture?.Invalidate(); - foreach (ArtemisDevice artemisDevice in Devices) - artemisDevice.CalculateRenderProperties(); - OnLedsChanged(); - } - - private void PreferredGraphicsContextOnSettingChanged(object? sender, EventArgs e) - { - ApplyPreferredGraphicsContext(false); - } - - public IReadOnlyCollection EnabledDevices { get; } - public IReadOnlyCollection Devices { get; } - public IReadOnlyDictionary LedMap { get; private set; } - - public RGBSurface Surface { get; set; } - public bool IsRenderPaused { get; set; } - public bool RenderOpen { get; private set; } - - /// - public bool FlushLeds { get; set; } - - public void AddDeviceProvider(IRGBDeviceProvider deviceProvider) - { - _logger.Verbose("[AddDeviceProvider] Pausing rendering to add {DeviceProvider}", deviceProvider.GetType().Name); - bool changedRenderPaused = SetRenderPaused(true); - - try - { - List toRemove = _devices.Where(a => deviceProvider.Devices.Any(d => a.RgbDevice == d)).ToList(); - _logger.Verbose("[AddDeviceProvider] Removing {Count} old device(s)", toRemove.Count); - Surface.Detach(toRemove.Select(d => d.RgbDevice)); - foreach (ArtemisDevice device in toRemove) - RemoveDevice(device); - - List providerExceptions = new(); - - void DeviceProviderOnException(object? sender, ExceptionEventArgs e) - { - if (e.IsCritical) - providerExceptions.Add(e.Exception); - else - _logger.Warning(e.Exception, "Device provider {deviceProvider} threw non-critical exception", deviceProvider.GetType().Name); - } - - _logger.Verbose("[AddDeviceProvider] Initializing device provider"); - deviceProvider.Exception += DeviceProviderOnException; - deviceProvider.Initialize(); - _logger.Verbose("[AddDeviceProvider] Attaching devices of device provider"); - Surface.Attach(deviceProvider.Devices); - deviceProvider.Exception -= DeviceProviderOnException; - if (providerExceptions.Count == 1) - throw new ArtemisPluginException("RGB.NET threw exception: " + providerExceptions.First().Message, providerExceptions.First()); - if (providerExceptions.Count > 1) - throw new ArtemisPluginException("RGB.NET threw multiple exceptions", new AggregateException(providerExceptions)); - - if (!deviceProvider.Devices.Any()) - { - _logger.Warning("Device provider {deviceProvider} has no devices", deviceProvider.GetType().Name); - return; - } - - foreach (IRGBDevice rgbDevice in deviceProvider.Devices) - { - ArtemisDevice artemisDevice = GetArtemisDevice(rgbDevice); - AddDevice(artemisDevice); - _logger.Debug("Device provider {deviceProvider} added {deviceName}", deviceProvider.GetType().Name, rgbDevice.DeviceInfo.DeviceName); - } - - _devices.Sort((a, b) => a.ZIndex - b.ZIndex); - } - catch (Exception e) - { - _logger.Error(e, "Exception during device loading for device provider {deviceProvider}", deviceProvider.GetType().Name); - throw; - } - finally - { - _logger.Verbose("[AddDeviceProvider] Updating the LED group"); - UpdateLedGroup(); - - _logger.Verbose("[AddDeviceProvider] Resuming rendering after adding {DeviceProvider}", deviceProvider.GetType().Name); - if (changedRenderPaused) - SetRenderPaused(false); - } - } - - public void RemoveDeviceProvider(IRGBDeviceProvider deviceProvider) - { - _logger.Verbose("[RemoveDeviceProvider] Pausing rendering to remove {DeviceProvider}", deviceProvider.GetType().Name); - bool changedRenderPaused = SetRenderPaused(true); - - try - { - List toRemove = _devices.Where(a => deviceProvider.Devices.Any(d => a.RgbDevice == d)).ToList(); - _logger.Verbose("[RemoveDeviceProvider] Removing {Count} old device(s)", toRemove.Count); - Surface.Detach(toRemove.Select(d => d.RgbDevice)); - foreach (ArtemisDevice device in toRemove) - RemoveDevice(device); - - _devices.Sort((a, b) => a.ZIndex - b.ZIndex); - } - catch (Exception e) - { - _logger.Error(e, "Exception during device removal for device provider {deviceProvider}", deviceProvider.GetType().Name); - throw; - } - finally - { - _logger.Verbose("[RemoveDeviceProvider] Updating the LED group"); - UpdateLedGroup(); - - _logger.Verbose("[RemoveDeviceProvider] Resuming rendering after adding {DeviceProvider}", deviceProvider.GetType().Name); - if (changedRenderPaused) - SetRenderPaused(false); - } - } - - public void Dispose() - { - Surface.UnregisterUpdateTrigger(UpdateTrigger); - - UpdateTrigger.Dispose(); - Surface.Dispose(); - } - - public bool SetRenderPaused(bool paused) - { - if (IsRenderPaused == paused) - return false; - - if (paused) - UpdateTrigger.Stop(); - else - UpdateTrigger.Start(); - - IsRenderPaused = paused; - return true; - } - - public event EventHandler? DeviceAdded; - public event EventHandler? DeviceRemoved; - public event EventHandler? LedsChanged; - - #region Rendering - - private IManagedGraphicsContext? _newGraphicsContext; - - - public SKTexture OpenRender() - { - if (RenderOpen) - throw new ArtemisCoreException("Render pipeline is already open"); - - if (_texture == null || _texture.IsInvalid) - CreateTexture(); - - RenderOpen = true; - return _texture!; - } - - public void CloseRender() - { - if (!RenderOpen) - throw new ArtemisCoreException("Render pipeline is already closed"); - - RenderOpen = false; - _texture?.CopyPixelData(); - } - - public void CreateTexture() - { - if (RenderOpen) - throw new ArtemisCoreException("Cannot update the texture while rendering"); - - lock (_devices) - { - IManagedGraphicsContext? graphicsContext = Constants.ManagedGraphicsContext = _newGraphicsContext; - if (!ReferenceEquals(graphicsContext, _newGraphicsContext)) - graphicsContext = _newGraphicsContext; - - if (graphicsContext != null) - _logger.Debug("Creating SKTexture with graphics context {graphicsContext}", graphicsContext.GetType().Name); - else - _logger.Debug("Creating SKTexture with software-based graphics context"); - - float evenWidth = Surface.Boundary.Size.Width; - if (evenWidth % 2 != 0) - evenWidth++; - float evenHeight = Surface.Boundary.Size.Height; - if (evenHeight % 2 != 0) - evenHeight++; - - float renderScale = (float) _renderScaleSetting.Value; - int width = Math.Max(1, MathF.Min(evenWidth * renderScale, 4096).RoundToInt()); - int height = Math.Max(1, MathF.Min(evenHeight * renderScale, 4096).RoundToInt()); - - _texture?.Dispose(); - _texture = new SKTexture(graphicsContext, width, height, renderScale, Devices); - _textureBrush.Texture = _texture; - - - if (!ReferenceEquals(_newGraphicsContext, Constants.ManagedGraphicsContext = _newGraphicsContext)) - { - Constants.ManagedGraphicsContext?.Dispose(); - Constants.ManagedGraphicsContext = _newGraphicsContext; - _newGraphicsContext = null; - } - } - } - - public void ApplyPreferredGraphicsContext(bool forceSoftware) - { - if (forceSoftware) - { - _logger.Warning("Startup argument '--force-software-render' is applied, forcing software rendering"); - UpdateGraphicsContext(null); - return; - } - - if (_preferredGraphicsContext.Value == "Software") - { - UpdateGraphicsContext(null); - return; - } - - - List providers = _graphicsContextProviders.ToList(); - if (!providers.Any()) - { - _logger.Warning("No graphics context provider found, defaulting to software rendering"); - UpdateGraphicsContext(null); - return; - } - - IManagedGraphicsContext? context = providers.FirstOrDefault(p => p.GraphicsContextName == _preferredGraphicsContext.Value)?.GetGraphicsContext(); - if (context == null) - { - _logger.Warning("No graphics context named '{Context}' found, defaulting to software rendering", _preferredGraphicsContext.Value); - UpdateGraphicsContext(null); - return; - } - - UpdateGraphicsContext(context); - } - - public void UpdateGraphicsContext(IManagedGraphicsContext? managedGraphicsContext) - { - if (ReferenceEquals(managedGraphicsContext, Constants.ManagedGraphicsContext)) - return; - - _newGraphicsContext = managedGraphicsContext; - _texture?.Invalidate(); - } - - #endregion - - #region EnabledDevices - - public void AutoArrangeDevices() - { - bool changedRenderPaused = SetRenderPaused(true); - - try - { - SurfaceArrangement surfaceArrangement = SurfaceArrangement.GetDefaultArrangement(); - surfaceArrangement.Arrange(_devices); - foreach (ArtemisDevice artemisDevice in _devices) - artemisDevice.ApplyDefaultCategories(); - - SaveDevices(); - } - finally - { - if (changedRenderPaused) - SetRenderPaused(false); - } - } - - public ArtemisLayout? ApplyBestDeviceLayout(ArtemisDevice device) - { - ArtemisLayout? layout; - - // Configured layout path takes precedence over all other options - if (device.CustomLayoutPath != null) - { - layout = new ArtemisLayout(device.CustomLayoutPath, LayoutSource.Configured); - if (layout.IsValid) - { - ApplyDeviceLayout(device, layout); - return layout; - } - } - - // Look for a layout provided by the user - layout = device.DeviceProvider.LoadUserLayout(device); - if (layout.IsValid) - { - ApplyDeviceLayout(device, layout); - return layout; - } - - if (device.DisableDefaultLayout) - { - layout = null; - ApplyDeviceLayout(device, layout); - return null; - } - - // Look for a layout provided by the plugin - layout = device.DeviceProvider.LoadLayout(device); - if (layout.IsValid) - { - ApplyDeviceLayout(device, layout); - return layout; - } - - // Finally fall back to a default layout - layout = ArtemisLayout.GetDefaultLayout(device); - ApplyDeviceLayout(device, layout); - return layout; - } - - public void ApplyDeviceLayout(ArtemisDevice device, ArtemisLayout? layout) - { - if (layout == null) - { - if (device.Layout != null) - ReloadDevice(device); - return; - } - - if (layout.Source == LayoutSource.Default) - device.ApplyLayout(layout, false, false); - else - device.ApplyLayout(layout, device.DeviceProvider.CreateMissingLedsSupported, device.DeviceProvider.RemoveExcessiveLedsSupported); - - UpdateLedGroup(); - } - - private void ReloadDevice(ArtemisDevice device) - { - // Any pending changes are otherwise lost including DisableDefaultLayout - device.ApplyToEntity(); - _deviceRepository.Save(device.DeviceEntity); - - DeviceProvider deviceProvider = device.DeviceProvider; - - // Feels bad but need to in order to get the initial LEDs back - _pluginManagementService.DisablePluginFeature(deviceProvider, false); - Thread.Sleep(500); - _pluginManagementService.EnablePluginFeature(deviceProvider, false); - } - - public ArtemisDevice? GetDevice(IRGBDevice rgbDevice) - { - return _devices.FirstOrDefault(d => d.RgbDevice == rgbDevice); - } - - public ArtemisLed? GetLed(Led led) - { - LedMap.TryGetValue(led, out ArtemisLed? artemisLed); - return artemisLed; - } - - public void EnableDevice(ArtemisDevice device) - { - if (device.IsEnabled) - return; - - _enabledDevices.Add(device); - device.IsEnabled = true; - device.ApplyToEntity(); - _deviceRepository.Save(device.DeviceEntity); - - OnDeviceAdded(new DeviceEventArgs(device)); - } - - public void DisableDevice(ArtemisDevice device) - { - if (!device.IsEnabled) - return; - - _enabledDevices.Remove(device); - device.IsEnabled = false; - device.ApplyToEntity(); - _deviceRepository.Save(device.DeviceEntity); - - OnDeviceRemoved(new DeviceEventArgs(device)); - } - - private void AddDevice(ArtemisDevice device) - { - if (_devices.Any(d => d.RgbDevice == device.RgbDevice)) - throw new ArtemisCoreException("Attempted to add a duplicate device to the RGB Service"); - - device.ApplyToRgbDevice(); - _devices.Add(device); - if (device.IsEnabled) - _enabledDevices.Add(device); - - // Will call UpdateBitmapBrush() - ApplyBestDeviceLayout(device); - OnDeviceAdded(new DeviceEventArgs(device)); - } - - private void RemoveDevice(ArtemisDevice device) - { - _devices.Remove(device); - if (device.IsEnabled) - _enabledDevices.Remove(device); - - OnDeviceRemoved(new DeviceEventArgs(device)); - } - - #endregion - - #region Storage - - private ArtemisDevice GetArtemisDevice(IRGBDevice rgbDevice) - { - string deviceIdentifier = rgbDevice.GetDeviceIdentifier(); - DeviceEntity? deviceEntity = _deviceRepository.Get(deviceIdentifier); - DeviceProvider deviceProvider = _pluginManagementService.GetDeviceProviderByDevice(rgbDevice); - - if (deviceEntity != null) - return new ArtemisDevice(rgbDevice, deviceProvider, deviceEntity); - - // Fall back on creating a new device - _logger.Information( - "No device config found for {deviceInfo}, device hash: {deviceHashCode}. Adding a new entry.", - rgbDevice.DeviceInfo, - deviceIdentifier - ); - return new ArtemisDevice(rgbDevice, deviceProvider); - } - - public void SaveDevice(ArtemisDevice artemisDevice) - { - artemisDevice.ApplyToEntity(); - artemisDevice.ApplyToRgbDevice(); - - _deviceRepository.Save(artemisDevice.DeviceEntity); - OnLedsChanged(); - } - - public void SaveDevices() - { - foreach (ArtemisDevice artemisDevice in _devices) - { - artemisDevice.ApplyToEntity(); - artemisDevice.ApplyToRgbDevice(); - } - - _deviceRepository.Save(_devices.Select(d => d.DeviceEntity)); - OnLedsChanged(); - } - - #endregion -} \ No newline at end of file diff --git a/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs b/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs index 59fae7a9d..4e8d357fa 100644 --- a/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs +++ b/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs @@ -41,6 +41,11 @@ public interface IProfileService : IArtemisService /// Gets or sets a value indicating whether the currently focused profile should receive updates. /// bool UpdateFocusProfile { get; set; } + + /// + /// Gets or sets whether profiles are rendered each frame by calling their Render method + /// + bool ProfileRenderingDisabled { get; set; } /// /// Creates a copy of the provided profile configuration. diff --git a/src/Artemis.Core/Services/Storage/ProfileService.cs b/src/Artemis.Core/Services/Storage/ProfileService.cs index 7321fc6b2..485dfd267 100644 --- a/src/Artemis.Core/Services/Storage/ProfileService.cs +++ b/src/Artemis.Core/Services/Storage/ProfileService.cs @@ -5,9 +5,9 @@ using System.IO; using System.IO.Compression; using System.Linq; using System.Text; -using System.Threading; using System.Threading.Tasks; using Artemis.Core.Modules; +using Artemis.Core.Services.Core; using Artemis.Storage.Entities.Profile; using Artemis.Storage.Repositories.Interfaces; using Newtonsoft.Json; @@ -16,36 +16,37 @@ using SkiaSharp; namespace Artemis.Core.Services; -internal class ProfileService : IProfileService +internal class ProfileService : IProfileService, IRenderer { private readonly ILogger _logger; - private readonly IRgbService _rgbService; private readonly IProfileCategoryRepository _profileCategoryRepository; private readonly IPluginManagementService _pluginManagementService; - + private readonly IDeviceService _deviceService; private readonly List _pendingKeyboardEvents = new(); private readonly List _profileCategories; private readonly IProfileRepository _profileRepository; private readonly List _renderExceptions = new(); private readonly List _updateExceptions = new(); + private DateTime _lastRenderExceptionLog; private DateTime _lastUpdateExceptionLog; public ProfileService(ILogger logger, - IRgbService rgbService, IProfileCategoryRepository profileCategoryRepository, IPluginManagementService pluginManagementService, IInputService inputService, + IDeviceService deviceService, + IRenderService renderService, IProfileRepository profileRepository) { _logger = logger; - _rgbService = rgbService; _profileCategoryRepository = profileCategoryRepository; _pluginManagementService = pluginManagementService; + _deviceService = deviceService; _profileRepository = profileRepository; _profileCategories = new List(_profileCategoryRepository.GetAll().Select(c => new ProfileCategory(c)).OrderBy(c => c.Order)); - _rgbService.LedsChanged += RgbServiceOnLedsChanged; + _deviceService.LedsChanged += DeviceServiceOnLedsChanged; _pluginManagementService.PluginFeatureEnabled += PluginManagementServiceOnPluginFeatureToggled; _pluginManagementService.PluginFeatureDisabled += PluginManagementServiceOnPluginFeatureToggled; @@ -54,11 +55,15 @@ internal class ProfileService : IProfileService if (!_profileCategories.Any()) CreateDefaultProfileCategories(); UpdateModules(); + + renderService.Renderers.Add(this); } public ProfileConfiguration? FocusProfile { get; set; } public ProfileElement? FocusProfileElement { get; set; } public bool UpdateFocusProfile { get; set; } + + public bool ProfileRenderingDisabled { get; set; } /// public void UpdateProfiles(double deltaTime) @@ -182,6 +187,21 @@ internal class ProfileService : IProfileService } } } + + /// + public void Render(SKCanvas canvas, double delta) + { + if (ProfileRenderingDisabled) + return; + + UpdateProfiles(delta); + RenderProfiles(canvas); + } + + /// + public void PostRender(SKTexture texture) + { + } /// public void LoadProfileConfigurationIcon(ProfileConfiguration profileConfiguration) @@ -235,7 +255,7 @@ internal class ProfileService : IProfileService throw new ArtemisCoreException($"Cannot find profile named: {profileConfiguration.Name} ID: {profileConfiguration.Entity.ProfileId}"); Profile profile = new(profileConfiguration, profileEntity); - profile.PopulateLeds(_rgbService.EnabledDevices); + profile.PopulateLeds(_deviceService.EnabledDevices); if (profile.IsFreshImport) { @@ -523,7 +543,7 @@ internal class ProfileService : IProfileService /// public void AdaptProfile(Profile profile) { - List devices = _rgbService.EnabledDevices.ToList(); + List devices = _deviceService.EnabledDevices.ToList(); foreach (Layer layer in profile.GetAllLayers()) layer.Adapter.Adapt(devices); @@ -552,7 +572,7 @@ internal class ProfileService : IProfileService foreach (ProfileConfiguration profileConfiguration in ProfileConfigurations) { if (profileConfiguration.Profile == null) continue; - profileConfiguration.Profile.PopulateLeds(_rgbService.EnabledDevices); + profileConfiguration.Profile.PopulateLeds(_deviceService.EnabledDevices); if (!profileConfiguration.Profile.IsFreshImport) continue; _logger.Debug("Profile is a fresh import, adapting to surface - {profile}", profileConfiguration.Profile); @@ -573,7 +593,7 @@ internal class ProfileService : IProfileService } } - private void RgbServiceOnLedsChanged(object? sender, EventArgs e) + private void DeviceServiceOnLedsChanged(object? sender, EventArgs e) { ActiveProfilesPopulateLeds(); } diff --git a/src/Artemis.Core/Utilities/RenderScale.cs b/src/Artemis.Core/Utilities/RenderScale.cs new file mode 100644 index 000000000..43f27d54d --- /dev/null +++ b/src/Artemis.Core/Utilities/RenderScale.cs @@ -0,0 +1,35 @@ +using System; +using SkiaSharp; + +namespace Artemis.Core; + +internal static class RenderScale +{ + internal static int RenderScaleMultiplier { get; private set; } = 2; + + internal static event EventHandler? RenderScaleMultiplierChanged; + + internal static void SetRenderScaleMultiplier(int renderScaleMultiplier) + { + RenderScaleMultiplier = renderScaleMultiplier; + RenderScaleMultiplierChanged?.Invoke(null, EventArgs.Empty); + } + + internal static SKRectI CreateScaleCompatibleRect(float x, float y, float width, float height) + { + int roundX = (int) MathF.Floor(x); + int roundY = (int) MathF.Floor(y); + int roundWidth = (int) MathF.Ceiling(width); + int roundHeight = (int) MathF.Ceiling(height); + + if (RenderScaleMultiplier == 1) + return SKRectI.Create(roundX, roundY, roundWidth, roundHeight); + + return SKRectI.Create( + roundX - roundX % RenderScaleMultiplier, + roundY - roundY % RenderScaleMultiplier, + roundWidth - roundWidth % RenderScaleMultiplier, + roundHeight - roundHeight % RenderScaleMultiplier + ); + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Utilities/Utilities.cs b/src/Artemis.Core/Utilities/Utilities.cs index db6e373a0..ed0ded1d7 100644 --- a/src/Artemis.Core/Utilities/Utilities.cs +++ b/src/Artemis.Core/Utilities/Utilities.cs @@ -156,28 +156,4 @@ public static class Utilities { UpdateRequested?.Invoke(null, e); } - - #region Scaling - - internal static int RenderScaleMultiplier { get; set; } = 2; - - internal static SKRectI CreateScaleCompatibleRect(float x, float y, float width, float height) - { - int roundX = (int) MathF.Floor(x); - int roundY = (int) MathF.Floor(y); - int roundWidth = (int) MathF.Ceiling(width); - int roundHeight = (int) MathF.Ceiling(height); - - if (RenderScaleMultiplier == 1) - return SKRectI.Create(roundX, roundY, roundWidth, roundHeight); - - return SKRectI.Create( - roundX - roundX % RenderScaleMultiplier, - roundY - roundY % RenderScaleMultiplier, - roundWidth - roundWidth % RenderScaleMultiplier, - roundHeight - roundHeight % RenderScaleMultiplier - ); - } - - #endregion } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs b/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs index 6908fadd3..84e4be2c2 100644 --- a/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs +++ b/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs @@ -27,8 +27,8 @@ namespace Artemis.UI.Shared; /// public class DeviceVisualizer : Control { - internal static readonly Dictionary BitmapCache = new(); - private readonly ICoreService _coreService; + internal static readonly Dictionary BitmapCache = new(); + private readonly IRenderService _renderService; private readonly List _deviceVisualizerLeds; private Rect _deviceBounds; @@ -40,7 +40,7 @@ public class DeviceVisualizer : Control /// public DeviceVisualizer() { - _coreService = UI.Locator.Resolve(); + _renderService = UI.Locator.Resolve(); _deviceVisualizerLeds = new List(); PointerReleased += OnPointerReleased; @@ -195,16 +195,6 @@ public class DeviceVisualizer : Control SetupForDevice(); } - private void DevicePropertyChanged(object? sender, PropertyChangedEventArgs e) - { - Dispatcher.UIThread.Invoke(async () => - { - if (Device != null) - BitmapCache.Remove(Device); - await SetupForDevice(); - }, DispatcherPriority.Background); - } - private void DeviceUpdated(object? sender, EventArgs e) { Dispatcher.UIThread.Invoke(SetupForDevice, DispatcherPriority.Background); @@ -251,7 +241,6 @@ public class DeviceVisualizer : Control { if (Device != null) { - Device.RgbDevice.PropertyChanged -= DevicePropertyChanged; Device.DeviceUpdated -= DeviceUpdated; } @@ -261,7 +250,7 @@ public class DeviceVisualizer : Control /// protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { - _coreService.FrameRendered += OnFrameRendered; + _renderService.FrameRendered += OnFrameRendered; base.OnAttachedToLogicalTree(e); } @@ -269,7 +258,7 @@ public class DeviceVisualizer : Control /// protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { - _coreService.FrameRendered -= OnFrameRendered; + _renderService.FrameRendered -= OnFrameRendered; base.OnDetachedFromLogicalTree(e); } @@ -283,7 +272,6 @@ public class DeviceVisualizer : Control if (_oldDevice != null) { - _oldDevice.RgbDevice.PropertyChanged -= DevicePropertyChanged; _oldDevice.DeviceUpdated -= DeviceUpdated; } @@ -294,7 +282,6 @@ public class DeviceVisualizer : Control _deviceBounds = MeasureDevice(); _loading = true; - Device.RgbDevice.PropertyChanged += DevicePropertyChanged; Device.DeviceUpdated += DeviceUpdated; // Create all the LEDs @@ -321,12 +308,15 @@ public class DeviceVisualizer : Control private RenderTargetBitmap? GetDeviceImage(ArtemisDevice device) { - if (BitmapCache.TryGetValue(device, out RenderTargetBitmap? existingBitmap)) + string? path = device.Layout?.Image?.LocalPath; + if (path == null) + return null; + + if (BitmapCache.TryGetValue(path, out RenderTargetBitmap? existingBitmap)) return existingBitmap; - - if (device.Layout?.Image == null || !File.Exists(device.Layout.Image.LocalPath)) + if (!File.Exists(path)) { - BitmapCache[device] = null; + BitmapCache[path] = null; return null; } @@ -335,7 +325,7 @@ public class DeviceVisualizer : Control RenderTargetBitmap renderTargetBitmap = new(new PixelSize((int) device.RgbDevice.ActualSize.Width * 2, (int) device.RgbDevice.ActualSize.Height * 2)); using DrawingContext context = renderTargetBitmap.CreateDrawingContext(); - using Bitmap bitmap = new(device.Layout.Image.LocalPath); + using Bitmap bitmap = new(path); using Bitmap scaledBitmap = bitmap.CreateScaledBitmap(renderTargetBitmap.PixelSize); context.DrawImage(scaledBitmap, new Rect(scaledBitmap.Size)); @@ -345,7 +335,7 @@ public class DeviceVisualizer : Control deviceVisualizerLed.DrawBitmap(context, 2 * device.Scale); } - BitmapCache[device] = renderTargetBitmap; + BitmapCache[path] = renderTargetBitmap; return renderTargetBitmap; } diff --git a/src/Artemis.UI.Shared/Services/MainWindow/IMainWindowService.cs b/src/Artemis.UI.Shared/Services/MainWindow/IMainWindowService.cs index d85066451..0e18f6c68 100644 --- a/src/Artemis.UI.Shared/Services/MainWindow/IMainWindowService.cs +++ b/src/Artemis.UI.Shared/Services/MainWindow/IMainWindowService.cs @@ -11,7 +11,12 @@ public interface IMainWindowService : IArtemisSharedUIService /// Gets a boolean indicating whether the main window is currently open /// bool IsMainWindowOpen { get; } - + + /// + /// Gets a boolean indicating whether the main window is currently focused + /// + bool IsMainWindowFocused { get; } + /// /// Sets up the main window provider that controls the state of the main window /// diff --git a/src/Artemis.UI.Shared/Services/MainWindow/MainWindowService.cs b/src/Artemis.UI.Shared/Services/MainWindow/MainWindowService.cs index a95126c66..0a95a0087 100644 --- a/src/Artemis.UI.Shared/Services/MainWindow/MainWindowService.cs +++ b/src/Artemis.UI.Shared/Services/MainWindow/MainWindowService.cs @@ -10,6 +10,9 @@ internal class MainWindowService : IMainWindowService /// public bool IsMainWindowOpen { get; private set; } + /// + public bool IsMainWindowFocused { get; private set; } + protected virtual void OnMainWindowOpened() { MainWindowOpened?.Invoke(this, EventArgs.Empty); @@ -24,11 +27,13 @@ internal class MainWindowService : IMainWindowService protected virtual void OnMainWindowFocused() { MainWindowFocused?.Invoke(this, EventArgs.Empty); + IsMainWindowFocused = true; } protected virtual void OnMainWindowUnfocused() { MainWindowUnfocused?.Invoke(this, EventArgs.Empty); + IsMainWindowFocused = false; } private void SyncWithManager() diff --git a/src/Artemis.UI.Shared/Services/ProfileEditor/IToolViewModel.cs b/src/Artemis.UI.Shared/Services/ProfileEditor/IToolViewModel.cs index e34febdfe..7b1bf30d8 100644 --- a/src/Artemis.UI.Shared/Services/ProfileEditor/IToolViewModel.cs +++ b/src/Artemis.UI.Shared/Services/ProfileEditor/IToolViewModel.cs @@ -1,4 +1,6 @@ using System; +using Artemis.Core; +using Avalonia.Input; using Material.Icons; namespace Artemis.UI.Shared.Services.ProfileEditor; @@ -43,6 +45,11 @@ public interface IToolViewModel : IDisposable /// Gets the tooltip which this tool should show in the toolbar. /// public string ToolTip { get; } + + /// + /// Gets the keyboard hotkey that activates the tool. + /// + Hotkey? Hotkey { get; } } /// @@ -98,5 +105,8 @@ public abstract class ToolViewModel : ActivatableViewModelBase, IToolViewModel /// public abstract string ToolTip { get; } + /// + public abstract Hotkey? Hotkey { get; } + #endregion } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs b/src/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs index f0c4cf085..6a69bdd3b 100644 --- a/src/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs +++ b/src/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs @@ -21,6 +21,7 @@ internal class ProfileEditorService : IProfileEditorService private readonly ILayerBrushService _layerBrushService; private readonly BehaviorSubject _layerPropertySubject = new(null); private readonly ILogger _logger; + private readonly IDeviceService _deviceService; private readonly IModuleService _moduleService; private readonly BehaviorSubject _pixelsPerSecondSubject = new(120); private readonly BehaviorSubject _playingSubject = new(false); @@ -28,7 +29,6 @@ internal class ProfileEditorService : IProfileEditorService private readonly Dictionary _profileEditorHistories = new(); private readonly BehaviorSubject _profileElementSubject = new(null); private readonly IProfileService _profileService; - private readonly IRgbService _rgbService; private readonly SourceList _selectedKeyframes; private readonly BehaviorSubject _suspendedEditingSubject = new(false); private readonly BehaviorSubject _timeSubject = new(TimeSpan.Zero); @@ -36,17 +36,17 @@ internal class ProfileEditorService : IProfileEditorService private ProfileEditorCommandScope? _profileEditorHistoryScope; public ProfileEditorService(ILogger logger, + IDeviceService deviceService, IProfileService profileService, IModuleService moduleService, - IRgbService rgbService, ILayerBrushService layerBrushService, IMainWindowService mainWindowService, IWindowService windowService) { _logger = logger; + _deviceService = deviceService; _profileService = profileService; _moduleService = moduleService; - _rgbService = rgbService; _layerBrushService = layerBrushService; _windowService = windowService; @@ -369,7 +369,7 @@ internal class ProfileEditorService : IProfileEditorService Layer layer = new(targetLayer.Parent, targetLayer.GetNewLayerName()); _layerBrushService.ApplyDefaultBrush(layer); - layer.AddLeds(_rgbService.EnabledDevices.SelectMany(d => d.Leds)); + layer.AddLeds(_deviceService.EnabledDevices.SelectMany(d => d.Leds)); ExecuteCommand(new AddProfileElement(layer, targetLayer.Parent, targetLayer.Order)); return layer; @@ -379,7 +379,7 @@ internal class ProfileEditorService : IProfileEditorService Layer layer = new(target, target.GetNewLayerName()); _layerBrushService.ApplyDefaultBrush(layer); - layer.AddLeds(_rgbService.EnabledDevices.SelectMany(d => d.Leds)); + layer.AddLeds(_deviceService.EnabledDevices.SelectMany(d => d.Leds)); ExecuteCommand(new AddProfileElement(layer, target, 0)); return layer; diff --git a/src/Artemis.UI.Shared/Utilities.cs b/src/Artemis.UI.Shared/Utilities.cs index 3b1c58495..48e0ae2b3 100644 --- a/src/Artemis.UI.Shared/Utilities.cs +++ b/src/Artemis.UI.Shared/Utilities.cs @@ -22,10 +22,11 @@ public static class UI static UI() { - KeyBindingsEnabled = InputElement.GotFocusEvent.Raised.Select(e => e.Item2.Source is not TextBox).StartWith(true); + CurrentKeyBindingsEnabled = InputElement.GotFocusEvent.Raised.Select(e => e.Item2.Source is not TextBox).StartWith(true); + CurrentKeyBindingsEnabled.Subscribe(b => KeyBindingsEnabled = b); MicaEnabled = MicaEnabledSubject.AsObservable(); } - + /// /// Gets the current IoC locator. /// @@ -36,10 +37,15 @@ public static class UI /// public static IClipboard Clipboard { get; set; } = null!; + /// + /// Gets an observable boolean indicating whether hotkeys are to be disabled. + /// + public static IObservable CurrentKeyBindingsEnabled { get; } + /// /// Gets a boolean indicating whether hotkeys are to be disabled. /// - public static IObservable KeyBindingsEnabled { get; } + public static bool KeyBindingsEnabled { get; private set; } /// /// Gets a boolean indicating whether the Mica effect should be enabled. diff --git a/src/Artemis.UI/Screens/Debugger/Tabs/Performance/PerformanceDebugViewModel.cs b/src/Artemis.UI/Screens/Debugger/Tabs/Performance/PerformanceDebugViewModel.cs index c21700423..d61cc8c25 100644 --- a/src/Artemis.UI/Screens/Debugger/Tabs/Performance/PerformanceDebugViewModel.cs +++ b/src/Artemis.UI/Screens/Debugger/Tabs/Performance/PerformanceDebugViewModel.cs @@ -14,7 +14,7 @@ namespace Artemis.UI.Screens.Debugger.Performance; public class PerformanceDebugViewModel : ActivatableViewModelBase { - private readonly ICoreService _coreService; + private readonly IRenderService _renderService; private readonly IPluginManagementService _pluginManagementService; private readonly DispatcherTimer _updateTimer; private double _currentFps; @@ -22,9 +22,9 @@ public class PerformanceDebugViewModel : ActivatableViewModelBase private int _renderHeight; private int _renderWidth; - public PerformanceDebugViewModel(ICoreService coreService, IPluginManagementService pluginManagementService) + public PerformanceDebugViewModel(IRenderService renderService, IPluginManagementService pluginManagementService) { - _coreService = coreService; + _renderService = renderService; _pluginManagementService = pluginManagementService; _updateTimer = new DispatcherTimer(TimeSpan.FromMilliseconds(500), DispatcherPriority.Background, (_, _) => Update()); @@ -87,12 +87,12 @@ public class PerformanceDebugViewModel : ActivatableViewModelBase private void HandleActivation() { Renderer = Constants.ManagedGraphicsContext != null ? Constants.ManagedGraphicsContext.GetType().Name : "Software"; - _coreService.FrameRendered += CoreServiceOnFrameRendered; + _renderService.FrameRendered += RenderServiceOnFrameRendered; } private void HandleDeactivation() { - _coreService.FrameRendered -= CoreServiceOnFrameRendered; + _renderService.FrameRendered -= RenderServiceOnFrameRendered; } private void PopulateItems() @@ -116,9 +116,9 @@ public class PerformanceDebugViewModel : ActivatableViewModelBase viewModel.Update(); } - private void CoreServiceOnFrameRendered(object? sender, FrameRenderedEventArgs e) + private void RenderServiceOnFrameRendered(object? sender, FrameRenderedEventArgs e) { - CurrentFps = _coreService.FrameRate; + CurrentFps = _renderService.FrameRate; SKImageInfo bitmapInfo = e.Texture.ImageInfo; RenderHeight = bitmapInfo.Height; diff --git a/src/Artemis.UI/Screens/Debugger/Tabs/Render/RenderDebugViewModel.cs b/src/Artemis.UI/Screens/Debugger/Tabs/Render/RenderDebugViewModel.cs index ae18a564c..b10317022 100644 --- a/src/Artemis.UI/Screens/Debugger/Tabs/Render/RenderDebugViewModel.cs +++ b/src/Artemis.UI/Screens/Debugger/Tabs/Render/RenderDebugViewModel.cs @@ -11,7 +11,7 @@ namespace Artemis.UI.Screens.Debugger.Render; public class RenderDebugViewModel : ActivatableViewModelBase { - private readonly ICoreService _coreService; + private readonly IRenderService _renderService; private double _currentFps; private Bitmap? _currentFrame; @@ -20,9 +20,9 @@ public class RenderDebugViewModel : ActivatableViewModelBase private int _renderHeight; private int _renderWidth; - public RenderDebugViewModel(ICoreService coreService) + public RenderDebugViewModel(IRenderService renderService) { - _coreService = coreService; + _renderService = renderService; DisplayName = "Rendering"; @@ -66,17 +66,17 @@ public class RenderDebugViewModel : ActivatableViewModelBase private void HandleActivation() { Renderer = Constants.ManagedGraphicsContext != null ? Constants.ManagedGraphicsContext.GetType().Name : "Software"; - _coreService.FrameRendered += CoreServiceOnFrameRendered; + _renderService.FrameRendered += RenderServiceOnFrameRendered; } private void HandleDeactivation() { - _coreService.FrameRendered -= CoreServiceOnFrameRendered; + _renderService.FrameRendered -= RenderServiceOnFrameRendered; } - private void CoreServiceOnFrameRendered(object? sender, FrameRenderedEventArgs e) + private void RenderServiceOnFrameRendered(object? sender, FrameRenderedEventArgs e) { - CurrentFps = _coreService.FrameRate; + CurrentFps = _renderService.FrameRate; using SKImage skImage = e.Texture.Surface.Snapshot(); SKImageInfo bitmapInfo = e.Texture.ImageInfo; diff --git a/src/Artemis.UI/Screens/Device/DeviceDetectInputViewModel.cs b/src/Artemis.UI/Screens/Device/DeviceDetectInputViewModel.cs index b915e14c6..9e70ce509 100644 --- a/src/Artemis.UI/Screens/Device/DeviceDetectInputViewModel.cs +++ b/src/Artemis.UI/Screens/Device/DeviceDetectInputViewModel.cs @@ -17,20 +17,16 @@ namespace Artemis.UI.Screens.Device; public class DeviceDetectInputViewModel : ContentDialogViewModelBase { private readonly IInputService _inputService; - private readonly ListLedGroup _ledGroup; private readonly INotificationService _notificationService; - private readonly IRgbService _rgbService; - public DeviceDetectInputViewModel(ArtemisDevice device, IInputService inputService, INotificationService notificationService, IRgbService rgbService) + public DeviceDetectInputViewModel(ArtemisDevice device, IInputService inputService, INotificationService notificationService, IRenderService renderService) { _inputService = inputService; _notificationService = notificationService; - _rgbService = rgbService; - Device = device; // Create a LED group way at the top - _ledGroup = new ListLedGroup(_rgbService.Surface, Device.Leds.Select(l => l.RgbLed)) + ListLedGroup ledGroup = new(renderService.Surface, Device.Leds.Select(l => l.RgbLed)) { Brush = new SolidColorBrush(new Color(255, 255, 0)), ZIndex = 999 @@ -46,7 +42,7 @@ public class DeviceDetectInputViewModel : ContentDialogViewModelBase Disposable.Create(() => { _inputService.StopIdentify(); - _ledGroup.Detach(); + ledGroup.Detach(); }).DisposeWith(disposables); }); } diff --git a/src/Artemis.UI/Screens/Device/DevicePropertiesViewModel.cs b/src/Artemis.UI/Screens/Device/DevicePropertiesViewModel.cs index 2c9cf056a..a87966726 100644 --- a/src/Artemis.UI/Screens/Device/DevicePropertiesViewModel.cs +++ b/src/Artemis.UI/Screens/Device/DevicePropertiesViewModel.cs @@ -17,7 +17,7 @@ public class DevicePropertiesViewModel : DialogViewModelBase private readonly IDeviceVmFactory _deviceVmFactory; private ArtemisDevice _device; - public DevicePropertiesViewModel(ArtemisDevice device, ICoreService coreService, IRgbService rgbService, IDeviceVmFactory deviceVmFactory) + public DevicePropertiesViewModel(ArtemisDevice device, IRenderService renderService, IDeviceService deviceService, IDeviceVmFactory deviceVmFactory) { _deviceVmFactory = deviceVmFactory; _device = device; @@ -28,10 +28,15 @@ public class DevicePropertiesViewModel : DialogViewModelBase AddTabs(); this.WhenActivated(d => { - rgbService.DeviceAdded += RgbServiceOnDeviceAdded; - rgbService.DeviceRemoved += RgbServiceOnDeviceRemoved; - coreService.FrameRendering += CoreServiceOnFrameRendering; - Disposable.Create(() => coreService.FrameRendering -= CoreServiceOnFrameRendering).DisposeWith(d); + deviceService.DeviceAdded += DeviceServiceOnDeviceAdded; + deviceService.DeviceRemoved += DeviceServiceOnDeviceRemoved; + renderService.FrameRendering += RenderServiceOnFrameRendering; + Disposable.Create(() => + { + deviceService.DeviceAdded -= DeviceServiceOnDeviceAdded; + deviceService.DeviceRemoved -= DeviceServiceOnDeviceRemoved; + renderService.FrameRendering -= RenderServiceOnFrameRendering; + }).DisposeWith(d); }); ClearSelectedLeds = ReactiveCommand.Create(ExecuteClearSelectedLeds); @@ -47,7 +52,7 @@ public class DevicePropertiesViewModel : DialogViewModelBase public ObservableCollection Tabs { get; } public ReactiveCommand ClearSelectedLeds { get; } - private void RgbServiceOnDeviceAdded(object? sender, DeviceEventArgs e) + private void DeviceServiceOnDeviceAdded(object? sender, DeviceEventArgs e) { if (e.Device.Identifier != Device.Identifier || Device == e.Device) return; @@ -56,7 +61,7 @@ public class DevicePropertiesViewModel : DialogViewModelBase AddTabs(); } - private void RgbServiceOnDeviceRemoved(object? sender, DeviceEventArgs e) + private void DeviceServiceOnDeviceRemoved(object? sender, DeviceEventArgs e) { Tabs.Clear(); SelectedLeds.Clear(); @@ -76,7 +81,7 @@ public class DevicePropertiesViewModel : DialogViewModelBase SelectedLeds.Clear(); } - private void CoreServiceOnFrameRendering(object? sender, FrameRenderingEventArgs e) + private void RenderServiceOnFrameRendering(object? sender, FrameRenderingEventArgs e) { if (!SelectedLeds.Any()) return; diff --git a/src/Artemis.UI/Screens/Device/DeviceSettingsViewModel.cs b/src/Artemis.UI/Screens/Device/DeviceSettingsViewModel.cs index 52faba0d3..55f01460d 100644 --- a/src/Artemis.UI/Screens/Device/DeviceSettingsViewModel.cs +++ b/src/Artemis.UI/Screens/Device/DeviceSettingsViewModel.cs @@ -18,18 +18,15 @@ public class DeviceSettingsViewModel : ActivatableViewModelBase private readonly IDeviceService _deviceService; private readonly DevicesTabViewModel _devicesTabViewModel; private readonly IDeviceVmFactory _deviceVmFactory; - private readonly IRgbService _rgbService; private readonly IWindowService _windowService; private bool _togglingDevice; - public DeviceSettingsViewModel(ArtemisDevice device, DevicesTabViewModel devicesTabViewModel, IDeviceService deviceService, IWindowService windowService, IDeviceVmFactory deviceVmFactory, - IRgbService rgbService) + public DeviceSettingsViewModel(ArtemisDevice device, DevicesTabViewModel devicesTabViewModel, IDeviceService deviceService, IWindowService windowService, IDeviceVmFactory deviceVmFactory) { _devicesTabViewModel = devicesTabViewModel; _deviceService = deviceService; _windowService = windowService; _deviceVmFactory = deviceVmFactory; - _rgbService = rgbService; Device = device; Type = Device.DeviceType.ToString().Humanize(); @@ -87,7 +84,7 @@ public class DeviceSettingsViewModel : ActivatableViewModelBase .ShowAsync(); if (viewModel.MadeChanges) - _rgbService.SaveDevice(Device); + _deviceService.SaveDevice(Device); } private async Task UpdateIsDeviceEnabled(bool value) @@ -103,9 +100,9 @@ public class DeviceSettingsViewModel : ActivatableViewModelBase value = !await _devicesTabViewModel.ShowDeviceDisableDialog(); if (value) - _rgbService.EnableDevice(Device); + _deviceService.EnableDevice(Device); else - _rgbService.DisableDevice(Device); + _deviceService.DisableDevice(Device); this.RaisePropertyChanged(nameof(IsDeviceEnabled)); SaveDevice(); @@ -118,6 +115,6 @@ public class DeviceSettingsViewModel : ActivatableViewModelBase private void SaveDevice() { - _rgbService.SaveDevice(Device); + _deviceService.SaveDevice(Device); } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Device/Tabs/DeviceGeneralTabViewModel.cs b/src/Artemis.UI/Screens/Device/Tabs/DeviceGeneralTabViewModel.cs index f8f21599b..892b270dc 100644 --- a/src/Artemis.UI/Screens/Device/Tabs/DeviceGeneralTabViewModel.cs +++ b/src/Artemis.UI/Screens/Device/Tabs/DeviceGeneralTabViewModel.cs @@ -20,7 +20,8 @@ namespace Artemis.UI.Screens.Device; public class DeviceGeneralTabViewModel : ActivatableViewModelBase { private readonly ICoreService _coreService; - private readonly IRgbService _rgbService; + private readonly IDeviceService _deviceService; + private readonly IRenderService _renderService; private readonly IWindowService _windowService; private readonly List _categories; @@ -39,10 +40,11 @@ public class DeviceGeneralTabViewModel : ActivatableViewModelBase private SKColor _currentColor; private bool _displayOnDevices; - public DeviceGeneralTabViewModel(ArtemisDevice device, ICoreService coreService, IRgbService rgbService, IWindowService windowService) + public DeviceGeneralTabViewModel(ArtemisDevice device, ICoreService coreService, IDeviceService deviceService, IRenderService renderService, IWindowService windowService) { _coreService = coreService; - _rgbService = rgbService; + _deviceService = deviceService; + _renderService = renderService; _windowService = windowService; _categories = new List(device.Categories); @@ -66,11 +68,11 @@ public class DeviceGeneralTabViewModel : ActivatableViewModelBase this.WhenActivated(d => { - _coreService.FrameRendering += OnFrameRendering; + _renderService.FrameRendering += OnFrameRendering; Disposable.Create(() => { - _coreService.FrameRendering -= OnFrameRendering; + _renderService.FrameRendering -= OnFrameRendering; Apply(); }).DisposeWith(d); }); @@ -191,17 +193,12 @@ public class DeviceGeneralTabViewModel : ActivatableViewModelBase return; await Task.Delay(400); - _rgbService.SaveDevice(Device); - _rgbService.ApplyBestDeviceLayout(Device); + _deviceService.SaveDevice(Device); + _deviceService.ApplyDeviceLayout(Device, Device.GetBestDeviceLayout()); } private void Apply() { - // TODO: Validation - - _coreService.ProfileRenderingDisabled = true; - Thread.Sleep(100); - Device.X = X; Device.Y = Y; Device.Scale = Scale; @@ -213,9 +210,7 @@ public class DeviceGeneralTabViewModel : ActivatableViewModelBase foreach (DeviceCategory deviceCategory in _categories) Device.Categories.Add(deviceCategory); - _rgbService.SaveDevice(Device); - - _coreService.ProfileRenderingDisabled = false; + _deviceService.SaveDevice(Device); } public void ApplyScaling() @@ -223,8 +218,7 @@ public class DeviceGeneralTabViewModel : ActivatableViewModelBase Device.RedScale = RedScale / 100f; Device.GreenScale = GreenScale / 100f; Device.BlueScale = BlueScale / 100f; - - _rgbService.FlushLeds = true; + Device.RgbDevice.Update(true); } public void ResetScaling() @@ -232,6 +226,7 @@ public class DeviceGeneralTabViewModel : ActivatableViewModelBase RedScale = _initialRedScale * 100; GreenScale = _initialGreenScale * 100; BlueScale = _initialBlueScale * 100; + Device.RgbDevice.Update(true); } private void OnFrameRendering(object? sender, FrameRenderingEventArgs e) diff --git a/src/Artemis.UI/Screens/Device/Tabs/DeviceLayoutTabViewModel.cs b/src/Artemis.UI/Screens/Device/Tabs/DeviceLayoutTabViewModel.cs index a0a11ca17..419243e9a 100644 --- a/src/Artemis.UI/Screens/Device/Tabs/DeviceLayoutTabViewModel.cs +++ b/src/Artemis.UI/Screens/Device/Tabs/DeviceLayoutTabViewModel.cs @@ -22,20 +22,13 @@ public class DeviceLayoutTabViewModel : ActivatableViewModelBase { private readonly IWindowService _windowService; private readonly INotificationService _notificationService; - private readonly ICoreService _coreService; - private readonly IRgbService _rgbService; + private readonly IDeviceService _deviceService; - public DeviceLayoutTabViewModel( - IWindowService windowService, - INotificationService notificationService, - ICoreService coreService, - IRgbService rgbService, - ArtemisDevice device) + public DeviceLayoutTabViewModel(IWindowService windowService, INotificationService notificationService, IDeviceService deviceService, ArtemisDevice device) { _windowService = windowService; _notificationService = notificationService; - _coreService = coreService; - _rgbService = rgbService; + _deviceService = deviceService; Device = device; DisplayName = "Layout"; @@ -44,11 +37,7 @@ public class DeviceLayoutTabViewModel : ActivatableViewModelBase this.WhenActivated(d => { Device.PropertyChanged += DeviceOnPropertyChanged; - - Disposable.Create(() => - { - Device.PropertyChanged -= DeviceOnPropertyChanged; - }).DisposeWith(d); + Disposable.Create(() => Device.PropertyChanged -= DeviceOnPropertyChanged).DisposeWith(d); }); } @@ -58,7 +47,7 @@ public class DeviceLayoutTabViewModel : ActivatableViewModelBase public string? ImagePath => Device.Layout?.Image?.LocalPath; - public string CustomLayoutPath => Device.CustomLayoutPath; + public string? CustomLayoutPath => Device.CustomLayoutPath; public bool HasCustomLayout => Device.CustomLayoutPath != null; @@ -68,6 +57,8 @@ public class DeviceLayoutTabViewModel : ActivatableViewModelBase _notificationService.CreateNotification() .WithMessage("Cleared imported layout.") .WithSeverity(NotificationSeverity.Informational); + + _deviceService.SaveDevice(Device); } public async Task BrowseCustomLayout() @@ -84,6 +75,8 @@ public class DeviceLayoutTabViewModel : ActivatableViewModelBase .WithTitle("Imported layout") .WithMessage($"File loaded from {files[0]}") .WithSeverity(NotificationSeverity.Informational); + + _deviceService.SaveDevice(Device); } } @@ -160,6 +153,10 @@ public class DeviceLayoutTabViewModel : ActivatableViewModelBase private void DeviceOnPropertyChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName is nameof(Device.CustomLayoutPath) or nameof(Device.DisableDefaultLayout)) - Task.Run(() => _rgbService.ApplyBestDeviceLayout(Device)); + { + Task.Run(() => _deviceService.ApplyDeviceLayout(Device, Device.GetBestDeviceLayout())); + this.RaisePropertyChanged(nameof(CustomLayoutPath)); + this.RaisePropertyChanged(nameof(HasCustomLayout)); + } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Device/Tabs/InputMappingsTabViewModel.cs b/src/Artemis.UI/Screens/Device/Tabs/InputMappingsTabViewModel.cs index 4d3acd760..a96395c7e 100644 --- a/src/Artemis.UI/Screens/Device/Tabs/InputMappingsTabViewModel.cs +++ b/src/Artemis.UI/Screens/Device/Tabs/InputMappingsTabViewModel.cs @@ -16,17 +16,17 @@ namespace Artemis.UI.Screens.Device; public class InputMappingsTabViewModel : ActivatableViewModelBase { private readonly IInputService _inputService; - private readonly IRgbService _rgbService; + private readonly IDeviceService _deviceService; private readonly ObservableCollection _selectedLeds; private ArtemisLed? _selectedLed; - public InputMappingsTabViewModel(ArtemisDevice device, ObservableCollection selectedLeds, IRgbService rgbService, IInputService inputService) + public InputMappingsTabViewModel(ArtemisDevice device, ObservableCollection selectedLeds, IInputService inputService, IDeviceService deviceService) { if (device.DeviceType != RGBDeviceType.Keyboard) throw new ArtemisUIException("The input mappings tab only supports keyboards"); - _rgbService = rgbService; _inputService = inputService; + _deviceService = deviceService; _selectedLeds = selectedLeds; Device = device; @@ -81,7 +81,7 @@ public class InputMappingsTabViewModel : ActivatableViewModelBase // Apply the new LED mapping Device.InputMappings[SelectedLed] = artemisLed; - _rgbService.SaveDevice(Device); + _deviceService.SaveDevice(Device); _selectedLeds.Clear(); UpdateInputMappings(); diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs index 9209789a1..7b323ab6e 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs @@ -58,7 +58,7 @@ public class MenuBarViewModel : ActivatableViewModelBase _focusNone = profileEditorService.FocusMode.Select(f => f == ProfileEditorFocusMode.None).ToProperty(this, vm => vm.FocusNone).DisposeWith(d); _focusFolder = profileEditorService.FocusMode.Select(f => f == ProfileEditorFocusMode.Folder).ToProperty(this, vm => vm.FocusFolder).DisposeWith(d); _focusSelection = profileEditorService.FocusMode.Select(f => f == ProfileEditorFocusMode.Selection).ToProperty(this, vm => vm.FocusSelection).DisposeWith(d); - _keyBindingsEnabled = Shared.UI.KeyBindingsEnabled.ToProperty(this, vm => vm.KeyBindingsEnabled).DisposeWith(d); + _keyBindingsEnabled = Shared.UI.CurrentKeyBindingsEnabled.ToProperty(this, vm => vm.KeyBindingsEnabled).DisposeWith(d); }); AddFolder = ReactiveCommand.Create(ExecuteAddFolder); diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/Playback/PlaybackViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/Playback/PlaybackViewModel.cs index 9f6009934..d7fc9516e 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/Playback/PlaybackViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/Playback/PlaybackViewModel.cs @@ -53,7 +53,7 @@ public class PlaybackViewModel : ActivatableViewModelBase _currentTime = _profileEditorService.Time.ToProperty(this, vm => vm.CurrentTime).DisposeWith(d); _formattedCurrentTime = _profileEditorService.Time.Select(t => $"{Math.Floor(t.TotalSeconds):00}.{t.Milliseconds:000}").ToProperty(this, vm => vm.FormattedCurrentTime).DisposeWith(d); _playing = _profileEditorService.Playing.ToProperty(this, vm => vm.Playing).DisposeWith(d); - _keyBindingsEnabled = Shared.UI.KeyBindingsEnabled.ToProperty(this, vm => vm.KeyBindingsEnabled).DisposeWith(d); + _keyBindingsEnabled = Shared.UI.CurrentKeyBindingsEnabled.ToProperty(this, vm => vm.KeyBindingsEnabled).DisposeWith(d); // Update timer Timer updateTimer = new(TimeSpan.FromMilliseconds(60.0 / 1000)); diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/Dialogs/LayerHintsDialogViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/Dialogs/LayerHintsDialogViewModel.cs index 5072135a1..edd5b07ec 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/Dialogs/LayerHintsDialogViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/Dialogs/LayerHintsDialogViewModel.cs @@ -15,12 +15,12 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs; public class LayerHintsDialogViewModel : DialogViewModelBase { - private readonly IRgbService _rgbService; + private readonly IDeviceService _deviceService; private readonly ILayerHintVmFactory _vmFactory; - public LayerHintsDialogViewModel(Layer layer, IRgbService rgbService, ILayerHintVmFactory vmFactory) + public LayerHintsDialogViewModel(Layer layer, IDeviceService deviceService, ILayerHintVmFactory vmFactory) { - _rgbService = rgbService; + _deviceService = deviceService; _vmFactory = vmFactory; Layer = layer; @@ -49,7 +49,7 @@ public class LayerHintsDialogViewModel : DialogViewModelBase public void AutoDetermineHints() { - Layer.Adapter.DetermineHints(_rgbService.EnabledDevices); + Layer.Adapter.DetermineHints(_deviceService.EnabledDevices); } public void AddCategoryHint() diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemViewModel.cs index 3e767e93f..df30c8b48 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemViewModel.cs @@ -14,17 +14,17 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree; public class FolderTreeItemViewModel : TreeItemViewModel { - private readonly IRgbService _rgbService; + private readonly IDeviceService _deviceService; public FolderTreeItemViewModel(TreeItemViewModel? parent, Folder folder, IWindowService windowService, + IDeviceService deviceService, IProfileEditorService profileEditorService, - IRgbService rgbService, IProfileEditorVmFactory profileEditorVmFactory) - : base(parent, folder, windowService, rgbService, profileEditorService, profileEditorVmFactory) + : base(parent, folder, windowService, deviceService, profileEditorService, profileEditorVmFactory) { - _rgbService = rgbService; + _deviceService = deviceService; Folder = folder; } @@ -63,7 +63,7 @@ public class FolderTreeItemViewModel : TreeItemViewModel pasted.Name = parent.GetNewFolderName(pasted.Name + " - copy"); ProfileEditorService.ExecuteCommand(new AddProfileElement(pasted, parent, Folder.Order - 1)); - Folder.Profile.PopulateLeds(_rgbService.EnabledDevices); + Folder.Profile.PopulateLeds(_deviceService.EnabledDevices); } private async Task ExecutePasteInto() diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemViewModel.cs index 920674f17..83554ac12 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemViewModel.cs @@ -14,17 +14,17 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree; public class LayerTreeItemViewModel : TreeItemViewModel { - private readonly IRgbService _rgbService; + private readonly IDeviceService _deviceService; public LayerTreeItemViewModel(TreeItemViewModel? parent, Layer layer, IWindowService windowService, IProfileEditorService profileEditorService, - IRgbService rgbService, + IDeviceService deviceService, IProfileEditorVmFactory profileEditorVmFactory) - : base(parent, layer, windowService, rgbService, profileEditorService, profileEditorVmFactory) + : base(parent, layer, windowService, deviceService, profileEditorService, profileEditorVmFactory) { - _rgbService = rgbService; + _deviceService = deviceService; Layer = layer; } @@ -43,7 +43,7 @@ public class LayerTreeItemViewModel : TreeItemViewModel Layer copied = new(Layer.Profile, Layer.Parent, copy, true); ProfileEditorService.ExecuteCommand(new AddProfileElement(copied, Layer.Parent, Layer.Order - 1)); - Layer.Profile.PopulateLeds(_rgbService.EnabledDevices); + Layer.Profile.PopulateLeds(_deviceService.EnabledDevices); } protected override async Task ExecuteCopy() @@ -65,7 +65,7 @@ public class LayerTreeItemViewModel : TreeItemViewModel pasted.Name = parent.GetNewLayerName(pasted.Name + " - copy"); ProfileEditorService.ExecuteCommand(new AddProfileElement(pasted, parent, Layer.Order - 1)); - Layer.Profile.PopulateLeds(_rgbService.EnabledDevices); + Layer.Profile.PopulateLeds(_deviceService.EnabledDevices); } /// diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs index 6d46177b7..82c559e98 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs @@ -23,8 +23,8 @@ public class ProfileTreeViewModel : TreeItemViewModel private ObservableAsPropertyHelper? _keyBindingsEnabled; private TreeItemViewModel? _selectedChild; - public ProfileTreeViewModel(IWindowService windowService, IRgbService rgbService, IProfileEditorService profileEditorService, IProfileEditorVmFactory profileEditorVmFactory) - : base(null, null, windowService, rgbService, profileEditorService, profileEditorVmFactory) + public ProfileTreeViewModel(IWindowService windowService, IDeviceService deviceService, IProfileEditorService profileEditorService, IProfileEditorVmFactory profileEditorVmFactory) + : base(null, null, windowService, deviceService, profileEditorService, profileEditorVmFactory) { this.WhenActivated(d => { @@ -46,7 +46,7 @@ public class ProfileTreeViewModel : TreeItemViewModel _focusNone = profileEditorService.FocusMode.Select(f => f == ProfileEditorFocusMode.None).ToProperty(this, vm => vm.FocusNone).DisposeWith(d); _focusFolder = profileEditorService.FocusMode.Select(f => f == ProfileEditorFocusMode.Folder).ToProperty(this, vm => vm.FocusFolder).DisposeWith(d); _focusSelection = profileEditorService.FocusMode.Select(f => f == ProfileEditorFocusMode.Selection).ToProperty(this, vm => vm.FocusSelection).DisposeWith(d); - _keyBindingsEnabled = Shared.UI.KeyBindingsEnabled.ToProperty(this, vm => vm.KeyBindingsEnabled).DisposeWith(d); + _keyBindingsEnabled = Shared.UI.CurrentKeyBindingsEnabled.ToProperty(this, vm => vm.KeyBindingsEnabled).DisposeWith(d); }); ClearSelection = ReactiveCommand.Create(() => profileEditorService.ChangeCurrentProfileElement(null), this.WhenAnyValue(vm => vm.KeyBindingsEnabled)); diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs index 9a35c54eb..16594700a 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs @@ -28,7 +28,7 @@ public abstract class TreeItemViewModel : ActivatableViewModelBase { private readonly IProfileEditorVmFactory _profileEditorVmFactory; private readonly IWindowService _windowService; - private readonly IRgbService _rgbService; + private readonly IDeviceService _deviceService; protected readonly IProfileEditorService ProfileEditorService; private bool _canPaste; private RenderProfileElement? _currentProfileElement; @@ -41,13 +41,13 @@ public abstract class TreeItemViewModel : ActivatableViewModelBase protected TreeItemViewModel(TreeItemViewModel? parent, ProfileElement? profileElement, IWindowService windowService, - IRgbService rgbService, + IDeviceService deviceService, IProfileEditorService profileEditorService, IProfileEditorVmFactory profileEditorVmFactory) { ProfileEditorService = profileEditorService; _windowService = windowService; - _rgbService = rgbService; + _deviceService = deviceService; _profileEditorVmFactory = profileEditorVmFactory; Parent = parent; @@ -267,7 +267,7 @@ public abstract class TreeItemViewModel : ActivatableViewModelBase if (ProfileElement is not Layer layer) return; - ProfileEditorService.ExecuteCommand(new ApplyAdaptionHints(layer, _rgbService.EnabledDevices.ToList())); + ProfileEditorService.ExecuteCommand(new ApplyAdaptionHints(layer, _deviceService.EnabledDevices.ToList())); } private async void UpdateCanPaste(bool isFlyoutOpen) diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Keyframes/TimelineEasingView.axaml b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Keyframes/TimelineEasingView.axaml index e925b4edb..ef1a69c32 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Keyframes/TimelineEasingView.axaml +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Keyframes/TimelineEasingView.axaml @@ -3,17 +3,21 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:keyframes="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes" + xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes.TimelineEasingView" x:DataType="keyframes:TimelineEasingViewModel"> - - + + - - + HorizontalAlignment="Left"/> + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Keyframes/TimelineEasingViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Keyframes/TimelineEasingViewModel.cs index cada48d40..3c196c0a1 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Keyframes/TimelineEasingViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Keyframes/TimelineEasingViewModel.cs @@ -1,8 +1,10 @@ using System.Collections.Generic; +using System.Reactive; using Artemis.Core; using Artemis.UI.Shared; using Avalonia; using Humanizer; +using ReactiveUI; namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes; @@ -10,11 +12,12 @@ public class TimelineEasingViewModel : ViewModelBase { private readonly ILayerPropertyKeyframe _keyframe; - public TimelineEasingViewModel(Easings.Functions easingFunction, ILayerPropertyKeyframe keyframe) + public TimelineEasingViewModel(Easings.Functions easingFunction, ILayerPropertyKeyframe keyframe, ReactiveCommand selectEasingFunction) { _keyframe = keyframe; EasingFunction = easingFunction; + SelectEasingFunction = selectEasingFunction; Description = easingFunction.Humanize(); EasingPoints = new List(); @@ -27,6 +30,7 @@ public class TimelineEasingViewModel : ViewModelBase } public Easings.Functions EasingFunction { get; } + public ReactiveCommand SelectEasingFunction { get; } public List EasingPoints { get; } public string Description { get; } public bool IsEasingModeSelected => _keyframe.EasingFunction == EasingFunction; diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Keyframes/TimelineKeyframeView.axaml b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Keyframes/TimelineKeyframeView.axaml index f72a45823..7977a73d9 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Keyframes/TimelineKeyframeView.axaml +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Keyframes/TimelineKeyframeView.axaml @@ -3,6 +3,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" + xmlns:keyframes="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes.TimelineKeyframeView" ClipToBounds="False"> @@ -34,16 +35,9 @@ - diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Keyframes/TimelineKeyframeViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Keyframes/TimelineKeyframeViewModel.cs index da70e2b5f..2f99088af 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Keyframes/TimelineKeyframeViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Keyframes/TimelineKeyframeViewModel.cs @@ -54,10 +54,12 @@ public class TimelineKeyframeViewModel : ActivatableViewModelBase, ITimelineK Copy = ReactiveCommand.CreateFromTask(ExecuteCopy); Paste = ReactiveCommand.CreateFromTask(ExecutePaste); Delete = ReactiveCommand.Create(ExecuteDelete); + SelectEasingFunction = ReactiveCommand.Create(ExecuteSelectEasingFunction); } public LayerPropertyKeyframe LayerPropertyKeyframe { get; } public ObservableCollection EasingViewModels { get; } + public double X { @@ -93,7 +95,8 @@ public class TimelineKeyframeViewModel : ActivatableViewModelBase, ITimelineK public ReactiveCommand Copy { get; } public ReactiveCommand Paste { get; } public ReactiveCommand Delete { get; } - + public ReactiveCommand SelectEasingFunction { get; } + public bool IsSelected => _isSelected?.Value ?? false; public TimeSpan Position => LayerPropertyKeyframe.Position; public ILayerPropertyKeyframe Keyframe => LayerPropertyKeyframe; @@ -255,10 +258,10 @@ public class TimelineKeyframeViewModel : ActivatableViewModelBase, ITimelineK EasingViewModels.AddRange(Enum.GetValues(typeof(Easings.Functions)) .Cast() - .Select(e => new TimelineEasingViewModel(e, Keyframe))); + .Select(e => new TimelineEasingViewModel(e, Keyframe, SelectEasingFunction))); } - public void SelectEasingFunction(Easings.Functions easingFunction) + private void ExecuteSelectEasingFunction(Easings.Functions easingFunction) { _profileEditorService.ExecuteCommand(new ChangeKeyframeEasing(Keyframe, easingFunction)); } diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionAddToolViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionAddToolViewModel.cs index b1bdf7297..484bca7f4 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionAddToolViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionAddToolViewModel.cs @@ -7,6 +7,7 @@ using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Shared.Services.ProfileEditor; using Artemis.UI.Shared.Services.ProfileEditor.Commands; +using Avalonia.Input; using Material.Icons; using ReactiveUI; using SkiaSharp; @@ -17,14 +18,14 @@ public class SelectionAddToolViewModel : ToolViewModel { private readonly ObservableAsPropertyHelper? _isEnabled; private readonly IProfileEditorService _profileEditorService; - private readonly IRgbService _rgbService; + private readonly IDeviceService _deviceService; private Layer? _layer; /// - public SelectionAddToolViewModel(IProfileEditorService profileEditorService, IRgbService rgbService) + public SelectionAddToolViewModel(IProfileEditorService profileEditorService, IDeviceService deviceService) { _profileEditorService = profileEditorService; - _rgbService = rgbService; + _deviceService = deviceService; // Not disposed when deactivated but when really disposed _isEnabled = profileEditorService.ProfileElement.Select(p => p is Layer).ToProperty(this, vm => vm.IsEnabled); @@ -45,9 +46,12 @@ public class SelectionAddToolViewModel : ToolViewModel /// public override MaterialIconKind Icon => MaterialIconKind.SelectionDrag; + + /// + public override Hotkey? Hotkey { get; } = new(KeyboardKey.OemPlus, KeyboardModifierKey.Control); /// - public override string ToolTip => "Add LEDs to the current layer"; + public override string ToolTip => "Add LEDs to the current layer (Ctrl + +)"; public void AddLedsInRectangle(SKRect rect, bool expand, bool inverse) { @@ -57,7 +61,7 @@ public class SelectionAddToolViewModel : ToolViewModel if (inverse) { List toRemove = _layer.Leds.Where(l => l.AbsoluteRectangle.IntersectsWith(rect)).ToList(); - List toAdd = _rgbService.EnabledDevices.SelectMany(d => d.Leds).Where(l => l.AbsoluteRectangle.IntersectsWith(rect)).Except(toRemove).ToList(); + List toAdd = _deviceService.EnabledDevices.SelectMany(d => d.Leds).Where(l => l.AbsoluteRectangle.IntersectsWith(rect)).Except(toRemove).ToList(); List leds = _layer.Leds.Except(toRemove).ToList(); leds.AddRange(toAdd); @@ -65,7 +69,7 @@ public class SelectionAddToolViewModel : ToolViewModel } else { - List leds = _rgbService.EnabledDevices.SelectMany(d => d.Leds).Where(l => l.AbsoluteRectangle.IntersectsWith(rect)).ToList(); + List leds = _deviceService.EnabledDevices.SelectMany(d => d.Leds).Where(l => l.AbsoluteRectangle.IntersectsWith(rect)).ToList(); if (expand) leds.AddRange(_layer.Leds); _profileEditorService.ExecuteCommand(new ChangeLayerLeds(_layer, leds.Distinct().ToList())); diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionRemoveToolViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionRemoveToolViewModel.cs index 2c7d0a995..806b9279a 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionRemoveToolViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionRemoveToolViewModel.cs @@ -4,8 +4,10 @@ using System.Linq; using System.Reactive.Disposables; 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.Input; using Material.Icons; using ReactiveUI; using SkiaSharp; @@ -38,12 +40,15 @@ public class SelectionRemoveToolViewModel : ToolViewModel /// public override int Order => 3; + + /// + public override Hotkey? Hotkey { get; } = new(KeyboardKey.OemMinus, KeyboardModifierKey.Control); /// public override MaterialIconKind Icon => MaterialIconKind.SelectOff; /// - public override string ToolTip => "Remove LEDs from the current layer"; + public override string ToolTip => "Remove LEDs from the current layer (Ctrl + -)"; public void RemoveLedsInRectangle(SKRect rect) { diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolViewModel.cs index 7475f15eb..dbb5c75bf 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolViewModel.cs @@ -3,11 +3,13 @@ using System.Reactive; using System.Reactive.Disposables; using System.Reactive.Linq; using Artemis.Core; +using Artemis.Core.Services; using Artemis.UI.Exceptions; using Artemis.UI.Shared.Extensions; using Artemis.UI.Shared.Services.ProfileEditor; using Artemis.UI.Shared.Services.ProfileEditor.Commands; using Avalonia; +using Avalonia.Input; using Material.Icons; using ReactiveUI; using SkiaSharp; @@ -95,12 +97,15 @@ public class TransformToolViewModel : ToolViewModel /// public override int Order => 3; + + /// + public override Hotkey? Hotkey { get; } = new(KeyboardKey.T, KeyboardModifierKey.Control); /// public override MaterialIconKind Icon => MaterialIconKind.TransitConnectionVariant; /// - public override string ToolTip => "Transform the shape of the current layer"; + public override string ToolTip => "Transform the shape of the current layer (Ctrl+T)"; public Rect ShapeBounds { diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs index 256595ce8..140548d88 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs @@ -25,7 +25,7 @@ public class VisualEditorViewModel : ActivatableViewModelBase private ObservableAsPropertyHelper? _suspendedEditing; private ReadOnlyObservableCollection? _tools; - public VisualEditorViewModel(IProfileEditorService profileEditorService, IRgbService rgbService, IProfileEditorVmFactory vmFactory) + public VisualEditorViewModel(IProfileEditorService profileEditorService, IDeviceService deviceService, IProfileEditorVmFactory vmFactory) { _vmFactory = vmFactory; _visualizers = new SourceList(); @@ -34,7 +34,7 @@ public class VisualEditorViewModel : ActivatableViewModelBase .Bind(out ReadOnlyObservableCollection visualizers) .Subscribe(); - Devices = new ObservableCollection(rgbService.EnabledDevices.OrderBy(d => d.ZIndex)); + Devices = new ObservableCollection(deviceService.EnabledDevices.OrderBy(d => d.ZIndex)); Visualizers = visualizers; this.WhenActivated(d => diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.axaml b/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.axaml index 1939fbbae..0ea17649e 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.axaml +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.axaml @@ -8,19 +8,11 @@ xmlns:shared="clr-namespace:Artemis.UI.Shared.Services.ProfileEditor;assembly=Artemis.UI.Shared" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.ProfileEditor.ProfileEditorView" - x:DataType="profileEditor:ProfileEditorViewModel"> + x:DataType="profileEditor:ProfileEditorViewModel" + Focusable="True"> - - - - - - - - -