diff --git a/src/Artemis.Core/Artemis.Core.csproj b/src/Artemis.Core/Artemis.Core.csproj index d6f7f9a4a..86d6f2a97 100644 --- a/src/Artemis.Core/Artemis.Core.csproj +++ b/src/Artemis.Core/Artemis.Core.csproj @@ -1,6 +1,4 @@ - - - + net7.0 false @@ -38,27 +36,20 @@ - + - - + - - - - - + + + + - - - - - - + diff --git a/src/Artemis.Core/Constants.cs b/src/Artemis.Core/Constants.cs index b5341f3e5..8386f5fcf 100644 --- a/src/Artemis.Core/Constants.cs +++ b/src/Artemis.Core/Constants.cs @@ -47,6 +47,7 @@ public static class Constants /// The full path to the Artemis logs folder /// public static readonly string LogsFolder = Path.Combine(DataFolder, "Logs"); + /// /// The full path to the Artemis logs folder /// @@ -61,6 +62,11 @@ public static class Constants /// The full path to the Artemis user layouts folder /// public static readonly string LayoutsFolder = Path.Combine(DataFolder, "User Layouts"); + + /// + /// The full path to the Artemis user layouts folder + /// + public static readonly string WorkshopFolder = Path.Combine(DataFolder, "workshop"); /// /// The current API version for plugins @@ -71,9 +77,9 @@ public static class Constants /// /// The current version of the application /// - public static readonly string CurrentVersion = CoreAssembly.GetCustomAttribute()!.InformationalVersion != "1.0.0" - ? CoreAssembly.GetCustomAttribute()!.InformationalVersion - : "local"; + public static readonly string CurrentVersion = CoreAssembly.GetCustomAttribute()!.InformationalVersion.StartsWith("1.0.0") + ? "local" + : CoreAssembly.GetCustomAttribute()!.InformationalVersion; /// /// The plugin info used by core components of Artemis @@ -151,8 +157,8 @@ public static class Constants public static ReadOnlyCollection StartupArguments { get; set; } = null!; /// - /// Gets the graphics context to be used for rendering by SkiaSharp. Can be set via - /// . + /// Gets the graphics context to be used for rendering by SkiaSharp. /// public static IManagedGraphicsContext? ManagedGraphicsContext { get; internal set; } + } \ No newline at end of file diff --git a/src/Artemis.Core/DryIoc/ContainerExtensions.cs b/src/Artemis.Core/DryIoc/ContainerExtensions.cs index b9b768c78..44d377e8a 100644 --- a/src/Artemis.Core/DryIoc/ContainerExtensions.cs +++ b/src/Artemis.Core/DryIoc/ContainerExtensions.cs @@ -2,6 +2,7 @@ using System; using System.Linq; using System.Reflection; using Artemis.Core.DryIoc.Factories; +using Artemis.Core.Providers; using Artemis.Core.Services; using Artemis.Storage; using Artemis.Storage.Migrations.Interfaces; @@ -35,7 +36,8 @@ public static class ContainerExtensions // Bind migrations container.RegisterMany(storageAssembly, type => type.IsAssignableTo(), Reuse.Singleton, nonPublicServiceTypes: true); - + + container.RegisterMany(coreAssembly, type => type.IsAssignableTo(), Reuse.Singleton); container.Register(Reuse.Singleton); container.Register(Made.Of(_ => ServiceInfo.Of(), f => f.CreatePluginSettings(Arg.Index(0)), r => r.Parent.ImplementationType)); container.Register(Reuse.Singleton); diff --git a/src/Artemis.Core/Extensions/DirectoryInfoExtensions.cs b/src/Artemis.Core/Extensions/DirectoryInfoExtensions.cs deleted file mode 100644 index 5a6864273..000000000 --- a/src/Artemis.Core/Extensions/DirectoryInfoExtensions.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.IO; - -namespace Artemis.Core; - -internal static class DirectoryInfoExtensions -{ - public static void CopyFilesRecursively(this DirectoryInfo source, DirectoryInfo target) - { - foreach (DirectoryInfo dir in source.GetDirectories()) - CopyFilesRecursively(dir, target.CreateSubdirectory(dir.Name)); - foreach (FileInfo file in source.GetFiles()) - file.CopyTo(Path.Combine(target.FullName, file.Name)); - } - - public static void DeleteRecursively(this DirectoryInfo baseDir) - { - if (!baseDir.Exists) - return; - - foreach (DirectoryInfo dir in baseDir.EnumerateDirectories()) - DeleteRecursively(dir); - FileInfo[] files = baseDir.GetFiles(); - foreach (FileInfo file in files) - { - file.IsReadOnly = false; - file.Delete(); - } - - baseDir.Delete(); - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs index b0dec1d29..04f84c8f1 100644 --- a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs +++ b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs @@ -23,7 +23,6 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable private bool _disposed; private Hotkey? _enableHotkey; private ProfileConfigurationHotkeyMode _hotkeyMode; - private bool _isBeingEdited; private bool _isMissingModule; private bool _isSuspended; private bool _fadeInAndOut; diff --git a/src/Artemis.Core/Models/Surface/ArtemisDevice.cs b/src/Artemis.Core/Models/Surface/ArtemisDevice.cs index 914b461c5..6a42aacc7 100644 --- a/src/Artemis.Core/Models/Surface/ArtemisDevice.cs +++ b/src/Artemis.Core/Models/Surface/ArtemisDevice.cs @@ -2,9 +2,10 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; -using System.Diagnostics; using System.Linq; using Artemis.Core.DeviceProviders; +using Artemis.Core.Providers; +using Artemis.Core.Services; using Artemis.Storage.Entities.Surface; using RGB.NET.Core; using SkiaSharp; @@ -45,6 +46,7 @@ public class ArtemisDevice : CorePropertyChanged InputIdentifiers = new List(); InputMappings = new Dictionary(); Categories = new HashSet(); + LayoutSelection = new LayoutSelection {Type = DefaultLayoutProvider.LayoutType}; RgbDevice.ColorCorrections.Clear(); RgbDevice.ColorCorrections.Add(new ScaleColorCorrection(this)); @@ -73,6 +75,7 @@ public class ArtemisDevice : CorePropertyChanged InputIdentifiers = new List(); InputMappings = new Dictionary(); Categories = new HashSet(); + LayoutSelection = new LayoutSelection {Type = DefaultLayoutProvider.LayoutType}; foreach (DeviceInputIdentifierEntity identifierEntity in DeviceEntity.InputIdentifiers) InputIdentifiers.Add(new ArtemisDeviceInputIdentifier(identifierEntity.InputProvider, identifierEntity.Identifier)); @@ -152,6 +155,8 @@ public class ArtemisDevice : CorePropertyChanged /// public HashSet Categories { get; } + public LayoutSelection LayoutSelection { get; } + /// /// Gets or sets the X-position of the device /// @@ -266,7 +271,7 @@ public class ArtemisDevice : CorePropertyChanged /// /// Gets a boolean indicating whether this devices is enabled or not - /// Note: To enable/disable a device use the methods provided by + /// Note: To enable/disable a device use the methods provided by /// public bool IsEnabled { @@ -292,19 +297,6 @@ public class ArtemisDevice : CorePropertyChanged } } - /// - /// Gets or sets a boolean indicating whether falling back to default layouts is enabled or not - /// - public bool DisableDefaultLayout - { - get => DeviceEntity.DisableDefaultLayout; - set - { - DeviceEntity.DisableDefaultLayout = value; - OnPropertyChanged(nameof(DisableDefaultLayout)); - } - } - /// /// Gets or sets the logical layout of the device e.g. DE, UK or US. /// Only applicable to keyboards @@ -319,20 +311,6 @@ public class ArtemisDevice : CorePropertyChanged } } - /// - /// Gets or sets the path of the custom layout to load when calling - /// for this device - /// - public string? CustomLayoutPath - { - get => DeviceEntity.CustomLayoutPath; - set - { - DeviceEntity.CustomLayoutPath = value; - OnPropertyChanged(nameof(CustomLayoutPath)); - } - } - /// /// Gets the layout of the device expanded with Artemis-specific data /// @@ -380,40 +358,6 @@ 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 /// @@ -454,14 +398,6 @@ public class ArtemisDevice : CorePropertyChanged } } - /// - /// Invokes the event - /// - protected virtual void OnDeviceUpdated() - { - DeviceUpdated?.Invoke(this, EventArgs.Empty); - } - /// /// Applies the provided layout to the device /// @@ -474,13 +410,13 @@ 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) + public void ApplyLayout(ArtemisLayout? layout, bool createMissingLeds, bool removeExcessiveLeds) { if (layout != null && layout.IsValid && createMissingLeds && !DeviceProvider.CreateMissingLedsSupported) throw new ArtemisCoreException($"Cannot apply layout with {nameof(createMissingLeds)} set to true because the device provider does not support it"); if (layout != null && layout.IsValid && removeExcessiveLeds && !DeviceProvider.RemoveExcessiveLedsSupported) throw new ArtemisCoreException($"Cannot apply layout with {nameof(removeExcessiveLeds)} set to true because the device provider does not support it"); - + // Always clear the current layout ClearLayout(); @@ -501,6 +437,14 @@ public class ArtemisDevice : CorePropertyChanged CalculateRenderProperties(); } + /// + /// Invokes the event + /// + protected virtual void OnDeviceUpdated() + { + DeviceUpdated?.Invoke(this, EventArgs.Empty); + } + private void ClearLayout() { if (Layout == null) @@ -533,6 +477,9 @@ public class ArtemisDevice : CorePropertyChanged DeviceEntity.Categories.Clear(); foreach (DeviceCategory deviceCategory in Categories) DeviceEntity.Categories.Add((int) deviceCategory); + + DeviceEntity.LayoutType = LayoutSelection.Type; + DeviceEntity.LayoutParameter = LayoutSelection.Parameter; } internal void Load() @@ -547,6 +494,9 @@ public class ArtemisDevice : CorePropertyChanged if (!Categories.Any()) ApplyDefaultCategories(); + LayoutSelection.Type = DeviceEntity.LayoutType; + LayoutSelection.Parameter = DeviceEntity.LayoutParameter; + LoadInputMappings(); } @@ -572,7 +522,7 @@ public class ArtemisDevice : CorePropertyChanged { Leds = RgbDevice.Select(l => new ArtemisLed(l, this)).ToList().AsReadOnly(); LedIds = new ReadOnlyDictionary(Leds.ToDictionary(l => l.RgbLed.Id, l => l)); - + if (loadInputMappings) LoadInputMappings(); } diff --git a/src/Artemis.Core/Models/Surface/Layout/ArtemisLayout.cs b/src/Artemis.Core/Models/Surface/Layout/ArtemisLayout.cs index f44e3185d..5304f5e37 100644 --- a/src/Artemis.Core/Models/Surface/Layout/ArtemisLayout.cs +++ b/src/Artemis.Core/Models/Surface/Layout/ArtemisLayout.cs @@ -12,17 +12,18 @@ namespace Artemis.Core; /// public class ArtemisLayout { + private static readonly string DefaultLayoutPath = Path.Combine(Constants.ApplicationFolder, "DefaultLayouts", "Artemis"); + /// /// Creates a new instance of the class /// /// The path of the layout XML file - /// The source from where this layout is being loaded - public ArtemisLayout(string filePath, LayoutSource source) + public ArtemisLayout(string filePath) { FilePath = filePath; - Source = source; Leds = new List(); - + IsDefaultLayout = filePath.StartsWith(DefaultLayoutPath); + LoadLayout(); } @@ -31,11 +32,6 @@ public class ArtemisLayout /// public string FilePath { get; } - /// - /// Gets the source from where this layout was loaded - /// - public LayoutSource Source { get; } - /// /// Gets a boolean indicating whether a valid layout was loaded /// @@ -61,6 +57,8 @@ public class ArtemisLayout /// public LayoutCustomDeviceData LayoutCustomDeviceData { get; private set; } = null!; + public bool IsDefaultLayout { get; private set; } + /// /// Applies the layout to the provided device /// @@ -121,29 +119,28 @@ public class ArtemisLayout internal static ArtemisLayout? GetDefaultLayout(ArtemisDevice device) { - string layoutFolder = Path.Combine(Constants.ApplicationFolder, "DefaultLayouts", "Artemis"); if (device.DeviceType == RGBDeviceType.Keyboard) { // XL layout is defined by its programmable macro keys if (device.Leds.Any(l => l.RgbLed.Id >= LedId.Keyboard_Programmable1 && l.RgbLed.Id <= LedId.Keyboard_Programmable32)) { if (device.PhysicalLayout == KeyboardLayoutType.ANSI) - return new ArtemisLayout(Path.Combine(layoutFolder, "Keyboard", "Artemis XL keyboard-ANSI.xml"), LayoutSource.Default); - return new ArtemisLayout(Path.Combine(layoutFolder, "Keyboard", "Artemis XL keyboard-ISO.xml"), LayoutSource.Default); + return new ArtemisLayout(Path.Combine(DefaultLayoutPath, "Keyboard", "Artemis XL keyboard-ANSI.xml")); + return new ArtemisLayout(Path.Combine(DefaultLayoutPath, "Keyboard", "Artemis XL keyboard-ISO.xml")); } // L layout is defined by its numpad if (device.Leds.Any(l => l.RgbLed.Id >= LedId.Keyboard_NumLock && l.RgbLed.Id <= LedId.Keyboard_NumPeriodAndDelete)) { if (device.PhysicalLayout == KeyboardLayoutType.ANSI) - return new ArtemisLayout(Path.Combine(layoutFolder + "Keyboard", "Artemis L keyboard-ANSI.xml"), LayoutSource.Default); - return new ArtemisLayout(Path.Combine(layoutFolder + "Keyboard", "Artemis L keyboard-ISO.xml"), LayoutSource.Default); + return new ArtemisLayout(Path.Combine(DefaultLayoutPath, "Keyboard", "Artemis L keyboard-ANSI.xml")); + return new ArtemisLayout(Path.Combine(DefaultLayoutPath, "Keyboard", "Artemis L keyboard-ISO.xml")); } // No numpad will result in TKL if (device.PhysicalLayout == KeyboardLayoutType.ANSI) - return new ArtemisLayout(Path.Combine(layoutFolder + "Keyboard", "Artemis TKL keyboard-ANSI.xml"), LayoutSource.Default); - return new ArtemisLayout(Path.Combine(layoutFolder + "Keyboard", "Artemis TKL keyboard-ISO.xml"), LayoutSource.Default); + return new ArtemisLayout(Path.Combine(DefaultLayoutPath, "Keyboard", "Artemis TKL keyboard-ANSI.xml")); + return new ArtemisLayout(Path.Combine(DefaultLayoutPath, "Keyboard", "Artemis TKL keyboard-ISO.xml")); } // if (device.DeviceType == RGBDeviceType.Mouse) @@ -151,21 +148,21 @@ public class ArtemisLayout // if (device.Leds.Count == 1) // { // if (device.Leds.Any(l => l.RgbLed.Id == LedId.Logo)) - // return new ArtemisLayout(Path.Combine(layoutFolder + "Mouse", "1 LED mouse logo.xml"), LayoutSource.Default); - // return new ArtemisLayout(Path.Combine(layoutFolder + "Mouse", "1 LED mouse.xml"), LayoutSource.Default); + // return new ArtemisLayout(Path.Combine(DefaultLayoutPath, "Mouse", "1 LED mouse logo.xml"), LayoutSource.Default); + // return new ArtemisLayout(Path.Combine(DefaultLayoutPath, "Mouse", "1 LED mouse.xml"), LayoutSource.Default); // } // if (device.Leds.Any(l => l.RgbLed.Id == LedId.Logo)) - // return new ArtemisLayout(Path.Combine(layoutFolder + "Mouse", "4 LED mouse logo.xml"), LayoutSource.Default); - // return new ArtemisLayout(Path.Combine(layoutFolder + "Mouse", "4 LED mouse.xml"), LayoutSource.Default); + // return new ArtemisLayout(Path.Combine(DefaultLayoutPath, "Mouse", "4 LED mouse logo.xml"), LayoutSource.Default); + // return new ArtemisLayout(Path.Combine(DefaultLayoutPath, "Mouse", "4 LED mouse.xml"), LayoutSource.Default); // } if (device.DeviceType == RGBDeviceType.Headset) { if (device.Leds.Count == 1) - return new ArtemisLayout(Path.Combine(layoutFolder + "Headset", "Artemis 1 LED headset.xml"), LayoutSource.Default); + return new ArtemisLayout(Path.Combine(DefaultLayoutPath, "Headset", "Artemis 1 LED headset.xml")); if (device.Leds.Count == 2) - return new ArtemisLayout(Path.Combine(layoutFolder + "Headset", "Artemis 2 LED headset.xml"), LayoutSource.Default); - return new ArtemisLayout(Path.Combine(layoutFolder + "Headset", "Artemis 4 LED headset.xml"), LayoutSource.Default); + return new ArtemisLayout(Path.Combine(DefaultLayoutPath, "Headset", "Artemis 2 LED headset.xml")); + return new ArtemisLayout(Path.Combine(DefaultLayoutPath, "Headset", "Artemis 4 LED headset.xml")); } return null; @@ -206,30 +203,10 @@ public class ArtemisLayout else Image = null; } -} -/// -/// Represents a source from where a layout came -/// -public enum LayoutSource -{ - /// - /// A layout loaded from config - /// - Configured, - - /// - /// A layout loaded from the user layout folder - /// - User, - - /// - /// A layout loaded from the plugin folder - /// - Plugin, - - /// - /// A default layout loaded as a fallback option - /// - Default + /// + public override string ToString() + { + return FilePath; + } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Surface/Layout/ArtemisLedLayout.cs b/src/Artemis.Core/Models/Surface/Layout/ArtemisLedLayout.cs index 29872c4b6..55e237b5d 100644 --- a/src/Artemis.Core/Models/Surface/Layout/ArtemisLedLayout.cs +++ b/src/Artemis.Core/Models/Surface/Layout/ArtemisLedLayout.cs @@ -15,6 +15,11 @@ public class ArtemisLedLayout DeviceLayout = deviceLayout; RgbLayout = led; LayoutCustomLedData = (LayoutCustomLedData?) led.CustomData ?? new LayoutCustomLedData(); + + // Default to the first logical layout for images + LayoutCustomLedDataLogicalLayout? defaultLogicalLayout = LayoutCustomLedData.LogicalLayouts?.FirstOrDefault(); + if (defaultLogicalLayout != null) + ApplyLogicalLayout(defaultLogicalLayout); } /// @@ -54,7 +59,17 @@ public class ArtemisLedLayout .ThenBy(l => l.Name == null) .First(); + ApplyLogicalLayout(logicalLayout); + } + + private void ApplyLogicalLayout(LayoutCustomLedDataLogicalLayout logicalLayout) + { + string? layoutDirectory = Path.GetDirectoryName(DeviceLayout.FilePath); + LogicalName = logicalLayout.Name; - Image = new Uri(Path.Combine(Path.GetDirectoryName(DeviceLayout.FilePath)!, logicalLayout.Image!), UriKind.Absolute); + if (layoutDirectory != null && logicalLayout.Image != null) + Image = new Uri(Path.Combine(layoutDirectory, logicalLayout.Image!), UriKind.Absolute); + else + Image = null; } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Surface/LayoutSelection.cs b/src/Artemis.Core/Models/Surface/LayoutSelection.cs new file mode 100644 index 000000000..f64c54eb1 --- /dev/null +++ b/src/Artemis.Core/Models/Surface/LayoutSelection.cs @@ -0,0 +1,28 @@ +namespace Artemis.Core; + +/// +/// Represents a reference to a layout for a device. +/// +public class LayoutSelection : CorePropertyChanged +{ + private string? _type; + private string? _parameter; + + /// + /// Gets or sets what kind of layout reference this is. + /// + public string? Type + { + get => _type; + set => SetAndNotify(ref _type, value); + } + + /// + /// Gets or sets the parameter of the layout reference, such as a file path of workshop entry ID. + /// + public string? Parameter + { + get => _parameter; + set => SetAndNotify(ref _parameter, value); + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/DeviceProviders/DeviceProvider.cs b/src/Artemis.Core/Plugins/DeviceProviders/DeviceProvider.cs index 843710370..ee9f256a9 100644 --- a/src/Artemis.Core/Plugins/DeviceProviders/DeviceProvider.cs +++ b/src/Artemis.Core/Plugins/DeviceProviders/DeviceProvider.cs @@ -62,7 +62,7 @@ public abstract class DeviceProvider : PluginFeature device.DeviceType.ToString(), GetDeviceLayoutName(device) ); - return new ArtemisLayout(filePath, LayoutSource.Plugin); + return new ArtemisLayout(filePath); } /// @@ -79,7 +79,7 @@ public abstract class DeviceProvider : PluginFeature device.DeviceType.ToString(), GetDeviceLayoutName(device) ); - return new ArtemisLayout(filePath, LayoutSource.User); + return new ArtemisLayout(filePath); } /// diff --git a/src/Artemis.Core/Providers/CustomPathLayoutProvider.cs b/src/Artemis.Core/Providers/CustomPathLayoutProvider.cs new file mode 100644 index 000000000..bd2ba228b --- /dev/null +++ b/src/Artemis.Core/Providers/CustomPathLayoutProvider.cs @@ -0,0 +1,37 @@ +namespace Artemis.Core.Providers; + +public class CustomPathLayoutProvider : ILayoutProvider +{ + public static string LayoutType = "CustomPath"; + + /// + public ArtemisLayout? GetDeviceLayout(ArtemisDevice device) + { + if (device.LayoutSelection.Parameter == null) + return null; + return new ArtemisLayout(device.LayoutSelection.Parameter); + } + + /// + public void ApplyLayout(ArtemisDevice device, ArtemisLayout layout) + { + device.ApplyLayout(layout, device.DeviceProvider.CreateMissingLedsSupported, device.DeviceProvider.RemoveExcessiveLedsSupported); + } + + /// + public bool IsMatch(ArtemisDevice device) + { + return device.LayoutSelection.Type == LayoutType; + } + + /// + /// Configures the provided device to use this layout provider. + /// + /// The device to apply the provider to. + /// The path to the custom layout. + public void ConfigureDevice(ArtemisDevice device, string? path) + { + device.LayoutSelection.Type = LayoutType; + device.LayoutSelection.Parameter = path; + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Providers/DefaultLayoutProvider.cs b/src/Artemis.Core/Providers/DefaultLayoutProvider.cs new file mode 100644 index 000000000..8c709ab27 --- /dev/null +++ b/src/Artemis.Core/Providers/DefaultLayoutProvider.cs @@ -0,0 +1,41 @@ +namespace Artemis.Core.Providers; + +public class DefaultLayoutProvider : ILayoutProvider +{ + public static string LayoutType = "Default"; + + /// + public ArtemisLayout? GetDeviceLayout(ArtemisDevice device) + { + // Look for a layout provided by the plugin + ArtemisLayout layout = device.DeviceProvider.LoadLayout(device); + + // Finally fall back to a default layout + return layout.IsValid ? layout : ArtemisLayout.GetDefaultLayout(device); + } + + /// + public void ApplyLayout(ArtemisDevice device, ArtemisLayout layout) + { + if (layout.IsDefaultLayout) + device.ApplyLayout(layout, false, false); + else + device.ApplyLayout(layout, device.DeviceProvider.CreateMissingLedsSupported, device.DeviceProvider.RemoveExcessiveLedsSupported); + } + + /// + public bool IsMatch(ArtemisDevice device) + { + return device.LayoutSelection.Type == LayoutType; + } + + /// + /// Configures the provided device to use this layout provider. + /// + /// The device to apply the provider to. + public void ConfigureDevice(ArtemisDevice device) + { + device.LayoutSelection.Type = LayoutType; + device.LayoutSelection.Parameter = null; + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Providers/Interfaces/ILayoutProvider.cs b/src/Artemis.Core/Providers/Interfaces/ILayoutProvider.cs new file mode 100644 index 000000000..3cccfe39d --- /dev/null +++ b/src/Artemis.Core/Providers/Interfaces/ILayoutProvider.cs @@ -0,0 +1,17 @@ +namespace Artemis.Core.Providers; + +/// +/// Represents a class that can provide Artemis layouts for devices. +/// +public interface ILayoutProvider +{ + /// + /// If available, loads an Artemis layout for the provided device. + /// + /// The device to load the layout for. + /// The resulting layout if one was available; otherwise . + ArtemisLayout? GetDeviceLayout(ArtemisDevice device); + + void ApplyLayout(ArtemisDevice device, ArtemisLayout layout); + bool IsMatch(ArtemisDevice device); +} \ No newline at end of file diff --git a/src/Artemis.Core/Providers/NoneLayoutProvider.cs b/src/Artemis.Core/Providers/NoneLayoutProvider.cs new file mode 100644 index 000000000..59c13391b --- /dev/null +++ b/src/Artemis.Core/Providers/NoneLayoutProvider.cs @@ -0,0 +1,34 @@ +namespace Artemis.Core.Providers; + +public class NoneLayoutProvider : ILayoutProvider +{ + public static string LayoutType = "None"; + + /// + public ArtemisLayout? GetDeviceLayout(ArtemisDevice device) + { + return null; + } + + /// + public void ApplyLayout(ArtemisDevice device, ArtemisLayout layout) + { + device.ApplyLayout(null, false, false); + } + + /// + public bool IsMatch(ArtemisDevice device) + { + return device.LayoutSelection.Type == LayoutType; + } + + /// + /// Configures the provided device to use this layout provider. + /// + /// The device to apply the provider to. + public void ConfigureDevice(ArtemisDevice device) + { + device.LayoutSelection.Type = LayoutType; + device.LayoutSelection.Parameter = null; + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/DeviceService.cs b/src/Artemis.Core/Services/DeviceService.cs index ae43ab5ef..afc949bba 100644 --- a/src/Artemis.Core/Services/DeviceService.cs +++ b/src/Artemis.Core/Services/DeviceService.cs @@ -4,9 +4,11 @@ using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; using Artemis.Core.DeviceProviders; +using Artemis.Core.Providers; using Artemis.Core.Services.Models; using Artemis.Storage.Entities.Surface; using Artemis.Storage.Repositories.Interfaces; +using DryIoc; using RGB.NET.Core; using Serilog; @@ -18,15 +20,21 @@ internal class DeviceService : IDeviceService private readonly IPluginManagementService _pluginManagementService; private readonly IDeviceRepository _deviceRepository; private readonly Lazy _renderService; + private readonly Func> _getLayoutProviders; private readonly List _enabledDevices = new(); private readonly List _devices = new(); - public DeviceService(ILogger logger, IPluginManagementService pluginManagementService, IDeviceRepository deviceRepository, Lazy renderService) + public DeviceService(ILogger logger, + IPluginManagementService pluginManagementService, + IDeviceRepository deviceRepository, + Lazy renderService, + Func> getLayoutProviders) { _logger = logger; _pluginManagementService = pluginManagementService; _deviceRepository = deviceRepository; _renderService = renderService; + _getLayoutProviders = getLayoutProviders; EnabledDevices = new ReadOnlyCollection(_enabledDevices); Devices = new ReadOnlyCollection(_devices); @@ -157,12 +165,30 @@ internal class DeviceService : IDeviceService } /// - public void ApplyDeviceLayout(ArtemisDevice device, ArtemisLayout? layout) + public void LoadDeviceLayout(ArtemisDevice device) { - if (layout == null || layout.Source == LayoutSource.Default) - device.ApplyLayout(layout, false, false); - else - device.ApplyLayout(layout, device.DeviceProvider.CreateMissingLedsSupported, device.DeviceProvider.RemoveExcessiveLedsSupported); + ILayoutProvider? provider = _getLayoutProviders().FirstOrDefault(p => p.IsMatch(device)); + if (provider == null) + _logger.Warning("Could not find a layout provider for type {LayoutType} of device {Device}", device.LayoutSelection.Type, device); + + ArtemisLayout? layout = provider?.GetDeviceLayout(device); + if (layout != null && !layout.IsValid) + { + _logger.Warning("Got an invalid layout {Layout} from {LayoutProvider}", layout, provider!.GetType().FullName); + layout = null; + } + + try + { + if (layout == null) + device.ApplyLayout(null, false, false); + else + provider?.ApplyLayout(device, layout); + } + catch (Exception e) + { + _logger.Error(e, "Failed to apply device layout"); + } UpdateLeds(); } @@ -230,7 +256,7 @@ internal class DeviceService : IDeviceService device = new ArtemisDevice(rgbDevice, deviceProvider); } - ApplyDeviceLayout(device, device.GetBestDeviceLayout()); + LoadDeviceLayout(device); return device; } diff --git a/src/Artemis.Core/Services/Interfaces/IDeviceService.cs b/src/Artemis.Core/Services/Interfaces/IDeviceService.cs index b66f14022..51b19d937 100644 --- a/src/Artemis.Core/Services/Interfaces/IDeviceService.cs +++ b/src/Artemis.Core/Services/Interfaces/IDeviceService.cs @@ -43,11 +43,10 @@ public interface IDeviceService : IArtemisService void AutoArrangeDevices(); /// - /// Apples the provided to the provided + /// Apples the best available to the provided /// /// - /// - void ApplyDeviceLayout(ArtemisDevice device, ArtemisLayout? layout); + void LoadDeviceLayout(ArtemisDevice device); /// /// Enables the provided device diff --git a/src/Artemis.Core/Services/PluginManagementService.cs b/src/Artemis.Core/Services/PluginManagementService.cs index 964f5b0dd..8194cef44 100644 --- a/src/Artemis.Core/Services/PluginManagementService.cs +++ b/src/Artemis.Core/Services/PluginManagementService.cs @@ -31,7 +31,7 @@ internal class PluginManagementService : IPluginManagementService private readonly IPluginRepository _pluginRepository; private readonly List _plugins; private readonly IQueuedActionRepository _queuedActionRepository; - private FileSystemWatcher _hotReloadWatcher; + private FileSystemWatcher? _hotReloadWatcher; private bool _disposed; private bool _isElevated; @@ -57,7 +57,7 @@ internal class PluginManagementService : IPluginManagementService // Remove the old directory if it exists if (Directory.Exists(pluginDirectory.FullName)) - pluginDirectory.DeleteRecursively(); + pluginDirectory.Delete(true); // Extract everything in the same archive directory to the unique plugin directory Utilities.CreateAccessibleDirectory(pluginDirectory.FullName); diff --git a/src/Artemis.Core/Services/ProcessMonitoring/ProcessInfo.cs b/src/Artemis.Core/Services/ProcessMonitoring/ProcessInfo.cs index c4cc6de1b..4c71c63e0 100644 --- a/src/Artemis.Core/Services/ProcessMonitoring/ProcessInfo.cs +++ b/src/Artemis.Core/Services/ProcessMonitoring/ProcessInfo.cs @@ -1,24 +1,49 @@ namespace Artemis.Core.Services; +/// +/// This readonly struct provides information about a process. +/// public readonly struct ProcessInfo { #region Properties & Fields + /// + /// Gets the Identifier for the process. + /// public readonly int ProcessId; + + /// + /// Gets the name of the process. + /// public readonly string ProcessName; - public readonly string ImageName; //TODO DarthAffe 01.09.2023: Do we need this if we can't get it through Process.GetProcesses()? + + /// + /// Gets the Image Name of the Process. + /// + public readonly string ImageName; // TODO DarthAffe 01.09.2023: Do we need this if we can't get it through Process.GetProcesses()? + + /// + /// Gets the Executable associated with the Process. + /// public readonly string Executable; #endregion #region Constructors + /// + /// Initializes a new instance of the struct. + /// + /// The identifier for the process. + /// The name of the process. + /// The Image Name of the process. + /// The executable associated with the process. public ProcessInfo(int processId, string processName, string imageName, string executable) { - this.ProcessId = processId; - this.ProcessName = processName; - this.ImageName = imageName; - this.Executable = executable; + ProcessId = processId; + ProcessName = processName; + ImageName = imageName; + Executable = executable; } #endregion diff --git a/src/Artemis.Core/Services/RenderService.cs b/src/Artemis.Core/Services/RenderService.cs index 7dc8dd64d..26e23ca6f 100644 --- a/src/Artemis.Core/Services/RenderService.cs +++ b/src/Artemis.Core/Services/RenderService.cs @@ -75,8 +75,8 @@ internal class RenderService : IRenderService, IRenderer, IDisposable _frameStopWatch.Restart(); try { - OnFrameRendering(new FrameRenderingEventArgs(canvas, delta, _surfaceManager.Surface)); _coreRenderer.Render(canvas, delta); + OnFrameRendering(new FrameRenderingEventArgs(canvas, delta, _surfaceManager.Surface)); } catch (Exception e) { diff --git a/src/Artemis.Storage/Artemis.Storage.csproj b/src/Artemis.Storage/Artemis.Storage.csproj index 8baf3a9c5..679f1c3c7 100644 --- a/src/Artemis.Storage/Artemis.Storage.csproj +++ b/src/Artemis.Storage/Artemis.Storage.csproj @@ -1,6 +1,4 @@  - - net7.0 false @@ -9,6 +7,6 @@ - + \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Surface/DeviceEntity.cs b/src/Artemis.Storage/Entities/Surface/DeviceEntity.cs index 31dd8ac3d..b19e38c03 100644 --- a/src/Artemis.Storage/Entities/Surface/DeviceEntity.cs +++ b/src/Artemis.Storage/Entities/Surface/DeviceEntity.cs @@ -22,11 +22,11 @@ public class DeviceEntity public float GreenScale { get; set; } public float BlueScale { get; set; } public bool IsEnabled { get; set; } - - public bool DisableDefaultLayout { get; set; } + public int PhysicalLayout { get; set; } public string LogicalLayout { get; set; } - public string CustomLayoutPath { get; set; } + public string LayoutType { get; set; } + public string LayoutParameter { get; set; } public List InputIdentifiers { get; set; } public List InputMappings { get; set; } diff --git a/src/Artemis.Storage/Entities/Workshop/EntryEntity.cs b/src/Artemis.Storage/Entities/Workshop/EntryEntity.cs index 86bff6dcd..e2acf7922 100644 --- a/src/Artemis.Storage/Entities/Workshop/EntryEntity.cs +++ b/src/Artemis.Storage/Entities/Workshop/EntryEntity.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace Artemis.Storage.Entities.Workshop; @@ -16,6 +17,6 @@ public class EntryEntity public long ReleaseId { get; set; } public string ReleaseVersion { get; set; } public DateTimeOffset InstalledAt { get; set; } - - public string LocalReference { get; set; } + + public Dictionary Metadata { get; set; } } \ No newline at end of file diff --git a/src/Artemis.Storage/Migrations/M0023LayoutProviders.cs b/src/Artemis.Storage/Migrations/M0023LayoutProviders.cs new file mode 100644 index 000000000..5914b01c2 --- /dev/null +++ b/src/Artemis.Storage/Migrations/M0023LayoutProviders.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using Artemis.Storage.Migrations.Interfaces; +using LiteDB; + +namespace Artemis.Storage.Migrations; + +public class M0023LayoutProviders : IStorageMigration +{ + public int UserVersion => 23; + + public void Apply(LiteRepository repository) + { + ILiteCollection deviceEntities = repository.Database.GetCollection("DeviceEntity"); + List toUpdate = new(); + + foreach (BsonDocument bsonDocument in deviceEntities.FindAll()) + { + if (bsonDocument.TryGetValue("CustomLayoutPath", out BsonValue customLayoutPath) && customLayoutPath.IsString && !string.IsNullOrEmpty(customLayoutPath.AsString)) + { + bsonDocument.Add("LayoutType", new BsonValue("CustomPath")); + bsonDocument.Add("LayoutParameter", new BsonValue(customLayoutPath.AsString)); + } + else if (bsonDocument.TryGetValue("DisableDefaultLayout", out BsonValue disableDefaultLayout) && disableDefaultLayout.AsBoolean) + bsonDocument.Add("LayoutType", new BsonValue("None")); + else + bsonDocument.Add("LayoutType", new BsonValue("Default")); + + bsonDocument.Remove("CustomLayoutPath"); + bsonDocument.Remove("DisableDefaultLayout"); + toUpdate.Add(bsonDocument); + } + + deviceEntities.Update(toUpdate); + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Linux/Artemis.UI.Linux.csproj b/src/Artemis.UI.Linux/Artemis.UI.Linux.csproj index 6bd6f0ef5..c346e5027 100644 --- a/src/Artemis.UI.Linux/Artemis.UI.Linux.csproj +++ b/src/Artemis.UI.Linux/Artemis.UI.Linux.csproj @@ -1,6 +1,4 @@ - - WinExe net7.0 @@ -17,14 +15,6 @@ - - - - - - - - diff --git a/src/Artemis.UI.MacOS/Artemis.UI.MacOS.csproj b/src/Artemis.UI.MacOS/Artemis.UI.MacOS.csproj index 464d61801..b5cc49ac7 100644 --- a/src/Artemis.UI.MacOS/Artemis.UI.MacOS.csproj +++ b/src/Artemis.UI.MacOS/Artemis.UI.MacOS.csproj @@ -1,6 +1,4 @@  - - WinExe net7.0 @@ -16,14 +14,6 @@ - - - - - - - - diff --git a/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj b/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj index 1f60cc8ab..1be9809f0 100644 --- a/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj +++ b/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj @@ -1,6 +1,4 @@  - - Library net7.0 @@ -12,19 +10,17 @@ - + - - - - - - - - + + + + + + + + - - diff --git a/src/Artemis.UI.Shared/Behaviors/LostFocusTextBoxBindingBehavior.cs b/src/Artemis.UI.Shared/Behaviors/LostFocusTextBoxBindingBehavior.cs index 4c095741c..f6a8f6ad1 100644 --- a/src/Artemis.UI.Shared/Behaviors/LostFocusTextBoxBindingBehavior.cs +++ b/src/Artemis.UI.Shared/Behaviors/LostFocusTextBoxBindingBehavior.cs @@ -15,7 +15,7 @@ public class LostFocusTextBoxBindingBehavior : Behavior /// /// Gets or sets the value of the binding. /// - public static readonly StyledProperty TextProperty = AvaloniaProperty.Register( + public static readonly StyledProperty TextProperty = AvaloniaProperty.Register( "Text", defaultBindingMode: BindingMode.TwoWay); static LostFocusTextBoxBindingBehavior() @@ -26,7 +26,7 @@ public class LostFocusTextBoxBindingBehavior : Behavior /// /// Gets or sets the value of the binding. /// - public string Text + public string? Text { get => GetValue(TextProperty); set => SetValue(TextProperty, value); diff --git a/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs b/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs index 46db27445..9eae5804d 100644 --- a/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs +++ b/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs @@ -1,12 +1,11 @@ using System; using System.Collections.Generic; -using System.ComponentModel; -using System.IO; using System.Linq; using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Shared.Events; +using Artemis.UI.Shared.Extensions; using Avalonia; using Avalonia.Controls; using Avalonia.Input; @@ -36,7 +35,7 @@ public class DeviceVisualizer : Control private ArtemisDevice? _oldDevice; private bool _loading; private Color[] _previousState = Array.Empty(); - + /// public DeviceVisualizer() { @@ -69,11 +68,7 @@ public class DeviceVisualizer : Control // Render device and LED images if (_deviceImage != null) - drawingContext.DrawImage( - _deviceImage, - new Rect(_deviceImage.Size), - new Rect(0, 0, Device.RgbDevice.ActualSize.Width, Device.RgbDevice.ActualSize.Height) - ); + drawingContext.DrawImage(_deviceImage, new Rect(_deviceImage.Size), new Rect(0, 0, Device.RgbDevice.ActualSize.Width, Device.RgbDevice.ActualSize.Height)); if (!ShowColors) return; @@ -163,7 +158,7 @@ public class DeviceVisualizer : Control { if (Device == null || float.IsNaN(Device.RgbDevice.ActualSize.Width) || float.IsNaN(Device.RgbDevice.ActualSize.Height)) return new Rect(); - + Rect deviceRect = new(0, 0, Device.RgbDevice.ActualSize.Width, Device.RgbDevice.ActualSize.Height); Geometry geometry = new RectangleGeometry(deviceRect); geometry.Transform = new RotateTransform(Device.Rotation); @@ -305,7 +300,7 @@ public class DeviceVisualizer : Control { _deviceImage = await Task.Run(() => GetDeviceImage(device)); } - catch (Exception e) + catch (Exception) { // ignored } @@ -316,34 +311,15 @@ public class DeviceVisualizer : Control private RenderTargetBitmap? GetDeviceImage(ArtemisDevice device) { - string? path = device.Layout?.Image?.LocalPath; - if (path == null) + ArtemisLayout? layout = device.Layout; + if (layout == null) return null; - - if (BitmapCache.TryGetValue(path, out RenderTargetBitmap? existingBitmap)) + + if (BitmapCache.TryGetValue(layout.FilePath, out RenderTargetBitmap? existingBitmap)) return existingBitmap; - if (!File.Exists(path)) - { - BitmapCache[path] = null; - return null; - } - - // Create a bitmap that'll be used to render the device and LED images just once - // Render 4 times the actual size of the device to make sure things look sharp when zoomed in - 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(path); - using Bitmap scaledBitmap = bitmap.CreateScaledBitmap(renderTargetBitmap.PixelSize); - - context.DrawImage(scaledBitmap, new Rect(scaledBitmap.Size)); - lock (_deviceVisualizerLeds) - { - foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds) - deviceVisualizerLed.DrawBitmap(context, 2 * device.Scale); - } - - // BitmapCache[path] = renderTargetBitmap; + + RenderTargetBitmap renderTargetBitmap = layout.RenderLayout(false); + BitmapCache[layout.FilePath] = renderTargetBitmap; return renderTargetBitmap; } diff --git a/src/Artemis.UI.Shared/Controls/DeviceVisualizerLed.cs b/src/Artemis.UI.Shared/Controls/DeviceVisualizerLed.cs index 576dd836d..0001c17f8 100644 --- a/src/Artemis.UI.Shared/Controls/DeviceVisualizerLed.cs +++ b/src/Artemis.UI.Shared/Controls/DeviceVisualizerLed.cs @@ -1,9 +1,7 @@ using System; -using System.IO; using Artemis.Core; using Avalonia; using Avalonia.Media; -using Avalonia.Media.Imaging; using RGB.NET.Core; using Color = Avalonia.Media.Color; using Point = Avalonia.Point; @@ -30,27 +28,7 @@ internal class DeviceVisualizerLed public ArtemisLed Led { get; } public Geometry? DisplayGeometry { get; private set; } - - public void DrawBitmap(DrawingContext drawingContext, double scale) - { - if (Led.Layout?.Image == null || !File.Exists(Led.Layout.Image.LocalPath)) - return; - - try - { - using Bitmap bitmap = new(Led.Layout.Image.LocalPath); - using Bitmap scaledBitmap = bitmap.CreateScaledBitmap(new PixelSize((Led.RgbLed.Size.Width * scale).RoundToInt(), (Led.RgbLed.Size.Height * scale).RoundToInt())); - drawingContext.DrawImage( - scaledBitmap, - new Rect(Led.RgbLed.Location.X * scale, Led.RgbLed.Location.Y * scale, scaledBitmap.Size.Width, scaledBitmap.Size.Height) - ); - } - catch - { - // ignored - } - } - + public void RenderGeometry(DrawingContext drawingContext) { if (DisplayGeometry == null) diff --git a/src/Artemis.UI.Shared/Controls/Pagination/Pagination.cs b/src/Artemis.UI.Shared/Controls/Pagination/Pagination.cs index 35b4fa96e..5a193f958 100644 --- a/src/Artemis.UI.Shared/Controls/Pagination/Pagination.cs +++ b/src/Artemis.UI.Shared/Controls/Pagination/Pagination.cs @@ -19,32 +19,32 @@ namespace Artemis.UI.Shared.Pagination; [TemplatePart("PART_PagesView", typeof(StackPanel))] public partial class Pagination : TemplatedControl { + private Button? _previousButton; + private Button? _nextButton; + private StackPanel? _pagesView; + /// public Pagination() { PropertyChanged += OnPropertyChanged; } - public Button? PreviousButton { get; set; } - public Button? NextButton { get; set; } - public StackPanel? PagesView { get; set; } - /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { - if (PreviousButton != null) - PreviousButton.Click -= PreviousButtonOnClick; - if (NextButton != null) - NextButton.Click -= NextButtonOnClick; + if (_previousButton != null) + _previousButton.Click -= PreviousButtonOnClick; + if (_nextButton != null) + _nextButton.Click -= NextButtonOnClick; - PreviousButton = e.NameScope.Find