[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.

![Light
Switch](https://github.com/user-attachments/assets/d24d7364-445f-4f23-ab5e-4b8c6a4147ab)

<!-- 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:
Jaylyn Barbee
2025-10-06 16:44:07 -04:00
committed by GitHub
parent ccc31c13ae
commit 0d5220561d
111 changed files with 5798 additions and 12 deletions

View File

@@ -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>

View File

@@ -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);

View File

@@ -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}"

View File

@@ -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>

View File

@@ -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);
}
}
}

View File

@@ -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="&#xE793;" />
<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="&#xE706;" />
<Setter Property="Foreground" Value="{ThemeResource TextFillColorSecondaryBrush}" />
</Style>
</ResourceDictionary>

View File

@@ -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>

View File

@@ -0,0 +1,33 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using 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();
}
}
}

View File

@@ -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}"

View File

@@ -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;

View File

@@ -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=&#xE708;}">
<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=&#xE823;}"
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=&#xECAF;,
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="&#xED39;" 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=&#xE790;}"
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=&#xE706;}"
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="&#xECAF;" />
<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="&#xECAF;">
<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="&#xED39;">
<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="&#xED3A;">
<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>

View File

@@ -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();
}
}
}

View File

@@ -30,7 +30,6 @@
Command="{x:Bind ViewModel.LaunchEventHandler}"
HeaderIcon="{ui:FontIcon Glyph=&#xEA37;}"
IsClickEnabled="True" />
<tkcontrols:SettingsCard
Name="RegistryPreviewDefaultRegApp"
x:Uid="RegistryPreview_DefaultRegApp"

View File

@@ -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"