diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchService.cpp b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.cpp index 503a98de29..74efeda933 100644 --- a/src/modules/LightSwitch/LightSwitchService/LightSwitchService.cpp +++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.cpp @@ -11,19 +11,17 @@ #include #include #include +#include "LightSwitchStateManager.h" +#include SERVICE_STATUS g_ServiceStatus = {}; SERVICE_STATUS_HANDLE g_StatusHandle = nullptr; HANDLE g_ServiceStopEvent = nullptr; -extern int g_lastUpdatedDay = -1; -static ScheduleMode prevMode = ScheduleMode::Off; -static std::wstring prevLat, prevLon; -static int prevMinutes = -1; -static bool lastOverrideStatus = false; VOID WINAPI ServiceMain(DWORD argc, LPTSTR* argv); VOID WINAPI ServiceCtrlHandler(DWORD dwCtrl); DWORD WINAPI ServiceWorkerThread(LPVOID lpParam); +void ApplyTheme(bool shouldBeLight); // Entry point for the executable int _tmain(int argc, TCHAR* argv[]) @@ -124,31 +122,66 @@ VOID WINAPI ServiceCtrlHandler(DWORD dwCtrl) } } -static void update_sun_times(auto& settings) +void ApplyTheme(bool shouldBeLight) { - double latitude = std::stod(settings.latitude); - double longitude = std::stod(settings.longitude); + const auto& s = LightSwitchSettings::settings(); + + if (s.changeSystem) + { + bool isSystemCurrentlyLight = GetCurrentSystemTheme(); + if (shouldBeLight != isSystemCurrentlyLight) + { + SetSystemTheme(shouldBeLight); + Logger::info(L"[LightSwitchService] Changed system theme to {}.", shouldBeLight ? L"light" : L"dark"); + } + } + + if (s.changeApps) + { + bool isAppsCurrentlyLight = GetCurrentAppsTheme(); + if (shouldBeLight != isAppsCurrentlyLight) + { + SetAppsTheme(shouldBeLight); + Logger::info(L"[LightSwitchService] Changed apps theme to {}.", shouldBeLight ? L"light" : L"dark"); + } + } +} + +static void DetectAndHandleExternalThemeChange(LightSwitchStateManager& stateManager) +{ + const auto& s = LightSwitchSettings::settings(); + if (s.scheduleMode == ScheduleMode::Off) + return; SYSTEMTIME st; GetLocalTime(&st); + int nowMinutes = st.wHour * 60 + st.wMinute; - SunTimes newTimes = CalculateSunriseSunset(latitude, longitude, st.wYear, st.wMonth, st.wDay); + // Compute effective boundaries (with offsets if needed) + int effectiveLight = s.lightTime; + int effectiveDark = s.darkTime; - int newLightTime = newTimes.sunriseHour * 60 + newTimes.sunriseMinute; - int newDarkTime = newTimes.sunsetHour * 60 + newTimes.sunsetMinute; - try + if (s.scheduleMode == ScheduleMode::SunsetToSunrise) { - auto values = PowerToysSettings::PowerToyValues::load_from_settings_file(L"LightSwitch"); - values.add_property(L"lightTime", newLightTime); - values.add_property(L"darkTime", newDarkTime); - values.save_to_settings_file(); - - Logger::info(L"[LightSwitchService] Updated sun times and saved to config."); + effectiveLight = (s.lightTime + s.sunrise_offset) % 1440; + effectiveDark = (s.darkTime + s.sunset_offset) % 1440; } - catch (const std::exception& e) + + // Use shared helper (handles wraparound logic) + bool shouldBeLight = ShouldBeLight(nowMinutes, effectiveLight, effectiveDark); + + // Compare current system/apps theme + bool currentSystemLight = GetCurrentSystemTheme(); + bool currentAppsLight = GetCurrentAppsTheme(); + + bool systemMismatch = s.changeSystem && (currentSystemLight != shouldBeLight); + bool appsMismatch = s.changeApps && (currentAppsLight != shouldBeLight); + + // Trigger manual override only if mismatch and not already active + if ((systemMismatch || appsMismatch) && !stateManager.GetState().isManualOverride) { - std::wstring wmsg(e.what(), e.what() + strlen(e.what())); - Logger::error(L"[LightSwitchService] Exception during sun time update: {}", wmsg); + Logger::info(L"[LightSwitchService] External theme change detected (Windows Settings). Entering manual override mode."); + stateManager.OnManualOverride(); } } @@ -162,307 +195,99 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam) Logger::info(L"[LightSwitchService] Worker thread starting..."); Logger::info(L"[LightSwitchService] Parent PID: {}", parentPid); + // ──────────────────────────────────────────────────────────────── + // Initialization + // ──────────────────────────────────────────────────────────────── + static LightSwitchStateManager stateManager; + LightSwitchSettings::instance().InitFileWatcher(); HANDLE hManualOverride = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE"); + HANDLE hSettingsChanged = LightSwitchSettings::instance().GetSettingsChangedEvent(); LightSwitchSettings::instance().LoadSettings(); - auto& settings = LightSwitchSettings::instance().settings(); + const auto& settings = LightSwitchSettings::instance().settings(); SYSTEMTIME st; GetLocalTime(&st); int nowMinutes = st.wHour * 60 + st.wMinute; - // Handle initial theme application if necessary - if (settings.scheduleMode != ScheduleMode::Off) - { - Logger::info(L"[LightSwitchService] Schedule mode is set to {}. Applying theme if necessary.", settings.scheduleMode); - LightSwitchSettings::instance().ApplyThemeIfNecessary(); - } - else - { - Logger::info(L"[LightSwitchService] Schedule mode is set to Off."); - } + Logger::info(L"[LightSwitchService] Initialized at {:02d}:{:02d}.", st.wHour, st.wMinute); + stateManager.SyncInitialThemeState(); + stateManager.OnTick(nowMinutes); - g_lastUpdatedDay = st.wDay; - Logger::info(L"[LightSwitchService] Initializing g_lastUpdatedDay to {}.", g_lastUpdatedDay); - ULONGLONG lastSettingsReload = 0; - - // ticker loop + // ──────────────────────────────────────────────────────────────── + // Worker Loop + // ──────────────────────────────────────────────────────────────── for (;;) { - HANDLE waits[2] = { g_ServiceStopEvent, hParent }; - DWORD count = hParent ? 2 : 1; - bool skipRest = false; - - const auto& settings = LightSwitchSettings::instance().settings(); - - // If the mode is set to Off, suspend the scheduler and avoid extra work - if (settings.scheduleMode == ScheduleMode::Off) - { - Logger::info(L"[LightSwitchService] Schedule mode is 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()); - 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; - } - - bool scheduleJustEnabled = (prevMode == ScheduleMode::Off && settings.scheduleMode != ScheduleMode::Off); - prevMode = settings.scheduleMode; - - ULONGLONG nowTick = GetTickCount64(); - bool recentSettingsReload = (nowTick - lastSettingsReload < 2000); - - Logger::debug(L"[LightSwitchService] Current g_lastUpdatedDay value = {}.", g_lastUpdatedDay); - - // Manual Override Detection Logic - bool manualOverrideActive = (hManualOverride && WaitForSingleObject(hManualOverride, 0) == WAIT_OBJECT_0); - - if (manualOverrideActive != lastOverrideStatus) - { - Logger::debug(L"[LightSwitchService] Manual override active = {}", manualOverrideActive); - lastOverrideStatus = manualOverrideActive; - } - - if (settings.scheduleMode != ScheduleMode::Off && !recentSettingsReload && !scheduleJustEnabled && !manualOverrideActive) - { - bool currentSystemTheme = GetCurrentSystemTheme(); - bool currentAppsTheme = GetCurrentAppsTheme(); - - SYSTEMTIME st; - GetLocalTime(&st); - int nowMinutes = st.wHour * 60 + st.wMinute; - - int lightBoundary = 0; - int darkBoundary = 0; - - if (settings.scheduleMode == ScheduleMode::SunsetToSunrise) - { - lightBoundary = (settings.lightTime + settings.sunrise_offset) % 1440; - darkBoundary = (settings.darkTime + settings.sunset_offset) % 1440; - } - else - { - lightBoundary = settings.lightTime; - darkBoundary = settings.darkTime; - } - - bool shouldBeLight = (lightBoundary < darkBoundary) ? (nowMinutes >= lightBoundary && nowMinutes < darkBoundary) : (nowMinutes >= lightBoundary || nowMinutes < darkBoundary); - - Logger::debug(L"[LightSwitchService] shouldBeLight = {}", shouldBeLight); - - bool systemMismatch = settings.changeSystem && (currentSystemTheme != shouldBeLight); - bool appsMismatch = settings.changeApps && (currentAppsTheme != shouldBeLight); - - if (systemMismatch || appsMismatch) - { - // Make sure this is not because we crossed a boundary - bool crossedBoundary = false; - if (prevMinutes != -1) - { - if (nowMinutes < prevMinutes) - { - // wrapped around midnight - crossedBoundary = (prevMinutes <= lightBoundary || nowMinutes >= lightBoundary) || - (prevMinutes <= darkBoundary || nowMinutes >= darkBoundary); - } - else - { - crossedBoundary = (prevMinutes < lightBoundary && nowMinutes >= lightBoundary) || - (prevMinutes < darkBoundary && nowMinutes >= darkBoundary); - } - } - - if (crossedBoundary) - { - Logger::info(L"[LightSwitchService] Missed boundary detected. Applying theme instead of triggering manual override."); - LightSwitchSettings::instance().ApplyThemeIfNecessary(); - } - else - { - Logger::info(L"[LightSwitchService] External {} theme change detected, enabling manual override.", - systemMismatch && appsMismatch ? L"system/app" : - systemMismatch ? L"system" : - L"app"); - SetEvent(hManualOverride); - skipRest = true; - } - } - } - else - { - Logger::debug(L"[LightSwitchService] Skipping external-change detection (schedule off, recent reload, or just enabled)."); - } - + HANDLE waits[4]; + DWORD count = 0; + waits[count++] = g_ServiceStopEvent; + if (hParent) + waits[count++] = hParent; if (hManualOverride) - manualOverrideActive = (WaitForSingleObject(hManualOverride, 0) == WAIT_OBJECT_0); + waits[count++] = hManualOverride; + waits[count++] = hSettingsChanged; - if (manualOverrideActive) - { - int lightBoundary = (settings.lightTime + settings.sunrise_offset) % 1440; - int darkBoundary = (settings.darkTime + settings.sunset_offset) % 1440; - - SYSTEMTIME st; - GetLocalTime(&st); - nowMinutes = st.wHour * 60 + st.wMinute; - - bool crossedLight = false; - bool crossedDark = false; - - if (prevMinutes != -1) - { - // this means we are in a new day cycle - if (nowMinutes < prevMinutes) - { - crossedLight = (prevMinutes <= lightBoundary || nowMinutes >= lightBoundary); - crossedDark = (prevMinutes <= darkBoundary || nowMinutes >= darkBoundary); - } - else - { - crossedLight = (prevMinutes < lightBoundary && nowMinutes >= lightBoundary); - crossedDark = (prevMinutes < darkBoundary && nowMinutes >= darkBoundary); - } - } - - if (crossedLight || crossedDark) - { - ResetEvent(hManualOverride); - Logger::info(L"[LightSwitchService] Manual override cleared after crossing schedule boundary."); - } - else - { - Logger::debug(L"[LightSwitchService] Skipping schedule due to manual override"); - skipRest = true; - } - } - - // Apply theme if nothing has made us skip - if (!skipRest) - { - // Next two conditionals check for any updates necessary to the sun times. - bool modeChangedToSunset = (prevMode != settings.scheduleMode && - settings.scheduleMode == ScheduleMode::SunsetToSunrise); - bool coordsChanged = (prevLat != settings.latitude || prevLon != settings.longitude); - - if ((modeChangedToSunset || coordsChanged) && settings.scheduleMode == ScheduleMode::SunsetToSunrise) - { - SYSTEMTIME st; - GetLocalTime(&st); - - Logger::info(L"[LightSwitchService] Mode or coordinates changed, recalculating sun times."); - update_sun_times(settings); - 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; - prevMinutes = -1; - Logger::info(L"[LightSwitchService] Recalculated sun times at new day boundary."); - } - - // settings after any necessary updates. - 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=%s", - st.wHour, - st.wMinute, - currentSettings.lightTime / 60, - currentSettings.lightTime % 60, - currentSettings.darkTime / 60, - currentSettings.darkTime % 60, - ToString(currentSettings.scheduleMode).c_str()); - Logger::info(msg); - - LightSwitchSettings::instance().ApplyThemeIfNecessary(); - } - - // ─── Wait For Next Minute Tick Or Stop Event ──────────────────────────────── + // Wait for one of these to trigger or for a new minute tick SYSTEMTIME st; GetLocalTime(&st); int msToNextMinute = (60 - st.wSecond) * 1000 - st.wMilliseconds; if (msToNextMinute < 50) msToNextMinute = 50; - prevMinutes = nowMinutes; - DWORD wait = WaitForMultipleObjects(count, waits, FALSE, msToNextMinute); + + if (wait == WAIT_TIMEOUT) + { + // regular minute tick + GetLocalTime(&st); + nowMinutes = st.wHour * 60 + st.wMinute; + DetectAndHandleExternalThemeChange(stateManager); + stateManager.OnTick(nowMinutes); + continue; + } + if (wait == WAIT_OBJECT_0) { - Logger::info(L"[LightSwitchService] Stop event triggered - exiting worker loop."); + Logger::info(L"[LightSwitchService] Stop event triggered — exiting."); break; } + if (hParent && wait == WAIT_OBJECT_0 + 1) { - Logger::info(L"[LightSwitchService] Parent process exited - stopping service."); + Logger::info(L"[LightSwitchService] Parent process exited — stopping service."); break; } + + if (hManualOverride && wait == WAIT_OBJECT_0 + (hParent ? 2 : 1)) + { + Logger::info(L"[LightSwitchService] Manual override event detected."); + stateManager.OnManualOverride(); + ResetEvent(hManualOverride); + continue; + } + + if (wait == WAIT_OBJECT_0 + (hParent ? (hManualOverride ? 3 : 2) : 2)) + { + Logger::info(L"[LightSwitchService] Settings file changed event detected."); + ResetEvent(hSettingsChanged); + LightSwitchSettings::instance().LoadSettings(); + stateManager.OnSettingsChanged(); + continue; + } } -cleanup: + // ──────────────────────────────────────────────────────────────── + // Cleanup + // ──────────────────────────────────────────────────────────────── if (hManualOverride) CloseHandle(hManualOverride); if (hParent) CloseHandle(hParent); + Logger::info(L"[LightSwitchService] Worker thread exiting cleanly."); return 0; } diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj index b082250f61..a3a505f897 100644 --- a/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj +++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj @@ -75,6 +75,7 @@ + @@ -85,6 +86,8 @@ + + diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj.filters b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj.filters index f5aa05afc3..795df99aba 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,12 @@ Header Files + + Header Files + + + Header Files + diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.cpp b/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.cpp index 8105b0ab3a..5221a197fe 100644 --- a/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.cpp +++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.cpp @@ -2,10 +2,8 @@ #include #include #include "SettingsObserver.h" -#include "ThemeHelper.h" #include #include -#include #include using namespace std; @@ -69,7 +67,6 @@ void LightSwitchSettings::InitFileWatcher() try { LoadSettings(); - ApplyThemeIfNecessary(); SetEvent(m_settingsChangedEvent); } catch (const std::exception& e) @@ -250,48 +247,3 @@ void LightSwitchSettings::LoadSettings() // Keeps defaults if load fails } } - -void LightSwitchSettings::ApplyThemeIfNecessary() -{ - std::lock_guard guard(m_settingsMutex); - - SYSTEMTIME st; - GetLocalTime(&st); - int nowMinutes = st.wHour * 60 + st.wMinute; - - bool shouldBeLight = false; - if (m_settings.lightTime < m_settings.darkTime) - shouldBeLight = (nowMinutes >= m_settings.lightTime && nowMinutes < m_settings.darkTime); - else - shouldBeLight = (nowMinutes >= m_settings.lightTime || nowMinutes < m_settings.darkTime); - - bool isSystemCurrentlyLight = GetCurrentSystemTheme(); - bool isAppsCurrentlyLight = GetCurrentAppsTheme(); - - if (shouldBeLight) - { - if (m_settings.changeSystem && !isSystemCurrentlyLight) - { - SetSystemTheme(true); - Logger::info(L"[LightSwitchService] Changing system theme to light mode."); - } - if (m_settings.changeApps && !isAppsCurrentlyLight) - { - SetAppsTheme(true); - Logger::info(L"[LightSwitchService] Changing apps theme to light mode."); - } - } - else - { - if (m_settings.changeSystem && isSystemCurrentlyLight) - { - SetSystemTheme(false); - Logger::info(L"[LightSwitchService] Changing system theme to dark mode."); - } - if (m_settings.changeApps && isAppsCurrentlyLight) - { - SetAppsTheme(false); - Logger::info(L"[LightSwitchService] Changing apps theme to dark mode."); - } - } -} \ No newline at end of file diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.h b/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.h index 25f60cec82..d4029d072d 100644 --- a/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.h +++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.h @@ -81,7 +81,6 @@ public: void RemoveObserver(SettingsObserver& observer); void LoadSettings(); - void ApplyThemeIfNecessary(); HANDLE GetSettingsChangedEvent() const; diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchStateManager.cpp b/src/modules/LightSwitch/LightSwitchService/LightSwitchStateManager.cpp new file mode 100644 index 0000000000..836d511159 --- /dev/null +++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchStateManager.cpp @@ -0,0 +1,243 @@ +#include "pch.h" +#include "LightSwitchStateManager.h" +#include +#include +#include "ThemeScheduler.h" +#include + +void ApplyTheme(bool shouldBeLight); + +// Constructor +LightSwitchStateManager::LightSwitchStateManager() +{ + Logger::info(L"[LightSwitchStateManager] Initialized"); +} + +// Called when settings.json changes +void LightSwitchStateManager::OnSettingsChanged() +{ + std::lock_guard lock(_stateMutex); + Logger::info(L"[LightSwitchStateManager] Settings changed event received"); + + // If manual override was active, clear it so new settings take effect + if (_state.isManualOverride) + { + Logger::info(L"[LightSwitchStateManager] Clearing manual override due to settings update."); + _state.isManualOverride = false; + } + + EvaluateAndApplyIfNeeded(); +} + +// Called once per minute +void LightSwitchStateManager::OnTick(int currentMinutes) +{ + std::lock_guard lock(_stateMutex); + Logger::debug(L"[LightSwitchStateManager] Tick received: {}", currentMinutes); + EvaluateAndApplyIfNeeded(); +} + +// Called when manual override is triggered +void LightSwitchStateManager::OnManualOverride() +{ + std::lock_guard lock(_stateMutex); + Logger::info(L"[LightSwitchStateManager] Manual override triggered"); + _state.isManualOverride = !_state.isManualOverride; + + // When entering manual override, sync internal theme state to match the current system + if (_state.isManualOverride) + { + _state.isSystemLightActive = GetCurrentSystemTheme(); + + _state.isAppsLightActive = GetCurrentAppsTheme(); + + Logger::info(L"[LightSwitchStateManager] Synced internal theme state to current system theme ({}) and apps theme ({}).", + (_state.isSystemLightActive ? L"light" : L"dark"), + (_state.isAppsLightActive ? L"light" : L"dark")); + } + + EvaluateAndApplyIfNeeded(); +} + +// Helpers +bool LightSwitchStateManager::CoordinatesAreValid(const std::wstring& lat, const std::wstring& lon) +{ + try + { + double latVal = std::stod(lat); + double lonVal = std::stod(lon); + return !(latVal == 0 && lonVal == 0) && (latVal >= -90.0 && latVal <= 90.0) && (lonVal >= -180.0 && lonVal <= 180.0); + } + catch (...) + { + return false; + } +} + +void LightSwitchStateManager::SyncInitialThemeState() +{ + std::lock_guard lock(_stateMutex); + _state.isSystemLightActive = GetCurrentSystemTheme(); + _state.isAppsLightActive = GetCurrentAppsTheme(); + Logger::info(L"[LightSwitchStateManager] Synced initial state to current system theme ({})", + _state.isSystemLightActive ? L"light" : L"dark"); + Logger::info(L"[LightSwitchStateManager] Synced initial state to current apps theme ({})", + _state.isAppsLightActive ? L"light" : L"dark"); +} + +static std::pair update_sun_times(auto& settings) +{ + double latitude = std::stod(settings.latitude); + double longitude = std::stod(settings.longitude); + + SYSTEMTIME st; + GetLocalTime(&st); + + SunTimes newTimes = CalculateSunriseSunset(latitude, longitude, st.wYear, st.wMonth, st.wDay); + + int newLightTime = newTimes.sunriseHour * 60 + newTimes.sunriseMinute; + int newDarkTime = newTimes.sunsetHour * 60 + newTimes.sunsetMinute; + + try + { + auto values = PowerToysSettings::PowerToyValues::load_from_settings_file(L"LightSwitch"); + values.add_property(L"lightTime", newLightTime); + values.add_property(L"darkTime", newDarkTime); + values.save_to_settings_file(); + + Logger::info(L"[LightSwitchService] Updated sun times and saved to config."); + } + catch (const std::exception& e) + { + std::string msg = e.what(); + std::wstring wmsg(msg.begin(), msg.end()); + Logger::error(L"[LightSwitchService] Exception during sun time update: {}", wmsg); + } + + return { newLightTime, newDarkTime }; +} + +// Internal: decide what should happen now +void LightSwitchStateManager::EvaluateAndApplyIfNeeded() +{ + LightSwitchSettings::instance().LoadSettings(); + const auto& _currentSettings = LightSwitchSettings::settings(); + auto now = GetNowMinutes(); + + // Early exit: OFF mode just pauses activity + if (_currentSettings.scheduleMode == ScheduleMode::Off) + { + Logger::debug(L"[LightSwitchStateManager] Mode is OFF — pausing service logic."); + _state.lastTickMinutes = now; + return; + } + + bool coordsValid = CoordinatesAreValid(_currentSettings.latitude, _currentSettings.longitude); + + // Handle Sun Mode recalculation + if (_currentSettings.scheduleMode == ScheduleMode::SunsetToSunrise && coordsValid) + { + SYSTEMTIME st; + GetLocalTime(&st); + bool newDay = (_state.lastEvaluatedDay != st.wDay); + bool modeChangedToSun = (_state.lastAppliedMode != ScheduleMode::SunsetToSunrise && + _currentSettings.scheduleMode == ScheduleMode::SunsetToSunrise); + + if (newDay || modeChangedToSun) + { + Logger::info(L"[LightSwitchStateManager] Recalculating sun times (mode/day change)."); + auto [newLightTime, newDarkTime] = update_sun_times(_currentSettings); + _state.lastEvaluatedDay = st.wDay; + _state.effectiveLightMinutes = newLightTime + _currentSettings.sunrise_offset; + _state.effectiveDarkMinutes = newDarkTime + _currentSettings.sunset_offset; + } + else + { + _state.effectiveLightMinutes = _currentSettings.lightTime + _currentSettings.sunrise_offset; + _state.effectiveDarkMinutes = _currentSettings.darkTime + _currentSettings.sunset_offset; + } + } + else if (_currentSettings.scheduleMode == ScheduleMode::FixedHours) + { + _state.effectiveLightMinutes = _currentSettings.lightTime; + _state.effectiveDarkMinutes = _currentSettings.darkTime; + } + + // Handle manual override logic + if (_state.isManualOverride) + { + bool crossedBoundary = false; + if (_state.lastTickMinutes != -1) + { + int prev = _state.lastTickMinutes; + + // Handle midnight wraparound safely + if (now < prev) + { + crossedBoundary = + (prev <= _state.effectiveLightMinutes || now >= _state.effectiveLightMinutes) || + (prev <= _state.effectiveDarkMinutes || now >= _state.effectiveDarkMinutes); + } + else + { + crossedBoundary = + (prev < _state.effectiveLightMinutes && now >= _state.effectiveLightMinutes) || + (prev < _state.effectiveDarkMinutes && now >= _state.effectiveDarkMinutes); + } + } + + if (crossedBoundary) + { + Logger::info(L"[LightSwitchStateManager] Manual override cleared after crossing boundary."); + _state.isManualOverride = false; + } + else + { + Logger::debug(L"[LightSwitchStateManager] Manual override active — skipping auto apply."); + _state.lastTickMinutes = now; + return; + } + } + + _state.lastAppliedMode = _currentSettings.scheduleMode; + + bool shouldBeLight = ShouldBeLight(now, _state.effectiveLightMinutes, _state.effectiveDarkMinutes); + + bool appsNeedsToChange = _currentSettings.changeApps && (_state.isAppsLightActive != shouldBeLight); + bool systemNeedsToChange = _currentSettings.changeSystem && (_state.isSystemLightActive != shouldBeLight); + + Logger::debug( + L"[LightSwitchStateManager] now = {:02d}:{:02d}, light boundary = {:02d}:{:02d} ({}), dark boundary = {:02d}:{:02d} ({})", + now / 60, + now % 60, + _state.effectiveLightMinutes / 60, + _state.effectiveLightMinutes % 60, + _state.effectiveLightMinutes, + _state.effectiveDarkMinutes / 60, + _state.effectiveDarkMinutes % 60, + _state.effectiveDarkMinutes); + + Logger::debug("should be light = {}, apps needs change = {}, system needs change = {}", + shouldBeLight ? "true" : "false", + appsNeedsToChange ? "true" : "false", + systemNeedsToChange ? "true" : "false"); + + // Only apply theme if there's a change or no override active + if (!_state.isManualOverride && (appsNeedsToChange || systemNeedsToChange)) + { + Logger::info(L"[LightSwitchStateManager] Applying {} theme", shouldBeLight ? L"light" : L"dark"); + ApplyTheme(shouldBeLight); + + _state.isSystemLightActive = GetCurrentSystemTheme(); + _state.isAppsLightActive = GetCurrentAppsTheme(); + + Logger::debug(L"[LightSwitchStateManager] Synced post-apply theme state — System: {}, Apps: {}", + _state.isSystemLightActive ? L"light" : L"dark", + _state.isAppsLightActive ? L"light" : L"dark"); + } + + _state.lastTickMinutes = now; +} + + + diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchStateManager.h b/src/modules/LightSwitch/LightSwitchService/LightSwitchStateManager.h new file mode 100644 index 0000000000..5c9bcc6e25 --- /dev/null +++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchStateManager.h @@ -0,0 +1,47 @@ +#pragma once +#include "LightSwitchSettings.h" +#include + +// Represents runtime-only information (not saved in settings.json) +struct LightSwitchState +{ + ScheduleMode lastAppliedMode = ScheduleMode::Off; + bool isManualOverride = false; + bool isSystemLightActive = false; + bool isAppsLightActive = false; + int lastEvaluatedDay = -1; + int lastTickMinutes = -1; + + // Derived, runtime-resolved times + int effectiveLightMinutes = 0; // the boundary we actually act on + int effectiveDarkMinutes = 0; // includes offsets if needed +}; + +// The controller that reacts to settings changes, time ticks, and manual overrides. +class LightSwitchStateManager +{ +public: + LightSwitchStateManager(); + + // Called when settings.json changes or stabilizes. + void OnSettingsChanged(); + + // Called every minute (from service worker tick). + void OnTick(int currentMinutes); + + // Called when manual override is toggled (via shortcut or system change). + void OnManualOverride(); + + // Initial sync at startup to align internal state with system theme + void SyncInitialThemeState(); + + // Accessor for current state (optional, for debugging or telemetry) + const LightSwitchState& GetState() const { return _state; } + +private: + LightSwitchState _state; + std::mutex _stateMutex; + + void EvaluateAndApplyIfNeeded(); + bool CoordinatesAreValid(const std::wstring& lat, const std::wstring& lon); +}; diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchUtils.h b/src/modules/LightSwitch/LightSwitchService/LightSwitchUtils.h new file mode 100644 index 0000000000..0f4462bb65 --- /dev/null +++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchUtils.h @@ -0,0 +1,24 @@ +#pragma once +#include + +constexpr bool ShouldBeLight(int nowMinutes, int lightTime, int darkTime) +{ + // Normalize values into [0, 1439] + int normalizedLightTime = (lightTime % 1440 + 1440) % 1440; + int normalizedDarkTime = (darkTime % 1440 + 1440) % 1440; + int normalizedNowMinutes = (nowMinutes % 1440 + 1440) % 1440; + + // Case 1: Normal range, e.g. light mode comes before dark mode in the same day + if (normalizedLightTime < normalizedDarkTime) + return normalizedNowMinutes >= normalizedLightTime && normalizedNowMinutes < normalizedDarkTime; + + // Case 2: Wrap-around range, e.g. light mode starts in the evening and dark mode starts in the morning + return normalizedNowMinutes >= normalizedLightTime || normalizedNowMinutes < normalizedDarkTime; +} + +inline int GetNowMinutes() +{ + SYSTEMTIME st; + GetLocalTime(&st); + return st.wHour * 60 + st.wMinute; +} diff --git a/src/modules/LightSwitch/Tests/LightSwitch.UITests/TestHelper.cs b/src/modules/LightSwitch/Tests/LightSwitch.UITests/TestHelper.cs index 7b586301f6..37041b4b2d 100644 --- a/src/modules/LightSwitch/Tests/LightSwitch.UITests/TestHelper.cs +++ b/src/modules/LightSwitch/Tests/LightSwitch.UITests/TestHelper.cs @@ -152,16 +152,16 @@ namespace LightSwitch.UITests var neededTabs = 6; - if (modeCombobox.Text != "Manual") + if (modeCombobox.Text != "Fixed hours") { modeCombobox.Click(); var manualListItem = testBase.Session.Find(By.AccessibilityId("ManualCBItem_LightSwitch"), 5000); - Assert.IsNotNull(manualListItem, "Manual combobox item not found."); + Assert.IsNotNull(manualListItem, "Fixed Hours combobox item not found."); manualListItem.Click(); neededTabs = 1; } - Assert.AreEqual("Manual", modeCombobox.Text, "Mode combobox should be set to Manual."); + Assert.AreEqual("Fixed hours", modeCombobox.Text, "Mode combobox should be set to Fixed hours."); var timeline = testBase.Session.Find(By.AccessibilityId("Timeline_LightSwitch"), 5000); Assert.IsNotNull(timeline, "Timeline not found."); @@ -198,7 +198,7 @@ namespace LightSwitch.UITests } /// - /// Perform a update geolocation test operation + /// Perform a update manual location test operation /// public static void PerformUserSelectedLocationTest(UITestBase testBase) { @@ -216,19 +216,22 @@ namespace LightSwitch.UITests Assert.AreEqual("Sunset to sunrise", modeCombobox.Text, "Mode combobox should be set to Sunset to sunrise."); + // Click the select location button var setLocationButton = testBase.Session.Find(By.AccessibilityId("SetLocationButton_LightSwitch"), 5000); Assert.IsNotNull(setLocationButton, "Set location button not found."); - setLocationButton.Click(); + setLocationButton.Click(msPostAction: 1000); - var autoSuggestTextbox = testBase.Session.Find(By.AccessibilityId("CitySearchBox_LightSwitch"), 5000); - Assert.IsNotNull(autoSuggestTextbox, "City search box not found."); - autoSuggestTextbox.Click(); - autoSuggestTextbox.SendKeys("Seattle"); - autoSuggestTextbox.SendKeys(OpenQA.Selenium.Keys.Down); - autoSuggestTextbox.SendKeys(OpenQA.Selenium.Keys.Enter); + var latitudeBox = testBase.Session.Find(By.AccessibilityId("LatitudeBox_LightSwitch"), 5000); + Assert.IsNotNull(latitudeBox, "Latitude text box not found."); + latitudeBox.Click(); - var latLong = testBase.Session.Find(By.AccessibilityId("LocationResultText_LightSwitch"), 5000); - Assert.IsFalse(string.IsNullOrWhiteSpace(latLong.Text)); + testBase.Session.SendKeys(Key.Up); + + var longitudeBox = testBase.Session.Find(By.AccessibilityId("LongitudeBox_LightSwitch"), 5000); + Assert.IsNotNull(longitudeBox, "Longitude text box not found."); + longitudeBox.Click(); + + testBase.Session.SendKeys(Key.Down); var sunrise = testBase.Session.Find(By.AccessibilityId("SunriseText_LightSwitch"), 5000); Assert.IsFalse(string.IsNullOrWhiteSpace(sunrise.Text)); @@ -256,13 +259,14 @@ namespace LightSwitch.UITests Assert.AreEqual("Sunset to sunrise", modeCombobox.Text, "Mode combobox should be set to Sunset to sunrise."); - // Click the select city button + // Click the select location button var setLocationButton = testBase.Session.Find(By.AccessibilityId("SetLocationButton_LightSwitch"), 5000); Assert.IsNotNull(setLocationButton, "Set location button not found."); - setLocationButton.Click(msPostAction: 8000); + setLocationButton.Click(msPostAction: 1000); - var latLong = testBase.Session.Find(By.AccessibilityId("LocationResultText_LightSwitch"), 5000); - Assert.IsFalse(string.IsNullOrWhiteSpace(latLong.Text)); + var syncLocationButton = testBase.Session.Find(By.AccessibilityId("SyncLocationButton_LightSwitch"), 5000); + Assert.IsNotNull(syncLocationButton, "Sync location button not found."); + syncLocationButton.Click(msPostAction: 8000); var sunrise = testBase.Session.Find(By.AccessibilityId("SunriseText_LightSwitch"), 5000); Assert.IsFalse(string.IsNullOrWhiteSpace(sunrise.Text)); @@ -363,6 +367,7 @@ namespace LightSwitch.UITests var systemBeforeValue = GetSystemTheme(); var appsBeforeValue = GetAppsTheme(); + Task.Delay(1000).Wait(); testBase.Session.SendKeys(activationKeys); Task.Delay(5000).Wait(); @@ -389,6 +394,7 @@ namespace LightSwitch.UITests var noneSystemBeforeValue = GetSystemTheme(); var noneAppsBeforeValue = GetAppsTheme(); + Task.Delay(1000).Wait(); testBase.Session.SendKeys(activationKeys); Task.Delay(5000).Wait(); diff --git a/src/settings-ui/Settings.UI/Converters/StringToDouble.cs b/src/settings-ui/Settings.UI/Converters/StringToDouble.cs new file mode 100644 index 0000000000..fae0618467 --- /dev/null +++ b/src/settings-ui/Settings.UI/Converters/StringToDouble.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Globalization; +using Microsoft.UI.Xaml.Data; + +namespace Microsoft.PowerToys.Settings.UI.Converters +{ + public partial class StringToDoubleConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, string language) + { + if (value is string s && double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out double result)) + { + return result; + } + + return 0.0; + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + if (value is double d) + { + return d.ToString(CultureInfo.InvariantCulture); + } + + return "0"; + } + } +} diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/LightSwitchPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/LightSwitchPage.xaml index 7f55e31cd8..29885d564c 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/LightSwitchPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/LightSwitchPage.xaml @@ -1,24 +1,23 @@  - - + - + + - - - + - - - + - - - - - - - - - - + + Text="{x:Bind ViewModel.LocationPanelLightTime, Converter={StaticResource TimeSpanToFriendlyTimeConverter}, Mode=OneWay}" + TextAlignment="Left" /> @@ -321,12 +327,12 @@ + Text="{x:Bind ViewModel.LocationPanelDarkTime, Converter={StaticResource TimeSpanToFriendlyTimeConverter}, Mode=OneWay}" + TextAlignment="Left" /> @@ -358,4 +364,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/LightSwitchPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/LightSwitchPage.xaml.cs index 4a8e8905d7..c61b5c5dd8 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/LightSwitchPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/LightSwitchPage.xaml.cs @@ -12,88 +12,97 @@ using ManagedCommon; using Microsoft.PowerToys.Settings.UI.Helpers; using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library.Helpers; -using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.PowerToys.Settings.UI.ViewModels; using Microsoft.UI.Dispatching; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using PowerToys.GPOWrapper; using Settings.UI.Library; -using Settings.UI.Library.Helpers; using Windows.Devices.Geolocation; -using Windows.Services.Maps; namespace Microsoft.PowerToys.Settings.UI.Views { - public sealed partial class LightSwitchPage : Page + public sealed partial class LightSwitchPage : NavigablePage, IRefreshablePage { - private readonly string _appName = "LightSwitch"; - private readonly SettingsUtils _settingsUtils; - private readonly Func _sendConfigMsg = ShellPage.SendDefaultIPCMessage; + private readonly string appName = "LightSwitch"; + private readonly SettingsUtils settingsUtils; + private readonly Func sendConfigMsg = ShellPage.SendDefaultIPCMessage; - private readonly ISettingsRepository _generalSettingsRepository; - private readonly ISettingsRepository _moduleSettingsRepository; + private readonly SettingsRepository generalSettingsRepository; + private readonly SettingsRepository moduleSettingsRepository; - private readonly IFileSystem _fileSystem; - private readonly IFileSystemWatcher _fileSystemWatcher; - private readonly DispatcherQueue _dispatcherQueue; + private readonly IFileSystem fileSystem; + private readonly IFileSystemWatcher fileSystemWatcher; + private readonly DispatcherQueue dispatcherQueue; + private bool suppressViewModelUpdates; + private bool suppressLatLonChange = true; + private bool latBoxLoaded; + private bool lonBoxLoaded; private LightSwitchViewModel ViewModel { get; set; } public LightSwitchPage() { - _settingsUtils = new SettingsUtils(); - _sendConfigMsg = ShellPage.SendDefaultIPCMessage; + this.settingsUtils = new SettingsUtils(); + this.sendConfigMsg = ShellPage.SendDefaultIPCMessage; - _generalSettingsRepository = SettingsRepository.GetInstance(_settingsUtils); - _moduleSettingsRepository = SettingsRepository.GetInstance(_settingsUtils); + this.generalSettingsRepository = SettingsRepository.GetInstance(this.settingsUtils); + this.moduleSettingsRepository = SettingsRepository.GetInstance(this.settingsUtils); // Get settings from JSON (or defaults if JSON missing) - var darkSettings = _moduleSettingsRepository.SettingsConfig; + var darkSettings = this.moduleSettingsRepository.SettingsConfig; // Pass them into the ViewModel - ViewModel = new LightSwitchViewModel(darkSettings, ShellPage.SendDefaultIPCMessage); - ViewModel.PropertyChanged += ViewModel_PropertyChanged; + this.ViewModel = new LightSwitchViewModel(darkSettings, this.sendConfigMsg); + this.ViewModel.PropertyChanged += ViewModel_PropertyChanged; - LoadSettings(_generalSettingsRepository, _moduleSettingsRepository); + this.LoadSettings(this.generalSettingsRepository, this.moduleSettingsRepository); - DataContext = ViewModel; + DataContext = this.ViewModel; - var settingsPath = _settingsUtils.GetSettingsFilePath(_appName); + var settingsPath = this.settingsUtils.GetSettingsFilePath(this.appName); - _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); - _fileSystem = new FileSystem(); + this.dispatcherQueue = DispatcherQueue.GetForCurrentThread(); + this.fileSystem = new FileSystem(); - _fileSystemWatcher = _fileSystem.FileSystemWatcher.New(); - _fileSystemWatcher.Path = _fileSystem.Path.GetDirectoryName(settingsPath); - _fileSystemWatcher.Filter = _fileSystem.Path.GetFileName(settingsPath); - _fileSystemWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.CreationTime; - _fileSystemWatcher.Changed += Settings_Changed; - _fileSystemWatcher.EnableRaisingEvents = true; + this.fileSystemWatcher = this.fileSystem.FileSystemWatcher.New(); + this.fileSystemWatcher.Path = this.fileSystem.Path.GetDirectoryName(settingsPath); + this.fileSystemWatcher.Filter = this.fileSystem.Path.GetFileName(settingsPath); + this.fileSystemWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.CreationTime; + this.fileSystemWatcher.Changed += Settings_Changed; + this.fileSystemWatcher.EnableRaisingEvents = true; this.InitializeComponent(); - this.Loaded += LightSwitchPage_Loaded; - this.Loaded += (s, e) => ViewModel.OnPageLoaded(); + Loaded += LightSwitchPage_Loaded; + Loaded += (s, e) => this.ViewModel.OnPageLoaded(); + } + + public void RefreshEnabledState() + { + this.ViewModel.RefreshEnabledState(); } private void LightSwitchPage_Loaded(object sender, RoutedEventArgs e) { - if (ViewModel.SearchLocations.Count == 0) + if (this.ViewModel.SearchLocations.Count == 0) { foreach (var city in SearchLocationLoader.GetAll()) { - ViewModel.SearchLocations.Add(city); + this.ViewModel.SearchLocations.Add(city); } } - ViewModel.InitializeScheduleMode(); + this.ViewModel.InitializeScheduleMode(); } - private async Task GetGeoLocation() + private async void GetGeoLocation_Click(object sender, RoutedEventArgs e) { - SyncButton.IsEnabled = false; - SyncLoader.IsActive = true; - SyncLoader.Visibility = Visibility.Visible; + this.LatitudeBox.IsEnabled = false; + this.LongitudeBox.IsEnabled = false; + this.SyncButton.IsEnabled = false; + this.SyncLoader.IsActive = true; + this.SyncLoader.Visibility = Visibility.Visible; + this.LocationResultPanel.Visibility = Visibility.Collapsed; try { @@ -112,75 +121,157 @@ namespace Microsoft.PowerToys.Settings.UI.Views double latitude = Math.Round(pos.Coordinate.Point.Position.Latitude); double longitude = Math.Round(pos.Coordinate.Point.Position.Longitude); - SunTimes result = SunCalc.CalculateSunriseSunset( - latitude, - longitude, - DateTime.Now.Year, - DateTime.Now.Month, - DateTime.Now.Day); + ViewModel.LocationPanelLatitude = latitude; + ViewModel.LocationPanelLongitude = longitude; - ViewModel.LightTime = (result.SunriseHour * 60) + result.SunriseMinute; - ViewModel.DarkTime = (result.SunsetHour * 60) + result.SunsetMinute; - ViewModel.Latitude = latitude.ToString(CultureInfo.InvariantCulture); - ViewModel.Longitude = longitude.ToString(CultureInfo.InvariantCulture); + var result = SunCalc.CalculateSunriseSunset(latitude, longitude, DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day); + + this.ViewModel.LocationPanelLightTimeMinutes = (result.SunriseHour * 60) + result.SunriseMinute; + this.ViewModel.LocationPanelDarkTimeMinutes = (result.SunsetHour * 60) + result.SunsetMinute; // Since we use this mode, we can remove the selected city data. - ViewModel.SelectedCity = null; + this.ViewModel.SelectedCity = null; - // CityAutoSuggestBox.Text = string.Empty; - ViewModel.SyncButtonInformation = $"{ViewModel.Latitude}°, {ViewModel.Longitude}°"; + this.suppressLatLonChange = false; // ViewModel.CityTimesText = $"Sunrise: {result.SunriseHour}:{result.SunriseMinute:D2}\n" + $"Sunset: {result.SunsetHour}:{result.SunsetMinute:D2}"; - SyncButton.IsEnabled = true; - SyncLoader.IsActive = false; - SyncLoader.Visibility = Visibility.Collapsed; - LocationDialog.IsPrimaryButtonEnabled = true; - LocationResultPanel.Visibility = Visibility.Visible; + this.SyncButton.IsEnabled = true; + this.SyncLoader.IsActive = false; + this.SyncLoader.Visibility = Visibility.Collapsed; + this.LocationDialog.IsPrimaryButtonEnabled = true; + this.LatitudeBox.IsEnabled = true; + this.LongitudeBox.IsEnabled = true; + this.LocationResultPanel.Visibility = Visibility.Visible; } catch (Exception ex) { - SyncButton.IsEnabled = true; - SyncLoader.IsActive = false; - System.Diagnostics.Debug.WriteLine("Location error: " + ex.Message); + this.SyncButton.IsEnabled = true; + this.SyncLoader.IsActive = false; + this.SyncLoader.Visibility = Visibility.Collapsed; + this.LocationResultPanel.Visibility = Visibility.Collapsed; + this.LatitudeBox.IsEnabled = true; + this.LongitudeBox.IsEnabled = true; + Logger.LogInfo($"Location error: " + ex.Message); + } + } + + private void LatLonBox_ValueChanged(NumberBox sender, NumberBoxValueChangedEventArgs args) + { + if (this.suppressLatLonChange) + { + return; + } + + double latitude = this.LatitudeBox.Value; + double longitude = this.LongitudeBox.Value; + + if (double.IsNaN(latitude) || double.IsNaN(longitude)) + { + return; + } + + double viewModelLatitude = double.TryParse(this.ViewModel.Latitude, out var lat) ? lat : 0.0; + double viewModelLongitude = double.TryParse(this.ViewModel.Longitude, out var lon) ? lon : 0.0; + + if (Math.Abs(latitude - viewModelLatitude) < 0.0001 && Math.Abs(longitude - viewModelLongitude) < 0.0001) + { + return; + } + + var result = SunCalc.CalculateSunriseSunset(latitude, longitude, DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day); + + this.ViewModel.LocationPanelLightTimeMinutes = (result.SunriseHour * 60) + result.SunriseMinute; + this.ViewModel.LocationPanelDarkTimeMinutes = (result.SunsetHour * 60) + result.SunsetMinute; + + // Show the panel with these values + this.LocationResultPanel.Visibility = Visibility.Visible; + if (this.LocationDialog != null) + { + this.LocationDialog.IsPrimaryButtonEnabled = true; } } private void LocationDialog_PrimaryButtonClick(object sender, ContentDialogButtonClickEventArgs args) { - if (ViewModel.ScheduleMode == "SunriseToSunsetUser") + if (double.IsNaN(this.LatitudeBox.Value) || double.IsNaN(this.LongitudeBox.Value)) { - ViewModel.SyncButtonInformation = ViewModel.SelectedCity.City; - } - else if (ViewModel.ScheduleMode == "SunriseToSunsetGeo") - { - ViewModel.SyncButtonInformation = $"{ViewModel.Latitude}°, {ViewModel.Longitude}°"; + return; } - SunriseModeChartState(); + double latitude = this.LatitudeBox.Value; + double longitude = this.LongitudeBox.Value; + + // need to save the values + this.ViewModel.Latitude = latitude.ToString(CultureInfo.InvariantCulture); + this.ViewModel.Longitude = longitude.ToString(CultureInfo.InvariantCulture); + this.ViewModel.SyncButtonInformation = $"{this.ViewModel.Latitude}°, {this.ViewModel.Longitude}°"; + + var result = SunCalc.CalculateSunriseSunset(latitude, longitude, DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day); + + this.ViewModel.LightTime = (result.SunriseHour * 60) + result.SunriseMinute; + this.ViewModel.DarkTime = (result.SunsetHour * 60) + result.SunsetMinute; + + this.SunriseModeChartState(); + } + + private void LocationDialog_Opened(ContentDialog sender, ContentDialogOpenedEventArgs args) + { + this.LatitudeBox.Loaded += LatLonBox_Loaded; + this.LongitudeBox.Loaded += LatLonBox_Loaded; + } + + private void LocationDialog_Closed(ContentDialog sender, ContentDialogClosedEventArgs args) + { + this.LatitudeBox.Loaded -= LatLonBox_Loaded; + this.LongitudeBox.Loaded -= LatLonBox_Loaded; + this.latBoxLoaded = false; + this.lonBoxLoaded = false; + } + + private void LatLonBox_Loaded(object sender, RoutedEventArgs e) + { + if (sender is NumberBox numberBox && numberBox == this.LatitudeBox && this.LatitudeBox.IsLoaded) + { + this.latBoxLoaded = true; + } + else if (sender is NumberBox numberBox2 && numberBox2 == this.LongitudeBox && this.LongitudeBox.IsLoaded) + { + this.lonBoxLoaded = true; + } + + if (this.latBoxLoaded && this.lonBoxLoaded) + { + this.suppressLatLonChange = false; + } } private void ViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) { + if (this.suppressViewModelUpdates) + { + return; + } + if (e.PropertyName == "IsEnabled") { - if (ViewModel.IsEnabled != _generalSettingsRepository.SettingsConfig.Enabled.LightSwitch) + if (this.ViewModel.IsEnabled != this.generalSettingsRepository.SettingsConfig.Enabled.LightSwitch) { - _generalSettingsRepository.SettingsConfig.Enabled.LightSwitch = ViewModel.IsEnabled; + this.generalSettingsRepository.SettingsConfig.Enabled.LightSwitch = this.ViewModel.IsEnabled; - var generalSettingsMessage = new OutGoingGeneralSettings(_generalSettingsRepository.SettingsConfig).ToString(); + var generalSettingsMessage = new OutGoingGeneralSettings(this.generalSettingsRepository.SettingsConfig).ToString(); Logger.LogInfo($"Saved general settings from Light Switch page."); - _sendConfigMsg?.Invoke(generalSettingsMessage); + this.sendConfigMsg?.Invoke(generalSettingsMessage); } } else { - if (ViewModel.ModuleSettings != null) + if (this.ViewModel.ModuleSettings != null) { - SndLightSwitchSettings currentSettings = new(_moduleSettingsRepository.SettingsConfig); + SndLightSwitchSettings currentSettings = new(this.moduleSettingsRepository.SettingsConfig); SndModuleSettings csIpcMessage = new(currentSettings); - SndLightSwitchSettings outSettings = new(ViewModel.ModuleSettings); + SndLightSwitchSettings outSettings = new(this.ViewModel.ModuleSettings); SndModuleSettings outIpcMessage = new(outSettings); string csMessage = csIpcMessage.ToJsonString(); @@ -190,13 +281,13 @@ namespace Microsoft.PowerToys.Settings.UI.Views { Logger.LogInfo($"Saved Light Switch settings from Light Switch page."); - _sendConfigMsg?.Invoke(outMessage); + this.sendConfigMsg?.Invoke(outMessage); } } } } - private void LoadSettings(ISettingsRepository generalSettingsRepository, ISettingsRepository moduleSettingsRepository) + private void LoadSettings(SettingsRepository generalSettingsRepository, SettingsRepository moduleSettingsRepository) { if (generalSettingsRepository != null) { @@ -221,8 +312,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views { if (generalSettings != null) { - ViewModel.IsEnabled = generalSettings.Enabled.LightSwitch; - ViewModel.ModuleSettings = (LightSwitchSettings)lightSwitchSettings.Clone(); + this.ViewModel.IsEnabled = generalSettings.Enabled.LightSwitch; + this.ViewModel.ModuleSettings = (LightSwitchSettings)lightSwitchSettings.Clone(); UpdateEnabledState(generalSettings.Enabled.LightSwitch); } @@ -239,10 +330,14 @@ namespace Microsoft.PowerToys.Settings.UI.Views private void Settings_Changed(object sender, FileSystemEventArgs e) { - _dispatcherQueue.TryEnqueue(() => + this.dispatcherQueue.TryEnqueue(() => { - _moduleSettingsRepository.ReloadSettings(); - LoadSettings(_generalSettingsRepository, _moduleSettingsRepository); + this.suppressViewModelUpdates = true; + + this.moduleSettingsRepository.ReloadSettings(); + this.LoadSettings(this.generalSettingsRepository, this.moduleSettingsRepository); + + this.suppressViewModelUpdates = false; }); } @@ -253,20 +348,20 @@ namespace Microsoft.PowerToys.Settings.UI.Views if (enabledGpoRuleConfiguration == GpoRuleConfigured.Disabled || enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled) { // Get the enabled state from GPO. - ViewModel.IsEnabledGpoConfigured = true; - ViewModel.EnabledGPOConfiguration = enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled; + this.ViewModel.IsEnabledGpoConfigured = true; + this.ViewModel.EnabledGPOConfiguration = enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled; } else { - ViewModel.IsEnabled = recommendedState; + this.ViewModel.IsEnabled = recommendedState; } } private async void SyncLocationButton_Click(object sender, RoutedEventArgs e) { - LocationDialog.IsPrimaryButtonEnabled = false; - LocationResultPanel.Visibility = Visibility.Collapsed; - await LocationDialog.ShowAsync(); + this.LocationDialog.IsPrimaryButtonEnabled = false; + this.LocationResultPanel.Visibility = Visibility.Collapsed; + await this.LocationDialog.ShowAsync(); } private void CityAutoSuggestBox_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args) @@ -276,7 +371,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views string query = sender.Text.ToLower(CultureInfo.CurrentCulture); // Filter your cities (assuming ViewModel.Cities is a List) - var filtered = ViewModel.SearchLocations + var filtered = this.ViewModel.SearchLocations .Where(c => (c.City?.Contains(query, StringComparison.CurrentCultureIgnoreCase) ?? false) || (c.Country?.Contains(query, StringComparison.CurrentCultureIgnoreCase) ?? false)) @@ -286,7 +381,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views } } - private void CityAutoSuggestBox_SuggestionChosen(AutoSuggestBox sender, AutoSuggestBoxSuggestionChosenEventArgs args) + /* private void CityAutoSuggestBox_SuggestionChosen(AutoSuggestBox sender, AutoSuggestBoxSuggestionChosenEventArgs args) { if (args.SelectedItem is SearchLocation location) { @@ -296,43 +391,38 @@ namespace Microsoft.PowerToys.Settings.UI.Views LocationDialog.IsPrimaryButtonEnabled = true; LocationResultPanel.Visibility = Visibility.Visible; } - } + } */ private void ModeSelector_SelectionChanged(object sender, SelectionChangedEventArgs e) { - switch (ViewModel.ScheduleMode) + switch (this.ViewModel.ScheduleMode) { case "FixedHours": VisualStateManager.GoToState(this, "ManualState", true); - TimelineCard.Visibility = Visibility.Visible; + this.TimelineCard.Visibility = Visibility.Visible; break; case "SunsetToSunrise": VisualStateManager.GoToState(this, "SunsetToSunriseState", true); - SunriseModeChartState(); + this.SunriseModeChartState(); break; default: VisualStateManager.GoToState(this, "OffState", true); - TimelineCard.Visibility = Visibility.Collapsed; + this.TimelineCard.Visibility = Visibility.Collapsed; break; } } - private async void LocationDialog_Opened(ContentDialog sender, ContentDialogOpenedEventArgs args) - { - await GetGeoLocation(); - } - private void SunriseModeChartState() { - if (ViewModel.Latitude != "0.0" && ViewModel.Longitude != "0.0") + if (this.ViewModel.Latitude != "0.0" && this.ViewModel.Longitude != "0.0") { - TimelineCard.Visibility = Visibility.Visible; - LocationWarningBar.Visibility = Visibility.Collapsed; + this.TimelineCard.Visibility = Visibility.Visible; + this.LocationWarningBar.Visibility = Visibility.Collapsed; } else { - TimelineCard.Visibility = Visibility.Collapsed; - LocationWarningBar.Visibility = Visibility.Visible; + this.TimelineCard.Visibility = Visibility.Collapsed; + this.LocationWarningBar.Visibility = Visibility.Visible; } } } diff --git a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw index d42d948f5c..650ac06727 100644 --- a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw +++ b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw @@ -2677,9 +2677,7 @@ From there, simply click on one of the supported files in the File Explorer and Use a keyboard shortcut to highlight left and right mouse clicks. Mouse as in the hardware peripheral. - - - + Enable CursorWrap @@ -2689,8 +2687,6 @@ From there, simply click on one of the supported files in the File Explorer and Wrap the mouse cursor between monitor edges - - Activation shortcut @@ -2700,19 +2696,15 @@ From there, simply click on one of the supported files in the File Explorer and Set shortcut - Disable wrapping while dragging - - - + Auto-activate on startup Automatically activate on utility startup - - + Mouse Pointer Crosshairs Mouse as in the hardware peripheral. @@ -5465,16 +5457,13 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m Select a location - Select + Save Cancel - - Get current location - - To calculate the sunrise and sunset, Light Switch needs a location. + Detect your location automatically or enter it manually to calculate sunrise and sunset times. Sunrise @@ -5482,8 +5471,17 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m Sunset - - Location + + Latitude + + + Longitude + + + Detect location + + + Detect location Sunrise @@ -5567,7 +5565,7 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m Shortcut conflicts - + No conflicts found @@ -5678,4 +5676,7 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m Backup count + + Set Location + \ No newline at end of file diff --git a/src/settings-ui/Settings.UI/ViewModels/LightSwitchViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/LightSwitchViewModel.cs index 3f9ea48c18..911ec81aa2 100644 --- a/src/settings-ui/Settings.UI/ViewModels/LightSwitchViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/LightSwitchViewModel.cs @@ -407,6 +407,71 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels } } + private double _locationPanelLatitude; + private double _locationPanelLongitude; + + public double LocationPanelLatitude + { + get => _locationPanelLatitude; + set + { + if (_locationPanelLatitude != value) + { + _locationPanelLatitude = value; + NotifyPropertyChanged(); + NotifyPropertyChanged(nameof(LocationPanelLightTime)); + } + } + } + + public double LocationPanelLongitude + { + get => _locationPanelLongitude; + set + { + if (_locationPanelLongitude != value) + { + _locationPanelLongitude = value; + NotifyPropertyChanged(); + } + } + } + + private int _locationPanelLightTime; + private int _locationPanelDarkTime; + + public int LocationPanelLightTimeMinutes + { + get => _locationPanelLightTime; + set + { + if (_locationPanelLightTime != value) + { + _locationPanelLightTime = value; + NotifyPropertyChanged(); + NotifyPropertyChanged(nameof(LocationPanelLightTime)); + } + } + } + + public int LocationPanelDarkTimeMinutes + { + get => _locationPanelDarkTime; + set + { + if (_locationPanelDarkTime != value) + { + _locationPanelDarkTime = value; + NotifyPropertyChanged(); + NotifyPropertyChanged(nameof(LocationPanelDarkTime)); + } + } + } + + public TimeSpan LocationPanelLightTime => TimeSpan.FromMinutes(_locationPanelLightTime); + + public TimeSpan LocationPanelDarkTime => TimeSpan.FromMinutes(_locationPanelDarkTime); + public HotkeySettings ToggleThemeActivationShortcut { get => ModuleSettings.Properties.ToggleThemeHotkey.Value;