diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..820f11d --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,39 @@ +name: CI + +on: + push: + pull_request: + workflow_dispatch: + +jobs: + build: + name: Build + env: + version: 1.0.0.0 + strategy: + matrix: + os: [windows-latest, ubuntu-latest] + runtime: [win-x64, linux-x64] + exclude: + - os: windows-latest + runtime: linux-x64 + - os: ubuntu-latest + runtime: win-x64 + runs-on: ${{ matrix.os }} + steps: + - name: Checkout + uses: actions/checkout@v4.1.7 + + - name: Setup .NET + uses: actions/setup-dotnet@v4.0.0 + with: + dotnet-version: '8.0.x' + + - name: Publish 'MSI Keyboard Illuminator' + run: dotnet publish 'MSI.Keyboard.Illuminator/MSI.Keyboard.Illuminator.csproj' -o 'binaries' -c 'Release' -r '${{ matrix.runtime }}' -v 'normal' -p:Version=${{ env.version }} + + - name: Upload artifacts + uses: actions/upload-artifact@v4.3.3 + with: + name: MSI.Keyboard.Illuminator_${{ matrix.runtime }}_${{ env.version }} + path: binaries/ diff --git a/.gitignore b/.gitignore index 259148f..834faf6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,32 +1,10 @@ -# Prerequisites -*.d - -# Compiled Object files -*.slo -*.lo -*.o -*.obj - -# Precompiled Headers -*.gch -*.pch - -# Compiled Dynamic libraries -*.so -*.dylib -*.dll - -# Fortran module files -*.mod -*.smod - -# Compiled Static libraries -*.lai -*.la -*.a -*.lib - -# Executables -*.exe -*.out -*.app +# C# +*.suo +*.user +.vs/ +/MSI.Keyboard.Illuminator/bin/ +/MSI.Keyboard.Illuminator/obj/ + +# Additional +/binaries/ +*appsettings.xml \ No newline at end of file diff --git a/MSI.Keyboard.Illuminator.sln b/MSI.Keyboard.Illuminator.sln new file mode 100644 index 0000000..94a441c --- /dev/null +++ b/MSI.Keyboard.Illuminator.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.10.35013.160 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MSI.Keyboard.Illuminator", "MSI.Keyboard.Illuminator\MSI.Keyboard.Illuminator.csproj", "{A3D78733-DFF8-4CB0-9F97-559342929D10}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A3D78733-DFF8-4CB0-9F97-559342929D10}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A3D78733-DFF8-4CB0-9F97-559342929D10}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A3D78733-DFF8-4CB0-9F97-559342929D10}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A3D78733-DFF8-4CB0-9F97-559342929D10}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {32C97D6A-0418-44A3-8579-2E9F12DC261B} + EndGlobalSection +EndGlobal diff --git a/MSI.Keyboard.Illuminator/App.axaml b/MSI.Keyboard.Illuminator/App.axaml new file mode 100644 index 0000000..bbb41bd --- /dev/null +++ b/MSI.Keyboard.Illuminator/App.axaml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MSI.Keyboard.Illuminator/App.axaml.cs b/MSI.Keyboard.Illuminator/App.axaml.cs new file mode 100644 index 0000000..a4a7213 --- /dev/null +++ b/MSI.Keyboard.Illuminator/App.axaml.cs @@ -0,0 +1,116 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; + +using MSI.Keyboard.Illuminator.Helpers; +using MSI.Keyboard.Illuminator.Providers; +using MSI.Keyboard.Illuminator.Services; +using MSI.Keyboard.Illuminator.ViewModels; + +using System; +using System.CommandLine; +using System.IO; + +namespace MSI.Keyboard.Illuminator; + +public partial class App : Application +{ + protected IAppSettingsManager appSettingsManager; + + protected IKeyboardService keyboardService; + + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime application) + { + application.ShutdownMode = ShutdownMode.OnExplicitShutdown; + + keyboardService = GetKeyboardService(); + WarnIfDeviceIsNotSupported(); + + appSettingsManager = GetAppSettingsManager(application.Args); + InitializeSettings(); + application.Exit += (s, e) => FinalizeSettings(); + + var colorProfilesViewModel = new ColorProfilesViewModel(appSettingsManager); + + DataContext = new TrayViewModel( + application, + colorProfilesViewModel, + keyboardService, + appSettingsManager); + } + + base.OnFrameworkInitializationCompleted(); + } + protected static IKeyboardService GetKeyboardService() + { + var keyboardDevice = new KeyboardDevice(); + + return new KeyboardService(keyboardDevice); + } + + protected void WarnIfDeviceIsNotSupported() + { + if (keyboardService.IsDeviceSupported()) + return; + + WindowHelper.ShowMessageWindow( + "MSI keyboard not found!", + "The supported \"MSI EPF USB\" SteelSeries keyboard has not been found!" + + Environment.NewLine + + "Exit an application as it is is not going to work properly anyway."); + } + + protected static IAppSettingsManager GetAppSettingsManager(params string[] args) + { + var fileOption = new Option( + name: "--settings", + getDefaultValue: () => new FileInfo("appsettings.xml"), + description: "A full path to the settings file."); + + var settingsFile = fileOption.Parse(args).GetValueForOption(fileOption); + + var appSettingsStreamer = new AppSettingsStreamer(settingsFile); + + return new AppSettingsManager(appSettingsStreamer); + } + + protected void InitializeSettings() + { + try + { + appSettingsManager.LoadSettings(); + } + catch (FileNotFoundException) + { + // supress and use default settings + } + catch (Exception ex) + { + WindowHelper.ShowMessageWindow( + "Loading application settings failed!", + ex.Message); + } + } + + protected void FinalizeSettings() + { + try + { + appSettingsManager.SaveSettings(); + } + catch (Exception ex) + { + WindowHelper.ShowMessageWindow( + "Saving application settings failed!", + ex.Message); + } + } +} \ No newline at end of file diff --git a/MSI.Keyboard.Illuminator/Assets/arrow-down16.png b/MSI.Keyboard.Illuminator/Assets/arrow-down16.png new file mode 100644 index 0000000..416d81f Binary files /dev/null and b/MSI.Keyboard.Illuminator/Assets/arrow-down16.png differ diff --git a/MSI.Keyboard.Illuminator/Assets/arrow-down22.png b/MSI.Keyboard.Illuminator/Assets/arrow-down22.png new file mode 100644 index 0000000..53204cb Binary files /dev/null and b/MSI.Keyboard.Illuminator/Assets/arrow-down22.png differ diff --git a/MSI.Keyboard.Illuminator/Assets/arrow-down32.png b/MSI.Keyboard.Illuminator/Assets/arrow-down32.png new file mode 100644 index 0000000..81f686f Binary files /dev/null and b/MSI.Keyboard.Illuminator/Assets/arrow-down32.png differ diff --git a/MSI.Keyboard.Illuminator/Assets/arrow-up16.png b/MSI.Keyboard.Illuminator/Assets/arrow-up16.png new file mode 100644 index 0000000..1a2561e Binary files /dev/null and b/MSI.Keyboard.Illuminator/Assets/arrow-up16.png differ diff --git a/MSI.Keyboard.Illuminator/Assets/arrow-up22.png b/MSI.Keyboard.Illuminator/Assets/arrow-up22.png new file mode 100644 index 0000000..d61de14 Binary files /dev/null and b/MSI.Keyboard.Illuminator/Assets/arrow-up22.png differ diff --git a/MSI.Keyboard.Illuminator/Assets/arrow-up32.png b/MSI.Keyboard.Illuminator/Assets/arrow-up32.png new file mode 100644 index 0000000..617029a Binary files /dev/null and b/MSI.Keyboard.Illuminator/Assets/arrow-up32.png differ diff --git a/MSI.Keyboard.Illuminator/Assets/checked-mark16.png b/MSI.Keyboard.Illuminator/Assets/checked-mark16.png new file mode 100644 index 0000000..9a2cd67 Binary files /dev/null and b/MSI.Keyboard.Illuminator/Assets/checked-mark16.png differ diff --git a/MSI.Keyboard.Illuminator/Assets/checked-mark32.png b/MSI.Keyboard.Illuminator/Assets/checked-mark32.png new file mode 100644 index 0000000..bdbc109 Binary files /dev/null and b/MSI.Keyboard.Illuminator/Assets/checked-mark32.png differ diff --git a/MSI.Keyboard.Illuminator/Assets/color-palette16.png b/MSI.Keyboard.Illuminator/Assets/color-palette16.png new file mode 100644 index 0000000..a08ff54 Binary files /dev/null and b/MSI.Keyboard.Illuminator/Assets/color-palette16.png differ diff --git a/MSI.Keyboard.Illuminator/Assets/color-palette32.png b/MSI.Keyboard.Illuminator/Assets/color-palette32.png new file mode 100644 index 0000000..3b80617 Binary files /dev/null and b/MSI.Keyboard.Illuminator/Assets/color-palette32.png differ diff --git a/MSI.Keyboard.Illuminator/Assets/exit16.png b/MSI.Keyboard.Illuminator/Assets/exit16.png new file mode 100644 index 0000000..1c16187 Binary files /dev/null and b/MSI.Keyboard.Illuminator/Assets/exit16.png differ diff --git a/MSI.Keyboard.Illuminator/Assets/exit32.png b/MSI.Keyboard.Illuminator/Assets/exit32.png new file mode 100644 index 0000000..b2ee9e3 Binary files /dev/null and b/MSI.Keyboard.Illuminator/Assets/exit32.png differ diff --git a/MSI.Keyboard.Illuminator/Assets/logo.ico b/MSI.Keyboard.Illuminator/Assets/logo.ico new file mode 100644 index 0000000..f3c35b9 Binary files /dev/null and b/MSI.Keyboard.Illuminator/Assets/logo.ico differ diff --git a/MSI.Keyboard.Illuminator/Assets/minus16.png b/MSI.Keyboard.Illuminator/Assets/minus16.png new file mode 100644 index 0000000..d240368 Binary files /dev/null and b/MSI.Keyboard.Illuminator/Assets/minus16.png differ diff --git a/MSI.Keyboard.Illuminator/Assets/minus22.png b/MSI.Keyboard.Illuminator/Assets/minus22.png new file mode 100644 index 0000000..ace24df Binary files /dev/null and b/MSI.Keyboard.Illuminator/Assets/minus22.png differ diff --git a/MSI.Keyboard.Illuminator/Assets/minus32.png b/MSI.Keyboard.Illuminator/Assets/minus32.png new file mode 100644 index 0000000..9e34c80 Binary files /dev/null and b/MSI.Keyboard.Illuminator/Assets/minus32.png differ diff --git a/MSI.Keyboard.Illuminator/Assets/palette16.png b/MSI.Keyboard.Illuminator/Assets/palette16.png new file mode 100644 index 0000000..bcaeffe Binary files /dev/null and b/MSI.Keyboard.Illuminator/Assets/palette16.png differ diff --git a/MSI.Keyboard.Illuminator/Assets/palette32.png b/MSI.Keyboard.Illuminator/Assets/palette32.png new file mode 100644 index 0000000..99879c1 Binary files /dev/null and b/MSI.Keyboard.Illuminator/Assets/palette32.png differ diff --git a/MSI.Keyboard.Illuminator/Assets/plus16.png b/MSI.Keyboard.Illuminator/Assets/plus16.png new file mode 100644 index 0000000..85597e9 Binary files /dev/null and b/MSI.Keyboard.Illuminator/Assets/plus16.png differ diff --git a/MSI.Keyboard.Illuminator/Assets/plus22.png b/MSI.Keyboard.Illuminator/Assets/plus22.png new file mode 100644 index 0000000..ac941e1 Binary files /dev/null and b/MSI.Keyboard.Illuminator/Assets/plus22.png differ diff --git a/MSI.Keyboard.Illuminator/Assets/plus32.png b/MSI.Keyboard.Illuminator/Assets/plus32.png new file mode 100644 index 0000000..8b0781e Binary files /dev/null and b/MSI.Keyboard.Illuminator/Assets/plus32.png differ diff --git a/MSI.Keyboard.Illuminator/Assets/svg/arrow-down.svg b/MSI.Keyboard.Illuminator/Assets/svg/arrow-down.svg new file mode 100644 index 0000000..4a6dff6 --- /dev/null +++ b/MSI.Keyboard.Illuminator/Assets/svg/arrow-down.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/MSI.Keyboard.Illuminator/Assets/svg/arrow-up.svg b/MSI.Keyboard.Illuminator/Assets/svg/arrow-up.svg new file mode 100644 index 0000000..f2948a9 --- /dev/null +++ b/MSI.Keyboard.Illuminator/Assets/svg/arrow-up.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/MSI.Keyboard.Illuminator/Assets/svg/checked-mark.svg b/MSI.Keyboard.Illuminator/Assets/svg/checked-mark.svg new file mode 100644 index 0000000..9f8f1d0 --- /dev/null +++ b/MSI.Keyboard.Illuminator/Assets/svg/checked-mark.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/MSI.Keyboard.Illuminator/Assets/svg/color-palette.svg b/MSI.Keyboard.Illuminator/Assets/svg/color-palette.svg new file mode 100644 index 0000000..a6ce8f6 --- /dev/null +++ b/MSI.Keyboard.Illuminator/Assets/svg/color-palette.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/MSI.Keyboard.Illuminator/Assets/svg/exit.svg b/MSI.Keyboard.Illuminator/Assets/svg/exit.svg new file mode 100644 index 0000000..2d0c4ef --- /dev/null +++ b/MSI.Keyboard.Illuminator/Assets/svg/exit.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/MSI.Keyboard.Illuminator/Assets/svg/keyboard.svg b/MSI.Keyboard.Illuminator/Assets/svg/keyboard.svg new file mode 100644 index 0000000..39e4f0e --- /dev/null +++ b/MSI.Keyboard.Illuminator/Assets/svg/keyboard.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + diff --git a/MSI.Keyboard.Illuminator/Assets/svg/minus.svg b/MSI.Keyboard.Illuminator/Assets/svg/minus.svg new file mode 100644 index 0000000..8764244 --- /dev/null +++ b/MSI.Keyboard.Illuminator/Assets/svg/minus.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/MSI.Keyboard.Illuminator/Assets/svg/palette.svg b/MSI.Keyboard.Illuminator/Assets/svg/palette.svg new file mode 100644 index 0000000..7b32666 --- /dev/null +++ b/MSI.Keyboard.Illuminator/Assets/svg/palette.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/MSI.Keyboard.Illuminator/Assets/svg/plus.svg b/MSI.Keyboard.Illuminator/Assets/svg/plus.svg new file mode 100644 index 0000000..80f9191 --- /dev/null +++ b/MSI.Keyboard.Illuminator/Assets/svg/plus.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/MSI.Keyboard.Illuminator/Helpers/AssetsHelper.cs b/MSI.Keyboard.Illuminator/Helpers/AssetsHelper.cs new file mode 100644 index 0000000..3253d37 --- /dev/null +++ b/MSI.Keyboard.Illuminator/Helpers/AssetsHelper.cs @@ -0,0 +1,21 @@ +using Avalonia.Media.Imaging; +using Avalonia.Platform; + +using System; +using System.IO; + +namespace MSI.Keyboard.Illuminator.Helpers; + +public static class AssetsHelper +{ + public static Bitmap GetImageFromAssets(string pathToImage) + { + var basePath = @"avares://MSI.Keyboard.Illuminator/Assets"; + var fullPath = Path.Combine(basePath, pathToImage); + var uri = new Uri(fullPath); + + using var assetStream = AssetLoader.Open(uri); + + return new(assetStream); + } +} diff --git a/MSI.Keyboard.Illuminator/Helpers/ColorExtensions.cs b/MSI.Keyboard.Illuminator/Helpers/ColorExtensions.cs new file mode 100644 index 0000000..2af42ae --- /dev/null +++ b/MSI.Keyboard.Illuminator/Helpers/ColorExtensions.cs @@ -0,0 +1,10 @@ +namespace MSI.Keyboard.Illuminator.Helpers; + +public static class ColorExtensions +{ + public static Avalonia.Media.Color ToAvaloniaColor(this System.Drawing.Color color) => + Avalonia.Media.Color.FromArgb(color.A, color.R, color.G, color.B); + + public static System.Drawing.Color ToSystemColor(this Avalonia.Media.Color color) => + System.Drawing.Color.FromArgb(color.A, color.R, color.G, color.B); +} diff --git a/MSI.Keyboard.Illuminator/Helpers/WindowHelper.cs b/MSI.Keyboard.Illuminator/Helpers/WindowHelper.cs new file mode 100644 index 0000000..2916cd7 --- /dev/null +++ b/MSI.Keyboard.Illuminator/Helpers/WindowHelper.cs @@ -0,0 +1,45 @@ +using Avalonia.Controls; + +using MSI.Keyboard.Illuminator.ViewModels; +using MSI.Keyboard.Illuminator.Views; + +namespace MSI.Keyboard.Illuminator.Helpers; + +public static class WindowHelper +{ + public static Window GetColorProfilesWindow(ColorProfilesViewModel colorProfilesViewModel) + { + var colorProfilesWindow = new ColorProfilesWindow() + { + DataContext = colorProfilesViewModel, + }; + + return colorProfilesWindow; + } + + public static void ShowColorProfilesWindow(ColorProfilesViewModel colorProfilesViewModel) + { + var colorProfilesWindow = GetColorProfilesWindow(colorProfilesViewModel); + + colorProfilesWindow.Show(); + } + + public static Window GetMessageWindow(string title, string message) + { + var messageViewModel = new MessageViewModel(title, message); + + var window = new MessageWindow() + { + DataContext = messageViewModel, + }; + + return window; + } + + public static void ShowMessageWindow(string title, string message) + { + var messageWindow = GetMessageWindow(title, message); + + messageWindow.Show(); + } +} diff --git a/MSI.Keyboard.Illuminator/MSI.Keyboard.Illuminator.csproj b/MSI.Keyboard.Illuminator/MSI.Keyboard.Illuminator.csproj new file mode 100644 index 0000000..ad7d997 --- /dev/null +++ b/MSI.Keyboard.Illuminator/MSI.Keyboard.Illuminator.csproj @@ -0,0 +1,38 @@ + + + WinExe + net8.0 + disable + true + app.manifest + true + Assets/logo.ico + true + + + + full + true + + + + none + false + + + + + + + + + + + + + + + + + + diff --git a/MSI.Keyboard.Illuminator/Models/AppSettings.cs b/MSI.Keyboard.Illuminator/Models/AppSettings.cs new file mode 100644 index 0000000..427d978 --- /dev/null +++ b/MSI.Keyboard.Illuminator/Models/AppSettings.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace MSI.Keyboard.Illuminator.Models; + +public class AppSettings +{ + public ColorProfile ActiveColorProfile { get; set; } + + public List ColorProfiles { get; set; } = []; +} diff --git a/MSI.Keyboard.Illuminator/Models/AppSettingsStreamerOptions.cs b/MSI.Keyboard.Illuminator/Models/AppSettingsStreamerOptions.cs new file mode 100644 index 0000000..3de99be --- /dev/null +++ b/MSI.Keyboard.Illuminator/Models/AppSettingsStreamerOptions.cs @@ -0,0 +1,10 @@ +namespace MSI.Keyboard.Illuminator.Models; + +public class AppSettingsStreamerOptions( + string appSettingsFilePath, + bool isXmlIndented = true) +{ + public string AppSettingsFilePath { get; } = appSettingsFilePath; + + public bool IsXmlIndented { get; } = isXmlIndented; +} diff --git a/MSI.Keyboard.Illuminator/Models/BlinkingMode.cs b/MSI.Keyboard.Illuminator/Models/BlinkingMode.cs new file mode 100644 index 0000000..97f6c2e --- /dev/null +++ b/MSI.Keyboard.Illuminator/Models/BlinkingMode.cs @@ -0,0 +1,10 @@ +namespace MSI.Keyboard.Illuminator.Models; + +public enum BlinkingMode +{ + Normal = 0x01, + Gaming = 0x02, + Breathe = 0x03, + Demo = 0x04, + Wave = 0x05, +} diff --git a/MSI.Keyboard.Illuminator/Models/ColorProfile.cs b/MSI.Keyboard.Illuminator/Models/ColorProfile.cs new file mode 100644 index 0000000..078fe67 --- /dev/null +++ b/MSI.Keyboard.Illuminator/Models/ColorProfile.cs @@ -0,0 +1,111 @@ +using Avalonia.Media; + +using ReactiveUI; + +using System; + +namespace MSI.Keyboard.Illuminator.Models; + +public class ColorProfile : ReactiveObject, ICloneable +{ + protected string name; + + public string Name + { + get => name; + set => this.RaiseAndSetIfChanged(ref name, value); + } + + protected BlinkingMode blinkingMode; + + public BlinkingMode BlinkingMode + { + get => blinkingMode; + set => this.RaiseAndSetIfChanged(ref blinkingMode, value); + } + + protected Color leftColor; + + public Color LeftColor + { + get => leftColor; + set => this.RaiseAndSetIfChanged(ref leftColor, value); + } + + protected Color centerColor; + + public Color CenterColor + { + get => centerColor; + set => this.RaiseAndSetIfChanged(ref centerColor, value); + } + + protected Color rightColor; + + public Color RightColor + { + get => rightColor; + set => this.RaiseAndSetIfChanged(ref rightColor, value); + } + + public ColorProfile() { } + + public ColorProfile( + string name, + BlinkingMode blinkingMode, + Color leftColor, + Color centerColor, + Color rightColor) + { + Name = name; + BlinkingMode = blinkingMode; + LeftColor = leftColor; + CenterColor = centerColor; + RightColor = rightColor; + } + + public static ColorProfile GetDefault() => new( + "Default", + BlinkingMode.Normal, + new(255, 255, 0, 0), + new(255, 255, 0, 0), + new(255, 255, 0, 0) + ); + + public static bool operator ==(ColorProfile left, ColorProfile right) + { + if (left is null) + return right is null; + + return left.Equals(right); + } + + public static bool operator !=(ColorProfile left, ColorProfile right) => + !(left == right); + + public object Clone() => new ColorProfile( + (string)Name.Clone(), + BlinkingMode, + LeftColor, + CenterColor, + RightColor); + + public override bool Equals(object obj) + { + if (obj is not ColorProfile other) + return false; + + return Name == other.Name + && BlinkingMode == other.BlinkingMode + && LeftColor == other.LeftColor + && CenterColor == other.CenterColor + && RightColor == other.RightColor; + } + + public override int GetHashCode() => HashCode.Combine( + Name, + BlinkingMode, + LeftColor, + CenterColor, + RightColor); +} diff --git a/MSI.Keyboard.Illuminator/Models/IlluminationConfiguration.cs b/MSI.Keyboard.Illuminator/Models/IlluminationConfiguration.cs new file mode 100644 index 0000000..d8636a7 --- /dev/null +++ b/MSI.Keyboard.Illuminator/Models/IlluminationConfiguration.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Drawing; + +namespace MSI.Keyboard.Illuminator.Models; + +public class IlluminationConfiguration( + IReadOnlyDictionary regionColors, + BlinkingMode blinkingMode) +{ + public IReadOnlyDictionary RegionColors { get; } = regionColors; + + public BlinkingMode BlinkingMode { get; } = blinkingMode; +} diff --git a/MSI.Keyboard.Illuminator/Models/KeyboardRegion.cs b/MSI.Keyboard.Illuminator/Models/KeyboardRegion.cs new file mode 100644 index 0000000..4436527 --- /dev/null +++ b/MSI.Keyboard.Illuminator/Models/KeyboardRegion.cs @@ -0,0 +1,8 @@ +namespace MSI.Keyboard.Illuminator.Models; + +public enum KeyboardRegion +{ + Start = 1, + Center = 2, + End = 3, +} \ No newline at end of file diff --git a/MSI.Keyboard.Illuminator/Models/Region.cs b/MSI.Keyboard.Illuminator/Models/Region.cs new file mode 100644 index 0000000..bd52296 --- /dev/null +++ b/MSI.Keyboard.Illuminator/Models/Region.cs @@ -0,0 +1,8 @@ +namespace MSI.Keyboard.Illuminator.Models; + +public enum Region +{ + Left = 0x01, + Center = 0x02, + Right = 0x03, +} \ No newline at end of file diff --git a/MSI.Keyboard.Illuminator/Program.cs b/MSI.Keyboard.Illuminator/Program.cs new file mode 100644 index 0000000..f1745ac --- /dev/null +++ b/MSI.Keyboard.Illuminator/Program.cs @@ -0,0 +1,24 @@ +using Avalonia; +using Avalonia.ReactiveUI; + +using System; + +namespace MSI.Keyboard.Illuminator; + +sealed class Program +{ + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + [STAThread] + public static void Main(string[] args) => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .WithInterFont() + .LogToTrace() + .UseReactiveUI(); +} diff --git a/MSI.Keyboard.Illuminator/Properties/launchSettings.json b/MSI.Keyboard.Illuminator/Properties/launchSettings.json new file mode 100644 index 0000000..17b6309 --- /dev/null +++ b/MSI.Keyboard.Illuminator/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "MSI.Keyboard.Illuminator": { + "commandName": "Project", + "commandLineArgs": "--settings \"appsettings.xml\"" + } + } +} \ No newline at end of file diff --git a/MSI.Keyboard.Illuminator/Providers/AppSettingsStreamer.cs b/MSI.Keyboard.Illuminator/Providers/AppSettingsStreamer.cs new file mode 100644 index 0000000..959cd42 --- /dev/null +++ b/MSI.Keyboard.Illuminator/Providers/AppSettingsStreamer.cs @@ -0,0 +1,148 @@ +using Avalonia.Media; + +using MSI.Keyboard.Illuminator.Models; + +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Linq; + +namespace MSI.Keyboard.Illuminator.Providers; + +public class AppSettingsStreamer( + FileInfo sourceFile) : IAppSettingsStreamer +{ + protected readonly FileInfo sourceFile = sourceFile; + + protected readonly Encoding encoding = Encoding.UTF8; + + public FileInfo GetSourceFile() => sourceFile; + + public AppSettings LoadSettings() + { + using var stream = GetStreamReader(); + + var xml = XDocument.Load(stream, LoadOptions.None); + + return GetAppSettings(xml.Root); + } + + public async Task LoadSettingsAsync() => + await LoadSettingsAsync(CancellationToken.None); + + public async Task LoadSettingsAsync(CancellationToken cancellationToken) + { + using var stream = GetStreamReader(); + + var xml = await XDocument.LoadAsync(stream, LoadOptions.None, cancellationToken); + + return GetAppSettings(xml.Root); + } + + public void SaveSettings(AppSettings appSettings) + { + var xml = GetXDocument(appSettings); + + using var writer = GetXmlTextWriter(); + + xml.Save(writer); + } + + public async Task SaveSettingsAsync(AppSettings appSettings) => + await SaveSettingsAsync(appSettings, CancellationToken.None); + + public async Task SaveSettingsAsync(AppSettings appSettings, CancellationToken cancellationToken) + { + var xml = GetXDocument(appSettings); + + using var writer = GetXmlTextWriter(); + + await xml.SaveAsync(writer, cancellationToken); + } + + protected StreamReader GetStreamReader() => new(sourceFile.FullName, encoding); + + protected XmlTextWriter GetXmlTextWriter() => new(sourceFile.FullName, encoding) + { + Formatting = Formatting.Indented, + }; + + protected static XElement GetXElement(Color color, string name) => new( + name, + new XElement(nameof(Color.A), color.A), + new XElement(nameof(Color.R), color.R), + new XElement(nameof(Color.G), color.G), + new XElement(nameof(Color.B), color.B)); + + protected static XElement GetXElement(ColorProfile colorProfile, string name = null) => new( + name ?? nameof(ColorProfile), + new XElement(nameof(ColorProfile.Name), colorProfile.Name), + new XElement(nameof(ColorProfile.BlinkingMode), colorProfile.BlinkingMode), + GetXElement(colorProfile.LeftColor, nameof(colorProfile.LeftColor)), + GetXElement(colorProfile.CenterColor, nameof(colorProfile.CenterColor)), + GetXElement(colorProfile.RightColor, nameof(colorProfile.RightColor))); + + protected static XElement GetXElement(AppSettings appSettings) + { + var appSettingsElement = new XElement(nameof(AppSettings)); + + if (appSettings.ActiveColorProfile != null) + { + var activeColorProfileElement = GetXElement( + appSettings.ActiveColorProfile, + nameof(appSettings.ActiveColorProfile)); + + appSettingsElement.Add(activeColorProfileElement); + } + + var colorProfilesElement = new XElement(nameof(AppSettings.ColorProfiles)); + + foreach (var colorProfile in appSettings.ColorProfiles) + colorProfilesElement.Add(GetXElement(colorProfile)); + + appSettingsElement.Add(colorProfilesElement); + + return appSettingsElement; + } + + protected static XDocument GetXDocument(AppSettings appSettings) => new( + new XDeclaration("1.0", "utf-8", "yes"), + GetXElement(appSettings)); + + protected static Color GetColor(XElement element) => new( + byte.Parse(element.Element(nameof(Color.A)).Value), + byte.Parse(element.Element(nameof(Color.R)).Value), + byte.Parse(element.Element(nameof(Color.G)).Value), + byte.Parse(element.Element(nameof(Color.B)).Value)); + + protected static ColorProfile GetColorProfile(XElement element) => new( + element.Element(nameof(ColorProfile.Name)).Value, + Enum.Parse( + element.Element(nameof(ColorProfile.BlinkingMode)).Value), + GetColor(element.Element(nameof(ColorProfile.LeftColor))), + GetColor(element.Element(nameof(ColorProfile.CenterColor))), + GetColor(element.Element(nameof(ColorProfile.RightColor)))); + + protected static AppSettings GetAppSettings(XElement element) + { + var appSettings = new AppSettings(); + + var activeColorProfileElement = element.Element(nameof(AppSettings.ActiveColorProfile)); + + if (activeColorProfileElement != null) + appSettings.ActiveColorProfile = GetColorProfile(activeColorProfileElement); + + + var colorProfilesElements = element + .Element(nameof(AppSettings.ColorProfiles)) + .Elements(); + + foreach (var colorProfileElement in colorProfilesElements) + appSettings.ColorProfiles.Add(GetColorProfile(colorProfileElement)); + + return appSettings; + } +} diff --git a/MSI.Keyboard.Illuminator/Providers/IAppSettingsStreamer.cs b/MSI.Keyboard.Illuminator/Providers/IAppSettingsStreamer.cs new file mode 100644 index 0000000..3590477 --- /dev/null +++ b/MSI.Keyboard.Illuminator/Providers/IAppSettingsStreamer.cs @@ -0,0 +1,24 @@ +using MSI.Keyboard.Illuminator.Models; + +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace MSI.Keyboard.Illuminator.Providers; + +public interface IAppSettingsStreamer +{ + public FileInfo GetSourceFile(); + + AppSettings LoadSettings(); + + Task LoadSettingsAsync(); + + Task LoadSettingsAsync(CancellationToken cancellationToken); + + void SaveSettings(AppSettings appSettings); + + Task SaveSettingsAsync(AppSettings appSettings); + + Task SaveSettingsAsync(AppSettings appSettings, CancellationToken cancellationToken); +} diff --git a/MSI.Keyboard.Illuminator/Providers/IIlluminationConfigurationBuilder.cs b/MSI.Keyboard.Illuminator/Providers/IIlluminationConfigurationBuilder.cs new file mode 100644 index 0000000..6651726 --- /dev/null +++ b/MSI.Keyboard.Illuminator/Providers/IIlluminationConfigurationBuilder.cs @@ -0,0 +1,16 @@ +using MSI.Keyboard.Illuminator.Models; + +using System.Drawing; + +namespace MSI.Keyboard.Illuminator.Providers; + +public interface IIlluminationConfigurationBuilder +{ + IIlluminationConfigurationBuilder ForAllRegions(BlinkingMode blinkingMode); + + IIlluminationConfigurationBuilder ForAllRegions(Color color); + + IIlluminationConfigurationBuilder ForRegion(Region region, Color color); + + IlluminationConfiguration Build(); +} diff --git a/MSI.Keyboard.Illuminator/Providers/IlluminationConfigurationBuilder.cs b/MSI.Keyboard.Illuminator/Providers/IlluminationConfigurationBuilder.cs new file mode 100644 index 0000000..8937954 --- /dev/null +++ b/MSI.Keyboard.Illuminator/Providers/IlluminationConfigurationBuilder.cs @@ -0,0 +1,52 @@ +using MSI.Keyboard.Illuminator.Models; + +using System.Collections.Generic; +using System.Drawing; +using System.Linq; + +namespace MSI.Keyboard.Illuminator.Providers; + +public class IlluminationConfigurationBuilder : IIlluminationConfigurationBuilder +{ + private readonly Dictionary regionColors; + + private BlinkingMode blinkingMode; + + public IlluminationConfigurationBuilder() + { + regionColors = new Dictionary() + { + { Region.Left, Color.Red }, + { Region.Center, Color.Red }, + { Region.Right, Color.Red }, + }; + + blinkingMode = BlinkingMode.Normal; + } + + public IIlluminationConfigurationBuilder ForAllRegions(BlinkingMode blinkingMode) + { + this.blinkingMode = blinkingMode; + + return this; + } + + public IIlluminationConfigurationBuilder ForAllRegions(Color color) + { + foreach (var region in regionColors.Keys.ToList()) + { + ForRegion(region, color); + } + + return this; + } + + public IIlluminationConfigurationBuilder ForRegion(Region region, Color color) + { + regionColors[region] = color; + + return this; + } + + public IlluminationConfiguration Build() => new(regionColors, blinkingMode); +} \ No newline at end of file diff --git a/MSI.Keyboard.Illuminator/Providers/IlluminationConfigurationFactory.cs b/MSI.Keyboard.Illuminator/Providers/IlluminationConfigurationFactory.cs new file mode 100644 index 0000000..b8d761f --- /dev/null +++ b/MSI.Keyboard.Illuminator/Providers/IlluminationConfigurationFactory.cs @@ -0,0 +1,24 @@ +using MSI.Keyboard.Illuminator.Helpers; +using MSI.Keyboard.Illuminator.Models; + +using System; + +namespace MSI.Keyboard.Illuminator.Providers; + +public static class IlluminationConfigurationFactory +{ + public static IlluminationConfiguration GetIlluminationConfiguration(ColorProfile colorProfile) + { + ArgumentNullException.ThrowIfNull(colorProfile); + + var configuration = new IlluminationConfigurationBuilder() + .ForAllRegions(colorProfile.BlinkingMode) + .ForRegion(Region.Left, colorProfile.LeftColor.ToSystemColor()) + .ForRegion(Region.Center, colorProfile.CenterColor.ToSystemColor()) + .ForRegion(Region.Right, colorProfile.RightColor.ToSystemColor()) + .Build(); + + return configuration; + } + +} diff --git a/MSI.Keyboard.Illuminator/Services/AppSettingsManager.cs b/MSI.Keyboard.Illuminator/Services/AppSettingsManager.cs new file mode 100644 index 0000000..8aed024 --- /dev/null +++ b/MSI.Keyboard.Illuminator/Services/AppSettingsManager.cs @@ -0,0 +1,37 @@ +using MSI.Keyboard.Illuminator.Models; +using MSI.Keyboard.Illuminator.Providers; + +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Linq; + +namespace MSI.Keyboard.Illuminator.Services; + +public class AppSettingsManager( + AppSettingsStreamer appSettingsStreamer) : IAppSettingsManager +{ + protected AppSettingsStreamer appSettingsStreamer = appSettingsStreamer; + + protected AppSettings appSettings = new(); + + public ColorProfile GetActiveColorProfile() => + appSettings.ActiveColorProfile?.Clone() as ColorProfile; + + public void UpdateActiveColorProfile(ColorProfile colorProfile) => + appSettings.ActiveColorProfile = colorProfile?.Clone() as ColorProfile; + + public List GetColorProfiles() => + GetDeepCopy(appSettings.ColorProfiles); + + public void UpdateColorProfiles(IEnumerable colorProfiles) => + appSettings.ColorProfiles = GetDeepCopy(colorProfiles); + + public void LoadSettings() => + appSettings = appSettingsStreamer.LoadSettings(); + + public void SaveSettings() => + appSettingsStreamer.SaveSettings(appSettings); + + protected static List GetDeepCopy(IEnumerable colorProfiles) => + colorProfiles.Select(s => s?.Clone() as ColorProfile).ToList(); +} diff --git a/MSI.Keyboard.Illuminator/Services/IAppSettingsManager.cs b/MSI.Keyboard.Illuminator/Services/IAppSettingsManager.cs new file mode 100644 index 0000000..06f82b8 --- /dev/null +++ b/MSI.Keyboard.Illuminator/Services/IAppSettingsManager.cs @@ -0,0 +1,20 @@ +using MSI.Keyboard.Illuminator.Models; + +using System.Collections.Generic; + +namespace MSI.Keyboard.Illuminator.Services; + +public interface IAppSettingsManager +{ + ColorProfile GetActiveColorProfile(); + + void UpdateActiveColorProfile(ColorProfile colorProfile); + + List GetColorProfiles(); + + void UpdateColorProfiles(IEnumerable colorProfiles); + + void LoadSettings(); + + void SaveSettings(); +} diff --git a/MSI.Keyboard.Illuminator/Services/IKeyboardDevice.cs b/MSI.Keyboard.Illuminator/Services/IKeyboardDevice.cs new file mode 100644 index 0000000..289ca9a --- /dev/null +++ b/MSI.Keyboard.Illuminator/Services/IKeyboardDevice.cs @@ -0,0 +1,31 @@ +using MSI.Keyboard.Illuminator.Models; + +using System.Drawing; +using System.IO; +using System.Threading.Tasks; + +namespace MSI.Keyboard.Illuminator.Services; + +public interface IKeyboardDevice +{ + /// + /// Changes the color of a keyboard for specific region. + /// + /// The region of a keyboard. + /// The color of a region's buttons. + /// Thrown when keyboard device has not been found. + Task ChangeColorAsync(Region region, Color color); + + /// + /// Checks if the current device is supported. + /// + /// true if a device is supported, otherwise false. + bool IsDeviceSupported(); + + /// + /// Sets the mode of keyboard's leds blinking. + /// + /// The specific blinking effect. + /// Thrown when keyboard device has not been found. + Task ChangeModeAsync(BlinkingMode blinkingMode); +} diff --git a/MSI.Keyboard.Illuminator/Services/IKeyboardService.cs b/MSI.Keyboard.Illuminator/Services/IKeyboardService.cs new file mode 100644 index 0000000..a9f9c7b --- /dev/null +++ b/MSI.Keyboard.Illuminator/Services/IKeyboardService.cs @@ -0,0 +1,29 @@ +using MSI.Keyboard.Illuminator.Models; + +using System; +using System.Threading.Tasks; + +namespace MSI.Keyboard.Illuminator.Services; + +public interface IKeyboardService +{ + /// + /// Gets the current illumination configuration. + /// + /// An instance of object. + IlluminationConfiguration GetCurrentConfiguration(); + + /// + /// Applies an illumination configuration. + /// + /// An illumination configuration. + /// Thrown when keyboard device not found. + /// If unrecognized IO exception occured. + Task ApplyConfigurationAsync(IlluminationConfiguration configuration); + + /// + /// Checks if the current device is supported. + /// + /// true if a device is supported, otherwise false. + bool IsDeviceSupported(); +} diff --git a/MSI.Keyboard.Illuminator/Services/KeyboardDevice.cs b/MSI.Keyboard.Illuminator/Services/KeyboardDevice.cs new file mode 100644 index 0000000..0ac5a46 --- /dev/null +++ b/MSI.Keyboard.Illuminator/Services/KeyboardDevice.cs @@ -0,0 +1,70 @@ +using HidSharp; + +using MSI.Keyboard.Illuminator.Models; + +using System; +using System.Drawing; +using System.IO; +using System.Threading.Tasks; + +namespace MSI.Keyboard.Illuminator.Services; + +public class KeyboardDevice : IKeyboardDevice +{ + protected const int VendorId = 6000; + + protected const int ProductId = 65280; + + public async Task ChangeColorAsync(Region region, Color color) + { + var intensity = (int)Math.Round(100.0 / 255 * color.A, MidpointRounding.AwayFromZero); + + var colorFragmentValue = new Func(c => (byte)(c * (intensity / 100d))); + + var data = new byte[] + { + 1, + 2, + 64, + (byte)region, + colorFragmentValue(color.R), + colorFragmentValue(color.G), + colorFragmentValue(color.B), + 0, + }; + + await SetFeature(data); + } + + public async Task ChangeModeAsync(BlinkingMode blinkingMode) + { + var data = new byte[] + { + 1, + 2, + 65, + (byte)blinkingMode, + 0, + 0, + 0, + 236, + }; + + await SetFeature(data); + } + + public bool IsDeviceSupported() => + DeviceList.Local.TryGetHidDevice(out _, VendorId, ProductId); + + protected static Task SetFeature(byte[] data) + { + var device = DeviceList.Local + .GetHidDeviceOrNull(VendorId, ProductId) + ?? throw new IOException("MSI keyboard has not been found!"); + + using var stream = device.Open(); + stream.SetFeature(data); + + return Task.CompletedTask; + } +} diff --git a/MSI.Keyboard.Illuminator/Services/KeyboardService.cs b/MSI.Keyboard.Illuminator/Services/KeyboardService.cs new file mode 100644 index 0000000..ac74520 --- /dev/null +++ b/MSI.Keyboard.Illuminator/Services/KeyboardService.cs @@ -0,0 +1,39 @@ +using MSI.Keyboard.Illuminator.Models; + +using System.Collections.Generic; +using System.Drawing; +using System.Threading.Tasks; + +namespace MSI.Keyboard.Illuminator.Services; + +public class KeyboardService(IKeyboardDevice keyboardDevice) : IKeyboardService +{ + private readonly IKeyboardDevice keyboardDevice = keyboardDevice; + + private IlluminationConfiguration configuration; + + public async Task ApplyConfigurationAsync(IlluminationConfiguration configuration) + { + this.configuration = configuration; + + await ApplyBlinkingModeAsync(this.configuration.BlinkingMode); + await ApplyRegionColors(this.configuration.RegionColors); + } + + public bool IsDeviceSupported() => keyboardDevice.IsDeviceSupported(); + + public IlluminationConfiguration GetCurrentConfiguration() => configuration; + + protected async Task ApplyRegionColors(IReadOnlyDictionary regionColors) + { + foreach (var regionColor in regionColors) + { + await keyboardDevice.ChangeColorAsync( + regionColor.Key, + regionColor.Value); + } + } + + protected async Task ApplyBlinkingModeAsync(BlinkingMode blinkingMode) => + await keyboardDevice.ChangeModeAsync(blinkingMode); +} diff --git a/MSI.Keyboard.Illuminator/ViewModels/ColorProfileViewModel.cs b/MSI.Keyboard.Illuminator/ViewModels/ColorProfileViewModel.cs new file mode 100644 index 0000000..8375ade --- /dev/null +++ b/MSI.Keyboard.Illuminator/ViewModels/ColorProfileViewModel.cs @@ -0,0 +1,17 @@ +using MSI.Keyboard.Illuminator.Models; + +using ReactiveUI; + +namespace MSI.Keyboard.Illuminator.ViewModels; + +public class ColorProfileViewModel(ColorProfile colorProfile) : ReactiveObject +{ + public static BlinkingMode[] BlinkingModes => [ + BlinkingMode.Normal, + BlinkingMode.Gaming, + BlinkingMode.Breathe, + BlinkingMode.Demo, + BlinkingMode.Wave, ]; + + public ColorProfile ColorProfile { get; } = colorProfile; +} diff --git a/MSI.Keyboard.Illuminator/ViewModels/ColorProfilesViewModel.cs b/MSI.Keyboard.Illuminator/ViewModels/ColorProfilesViewModel.cs new file mode 100644 index 0000000..9eeb84e --- /dev/null +++ b/MSI.Keyboard.Illuminator/ViewModels/ColorProfilesViewModel.cs @@ -0,0 +1,124 @@ +using Avalonia.Controls.Selection; + +using MSI.Keyboard.Illuminator.Models; +using MSI.Keyboard.Illuminator.Services; + +using ReactiveUI; + +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive; +using System.Reactive.Linq; + +namespace MSI.Keyboard.Illuminator.ViewModels; + +public class ColorProfilesViewModel : ReactiveObject +{ + protected readonly IAppSettingsManager appSettingsManager; + + protected ObservableCollection colorProfileViewModels; + + public ObservableCollection ColorProfileViewModels + { + get => colorProfileViewModels; + set => this.RaiseAndSetIfChanged(ref colorProfileViewModels, value); + } + + protected SelectionModel selection; + + public SelectionModel Selection + { + get => selection; + set => this.RaiseAndSetIfChanged(ref selection, value); + } + + public ReactiveCommand Save { get; } + + public ReactiveCommand Cancel { get; } + + public ReactiveCommand AddNewColorProfile { get; } + + public ReactiveCommand RemoveSelectedColorProfile { get; } + + public ReactiveCommand MoveSelectedColorProfileUp { get; } + + public ReactiveCommand MoveSelectedColorProfileDown { get; } + + public ColorProfilesViewModel( + IAppSettingsManager appSettingsManager) + { + this.appSettingsManager = appSettingsManager; + + Selection = new(); + + LoadColorProfiles(); + + Save = ReactiveCommand.Create(() => + { + appSettingsManager.UpdateColorProfiles( + ColorProfileViewModels.Select(s => s.ColorProfile).Distinct()); + }); + + Cancel = ReactiveCommand.Create(LoadColorProfiles); + + AddNewColorProfile = ReactiveCommand.Create(() => + { + var newColorProfile = ColorProfile.GetDefault(); + newColorProfile.Name = $"Profile {ColorProfileViewModels?.Count + 1 ?? 1}"; + + ColorProfileViewModels.Add(new ColorProfileViewModel(newColorProfile)); + }); + + var canRemoveSelectedColorProfile = this + .WhenAnyValue(s => s.Selection.SelectedItem) + .Select(s => s != null); + + RemoveSelectedColorProfile = ReactiveCommand.Create(() => + { + var idx = Selection.SelectedIndex; + ColorProfileViewModels.Remove(Selection.SelectedItem); + + idx = ColorProfileViewModels.Count > idx ? idx : ColorProfileViewModels.Count - 1; + + selection.SelectedIndex = idx; + + }, canRemoveSelectedColorProfile); + + void MoveSelectedColorProfile(bool moveUp) + { + var oldIdx = Selection.SelectedIndex; + var newIdx = Selection.SelectedIndex + (moveUp ? -1 : 1); + + ColorProfileViewModels.Move(oldIdx, newIdx); + Selection.SelectedIndex = newIdx; + } + + var canMoveSelectedColorProfileUp = this + .WhenAnyValue(s => s.Selection.SelectedIndex) + .Select(s => s > 0); + + MoveSelectedColorProfileUp = ReactiveCommand.Create( + () => MoveSelectedColorProfile(true), + canMoveSelectedColorProfileUp); + + var canMoveSelectedColorProfileDown = this + .WhenAnyValue(s => s.Selection.SelectedIndex) + .Select(s => s >= 0 && s < ColorProfileViewModels.Count - 1); + + MoveSelectedColorProfileDown = ReactiveCommand.Create( + () => MoveSelectedColorProfile(false), + canMoveSelectedColorProfileDown); + } + + protected void LoadColorProfiles() + { + var colorProfiles = appSettingsManager.GetColorProfiles() + .Select(cp => new ColorProfileViewModel(cp)) + .ToList(); + + ColorProfileViewModels = new ObservableCollection(colorProfiles); + + if (ColorProfileViewModels.Any()) + Selection.SelectedIndex = 0; + } +} diff --git a/MSI.Keyboard.Illuminator/ViewModels/MessageViewModel.cs b/MSI.Keyboard.Illuminator/ViewModels/MessageViewModel.cs new file mode 100644 index 0000000..c85bc0b --- /dev/null +++ b/MSI.Keyboard.Illuminator/ViewModels/MessageViewModel.cs @@ -0,0 +1,36 @@ +using Avalonia.Controls; + +using ReactiveUI; + +using System.Reactive; + +namespace MSI.Keyboard.Illuminator.ViewModels; + +public class MessageViewModel : ReactiveObject +{ + protected string title; + + public string Title + { + get => title; + set => this.RaiseAndSetIfChanged(ref title, value); + } + + protected string message; + + public string Message + { + get => message; + set => this.RaiseAndSetIfChanged(ref message, value); + } + + public ReactiveCommand Close { get; } + + public MessageViewModel(string title, string message) + { + Title = title; + Message = message; + + Close = ReactiveCommand.Create(window => window?.Close()); + } +} diff --git a/MSI.Keyboard.Illuminator/ViewModels/TrayViewModel.cs b/MSI.Keyboard.Illuminator/ViewModels/TrayViewModel.cs new file mode 100644 index 0000000..295c3fb --- /dev/null +++ b/MSI.Keyboard.Illuminator/ViewModels/TrayViewModel.cs @@ -0,0 +1,163 @@ +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; + +using MSI.Keyboard.Illuminator.Helpers; +using MSI.Keyboard.Illuminator.Models; +using MSI.Keyboard.Illuminator.Providers; +using MSI.Keyboard.Illuminator.Services; +using MSI.Keyboard.Illuminator.Views; + +using ReactiveUI; + +using System; +using System.Linq; +using System.Reactive; +using System.Reactive.Linq; + +namespace MSI.Keyboard.Illuminator.ViewModels; + +public class TrayViewModel : ReactiveObject +{ + protected readonly IKeyboardService keyboardService; + + protected readonly IAppSettingsManager appSettingsManager; + + public ColorProfilesViewModel ColorProfilesViewModel { get; } + + + protected NativeMenu trayMenu; + + public NativeMenu TrayMenu + { + get => trayMenu; + set => this.RaiseAndSetIfChanged(ref trayMenu, value); + } + + public ReactiveCommand SelectColorProfile { get; } + + public ReactiveCommand ShowColorProfiles { get; } + + public ReactiveCommand Exit { get; } + + public TrayViewModel( + IClassicDesktopStyleApplicationLifetime application, + ColorProfilesViewModel colorProfilesViewModel, + IKeyboardService keyboardService, + IAppSettingsManager appSettingsManager) + { + ColorProfilesViewModel = colorProfilesViewModel; + this.keyboardService = keyboardService; + this.appSettingsManager = appSettingsManager; + + SelectColorProfile = ReactiveCommand.CreateFromTask(async colorProfile => + { + var configuration = IlluminationConfigurationFactory + .GetIlluminationConfiguration(colorProfile); + + await keyboardService.ApplyConfigurationAsync(configuration); + appSettingsManager.UpdateActiveColorProfile(colorProfile); + + SelectColorProfileOnTrayMenu(colorProfile); + }); + + SelectColorProfile.ThrownExceptions.Subscribe(ex => + { + WindowHelper.ShowMessageWindow( + "Changing keyboard colors failed!", + ex.Message); + }); + + ShowColorProfiles = ReactiveCommand.Create(() => + { + if (application.Windows.Any(w => w is ColorProfilesWindow)) + return; + + WindowHelper.ShowColorProfilesWindow(colorProfilesViewModel); + }); + + Exit = ReactiveCommand.Create(() => application.Shutdown(0)); + + ColorProfilesViewModel.Save.Subscribe(x => + { + GenerateTrayMenu(); + SelectColorProfileOnTrayMenu(appSettingsManager.GetActiveColorProfile()); + + CloseAllColorProfilesWindows(application); + }); + + ColorProfilesViewModel.Cancel.Subscribe(x => + CloseAllColorProfilesWindows(application)); + + GenerateTrayMenu(); + + var activeColorProfile = appSettingsManager.GetActiveColorProfile(); + if (activeColorProfile != null) + SelectColorProfile.Execute(activeColorProfile).Subscribe(); + } + + /// + /// Generates tray menu. + /// + protected void GenerateTrayMenu() + { + var newTrayMenu = new NativeMenu(); + + foreach (var colorProfile in appSettingsManager.GetColorProfiles()) + { + newTrayMenu.Add(new NativeMenuItem() + { + Header = colorProfile.Name, + Command = SelectColorProfile, + CommandParameter = colorProfile, + }); + } + + if (newTrayMenu.Items.Any()) + newTrayMenu.Add(new NativeMenuItemSeparator()); + + newTrayMenu.Add(new NativeMenuItem() + { + Header = "Profiles", + Command = ShowColorProfiles, + Icon = AssetsHelper.GetImageFromAssets("palette16.png"), + }); + newTrayMenu.Add(new NativeMenuItemSeparator()); + newTrayMenu.Add(new NativeMenuItem() + { + Header = "Exit", + Command = Exit, + Icon = AssetsHelper.GetImageFromAssets("exit16.png"), + }); + + TrayMenu = newTrayMenu; + } + + /// + /// Selects a given on tray menu, if it is present there. + /// + /// A color profile to be selected. + protected void SelectColorProfileOnTrayMenu(ColorProfile colorProfile) + { + foreach (var item in TrayMenu.Items.SkipLast(4).Cast()) + { + if (item == null) continue; + + item.Icon = item.CommandParameter as ColorProfile == colorProfile + ? AssetsHelper.GetImageFromAssets("checked-mark16.png") + : null; + } + } + + /// + /// Closes all windows. + /// + protected static void CloseAllColorProfilesWindows( + IClassicDesktopStyleApplicationLifetime application) + { + for (int i = application.Windows.Count - 1; i >= 0; i--) + { + var colorProfilesWindow = application.Windows[i] as ColorProfilesWindow; + colorProfilesWindow?.Close(); + } + } +} diff --git a/MSI.Keyboard.Illuminator/Views/ColorProfileView.axaml b/MSI.Keyboard.Illuminator/Views/ColorProfileView.axaml new file mode 100644 index 0000000..44bafd6 --- /dev/null +++ b/MSI.Keyboard.Illuminator/Views/ColorProfileView.axaml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MSI.Keyboard.Illuminator/Views/ColorProfileView.axaml.cs b/MSI.Keyboard.Illuminator/Views/ColorProfileView.axaml.cs new file mode 100644 index 0000000..9365eba --- /dev/null +++ b/MSI.Keyboard.Illuminator/Views/ColorProfileView.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace MSI.Keyboard.Illuminator.Views; + +public partial class ColorProfileView : UserControl +{ + public ColorProfileView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/MSI.Keyboard.Illuminator/Views/ColorProfilesView.axaml b/MSI.Keyboard.Illuminator/Views/ColorProfilesView.axaml new file mode 100644 index 0000000..4619c5b --- /dev/null +++ b/MSI.Keyboard.Illuminator/Views/ColorProfilesView.axaml @@ -0,0 +1,124 @@ + + + + + + + + WhiteSmoke + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +