diff --git a/.gitignore b/.gitignore index 10351521f..f25a67c8b 100644 --- a/.gitignore +++ b/.gitignore @@ -196,3 +196,5 @@ FakesAssemblies/ src/Artemis\.Storage/Storage\.db !src/Artemis.UI/screens/Settings/Debug docfx/docfx_project/_site/ + +src/.idea/ diff --git a/src/.idea/.idea.Artemis/.idea/.gitignore b/src/.idea/.idea.Artemis/.idea/.gitignore deleted file mode 100644 index 0f04753a2..000000000 --- a/src/.idea/.idea.Artemis/.idea/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Rider ignored files -/modules.xml -/contentModel.xml -/.idea.Artemis.iml -/projectSettingsUpdater.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/src/.idea/.idea.Artemis/.idea/.name b/src/.idea/.idea.Artemis/.idea/.name deleted file mode 100644 index cc79fd55c..000000000 --- a/src/.idea/.idea.Artemis/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -Artemis \ No newline at end of file diff --git a/src/.idea/.idea.Artemis/.idea/avalonia.xml b/src/.idea/.idea.Artemis/.idea/avalonia.xml deleted file mode 100644 index c9701a626..000000000 --- a/src/.idea/.idea.Artemis/.idea/avalonia.xml +++ /dev/null @@ -1,102 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/.idea/.idea.Artemis/.idea/encodings.xml b/src/.idea/.idea.Artemis/.idea/encodings.xml deleted file mode 100644 index df87cf951..000000000 --- a/src/.idea/.idea.Artemis/.idea/encodings.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/.idea/.idea.Artemis/.idea/indexLayout.xml b/src/.idea/.idea.Artemis/.idea/indexLayout.xml deleted file mode 100644 index 7b08163ce..000000000 --- a/src/.idea/.idea.Artemis/.idea/indexLayout.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/src/.idea/.idea.Artemis/.idea/misc.xml b/src/.idea/.idea.Artemis/.idea/misc.xml deleted file mode 100644 index 283b9b4d4..000000000 --- a/src/.idea/.idea.Artemis/.idea/misc.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/.idea/.idea.Artemis/.idea/vcs.xml b/src/.idea/.idea.Artemis/.idea/vcs.xml deleted file mode 100644 index 6c0b86358..000000000 --- a/src/.idea/.idea.Artemis/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Services/Builders/ContentDialogBuilder.cs b/src/Artemis.UI.Shared/Services/Builders/ContentDialogBuilder.cs index b0a5c49d1..6825c2729 100644 --- a/src/Artemis.UI.Shared/Services/Builders/ContentDialogBuilder.cs +++ b/src/Artemis.UI.Shared/Services/Builders/ContentDialogBuilder.cs @@ -142,6 +142,16 @@ namespace Artemis.UI.Shared.Services.Builders return this; } + /// + /// Changes the dialog to take the full height of the window it's being hosted in. + /// + /// The builder that can be used to further build the dialog. + public ContentDialogBuilder WithFullSize() + { + _contentDialog.FullSizeDesired = true; + return this; + } + /// /// Asynchronously shows the content dialog. /// diff --git a/src/Artemis.UI/Assets/Images/PhysicalLayouts/abnt.png b/src/Artemis.UI/Assets/Images/PhysicalLayouts/abnt.png new file mode 100644 index 000000000..dc7593429 Binary files /dev/null and b/src/Artemis.UI/Assets/Images/PhysicalLayouts/abnt.png differ diff --git a/src/Artemis.UI/Assets/Images/PhysicalLayouts/ansi.png b/src/Artemis.UI/Assets/Images/PhysicalLayouts/ansi.png new file mode 100644 index 000000000..940a8f233 Binary files /dev/null and b/src/Artemis.UI/Assets/Images/PhysicalLayouts/ansi.png differ diff --git a/src/Artemis.UI/Assets/Images/PhysicalLayouts/iso.png b/src/Artemis.UI/Assets/Images/PhysicalLayouts/iso.png new file mode 100644 index 000000000..ee4272e81 Binary files /dev/null and b/src/Artemis.UI/Assets/Images/PhysicalLayouts/iso.png differ diff --git a/src/Artemis.UI/Assets/Images/PhysicalLayouts/jis.png b/src/Artemis.UI/Assets/Images/PhysicalLayouts/jis.png new file mode 100644 index 000000000..d872ddab3 Binary files /dev/null and b/src/Artemis.UI/Assets/Images/PhysicalLayouts/jis.png differ diff --git a/src/Artemis.UI/Assets/Images/PhysicalLayouts/ks.png b/src/Artemis.UI/Assets/Images/PhysicalLayouts/ks.png new file mode 100644 index 000000000..898f8a85e Binary files /dev/null and b/src/Artemis.UI/Assets/Images/PhysicalLayouts/ks.png differ diff --git a/src/Artemis.UI/Screens/Device/Tabs/DeviceLogicalLayoutDialogView.axaml b/src/Artemis.UI/Screens/Device/Tabs/DeviceLogicalLayoutDialogView.axaml new file mode 100644 index 000000000..28514fcdc --- /dev/null +++ b/src/Artemis.UI/Screens/Device/Tabs/DeviceLogicalLayoutDialogView.axaml @@ -0,0 +1,39 @@ + + + Artemis couldn't automatically determine the logical layout of your + + + + While not as important as the physical layout, setting the correct logical layout will allow Artemis to show the right keycaps (if a matching layout file is present) + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Device/Tabs/DeviceLogicalLayoutDialogView.axaml.cs b/src/Artemis.UI/Screens/Device/Tabs/DeviceLogicalLayoutDialogView.axaml.cs new file mode 100644 index 000000000..d3ebaf1ae --- /dev/null +++ b/src/Artemis.UI/Screens/Device/Tabs/DeviceLogicalLayoutDialogView.axaml.cs @@ -0,0 +1,47 @@ +using System; +using System.Globalization; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; +using Avalonia.Threading; +using ReactiveUI; + +namespace Artemis.UI.Screens.Device; + +public partial class DeviceLogicalLayoutDialogView : ReactiveUserControl +{ + private readonly AutoCompleteBox _autoCompleteBox; + + public DeviceLogicalLayoutDialogView() + { + InitializeComponent(); + + _autoCompleteBox = this.Get("RegionsAutoCompleteBox"); + _autoCompleteBox.ItemFilter += SearchRegions; + + Dispatcher.UIThread.InvokeAsync(DelayedAutoFocus); + } + + private async Task DelayedAutoFocus() + { + await Task.Delay(200); + _autoCompleteBox.Focus(); + _autoCompleteBox.PopulateComplete(); + } + + private bool SearchRegions(string search, object item) + { + if (item is not RegionInfo regionInfo) + return false; + + return regionInfo.EnglishName.Contains(search, StringComparison.OrdinalIgnoreCase) || + regionInfo.NativeName.Contains(search, StringComparison.OrdinalIgnoreCase) || + regionInfo.TwoLetterISORegionName.Contains(search, StringComparison.OrdinalIgnoreCase); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Device/Tabs/DeviceLogicalLayoutDialogViewModel.cs b/src/Artemis.UI/Screens/Device/Tabs/DeviceLogicalLayoutDialogViewModel.cs new file mode 100644 index 000000000..936b3968c --- /dev/null +++ b/src/Artemis.UI/Screens/Device/Tabs/DeviceLogicalLayoutDialogViewModel.cs @@ -0,0 +1,72 @@ +using System.Collections.ObjectModel; +using System.Globalization; +using System.Linq; +using System.Reactive; +using System.Reactive.Linq; +using System.Threading.Tasks; +using Artemis.Core; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Services; +using FluentAvalonia.UI.Controls; +using ReactiveUI; + +namespace Artemis.UI.Screens.Device; + +public class DeviceLogicalLayoutDialogViewModel : ContentDialogViewModelBase +{ + private const int LOCALE_NEUTRAL = 0x0000; + private const int LOCALE_CUSTOM_DEFAULT = 0x0c00; + private const int LOCALE_INVARIANT = 0x007F; + + private RegionInfo? _selectedRegion; + + public DeviceLogicalLayoutDialogViewModel(ArtemisDevice device) + { + Device = device; + ApplyLogicalLayout = ReactiveCommand.Create(ExecuteApplyLogicalLayout, this.WhenAnyValue(vm => vm.SelectedRegion).Select(r => r != null)); + Regions = new ObservableCollection(CultureInfo.GetCultures(CultureTypes.SpecificCultures) + .Where(c => c.LCID != LOCALE_INVARIANT && c.LCID != LOCALE_NEUTRAL && c.LCID != LOCALE_CUSTOM_DEFAULT) + .Select(c => new RegionInfo(c.LCID)) + .GroupBy(r => r.EnglishName) + .Select(g => g.First()) + .OrderBy(r => r.EnglishName)); + + // Default to US/international + SelectedRegion = Regions.FirstOrDefault(r => r.TwoLetterISORegionName == "US"); + } + + public ArtemisDevice Device { get; } + public ReactiveCommand ApplyLogicalLayout { get; } + public ObservableCollection Regions { get; } + public bool Applied { get; set; } + + public RegionInfo? SelectedRegion + { + get => _selectedRegion; + set => RaiseAndSetIfChanged(ref _selectedRegion, value); + } + + private void ExecuteApplyLogicalLayout() + { + if (SelectedRegion == null) + return; + + Device.LogicalLayout = SelectedRegion.TwoLetterISORegionName; + Applied = true; + ContentDialog?.Hide(ContentDialogResult.Primary); + } + + public static async Task SelectLogicalLayout(IWindowService windowService, ArtemisDevice device) + { + await windowService.CreateContentDialog() + .WithTitle("Select logical layout") + .WithViewModel(out DeviceLogicalLayoutDialogViewModel vm, ("device", device)) + .WithCloseButtonText("Cancel") + .WithDefaultButton(Shared.Services.Builders.ContentDialogButton.Primary) + .HavingPrimaryButton(b => b.WithText("Select").WithCommand(vm.ApplyLogicalLayout)) + .ShowAsync(); + + return vm.Applied; + } + +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Device/Tabs/DevicePhysicalLayoutDialogView.axaml b/src/Artemis.UI/Screens/Device/Tabs/DevicePhysicalLayoutDialogView.axaml new file mode 100644 index 000000000..484a95b26 --- /dev/null +++ b/src/Artemis.UI/Screens/Device/Tabs/DevicePhysicalLayoutDialogView.axaml @@ -0,0 +1,104 @@ + + + + + Artemis couldn't automatically determine the physical layout of your + + + In order for Artemis to know which keys are on your keyboard and where they're located, select the matching layout below. + P.S. Don't worry about missing special keys like num keys/function keys or macro keys, they aren't important here. + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Device/Tabs/DevicePhysicalLayoutDialogView.axaml.cs b/src/Artemis.UI/Screens/Device/Tabs/DevicePhysicalLayoutDialogView.axaml.cs new file mode 100644 index 000000000..d2314c958 --- /dev/null +++ b/src/Artemis.UI/Screens/Device/Tabs/DevicePhysicalLayoutDialogView.axaml.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.Device; + +public partial class DevicePhysicalLayoutDialogView : ReactiveUserControl +{ + public DevicePhysicalLayoutDialogView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Device/Tabs/DevicePhysicalLayoutDialogViewModel.cs b/src/Artemis.UI/Screens/Device/Tabs/DevicePhysicalLayoutDialogViewModel.cs new file mode 100644 index 000000000..337277ed0 --- /dev/null +++ b/src/Artemis.UI/Screens/Device/Tabs/DevicePhysicalLayoutDialogViewModel.cs @@ -0,0 +1,41 @@ +using System; +using System.Reactive; +using System.Threading.Tasks; +using Artemis.Core; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Services; +using FluentAvalonia.UI.Controls; +using ReactiveUI; + +namespace Artemis.UI.Screens.Device; + +public class DevicePhysicalLayoutDialogViewModel : ContentDialogViewModelBase +{ + public DevicePhysicalLayoutDialogViewModel(ArtemisDevice device) + { + Device = device; + ApplyPhysicalLayout = ReactiveCommand.Create(ExecuteApplyPhysicalLayout); + } + + public ArtemisDevice Device { get; } + public ReactiveCommand ApplyPhysicalLayout { get; } + public bool Applied { get; set; } + + private void ExecuteApplyPhysicalLayout(string physicalLayout) + { + Device.PhysicalLayout = Enum.Parse(physicalLayout); + Applied = true; + ContentDialog?.Hide(ContentDialogResult.Primary); + } + + public static async Task SelectPhysicalLayout(IWindowService windowService, ArtemisDevice device) + { + await windowService.CreateContentDialog() + .WithTitle("Select physical layout") + .WithViewModel(out DevicePhysicalLayoutDialogViewModel vm, ("device", device)) + .WithCloseButtonText("Cancel") + .ShowAsync(); + + return vm.Applied; + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Device/Tabs/DevicePropertiesTabView.axaml b/src/Artemis.UI/Screens/Device/Tabs/DevicePropertiesTabView.axaml index 9ab42b07b..92cc7395e 100644 --- a/src/Artemis.UI/Screens/Device/Tabs/DevicePropertiesTabView.axaml +++ b/src/Artemis.UI/Screens/Device/Tabs/DevicePropertiesTabView.axaml @@ -7,13 +7,11 @@ xmlns:converters="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared" xmlns:device="clr-namespace:Artemis.UI.Screens.Device" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="1200" - x:Class="Artemis.UI.Screens.Device.DevicePropertiesTabView"> + x:Class="Artemis.UI.Screens.Device.DevicePropertiesTabView" + x:DataType="device:DevicePropertiesTabViewModel"> - - - @@ -30,19 +28,19 @@ You can hover over a category for a more detailed description. - - - - @@ -58,7 +56,7 @@ Grid.Column="1" Margin="10 0" VerticalAlignment="Center" - Value="{Binding X}" /> + Value="{CompiledBinding X}" /> mm Y-coordinate @@ -66,7 +64,7 @@ Grid.Column="1" Margin="10 0" VerticalAlignment="Center" - Value="{Binding Y}" /> + Value="{CompiledBinding Y}" /> mm Scale @@ -74,7 +72,7 @@ Grid.Column="1" Margin="10 0" VerticalAlignment="Center" - Value="{Binding Scale}" /> + Value="{CompiledBinding Scale}" /> times Rotation @@ -82,7 +80,7 @@ Grid.Column="1" Margin="10 0" VerticalAlignment="Center" - Value="{Binding Rotation}" /> + Value="{CompiledBinding Rotation}" /> deg @@ -100,12 +98,12 @@ @@ -187,7 +185,10 @@ - diff --git a/src/Artemis.UI/Screens/Device/Tabs/DevicePropertiesTabViewModel.cs b/src/Artemis.UI/Screens/Device/Tabs/DevicePropertiesTabViewModel.cs index af45e942d..1d96e4e90 100644 --- a/src/Artemis.UI/Screens/Device/Tabs/DevicePropertiesTabViewModel.cs +++ b/src/Artemis.UI/Screens/Device/Tabs/DevicePropertiesTabViewModel.cs @@ -33,12 +33,6 @@ namespace Artemis.UI.Screens.Device private int _x; private int _y; -#pragma warning disable CS8618 // Design-time constructor - public DevicePropertiesTabViewModel() - { - } -#pragma warning restore CS8618 - public DevicePropertiesTabViewModel(ArtemisDevice device, ICoreService coreService, IRgbService rgbService, IWindowService windowService, INotificationService notificationService) { _coreService = coreService; @@ -167,6 +161,8 @@ namespace Artemis.UI.Screens.Device set => SetCategory(DeviceCategory.Peripherals, value); } + public bool RequiresManualSetup => !Device.DeviceProvider.CanDetectPhysicalLayout || !Device.DeviceProvider.CanDetectLogicalLayout; + public void ApplyScaling() { Device.RedScale = RedScale / 100f; @@ -201,12 +197,18 @@ namespace Artemis.UI.Screens.Device } } - public async Task SelectPhysicalLayout() + public async Task RestartSetup() { - await _windowService.CreateContentDialog() - .WithTitle("Select layout") - .WithViewModel(("device", Device)) - .ShowAsync(); + if (!RequiresManualSetup) + return; + if (!Device.DeviceProvider.CanDetectPhysicalLayout && !await DevicePhysicalLayoutDialogViewModel.SelectPhysicalLayout(_windowService, Device)) + return; + if (!Device.DeviceProvider.CanDetectLogicalLayout && !await DeviceLogicalLayoutDialogViewModel.SelectLogicalLayout(_windowService, Device)) + return; + + await Task.Delay(400); + _rgbService.SaveDevice(Device); + _rgbService.ApplyBestDeviceLayout(Device); } public async Task Apply() diff --git a/src/Artemis.UI/Screens/Device/Tabs/InputMappingsTabViewModel.cs b/src/Artemis.UI/Screens/Device/Tabs/InputMappingsTabViewModel.cs index 83aa460aa..a76dc163b 100644 --- a/src/Artemis.UI/Screens/Device/Tabs/InputMappingsTabViewModel.cs +++ b/src/Artemis.UI/Screens/Device/Tabs/InputMappingsTabViewModel.cs @@ -47,7 +47,6 @@ namespace Artemis.UI.Screens.Device }); } - public ArtemisDevice Device { get; } public ArtemisLed? SelectedLed diff --git a/src/Artemis.UI/Services/DeviceLayoutService.cs b/src/Artemis.UI/Services/DeviceLayoutService.cs new file mode 100644 index 000000000..7c25fba57 --- /dev/null +++ b/src/Artemis.UI/Services/DeviceLayoutService.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Artemis.Core; +using Artemis.Core.Services; +using Artemis.UI.Screens.Device; +using Artemis.UI.Services.Interfaces; +using Artemis.UI.Shared.Services; +using Artemis.UI.Shared.Services.Builders; +using Artemis.UI.Shared.Services.MainWindow; +using Avalonia.Threading; +using RGB.NET.Core; +using KeyboardLayoutType = Artemis.Core.KeyboardLayoutType; + +namespace Artemis.UI.Services; + +public class DeviceLayoutService : IDeviceLayoutService +{ + private readonly List _ignoredDevices; + private readonly IMainWindowService _mainWindowService; + private readonly IRgbService _rgbService; + private readonly IWindowService _windowService; + + public DeviceLayoutService(IRgbService rgbService, IMainWindowService mainWindowService, IWindowService windowService) + { + _rgbService = rgbService; + _mainWindowService = mainWindowService; + _windowService = windowService; + _ignoredDevices = new List(); + + rgbService.DeviceAdded += RgbServiceOnDeviceAdded; + mainWindowService.MainWindowOpened += WindowServiceOnMainWindowOpened; + } + + private async Task RequestLayoutInput(ArtemisDevice artemisDevice) + { + bool configure = await _windowService.ShowConfirmContentDialog( + "Device requires layout info", + $"Artemis could not detect the layout of your {artemisDevice.RgbDevice.DeviceInfo.DeviceName}. Please configure it manually", + "Configure", + "Ignore for now" + ); + + if (!configure) + { + _ignoredDevices.Add(artemisDevice); + return; + } + + if (!artemisDevice.DeviceProvider.CanDetectPhysicalLayout && !await DevicePhysicalLayoutDialogViewModel.SelectPhysicalLayout(_windowService, artemisDevice)) + { + _ignoredDevices.Add(artemisDevice); + return; + } + + if (!artemisDevice.DeviceProvider.CanDetectLogicalLayout && !await DeviceLogicalLayoutDialogViewModel.SelectLogicalLayout(_windowService, artemisDevice)) + { + _ignoredDevices.Add(artemisDevice); + return; + } + + await Task.Delay(400); + _rgbService.SaveDevice(artemisDevice); + _rgbService.ApplyBestDeviceLayout(artemisDevice); + } + + private bool DeviceNeedsLayout(ArtemisDevice d) + { + return d.DeviceType == RGBDeviceType.Keyboard && + (d.LogicalLayout == null || d.PhysicalLayout == KeyboardLayoutType.Unknown) && + (!d.DeviceProvider.CanDetectLogicalLayout || !d.DeviceProvider.CanDetectPhysicalLayout); + } + + private async void WindowServiceOnMainWindowOpened(object? sender, EventArgs e) + { + List devices = _rgbService.Devices.Where(device => DeviceNeedsLayout(device) && !_ignoredDevices.Contains(device)).ToList(); + await Dispatcher.UIThread.InvokeAsync(async () => + { + foreach (ArtemisDevice artemisDevice in devices) + await RequestLayoutInput(artemisDevice); + }); + } + + private async void RgbServiceOnDeviceAdded(object? sender, DeviceEventArgs e) + { + if (_ignoredDevices.Contains(e.Device) || !DeviceNeedsLayout(e.Device) || !_mainWindowService.IsMainWindowOpen) + return; + + await Dispatcher.UIThread.InvokeAsync(async () => await RequestLayoutInput(e.Device)); + } +} + +public interface IDeviceLayoutService : IArtemisUIService +{ +} \ No newline at end of file diff --git a/src/Artemis.UI/Services/RegistrationService.cs b/src/Artemis.UI/Services/RegistrationService.cs index 8bed959bd..b89412e50 100644 --- a/src/Artemis.UI/Services/RegistrationService.cs +++ b/src/Artemis.UI/Services/RegistrationService.cs @@ -33,6 +33,7 @@ public class RegistrationService : IRegistrationService IProfileEditorService profileEditorService, INodeService nodeService, IDataModelUIService dataModelUIService, + IDeviceLayoutService deviceLayoutService, // here to make sure it is instantiated IEnumerable toolViewModels) { _kernel = kernel;