LightSwitchStateManager.cpp
1 #include "pch.h" 2 #include "LightSwitchStateManager.h" 3 #include <logger.h> 4 #include <LightSwitchUtils.h> 5 #include "ThemeScheduler.h" 6 #include <ThemeHelper.h> 7 #include <common/interop/shared_constants.h> 8 9 void ApplyTheme(bool shouldBeLight); 10 11 // Constructor 12 LightSwitchStateManager::LightSwitchStateManager() 13 { 14 Logger::info(L"[LightSwitchStateManager] Initialized"); 15 } 16 17 // Called when settings.json changes 18 void LightSwitchStateManager::OnSettingsChanged() 19 { 20 std::lock_guard<std::mutex> lock(_stateMutex); 21 22 // If manual override was active, clear it so new settings take effect 23 if (_state.isManualOverride) 24 { 25 _state.isManualOverride = false; 26 } 27 28 EvaluateAndApplyIfNeeded(); 29 } 30 31 // Called once per minute 32 void LightSwitchStateManager::OnTick() 33 { 34 std::lock_guard<std::mutex> lock(_stateMutex); 35 if (_state.lastAppliedMode != ScheduleMode::FollowNightLight) 36 { 37 EvaluateAndApplyIfNeeded(); 38 } 39 } 40 41 // Called when manual override is triggered (via hotkey) 42 void LightSwitchStateManager::OnManualOverride() 43 { 44 std::lock_guard<std::mutex> lock(_stateMutex); 45 Logger::info(L"[LightSwitchStateManager] Manual override triggered"); 46 _state.isManualOverride = !_state.isManualOverride; 47 48 // When entering manual override, sync internal theme state to match the current system 49 // The hotkey handler in ModuleInterface has already toggled the theme, so we read the new state 50 if (_state.isManualOverride) 51 { 52 _state.isSystemLightActive = GetCurrentSystemTheme(); 53 _state.isAppsLightActive = GetCurrentAppsTheme(); 54 55 Logger::debug(L"[LightSwitchStateManager] Synced internal theme state to current system theme ({}) and apps theme ({}).", 56 (_state.isSystemLightActive ? L"light" : L"dark"), 57 (_state.isAppsLightActive ? L"light" : L"dark")); 58 59 // Notify PowerDisplay about the theme change triggered by hotkey 60 // The theme has already been applied by ModuleInterface, we just need to notify PowerDisplay 61 NotifyPowerDisplay(_state.isSystemLightActive); 62 } 63 64 EvaluateAndApplyIfNeeded(); 65 } 66 67 // Runs with the registry observer detects a change in Night Light settings. 68 void LightSwitchStateManager::OnNightLightChange() 69 { 70 std::lock_guard<std::mutex> lock(_stateMutex); 71 72 bool newNightLightState = IsNightLightEnabled(); 73 74 // In Follow Night Light mode, treat a Night Light toggle as a boundary 75 if (_state.lastAppliedMode == ScheduleMode::FollowNightLight && _state.isManualOverride) 76 { 77 Logger::info(L"[LightSwitchStateManager] Night Light changed while manual override active; " 78 L"treating as a boundary and clearing manual override."); 79 _state.isManualOverride = false; 80 } 81 82 if (newNightLightState != _state.isNightLightActive) 83 { 84 Logger::info(L"[LightSwitchStateManager] Night Light toggled to {}", 85 newNightLightState ? L"ON" : L"OFF"); 86 87 _state.isNightLightActive = newNightLightState; 88 } 89 else 90 { 91 Logger::debug(L"[LightSwitchStateManager] Night Light change event fired, but no actual change."); 92 } 93 94 EvaluateAndApplyIfNeeded(); 95 } 96 97 // Helpers 98 bool LightSwitchStateManager::CoordinatesAreValid(const std::wstring& lat, const std::wstring& lon) 99 { 100 try 101 { 102 double latVal = std::stod(lat); 103 double lonVal = std::stod(lon); 104 return !(latVal == 0 && lonVal == 0) && (latVal >= -90.0 && latVal <= 90.0) && (lonVal >= -180.0 && lonVal <= 180.0); 105 } 106 catch (...) 107 { 108 return false; 109 } 110 } 111 112 void LightSwitchStateManager::SyncInitialThemeState() 113 { 114 std::lock_guard<std::mutex> lock(_stateMutex); 115 _state.isSystemLightActive = GetCurrentSystemTheme(); 116 _state.isAppsLightActive = GetCurrentAppsTheme(); 117 _state.isNightLightActive = IsNightLightEnabled(); 118 Logger::debug(L"[LightSwitchStateManager] Synced initial state to current system theme ({})", 119 _state.isSystemLightActive ? L"light" : L"dark"); 120 Logger::debug(L"[LightSwitchStateManager] Synced initial state to current apps theme ({})", 121 _state.isAppsLightActive ? L"light" : L"dark"); 122 123 // This will ensure that the theme is applied according to current settings at startup 124 EvaluateAndApplyIfNeeded(); 125 } 126 127 static std::pair<int, int> update_sun_times(auto& settings) 128 { 129 double latitude = std::stod(settings.latitude); 130 double longitude = std::stod(settings.longitude); 131 132 SYSTEMTIME st; 133 GetLocalTime(&st); 134 135 SunTimes newTimes = CalculateSunriseSunset(latitude, longitude, st.wYear, st.wMonth, st.wDay); 136 137 int newLightTime = newTimes.sunriseHour * 60 + newTimes.sunriseMinute; 138 int newDarkTime = newTimes.sunsetHour * 60 + newTimes.sunsetMinute; 139 140 try 141 { 142 auto values = PowerToysSettings::PowerToyValues::load_from_settings_file(L"LightSwitch"); 143 values.add_property(L"lightTime", newLightTime); 144 values.add_property(L"darkTime", newDarkTime); 145 values.save_to_settings_file(); 146 147 Logger::info(L"[LightSwitchService] Updated sun times and saved to config."); 148 } 149 catch (const std::exception& e) 150 { 151 std::string msg = e.what(); 152 std::wstring wmsg(msg.begin(), msg.end()); 153 Logger::error(L"[LightSwitchService] Exception during sun time update: {}", wmsg); 154 } 155 156 return { newLightTime, newDarkTime }; 157 } 158 159 // Internal: decide what should happen now 160 void LightSwitchStateManager::EvaluateAndApplyIfNeeded() 161 { 162 LightSwitchSettings::instance().LoadSettings(); 163 const auto& _currentSettings = LightSwitchSettings::settings(); 164 auto now = GetNowMinutes(); 165 166 // Early exit: OFF mode just pauses activity 167 if (_currentSettings.scheduleMode == ScheduleMode::Off) 168 { 169 _state.lastTickMinutes = now; 170 return; 171 } 172 173 bool coordsValid = CoordinatesAreValid(_currentSettings.latitude, _currentSettings.longitude); 174 175 // Handle Sun Mode recalculation 176 if (_currentSettings.scheduleMode == ScheduleMode::SunsetToSunrise && coordsValid) 177 { 178 SYSTEMTIME st; 179 GetLocalTime(&st); 180 bool newDay = (_state.lastEvaluatedDay != st.wDay); 181 bool modeChangedToSun = (_state.lastAppliedMode != ScheduleMode::SunsetToSunrise && 182 _currentSettings.scheduleMode == ScheduleMode::SunsetToSunrise); 183 184 if (newDay || modeChangedToSun) 185 { 186 auto [newLightTime, newDarkTime] = update_sun_times(_currentSettings); 187 _state.lastEvaluatedDay = st.wDay; 188 _state.effectiveLightMinutes = newLightTime + _currentSettings.sunrise_offset; 189 _state.effectiveDarkMinutes = newDarkTime + _currentSettings.sunset_offset; 190 } 191 else 192 { 193 _state.effectiveLightMinutes = _currentSettings.lightTime + _currentSettings.sunrise_offset; 194 _state.effectiveDarkMinutes = _currentSettings.darkTime + _currentSettings.sunset_offset; 195 } 196 } 197 else if (_currentSettings.scheduleMode == ScheduleMode::FixedHours) 198 { 199 _state.effectiveLightMinutes = _currentSettings.lightTime; 200 _state.effectiveDarkMinutes = _currentSettings.darkTime; 201 } 202 203 // Handle manual override logic 204 if (_state.isManualOverride) 205 { 206 bool crossedBoundary = false; 207 if (_state.lastTickMinutes != -1) 208 { 209 int prev = _state.lastTickMinutes; 210 211 // Handle midnight wraparound safely 212 if (now < prev) 213 { 214 crossedBoundary = 215 (prev <= _state.effectiveLightMinutes || now >= _state.effectiveLightMinutes) || 216 (prev <= _state.effectiveDarkMinutes || now >= _state.effectiveDarkMinutes); 217 } 218 else 219 { 220 crossedBoundary = 221 (prev < _state.effectiveLightMinutes && now >= _state.effectiveLightMinutes) || 222 (prev < _state.effectiveDarkMinutes && now >= _state.effectiveDarkMinutes); 223 } 224 } 225 226 if (crossedBoundary) 227 { 228 _state.isManualOverride = false; 229 } 230 else 231 { 232 _state.lastTickMinutes = now; 233 return; 234 } 235 } 236 237 _state.lastAppliedMode = _currentSettings.scheduleMode; 238 239 bool shouldBeLight = false; 240 if (_currentSettings.scheduleMode == ScheduleMode::FollowNightLight) 241 { 242 shouldBeLight = !_state.isNightLightActive; 243 } 244 else 245 { 246 shouldBeLight = ShouldBeLight(now, _state.effectiveLightMinutes, _state.effectiveDarkMinutes); 247 } 248 249 bool appsNeedsToChange = _currentSettings.changeApps && (_state.isAppsLightActive != shouldBeLight); 250 bool systemNeedsToChange = _currentSettings.changeSystem && (_state.isSystemLightActive != shouldBeLight); 251 252 /* Logger::debug( 253 L"[LightSwitchStateManager] now = {:02d}:{:02d}, light boundary = {:02d}:{:02d} ({}), dark boundary = {:02d}:{:02d} ({})", 254 now / 60, 255 now % 60, 256 _state.effectiveLightMinutes / 60, 257 _state.effectiveLightMinutes % 60, 258 _state.effectiveLightMinutes, 259 _state.effectiveDarkMinutes / 60, 260 _state.effectiveDarkMinutes % 60, 261 _state.effectiveDarkMinutes); */ 262 263 /* Logger::debug("should be light = {}, apps needs change = {}, system needs change = {}", 264 shouldBeLight ? "true" : "false", 265 appsNeedsToChange ? "true" : "false", 266 systemNeedsToChange ? "true" : "false"); */ 267 268 // Only apply theme if there's a change or no override active 269 if (!_state.isManualOverride && (appsNeedsToChange || systemNeedsToChange)) 270 { 271 Logger::info(L"[LightSwitchStateManager] Applying {} theme", shouldBeLight ? L"light" : L"dark"); 272 ApplyTheme(shouldBeLight); 273 274 _state.isSystemLightActive = GetCurrentSystemTheme(); 275 _state.isAppsLightActive = GetCurrentAppsTheme(); 276 277 // Notify PowerDisplay to apply display profile if configured 278 NotifyPowerDisplay(shouldBeLight); 279 } 280 281 _state.lastTickMinutes = now; 282 } 283 284 // Notify PowerDisplay module about theme change to apply display profiles 285 void LightSwitchStateManager::NotifyPowerDisplay(bool isLight) 286 { 287 const auto& settings = LightSwitchSettings::settings(); 288 289 // Check if any profile is enabled and configured 290 bool shouldNotify = false; 291 292 if (isLight && settings.enableLightModeProfile && !settings.lightModeProfile.empty()) 293 { 294 shouldNotify = true; 295 } 296 else if (!isLight && settings.enableDarkModeProfile && !settings.darkModeProfile.empty()) 297 { 298 shouldNotify = true; 299 } 300 301 if (!shouldNotify) 302 { 303 return; 304 } 305 306 try 307 { 308 // Signal PowerDisplay with the specific theme event 309 // Using separate events for light/dark eliminates race conditions where PowerDisplay 310 // might read the registry before LightSwitch has finished updating it 311 const wchar_t* eventName = isLight 312 ? CommonSharedConstants::LIGHT_SWITCH_LIGHT_THEME_EVENT 313 : CommonSharedConstants::LIGHT_SWITCH_DARK_THEME_EVENT; 314 315 Logger::info(L"[LightSwitchStateManager] Notifying PowerDisplay about theme change (isLight: {})", isLight); 316 317 HANDLE hThemeEvent = CreateEventW(nullptr, FALSE, FALSE, eventName); 318 if (hThemeEvent) 319 { 320 SetEvent(hThemeEvent); 321 CloseHandle(hThemeEvent); 322 Logger::info(L"[LightSwitchStateManager] Theme event signaled to PowerDisplay: {}", eventName); 323 } 324 else 325 { 326 Logger::warn(L"[LightSwitchStateManager] Failed to create theme event (error: {})", GetLastError()); 327 } 328 } 329 catch (...) 330 { 331 Logger::error(L"[LightSwitchStateManager] Failed to notify PowerDisplay"); 332 } 333 }