[Light Switch] Enter latitude and longitude manually in Sunrise to sunset mode (#43276)

<!-- 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 introduces new UI to allow the users to manually enter their
lat/long.

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #42429
- [x] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [x] **Localization:** All end-user-facing strings can be localized
- [x] **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:
#[5979](https://github.com/MicrosoftDocs/windows-dev-docs-pr/pull/5979)

---------

Co-authored-by: Niels Laute <niels.laute@live.nl>
This commit is contained in:
Jaylyn Barbee
2025-11-11 16:18:18 -05:00
committed by GitHub
parent a0f33c8af1
commit 29688cea0e
14 changed files with 837 additions and 534 deletions

View File

@@ -0,0 +1,243 @@
#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)
{
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;
}