diff --git a/AppApi.cs b/AppApi.cs index ae676c02..2dc14060 100644 --- a/AppApi.cs +++ b/AppApi.cs @@ -415,6 +415,11 @@ namespace VRCX return System.Globalization.CultureInfo.CurrentCulture.ToString(); } + public void ChangeTheme(int value) + { + WinformThemer.SetGlobalTheme(value); + } + public void SetStartup(bool enabled) { try diff --git a/MainForm.cs b/MainForm.cs index 595e701b..19b1a47c 100644 --- a/MainForm.cs +++ b/MainForm.cs @@ -13,7 +13,7 @@ using CefSharp.WinForms; namespace VRCX { - public partial class MainForm : Form + public partial class MainForm : WinformBase { public static MainForm Instance; public ChromiumWebBrowser Browser; diff --git a/VRCX.csproj b/VRCX.csproj index a9af5232..5cde8085 100644 --- a/VRCX.csproj +++ b/VRCX.csproj @@ -1,4 +1,4 @@ - + @@ -119,6 +119,10 @@ + + Form + + VRForm.cs diff --git a/VRForm.cs b/VRForm.cs index a8fbda98..9e543640 100644 --- a/VRForm.cs +++ b/VRForm.cs @@ -10,7 +10,7 @@ using CefSharp.WinForms; namespace VRCX { - public partial class VRForm : Form + public partial class VRForm : WinformBase { public static VRForm Instance; private ChromiumWebBrowser _browser1; diff --git a/WinformBase.cs b/WinformBase.cs new file mode 100644 index 00000000..4865e5b1 --- /dev/null +++ b/WinformBase.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace VRCX +{ + public class WinformBase : Form + { + protected override void OnHandleCreated(EventArgs e) + { + WinformThemer.SetThemeToGlobal(this); + base.OnHandleCreated(e); + } + } +} diff --git a/WinformThemer.cs b/WinformThemer.cs new file mode 100644 index 00000000..057908d7 --- /dev/null +++ b/WinformThemer.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace VRCX +{ + //Based off DWMWA_USE_IMMERSIVE_DARK_MODE, documentation: https://docs.microsoft.com/en-us/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute + //dwAttribute was 19 before Windows 20H1, 20 after Windows 20H1 + + internal static class WinformThemer + { + /// + /// Private holder of current theme + /// + private static int currentTheme = 0; + + /// + /// Sets the global theme of the app + /// Light = 0 + /// Dark = 1 + /// + public static void SetGlobalTheme(int theme) + { + currentTheme = theme; + + //Make a seperate list for all current forms (causes issues otherwise) + List
forms = new List(); + foreach(Form form in Application.OpenForms) + { + forms.Add(form); + } + + SetThemeToGlobal(forms); + } + + /// + /// Gets the global theme of the app + /// Light = 0 + /// Dark = 1 + /// + public static int GetGlobalTheme() => currentTheme; + + /// + /// Set given form to the current global theme + /// + /// + public static void SetThemeToGlobal(Form form) + { + SetThemeToGlobal(new List() { form }); + } + + /// + /// Set a list of given forms to the current global theme + /// + /// + public static void SetThemeToGlobal(List forms) + { + //For each form, set the theme, then move focus onto it to force refresh + foreach(Form form in forms) + { + //Set the theme of the window + SetThemeToGlobal(form.Handle); + + //Change opacity to foce full redraw + form.Opacity = 0.99999; + form.Opacity = 1; + } + } + + private static void SetThemeToGlobal(IntPtr handle) + { + if (GetTheme(handle) != currentTheme) + { + if (PInvoke.DwmSetWindowAttribute(handle, 19, new[] { currentTheme }, 4) != 0) + PInvoke.DwmSetWindowAttribute(handle, 20, new[] { currentTheme }, 4); + } + } + + private static int GetTheme(IntPtr handle) + { + //Allocate needed memory + IntPtr curThemePtr = Marshal.AllocHGlobal(4); + + //See what window state it currently is + if (PInvoke.DwmGetWindowAttribute(handle, 19, curThemePtr, 4) != 0) + PInvoke.DwmGetWindowAttribute(handle, 20, curThemePtr, 4); + + //Read current theme (light = 0, dark = 1) + int theme = Marshal.ReadInt32(curThemePtr); + + //Free previously allocated + Marshal.FreeHGlobal(curThemePtr); + + return theme; + } + + internal static class PInvoke + { + [DllImport("DwmApi")] + internal static extern int DwmSetWindowAttribute(IntPtr hwnd, int dwAttribute, int[] pvAttribute, int cbAttribute); + + [DllImport("DwmApi")] + internal static extern int DwmGetWindowAttribute(IntPtr hwnd, int dwAttribute, IntPtr pvAttribute, int cbAttribute); + } + } +} diff --git a/html/src/app.js b/html/src/app.js index b89e03d4..41978a7e 100644 --- a/html/src/app.js +++ b/html/src/app.js @@ -11397,8 +11397,18 @@ speechSynthesis.getVoices(); $app.watch.isDarkMode = function () { configRepository.setBool('isDarkMode', this.isDarkMode); $appDarkStyle.disabled = this.isDarkMode === false; + if (this.isDarkMode) { + AppApi.ChangeTheme(1); + } else { + AppApi.ChangeTheme(0); + } this.updateVRConfigVars(); }; + if ($app.data.isDarkMode) { + AppApi.ChangeTheme(1); + } else { + AppApi.ChangeTheme(0); + } window .matchMedia('(prefers-color-scheme: dark)') .addEventListener('change', (e) => {