mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-15 03:07:56 +01:00
[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:
@@ -11,19 +11,17 @@
|
||||
#include <logger/logger_settings.h>
|
||||
#include <logger/logger.h>
|
||||
#include <utils/logger_helper.h>
|
||||
#include "LightSwitchStateManager.h"
|
||||
#include <LightSwitchUtils.h>
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
<ItemGroup>
|
||||
<ClCompile Include="LightSwitchService.cpp" />
|
||||
<ClCompile Include="LightSwitchSettings.cpp" />
|
||||
<ClCompile Include="LightSwitchStateManager.cpp" />
|
||||
<ClCompile Include="SettingsConstants.cpp" />
|
||||
<ClCompile Include="ThemeHelper.cpp" />
|
||||
<ClCompile Include="ThemeScheduler.cpp" />
|
||||
@@ -85,6 +86,8 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="LightSwitchSettings.h" />
|
||||
<ClInclude Include="LightSwitchStateManager.h" />
|
||||
<ClInclude Include="LightSwitchUtils.h" />
|
||||
<ClInclude Include="SettingsConstants.h" />
|
||||
<ClInclude Include="SettingsObserver.h" />
|
||||
<ClInclude Include="ThemeHelper.h" />
|
||||
|
||||
@@ -33,6 +33,9 @@
|
||||
<ClCompile Include="WinHookEventIDs.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="LightSwitchStateManager.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="ThemeScheduler.h">
|
||||
@@ -53,6 +56,12 @@
|
||||
<ClInclude Include="WinHookEventIDs.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="LightSwitchStateManager.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="LightSwitchUtils.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" />
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
#include <common/utils/json.h>
|
||||
#include <common/SettingsAPI/settings_helpers.h>
|
||||
#include "SettingsObserver.h"
|
||||
#include "ThemeHelper.h"
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <WinHookEventIDs.h>
|
||||
#include <logger.h>
|
||||
|
||||
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<std::mutex> 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,7 +81,6 @@ public:
|
||||
void RemoveObserver(SettingsObserver& observer);
|
||||
|
||||
void LoadSettings();
|
||||
void ApplyThemeIfNecessary();
|
||||
|
||||
HANDLE GetSettingsChangedEvent() const;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
#include <windows.h>
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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<Element>(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<Element>(By.AccessibilityId("Timeline_LightSwitch"), 5000);
|
||||
Assert.IsNotNull(timeline, "Timeline not found.");
|
||||
@@ -198,7 +198,7 @@ namespace LightSwitch.UITests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform a update geolocation test operation
|
||||
/// Perform a update manual location test operation
|
||||
/// </summary>
|
||||
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<Element>(By.AccessibilityId("SetLocationButton_LightSwitch"), 5000);
|
||||
Assert.IsNotNull(setLocationButton, "Set location button not found.");
|
||||
setLocationButton.Click();
|
||||
setLocationButton.Click(msPostAction: 1000);
|
||||
|
||||
var autoSuggestTextbox = testBase.Session.Find<Element>(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<Element>(By.AccessibilityId("LatitudeBox_LightSwitch"), 5000);
|
||||
Assert.IsNotNull(latitudeBox, "Latitude text box not found.");
|
||||
latitudeBox.Click();
|
||||
|
||||
var latLong = testBase.Session.Find<Element>(By.AccessibilityId("LocationResultText_LightSwitch"), 5000);
|
||||
Assert.IsFalse(string.IsNullOrWhiteSpace(latLong.Text));
|
||||
testBase.Session.SendKeys(Key.Up);
|
||||
|
||||
var longitudeBox = testBase.Session.Find<Element>(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<Element>(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<Element>(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<Element>(By.AccessibilityId("LocationResultText_LightSwitch"), 5000);
|
||||
Assert.IsFalse(string.IsNullOrWhiteSpace(latLong.Text));
|
||||
var syncLocationButton = testBase.Session.Find<Element>(By.AccessibilityId("SyncLocationButton_LightSwitch"), 5000);
|
||||
Assert.IsNotNull(syncLocationButton, "Sync location button not found.");
|
||||
syncLocationButton.Click(msPostAction: 8000);
|
||||
|
||||
var sunrise = testBase.Session.Find<Element>(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();
|
||||
|
||||
|
||||
33
src/settings-ui/Settings.UI/Converters/StringToDouble.cs
Normal file
33
src/settings-ui/Settings.UI/Converters/StringToDouble.cs
Normal file
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Page
|
||||
<local:NavigablePage
|
||||
x:Class="Microsoft.PowerToys.Settings.UI.Views.LightSwitchPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:ViewModel="using:Microsoft.PowerToys.Settings.UI.ViewModels"
|
||||
xmlns:animations="using:CommunityToolkit.WinUI.Animations"
|
||||
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
|
||||
xmlns:converters="using:Microsoft.PowerToys.Settings.UI.Converters"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:helpers="using:Settings.UI.Library.Helpers"
|
||||
xmlns:local="using:Microsoft.PowerToys.Settings.UI.Helpers"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:tkconverters="using:CommunityToolkit.WinUI.Converters"
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
d:DataContext="{d:DesignInstance Type=ViewModel:LightSwitchViewModel}"
|
||||
xmlns:viewModels="using:Microsoft.PowerToys.Settings.UI.ViewModels"
|
||||
d:DataContext="{d:DesignInstance Type=viewModels:LightSwitchViewModel}"
|
||||
AutomationProperties.LandmarkType="Main"
|
||||
mc:Ignorable="d">
|
||||
<Page.Resources>
|
||||
<local:NavigablePage.Resources>
|
||||
<converters:TimeSpanToFriendlyTimeConverter x:Key="TimeSpanToFriendlyTimeConverter" />
|
||||
</Page.Resources>
|
||||
<converters:StringToDoubleConverter x:Key="StringToDoubleConverter" />
|
||||
</local:NavigablePage.Resources>
|
||||
<Grid>
|
||||
<controls:SettingsPageControl
|
||||
x:Uid="LightSwitch"
|
||||
@@ -88,16 +87,14 @@
|
||||
x:Uid="LightSwitch_LocationSettingsCard"
|
||||
Visibility="Collapsed">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock
|
||||
<controls:IsEnabledTextBlock
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="{x:Bind ViewModel.SyncButtonInformation, Mode=OneWay}" />
|
||||
<Button
|
||||
Padding="8"
|
||||
x:Uid="LightSwitch_SetLocationButton"
|
||||
AutomationProperties.AutomationId="SetLocationButton_LightSwitch"
|
||||
Click="SyncLocationButton_Click"
|
||||
Content="{ui:FontIcon Glyph=,
|
||||
FontSize=16}" />
|
||||
Click="SyncLocationButton_Click" />
|
||||
</StackPanel>
|
||||
</tkcontrols:SettingsCard>
|
||||
|
||||
@@ -202,17 +199,22 @@
|
||||
<ContentDialog
|
||||
x:Name="LocationDialog"
|
||||
x:Uid="LightSwitch_LocationDialog"
|
||||
IsPrimaryButtonEnabled="True"
|
||||
Closed="LocationDialog_Closed"
|
||||
IsPrimaryButtonEnabled="False"
|
||||
IsSecondaryButtonEnabled="True"
|
||||
Opened="LocationDialog_Opened"
|
||||
PrimaryButtonClick="LocationDialog_PrimaryButtonClick"
|
||||
PrimaryButtonStyle="{StaticResource AccentButtonStyle}">
|
||||
<Grid RowSpacing="48">
|
||||
<Grid RowSpacing="16">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" MinHeight="64" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock x:Uid="LightSwitch_LocationDialog_Description" Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||
<TextBlock
|
||||
x:Uid="LightSwitch_LocationDialog_Description"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
TextWrapping="Wrap" />
|
||||
<!--<AutoSuggestBox
|
||||
x:Name="CityAutoSuggestBox"
|
||||
Grid.Row="1"
|
||||
@@ -240,80 +242,84 @@
|
||||
</DataTemplate>
|
||||
</AutoSuggestBox.ItemTemplate>
|
||||
</AutoSuggestBox>-->
|
||||
<StackPanel
|
||||
Grid.Row="2"
|
||||
Margin="0,24,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
Orientation="Vertical"
|
||||
Spacing="32">
|
||||
|
||||
<Grid
|
||||
Grid.Row="1"
|
||||
Margin="0,12,0,0"
|
||||
ColumnSpacing="4">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="124" />
|
||||
<ColumnDefinition Width="124" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<NumberBox
|
||||
x:Name="LatitudeBox"
|
||||
x:Uid="LightSwitch_LatitudeBox"
|
||||
AutomationProperties.AutomationId="LatitudeBox_LightSwitch"
|
||||
Maximum="90"
|
||||
Minimum="-90"
|
||||
ValueChanged="LatLonBox_ValueChanged"
|
||||
Value="{x:Bind ViewModel.LocationPanelLatitude, Mode=TwoWay}" />
|
||||
<NumberBox
|
||||
x:Name="LongitudeBox"
|
||||
x:Uid="LightSwitch_LongitudeBox"
|
||||
Grid.Column="1"
|
||||
AutomationProperties.AutomationId="LongitudeBox_LightSwitch"
|
||||
Maximum="180"
|
||||
Minimum="-180"
|
||||
ValueChanged="LatLonBox_ValueChanged"
|
||||
Value="{x:Bind ViewModel.LocationPanelLongitude, Mode=TwoWay}" />
|
||||
<Button
|
||||
x:Name="SyncButton"
|
||||
x:Uid="LightSwitch_FindLocationAutomation"
|
||||
Grid.Column="2"
|
||||
Margin="4,0,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Bottom"
|
||||
AutomationProperties.AutomationId="SyncLocationButton_LightSwitch"
|
||||
Style="{StaticResource AccentButtonStyle}"
|
||||
Visibility="Collapsed">
|
||||
Click="GetGeoLocation_Click">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="14" Glyph="" />
|
||||
<TextBlock x:Uid="LightSwitch_GetCurrentLocation" />
|
||||
<FontIcon FontSize="16" Glyph="" />
|
||||
<TextBlock x:Uid="LightSwitch_FindLocation" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
</Grid>
|
||||
<ProgressRing
|
||||
x:Name="SyncLoader"
|
||||
Grid.Row="1"
|
||||
Grid.Row="2"
|
||||
Width="40"
|
||||
Height="40"
|
||||
VerticalAlignment="Center"
|
||||
IsActive="False"
|
||||
Visibility="Collapsed" />
|
||||
|
||||
<Grid
|
||||
x:Name="LocationResultPanel"
|
||||
Grid.Row="1"
|
||||
Grid.Row="2"
|
||||
VerticalAlignment="Bottom"
|
||||
ColumnSpacing="16"
|
||||
RowSpacing="12"
|
||||
Visibility="Collapsed">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<FontIcon FontSize="16" Glyph="">
|
||||
<ToolTipService.ToolTip>
|
||||
<TextBlock x:Uid="LightSwitch_LocationTooltip" />
|
||||
</ToolTipService.ToolTip>
|
||||
</FontIcon>
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
AutomationProperties.AutomationId="LocationResultText_LightSwitch"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
TextAlignment="Center">
|
||||
<Run Text="{x:Bind ViewModel.Latitude, Mode=OneWay}" /><Run Text="°, " />
|
||||
<Run Text="{x:Bind ViewModel.Longitude, Mode=OneWay}" /><Run Text="°" />
|
||||
</TextBlock>
|
||||
<FontIcon
|
||||
Grid.Column="1"
|
||||
FontSize="20"
|
||||
Glyph="">
|
||||
<FontIcon FontSize="20" Glyph="">
|
||||
<ToolTipService.ToolTip>
|
||||
<TextBlock x:Uid="LightSwitch_SunriseTooltip" />
|
||||
</ToolTipService.ToolTip>
|
||||
</FontIcon>
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
AutomationProperties.AutomationId="SunriseText_LightSwitch"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="{x:Bind ViewModel.LightTimeTimeSpan, Converter={StaticResource TimeSpanToFriendlyTimeConverter}, Mode=OneWay}"
|
||||
TextAlignment="Center" />
|
||||
Text="{x:Bind ViewModel.LocationPanelLightTime, Converter={StaticResource TimeSpanToFriendlyTimeConverter}, Mode=OneWay}"
|
||||
TextAlignment="Left" />
|
||||
<FontIcon
|
||||
Grid.Column="2"
|
||||
Grid.Row="1"
|
||||
FontSize="20"
|
||||
Glyph="">
|
||||
<ToolTipService.ToolTip>
|
||||
@@ -321,12 +327,12 @@
|
||||
</ToolTipService.ToolTip>
|
||||
</FontIcon>
|
||||
<TextBlock
|
||||
Grid.Row="2"
|
||||
Grid.Column="2"
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
AutomationProperties.AutomationId="SunsetText_LightSwitch"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="{x:Bind ViewModel.DarkTimeTimeSpan, Converter={StaticResource TimeSpanToFriendlyTimeConverter}, Mode=OneWay}"
|
||||
TextAlignment="Center" />
|
||||
Text="{x:Bind ViewModel.LocationPanelDarkTime, Converter={StaticResource TimeSpanToFriendlyTimeConverter}, Mode=OneWay}"
|
||||
TextAlignment="Left" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ContentDialog>
|
||||
@@ -358,4 +364,4 @@
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Grid>
|
||||
</Page>
|
||||
</local:NavigablePage>
|
||||
@@ -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<string, int> _sendConfigMsg = ShellPage.SendDefaultIPCMessage;
|
||||
private readonly string appName = "LightSwitch";
|
||||
private readonly SettingsUtils settingsUtils;
|
||||
private readonly Func<string, int> sendConfigMsg = ShellPage.SendDefaultIPCMessage;
|
||||
|
||||
private readonly ISettingsRepository<GeneralSettings> _generalSettingsRepository;
|
||||
private readonly ISettingsRepository<LightSwitchSettings> _moduleSettingsRepository;
|
||||
private readonly SettingsRepository<GeneralSettings> generalSettingsRepository;
|
||||
private readonly SettingsRepository<LightSwitchSettings> 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<GeneralSettings>.GetInstance(_settingsUtils);
|
||||
_moduleSettingsRepository = SettingsRepository<LightSwitchSettings>.GetInstance(_settingsUtils);
|
||||
this.generalSettingsRepository = SettingsRepository<GeneralSettings>.GetInstance(this.settingsUtils);
|
||||
this.moduleSettingsRepository = SettingsRepository<LightSwitchSettings>.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}<7D>, {ViewModel.Longitude}<7D>";
|
||||
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}<7D>, {ViewModel.Longitude}<7D>";
|
||||
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}<7D>, {this.ViewModel.Longitude}<7D>";
|
||||
|
||||
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<SndLightSwitchSettings> csIpcMessage = new(currentSettings);
|
||||
|
||||
SndLightSwitchSettings outSettings = new(ViewModel.ModuleSettings);
|
||||
SndLightSwitchSettings outSettings = new(this.ViewModel.ModuleSettings);
|
||||
SndModuleSettings<SndLightSwitchSettings> 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<GeneralSettings> generalSettingsRepository, ISettingsRepository<LightSwitchSettings> moduleSettingsRepository)
|
||||
private void LoadSettings(SettingsRepository<GeneralSettings> generalSettingsRepository, SettingsRepository<LightSwitchSettings> 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<City>)
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2677,9 +2677,7 @@ From there, simply click on one of the supported files in the File Explorer and
|
||||
<data name="Oobe_MouseUtils_MouseHighlighter_Description.Text" xml:space="preserve">
|
||||
<value>Use a keyboard shortcut to highlight left and right mouse clicks.</value>
|
||||
<comment>Mouse as in the hardware peripheral.</comment>
|
||||
</data>
|
||||
|
||||
<!-- CursorWrap Module -->
|
||||
</data>
|
||||
<data name="MouseUtils_Enable_CursorWrap.Header" xml:space="preserve">
|
||||
<value>Enable CursorWrap</value>
|
||||
</data>
|
||||
@@ -2689,8 +2687,6 @@ From there, simply click on one of the supported files in the File Explorer and
|
||||
<data name="MouseUtils_CursorWrap.Description" xml:space="preserve">
|
||||
<value>Wrap the mouse cursor between monitor edges</value>
|
||||
</data>
|
||||
|
||||
<!-- Activation Shortcut -->
|
||||
<data name="MouseUtils_CursorWrap_ActivationShortcut.Header" xml:space="preserve">
|
||||
<value>Activation shortcut</value>
|
||||
</data>
|
||||
@@ -2700,19 +2696,15 @@ From there, simply click on one of the supported files in the File Explorer and
|
||||
<data name="MouseUtils_CursorWrap_ActivationShortcut_Button.Content" xml:space="preserve">
|
||||
<value>Set shortcut</value>
|
||||
</data>
|
||||
|
||||
<data name="MouseUtils_CursorWrap_DisableWrapDuringDrag.Content" xml:space="preserve">
|
||||
<value>Disable wrapping while dragging</value>
|
||||
</data>
|
||||
|
||||
<!-- Auto-activate -->
|
||||
</data>
|
||||
<data name="MouseUtils_CursorWrap_AutoActivate.Header" xml:space="preserve">
|
||||
<value>Auto-activate on startup</value>
|
||||
</data>
|
||||
<data name="MouseUtils_CursorWrap_AutoActivate.Content" xml:space="preserve">
|
||||
<value>Automatically activate on utility startup</value>
|
||||
</data>
|
||||
|
||||
</data>
|
||||
<data name="Oobe_MouseUtils_MousePointerCrosshairs.Text" xml:space="preserve">
|
||||
<value>Mouse Pointer Crosshairs</value>
|
||||
<comment>Mouse as in the hardware peripheral.</comment>
|
||||
@@ -5465,16 +5457,13 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
|
||||
<value>Select a location</value>
|
||||
</data>
|
||||
<data name="LightSwitch_LocationDialog.PrimaryButtonText" xml:space="preserve">
|
||||
<value>Select</value>
|
||||
<value>Save</value>
|
||||
</data>
|
||||
<data name="LightSwitch_LocationDialog.SecondaryButtonText" xml:space="preserve">
|
||||
<value>Cancel</value>
|
||||
</data>
|
||||
<data name="LightSwitch_GetCurrentLocation.Text" xml:space="preserve">
|
||||
<value>Get current location</value>
|
||||
</data>
|
||||
<data name="LightSwitch_LocationDialog_Description.Text" xml:space="preserve">
|
||||
<value>To calculate the sunrise and sunset, Light Switch needs a location.</value>
|
||||
<value>Detect your location automatically or enter it manually to calculate sunrise and sunset times.</value>
|
||||
</data>
|
||||
<data name="LightSwitch_SunriseText.Text" xml:space="preserve">
|
||||
<value>Sunrise</value>
|
||||
@@ -5482,8 +5471,17 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
|
||||
<data name="LightSwitch_SunsetText.Text" xml:space="preserve">
|
||||
<value>Sunset</value>
|
||||
</data>
|
||||
<data name="LightSwitch_LocationTooltip.Text" xml:space="preserve">
|
||||
<value>Location</value>
|
||||
<data name="LightSwitch_LatitudeBox.Header" xml:space="preserve">
|
||||
<value>Latitude</value>
|
||||
</data>
|
||||
<data name="LightSwitch_LongitudeBox.Header" xml:space="preserve">
|
||||
<value>Longitude</value>
|
||||
</data>
|
||||
<data name="LightSwitch_FindLocation.Text" xml:space="preserve">
|
||||
<value>Detect location</value>
|
||||
</data>
|
||||
<data name="LightSwitch_FindLocationAutomation.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Detect location</value>
|
||||
</data>
|
||||
<data name="LightSwitch_SunriseTooltip.Text" xml:space="preserve">
|
||||
<value>Sunrise</value>
|
||||
@@ -5567,7 +5565,7 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
|
||||
</data>
|
||||
<data name="ShortcutConflictControl_Automation.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Shortcut conflicts</value>
|
||||
</data>
|
||||
</data>
|
||||
<data name="ShortcutConflictControl_NoConflictsFound" xml:space="preserve">
|
||||
<value>No conflicts found</value>
|
||||
</data>
|
||||
@@ -5678,4 +5676,7 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
|
||||
<data name="Hosts_Backup_CountInput_Header" xml:space="preserve">
|
||||
<value>Backup count</value>
|
||||
</data>
|
||||
<data name="LightSwitch_SetLocationButton.Content" xml:space="preserve">
|
||||
<value>Set Location</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user