[Light Switch] Light Switch should detect changes in Windows Settings and treat as manual override (same as using shortcut) (#42882)

<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## 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.

<!-- Please review the items on the PR checklist before submitting-->
## 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
This commit is contained in:
Jaylyn Barbee
2025-10-28 15:00:38 -04:00
committed by GitHub
parent d197af3da9
commit 01fb831e4e
8 changed files with 265 additions and 104 deletions

View File

@@ -1,4 +1,4 @@
#include <windows.h>
#include <windows.h>
#include <tchar.h>
#include "ThemeScheduler.h"
#include "ThemeHelper.h"
@@ -11,11 +11,12 @@
#include <logger/logger_settings.h>
#include <logger/logger.h>
#include <utils/logger_helper.h>
#include <LightSwitchServiceObserver.h>
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,33 +206,155 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
}
};
// --- Initial settings load ---
LightSwitchSettings::instance().LoadSettings();
auto& settings = LightSwitchSettings::instance().settings();
// --- Initial theme application (if schedule enabled) ---
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);
if (settings.scheduleMode != ScheduleMode::Off)
{
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 scheduleJustEnabled = (prevMode == ScheduleMode::Off && settings.scheduleMode != ScheduleMode::Off);
prevMode = settings.scheduleMode;
// ─── 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 waitsOff[4];
DWORD countOff = 0;
waitsOff[countOff++] = g_ServiceStopEvent;
if (hParent)
waitsOff[countOff++] = hParent;
if (hManualOverride)
waitsOff[countOff++] = hManualOverride;
waitsOff[countOff++] = LightSwitchSettings::instance().GetSettingsChangedEvent();
for (;;)
{
DWORD wait = WaitForMultipleObjects(countOff, waitsOff, FALSE, INFINITE);
if (wait == WAIT_OBJECT_0)
{
Logger::info(L"[LightSwitchService] Stop event triggered - exiting worker loop.");
goto cleanup;
}
if (hParent && wait == WAIT_OBJECT_0 + 1)
{
Logger::info(L"[LightSwitchService] Parent exited - stopping service.");
goto cleanup;
}
if (wait == WAIT_OBJECT_0 + (hParent ? 2 : 1))
{
Logger::info(L"[LightSwitchService] Manual override received while schedule OFF.");
ResetEvent(hManualOverride);
continue;
}
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)
{
Logger::info(L"[LightSwitchService] Schedule re-enabled, resuming normal loop.");
break;
}
}
}
continue;
}
// ─── Normal Schedule Loop ───────────────────────────────────────────────────
ULONGLONG nowTick = GetTickCount64();
bool recentSettingsReload = (nowTick - lastSettingsReload < 5000);
if (g_lastUpdatedDay != -1)
{
bool manualOverrideActive = (hManualOverride && WaitForSingleObject(hManualOverride, 0) == WAIT_OBJECT_0);
if (settings.scheduleMode != ScheduleMode::Off && !recentSettingsReload && !scheduleJustEnabled)
{
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::debug(L"[LightSwitchService] Skipping external-change detection (schedule off, recent reload, or just enabled).");
}
}
// ─── Apply Schedule Logic ───────────────────────────────────────────────────
if (!skipRest)
{
bool modeChangedToSunset = (prevMode != settings.scheduleMode &&
settings.scheduleMode == ScheduleMode::SunsetToSunrise);
bool coordsChanged = (prevLat != settings.latitude || prevLon != settings.longitude);
@@ -255,75 +371,10 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
prevLon = settings.longitude;
}
// If schedule is off, idle but keep watching settings and manual override
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;
if (hParent)
waits[count++] = hParent;
if (hManualOverride)
waits[count++] = hManualOverride;
waits[count++] = LightSwitchSettings::instance().GetSettingsChangedEvent();
for (;;)
{
DWORD wait = WaitForMultipleObjects(count, waits, FALSE, INFINITE);
// --- Handle exit signals ---
if (wait == WAIT_OBJECT_0) // stop event
{
Logger::info(L"[LightSwitchService] Stop event triggered - exiting worker loop.");
break;
}
if (hParent && wait == WAIT_OBJECT_0 + 1)
{
Logger::info(L"[LightSwitchService] Parent exited - stopping service.");
break;
}
// --- Manual override triggered ---
if (wait == WAIT_OBJECT_0 + (hParent ? 2 : 1))
{
Logger::info(L"[LightSwitchService] Manual override received while schedule OFF.");
ResetEvent(hManualOverride);
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();
if (newSettings.scheduleMode != ScheduleMode::Off)
{
Logger::info(L"[LightSwitchService] Schedule re-enabled, resuming normal loop.");
break;
}
}
}
}
// --- 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))
{
update_sun_times(settings);
@@ -331,7 +382,6 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
Logger::info(L"[LightSwitchService] Recalculated sun times at new day boundary.");
}
// Have to do this again in case settings got updated in the refresh suntimes chunk
LightSwitchSettings::instance().LoadSettings();
const auto& currentSettings = LightSwitchSettings::instance().settings();
@@ -347,12 +397,9 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
static_cast<int>(currentSettings.scheduleMode));
Logger::info(msg);
// --- Manual override check ---
bool manualOverrideActive = false;
if (hManualOverride)
{
manualOverrideActive = (WaitForSingleObject(hManualOverride, 0) == WAIT_OBJECT_0);
}
if (manualOverrideActive)
{
@@ -365,13 +412,19 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
else
{
Logger::info(L"[LightSwitchService] Skipping schedule due to manual override");
goto sleep_until_next_minute;
skipRest = true;
}
}
applyTheme(nowMinutes, currentSettings.lightTime + currentSettings.sunrise_offset, currentSettings.darkTime + currentSettings.sunset_offset, currentSettings);
if (!skipRest)
applyTheme(nowMinutes,
currentSettings.lightTime + currentSettings.sunrise_offset,
currentSettings.darkTime + currentSettings.sunset_offset,
currentSettings);
}
sleep_until_next_minute:
// ─── 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)
{

View File

@@ -74,6 +74,7 @@
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="LightSwitchService.cpp" />
<ClCompile Include="LightSwitchServiceObserver.cpp" />
<ClCompile Include="LightSwitchSettings.cpp" />
<ClCompile Include="SettingsConstants.cpp" />
<ClCompile Include="ThemeHelper.cpp" />
@@ -84,6 +85,7 @@
<ResourceCompile Include="LightSwitchService.rc" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="LightSwitchServiceObserver.h" />
<ClInclude Include="LightSwitchSettings.h" />
<ClInclude Include="SettingsConstants.h" />
<ClInclude Include="SettingsObserver.h" />

View File

@@ -33,6 +33,9 @@
<ClCompile Include="WinHookEventIDs.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="LightSwitchServiceObserver.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="ThemeScheduler.h">
@@ -53,6 +56,9 @@
<ClInclude Include="WinHookEventIDs.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="LightSwitchServiceObserver.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" />

View File

@@ -0,0 +1,29 @@
#include "LightSwitchServiceObserver.h"
#include <logger.h>
#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<int>(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;
}
}

View File

@@ -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<SettingId> observedSettings) :
SettingsObserver(std::move(observedSettings))
{
}
void SettingsUpdate(SettingId id) override;
bool WantsToBeNotified(SettingId id) const noexcept override;
};

View File

@@ -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

View File

@@ -79,7 +79,7 @@ public:
void LoadSettings();
HANDLE GetSettingsChangedEvent() const { return m_settingsChangedEvent; }
HANDLE GetSettingsChangedEvent() const;
private:
LightSwitchSettings();

View File

@@ -2,6 +2,7 @@
#include <unordered_set>
#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);
}