From 01fb831e4efb7d50a4b321934ecb5d204f020fd7 Mon Sep 17 00:00:00 2001 From: Jaylyn Barbee <51131738+Jaylyn-Barbee@users.noreply.github.com> Date: Tue, 28 Oct 2025 15:00:38 -0400 Subject: [PATCH] [Light Switch] Light Switch should detect changes in Windows Settings and treat as manual override (same as using shortcut) (#42882) ## Summary of the Pull Request This PR ensures that Light Switch detects changes to app/system theme from Windows Settings. This PR also introduces new behavior where switching the schedule will cause an instant update to the theme if necessary. ## PR Checklist - [x] Closes: #42878 - [x] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx --- .../LightSwitchService/LightSwitchService.cpp | 305 ++++++++++++------ .../LightSwitchService.vcxproj | 2 + .../LightSwitchService.vcxproj.filters | 6 + .../LightSwitchServiceObserver.cpp | 29 ++ .../LightSwitchServiceObserver.h | 16 + .../LightSwitchSettings.cpp | 6 + .../LightSwitchService/LightSwitchSettings.h | 2 +- .../LightSwitchService/SettingsObserver.h | 3 +- 8 files changed, 265 insertions(+), 104 deletions(-) create mode 100644 src/modules/LightSwitch/LightSwitchService/LightSwitchServiceObserver.cpp create mode 100644 src/modules/LightSwitch/LightSwitchService/LightSwitchServiceObserver.h diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchService.cpp b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.cpp index 7ebe4a67eb..9d51a6cf75 100644 --- a/src/modules/LightSwitch/LightSwitchService/LightSwitchService.cpp +++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include "ThemeScheduler.h" #include "ThemeHelper.h" @@ -11,11 +11,12 @@ #include #include #include +#include SERVICE_STATUS g_ServiceStatus = {}; SERVICE_STATUS_HANDLE g_StatusHandle = nullptr; HANDLE g_ServiceStopEvent = nullptr; -static int g_lastUpdatedDay = -1; +extern int g_lastUpdatedDay = -1; static ScheduleMode prevMode = ScheduleMode::Off; static std::wstring prevLat, prevLon; @@ -161,25 +162,18 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam) Logger::info(L"[LightSwitchService] Worker thread starting..."); Logger::info(L"[LightSwitchService] Parent PID: {}", parentPid); - // Initialize settings system LightSwitchSettings::instance().InitFileWatcher(); - // Open the manual override event created by the module interface + LightSwitchServiceObserver observer({ SettingId::LightTime, + SettingId::DarkTime, + SettingId::ScheduleMode, + SettingId::Sunrise_Offset, + SettingId::Sunset_Offset }); + HANDLE hManualOverride = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE"); auto applyTheme = [](int nowMinutes, int lightMinutes, int darkMinutes, const auto& settings) { - bool isLightActive = false; - - if (lightMinutes < darkMinutes) - { - // Normal case: sunrise < sunset - isLightActive = (nowMinutes >= lightMinutes && nowMinutes < darkMinutes); - } - else - { - // Wraparound case: e.g. light at 21:00, dark at 06:00 - isLightActive = (nowMinutes >= lightMinutes || nowMinutes < darkMinutes); - } + bool isLightActive = (lightMinutes < darkMinutes) ? (nowMinutes >= lightMinutes && nowMinutes < darkMinutes) : (nowMinutes >= lightMinutes || nowMinutes < darkMinutes); bool isSystemCurrentlyLight = GetCurrentSystemTheme(); bool isAppsCurrentlyLight = GetCurrentAppsTheme(); @@ -212,85 +206,72 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam) } }; - // --- Initial settings load --- LightSwitchSettings::instance().LoadSettings(); auto& settings = LightSwitchSettings::instance().settings(); - // --- Initial theme application (if schedule enabled) --- + SYSTEMTIME st; + GetLocalTime(&st); + int nowMinutes = st.wHour * 60 + st.wMinute; + if (settings.scheduleMode != ScheduleMode::Off) { - SYSTEMTIME st; - GetLocalTime(&st); - int nowMinutes = st.wHour * 60 + st.wMinute; - applyTheme(nowMinutes, settings.lightTime + settings.sunrise_offset, settings.darkTime + settings.sunset_offset, settings); + applyTheme(nowMinutes, + settings.lightTime + settings.sunrise_offset, + settings.darkTime + settings.sunset_offset, + settings); + Logger::trace(L"[LightSwitchService] Initialized g_lastUpdatedDay = {}", g_lastUpdatedDay); } else { Logger::info(L"[LightSwitchService] Schedule mode is OFF - ticker suspended, waiting for manual action or mode change."); } - // --- Main loop --- + g_lastUpdatedDay = st.wDay; + ULONGLONG lastSettingsReload = 0; + for (;;) { HANDLE waits[2] = { g_ServiceStopEvent, hParent }; DWORD count = hParent ? 2 : 1; + bool skipRest = false; - LightSwitchSettings::instance().LoadSettings(); const auto& settings = LightSwitchSettings::instance().settings(); - // Check for changes in schedule mode or coordinates - bool modeChangedToSunset = (prevMode != settings.scheduleMode && - settings.scheduleMode == ScheduleMode::SunsetToSunrise); - bool coordsChanged = (prevLat != settings.latitude || prevLon != settings.longitude); + bool scheduleJustEnabled = (prevMode == ScheduleMode::Off && settings.scheduleMode != ScheduleMode::Off); + prevMode = settings.scheduleMode; - if ((modeChangedToSunset || coordsChanged) && settings.scheduleMode == ScheduleMode::SunsetToSunrise) - { - Logger::info(L"[LightSwitchService] Mode or coordinates changed, recalculating sun times."); - update_sun_times(settings); - SYSTEMTIME st; - GetLocalTime(&st); - g_lastUpdatedDay = st.wDay; - prevMode = settings.scheduleMode; - prevLat = settings.latitude; - prevLon = settings.longitude; - } - - // If schedule is off, idle but keep watching settings and manual override + // ─── Handle "Schedule Off" Mode ───────────────────────────────────────────── if (settings.scheduleMode == ScheduleMode::Off) { Logger::info(L"[LightSwitchService] Schedule mode OFF - suspending scheduler but keeping service alive."); if (!hManualOverride) - { hManualOverride = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE"); - } - HANDLE waits[4]; - DWORD count = 0; - waits[count++] = g_ServiceStopEvent; + HANDLE waitsOff[4]; + DWORD countOff = 0; + waitsOff[countOff++] = g_ServiceStopEvent; if (hParent) - waits[count++] = hParent; + waitsOff[countOff++] = hParent; if (hManualOverride) - waits[count++] = hManualOverride; - waits[count++] = LightSwitchSettings::instance().GetSettingsChangedEvent(); + waitsOff[countOff++] = hManualOverride; + waitsOff[countOff++] = LightSwitchSettings::instance().GetSettingsChangedEvent(); for (;;) { - DWORD wait = WaitForMultipleObjects(count, waits, FALSE, INFINITE); + DWORD wait = WaitForMultipleObjects(countOff, waitsOff, FALSE, INFINITE); - // --- Handle exit signals --- - if (wait == WAIT_OBJECT_0) // stop event + if (wait == WAIT_OBJECT_0) { Logger::info(L"[LightSwitchService] Stop event triggered - exiting worker loop."); - break; + goto cleanup; } if (hParent && wait == WAIT_OBJECT_0 + 1) { Logger::info(L"[LightSwitchService] Parent exited - stopping service."); - break; + goto cleanup; } - // --- Manual override triggered --- if (wait == WAIT_OBJECT_0 + (hParent ? 2 : 1)) { Logger::info(L"[LightSwitchService] Manual override received while schedule OFF."); @@ -298,15 +279,13 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam) continue; } - // --- Settings file changed --- if (wait == WAIT_OBJECT_0 + (hParent ? 3 : 2)) { Logger::trace(L"[LightSwitchService] Settings change event triggered, reloading settings..."); - ResetEvent(LightSwitchSettings::instance().GetSettingsChangedEvent()); - LightSwitchSettings::instance().LoadSettings(); const auto& newSettings = LightSwitchSettings::instance().settings(); + lastSettingsReload = GetTickCount64(); if (newSettings.scheduleMode != ScheduleMode::Off) { @@ -315,63 +294,137 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam) } } } + + continue; } + // ─── Normal Schedule Loop ─────────────────────────────────────────────────── + ULONGLONG nowTick = GetTickCount64(); + bool recentSettingsReload = (nowTick - lastSettingsReload < 5000); - // --- When schedule is active, run once per minute --- - SYSTEMTIME st; - GetLocalTime(&st); - int nowMinutes = st.wHour * 60 + st.wMinute; - - // Refresh suntimes at day boundary - if ((g_lastUpdatedDay != st.wDay) && (settings.scheduleMode == ScheduleMode::SunsetToSunrise)) + if (g_lastUpdatedDay != -1) { - update_sun_times(settings); - g_lastUpdatedDay = st.wDay; - Logger::info(L"[LightSwitchService] Recalculated sun times at new day boundary."); - } + bool manualOverrideActive = (hManualOverride && WaitForSingleObject(hManualOverride, 0) == WAIT_OBJECT_0); - // Have to do this again in case settings got updated in the refresh suntimes chunk - LightSwitchSettings::instance().LoadSettings(); - const auto& currentSettings = LightSwitchSettings::instance().settings(); - - wchar_t msg[160]; - swprintf_s(msg, - L"[LightSwitchService] now=%02d:%02d | light=%02d:%02d | dark=%02d:%02d | mode=%d", - st.wHour, - st.wMinute, - currentSettings.lightTime / 60, - currentSettings.lightTime % 60, - currentSettings.darkTime / 60, - currentSettings.darkTime % 60, - static_cast(currentSettings.scheduleMode)); - Logger::info(msg); - - // --- Manual override check --- - bool manualOverrideActive = false; - if (hManualOverride) - { - manualOverrideActive = (WaitForSingleObject(hManualOverride, 0) == WAIT_OBJECT_0); - } - - if (manualOverrideActive) - { - if (nowMinutes == (currentSettings.lightTime + currentSettings.sunrise_offset) % 1440 || - nowMinutes == (currentSettings.darkTime + currentSettings.sunset_offset) % 1440) + if (settings.scheduleMode != ScheduleMode::Off && !recentSettingsReload && !scheduleJustEnabled) { - ResetEvent(hManualOverride); - Logger::info(L"[LightSwitchService] Manual override cleared at boundary"); + Logger::debug(L"[LightSwitchService] Checking if manual override is active..."); + bool manualOverrideActive = (hManualOverride && WaitForSingleObject(hManualOverride, 0) == WAIT_OBJECT_0); + Logger::debug(L"[LightSwitchService] Manual override active = {}", manualOverrideActive); + + if (!manualOverrideActive) + { + bool currentSystemTheme = GetCurrentSystemTheme(); + bool currentAppsTheme = GetCurrentAppsTheme(); + + SYSTEMTIME st; + GetLocalTime(&st); + int nowMinutes = st.wHour * 60 + st.wMinute; + + bool shouldBeLight = (settings.lightTime < settings.darkTime) ? (nowMinutes >= settings.lightTime && nowMinutes < settings.darkTime) : (nowMinutes >= settings.lightTime || nowMinutes < settings.darkTime); + + Logger::debug(L"[LightSwitchService] shouldBeLight = {}", shouldBeLight); + + if ((settings.changeSystem && (currentSystemTheme != shouldBeLight)) || + (settings.changeApps && (currentAppsTheme != shouldBeLight))) + { + Logger::debug(L"[LightSwitchService] External theme change detected - enabling manual override"); + + if (!hManualOverride) + { + hManualOverride = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE"); + if (!hManualOverride) + hManualOverride = CreateEventW(nullptr, TRUE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE"); + } + + if (hManualOverride) + { + SetEvent(hManualOverride); + Logger::info(L"[LightSwitchService] Detected manual theme change outside of LightSwitch. Triggering manual override."); + skipRest = true; + } + } + } } else { - Logger::info(L"[LightSwitchService] Skipping schedule due to manual override"); - goto sleep_until_next_minute; + Logger::debug(L"[LightSwitchService] Skipping external-change detection (schedule off, recent reload, or just enabled)."); } } - applyTheme(nowMinutes, currentSettings.lightTime + currentSettings.sunrise_offset, currentSettings.darkTime + currentSettings.sunset_offset, currentSettings); + // ─── Apply Schedule Logic ─────────────────────────────────────────────────── + if (!skipRest) + { + bool modeChangedToSunset = (prevMode != settings.scheduleMode && + settings.scheduleMode == ScheduleMode::SunsetToSunrise); + bool coordsChanged = (prevLat != settings.latitude || prevLon != settings.longitude); - sleep_until_next_minute: + if ((modeChangedToSunset || coordsChanged) && settings.scheduleMode == ScheduleMode::SunsetToSunrise) + { + Logger::info(L"[LightSwitchService] Mode or coordinates changed, recalculating sun times."); + update_sun_times(settings); + SYSTEMTIME st; + GetLocalTime(&st); + g_lastUpdatedDay = st.wDay; + prevMode = settings.scheduleMode; + prevLat = settings.latitude; + prevLon = settings.longitude; + } + + SYSTEMTIME st; + GetLocalTime(&st); + int nowMinutes = st.wHour * 60 + st.wMinute; + + if ((g_lastUpdatedDay != st.wDay) && (settings.scheduleMode == ScheduleMode::SunsetToSunrise)) + { + update_sun_times(settings); + g_lastUpdatedDay = st.wDay; + Logger::info(L"[LightSwitchService] Recalculated sun times at new day boundary."); + } + + LightSwitchSettings::instance().LoadSettings(); + const auto& currentSettings = LightSwitchSettings::instance().settings(); + + wchar_t msg[160]; + swprintf_s(msg, + L"[LightSwitchService] now=%02d:%02d | light=%02d:%02d | dark=%02d:%02d | mode=%d", + st.wHour, + st.wMinute, + currentSettings.lightTime / 60, + currentSettings.lightTime % 60, + currentSettings.darkTime / 60, + currentSettings.darkTime % 60, + static_cast(currentSettings.scheduleMode)); + Logger::info(msg); + + bool manualOverrideActive = false; + if (hManualOverride) + manualOverrideActive = (WaitForSingleObject(hManualOverride, 0) == WAIT_OBJECT_0); + + if (manualOverrideActive) + { + if (nowMinutes == (currentSettings.lightTime + currentSettings.sunrise_offset) % 1440 || + nowMinutes == (currentSettings.darkTime + currentSettings.sunset_offset) % 1440) + { + ResetEvent(hManualOverride); + Logger::info(L"[LightSwitchService] Manual override cleared at boundary"); + } + else + { + Logger::info(L"[LightSwitchService] Skipping schedule due to manual override"); + skipRest = true; + } + } + + if (!skipRest) + applyTheme(nowMinutes, + currentSettings.lightTime + currentSettings.sunrise_offset, + currentSettings.darkTime + currentSettings.sunset_offset, + currentSettings); + } + + // ─── Wait For Next Minute Tick Or Stop Event ──────────────────────────────── + SYSTEMTIME st; GetLocalTime(&st); int msToNextMinute = (60 - st.wSecond) * 1000 - st.wMilliseconds; if (msToNextMinute < 50) @@ -390,6 +443,7 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam) } } +cleanup: if (hManualOverride) CloseHandle(hManualOverride); if (hParent) @@ -398,6 +452,53 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam) return 0; } +void ApplyThemeNow() +{ + LightSwitchSettings::instance().LoadSettings(); + const auto& settings = LightSwitchSettings::instance().settings(); + + SYSTEMTIME st; + GetLocalTime(&st); + int nowMinutes = st.wHour * 60 + st.wMinute; + + bool shouldBeLight = false; + if (settings.lightTime < settings.darkTime) + shouldBeLight = (nowMinutes >= settings.lightTime && nowMinutes < settings.darkTime); + else + shouldBeLight = (nowMinutes >= settings.lightTime || nowMinutes < settings.darkTime); + + bool isSystemCurrentlyLight = GetCurrentSystemTheme(); + bool isAppsCurrentlyLight = GetCurrentAppsTheme(); + + Logger::info(L"[LightSwitchService] Applying (if needed) theme immediately due to schedule change."); + + if (shouldBeLight) + { + if (settings.changeSystem && !isSystemCurrentlyLight) + { + SetSystemTheme(true); + Logger::info(L"[LightSwitchService] Changing system theme to light mode."); + } + if (settings.changeApps && !isAppsCurrentlyLight) + { + SetAppsTheme(true); + Logger::info(L"[LightSwitchService] Changing apps theme to light mode."); + } + } + else + { + if (settings.changeSystem && isSystemCurrentlyLight) + { + SetSystemTheme(false); + Logger::info(L"[LightSwitchService] Changing system theme to dark mode."); + } + if (settings.changeApps && isAppsCurrentlyLight) + { + SetAppsTheme(false); + Logger::info(L"[LightSwitchService] Changing apps theme to dark mode."); + } + } +} int APIENTRY wWinMain(HINSTANCE, HINSTANCE, PWSTR, int) { diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj index b082250f61..832f8ab50c 100644 --- a/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj +++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj @@ -74,6 +74,7 @@ + @@ -84,6 +85,7 @@ + diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj.filters b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj.filters index f5aa05afc3..7aa39aa0c2 100644 --- a/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj.filters +++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj.filters @@ -33,6 +33,9 @@ Source Files + + Source Files + @@ -53,6 +56,9 @@ Header Files + + Header Files + diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchServiceObserver.cpp b/src/modules/LightSwitch/LightSwitchService/LightSwitchServiceObserver.cpp new file mode 100644 index 0000000000..e28a2625fe --- /dev/null +++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchServiceObserver.cpp @@ -0,0 +1,29 @@ +#include "LightSwitchServiceObserver.h" +#include +#include "LightSwitchSettings.h" + +// These are defined elsewhere in your service module (ServiceWorkerThread.cpp) +extern int g_lastUpdatedDay; +void ApplyThemeNow(); + +void LightSwitchServiceObserver::SettingsUpdate(SettingId id) +{ + Logger::info(L"[LightSwitchService] Setting changed: {}", static_cast(id)); + g_lastUpdatedDay = -1; + ApplyThemeNow(); +} + +bool LightSwitchServiceObserver::WantsToBeNotified(SettingId id) const noexcept +{ + switch (id) + { + case SettingId::LightTime: + case SettingId::DarkTime: + case SettingId::ScheduleMode: + case SettingId::Sunrise_Offset: + case SettingId::Sunset_Offset: + return true; + default: + return false; + } +} diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchServiceObserver.h b/src/modules/LightSwitch/LightSwitchService/LightSwitchServiceObserver.h new file mode 100644 index 0000000000..14c88e656d --- /dev/null +++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchServiceObserver.h @@ -0,0 +1,16 @@ +#pragma once + +#include "SettingsObserver.h" + +// The LightSwitchServiceObserver reacts when LightSwitchSettings changes. +class LightSwitchServiceObserver : public SettingsObserver +{ +public: + explicit LightSwitchServiceObserver(std::unordered_set observedSettings) : + SettingsObserver(std::move(observedSettings)) + { + } + + void SettingsUpdate(SettingId id) override; + bool WantsToBeNotified(SettingId id) const noexcept override; +}; diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.cpp b/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.cpp index a7f44cca6d..2e00417001 100644 --- a/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.cpp +++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.cpp @@ -39,6 +39,7 @@ void LightSwitchSettings::InitFileWatcher() GetSettingsFileName(), [this]() { Logger::info(L"[LightSwitchSettings] Settings file changed, signaling event."); + LoadSettings(); SetEvent(m_settingsChangedEvent); }); } @@ -65,6 +66,11 @@ void LightSwitchSettings::NotifyObservers(SettingId id) const } } +HANDLE LightSwitchSettings::GetSettingsChangedEvent() const +{ + return m_settingsChangedEvent; +} + void LightSwitchSettings::LoadSettings() { try diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.h b/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.h index 32d011313f..e5e993c696 100644 --- a/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.h +++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.h @@ -79,7 +79,7 @@ public: void LoadSettings(); - HANDLE GetSettingsChangedEvent() const { return m_settingsChangedEvent; } + HANDLE GetSettingsChangedEvent() const; private: LightSwitchSettings(); diff --git a/src/modules/LightSwitch/LightSwitchService/SettingsObserver.h b/src/modules/LightSwitch/LightSwitchService/SettingsObserver.h index 88d0194eef..b0ddde72ec 100644 --- a/src/modules/LightSwitch/LightSwitchService/SettingsObserver.h +++ b/src/modules/LightSwitch/LightSwitchService/SettingsObserver.h @@ -2,6 +2,7 @@ #include #include "SettingsConstants.h" +#include "LightSwitchSettings.h" class LightSwitchSettings; @@ -22,7 +23,7 @@ public: // Override this in your class to respond to updates virtual void SettingsUpdate(SettingId type) {} - bool WantsToBeNotified(SettingId type) const noexcept + virtual bool WantsToBeNotified(SettingId type) const noexcept { return m_observedSettings.contains(type); }