From 67e17ee2cbb6b5129b7d31afc841654561dca07c Mon Sep 17 00:00:00 2001 From: Ben Olden-Cooligan Date: Sat, 31 Aug 2024 13:59:20 -0700 Subject: [PATCH] WinForms: Dark mode #109 --- NAPS2.Lib.Gtk/EtoForms/Gtk/GtkEtoPlatform.cs | 2 +- .../EtoForms/Ui/WinFormsDesktopForm.cs | 15 +++++++++++++ .../WinForms/WinFormsDarkModeProvider.cs | 21 +++++++++---------- .../EtoForms/WinForms/WinFormsEtoPlatform.cs | 8 +++++-- .../EtoForms/WinForms/WinFormsListView.cs | 8 +++---- NAPS2.Lib.WinForms/Modules/WinFormsModule.cs | 3 +-- NAPS2.Lib/EtoForms/ColorScheme.cs | 8 +++++++ NAPS2.Lib/EtoForms/EtoPlatform.cs | 3 ++- NAPS2.Lib/EtoForms/Ui/DesktopForm.cs | 6 +++--- 9 files changed, 50 insertions(+), 24 deletions(-) diff --git a/NAPS2.Lib.Gtk/EtoForms/Gtk/GtkEtoPlatform.cs b/NAPS2.Lib.Gtk/EtoForms/Gtk/GtkEtoPlatform.cs index 924a2103f5..9c1624b65e 100644 --- a/NAPS2.Lib.Gtk/EtoForms/Gtk/GtkEtoPlatform.cs +++ b/NAPS2.Lib.Gtk/EtoForms/Gtk/GtkEtoPlatform.cs @@ -268,7 +268,7 @@ public override Control AccessibleImageButton(Image image, string text, Action o return button.ToEto(); } - public override void ConfigureZoomButton(Button button, string icon) + public override void ConfigureZoomButton(Button button, string icon, ColorScheme colorScheme) { var gtkButton = button.ToNative(); button.Text = ""; diff --git a/NAPS2.Lib.WinForms/EtoForms/Ui/WinFormsDesktopForm.cs b/NAPS2.Lib.WinForms/EtoForms/Ui/WinFormsDesktopForm.cs index 837630d8d3..dd000f1477 100644 --- a/NAPS2.Lib.WinForms/EtoForms/Ui/WinFormsDesktopForm.cs +++ b/NAPS2.Lib.WinForms/EtoForms/Ui/WinFormsDesktopForm.cs @@ -51,6 +51,21 @@ public WinFormsDesktopForm( // TODO: Remove this if https://github.com/picoe/Eto/issues/2601 is fixed NativeListView.KeyDown += (_, e) => OnKeyDown(new KeyEventArgs(e.KeyData.ToEto(), KeyEventType.KeyDown)); + + Load += (_, _) => colorScheme.ColorSchemeChanged += ColorSchemeChanged; + UnLoad += (_, _) => colorScheme.ColorSchemeChanged -= ColorSchemeChanged; + } + + private void ColorSchemeChanged(object? sender, EventArgs e) + { + Invoker.Current.InvokeDispatch(() => + { + if (WF.Application.OpenForms.Count == 1) + { + // Reload the form as WinForms dark mode doesn't dynamically switch everything + SetCulture(Config.Get(c => c.Culture) ?? "en"); + } + }); } protected override void OnClosing(CancelEventArgs e) diff --git a/NAPS2.Lib.WinForms/EtoForms/WinForms/WinFormsDarkModeProvider.cs b/NAPS2.Lib.WinForms/EtoForms/WinForms/WinFormsDarkModeProvider.cs index 9440cf564e..7f62db4f4f 100644 --- a/NAPS2.Lib.WinForms/EtoForms/WinForms/WinFormsDarkModeProvider.cs +++ b/NAPS2.Lib.WinForms/EtoForms/WinForms/WinFormsDarkModeProvider.cs @@ -3,16 +3,13 @@ namespace NAPS2.EtoForms.WinForms; -public class WinFormsDarkModeProvider : IDarkModeProvider, IMessageFilter +public class WinFormsDarkModeProvider : IDarkModeProvider { - private const int WM_SETTINGCHANGE = 0x1A; - private const int WM_REFLECT = 0x2000; - private bool? _value; public WinFormsDarkModeProvider() { - Application.AddMessageFilter(this); + SystemEvents.UserPreferenceChanged += OnUserPreferenceChanged; } public bool IsDarkModeEnabled => _value ??= ReadDarkMode(); @@ -33,15 +30,17 @@ private bool ReadDarkMode() public event EventHandler? DarkModeChanged; - public bool PreFilterMessage(ref Message m) + private void OnUserPreferenceChanged(object sender, UserPreferenceChangedEventArgs e) { - if (m.Msg is WM_SETTINGCHANGE or (WM_SETTINGCHANGE | WM_REFLECT)) + var newValue = ReadDarkMode(); + if (newValue != _value) { - // TODO: Maybe we can narrow down the changed setting based on lParam? - // https://learn.microsoft.com/en-us/windows/win32/winmsg/wm-settingchange - _value = null; + _value = newValue; + // WinForms dark mode is experimental +#pragma warning disable WFO5001 + Application.SetColorMode(newValue ? SystemColorMode.Dark : SystemColorMode.Classic); +#pragma warning restore WFO5001 DarkModeChanged?.Invoke(this, EventArgs.Empty); } - return false; } } \ No newline at end of file diff --git a/NAPS2.Lib.WinForms/EtoForms/WinForms/WinFormsEtoPlatform.cs b/NAPS2.Lib.WinForms/EtoForms/WinForms/WinFormsEtoPlatform.cs index cf6239f983..0a3935413e 100644 --- a/NAPS2.Lib.WinForms/EtoForms/WinForms/WinFormsEtoPlatform.cs +++ b/NAPS2.Lib.WinForms/EtoForms/WinForms/WinFormsEtoPlatform.cs @@ -30,6 +30,10 @@ public override Application CreateApplication() WF.Application.EnableVisualStyles(); WF.Application.SetCompatibleTextRenderingDefault(false); WF.Application.SetHighDpiMode(WF.HighDpiMode.PerMonitorV2); + // WinForms dark mode is experimental +#pragma warning disable WFO5001 + WF.Application.SetColorMode(WF.SystemColorMode.System); +#pragma warning restore WFO5001 return new Application(Eto.Platforms.WinForms); } @@ -279,7 +283,7 @@ public override void SetImageSize(ButtonToolItem menuItem, int size) handler.ImageSize = size; } - public override void ConfigureZoomButton(Button button, string icon) + public override void ConfigureZoomButton(Button button, string icon, ColorScheme colorScheme) { AttachDpiDependency(button, scale => { @@ -289,7 +293,7 @@ public override void ConfigureZoomButton(Button button, string icon) var wfButton = (WF.Button) button.ToNative(); wfButton.AccessibleName = button.Text; wfButton.Text = ""; - wfButton.BackColor = SD.Color.White; + wfButton.BackColor = colorScheme.BackgroundColor.ToSD(); wfButton.FlatStyle = WF.FlatStyle.Flat; } diff --git a/NAPS2.Lib.WinForms/EtoForms/WinForms/WinFormsListView.cs b/NAPS2.Lib.WinForms/EtoForms/WinForms/WinFormsListView.cs index 4b16da606d..97a1a2c9aa 100644 --- a/NAPS2.Lib.WinForms/EtoForms/WinForms/WinFormsListView.cs +++ b/NAPS2.Lib.WinForms/EtoForms/WinForms/WinFormsListView.cs @@ -10,12 +10,12 @@ namespace NAPS2.EtoForms.WinForms; public class WinFormsListView : IListView where T : notnull { - private static readonly Pen DefaultPen = new(Color.Black, 1); + private Pen DefaultPen => new(_behavior.ColorScheme.ForegroundColor.ToSD(), 1); private static readonly Pen BasicSelectionPen = new(Color.FromArgb(0x60, 0xa0, 0xe8), 3); private const int PageNumberTextPadding = 6; private const int PageNumberSelectionPadding = 3; - private static readonly SolidBrush PageNumberOutlineBrush = new(Color.FromArgb(0x60, 0xa0, 0xe8)); - private static readonly SolidBrush PageNumberSelectionBrush = new(Color.FromArgb(0xcc, 0xe8, 0xff)); + private SolidBrush PageNumberOutlineBrush => new(_behavior.ColorScheme.HighlightBorderColor.ToSD()); + private SolidBrush PageNumberSelectionBrush => new(_behavior.ColorScheme.HighlightBackgroundColor.ToSD()); private static readonly StringFormat PageNumberLabelFormat = new() { Alignment = StringAlignment.Center, Trimming = StringTrimming.EllipsisCharacter }; @@ -141,7 +141,7 @@ private void CustomRenderItem(object? sender, DrawListViewItemEventArgs e) e.Graphics.DrawImage(image, new Rectangle(x, y, width, height)); // Draw the text below the image - var drawBrush = Brushes.Black; + var drawBrush = new SolidBrush(_behavior.ColorScheme.ForegroundColor.ToSD()); float x1 = x + width / 2f; float y1 = y + height + tp; RectangleF labelRect = new(x1, y1, 0, textSize.Height); diff --git a/NAPS2.Lib.WinForms/Modules/WinFormsModule.cs b/NAPS2.Lib.WinForms/Modules/WinFormsModule.cs index 5e6d0adade..7ba4ca50ef 100644 --- a/NAPS2.Lib.WinForms/Modules/WinFormsModule.cs +++ b/NAPS2.Lib.WinForms/Modules/WinFormsModule.cs @@ -15,8 +15,7 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType().As(); builder.RegisterType().As(); - // TODO: Change this when implementing dark mode on Windows - builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); builder.RegisterType().As(); diff --git a/NAPS2.Lib/EtoForms/ColorScheme.cs b/NAPS2.Lib/EtoForms/ColorScheme.cs index 909edf9801..7bbd423917 100644 --- a/NAPS2.Lib/EtoForms/ColorScheme.cs +++ b/NAPS2.Lib/EtoForms/ColorScheme.cs @@ -8,6 +8,10 @@ public class ColorScheme private static readonly Color MidGray = Color.FromRgb(0x606060); private static readonly Color LightGray = Color.FromRgb(0xdddddd); private static readonly Color HighlightBlue = Color.FromRgb(0x007bff); + private static readonly Color MidBlue = Color.FromRgb(0x60a0e8); + private static readonly Color PaleBlue = Color.FromRgb(0xcce8ff); + private static readonly Color DarkGrayBlue = Color.FromRgb(0x28445b); + private static readonly Color DarkOutlineBlue = Color.FromRgb(0x0078d4); private readonly IDarkModeProvider _darkModeProvider; @@ -29,6 +33,10 @@ public ColorScheme(IDarkModeProvider darkModeProvider) public Color CropColor => DarkMode ? HighlightBlue : Colors.Black; + public Color HighlightBorderColor => DarkMode ? DarkOutlineBlue : MidBlue; + + public Color HighlightBackgroundColor => DarkMode ? DarkGrayBlue : PaleBlue; + public Color NotificationBackgroundColor => DarkMode ? Color.FromRgb(0x323232) : Color.FromRgb(0xf2f2f2); public Color NotificationBorderColor => DarkMode ? Color.FromRgb(0x606060) : Color.FromRgb(0xb2b2b2); diff --git a/NAPS2.Lib/EtoForms/EtoPlatform.cs b/NAPS2.Lib/EtoForms/EtoPlatform.cs index a56ae00acd..581d8c8688 100644 --- a/NAPS2.Lib/EtoForms/EtoPlatform.cs +++ b/NAPS2.Lib/EtoForms/EtoPlatform.cs @@ -98,7 +98,7 @@ public virtual void InitForm(Window window) { } - public virtual void ConfigureZoomButton(Button button, string icon) + public virtual void ConfigureZoomButton(Button button, string icon, ColorScheme colorScheme) { } @@ -131,6 +131,7 @@ public virtual void ShowIcon(Dialog dialog) public virtual void ConfigureEllipsis(Label label) { + // TODO: Maybe implement our own ellipsis logic that uses text-measuring to strip trailing characters and add "..."? } public virtual Bitmap? ExtractAssociatedIcon(string exePath) => throw new NotSupportedException(); diff --git a/NAPS2.Lib/EtoForms/Ui/DesktopForm.cs b/NAPS2.Lib/EtoForms/Ui/DesktopForm.cs index c925e8da01..4fedf76f50 100644 --- a/NAPS2.Lib/EtoForms/Ui/DesktopForm.cs +++ b/NAPS2.Lib/EtoForms/Ui/DesktopForm.cs @@ -446,16 +446,16 @@ protected virtual LayoutElement GetControlButtons() protected LayoutElement GetSidebarButton() { var toggleSidebar = C.ImageButton(Commands.ToggleSidebar); - EtoPlatform.Current.ConfigureZoomButton(toggleSidebar, "application_side_list_small"); + EtoPlatform.Current.ConfigureZoomButton(toggleSidebar, "application_side_list_small", _colorScheme); return toggleSidebar.AlignTrailing(); } protected LayoutElement GetZoomButtons() { var zoomIn = C.ImageButton(Commands.ZoomIn); - EtoPlatform.Current.ConfigureZoomButton(zoomIn, "zoom_in_small"); + EtoPlatform.Current.ConfigureZoomButton(zoomIn, "zoom_in_small", _colorScheme); var zoomOut = C.ImageButton(Commands.ZoomOut); - EtoPlatform.Current.ConfigureZoomButton(zoomOut, "zoom_out_small"); + EtoPlatform.Current.ConfigureZoomButton(zoomOut, "zoom_out_small", _colorScheme); return L.Row(zoomOut.AlignTrailing(), zoomIn.AlignTrailing()).Spacing(-1); }