mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-03 09:46:54 +02:00
[New Module] Light Switch (#41987)
<!-- 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 pull request introduces a new module called "Light Switch" which allows users to automatically switch between light and dark mode on a timer.  <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [x] Closes: #1331 - [x] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [x] **Tests:** Added/updated and all pass - [x] **Localization:** All end-user-facing strings can be localized - [x] **Dev docs:** Added/updated - [x] **New binaries:** Added on the required places - [x] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [x] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [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: [#5867](https://github.com/MicrosoftDocs/windows-dev-docs-pr/pull/5867) <!-- Provide a more detailed description of the PR, other things fixed, or any additional comments/features here --> ## Detailed Description of the Pull Request / Additional comments ### Known bugs: - Default settings not saving correctly when switching modes - Issue: Sometimes when you switch from one mode to another, they are supposed to update with new defaults but sometimes this fails for the second variable. Potentially has to do with accessing the settings file while another chunk of code is still updating. - Sometimes the system looks "glitched" when switching themes ### To do: - [x] OOBE page and assets - [x] Logic to disable the chart when no location has been selected - [x] Localization ### How to and what to test Grab the latest installer from the pipeline below for your architecture and install PowerToys from there. - Toggle theme shortcutSystem only, Apps only, Both system and apps selected - Does changing the values on the settings page update the settings file? %LOCALAPPDATA%/Microsoft/PowerToys/LightSwitch/settings.json - Manual mode: System only, Apps only, Both system and apps selected - Sunrise modes: Are the times accurate? - If you manage to let this run through sunset/rise does the theme change? - Set your theme to change within the next minute using manual mode and set your device to sleepOpen your device and login once the time you set has passed. --> Do your settings resync once the next minute ticks after logging back into your device? - Disable the service and ensure the tasks actually ends. - While the module is disabled: - Make sure the shortcut no longer works - Make sure the last time you set doesn't trigger a theme change - Bonus: Toggle GPO Configuration and make sure you are unable to enable the module --------- Co-authored-by: Niels Laute <niels.laute@live.nl> Co-authored-by: Gordon Lam (SH) <yeelam@microsoft.com>
This commit is contained in:
@@ -513,6 +513,23 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
}
|
||||
}
|
||||
|
||||
private bool lightSwitch;
|
||||
|
||||
[JsonPropertyName("LightSwitch")]
|
||||
public bool LightSwitch
|
||||
{
|
||||
get => lightSwitch;
|
||||
set
|
||||
{
|
||||
if (lightSwitch != value)
|
||||
{
|
||||
LogTelemetryEvent(value);
|
||||
lightSwitch = value;
|
||||
NotifyChange();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void NotifyChange()
|
||||
{
|
||||
notifyEnabledChangedAction?.Invoke();
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Settings.UI.Library.Helpers
|
||||
{
|
||||
public class SearchLocation
|
||||
{
|
||||
public string City { get; set; }
|
||||
|
||||
public string Country { get; set; }
|
||||
|
||||
public double Latitude { get; set; }
|
||||
|
||||
public double Longitude { get; set; }
|
||||
|
||||
public SearchLocation(string city, string country, double latitude, double longitude)
|
||||
{
|
||||
City = city;
|
||||
Country = country;
|
||||
Latitude = latitude;
|
||||
Longitude = longitude;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Settings.UI.Library.Helpers;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Helpers
|
||||
{
|
||||
public static class SearchLocationLoader
|
||||
{
|
||||
private static readonly List<SearchLocation> LocationDataList = new List<SearchLocation>();
|
||||
|
||||
public static IEnumerable<SearchLocation> GetAll()
|
||||
{
|
||||
return LocationDataList
|
||||
.GroupBy(l => $"{l.Country}|{l.City}|{l.Latitude.ToString(CultureInfo.InvariantCulture)}|{l.Longitude.ToString(CultureInfo.InvariantCulture)}")
|
||||
.Select(g => g.First())
|
||||
.OrderBy(l => l.Country, StringComparer.OrdinalIgnoreCase)
|
||||
.ThenBy(l => l.City, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
131
src/settings-ui/Settings.UI.Library/Helpers/SunCalc.cs
Normal file
131
src/settings-ui/Settings.UI.Library/Helpers/SunCalc.cs
Normal file
@@ -0,0 +1,131 @@
|
||||
// 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;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Library.Helpers
|
||||
{
|
||||
public static class SunCalc
|
||||
{
|
||||
public static SunTimes CalculateSunriseSunset(double latitude, double longitude, int year, int month, int day)
|
||||
{
|
||||
double zenith = 90.833; // official sunrise/sunset
|
||||
|
||||
int n1 = (int)Math.Floor(275.0 * month / 9.0);
|
||||
int n2 = (int)Math.Floor((month + 9.0) / 12.0);
|
||||
int n3 = (int)Math.Floor(1.0 + Math.Floor((year - (4.0 * Math.Floor(year / 4.0)) + 2.0) / 3.0));
|
||||
int n = n1 - (n2 * n3) + day - 30;
|
||||
|
||||
double? riseUT = CalcTime(isSunrise: true);
|
||||
double? setUT = CalcTime(isSunrise: false);
|
||||
|
||||
var riseLocal = ToLocal(riseUT, year, month, day);
|
||||
var setLocal = ToLocal(setUT, year, month, day);
|
||||
|
||||
var result = new SunTimes
|
||||
{
|
||||
HasSunrise = riseLocal.HasValue,
|
||||
HasSunset = setLocal.HasValue,
|
||||
SunriseHour = riseLocal?.Hour ?? -1,
|
||||
SunriseMinute = riseLocal?.Minute ?? -1,
|
||||
SunsetHour = setLocal?.Hour ?? -1,
|
||||
SunsetMinute = setLocal?.Minute ?? -1,
|
||||
};
|
||||
|
||||
return result;
|
||||
|
||||
// Local functions
|
||||
double? CalcTime(bool isSunrise)
|
||||
{
|
||||
double lngHour = longitude / 15.0;
|
||||
double t = isSunrise ? n + ((6 - lngHour) / 24.0) : n + ((18 - lngHour) / 24.0);
|
||||
|
||||
double m1 = (0.9856 * t) - 3.289;
|
||||
double l = m1 + (1.916 * Math.Sin(Deg2Rad(m1))) + (0.020 * Math.Sin(2 * Deg2Rad(m1))) + 282.634;
|
||||
l = NormalizeDegrees(l);
|
||||
|
||||
double rA = Rad2Deg(Math.Atan(0.91764 * Math.Tan(Deg2Rad(l))));
|
||||
rA = NormalizeDegrees(rA);
|
||||
|
||||
double lquadrant = Math.Floor(l / 90.0) * 90.0;
|
||||
double rAquadrant = Math.Floor(rA / 90.0) * 90.0;
|
||||
rA = rA + (lquadrant - rAquadrant);
|
||||
rA /= 15.0;
|
||||
|
||||
double sinDec = 0.39782 * Math.Sin(Deg2Rad(l));
|
||||
double cosDec = Math.Cos(Math.Asin(sinDec));
|
||||
|
||||
double cosH = (Math.Cos(Deg2Rad(zenith)) - (sinDec * Math.Sin(Deg2Rad(latitude))))
|
||||
/ (cosDec * Math.Cos(Deg2Rad(latitude)));
|
||||
|
||||
if (cosH > 1.0 || cosH < -1.0)
|
||||
{
|
||||
// Sun never rises or never sets on this date at this location
|
||||
return null;
|
||||
}
|
||||
|
||||
double h = isSunrise ? 360.0 - Rad2Deg(Math.Acos(cosH)) : Rad2Deg(Math.Acos(cosH));
|
||||
h /= 15.0;
|
||||
|
||||
double t1 = h + rA - (0.06571 * t) - 6.622;
|
||||
double uT = t1 - lngHour;
|
||||
uT = NormalizeHours(uT);
|
||||
|
||||
return uT;
|
||||
}
|
||||
|
||||
static (int Hour, int Minute)? ToLocal(double? ut, int y, int m, int d)
|
||||
{
|
||||
if (!ut.HasValue)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Convert fractional hours to hh:mm with proper rounding
|
||||
int hours = (int)Math.Floor(ut.Value);
|
||||
int minutes = (int)((ut.Value - hours) * 60.0);
|
||||
|
||||
// Normalize minute overflow
|
||||
if (minutes == 60)
|
||||
{
|
||||
minutes = 0;
|
||||
hours = (hours + 1) % 24;
|
||||
}
|
||||
|
||||
// Build a UTC DateTime on the given date
|
||||
var utc = new DateTime(y, m, d, hours, minutes, 0, DateTimeKind.Utc);
|
||||
|
||||
// Convert to local time using system time zone rules for that date
|
||||
var local = TimeZoneInfo.ConvertTimeFromUtc(utc, TimeZoneInfo.Local);
|
||||
|
||||
return (local.Hour, local.Minute);
|
||||
}
|
||||
|
||||
static double Deg2Rad(double deg) => deg * Math.PI / 180.0;
|
||||
static double Rad2Deg(double rad) => rad * 180.0 / Math.PI;
|
||||
|
||||
static double NormalizeDegrees(double angle)
|
||||
{
|
||||
angle %= 360.0;
|
||||
if (angle < 0)
|
||||
{
|
||||
angle += 360.0;
|
||||
}
|
||||
|
||||
return angle;
|
||||
}
|
||||
|
||||
static double NormalizeHours(double hours)
|
||||
{
|
||||
hours %= 24.0;
|
||||
if (hours < 0)
|
||||
{
|
||||
hours += 24.0;
|
||||
}
|
||||
|
||||
return hours;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src/settings-ui/Settings.UI.Library/Helpers/SunTimes.cs
Normal file
24
src/settings-ui/Settings.UI.Library/Helpers/SunTimes.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Library.Helpers
|
||||
{
|
||||
public struct SunTimes
|
||||
{
|
||||
public int SunriseHour;
|
||||
public int SunriseMinute;
|
||||
public int SunsetHour;
|
||||
public int SunsetMinute;
|
||||
public string Text;
|
||||
|
||||
public bool HasSunrise;
|
||||
public bool HasSunset;
|
||||
}
|
||||
}
|
||||
66
src/settings-ui/Settings.UI.Library/LightSwitchProperties.cs
Normal file
66
src/settings-ui/Settings.UI.Library/LightSwitchProperties.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
// 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.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
{
|
||||
public class LightSwitchProperties
|
||||
{
|
||||
public const bool DefaultChangeSystem = true;
|
||||
public const bool DefaultChangeApps = true;
|
||||
public const int DefaultLightTime = 480;
|
||||
public const int DefaultDarkTime = 1200;
|
||||
public const int DefaultSunriseOffset = 0;
|
||||
public const int DefaultSunsetOffset = 0;
|
||||
public const string DefaultLatitude = "0.0";
|
||||
public const string DefaultLongitude = "0.0";
|
||||
public const string DefaultScheduleMode = "FixedHours";
|
||||
public static readonly HotkeySettings DefaultToggleThemeHotkey = new HotkeySettings(true, true, false, true, 0x44); // Ctrl+Win+Shift+D
|
||||
|
||||
public LightSwitchProperties()
|
||||
{
|
||||
ChangeSystem = new BoolProperty(DefaultChangeSystem);
|
||||
ChangeApps = new BoolProperty(DefaultChangeApps);
|
||||
LightTime = new IntProperty(DefaultLightTime);
|
||||
DarkTime = new IntProperty(DefaultDarkTime);
|
||||
Latitude = new StringProperty(DefaultLatitude);
|
||||
Longitude = new StringProperty(DefaultLongitude);
|
||||
SunriseOffset = new IntProperty(DefaultSunriseOffset);
|
||||
SunsetOffset = new IntProperty(DefaultSunsetOffset);
|
||||
ScheduleMode = new StringProperty(DefaultScheduleMode);
|
||||
ToggleThemeHotkey = new KeyboardKeysProperty(DefaultToggleThemeHotkey);
|
||||
}
|
||||
|
||||
[JsonPropertyName("changeSystem")]
|
||||
public BoolProperty ChangeSystem { get; set; }
|
||||
|
||||
[JsonPropertyName("changeApps")]
|
||||
public BoolProperty ChangeApps { get; set; }
|
||||
|
||||
[JsonPropertyName("lightTime")]
|
||||
public IntProperty LightTime { get; set; }
|
||||
|
||||
[JsonPropertyName("darkTime")]
|
||||
public IntProperty DarkTime { get; set; }
|
||||
|
||||
[JsonPropertyName("sunrise_offset")]
|
||||
public IntProperty SunriseOffset { get; set; }
|
||||
|
||||
[JsonPropertyName("sunset_offset")]
|
||||
public IntProperty SunsetOffset { get; set; }
|
||||
|
||||
[JsonPropertyName("latitude")]
|
||||
public StringProperty Latitude { get; set; }
|
||||
|
||||
[JsonPropertyName("longitude")]
|
||||
public StringProperty Longitude { get; set; }
|
||||
|
||||
[JsonPropertyName("scheduleMode")]
|
||||
public StringProperty ScheduleMode { get; set; }
|
||||
|
||||
[JsonPropertyName("toggle-theme-hotkey")]
|
||||
public KeyboardKeysProperty ToggleThemeHotkey { get; set; }
|
||||
}
|
||||
}
|
||||
58
src/settings-ui/Settings.UI.Library/LightSwitchSettings.cs
Normal file
58
src/settings-ui/Settings.UI.Library/LightSwitchSettings.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
// 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.Reflection;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
|
||||
|
||||
namespace Settings.UI.Library
|
||||
{
|
||||
public class LightSwitchSettings : BasePTModuleSettings, ISettingsConfig, ICloneable
|
||||
{
|
||||
public const string ModuleName = "LightSwitch";
|
||||
|
||||
public LightSwitchSettings()
|
||||
{
|
||||
Name = ModuleName;
|
||||
Version = Assembly.GetExecutingAssembly().GetName().Version.ToString();
|
||||
Properties = new LightSwitchProperties();
|
||||
}
|
||||
|
||||
[JsonPropertyName("properties")]
|
||||
public LightSwitchProperties Properties { get; set; }
|
||||
|
||||
public object Clone()
|
||||
{
|
||||
return new LightSwitchSettings()
|
||||
{
|
||||
Name = Name,
|
||||
Version = Version,
|
||||
Properties = new LightSwitchProperties()
|
||||
{
|
||||
ChangeSystem = new BoolProperty(Properties.ChangeSystem.Value),
|
||||
ChangeApps = new BoolProperty(Properties.ChangeApps.Value),
|
||||
ScheduleMode = new StringProperty(Properties.ScheduleMode.Value),
|
||||
LightTime = new IntProperty((int)Properties.LightTime.Value),
|
||||
DarkTime = new IntProperty((int)Properties.DarkTime.Value),
|
||||
SunriseOffset = new IntProperty((int)Properties.SunriseOffset.Value),
|
||||
SunsetOffset = new IntProperty((int)Properties.SunsetOffset.Value),
|
||||
Latitude = new StringProperty(Properties.Latitude.Value),
|
||||
Longitude = new StringProperty(Properties.Longitude.Value),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public string GetModuleName()
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
|
||||
public bool UpgradeSettingsConfiguration()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// 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.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Settings.UI.Library;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
{
|
||||
public class SndLightSwitchSettings
|
||||
{
|
||||
[JsonPropertyName("LightSwitch")]
|
||||
public LightSwitchSettings Settings { get; set; }
|
||||
|
||||
public SndLightSwitchSettings()
|
||||
{
|
||||
}
|
||||
|
||||
public SndLightSwitchSettings(LightSwitchSettings settings)
|
||||
{
|
||||
Settings = settings;
|
||||
}
|
||||
|
||||
public string ToJsonString()
|
||||
{
|
||||
return JsonSerializer.Serialize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 228 KiB |
@@ -0,0 +1,37 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Converters
|
||||
{
|
||||
public partial class EnumToVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value == null || parameter == null)
|
||||
{
|
||||
return Visibility.Collapsed;
|
||||
}
|
||||
|
||||
string enumString = value.ToString();
|
||||
string targetString = parameter.ToString();
|
||||
|
||||
return enumString.Equals(targetString, StringComparison.OrdinalIgnoreCase)
|
||||
? Visibility.Visible
|
||||
: Visibility.Collapsed;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// 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 Microsoft.PowerToys.Settings.UI.Helpers;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Converters;
|
||||
|
||||
public sealed partial class TimeSpanToFriendlyTimeConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is TimeSpan time)
|
||||
{
|
||||
return TimeSpanHelper.Convert(time);
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language) => new NotImplementedException();
|
||||
}
|
||||
@@ -52,6 +52,7 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers
|
||||
case ModuleType.CmdPal: return generalSettingsConfig.Enabled.CmdPal;
|
||||
case ModuleType.ColorPicker: return generalSettingsConfig.Enabled.ColorPicker;
|
||||
case ModuleType.CropAndLock: return generalSettingsConfig.Enabled.CropAndLock;
|
||||
case ModuleType.LightSwitch: return generalSettingsConfig.Enabled.LightSwitch;
|
||||
case ModuleType.EnvironmentVariables: return generalSettingsConfig.Enabled.EnvironmentVariables;
|
||||
case ModuleType.FancyZones: return generalSettingsConfig.Enabled.FancyZones;
|
||||
case ModuleType.FileLocksmith: return generalSettingsConfig.Enabled.FileLocksmith;
|
||||
@@ -88,6 +89,7 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers
|
||||
case ModuleType.CmdPal: generalSettingsConfig.Enabled.CmdPal = isEnabled; break;
|
||||
case ModuleType.ColorPicker: generalSettingsConfig.Enabled.ColorPicker = isEnabled; break;
|
||||
case ModuleType.CropAndLock: generalSettingsConfig.Enabled.CropAndLock = isEnabled; break;
|
||||
case ModuleType.LightSwitch: generalSettingsConfig.Enabled.LightSwitch = isEnabled; break;
|
||||
case ModuleType.EnvironmentVariables: generalSettingsConfig.Enabled.EnvironmentVariables = isEnabled; break;
|
||||
case ModuleType.FancyZones: generalSettingsConfig.Enabled.FancyZones = isEnabled; break;
|
||||
case ModuleType.FileLocksmith: generalSettingsConfig.Enabled.FileLocksmith = isEnabled; break;
|
||||
@@ -159,6 +161,7 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers
|
||||
ModuleType.CmdPal => typeof(CmdPalPage),
|
||||
ModuleType.ColorPicker => typeof(ColorPickerPage),
|
||||
ModuleType.CropAndLock => typeof(CropAndLockPage),
|
||||
ModuleType.LightSwitch => typeof(LightSwitchPage),
|
||||
ModuleType.EnvironmentVariables => typeof(EnvironmentVariablesPage),
|
||||
ModuleType.FancyZones => typeof(FancyZonesPage),
|
||||
ModuleType.FileLocksmith => typeof(FileLocksmithPage),
|
||||
|
||||
38
src/settings-ui/Settings.UI/Helpers/TimeSpanHelper.cs
Normal file
38
src/settings-ui/Settings.UI/Helpers/TimeSpanHelper.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Helpers;
|
||||
|
||||
public static class TimeSpanHelper
|
||||
{
|
||||
public static string Convert(TimeSpan? time)
|
||||
{
|
||||
if (time is not TimeSpan ts)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// If user passed in a negative TimeSpan, normalize
|
||||
if (ts < TimeSpan.Zero)
|
||||
{
|
||||
ts = ts.Duration();
|
||||
}
|
||||
|
||||
// Map the TimeSpan to a DateTime on today's date
|
||||
var dt = DateTime.Today.Add(ts);
|
||||
|
||||
// This pattern automatically respects system 12/24-hour setting
|
||||
string pattern = CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern;
|
||||
|
||||
return dt.ToString(pattern, CultureInfo.CurrentCulture);
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Enums
|
||||
FileExplorer,
|
||||
ImageResizer,
|
||||
KBM,
|
||||
LightSwitch,
|
||||
MouseUtils,
|
||||
MouseWithoutBorders,
|
||||
Peek,
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
<ItemGroup>
|
||||
<None Remove="Assets\Settings\Modules\APDialog.dark.png" />
|
||||
<None Remove="Assets\Settings\Modules\APDialog.light.png" />
|
||||
<None Remove="Assets\Settings\Modules\LightSwitch.png" />
|
||||
<None Remove="SettingsXAML\Controls\Dashboard\CheckUpdateControl.xaml" />
|
||||
<None Remove="SettingsXAML\Controls\Dashboard\ShortcutConflictControl.xaml" />
|
||||
<None Remove="SettingsXAML\Controls\KeyVisual\KeyCharPresenter.xaml" />
|
||||
@@ -156,7 +157,9 @@
|
||||
</None>
|
||||
<None Update="Assets\Settings\Scripts\DisableModule.ps1">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None> <Page Update="SettingsXAML\Controls\TitleBar\TitleBar.xaml">
|
||||
</None>
|
||||
<Page Update="SettingsXAML\Controls\TitleBar\TitleBar.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Update="SettingsXAML\Controls\KeyVisual\KeyCharPresenter.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
@@ -170,6 +173,12 @@
|
||||
<Page Update="SettingsXAML\Controls\Dashboard\CheckUpdateControl.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Update="SettingsXAML\Controls\Timeline\TimelineStyles.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Update="SettingsXAML\Controls\Timeline\Timeline.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -10,6 +10,7 @@ using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.PowerToys.Settings.UI.Helpers;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Settings.UI.Library;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.SerializationContext;
|
||||
|
||||
@@ -22,6 +23,7 @@ namespace Microsoft.PowerToys.Settings.UI.SerializationContext;
|
||||
[JsonSerializable(typeof(FileLocksmithSettings))]
|
||||
[JsonSerializable(typeof(FindMyMouseSettings))]
|
||||
[JsonSerializable(typeof(IList<PowerToysReleaseInfo>))]
|
||||
[JsonSerializable(typeof(LightSwitchSettings))]
|
||||
[JsonSerializable(typeof(MeasureToolSettings))]
|
||||
[JsonSerializable(typeof(MouseHighlighterSettings))]
|
||||
[JsonSerializable(typeof(MouseJumpSettings))]
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<ResourceDictionary Source="/SettingsXAML/Styles/InfoBadge.xaml" />
|
||||
<ResourceDictionary Source="/SettingsXAML/Themes/Colors.xaml" />
|
||||
<ResourceDictionary Source="/SettingsXAML/Themes/Generic.xaml" />
|
||||
<ResourceDictionary Source="/SettingsXAML/Controls/Timeline/TimelineStyles.xaml" />
|
||||
|
||||
<!-- Other merged dictionaries here -->
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
@@ -417,6 +417,7 @@ namespace Microsoft.PowerToys.Settings.UI
|
||||
case "Awake": return typeof(AwakePage);
|
||||
case "CmdNotFound": return typeof(CmdNotFoundPage);
|
||||
case "ColorPicker": return typeof(ColorPickerPage);
|
||||
case "LightSwitch": return typeof(LightSwitchPage);
|
||||
case "FancyZones": return typeof(FancyZonesPage);
|
||||
case "FileLocksmith": return typeof(FileLocksmithPage);
|
||||
case "Run": return typeof(PowerLauncherPage);
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
Style="{StaticResource TitleTextBlockStyle}"
|
||||
Text="{x:Bind ModuleTitle}" />
|
||||
|
||||
<ScrollViewer Grid.Row="1">
|
||||
<ScrollViewer Grid.Row="1" AutomationProperties.AutomationId="PageScrollViewer">
|
||||
<Grid
|
||||
Padding="0,0,20,48"
|
||||
ChildrenTransitions="{StaticResource SettingsCardsAnimations}"
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<UserControl
|
||||
x:Class="Microsoft.PowerToys.Settings.UI.Controls.Timeline"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="using:Microsoft.PowerToys.Settings.UI.Controls"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Canvas
|
||||
x:Name="HeaderCanvas"
|
||||
Grid.Row="0"
|
||||
Height="24" />
|
||||
|
||||
<!-- Timeline (bands + ticks + labels) -->
|
||||
<Border
|
||||
Grid.Row="1"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{ThemeResource OverlayCornerRadius}">
|
||||
<Canvas
|
||||
x:Name="TimelineCanvas"
|
||||
Height="36"
|
||||
Loaded="TimelineCanvas_Loaded" />
|
||||
</Border>
|
||||
|
||||
<!-- Below-chart annotations (sunrise/sunset panels + major labels) -->
|
||||
<Canvas
|
||||
x:Name="AnnotationCanvas"
|
||||
Grid.Row="2"
|
||||
Height="32" />
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,658 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using Microsoft.PowerToys.Settings.UI.Helpers;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Automation;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Shapes;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Controls
|
||||
{
|
||||
public sealed partial class Timeline : UserControl
|
||||
{
|
||||
public TimeSpan StartTime
|
||||
{
|
||||
get => (TimeSpan)GetValue(StartTimeProperty);
|
||||
set => SetValue(StartTimeProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty StartTimeProperty = DependencyProperty.Register(nameof(StartTime), typeof(TimeSpan), typeof(Timeline), new PropertyMetadata(defaultValue: new TimeSpan(22, 0, 0), OnTimeChanged));
|
||||
|
||||
public TimeSpan EndTime
|
||||
{
|
||||
get => (TimeSpan)GetValue(EndTimeProperty);
|
||||
set => SetValue(EndTimeProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty EndTimeProperty = DependencyProperty.Register(nameof(EndTime), typeof(TimeSpan), typeof(Timeline), new PropertyMetadata(defaultValue: new TimeSpan(7, 0, 0), OnTimeChanged));
|
||||
|
||||
public TimeSpan? Sunrise
|
||||
{
|
||||
get => (TimeSpan?)GetValue(SunriseProperty);
|
||||
set => SetValue(SunriseProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty SunriseProperty = DependencyProperty.Register(nameof(Sunrise), typeof(TimeSpan), typeof(Timeline), new PropertyMetadata(defaultValue: null, OnTimeChanged));
|
||||
|
||||
public TimeSpan? Sunset
|
||||
{
|
||||
get => (TimeSpan?)GetValue(SunsetProperty);
|
||||
set => SetValue(SunsetProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty SunsetProperty = DependencyProperty.Register(nameof(Sunset), typeof(TimeSpan), typeof(Timeline), new PropertyMetadata(defaultValue: null, OnTimeChanged));
|
||||
|
||||
private readonly List<int> _tickHours = new();
|
||||
|
||||
// Locale 24h/12h
|
||||
private readonly bool _is24h = CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern.Contains('H');
|
||||
|
||||
// Visuals
|
||||
private readonly List<Line> _ticks = new();
|
||||
private readonly List<TextBlock> _majorTickBottomLabels = new(); // 00,06,12,18,24 (below)
|
||||
|
||||
private readonly List<Border> _darkRects = new(); // up to 2 (wrap)
|
||||
private readonly List<Border> _lightRects = new(); // up to 2 (complement)
|
||||
|
||||
private TextBlock _startEdgeLabel; // top-of-chart
|
||||
private TextBlock _endEdgeLabel;
|
||||
|
||||
private Line _sunriseTick;
|
||||
private Line _sunsetTick;
|
||||
|
||||
// Add/replace these constants (top of your class)
|
||||
private const int TickHourStep = 2; // <-- every 2 hours
|
||||
|
||||
private StackPanel _sunrisePanel; // icon + time (below chart)
|
||||
private StackPanel _sunsetPanel;
|
||||
|
||||
public Timeline()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
this.Loaded += Timeline_Loaded;
|
||||
this.IsEnabledChanged += Timeline_IsEnabledChanged;
|
||||
}
|
||||
|
||||
private void Timeline_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
CheckEnabledState();
|
||||
}
|
||||
|
||||
private void Timeline_IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
CheckEnabledState();
|
||||
}
|
||||
|
||||
private void CheckEnabledState()
|
||||
{
|
||||
if (IsEnabled)
|
||||
{
|
||||
this.Opacity = 1.0;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Opacity = 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnTimeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
((Timeline)d).Setup();
|
||||
}
|
||||
|
||||
private void Setup()
|
||||
{
|
||||
EnsureBands();
|
||||
EnsureTicks();
|
||||
EnsureStartEndEdgeLabels();
|
||||
EnsureSunriseSunsetTicks();
|
||||
EnsureSunPanels();
|
||||
EnsureMajorTickLabels();
|
||||
UpdateAll();
|
||||
}
|
||||
|
||||
private void TimelineCanvas_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// SizeChanged wiring here (as requested)
|
||||
HeaderCanvas.SizeChanged += (_, __) => UpdateAll();
|
||||
TimelineCanvas.SizeChanged += (_, __) => UpdateAll();
|
||||
AnnotationCanvas.SizeChanged += (_, __) => UpdateAll();
|
||||
Setup();
|
||||
}
|
||||
|
||||
private void UpdateAll()
|
||||
{
|
||||
UpdateBandsLayout();
|
||||
UpdateTicksLayout();
|
||||
UpdateStartEndEdgeLabelsLayout();
|
||||
UpdateSunriseSunsetTicksLayout();
|
||||
UpdateSunPanelsLayout();
|
||||
UpdateMajorTickLabelsLayout();
|
||||
AutomationProperties.SetHelpText(
|
||||
this,
|
||||
$"Start={StartTime};End={EndTime};Sunrise={Sunrise};Sunset={Sunset}");
|
||||
}
|
||||
|
||||
// ===== Ticks =====
|
||||
private void EnsureTicks()
|
||||
{
|
||||
if (_ticks.Count > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_tickHours.Clear();
|
||||
|
||||
// Build ticks at 0,2,4,...,24 but skip the first/last MAJOR ticks (0 and 24)
|
||||
for (int hour = 0; hour <= 24; hour += TickHourStep)
|
||||
{
|
||||
bool isMajor = hour % 6 == 0;
|
||||
if (isMajor && (hour == 0 || hour == 24))
|
||||
{
|
||||
continue; // skip first/last major ticks
|
||||
}
|
||||
|
||||
var line = new Line
|
||||
{
|
||||
Style = (Style)Application.Current.Resources[isMajor ? "MajorHourTickStyle" : "HourTickStyle"],
|
||||
};
|
||||
|
||||
Canvas.SetZIndex(line, 0); // above bands (adjust if needed)
|
||||
|
||||
_ticks.Add(line);
|
||||
_tickHours.Add(hour);
|
||||
|
||||
// If you actually want these IN the chart, use TimelineCanvas instead:
|
||||
AnnotationCanvas.Children.Add(line); // or TimelineCanvas.Children.Add(line);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateTicksLayout()
|
||||
{
|
||||
double w = TimelineCanvas.ActualWidth;
|
||||
double h = TimelineCanvas.ActualHeight; // keeping your offset
|
||||
if (w <= 0 || h <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
double minorLen = h * 0.1;
|
||||
double majorLen = h * 0.2;
|
||||
|
||||
for (int i = 0; i < _ticks.Count; i++)
|
||||
{
|
||||
int hour = _tickHours[i];
|
||||
double x = Math.Round((hour / 24.0) * w);
|
||||
|
||||
var line = _ticks[i];
|
||||
double len = (hour % 6 == 0) ? majorLen : minorLen;
|
||||
|
||||
line.X1 = x;
|
||||
line.Y1 = 0;
|
||||
line.X2 = x;
|
||||
line.Y2 = len;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Bands (Dark + Light) =====
|
||||
private void EnsureBands()
|
||||
{
|
||||
if (_darkRects.Count == 0)
|
||||
{
|
||||
_darkRects.Add(MakeBandRect(isDark: false));
|
||||
_darkRects.Add(MakeBandRect(isDark: false));
|
||||
}
|
||||
|
||||
if (_lightRects.Count == 0)
|
||||
{
|
||||
_lightRects.Add(MakeBandRect(isDark: true));
|
||||
_lightRects.Add(MakeBandRect(isDark: true));
|
||||
}
|
||||
}
|
||||
|
||||
private Border MakeBandRect(bool isDark)
|
||||
{
|
||||
var r = new Border();
|
||||
if (isDark)
|
||||
{
|
||||
r.Style = (Style)Application.Current.Resources["DarkBandStyle"];
|
||||
FontIcon icon = new FontIcon();
|
||||
icon.Style = (Style)Application.Current.Resources["DarkBandIconStyle"];
|
||||
r.Child = icon;
|
||||
}
|
||||
else
|
||||
{
|
||||
r.Style = (Style)Application.Current.Resources["LightBandStyle"];
|
||||
}
|
||||
|
||||
Canvas.SetZIndex(r, 5); // below ticks/labels
|
||||
TimelineCanvas.Children.Add(r);
|
||||
return r;
|
||||
}
|
||||
|
||||
private void UpdateBandsLayout()
|
||||
{
|
||||
double w = TimelineCanvas.ActualWidth;
|
||||
double h = TimelineCanvas.ActualHeight;
|
||||
if (w <= 0 || h <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var r in _darkRects)
|
||||
{
|
||||
r.Height = h;
|
||||
Canvas.SetTop(r, 0);
|
||||
}
|
||||
|
||||
foreach (var r in _lightRects)
|
||||
{
|
||||
r.Height = h;
|
||||
Canvas.SetTop(r, 0);
|
||||
}
|
||||
|
||||
var darkRanges = ToRanges(StartTime, EndTime); // 1 or 2 segments
|
||||
var lightRanges = ComplementRanges(darkRanges); // 0..2
|
||||
|
||||
LayoutRangeRects(_darkRects, darkRanges, w);
|
||||
LayoutRangeRects(_lightRects, lightRanges, w);
|
||||
}
|
||||
|
||||
private static void LayoutRangeRects(List<Border> rects, List<(TimeSpan Start, TimeSpan End)> ranges, double width)
|
||||
{
|
||||
for (int i = 0; i < rects.Count; i++)
|
||||
{
|
||||
if (i < ranges.Count)
|
||||
{
|
||||
var (start, end) = ranges[i];
|
||||
double x = Math.Round((start.TotalHours / 24.0) * width);
|
||||
double x2 = Math.Round((end.TotalHours / 24.0) * width);
|
||||
|
||||
var r = rects[i];
|
||||
Canvas.SetLeft(r, x);
|
||||
r.Width = Math.Max(0, x2 - x);
|
||||
r.Visibility = Visibility.Visible;
|
||||
}
|
||||
else
|
||||
{
|
||||
rects[i].Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static List<(TimeSpan Start, TimeSpan End)> ToRanges(TimeSpan start, TimeSpan end)
|
||||
{
|
||||
// Full day
|
||||
if (start == end)
|
||||
{
|
||||
return new() { (TimeSpan.Zero, TimeSpan.FromHours(24)) };
|
||||
}
|
||||
|
||||
if (start < end)
|
||||
{
|
||||
return new() { (start, end) };
|
||||
}
|
||||
|
||||
// Wraps midnight
|
||||
return new()
|
||||
{
|
||||
(start, TimeSpan.FromHours(24)),
|
||||
(TimeSpan.Zero, end),
|
||||
};
|
||||
}
|
||||
|
||||
private static List<(TimeSpan Start, TimeSpan End)> ComplementRanges(List<(TimeSpan Start, TimeSpan End)> dark)
|
||||
{
|
||||
var res = new List<(TimeSpan, TimeSpan)>();
|
||||
|
||||
// If dark covers the full day, there is no light
|
||||
if (dark.Count == 1 && dark[0].Start == TimeSpan.Zero && dark[0].End == TimeSpan.FromHours(24))
|
||||
{
|
||||
return res;
|
||||
}
|
||||
|
||||
if (dark.Count == 1)
|
||||
{
|
||||
var (ds, de) = dark[0];
|
||||
if (ds > TimeSpan.Zero)
|
||||
{
|
||||
res.Add((TimeSpan.Zero, ds));
|
||||
}
|
||||
|
||||
if (de < TimeSpan.FromHours(24))
|
||||
{
|
||||
res.Add((de, TimeSpan.FromHours(24)));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// dark[0] = [a,24), dark[1] = [0,b) => single light [b,a)
|
||||
var a = dark[0].Start;
|
||||
var b = dark[1].End;
|
||||
res.Add((b, a));
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
// ===== Start & End labels (TOP of chart, ABOVE rectangles) =====
|
||||
private void EnsureStartEndEdgeLabels()
|
||||
{
|
||||
if (_startEdgeLabel == null)
|
||||
{
|
||||
_startEdgeLabel = new TextBlock { Style = (Style)Application.Current.Resources["EdgeLabelStyle"] };
|
||||
HeaderCanvas.Children.Add(_startEdgeLabel);
|
||||
Canvas.SetZIndex(_startEdgeLabel, 25);
|
||||
}
|
||||
|
||||
if (_endEdgeLabel == null)
|
||||
{
|
||||
_endEdgeLabel = new TextBlock { Style = (Style)Application.Current.Resources["EdgeLabelStyle"] };
|
||||
HeaderCanvas.Children.Add(_endEdgeLabel);
|
||||
Canvas.SetZIndex(_endEdgeLabel, 25);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateStartEndEdgeLabelsLayout()
|
||||
{
|
||||
double w = TimelineCanvas.ActualWidth;
|
||||
if (w <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_startEdgeLabel.Text = TimeSpanHelper.Convert(StartTime);
|
||||
_endEdgeLabel.Text = TimeSpanHelper.Convert(EndTime);
|
||||
|
||||
PlaceTopLabelAtTime(_startEdgeLabel, StartTime, w);
|
||||
PlaceTopLabelAtTime(_endEdgeLabel, EndTime, w);
|
||||
}
|
||||
|
||||
private void PlaceTopLabelAtTime(TextBlock tb, TimeSpan t, double timelineWidth)
|
||||
{
|
||||
double x = Math.Round((t.TotalHours / 24.0) * timelineWidth);
|
||||
double textW = MeasureTextWidth(tb);
|
||||
double desiredLeft = x - (textW / 2.0);
|
||||
|
||||
Canvas.SetLeft(tb, Clamp(desiredLeft, 0, timelineWidth - textW));
|
||||
Canvas.SetTop(tb, 0);
|
||||
tb.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
// ===== Sunrise/Sunset ticks on chart =====
|
||||
private void EnsureSunriseSunsetTicks()
|
||||
{
|
||||
if (_sunriseTick == null)
|
||||
{
|
||||
_sunriseTick = new Line { Style = (Style)Application.Current.Resources["SunRiseMarkerTickStyle"] };
|
||||
TimelineCanvas.Children.Add(_sunriseTick);
|
||||
Canvas.SetZIndex(_sunriseTick, 12);
|
||||
}
|
||||
|
||||
if (_sunsetTick == null)
|
||||
{
|
||||
_sunsetTick = new Line { Style = (Style)Application.Current.Resources["SunSetMarkerTickStyle"] };
|
||||
TimelineCanvas.Children.Add(_sunsetTick);
|
||||
Canvas.SetZIndex(_sunsetTick, 12);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateSunriseSunsetTicksLayout()
|
||||
{
|
||||
double w = TimelineCanvas.ActualWidth;
|
||||
double h = TimelineCanvas.ActualHeight + 24;
|
||||
if (w <= 0 || h <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
void Place(Line tick, TimeSpan t)
|
||||
{
|
||||
double x = Math.Round((t.TotalHours / 24.0) * w);
|
||||
tick.X1 = x;
|
||||
tick.X2 = x;
|
||||
tick.Y1 = 0;
|
||||
tick.Y2 = h;
|
||||
}
|
||||
|
||||
if (_sunriseTick != null)
|
||||
{
|
||||
if (Sunrise.HasValue)
|
||||
{
|
||||
Place(_sunriseTick, Sunrise.Value);
|
||||
_sunriseTick.Visibility = Visibility.Visible;
|
||||
}
|
||||
else
|
||||
{
|
||||
_sunriseTick.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
if (_sunsetTick != null)
|
||||
{
|
||||
if (Sunset.HasValue)
|
||||
{
|
||||
Place(_sunsetTick, Sunset.Value);
|
||||
_sunsetTick.Visibility = Visibility.Visible;
|
||||
}
|
||||
else
|
||||
{
|
||||
_sunsetTick.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Sunrise/Sunset panels (below chart) =====
|
||||
private void EnsureSunPanels()
|
||||
{
|
||||
if (_sunrisePanel == null)
|
||||
{
|
||||
_sunrisePanel = MakeSunPanel("\uEC8A");
|
||||
AnnotationCanvas.Children.Add(_sunrisePanel);
|
||||
}
|
||||
|
||||
if (_sunsetPanel == null)
|
||||
{
|
||||
_sunsetPanel = MakeSunPanel("\uED3A");
|
||||
AnnotationCanvas.Children.Add(_sunsetPanel);
|
||||
}
|
||||
}
|
||||
|
||||
private StackPanel MakeSunPanel(string iconEmoji)
|
||||
{
|
||||
var icon = new FontIcon { Glyph = iconEmoji, Style = (Style)Application.Current.Resources["SunIconStyle"] };
|
||||
var sp = new StackPanel { Orientation = Orientation.Vertical, Spacing = 2 };
|
||||
sp.Children.Add(icon);
|
||||
return sp;
|
||||
}
|
||||
|
||||
private void UpdateSunPanelsLayout()
|
||||
{
|
||||
double timelineW = TimelineCanvas.ActualWidth;
|
||||
double annotationW = AnnotationCanvas.ActualWidth;
|
||||
if (annotationW <= 0)
|
||||
{
|
||||
annotationW = timelineW;
|
||||
}
|
||||
|
||||
if (timelineW <= 0 || annotationW <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
void Place(StackPanel sp, TimeSpan t)
|
||||
{
|
||||
double panelW = MeasureElementWidth(sp);
|
||||
double xTimeline = Math.Round((t.TotalHours / 24.0) * timelineW);
|
||||
double left = Clamp(xTimeline - (panelW / 2.0), 0, annotationW - panelW);
|
||||
Canvas.SetLeft(sp, left);
|
||||
Canvas.SetTop(sp, 8);
|
||||
}
|
||||
|
||||
if (_sunrisePanel != null)
|
||||
{
|
||||
if (Sunrise.HasValue)
|
||||
{
|
||||
ToolTipService.SetToolTip(_sunrisePanel, $"Sunrise: {TimeSpanHelper.Convert(Sunrise.Value)}");
|
||||
_sunrisePanel.Visibility = Visibility.Visible;
|
||||
Place(_sunrisePanel, Sunrise.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
ToolTipService.SetToolTip(_sunrisePanel, null);
|
||||
_sunrisePanel.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
if (_sunsetPanel != null)
|
||||
{
|
||||
if (Sunset.HasValue)
|
||||
{
|
||||
ToolTipService.SetToolTip(_sunsetPanel, $"Sunset: {TimeSpanHelper.Convert(Sunset.Value)}");
|
||||
_sunsetPanel.Visibility = Visibility.Visible;
|
||||
Place(_sunsetPanel, Sunset.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
ToolTipService.SetToolTip(_sunsetPanel, null);
|
||||
_sunsetPanel.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Major labels BELOW chart (00,06,12,18,24) =====
|
||||
private void EnsureMajorTickLabels()
|
||||
{
|
||||
if (_majorTickBottomLabels.Count > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Includes 24:00 at end
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
var tb = new TextBlock { Style = (Style)Application.Current.Resources["MajorTickLabelStyle"] };
|
||||
Canvas.SetZIndex(tb, 5); // on annotation canvas
|
||||
_majorTickBottomLabels.Add(tb);
|
||||
AnnotationCanvas.Children.Add(tb);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateMajorTickLabelsLayout()
|
||||
{
|
||||
double timelineW = TimelineCanvas.ActualWidth;
|
||||
double annotationW = AnnotationCanvas.ActualWidth;
|
||||
if (annotationW <= 0)
|
||||
{
|
||||
annotationW = timelineW;
|
||||
}
|
||||
|
||||
if (timelineW <= 0 || annotationW <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int[] hours = { 0, 6, 12, 18, 24 };
|
||||
|
||||
// 1) Place labels first
|
||||
for (int i = 0; i < hours.Length; i++)
|
||||
{
|
||||
var tb = _majorTickBottomLabels[i];
|
||||
var t = TimeSpan.FromHours(hours[i]);
|
||||
tb.Text = TimeSpanHelper.Convert(t);
|
||||
|
||||
double xTimeline = Math.Round((t.TotalHours / 24.0) * timelineW);
|
||||
double textW = MeasureTextWidth(tb);
|
||||
double left = xTimeline - (textW / 2.0);
|
||||
|
||||
// Middle ones (06, 12) exact center; edges clamp inside canvas
|
||||
if (i == 1 || i == 2)
|
||||
{
|
||||
Canvas.SetLeft(tb, left);
|
||||
}
|
||||
else
|
||||
{
|
||||
Canvas.SetLeft(tb, Clamp(left, 0, annotationW - textW));
|
||||
}
|
||||
|
||||
Canvas.SetTop(tb, 8); // your existing baseline below chart
|
||||
tb.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
// 2) Compute sunrise/sunset occupied horizontal ranges (if present)
|
||||
(double Left, double Right)? sunriseBounds = null;
|
||||
(double Left, double Right)? sunsetBounds = null;
|
||||
|
||||
if (Sunrise.HasValue && _sunrisePanel != null)
|
||||
{
|
||||
sunriseBounds = GetAnnotationBoundsForTime(Sunrise.Value, timelineW, annotationW, _sunrisePanel);
|
||||
}
|
||||
|
||||
if (Sunset.HasValue && _sunsetPanel != null)
|
||||
{
|
||||
sunsetBounds = GetAnnotationBoundsForTime(Sunset.Value, timelineW, annotationW, _sunsetPanel);
|
||||
}
|
||||
|
||||
// 3) Hide any label that intersects the sunrise/sunset panel bounds
|
||||
for (int i = 0; i < hours.Length; i++)
|
||||
{
|
||||
var tb = _majorTickBottomLabels[i];
|
||||
if (tb.Visibility != Visibility.Visible)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var lbl = GetLabelBounds(tb);
|
||||
|
||||
bool hide =
|
||||
(sunriseBounds.HasValue && Intersects(lbl, sunriseBounds.Value)) ||
|
||||
(sunsetBounds.HasValue && Intersects(lbl, sunsetBounds.Value)); // include sunset too; remove if you only want sunrise
|
||||
|
||||
tb.Visibility = hide ? Visibility.Collapsed : Visibility.Visible;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Utilities =====
|
||||
private static double Clamp(double v, double min, double max) => Math.Max(min, Math.Min(max, v));
|
||||
|
||||
private static double MeasureElementWidth(FrameworkElement el)
|
||||
{
|
||||
el.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
|
||||
return el.DesiredSize.Width;
|
||||
}
|
||||
|
||||
private static double MeasureTextWidth(TextBlock tb)
|
||||
{
|
||||
tb.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
|
||||
return tb.DesiredSize.Width;
|
||||
}
|
||||
|
||||
private static bool Intersects((double Left, double Right) a, (double Left, double Right) b, double pad = 4)
|
||||
{
|
||||
// Horizontal overlap with padding
|
||||
return !(a.Right + pad <= b.Left || b.Right + pad <= a.Left);
|
||||
}
|
||||
|
||||
private (double Left, double Right) GetAnnotationBoundsForTime(TimeSpan t, double timelineW, double annotationW, FrameworkElement element)
|
||||
{
|
||||
// Compute the *actual* left/right the panel will occupy in AnnotationCanvas
|
||||
double panelW = MeasureElementWidth(element);
|
||||
double xTimeline = Math.Round((t.TotalHours / 24.0) * timelineW);
|
||||
double left = Clamp(xTimeline - (panelW / 2.0), 0, annotationW - panelW);
|
||||
return (left, left + panelW);
|
||||
}
|
||||
|
||||
private (double Left, double Right) GetLabelBounds(TextBlock tb)
|
||||
{
|
||||
double w = MeasureTextWidth(tb);
|
||||
double left = Canvas.GetLeft(tb);
|
||||
return (left, left + w);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<Style x:Key="HourTickStyle" TargetType="Line">
|
||||
<Setter Property="Stroke" Value="{ThemeResource TextFillColorTertiaryBrush}" />
|
||||
<Setter Property="StrokeThickness" Value="1" />
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="MajorHourTickStyle"
|
||||
BasedOn="{StaticResource HourTickStyle}"
|
||||
TargetType="Line">
|
||||
<Setter Property="StrokeThickness" Value="2" />
|
||||
<Setter Property="Stroke" Value="{ThemeResource TextFillColorTertiaryBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style x:Key="SunRiseMarkerTickStyle" TargetType="Line">
|
||||
<Setter Property="Stroke" Value="{ThemeResource TextFillColorTertiaryBrush}" />
|
||||
<Setter Property="StrokeThickness" Value="1" />
|
||||
<Setter Property="StrokeDashArray" Value="2,2" />
|
||||
</Style>
|
||||
|
||||
|
||||
<Style x:Key="SunSetMarkerTickStyle" TargetType="Line">
|
||||
<Setter Property="Stroke" Value="{ThemeResource TextFillColorTertiaryBrush}" />
|
||||
<Setter Property="StrokeThickness" Value="1" />
|
||||
<Setter Property="StrokeDashArray" Value="2,2" />
|
||||
</Style>
|
||||
|
||||
<!-- ===== Text / Labels ===== -->
|
||||
<Style x:Key="EdgeLabelStyle" TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Setter Property="Foreground" Value="{ThemeResource AccentTextFillColorPrimaryBrush}" />
|
||||
<Setter Property="IsHitTestVisible" Value="False" />
|
||||
</Style>
|
||||
|
||||
<!-- Below-chart labels for 00/06/12/18/24 -->
|
||||
<Style x:Key="MajorTickLabelStyle" TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="Foreground" Value="{ThemeResource TextFillColorTertiaryBrush}" />
|
||||
<Setter Property="IsHitTestVisible" Value="False" />
|
||||
</Style>
|
||||
|
||||
<!-- Sunrise/Sunset panel styles -->
|
||||
<Style x:Key="SunIconStyle" TargetType="FontIcon">
|
||||
<Setter Property="FontSize" Value="18" />
|
||||
<Setter Property="Foreground" Value="{ThemeResource TextFillColorTertiaryBrush}" />
|
||||
<Setter Property="HorizontalAlignment" Value="Center" />
|
||||
</Style>
|
||||
|
||||
<!-- ===== Bands ===== -->
|
||||
<Style x:Key="DarkBandStyle" TargetType="Border">
|
||||
<!--<Setter Property="Background">
|
||||
<Setter.Value>
|
||||
<SolidColorBrush Opacity="0.6" Color="{ThemeResource SystemAccentColorDark1}"/>
|
||||
</Setter.Value>
|
||||
</Setter>-->
|
||||
<Setter Property="Background" Value="{ThemeResource AccentFillColorTertiaryBrush}" />
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
<Setter Property="Padding" Value="4" />
|
||||
<Setter Property="ToolTipService.ToolTip" Value="Dark mode" />
|
||||
</Style>
|
||||
|
||||
<Style x:Key="LightBandStyle" TargetType="Border">
|
||||
<Setter Property="Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}" />
|
||||
<Setter Property="ToolTipService.ToolTip" Value="Light mode" />
|
||||
</Style>
|
||||
|
||||
<Style x:Key="DarkBandIconStyle" TargetType="FontIcon">
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="Glyph" Value="" />
|
||||
<Setter Property="Foreground" Value="{ThemeResource TextOnAccentFillColorPrimaryBrush}" />
|
||||
<Setter Property="HorizontalAlignment" Value="Center" />
|
||||
</Style>
|
||||
<Style
|
||||
x:Key="LightBandIconStyle"
|
||||
BasedOn="{StaticResource DarkBandIconStyle}"
|
||||
TargetType="FontIcon">
|
||||
<Setter Property="Glyph" Value="" />
|
||||
<Setter Property="Foreground" Value="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||
</Style>
|
||||
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Page
|
||||
x:Class="Microsoft.PowerToys.Settings.UI.OOBE.Views.OobeLightSwitch"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<controls:OOBEPageControl x:Uid="Oobe_LightSwitch" HeroImage="ms-appx:///Assets/Settings/Modules/OOBE/LightSwitch.png">
|
||||
<controls:OOBEPageControl.PageContent>
|
||||
<StackPanel Orientation="Vertical" Spacing="12">
|
||||
<TextBlock x:Uid="Oobe_HowToUse" Style="{ThemeResource OobeSubtitleStyle}" />
|
||||
|
||||
<tkcontrols:MarkdownTextBlock x:Uid="Oobe_LightSwitch_HowToUse" />
|
||||
|
||||
<TextBlock x:Uid="Oobe_TipsAndTricks" Style="{ThemeResource OobeSubtitleStyle}" />
|
||||
|
||||
<tkcontrols:MarkdownTextBlock x:Uid="Oobe_LightSwitch_TipsAndTricks" />
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button x:Uid="OOBE_Settings" Click="SettingsLaunchButton_Click" />
|
||||
|
||||
<HyperlinkButton NavigateUri="https://aka.ms/PowerToysOverview_LightSwitch" Style="{StaticResource TextButtonStyle}">
|
||||
<TextBlock x:Uid="LearnMore_LightSwitch" TextWrapping="Wrap" />
|
||||
</HyperlinkButton>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</controls:OOBEPageControl.PageContent>
|
||||
</controls:OOBEPageControl>
|
||||
</Page>
|
||||
@@ -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 Microsoft.PowerToys.Settings.UI.OOBE.Enums;
|
||||
using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel;
|
||||
using Microsoft.PowerToys.Settings.UI.Views;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
|
||||
{
|
||||
public sealed partial class OobeLightSwitch : Page
|
||||
{
|
||||
public OobePowerToysModule ViewModel { get; set; }
|
||||
|
||||
public OobeLightSwitch()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
ViewModel = new OobePowerToysModule(OobeShellPage.OobeShellHandler.Modules[(int)PowerToysModules.LightSwitch]);
|
||||
}
|
||||
|
||||
private void SettingsLaunchButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (OobeShellPage.OpenMainWindowCallback != null)
|
||||
{
|
||||
OobeShellPage.OpenMainWindowCallback(typeof(LightSwitchPage));
|
||||
}
|
||||
|
||||
ViewModel.LogOpeningSettingsEvent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -117,6 +117,10 @@
|
||||
x:Uid="Shell_KeyboardManager"
|
||||
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/KeyboardManager.png}"
|
||||
Tag="KBM" />
|
||||
<NavigationViewItem
|
||||
x:Uid="Shell_LightSwitch"
|
||||
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/LightSwitch.png}"
|
||||
Tag="LightSwitch" />
|
||||
<NavigationViewItem
|
||||
x:Uid="Shell_MouseUtilities"
|
||||
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/MouseUtils.png}"
|
||||
|
||||
@@ -138,6 +138,11 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
|
||||
ModuleName = "KBM",
|
||||
IsNew = false,
|
||||
});
|
||||
Modules.Insert((int)PowerToysModules.LightSwitch, new OobePowerToysModule()
|
||||
{
|
||||
ModuleName = "LightSwitch",
|
||||
IsNew = true,
|
||||
});
|
||||
Modules.Insert((int)PowerToysModules.MouseUtils, new OobePowerToysModule()
|
||||
{
|
||||
ModuleName = "MouseUtils",
|
||||
@@ -287,6 +292,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
|
||||
case "Run": NavigationFrame.Navigate(typeof(OobeRun)); break;
|
||||
case "ImageResizer": NavigationFrame.Navigate(typeof(OobeImageResizer)); break;
|
||||
case "KBM": NavigationFrame.Navigate(typeof(OobeKBM)); break;
|
||||
case "LightSwitch": NavigationFrame.Navigate(typeof(OobeLightSwitch)); break;
|
||||
case "PowerRename": NavigationFrame.Navigate(typeof(OobePowerRename)); break;
|
||||
case "QuickAccent": NavigationFrame.Navigate(typeof(OobePowerAccent)); break;
|
||||
case "FileExplorer": NavigationFrame.Navigate(typeof(OobeFileExplorer)); break;
|
||||
|
||||
@@ -0,0 +1,320 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Page
|
||||
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: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}"
|
||||
AutomationProperties.LandmarkType="Main"
|
||||
mc:Ignorable="d">
|
||||
<Page.Resources>
|
||||
<converters:EnumToVisibilityConverter x:Key="EnumToVisibilityConverter" />
|
||||
<converters:TimeSpanToFriendlyTimeConverter x:Key="TimeSpanToFriendlyTimeConverter" />
|
||||
</Page.Resources>
|
||||
<controls:SettingsPageControl
|
||||
x:Uid="LightSwitch"
|
||||
IsTabStop="False"
|
||||
ModuleImageSource="ms-appx:///Assets/Settings/Modules/LightSwitch.png">
|
||||
<controls:SettingsPageControl.ModuleContent>
|
||||
<StackPanel Orientation="Vertical">
|
||||
<controls:GPOInfoControl ShowWarning="{x:Bind ViewModel.IsEnabledGpoConfigured, Mode=OneWay}">
|
||||
<tkcontrols:SettingsCard
|
||||
x:Uid="LightSwitch_EnableSettingsCard"
|
||||
HeaderIcon="{ui:BitmapIcon Source=/Assets/Settings/Icons/LightSwitch.png}"
|
||||
IsEnabled="{x:Bind ViewModel.IsEnabledGpoConfigured, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}">
|
||||
<ToggleSwitch AutomationProperties.AutomationId="Toggle_LightSwitch" IsOn="{x:Bind ViewModel.IsEnabled, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
</controls:GPOInfoControl>
|
||||
|
||||
<controls:SettingsGroup x:Uid="LightSwitch_ShortcutsSettingsGroup" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
|
||||
<tkcontrols:SettingsCard x:Uid="LightSwitch_ThemeToggle_Shortcut" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<controls:ShortcutControl
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
AllowDisable="True"
|
||||
AutomationProperties.AutomationId="Shortcut_LightSwitch"
|
||||
HotkeySettings="{x:Bind Path=ViewModel.ToggleThemeActivationShortcut, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
</controls:SettingsGroup>
|
||||
|
||||
<controls:SettingsGroup x:Uid="LightSwitch_ScheduleSettingsGroup" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
|
||||
<tkcontrols:SettingsExpander
|
||||
x:Uid="LightSwitch_ModeSettingsExpander"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsExpanded="True">
|
||||
<ComboBox
|
||||
x:Name="ModeSelector"
|
||||
AutomationProperties.AutomationId="ModeSelection_LightSwitch"
|
||||
SelectedValue="{x:Bind ViewModel.ScheduleMode, Mode=TwoWay}"
|
||||
SelectedValuePath="Tag"
|
||||
SelectionChanged="ModeSelector_SelectionChanged">
|
||||
<ComboBoxItem
|
||||
x:Uid="LightSwitch_ModeManual"
|
||||
AutomationProperties.AutomationId="ManualCBItem_LightSwitch"
|
||||
Tag="FixedHours" />
|
||||
<ComboBoxItem
|
||||
x:Uid="LightSwitch_ModeSunsetToSunrise"
|
||||
AutomationProperties.AutomationId="SunCBItem_LightSwitch"
|
||||
Tag="SunsetToSunrise" />
|
||||
</ComboBox>
|
||||
<tkcontrols:SettingsExpander.Items>
|
||||
<tkcontrols:SettingsCard x:Uid="LightSwitch_TurnOnDarkMode" Visibility="{x:Bind ViewModel.ScheduleMode, Mode=OneWay, Converter={StaticResource EnumToVisibilityConverter}, ConverterParameter=FixedHours}">
|
||||
<TimePicker AutomationProperties.AutomationId="DarkTimePicker" Time="{x:Bind ViewModel.DarkTimePickerValue, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard x:Uid="LightSwitch_TurnOffDarkMode" Visibility="{x:Bind ViewModel.ScheduleMode, Mode=OneWay, Converter={StaticResource EnumToVisibilityConverter}, ConverterParameter=FixedHours}">
|
||||
<TimePicker AutomationProperties.AutomationId="LightTimePicker" Time="{x:Bind ViewModel.LightTimePickerValue, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
|
||||
<tkcontrols:SettingsCard x:Uid="LightSwitch_LocationSettingsCard" Visibility="{x:Bind ViewModel.ScheduleMode, Mode=OneWay, Converter={StaticResource EnumToVisibilityConverter}, ConverterParameter=SunsetToSunrise}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="{x:Bind ViewModel.SyncButtonInformation, Mode=OneWay}" />
|
||||
<Button
|
||||
Padding="8"
|
||||
AutomationProperties.AutomationId="SetLocationButton_LightSwitch"
|
||||
Click="SyncLocationButton_Click"
|
||||
Content="{ui:FontIcon Glyph=,
|
||||
FontSize=16}" />
|
||||
</StackPanel>
|
||||
</tkcontrols:SettingsCard>
|
||||
|
||||
<tkcontrols:SettingsCard x:Uid="LightSwitch_OffsetSettingsCard" Visibility="{x:Bind ViewModel.ScheduleMode, Mode=OneWay, Converter={StaticResource EnumToVisibilityConverter}, ConverterParameter=SunsetToSunrise}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="20">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<!--<FontIcon Glyph="" FontSize="16" />-->
|
||||
<controls:IsEnabledTextBlock x:Uid="LightSwitch_SunriseText" VerticalAlignment="Center" />
|
||||
<NumberBox
|
||||
AutomationProperties.AutomationId="SunriseOffset_LightSwitch"
|
||||
Maximum="60"
|
||||
Minimum="-60"
|
||||
SpinButtonPlacementMode="Compact"
|
||||
Value="{x:Bind ViewModel.SunriseOffset, Mode=TwoWay}" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<controls:IsEnabledTextBlock x:Uid="LightSwitch_SunsetText" VerticalAlignment="Center" />
|
||||
<NumberBox
|
||||
AutomationProperties.AutomationId="SunsetOffset_LightSwitch"
|
||||
Maximum="60"
|
||||
Minimum="-60"
|
||||
SpinButtonPlacementMode="Compact"
|
||||
Value="{x:Bind ViewModel.SunsetOffset, Mode=TwoWay}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard
|
||||
x:Name="TimelineCard"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
ContentAlignment="Vertical">
|
||||
<controls:Timeline
|
||||
Margin="0,24,0,24"
|
||||
AutomationProperties.AutomationId="Timeline_LightSwitch"
|
||||
EndTime="{x:Bind ViewModel.DarkTimeTimeSpan, Mode=OneWay}"
|
||||
StartTime="{x:Bind ViewModel.LightTimeTimeSpan, Mode=OneWay}"
|
||||
Sunrise="{x:Bind ViewModel.SunriseTimeSpan, Mode=OneWay}"
|
||||
Sunset="{x:Bind ViewModel.SunsetTimeSpan, Mode=OneWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
</tkcontrols:SettingsExpander.Items>
|
||||
</tkcontrols:SettingsExpander>
|
||||
<InfoBar
|
||||
x:Name="LocationWarningBar"
|
||||
x:Uid="LightSwitch_LocationWarningBar"
|
||||
IsOpen="True"
|
||||
Severity="Informational"
|
||||
Visibility="Collapsed" />
|
||||
<controls:SettingsGroup x:Uid="LightSwitch_BehaviorSettingsGroup" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
|
||||
<tkcontrols:SettingsExpander
|
||||
x:Uid="LightSwitch_ApplyDarkModeExpander"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsExpanded="True">
|
||||
<tkcontrols:SettingsExpander.Items>
|
||||
<tkcontrols:SettingsCard HorizontalContentAlignment="Stretch" ContentAlignment="Left">
|
||||
<controls:CheckBoxWithDescriptionControl
|
||||
x:Uid="LightSwitch_SystemCheckbox"
|
||||
AutomationProperties.AutomationId="ChangeSystemCheckbox_LightSwitch"
|
||||
IsChecked="{x:Bind ViewModel.ChangeSystem, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard HorizontalContentAlignment="Stretch" ContentAlignment="Left">
|
||||
<controls:CheckBoxWithDescriptionControl
|
||||
x:Uid="LightSwitch_AppsCheckbox"
|
||||
AutomationProperties.AutomationId="ChangeAppsCheckbox_LightSwitch"
|
||||
IsChecked="{x:Bind ViewModel.ChangeApps, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
</tkcontrols:SettingsExpander.Items>
|
||||
</tkcontrols:SettingsExpander>
|
||||
</controls:SettingsGroup>
|
||||
<!-- Force mode buttons -->
|
||||
<!--<tkcontrols:SettingsCard
|
||||
Header="Force mode now"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
Description="Apply light or dark mode immediately">
|
||||
<StackPanel Orientation="Horizontal" Spacing="12">
|
||||
<Button
|
||||
Content="Force Light"
|
||||
Command="{x:Bind ViewModel.ForceLightCommand}" />
|
||||
<Button
|
||||
Content="Force Dark"
|
||||
Command="{x:Bind ViewModel.ForceDarkCommand}" />
|
||||
</StackPanel>
|
||||
</tkcontrols:SettingsCard>-->
|
||||
|
||||
<ContentDialog
|
||||
x:Name="LocationDialog"
|
||||
x:Uid="LightSwitch_LocationDialog"
|
||||
IsPrimaryButtonEnabled="True"
|
||||
IsSecondaryButtonEnabled="True"
|
||||
Opened="LocationDialog_Opened"
|
||||
PrimaryButtonClick="LocationDialog_PrimaryButtonClick"
|
||||
PrimaryButtonStyle="{StaticResource AccentButtonStyle}">
|
||||
<Grid RowSpacing="48">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock x:Uid="LightSwitch_LocationDialog_Description" Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||
<!--<AutoSuggestBox
|
||||
x:Name="CityAutoSuggestBox"
|
||||
Grid.Row="1"
|
||||
Margin="0,16,0,8"
|
||||
AutomationProperties.AutomationId="CitySearchBox_LightSwitch"
|
||||
ItemsSource="{x:Bind ViewModel.SearchLocations, Mode=OneWay}"
|
||||
PlaceholderText="Search for a city near you.."
|
||||
QueryIcon="Find"
|
||||
SuggestionChosen="CityAutoSuggestBox_SuggestionChosen"
|
||||
TextChanged="CityAutoSuggestBox_TextChanged">
|
||||
<AutoSuggestBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="helpers:SearchLocation">
|
||||
<Grid Padding="12,8,0,8">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Text="{x:Bind City}" />
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind Country}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</AutoSuggestBox.ItemTemplate>
|
||||
</AutoSuggestBox>-->
|
||||
<StackPanel
|
||||
Grid.Row="2"
|
||||
Margin="0,24,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
Orientation="Vertical"
|
||||
Spacing="32">
|
||||
<Button
|
||||
x:Name="SyncButton"
|
||||
HorizontalAlignment="Stretch"
|
||||
AutomationProperties.AutomationId="SyncLocationButton_LightSwitch"
|
||||
Style="{StaticResource AccentButtonStyle}"
|
||||
Visibility="Collapsed">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="14" Glyph="" />
|
||||
<TextBlock x:Uid="LightSwitch_GetCurrentLocation" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
<ProgressRing
|
||||
x:Name="SyncLoader"
|
||||
Grid.Row="1"
|
||||
Width="40"
|
||||
Height="40"
|
||||
VerticalAlignment="Center"
|
||||
IsActive="False"
|
||||
Visibility="Collapsed" />
|
||||
|
||||
<Grid
|
||||
x:Name="LocationResultPanel"
|
||||
Grid.Row="1"
|
||||
VerticalAlignment="Bottom"
|
||||
ColumnSpacing="16"
|
||||
RowSpacing="12"
|
||||
Visibility="Collapsed">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<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="">
|
||||
<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" />
|
||||
<FontIcon
|
||||
Grid.Column="2"
|
||||
FontSize="20"
|
||||
Glyph="">
|
||||
<ToolTipService.ToolTip>
|
||||
<TextBlock x:Uid="LightSwitch_SunsetTooltip" />
|
||||
</ToolTipService.ToolTip>
|
||||
</FontIcon>
|
||||
<TextBlock
|
||||
Grid.Row="2"
|
||||
Grid.Column="2"
|
||||
AutomationProperties.AutomationId="SunsetText_LightSwitch"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="{x:Bind ViewModel.DarkTimeTimeSpan, Converter={StaticResource TimeSpanToFriendlyTimeConverter}, Mode=OneWay}"
|
||||
TextAlignment="Center" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ContentDialog>
|
||||
</controls:SettingsGroup>
|
||||
|
||||
</StackPanel>
|
||||
</controls:SettingsPageControl.ModuleContent>
|
||||
<controls:SettingsPageControl.PrimaryLinks>
|
||||
<controls:PageLink x:Uid="LearnMore_LightSwitch" Link="https://aka.ms/PowerToysOverview_LightSwitch" />
|
||||
</controls:SettingsPageControl.PrimaryLinks>
|
||||
</controls:SettingsPageControl>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="LocationEnabledStates">
|
||||
<VisualState x:Name="LocationSet" />
|
||||
<VisualState x:Name="LocationNotSet">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="TimelineCard.Visibility" Value="Collapsed" />
|
||||
<Setter Target="LocationWarningBar.Visibility" Value="Visible" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Page>
|
||||
@@ -0,0 +1,324 @@
|
||||
// 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 System.IO;
|
||||
using System.IO.Abstractions;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
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
|
||||
{
|
||||
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 IFileSystem _fileSystem;
|
||||
private readonly IFileSystemWatcher _fileSystemWatcher;
|
||||
private readonly DispatcherQueue _dispatcherQueue;
|
||||
|
||||
private LightSwitchViewModel ViewModel { get; set; }
|
||||
|
||||
public LightSwitchPage()
|
||||
{
|
||||
_settingsUtils = new SettingsUtils();
|
||||
_sendConfigMsg = ShellPage.SendDefaultIPCMessage;
|
||||
|
||||
_generalSettingsRepository = SettingsRepository<GeneralSettings>.GetInstance(_settingsUtils);
|
||||
_moduleSettingsRepository = SettingsRepository<LightSwitchSettings>.GetInstance(_settingsUtils);
|
||||
|
||||
// Get settings from JSON (or defaults if JSON missing)
|
||||
var darkSettings = _moduleSettingsRepository.SettingsConfig;
|
||||
|
||||
// Pass them into the ViewModel
|
||||
ViewModel = new LightSwitchViewModel(darkSettings, ShellPage.SendDefaultIPCMessage);
|
||||
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
|
||||
|
||||
LoadSettings(_generalSettingsRepository, _moduleSettingsRepository);
|
||||
|
||||
DataContext = ViewModel;
|
||||
|
||||
var settingsPath = _settingsUtils.GetSettingsFilePath(_appName);
|
||||
|
||||
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
_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.InitializeComponent();
|
||||
this.Loaded += LightSwitchPage_Loaded;
|
||||
}
|
||||
|
||||
private void LightSwitchPage_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (ViewModel.SearchLocations.Count == 0)
|
||||
{
|
||||
foreach (var city in SearchLocationLoader.GetAll())
|
||||
{
|
||||
ViewModel.SearchLocations.Add(city);
|
||||
}
|
||||
}
|
||||
|
||||
ViewModel.InitializeScheduleMode();
|
||||
}
|
||||
|
||||
private async Task GetGeoLocation()
|
||||
{
|
||||
SyncButton.IsEnabled = false;
|
||||
SyncLoader.IsActive = true;
|
||||
SyncLoader.Visibility = Visibility.Visible;
|
||||
|
||||
try
|
||||
{
|
||||
// Request access
|
||||
var accessStatus = await Geolocator.RequestAccessAsync();
|
||||
if (accessStatus != GeolocationAccessStatus.Allowed)
|
||||
{
|
||||
// User denied location or it's not available
|
||||
return;
|
||||
}
|
||||
|
||||
var geolocator = new Geolocator { DesiredAccuracy = PositionAccuracy.Default };
|
||||
|
||||
Geoposition pos = await geolocator.GetGeopositionAsync();
|
||||
|
||||
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.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);
|
||||
|
||||
// Since we use this mode, we can remove the selected city data.
|
||||
ViewModel.SelectedCity = null;
|
||||
|
||||
// CityAutoSuggestBox.Text = string.Empty;
|
||||
ViewModel.SyncButtonInformation = $"{ViewModel.Latitude}<7D>, {ViewModel.Longitude}<7D>";
|
||||
|
||||
// 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;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
SyncButton.IsEnabled = true;
|
||||
SyncLoader.IsActive = false;
|
||||
System.Diagnostics.Debug.WriteLine("Location error: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private void LocationDialog_PrimaryButtonClick(object sender, ContentDialogButtonClickEventArgs args)
|
||||
{
|
||||
if (ViewModel.ScheduleMode == "SunriseToSunsetUser")
|
||||
{
|
||||
ViewModel.SyncButtonInformation = ViewModel.SelectedCity.City;
|
||||
}
|
||||
else if (ViewModel.ScheduleMode == "SunriseToSunsetGeo")
|
||||
{
|
||||
ViewModel.SyncButtonInformation = $"{ViewModel.Latitude}<7D>, {ViewModel.Longitude}<7D>";
|
||||
}
|
||||
|
||||
SunriseModeChartState();
|
||||
}
|
||||
|
||||
private void ViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == "IsEnabled")
|
||||
{
|
||||
if (ViewModel.IsEnabled != _generalSettingsRepository.SettingsConfig.Enabled.LightSwitch)
|
||||
{
|
||||
_generalSettingsRepository.SettingsConfig.Enabled.LightSwitch = ViewModel.IsEnabled;
|
||||
|
||||
var generalSettingsMessage = new OutGoingGeneralSettings(_generalSettingsRepository.SettingsConfig).ToString();
|
||||
Logger.LogInfo($"Saved general settings from Light Switch page.");
|
||||
|
||||
_sendConfigMsg?.Invoke(generalSettingsMessage);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ViewModel.ModuleSettings != null)
|
||||
{
|
||||
SndLightSwitchSettings currentSettings = new(_moduleSettingsRepository.SettingsConfig);
|
||||
SndModuleSettings<SndLightSwitchSettings> csIpcMessage = new(currentSettings);
|
||||
|
||||
SndLightSwitchSettings outSettings = new(ViewModel.ModuleSettings);
|
||||
SndModuleSettings<SndLightSwitchSettings> outIpcMessage = new(outSettings);
|
||||
|
||||
string csMessage = csIpcMessage.ToJsonString();
|
||||
string outMessage = outIpcMessage.ToJsonString();
|
||||
|
||||
if (!csMessage.Equals(outMessage, StringComparison.Ordinal))
|
||||
{
|
||||
Logger.LogInfo($"Saved Light Switch settings from Light Switch page.");
|
||||
|
||||
_sendConfigMsg?.Invoke(outMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadSettings(ISettingsRepository<GeneralSettings> generalSettingsRepository, ISettingsRepository<LightSwitchSettings> moduleSettingsRepository)
|
||||
{
|
||||
if (generalSettingsRepository != null)
|
||||
{
|
||||
if (moduleSettingsRepository != null)
|
||||
{
|
||||
UpdateViewModelSettings(moduleSettingsRepository.SettingsConfig, generalSettingsRepository.SettingsConfig);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentNullException(nameof(moduleSettingsRepository));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentNullException(nameof(generalSettingsRepository));
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateViewModelSettings(LightSwitchSettings lightSwitchSettings, GeneralSettings generalSettings)
|
||||
{
|
||||
if (lightSwitchSettings != null)
|
||||
{
|
||||
if (generalSettings != null)
|
||||
{
|
||||
ViewModel.IsEnabled = generalSettings.Enabled.LightSwitch;
|
||||
ViewModel.ModuleSettings = (LightSwitchSettings)lightSwitchSettings.Clone();
|
||||
|
||||
UpdateEnabledState(generalSettings.Enabled.LightSwitch);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentNullException(nameof(generalSettings));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentNullException(nameof(lightSwitchSettings));
|
||||
}
|
||||
}
|
||||
|
||||
private void Settings_Changed(object sender, FileSystemEventArgs e)
|
||||
{
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
_moduleSettingsRepository.ReloadSettings();
|
||||
LoadSettings(_generalSettingsRepository, _moduleSettingsRepository);
|
||||
});
|
||||
}
|
||||
|
||||
private void UpdateEnabledState(bool recommendedState)
|
||||
{
|
||||
var enabledGpoRuleConfiguration = GPOWrapper.GetConfiguredLightSwitchEnabledValue();
|
||||
|
||||
if (enabledGpoRuleConfiguration == GpoRuleConfigured.Disabled || enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled)
|
||||
{
|
||||
// Get the enabled state from GPO.
|
||||
ViewModel.IsEnabledGpoConfigured = true;
|
||||
ViewModel.EnabledGPOConfiguration = enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled;
|
||||
}
|
||||
else
|
||||
{
|
||||
ViewModel.IsEnabled = recommendedState;
|
||||
}
|
||||
}
|
||||
|
||||
private async void SyncLocationButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
LocationDialog.IsPrimaryButtonEnabled = false;
|
||||
LocationResultPanel.Visibility = Visibility.Collapsed;
|
||||
await LocationDialog.ShowAsync();
|
||||
}
|
||||
|
||||
private void CityAutoSuggestBox_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
|
||||
{
|
||||
if (args.Reason == AutoSuggestionBoxTextChangeReason.UserInput && !string.IsNullOrWhiteSpace(sender.Text))
|
||||
{
|
||||
string query = sender.Text.ToLower(CultureInfo.CurrentCulture);
|
||||
|
||||
// Filter your cities (assuming ViewModel.Cities is a List<City>)
|
||||
var filtered = ViewModel.SearchLocations
|
||||
.Where(c =>
|
||||
(c.City?.Contains(query, StringComparison.CurrentCultureIgnoreCase) ?? false) ||
|
||||
(c.Country?.Contains(query, StringComparison.CurrentCultureIgnoreCase) ?? false))
|
||||
.ToList();
|
||||
|
||||
sender.ItemsSource = filtered;
|
||||
}
|
||||
}
|
||||
|
||||
private void CityAutoSuggestBox_SuggestionChosen(AutoSuggestBox sender, AutoSuggestBoxSuggestionChosenEventArgs args)
|
||||
{
|
||||
if (args.SelectedItem is SearchLocation location)
|
||||
{
|
||||
ViewModel.SelectedCity = location;
|
||||
|
||||
// CityAutoSuggestBox.Text = $"{location.City}, {location.Country}";
|
||||
LocationDialog.IsPrimaryButtonEnabled = true;
|
||||
LocationResultPanel.Visibility = Visibility.Visible;
|
||||
}
|
||||
}
|
||||
|
||||
private void ModeSelector_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
SunriseModeChartState();
|
||||
}
|
||||
|
||||
private void SunriseModeChartState()
|
||||
{
|
||||
if (ViewModel.Latitude == "0.0" && ViewModel.Longitude == "0.0" && ViewModel.ScheduleMode == "SunsetToSunrise")
|
||||
{
|
||||
TimelineCard.Visibility = Visibility.Collapsed;
|
||||
LocationWarningBar.Visibility = Visibility.Visible;
|
||||
}
|
||||
else
|
||||
{
|
||||
TimelineCard.Visibility = Visibility.Visible;
|
||||
LocationWarningBar.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
private async void LocationDialog_Opened(ContentDialog sender, ContentDialogOpenedEventArgs args)
|
||||
{
|
||||
await GetGeoLocation();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,6 @@
|
||||
Command="{x:Bind ViewModel.LaunchEventHandler}"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsClickEnabled="True" />
|
||||
|
||||
<tkcontrols:SettingsCard
|
||||
Name="RegistryPreviewDefaultRegApp"
|
||||
x:Uid="RegistryPreview_DefaultRegApp"
|
||||
|
||||
@@ -202,6 +202,12 @@
|
||||
helpers:NavHelper.NavigateTo="views:ColorPickerPage"
|
||||
AutomationProperties.AutomationId="ColorPickerNavItem"
|
||||
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/ColorPicker.png}" />
|
||||
<NavigationViewItem
|
||||
x:Name="LightSwitchNavigationItem"
|
||||
x:Uid="Shell_LightSwitch"
|
||||
helpers:NavHelper.NavigateTo="views:LightSwitchPage"
|
||||
AutomationProperties.AutomationId="LightSwitchNavItem"
|
||||
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/LightSwitch.png}" />
|
||||
<NavigationViewItem
|
||||
x:Name="PowerLauncherNavigationItem"
|
||||
x:Uid="Shell_PowerLauncher"
|
||||
|
||||
@@ -3161,19 +3161,19 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<data name="Peek_ConfirmFileDelete.Description" xml:space="preserve">
|
||||
<value>You'll be asked to confirm before files are moved to the Recycle Bin</value>
|
||||
</data>
|
||||
<data name="Peek_ActivationMethod.Header" xml:space="preserve">
|
||||
<data name="Peek_ActivationMethod.Header" xml:space="preserve">
|
||||
<value>Activation method</value>
|
||||
</data>
|
||||
<data name="Peek_ActivationMethod.Description" xml:space="preserve">
|
||||
<data name="Peek_ActivationMethod.Description" xml:space="preserve">
|
||||
<value>Use a shortcut or press the Spacebar when a file is selected</value>
|
||||
<comment>Spacebar is a physical keyboard key</comment>
|
||||
<comment>Spacebar is a physical keyboard key</comment>
|
||||
</data>
|
||||
<data name="Peek_ActivationMethod_CustomizedShortcut.Content" xml:space="preserve">
|
||||
<data name="Peek_ActivationMethod_CustomizedShortcut.Content" xml:space="preserve">
|
||||
<value>Custom shortcut</value>
|
||||
</data>
|
||||
<data name="Peek_ActivationMethod_SpaceBar.Content" xml:space="preserve">
|
||||
<data name="Peek_ActivationMethod_SpaceBar.Content" xml:space="preserve">
|
||||
<value>Spacebar</value>
|
||||
</data>
|
||||
</data>
|
||||
<data name="FancyZones_DisableRoundCornersOnWindowSnap.Content" xml:space="preserve">
|
||||
<value>Disable rounded corners when a window is snapped</value>
|
||||
</data>
|
||||
@@ -3261,6 +3261,15 @@ Activate by holding the key for the character you want to add an accent to, then
|
||||
<data name="AlwaysOnTop_ShortDescription" xml:space="preserve">
|
||||
<value>Pin a window</value>
|
||||
</data>
|
||||
<data name="LightSwitch_ThemeToggle_Shortcut.Header" xml:space="preserve">
|
||||
<value>Theme toggle shortcut</value>
|
||||
</data>
|
||||
<data name="LightSwitch_ThemeToggle_Shortcut.Description" xml:space="preserve">
|
||||
<value>Switch between light and dark mode</value>
|
||||
</data>
|
||||
<data name="LightSwitch_ForceDarkMode" xml:space="preserve">
|
||||
<value>Toggle theme</value>
|
||||
</data>
|
||||
<data name="ColorPicker_ShortDescription" xml:space="preserve">
|
||||
<value>Pick a color</value>
|
||||
</data>
|
||||
@@ -5224,6 +5233,117 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
|
||||
<data name="GeneralPage_EnableViewDiagnosticDataText.Text" xml:space="preserve">
|
||||
<value>Stores diagnostic data locally in .xml format; folder may include .etl files as well. May use up 1 GB or more of disk space.</value>
|
||||
</data>
|
||||
<data name="Shell_LightSwitch.Content" xml:space="preserve">
|
||||
<value>Light Switch</value>
|
||||
</data>
|
||||
<data name="LightSwitch_EnableToggleControl_HeaderText.Header" xml:space="preserve">
|
||||
<value>Enable Light Switch</value>
|
||||
</data>
|
||||
<data name="LightSwitch.ModuleDescription" xml:space="preserve">
|
||||
<value>Easily switch between light and dark mode - on a schedule, automatically, or with a shortcut.</value>
|
||||
</data>
|
||||
<data name="LightSwitch.ModuleTitle" xml:space="preserve">
|
||||
<value>Light Switch</value>
|
||||
</data>
|
||||
<data name="LearnMore_LightSwitch.Text" xml:space="preserve">
|
||||
<value>Learn more about Light Switch</value>
|
||||
</data>
|
||||
<data name="LightSwitch_BehaviorSettingsGroup.Header" xml:space="preserve">
|
||||
<value>Behavior</value>
|
||||
</data>
|
||||
<data name="LightSwitch_EnableSettingsCard.Header" xml:space="preserve">
|
||||
<value>Enable Light Switch</value>
|
||||
</data>
|
||||
<data name="LightSwitch_ShortcutsSettingsGroup.Header" xml:space="preserve">
|
||||
<value>Shortcuts</value>
|
||||
</data>
|
||||
<data name="LightSwitch_ScheduleSettingsGroup.Header" xml:space="preserve">
|
||||
<value>Schedule</value>
|
||||
</data>
|
||||
<data name="LightSwitch_ModeSettingsExpander.Header" xml:space="preserve">
|
||||
<value>Mode</value>
|
||||
</data>
|
||||
<data name="LightSwitch_ModeSettingsExpander.Description" xml:space="preserve">
|
||||
<value>Determine when dark mode should be turned on</value>
|
||||
</data>
|
||||
<data name="LightSwitch_ModeManual.Content" xml:space="preserve">
|
||||
<value>Manual</value>
|
||||
</data>
|
||||
<data name="LightSwitch_ModeSunsetToSunrise.Content" xml:space="preserve">
|
||||
<value>Sunset to sunrise</value>
|
||||
</data>
|
||||
<data name="LightSwitch_TurnOnDarkMode.Header" xml:space="preserve">
|
||||
<value>Turn on dark mode</value>
|
||||
</data>
|
||||
<data name="LightSwitch_TurnOffDarkMode.Header" xml:space="preserve">
|
||||
<value>Turn off dark mode</value>
|
||||
</data>
|
||||
<data name="LightSwitch_LocationSettingsCard.Header" xml:space="preserve">
|
||||
<value>Location</value>
|
||||
</data>
|
||||
<data name="LightSwitch_LocationSettingsCard.Description" xml:space="preserve">
|
||||
<value>Used to automatically calculate accurate sunrise and sunset times</value>
|
||||
</data>
|
||||
<data name="LightSwitch_OffsetSettingsCard.Header" xml:space="preserve">
|
||||
<value>Offset (in minutes)</value>
|
||||
</data>
|
||||
<data name="LightSwitch_OffsetSettingsCard.Description" xml:space="preserve">
|
||||
<value>Adjust the trigger time by starting earlier or later</value>
|
||||
</data>
|
||||
<data name="LightSwitch_LocationWarningBar.Title" xml:space="preserve">
|
||||
<value>Location required</value>
|
||||
</data>
|
||||
<data name="LightSwitch_LocationWarningBar.Message" xml:space="preserve">
|
||||
<value>Sync your location so Light Switch can calculate the correct sunrise- and sunset times</value>
|
||||
</data>
|
||||
<data name="LightSwitch_ApplyDarkModeExpander.Header" xml:space="preserve">
|
||||
<value>Apply dark mode to</value>
|
||||
</data>
|
||||
<data name="LightSwitch_ApplyDarkModeExpander.Description" xml:space="preserve">
|
||||
<value>Pick which parts of your PC should follow Light Switch</value>
|
||||
</data>
|
||||
<data name="LightSwitch_SystemCheckbox.Header" xml:space="preserve">
|
||||
<value>System</value>
|
||||
</data>
|
||||
<data name="LightSwitch_SystemCheckbox.Description" xml:space="preserve">
|
||||
<value>Taskbar, Start, and other system UI</value>
|
||||
</data>
|
||||
<data name="LightSwitch_AppsCheckbox.Header" xml:space="preserve">
|
||||
<value>Apps</value>
|
||||
</data>
|
||||
<data name="LightSwitch_AppsCheckbox.Description" xml:space="preserve">
|
||||
<value>Supported applications</value>
|
||||
</data>
|
||||
<data name="LightSwitch_LocationDialog.Title" xml:space="preserve">
|
||||
<value>Select a location</value>
|
||||
</data>
|
||||
<data name="LightSwitch_LocationDialog.PrimaryButtonText" xml:space="preserve">
|
||||
<value>Select</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>
|
||||
</data>
|
||||
<data name="LightSwitch_SunriseText.Text" xml:space="preserve">
|
||||
<value>Sunrise</value>
|
||||
</data>
|
||||
<data name="LightSwitch_SunsetText.Text" xml:space="preserve">
|
||||
<value>Sunset</value>
|
||||
</data>
|
||||
<data name="LightSwitch_LocationTooltip.Text" xml:space="preserve">
|
||||
<value>Location</value>
|
||||
</data>
|
||||
<data name="LightSwitch_SunriseTooltip.Text" xml:space="preserve">
|
||||
<value>Sunrise</value>
|
||||
</data>
|
||||
<data name="LightSwitch_SunsetTooltip.Text" xml:space="preserve">
|
||||
<value>Sunset</value>
|
||||
</data>
|
||||
<data name="Close_NavViewItem.Content" xml:space="preserve">
|
||||
<value>Close PowerToys</value>
|
||||
<comment>Don't loc "PowerToys"</comment>
|
||||
@@ -5321,6 +5441,22 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
|
||||
<data name="UtilitiesHeader.Title" xml:space="preserve">
|
||||
<value>Utilities</value>
|
||||
</data>
|
||||
<data name="Oobe_LightSwitch.Title" xml:space="preserve">
|
||||
<value>Light Switch</value>
|
||||
<comment>Product name. Do not localize this string</comment>
|
||||
</data>
|
||||
<data name="Oobe_LightSwitch.Description" xml:space="preserve">
|
||||
<value>Light Switch automatically manages your Windows light and dark mode based on schedules, sunrise/sunset times, or manual control. Keep your system theme synchronized with your preferences and daily rhythm.</value>
|
||||
<comment>Light Switch is a product name, do not localize</comment>
|
||||
</data>
|
||||
<data name="Oobe_LightSwitch_HowToUse.Text" xml:space="preserve">
|
||||
<value>Open **PowerToys Settings** and enable Light Switch to set up automatic theme switching</value>
|
||||
<comment>Light Switch is a product name, do not localize</comment>
|
||||
</data>
|
||||
<data name="Oobe_LightSwitch_TipsAndTricks.Text" xml:space="preserve">
|
||||
<value>Use the **keyboard shortcut** to instantly toggle between light and dark modes, or set up **sunrise/sunset automation** for natural theme transitions.</value>
|
||||
<comment>Light Switch is a product name, do not localize</comment>
|
||||
</data>
|
||||
<data name="DismissConflictBtn.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Dismiss</value>
|
||||
</data>
|
||||
|
||||
@@ -22,6 +22,7 @@ using Microsoft.PowerToys.Settings.UI.Services;
|
||||
using Microsoft.PowerToys.Settings.UI.Views;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Settings.UI.Library;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
{
|
||||
@@ -226,6 +227,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
ModuleType.FancyZones => GetModuleItemsFancyZones(),
|
||||
ModuleType.FindMyMouse => GetModuleItemsFindMyMouse(),
|
||||
ModuleType.Hosts => GetModuleItemsHosts(),
|
||||
ModuleType.LightSwitch => GetModuleItemsLightSwitch(),
|
||||
ModuleType.MouseHighlighter => GetModuleItemsMouseHighlighter(),
|
||||
ModuleType.MouseJump => GetModuleItemsMouseJump(),
|
||||
ModuleType.MousePointerCrosshairs => GetModuleItemsMousePointerCrosshairs(),
|
||||
@@ -274,6 +276,17 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
return new ObservableCollection<DashboardModuleItem>(list);
|
||||
}
|
||||
|
||||
private ObservableCollection<DashboardModuleItem> GetModuleItemsLightSwitch()
|
||||
{
|
||||
ISettingsRepository<LightSwitchSettings> moduleSettingsRepository = SettingsRepository<LightSwitchSettings>.GetInstance(new SettingsUtils());
|
||||
var settings = moduleSettingsRepository.SettingsConfig;
|
||||
var list = new List<DashboardModuleItem>
|
||||
{
|
||||
new DashboardModuleShortcutItem() { Label = resourceLoader.GetString("LightSwitch_ForceDarkMode"), Shortcut = settings.Properties.ToggleThemeHotkey.Value.GetKeysList() },
|
||||
};
|
||||
return new ObservableCollection<DashboardModuleItem>(list);
|
||||
}
|
||||
|
||||
private ObservableCollection<DashboardModuleItem> GetModuleItemsCropAndLock()
|
||||
{
|
||||
ISettingsRepository<CropAndLockSettings> moduleSettingsRepository = SettingsRepository<CropAndLockSettings>.GetInstance(new SettingsUtils());
|
||||
|
||||
510
src/settings-ui/Settings.UI/ViewModels/LightSwitchViewModel.cs
Normal file
510
src/settings-ui/Settings.UI/ViewModels/LightSwitchViewModel.cs
Normal file
@@ -0,0 +1,510 @@
|
||||
// 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.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using System.Windows.Input;
|
||||
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.SerializationContext;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Settings.UI.Library;
|
||||
using Settings.UI.Library.Helpers;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
{
|
||||
public partial class LightSwitchViewModel : Observable
|
||||
{
|
||||
private Func<string, int> SendConfigMSG { get; }
|
||||
|
||||
public ObservableCollection<SearchLocation> SearchLocations { get; } = new();
|
||||
|
||||
public LightSwitchViewModel(LightSwitchSettings initialSettings = null, Func<string, int> ipcMSGCallBackFunc = null)
|
||||
{
|
||||
_moduleSettings = initialSettings ?? new LightSwitchSettings();
|
||||
SendConfigMSG = ipcMSGCallBackFunc;
|
||||
|
||||
ForceLightCommand = new RelayCommand(ForceLightNow);
|
||||
ForceDarkCommand = new RelayCommand(ForceDarkNow);
|
||||
|
||||
AvailableScheduleModes = new ObservableCollection<string>
|
||||
{
|
||||
"FixedHours",
|
||||
"SunsetToSunrise",
|
||||
};
|
||||
|
||||
_toggleThemeHotkey = _moduleSettings.Properties.ToggleThemeHotkey.Value;
|
||||
}
|
||||
|
||||
private void ForceLightNow()
|
||||
{
|
||||
Logger.LogInfo("Sending custom action: forceLight");
|
||||
SendCustomAction("forceLight");
|
||||
}
|
||||
|
||||
private void ForceDarkNow()
|
||||
{
|
||||
Logger.LogInfo("Sending custom action: forceDark");
|
||||
SendCustomAction("forceDark");
|
||||
}
|
||||
|
||||
private void SendCustomAction(string actionName)
|
||||
{
|
||||
SendConfigMSG("{\"action\":{\"LightSwitch\":{\"action_name\":\"" + actionName + "\", \"value\":\"\"}}}");
|
||||
}
|
||||
|
||||
public LightSwitchSettings ModuleSettings
|
||||
{
|
||||
get => _moduleSettings;
|
||||
set
|
||||
{
|
||||
if (_moduleSettings != value)
|
||||
{
|
||||
_moduleSettings = value;
|
||||
|
||||
OnPropertyChanged(nameof(ModuleSettings));
|
||||
RefreshModuleSettings();
|
||||
RefreshEnabledState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsEnabled
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_enabledStateIsGPOConfigured)
|
||||
{
|
||||
return _enabledGPOConfiguration;
|
||||
}
|
||||
else
|
||||
{
|
||||
return _isEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (_isEnabled != value)
|
||||
{
|
||||
if (_enabledStateIsGPOConfigured)
|
||||
{
|
||||
// If it's GPO configured, shouldn't be able to change this state.
|
||||
return;
|
||||
}
|
||||
|
||||
_isEnabled = value;
|
||||
|
||||
RefreshEnabledState();
|
||||
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsEnabledGpoConfigured
|
||||
{
|
||||
get => _enabledStateIsGPOConfigured;
|
||||
set
|
||||
{
|
||||
if (_enabledStateIsGPOConfigured != value)
|
||||
{
|
||||
_enabledStateIsGPOConfigured = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool EnabledGPOConfiguration
|
||||
{
|
||||
get => _enabledGPOConfiguration;
|
||||
set
|
||||
{
|
||||
if (_enabledGPOConfiguration != value)
|
||||
{
|
||||
_enabledGPOConfiguration = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string ScheduleMode
|
||||
{
|
||||
get => ModuleSettings.Properties.ScheduleMode.Value;
|
||||
set
|
||||
{
|
||||
var oldMode = ModuleSettings.Properties.ScheduleMode.Value;
|
||||
if (ModuleSettings.Properties.ScheduleMode.Value != value)
|
||||
{
|
||||
ModuleSettings.Properties.ScheduleMode.Value = value;
|
||||
OnPropertyChanged(nameof(ScheduleMode));
|
||||
}
|
||||
|
||||
if (ModuleSettings.Properties.ScheduleMode.Value == "FixedHours" && oldMode != "FixedHours")
|
||||
{
|
||||
LightTime = 360;
|
||||
DarkTime = 1080;
|
||||
SunsetTimeSpan = null;
|
||||
SunriseTimeSpan = null;
|
||||
|
||||
OnPropertyChanged(nameof(LightTimePickerValue));
|
||||
OnPropertyChanged(nameof(DarkTimePickerValue));
|
||||
}
|
||||
|
||||
if (ModuleSettings.Properties.ScheduleMode.Value == "SunsetToSunrise")
|
||||
{
|
||||
if (ModuleSettings.Properties.Latitude != "0.0" && ModuleSettings.Properties.Longitude != "0.0")
|
||||
{
|
||||
double lat = double.Parse(ModuleSettings.Properties.Latitude.Value, CultureInfo.InvariantCulture);
|
||||
double lon = double.Parse(ModuleSettings.Properties.Longitude.Value, CultureInfo.InvariantCulture);
|
||||
UpdateSunTimes(lat, lon);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ObservableCollection<string> AvailableScheduleModes { get; }
|
||||
|
||||
public bool ChangeSystem
|
||||
{
|
||||
get => ModuleSettings.Properties.ChangeSystem.Value;
|
||||
set
|
||||
{
|
||||
if (ModuleSettings.Properties.ChangeSystem.Value != value)
|
||||
{
|
||||
ModuleSettings.Properties.ChangeSystem.Value = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ChangeApps
|
||||
{
|
||||
get => ModuleSettings.Properties.ChangeApps.Value;
|
||||
set
|
||||
{
|
||||
if (ModuleSettings.Properties.ChangeApps.Value != value)
|
||||
{
|
||||
ModuleSettings.Properties.ChangeApps.Value = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int LightTime
|
||||
{
|
||||
get => ModuleSettings.Properties.LightTime.Value;
|
||||
set
|
||||
{
|
||||
if (ModuleSettings.Properties.LightTime.Value != value)
|
||||
{
|
||||
ModuleSettings.Properties.LightTime.Value = value;
|
||||
NotifyPropertyChanged();
|
||||
|
||||
OnPropertyChanged(nameof(LightTimeTimeSpan));
|
||||
|
||||
if (ScheduleMode == "SunsetToSunrise")
|
||||
{
|
||||
SunriseTimeSpan = TimeSpan.FromMinutes(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int DarkTime
|
||||
{
|
||||
get => ModuleSettings.Properties.DarkTime.Value;
|
||||
set
|
||||
{
|
||||
if (ModuleSettings.Properties.DarkTime.Value != value)
|
||||
{
|
||||
ModuleSettings.Properties.DarkTime.Value = value;
|
||||
NotifyPropertyChanged();
|
||||
|
||||
OnPropertyChanged(nameof(DarkTimeTimeSpan));
|
||||
|
||||
if (ScheduleMode == "SunsetToSunrise")
|
||||
{
|
||||
SunsetTimeSpan = TimeSpan.FromMinutes(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int SunriseOffset
|
||||
{
|
||||
get => ModuleSettings.Properties.SunriseOffset.Value;
|
||||
set
|
||||
{
|
||||
if (ModuleSettings.Properties.SunriseOffset.Value != value)
|
||||
{
|
||||
ModuleSettings.Properties.SunriseOffset.Value = value;
|
||||
OnPropertyChanged(nameof(LightTimeTimeSpan));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int SunsetOffset
|
||||
{
|
||||
get => ModuleSettings.Properties.SunsetOffset.Value;
|
||||
set
|
||||
{
|
||||
if (ModuleSettings.Properties.SunsetOffset.Value != value)
|
||||
{
|
||||
ModuleSettings.Properties.SunsetOffset.Value = value;
|
||||
OnPropertyChanged(nameof(DarkTimeTimeSpan));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === Computed projections (OneWay bindings only) ===
|
||||
public TimeSpan LightTimeTimeSpan
|
||||
{
|
||||
get
|
||||
{
|
||||
if (ScheduleMode == "SunsetToSunrise")
|
||||
{
|
||||
return TimeSpan.FromMinutes(LightTime + SunriseOffset);
|
||||
}
|
||||
else
|
||||
{
|
||||
return TimeSpan.FromMinutes(LightTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public TimeSpan DarkTimeTimeSpan
|
||||
{
|
||||
get
|
||||
{
|
||||
if (ScheduleMode == "SunsetToSunrise")
|
||||
{
|
||||
return TimeSpan.FromMinutes(DarkTime + SunsetOffset);
|
||||
}
|
||||
else
|
||||
{
|
||||
return TimeSpan.FromMinutes(DarkTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === Values to pass to timeline ===
|
||||
public TimeSpan? SunriseTimeSpan
|
||||
{
|
||||
get => _sunriseTimeSpan;
|
||||
set
|
||||
{
|
||||
if (_sunriseTimeSpan != value)
|
||||
{
|
||||
_sunriseTimeSpan = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public TimeSpan? SunsetTimeSpan
|
||||
{
|
||||
get => _sunsetTimeSpan;
|
||||
set
|
||||
{
|
||||
if (_sunsetTimeSpan != value)
|
||||
{
|
||||
_sunsetTimeSpan = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === Picker values (TwoWay binding targets for TimePickers) ===
|
||||
public TimeSpan LightTimePickerValue
|
||||
{
|
||||
get => TimeSpan.FromMinutes(LightTime);
|
||||
set => LightTime = (int)value.TotalMinutes;
|
||||
}
|
||||
|
||||
public TimeSpan DarkTimePickerValue
|
||||
{
|
||||
get => TimeSpan.FromMinutes(DarkTime);
|
||||
set => DarkTime = (int)value.TotalMinutes;
|
||||
}
|
||||
|
||||
public string Latitude
|
||||
{
|
||||
get => ModuleSettings.Properties.Latitude.Value;
|
||||
set
|
||||
{
|
||||
if (ModuleSettings.Properties.Latitude.Value != value)
|
||||
{
|
||||
ModuleSettings.Properties.Latitude.Value = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string Longitude
|
||||
{
|
||||
get => ModuleSettings.Properties.Longitude.Value;
|
||||
set
|
||||
{
|
||||
if (ModuleSettings.Properties.Longitude.Value != value)
|
||||
{
|
||||
ModuleSettings.Properties.Longitude.Value = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private SearchLocation _selectedSearchLocation;
|
||||
|
||||
public SearchLocation SelectedCity
|
||||
{
|
||||
get => _selectedSearchLocation;
|
||||
set
|
||||
{
|
||||
if (_selectedSearchLocation != value)
|
||||
{
|
||||
_selectedSearchLocation = value;
|
||||
NotifyPropertyChanged();
|
||||
|
||||
UpdateSunTimes(_selectedSearchLocation.Latitude, _selectedSearchLocation.Longitude, _selectedSearchLocation.City);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string _syncButtonInformation = "Please sync your location";
|
||||
|
||||
public string SyncButtonInformation
|
||||
{
|
||||
get => _syncButtonInformation;
|
||||
set
|
||||
{
|
||||
if (_syncButtonInformation != value)
|
||||
{
|
||||
_syncButtonInformation = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public HotkeySettings ToggleThemeActivationShortcut
|
||||
{
|
||||
get => _toggleThemeHotkey;
|
||||
|
||||
set
|
||||
{
|
||||
if (value != _toggleThemeHotkey)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
_toggleThemeHotkey = LightSwitchProperties.DefaultToggleThemeHotkey;
|
||||
}
|
||||
else
|
||||
{
|
||||
_toggleThemeHotkey = value;
|
||||
}
|
||||
|
||||
_moduleSettings.Properties.ToggleThemeHotkey.Value = _toggleThemeHotkey;
|
||||
NotifyPropertyChanged();
|
||||
|
||||
SendConfigMSG(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{{ \"powertoys\": {{ \"{0}\": {1} }} }}",
|
||||
LightSwitchSettings.ModuleName,
|
||||
JsonSerializer.Serialize(_moduleSettings, (System.Text.Json.Serialization.Metadata.JsonTypeInfo<LightSwitchSettings>)SourceGenerationContextContext.Default.LightSwitchSettings)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
|
||||
{
|
||||
Logger.LogInfo($"Changed the property {propertyName}");
|
||||
OnPropertyChanged(propertyName);
|
||||
}
|
||||
|
||||
public void RefreshEnabledState()
|
||||
{
|
||||
OnPropertyChanged(nameof(IsEnabled));
|
||||
}
|
||||
|
||||
public void RefreshModuleSettings()
|
||||
{
|
||||
OnPropertyChanged(nameof(ChangeSystem));
|
||||
OnPropertyChanged(nameof(ChangeApps));
|
||||
OnPropertyChanged(nameof(LightTime));
|
||||
OnPropertyChanged(nameof(DarkTime));
|
||||
OnPropertyChanged(nameof(SunriseOffset));
|
||||
OnPropertyChanged(nameof(SunsetOffset));
|
||||
OnPropertyChanged(nameof(Latitude));
|
||||
OnPropertyChanged(nameof(Longitude));
|
||||
OnPropertyChanged(nameof(ScheduleMode));
|
||||
}
|
||||
|
||||
private void UpdateSunTimes(double latitude, double longitude, string city = "n/a")
|
||||
{
|
||||
SunTimes result = SunCalc.CalculateSunriseSunset(
|
||||
latitude,
|
||||
longitude,
|
||||
DateTime.Now.Year,
|
||||
DateTime.Now.Month,
|
||||
DateTime.Now.Day);
|
||||
|
||||
LightTime = (result.SunriseHour * 60) + result.SunriseMinute;
|
||||
DarkTime = (result.SunsetHour * 60) + result.SunsetMinute;
|
||||
Latitude = latitude.ToString(CultureInfo.InvariantCulture);
|
||||
Longitude = longitude.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
if (city != "n/a")
|
||||
{
|
||||
SyncButtonInformation = city;
|
||||
}
|
||||
}
|
||||
|
||||
public void InitializeScheduleMode()
|
||||
{
|
||||
if (ScheduleMode == "SunsetToSunrise" &&
|
||||
double.TryParse(Latitude, NumberStyles.Float, CultureInfo.InvariantCulture, out double savedLat) &&
|
||||
double.TryParse(Longitude, NumberStyles.Float, CultureInfo.InvariantCulture, out double savedLng))
|
||||
{
|
||||
var match = SearchLocations.FirstOrDefault(c =>
|
||||
Math.Abs(c.Latitude - savedLat) < 0.0001 &&
|
||||
Math.Abs(c.Longitude - savedLng) < 0.0001);
|
||||
|
||||
if (match != null)
|
||||
{
|
||||
SelectedCity = match;
|
||||
}
|
||||
|
||||
SyncButtonInformation = SelectedCity != null
|
||||
? SelectedCity.City
|
||||
: $"{Latitude},{Longitude}";
|
||||
|
||||
double lat = double.Parse(ModuleSettings.Properties.Latitude.Value, CultureInfo.InvariantCulture);
|
||||
double lon = double.Parse(ModuleSettings.Properties.Longitude.Value, CultureInfo.InvariantCulture);
|
||||
UpdateSunTimes(lat, lon);
|
||||
|
||||
SunriseTimeSpan = TimeSpan.FromMinutes(LightTime);
|
||||
SunsetTimeSpan = TimeSpan.FromMinutes(DarkTime);
|
||||
}
|
||||
}
|
||||
|
||||
private bool _enabledStateIsGPOConfigured;
|
||||
private bool _enabledGPOConfiguration;
|
||||
private LightSwitchSettings _moduleSettings;
|
||||
private bool _isEnabled;
|
||||
private HotkeySettings _toggleThemeHotkey;
|
||||
private TimeSpan? _sunriseTimeSpan;
|
||||
private TimeSpan? _sunsetTimeSpan;
|
||||
|
||||
public ICommand ForceLightCommand { get; }
|
||||
|
||||
public ICommand ForceDarkCommand { get; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user