-
Notifications
You must be signed in to change notification settings - Fork 7.5k
[Light Switch] Enter latitude and longitude manually in Sunrise to sunset mode #43276
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 27 commits
Commits
Show all changes
30 commits
Select commit
Hold shift + click to select a range
b745f61
Update LightSwitchPage.xaml
niels9001 b3e09e6
Adding manual lat lon UI
niels9001 be87a44
added in logic
Jaylyn-Barbee 814eb78
XAML styling
Jaylyn-Barbee 3b1dfeb
Disable input boxes if using location services to fetch location
Jaylyn-Barbee e8ee996
enable button on manual entry
Jaylyn-Barbee 26b22b5
xaml styling
Jaylyn-Barbee fd99820
trying to fix infinite settings updates
Jaylyn-Barbee e749f21
fixed default state issue and circular dependency causing looping
Jaylyn-Barbee b71140e
Merge branch 'main' into feature/lightswitch-manual-location
Jaylyn-Barbee ac0b56a
Xaml styling
Jaylyn-Barbee eec7940
Copilot Suggestions
Jaylyn-Barbee d172c21
Merge branch 'main' into feature/lightswitch-manual-location
Jaylyn-Barbee da0fe7a
Resolve merge: keep ours for ShortcutConflictControl.xaml
niels9001 5cf0b4d
Merge branch 'main' into feature/lightswitch-manual-location
Jaylyn-Barbee 55f28f8
Refactored to use MVC layout in service
Jaylyn-Barbee 48ddffa
manual boxes should be working
Jaylyn-Barbee 89d58aa
Copilot suggestions
Jaylyn-Barbee 95b9236
xaml clean up
Jaylyn-Barbee bd73f4a
added locks to prevent race conditions
Jaylyn-Barbee 62ea01e
Fixed initial theme desync
Jaylyn-Barbee 29de283
Merge branch 'main' of https://github.com/microsoft/PowerToys
niels9001 64dafdc
Merge branch 'main' into feature/lightswitch-manual-location
niels9001 ae2f817
removed unused vars
Jaylyn-Barbee b01c819
Merge branch 'feature/lightswitch-manual-location' of https://github.…
Jaylyn-Barbee c0bc441
Made LightSwitchPage Navigable
Jaylyn-Barbee 7bb7d92
UI Tests
Jaylyn-Barbee 0d52787
better responsiveness from lat/lon boxes
Jaylyn-Barbee a19de59
Fixing lat/lon boxes
Jaylyn-Barbee eb41e17
Everything working as expected
Jaylyn-Barbee File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
393 changes: 109 additions & 284 deletions
393
src/modules/LightSwitch/LightSwitchService/LightSwitchService.cpp
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
226 changes: 226 additions & 0 deletions
226
src/modules/LightSwitch/LightSwitchService/LightSwitchStateManager.cpp
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,226 @@ | ||
| #include "pch.h" | ||
| #include "LightSwitchStateManager.h" | ||
| #include <logger.h> | ||
| #include <LightSwitchUtils.h> | ||
| #include "ThemeScheduler.h" | ||
| #include <ThemeHelper.h> | ||
|
|
||
| void ApplyTheme(bool shouldBeLight); | ||
|
|
||
| // Constructor | ||
| LightSwitchStateManager::LightSwitchStateManager() | ||
| { | ||
| Logger::info(L"[LightSwitchStateManager] Initialized"); | ||
| } | ||
|
|
||
| // Called when settings.json changes | ||
| void LightSwitchStateManager::OnSettingsChanged() | ||
| { | ||
| std::lock_guard<std::mutex> 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<std::mutex> lock(_stateMutex); | ||
| Logger::debug(L"[LightSwitchStateManager] Tick received: {}", currentMinutes); | ||
| EvaluateAndApplyIfNeeded(); | ||
| } | ||
|
|
||
| // Called when manual override is triggered | ||
| void LightSwitchStateManager::OnManualOverride() | ||
| { | ||
| std::lock_guard<std::mutex> 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<std::mutex> 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<int, int> update_sun_times(auto& settings) | ||
Jaylyn-Barbee marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| { | ||
| 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() | ||
| { | ||
| 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 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("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; | ||
| } | ||
|
|
||
|
|
||
|
|
||
47 changes: 47 additions & 0 deletions
47
src/modules/LightSwitch/LightSwitchService/LightSwitchStateManager.h
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| #pragma once | ||
| #include "LightSwitchSettings.h" | ||
| #include <optional> | ||
|
|
||
| // 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); | ||
| }; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why can't latitude and longitude both be equal to zero? not sure this is a problem but struck me
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's what we use as the defaults and it just prevents updates from happening when they aren't supposed to. Luckily, I don't think anyone lives here

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't we just have
nullas defaults?