/ src / modules / LightSwitch / LightSwitchService / LightSwitchStateManager.cpp
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  }