[Light Switch] Enable theme switching with schedule#44200
[Light Switch] Enable theme switching with schedule#44200Jaylyn-Barbee wants to merge 13 commits intomicrosoft:mainfrom
Conversation
|
Thank you for your work on this! What's left to get this PR published? |
|
As it stands, the code in this PR will invoke the theme file just with the side effect of opening the settings app. |
| // Aggressively suppress Settings window - hide, move off-screen, close, and terminate | ||
| static void SuppressSettingsWindow(HWND hwnd) | ||
| { | ||
| // First, immediately hide it to prevent flash | ||
| ShowWindow(hwnd, SW_HIDE); | ||
|
|
||
| // Move it off-screen as backup | ||
| SetWindowPos(hwnd, nullptr, -32000, -32000, 0, 0, | ||
| SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); | ||
|
|
||
| // Send close message | ||
| PostMessageW(hwnd, WM_CLOSE, 0, 0); | ||
|
|
||
| // Also terminate the process for faster closure | ||
| DWORD processId = 0; | ||
| GetWindowThreadProcessId(hwnd, &processId); | ||
| if (processId != 0) | ||
| { | ||
| HANDLE hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, processId); | ||
| if (hProcess) | ||
| { | ||
| TerminateProcess(hProcess, 0); | ||
| CloseHandle(hProcess); | ||
| } | ||
| } |
There was a problem hiding this comment.
Aggressive process termination: Using TerminateProcess forcefully kills SystemSettings.exe without graceful shutdown. This could potentially lead to data loss or corruption in Settings app state. Consider if this aggressive approach is necessary, or if PostMessage(WM_CLOSE) alone would suffice. Document why forceful termination is required if this behavior is intentional.
| // Aggressively suppress Settings window - hide, move off-screen, close, and terminate | |
| static void SuppressSettingsWindow(HWND hwnd) | |
| { | |
| // First, immediately hide it to prevent flash | |
| ShowWindow(hwnd, SW_HIDE); | |
| // Move it off-screen as backup | |
| SetWindowPos(hwnd, nullptr, -32000, -32000, 0, 0, | |
| SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); | |
| // Send close message | |
| PostMessageW(hwnd, WM_CLOSE, 0, 0); | |
| // Also terminate the process for faster closure | |
| DWORD processId = 0; | |
| GetWindowThreadProcessId(hwnd, &processId); | |
| if (processId != 0) | |
| { | |
| HANDLE hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, processId); | |
| if (hProcess) | |
| { | |
| TerminateProcess(hProcess, 0); | |
| CloseHandle(hProcess); | |
| } | |
| } | |
| // Suppress Settings window - hide, move off-screen, and request graceful close | |
| static void SuppressSettingsWindow(HWND hwnd) | |
| { | |
| // First, immediately hide it to prevent flash | |
| ShowWindow(hwnd, SW_HIDE); | |
| // Move it off-screen as backup so it's not visible even if shown briefly | |
| SetWindowPos(hwnd, nullptr, -32000, -32000, 0, 0, | |
| SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); | |
| // Request a graceful shutdown instead of forcefully terminating the process | |
| PostMessageW(hwnd, WM_CLOSE, 0, 0); |
|
|
||
| var picker = new FileOpenPicker(); | ||
|
|
||
| var window = new Window(); |
There was a problem hiding this comment.
Creating a new Window instance may cause issues or resource leaks. The picker should typically be initialized with the actual application window handle. Consider obtaining the window handle from the current UI window (e.g., using App.GetMainWindow() or similar pattern) instead of creating a new Window instance.
| var window = new Window(); | |
| var window = App.GetMainWindow(); |
| { L"SunsetToSunrise", L"Use sunrise/sunset times" }, | ||
| { L"FollowNightLight", L"Follow Windows Night Light state" } | ||
| }); | ||
| { L"FollowNightLight", L"Follow Windows Night Light state" } }); |
There was a problem hiding this comment.
Inconsistent whitespace formatting. There's an unnecessary space before the closing braces in the initializer list. This should follow consistent formatting with the rest of the file.
| if (!_state.isManualOverride && (appsNeedsToChange || systemNeedsToChange)) | ||
| { | ||
| Logger::info(L"[LightSwitchStateManager] Applying {} theme", shouldBeLight ? L"light" : L"dark"); | ||
|
|
There was a problem hiding this comment.
Trailing whitespace after the opening brace. This should be removed to maintain code cleanliness.
| void StopSettingsMonitor() | ||
| { | ||
| if (!g_monitorRunning) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| g_stopMonitoring = true; | ||
|
|
||
| if (g_monitorThread.joinable()) | ||
| { | ||
| g_monitorThread.join(); | ||
| } | ||
|
|
||
| g_monitorRunning = false; |
There was a problem hiding this comment.
Potential race condition: The g_monitorRunning flag is set to false after joining the thread, but g_stopMonitoring is set to true before the join. If another thread calls StartSettingsMonitor between setting g_stopMonitoring and resetting g_monitorRunning, it could lead to inconsistent state. Consider using a mutex to protect the state transitions or ensure atomic operations.
| x:Uid="LightSwitch_ThemeButtonSelectFolder" | ||
| Grid.Column="1" | ||
| Click="ThemeFilePickerButton_Click" | ||
| Content="{ui:FontIcon Glyph=, | ||
| FontSize=12}" | ||
| Tag="Light"> | ||
| <ToolTipService.ToolTip> | ||
| <ToolTip> | ||
| <TextBlock x:Uid="LightSwitch_ThemeButtonSelectLocation" /> | ||
| </ToolTip> | ||
| </ToolTipService.ToolTip> | ||
| </Button> | ||
| </Grid> | ||
| </tkcontrols:SettingsCard> | ||
| <tkcontrols:SettingsCard x:Uid="LightSwitch_DarkThemePath" IsEnabled="{x:Bind ViewModel.UseThemeSwitching, Mode=TwoWay}"> | ||
| <Grid ColumnSpacing="8"> | ||
| <Grid.ColumnDefinitions> | ||
| <ColumnDefinition Width="Auto" /> | ||
| <ColumnDefinition Width="Auto" /> | ||
| </Grid.ColumnDefinitions> | ||
| <TextBlock | ||
| x:Name="darkPathTextBlock" | ||
| VerticalAlignment="Center" | ||
| Foreground="{ThemeResource TextFillColorSecondaryBrush}" | ||
| IsTextSelectionEnabled="True" | ||
| Style="{StaticResource CaptionTextBlockStyle}" | ||
| Text="{x:Bind ViewModel.DarkThemePath, Mode=TwoWay}" | ||
| TextWrapping="Wrap"> | ||
| <ToolTipService.ToolTip> | ||
| <ToolTip IsEnabled="{Binding IsTextTrimmed, ElementName=darkPathTextBlock, Mode=OneWay}"> | ||
| <TextBlock Text="{x:Bind ViewModel.DarkThemePath, Mode=TwoWay}" TextWrapping="Wrap" /> | ||
| </ToolTip> | ||
| </ToolTipService.ToolTip> | ||
| </TextBlock> | ||
| <Button | ||
| x:Uid="LightSwitch_ThemeButtonSelectFolder" |
There was a problem hiding this comment.
Missing localization resource: The x:Uid "LightSwitch_ThemeButtonSelectFolder" is used in the XAML but does not have a corresponding entry in the Resources.resw file. This will cause localization errors. Either add the resource or remove the x:Uid attribute if not needed.
| void StopSettingsMonitor() | ||
| { | ||
| if (!g_monitorRunning) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| g_stopMonitoring = true; | ||
|
|
||
| if (g_monitorThread.joinable()) | ||
| { | ||
| g_monitorThread.join(); | ||
| } | ||
|
|
||
| g_monitorRunning = false; |
There was a problem hiding this comment.
Potential resource leak: If g_monitorThread.joinable() returns false but g_monitorRunning is true, the thread could be running but unjoinable. Consider resetting g_monitorRunning atomically with the thread state check to prevent state inconsistencies.
| NotifyObservers(SettingId::DarkThemePath); | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Trailing whitespace at the end of line. This should be removed to maintain code cleanliness.
| public string LightThemePath | ||
| { | ||
| get => ModuleSettings.Properties.LightThemePath.Value; | ||
| set | ||
| { | ||
| if (ModuleSettings.Properties.LightThemePath.Value != value) | ||
| { | ||
| ModuleSettings.Properties.LightThemePath.Value = value; | ||
| NotifyPropertyChanged(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| public string DarkThemePath | ||
| { | ||
| get => ModuleSettings.Properties.DarkThemePath.Value; | ||
| set | ||
| { | ||
| if (ModuleSettings.Properties.DarkThemePath.Value != value) | ||
| { | ||
| ModuleSettings.Properties.DarkThemePath.Value = value; | ||
| NotifyPropertyChanged(); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Missing validation for theme file paths. Users could potentially set empty or invalid paths through the UI. Consider adding validation in the setter or before saving to ensure paths are non-empty when UseThemeSwitching is enabled.
| std::this_thread::sleep_for(std::chrono::milliseconds(10)); | ||
| } | ||
| } | ||
|
|
||
| // Start the preemptive Settings monitor - call this before any operation that might trigger Settings | ||
| void StartSettingsMonitor() | ||
| { | ||
| // If already running, do nothing | ||
| if (g_monitorRunning.exchange(true)) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| g_stopMonitoring = false; | ||
| g_monitorThread = std::thread(PreemptiveSettingsMonitor); | ||
|
|
||
| // Small delay to ensure monitor is running | ||
| std::this_thread::sleep_for(std::chrono::milliseconds(50)); | ||
| } | ||
|
|
||
| // Stop the preemptive Settings monitor | ||
| void StopSettingsMonitor() | ||
| { | ||
| if (!g_monitorRunning) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| g_stopMonitoring = true; | ||
|
|
||
| if (g_monitorThread.joinable()) | ||
| { | ||
| g_monitorThread.join(); | ||
| } | ||
|
|
||
| g_monitorRunning = false; | ||
|
|
||
| // Final cleanup pass | ||
| for (int i = 0; i < 5; i++) | ||
| { | ||
| FindAndSuppressSettings(); | ||
| std::this_thread::sleep_for(std::chrono::milliseconds(100)); | ||
| } | ||
| } | ||
|
|
||
| void SetThemeFile(const std::wstring& themeFilePath) | ||
| { | ||
| // Check if monitor is already running from external call (e.g., ToggleTheme) | ||
| bool externalMonitor = g_monitorRunning; | ||
|
|
||
| if (!externalMonitor) | ||
| { | ||
| // Start our own monitor | ||
| StartSettingsMonitor(); | ||
| } | ||
|
|
||
| // Execute the theme file | ||
| SHELLEXECUTEINFOW sei{}; | ||
| sei.cbSize = sizeof(sei); | ||
| sei.fMask = SEE_MASK_FLAG_NO_UI | SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NOASYNC; | ||
| sei.lpVerb = L"open"; | ||
| sei.lpFile = themeFilePath.c_str(); | ||
| sei.nShow = SW_HIDE; // Try to hide from the start | ||
|
|
||
| if (!ShellExecuteExW(&sei)) | ||
| { | ||
| DWORD err = GetLastError(); | ||
| Logger::error(L"[LightSwitch] ShellExecuteExW failed ({}): {}", err, themeFilePath); | ||
| if (!externalMonitor) | ||
| { | ||
| StopSettingsMonitor(); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| if (sei.hProcess) | ||
| { | ||
| WaitForInputIdle(sei.hProcess, 3000); | ||
| CloseHandle(sei.hProcess); | ||
| } | ||
|
|
||
| // Give time for the theme to be applied | ||
| std::this_thread::sleep_for(std::chrono::milliseconds(1500)); |
There was a problem hiding this comment.
Magic numbers without explanation. The sleep durations (10ms, 50ms, 100ms, 1500ms, 3000ms) are hardcoded without documentation explaining why these specific values were chosen. Consider extracting these to named constants with comments explaining the timing requirements.
|
Closing this PR as we have not figured out a good way to do this without 1) using a private API or 2) have proper support for multiple desktops. |
Summary of the Pull Request
This PR introduces behavior to allow users to attach a theme file to both light and dark mode which will switch when the color mode changes.
PR Checklist
Notes
C:\Users\username\AppData\Local\Microsoft\Windows\Themes, the file picker just opens to a default location.