Awake vNext - NOBLE_SIX_02162023 (#24183)

* Initial scaffolding for expiration configuration

* Simplifying the code and adding support for expiration events

* Bit more cleanup

* Initial support for expirable keep-awake

* Update some of the threading logic

* Logging and timing consistency

* Initial UI scaffolding

* Fix pathing issue for the icon when using config file

* Add missing definitions

* Update with basic interface

* Cleanup redundant calls

* Update name per convention

* Simplify declaration

* Proper binding to secondary Time property

* Cleanup the terminology use

* Standardize naming conventions.

* More Awake cleanup

* Ability to update the UI when the tray icon updates

* Small tweaks before ViewModel refactor

* Refactor the view model logic

* Some consistency fixes

* Remove the build props change

* Add settings scaffolding when a file does not exist

* Update expect.txt

* Fix typos

* Update build in logs

* Updating based on discussion in #24183.
This specifically addresses the fact that the `ExpirationDateTime` property was incorrectly auto-initialized to `DateTime.MinValue` when it should've been set to `DateTimeOffset.MinValue` to be consistent with the underlying type and assumptions around date/time.

---------

Co-authored-by: Clint Rutkas <clint@rutkas.com>
This commit is contained in:
Den
2023-03-15 01:42:47 -07:00
committed by GitHub
parent 13cb52763d
commit 466252745d
20 changed files with 664 additions and 323 deletions

View File

@@ -227,6 +227,7 @@ clientside
CLIPCHILDREN
Clipperton
CLIPSIBLINGS
Cloneable
clrcall
Cls
CLSCTX

View File

@@ -6,6 +6,8 @@ using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
@@ -82,31 +84,54 @@ namespace Awake.Core
}
}
public static void SetIndefiniteKeepAwake(Action<bool> callback, Action failureCallback, bool keepDisplayOn = false)
private static bool SetAwakeStateBasedOnDisplaySetting(bool keepDisplayOn)
{
PowerToysTelemetry.Log.WriteEvent(new Awake.Telemetry.AwakeIndefinitelyKeepAwakeEvent());
if (keepDisplayOn)
{
return SetAwakeState(EXECUTION_STATE.ES_SYSTEM_REQUIRED | EXECUTION_STATE.ES_DISPLAY_REQUIRED | EXECUTION_STATE.ES_CONTINUOUS);
}
else
{
return SetAwakeState(EXECUTION_STATE.ES_SYSTEM_REQUIRED | EXECUTION_STATE.ES_CONTINUOUS);
}
}
public static void CancelExistingThread()
{
_tokenSource.Cancel();
try
{
_log.Info("Attempting to ensure that the thread is properly cleaned up...");
if (_runnerThread != null && !_runnerThread.IsCanceled)
{
_runnerThread.Wait(_threadToken);
}
_log.Info("Thread is clean.");
}
catch (OperationCanceledException)
{
_log.Info("Confirmed background thread cancellation when setting indefinite keep awake.");
_log.Info("Confirmed background thread cancellation when disabling explicit keep awake.");
}
_tokenSource = new CancellationTokenSource();
_threadToken = _tokenSource.Token;
_log.Info("Instantiating of new token source and thread token completed.");
}
public static void SetIndefiniteKeepAwake(Action callback, Action failureCallback, bool keepDisplayOn = false)
{
PowerToysTelemetry.Log.WriteEvent(new Awake.Telemetry.AwakeIndefinitelyKeepAwakeEvent());
CancelExistingThread();
try
{
_runnerThread = Task.Run(() => RunIndefiniteLoop(keepDisplayOn), _threadToken)
.ContinueWith((result) => callback(result.Result), TaskContinuationOptions.OnlyOnRanToCompletion)
_runnerThread = Task.Run(() => RunIndefiniteJob(keepDisplayOn), _threadToken)
.ContinueWith((result) => callback, TaskContinuationOptions.OnlyOnRanToCompletion)
.ContinueWith((result) => failureCallback, TaskContinuationOptions.NotOnRanToCompletion);
}
catch (Exception ex)
@@ -117,80 +142,101 @@ namespace Awake.Core
public static void SetNoKeepAwake()
{
_tokenSource.Cancel();
PowerToysTelemetry.Log.WriteEvent(new Awake.Telemetry.AwakeNoKeepAwakeEvent());
try
{
if (_runnerThread != null && !_runnerThread.IsCanceled)
{
_runnerThread.Wait(_threadToken);
}
}
catch (OperationCanceledException)
{
_log.Info("Confirmed background thread cancellation when disabling explicit keep awake.");
}
CancelExistingThread();
}
public static void SetTimedKeepAwake(uint seconds, Action<bool> callback, Action failureCallback, bool keepDisplayOn = true)
public static void SetExpirableKeepAwake(DateTimeOffset expireAt, Action callback, Action failureCallback, bool keepDisplayOn = true)
{
PowerToysTelemetry.Log.WriteEvent(new Awake.Telemetry.AwakeTimedKeepAwakeEvent());
PowerToysTelemetry.Log.WriteEvent(new Awake.Telemetry.AwakeExpirableKeepAwakeEvent());
_tokenSource.Cancel();
CancelExistingThread();
try
if (expireAt > DateTime.Now && expireAt != null)
{
if (_runnerThread != null && !_runnerThread.IsCanceled)
{
_runnerThread.Wait(_threadToken);
}
}
catch (OperationCanceledException)
{
_log.Info("Confirmed background thread cancellation when setting timed keep awake.");
}
_tokenSource = new CancellationTokenSource();
_threadToken = _tokenSource.Token;
_runnerThread = Task.Run(() => RunTimedLoop(seconds, keepDisplayOn), _threadToken)
.ContinueWith((result) => callback(result.Result), TaskContinuationOptions.OnlyOnRanToCompletion)
.ContinueWith((result) => failureCallback, TaskContinuationOptions.NotOnRanToCompletion);
}
private static bool RunIndefiniteLoop(bool keepDisplayOn = false)
{
bool success;
if (keepDisplayOn)
{
success = SetAwakeState(EXECUTION_STATE.ES_SYSTEM_REQUIRED | EXECUTION_STATE.ES_DISPLAY_REQUIRED | EXECUTION_STATE.ES_CONTINUOUS);
_runnerThread = Task.Run(() => RunExpiringJob(expireAt, keepDisplayOn), _threadToken)
.ContinueWith((result) => callback, TaskContinuationOptions.OnlyOnRanToCompletion)
.ContinueWith((result) => failureCallback, TaskContinuationOptions.NotOnRanToCompletion);
}
else
{
success = SetAwakeState(EXECUTION_STATE.ES_SYSTEM_REQUIRED | EXECUTION_STATE.ES_CONTINUOUS);
// The target date is not in the future.
_log.Error("The specified target date and time is not in the future.");
_log.Error($"Current time: {DateTime.Now}\tTarget time: {expireAt}");
}
}
public static void SetTimedKeepAwake(uint seconds, Action callback, Action failureCallback, bool keepDisplayOn = true)
{
PowerToysTelemetry.Log.WriteEvent(new Awake.Telemetry.AwakeTimedKeepAwakeEvent());
CancelExistingThread();
_runnerThread = Task.Run(() => RunTimedJob(seconds, keepDisplayOn), _threadToken)
.ContinueWith((result) => callback, TaskContinuationOptions.OnlyOnRanToCompletion)
.ContinueWith((result) => failureCallback, TaskContinuationOptions.NotOnRanToCompletion);
}
private static void RunExpiringJob(DateTimeOffset expireAt, bool keepDisplayOn = false)
{
bool success = false;
// In case cancellation was already requested.
_threadToken.ThrowIfCancellationRequested();
try
{
success = SetAwakeStateBasedOnDisplaySetting(keepDisplayOn);
if (success)
{
_log.Info($"Initiated indefinite keep awake in background thread: {PInvoke.GetCurrentThreadId()}. Screen on: {keepDisplayOn}");
_log.Info($"Initiated expirable keep awake in background thread: {PInvoke.GetCurrentThreadId()}. Screen on: {keepDisplayOn}");
WaitHandle.WaitAny(new[] { _threadToken.WaitHandle });
return success;
Observable.Timer(expireAt, Scheduler.CurrentThread).Subscribe(
_ =>
{
_log.Info($"Completed expirable thread in {PInvoke.GetCurrentThreadId()}.");
CancelExistingThread();
},
_tokenSource.Token);
}
else
{
_log.Info("Could not successfully set up indefinite keep awake.");
return success;
_log.Info("Could not successfully set up expirable keep awake.");
}
}
catch (OperationCanceledException ex)
{
// Task was clearly cancelled.
_log.Info($"Background thread termination: {PInvoke.GetCurrentThreadId()}. Message: {ex.Message}");
}
}
private static void RunIndefiniteJob(bool keepDisplayOn = false)
{
// In case cancellation was already requested.
_threadToken.ThrowIfCancellationRequested();
try
{
bool success = SetAwakeStateBasedOnDisplaySetting(keepDisplayOn);
if (success)
{
_log.Info($"Initiated indefinite keep awake in background thread: {PInvoke.GetCurrentThreadId()}. Screen on: {keepDisplayOn}");
WaitHandle.WaitAny(new[] { _threadToken.WaitHandle });
}
else
{
_log.Info("Could not successfully set up indefinite keep awake.");
}
}
catch (OperationCanceledException ex)
{
// Task was clearly cancelled.
_log.Info($"Background thread termination: {PInvoke.GetCurrentThreadId()}. Message: {ex.Message}");
return success;
}
}
@@ -221,59 +267,38 @@ namespace Awake.Core
}
}
private static bool RunTimedLoop(uint seconds, bool keepDisplayOn = true)
private static void RunTimedJob(uint seconds, bool keepDisplayOn = true)
{
bool success = false;
// In case cancellation was already requested.
_threadToken.ThrowIfCancellationRequested();
try
{
if (keepDisplayOn)
{
success = SetAwakeState(EXECUTION_STATE.ES_SYSTEM_REQUIRED | EXECUTION_STATE.ES_DISPLAY_REQUIRED | EXECUTION_STATE.ES_CONTINUOUS);
}
else
{
success = SetAwakeState(EXECUTION_STATE.ES_SYSTEM_REQUIRED | EXECUTION_STATE.ES_CONTINUOUS);
}
success = SetAwakeStateBasedOnDisplaySetting(keepDisplayOn);
if (success)
{
_log.Info($"Initiated temporary keep awake in background thread: {PInvoke.GetCurrentThreadId()}. Screen on: {keepDisplayOn}");
_log.Info($"Initiated timed keep awake in background thread: {PInvoke.GetCurrentThreadId()}. Screen on: {keepDisplayOn}");
_timedLoopTimer = new System.Timers.Timer((seconds * 1000) + 1);
_timedLoopTimer.Elapsed += (s, e) =>
{
_tokenSource.Cancel();
_timedLoopTimer.Stop();
};
_timedLoopTimer.Disposed += (s, e) =>
{
_log.Info("Old timer disposed.");
};
_timedLoopTimer.Start();
WaitHandle.WaitAny(new[] { _threadToken.WaitHandle });
_timedLoopTimer.Stop();
_timedLoopTimer.Dispose();
return success;
Observable.Timer(TimeSpan.FromSeconds(seconds), Scheduler.CurrentThread).Subscribe(
_ =>
{
_log.Info($"Completed timed thread in {PInvoke.GetCurrentThreadId()}.");
CancelExistingThread();
},
_tokenSource.Token);
}
else
{
_log.Info("Could not set up timed keep-awake with display on.");
return success;
}
}
catch (OperationCanceledException ex)
{
// Task was clearly cancelled.
_log.Info($"Background thread termination: {PInvoke.GetCurrentThreadId()}. Message: {ex.Message}");
return success;
}
}
@@ -357,10 +382,12 @@ namespace Awake.Core
public static Dictionary<string, int> GetDefaultTrayOptions()
{
Dictionary<string, int> optionsList = new Dictionary<string, int>();
optionsList.Add("30 minutes", 1800);
optionsList.Add("1 hour", 3600);
optionsList.Add("2 hours", 7200);
Dictionary<string, int> optionsList = new Dictionary<string, int>
{
{ "30 minutes", 1800 },
{ "1 hour", 3600 },
{ "2 hours", 7200 },
};
return optionsList;
}
}

View File

@@ -1,12 +0,0 @@
// 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.
namespace Awake.Core.Models
{
public struct BatteryReportingScale
{
public uint Granularity;
public uint Capacity;
}
}

View File

@@ -1,16 +0,0 @@
// 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.
namespace Awake.Core.Models
{
// See: https://learn.microsoft.com/windows/console/handlerroutine
public enum ControlType
{
CTRL_C_EVENT = 0,
CTRL_BREAK_EVENT = 1,
CTRL_CLOSE_EVENT = 2,
CTRL_LOGOFF_EVENT = 5,
CTRL_SHUTDOWN_EVENT = 6,
}
}

View File

@@ -11,7 +11,8 @@ namespace Awake.Core.Models
TC_DISPLAY_SETTING = PInvoke.WM_USER + 1,
TC_MODE_PASSIVE = PInvoke.WM_USER + 2,
TC_MODE_INDEFINITE = PInvoke.WM_USER + 3,
TC_EXIT = PInvoke.WM_USER + 4,
TC_TIME = PInvoke.WM_USER + 5,
TC_MODE_EXPIRABLE = PInvoke.WM_USER + 4,
TC_EXIT = PInvoke.WM_USER + 100,
TC_TIME = PInvoke.WM_USER + 101,
}
}

View File

@@ -20,6 +20,13 @@ using Windows.Win32.UI.WindowsAndMessaging;
namespace Awake.Core
{
/// <summary>
/// Helper class used to manage the system tray.
/// </summary>
/// <remarks>
/// Because Awake is a console application, there is no built-in
/// way to embed UI components so we have to heavily rely on the native Windows API.
/// </remarks>
internal static class TrayHelper
{
private static readonly Logger _log;
@@ -89,7 +96,7 @@ namespace Awake.Core
text,
settings.Properties.KeepDisplayOn,
settings.Properties.Mode,
settings.Properties.TrayTimeShortcuts,
settings.Properties.CustomTrayTimes,
startedFromPowerToys);
}
@@ -116,19 +123,18 @@ namespace Awake.Core
trayTimeShortcuts.AddRange(APIHelper.GetDefaultTrayOptions());
}
// TODO: Make sure that this loads from JSON instead of being hard-coded.
var awakeTimeMenu = new DestroyMenuSafeHandle(PInvoke.CreatePopupMenu(), false);
for (int i = 0; i < trayTimeShortcuts.Count; i++)
{
PInvoke.InsertMenu(awakeTimeMenu, (uint)i, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_STRING, (uint)TrayCommands.TC_TIME + (uint)i, trayTimeShortcuts.ElementAt(i).Key);
}
var modeMenu = new DestroyMenuSafeHandle(PInvoke.CreatePopupMenu(), false);
PInvoke.InsertMenu(modeMenu, 0, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_STRING | (mode == AwakeMode.PASSIVE ? MENU_ITEM_FLAGS.MF_CHECKED : MENU_ITEM_FLAGS.MF_UNCHECKED), (uint)TrayCommands.TC_MODE_PASSIVE, "Off (keep using the selected power plan)");
PInvoke.InsertMenu(modeMenu, 1, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_STRING | (mode == AwakeMode.INDEFINITE ? MENU_ITEM_FLAGS.MF_CHECKED : MENU_ITEM_FLAGS.MF_UNCHECKED), (uint)TrayCommands.TC_MODE_INDEFINITE, "Keep awake indefinitely");
PInvoke.InsertMenu(TrayMenu, 0, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_SEPARATOR, 0, string.Empty);
PInvoke.InsertMenu(modeMenu, 2, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_POPUP | (mode == AwakeMode.TIMED ? MENU_ITEM_FLAGS.MF_CHECKED : MENU_ITEM_FLAGS.MF_UNCHECKED), (uint)awakeTimeMenu.DangerousGetHandle(), "Keep awake temporarily");
PInvoke.InsertMenu(TrayMenu, 0, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_POPUP, (uint)modeMenu.DangerousGetHandle(), "Mode");
PInvoke.InsertMenu(TrayMenu, 0, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_STRING | (mode == AwakeMode.PASSIVE ? MENU_ITEM_FLAGS.MF_CHECKED : MENU_ITEM_FLAGS.MF_UNCHECKED), (uint)TrayCommands.TC_MODE_PASSIVE, "Off (keep using the selected power plan)");
PInvoke.InsertMenu(TrayMenu, 0, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_STRING | (mode == AwakeMode.INDEFINITE ? MENU_ITEM_FLAGS.MF_CHECKED : MENU_ITEM_FLAGS.MF_UNCHECKED), (uint)TrayCommands.TC_MODE_INDEFINITE, "Keep awake indefinitely");
PInvoke.InsertMenu(TrayMenu, 0, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_POPUP | (mode == AwakeMode.TIMED ? MENU_ITEM_FLAGS.MF_CHECKED : MENU_ITEM_FLAGS.MF_UNCHECKED), (uint)awakeTimeMenu.DangerousGetHandle(), "Keep awake on interval");
PInvoke.InsertMenu(TrayMenu, 0, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_STRING | MENU_ITEM_FLAGS.MF_DISABLED | (mode == AwakeMode.EXPIRABLE ? MENU_ITEM_FLAGS.MF_CHECKED : MENU_ITEM_FLAGS.MF_UNCHECKED), (uint)TrayCommands.TC_MODE_EXPIRABLE, "Keep awake until expiration date and time");
TrayIcon.Text = text;
}

View File

@@ -56,13 +56,13 @@ namespace Awake.Core
// Format for the timer block:
// TrayCommands.TC_TIME + ZERO_BASED_INDEX_IN_SETTINGS
AwakeSettings settings = ModuleSettings.GetSettings<AwakeSettings>(InternalConstants.AppName);
if (settings.Properties.TrayTimeShortcuts.Count == 0)
if (settings.Properties.CustomTrayTimes.Count == 0)
{
settings.Properties.TrayTimeShortcuts.AddRange(APIHelper.GetDefaultTrayOptions());
settings.Properties.CustomTrayTimes.AddRange(APIHelper.GetDefaultTrayOptions());
}
int index = (int)targetCommandIndex - (int)TrayCommands.TC_TIME;
var targetTime = settings.Properties.TrayTimeShortcuts.ElementAt(index).Value;
var targetTime = settings.Properties.CustomTrayTimes.ElementAt(index).Value;
TimedKeepAwakeCommandHandler(InternalConstants.AppName, targetTime);
break;
}
@@ -112,8 +112,8 @@ namespace Awake.Core
}
currentSettings.Properties.Mode = AwakeMode.TIMED;
currentSettings.Properties.Hours = (uint)timeSpan.Hours;
currentSettings.Properties.Minutes = (uint)timeSpan.Minutes;
currentSettings.Properties.IntervalHours = (uint)timeSpan.Hours;
currentSettings.Properties.IntervalMinutes = (uint)timeSpan.Minutes;
ModuleSettings.SaveSettings(JsonSerializer.Serialize(currentSettings), moduleName);
}

View File

@@ -2,7 +2,7 @@
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<variable name="buildId" value="LIBRARIAN_03202022" />
<variable name="buildId" value="NOBLE_SIX_02162023" />
<targets async="true">
<target name="logfile"

View File

@@ -38,7 +38,7 @@ namespace Awake
// Format of the build ID is: CODENAME_MMDDYYYY, where MMDDYYYY
// is representative of the date when the last change was made before
// the pull request is issued.
private static readonly string BuildId = "ARBITER_01312022";
private static readonly string BuildId = "NOBLE_SIX_02162023";
private static Mutex? _mutex;
private static FileSystemWatcher? _watcher;
@@ -65,7 +65,7 @@ namespace Awake
if (PowerToys.GPOWrapper.GPOWrapper.GetConfiguredAwakeEnabledValue() == PowerToys.GPOWrapper.GpoRuleConfigured.Disabled)
{
Exit("Tried to start with a GPO policy setting the utility to always be disabled. Please contact your systems administrator.", 1, _exitSignal, true);
Exit("PowerToys.Awake tried to start with a group policy setting that disables the tool. Please contact your system administrator.", 1, _exitSignal, true);
return 0;
}
@@ -97,7 +97,7 @@ namespace Awake
_log.Info("Parsing parameters...");
var configOption = new Option<bool>(
Option<bool> configOption = new(
aliases: new[] { "--use-pt-config", "-c" },
getDefaultValue: () => false,
description: $"Specifies whether {InternalConstants.AppName} will be using the PowerToys configuration file for managing the state.")
@@ -106,11 +106,10 @@ namespace Awake
{
Arity = ArgumentArity.ZeroOrOne,
},
Required = false,
};
configOption.Required = false;
var displayOption = new Option<bool>(
Option<bool> displayOption = new(
aliases: new[] { "--display-on", "-d" },
getDefaultValue: () => true,
description: "Determines whether the display should be kept awake.")
@@ -119,11 +118,10 @@ namespace Awake
{
Arity = ArgumentArity.ZeroOrOne,
},
Required = false,
};
displayOption.Required = false;
var timeOption = new Option<uint>(
Option<uint> timeOption = new(
aliases: new[] { "--time-limit", "-t" },
getDefaultValue: () => 0,
description: "Determines the interval, in seconds, during which the computer is kept awake.")
@@ -132,34 +130,45 @@ namespace Awake
{
Arity = ArgumentArity.ExactlyOne,
},
Required = false,
};
timeOption.Required = false;
var pidOption = new Option<int>(
Option<int> pidOption = new(
aliases: new[] { "--pid", "-p" },
getDefaultValue: () => 0,
description: $"Bind the execution of {InternalConstants.AppName} to another process.")
description: $"Bind the execution of {InternalConstants.AppName} to another process. When the process ends, the system will resume managing the current sleep/display mode.")
{
Argument = new Argument<int>(() => 0)
{
Arity = ArgumentArity.ZeroOrOne,
},
Required = false,
};
pidOption.Required = false;
Option<string> expireAtOption = new(
aliases: new[] { "--expire-at", "-e" },
getDefaultValue: () => string.Empty,
description: $"Determines the end date/time when {InternalConstants.AppName} will back off and let the system manage the current sleep/display mode.")
{
Argument = new Argument<string>(() => string.Empty)
{
Arity = ArgumentArity.ZeroOrOne,
},
Required = false,
};
RootCommand? rootCommand = new RootCommand
RootCommand? rootCommand = new()
{
configOption,
displayOption,
timeOption,
pidOption,
expireAtOption,
};
rootCommand.Description = InternalConstants.AppName;
rootCommand.Handler = CommandHandler.Create<bool, bool, uint, int>(HandleCommandLineArguments);
rootCommand.Handler = CommandHandler.Create<bool, bool, uint, int, string>(HandleCommandLineArguments);
_log.Info("Parameter setup complete. Proceeding to the rest of the app initiation...");
@@ -180,7 +189,7 @@ namespace Awake
APIHelper.CompleteExit(exitCode, exitSignal, force);
}
private static void HandleCommandLineArguments(bool usePtConfig, bool displayOn, uint timeLimit, int pid)
private static void HandleCommandLineArguments(bool usePtConfig, bool displayOn, uint timeLimit, int pid, string expireAt)
{
_handler += ExitHandler;
APIHelper.SetConsoleControlHandler(_handler, true);
@@ -199,6 +208,7 @@ namespace Awake
_log.Info($"The value for --display-on is: {displayOn}");
_log.Info($"The value for --time-limit is: {timeLimit}");
_log.Info($"The value for --pid is: {pid}");
_log.Info($"The value for --expire is: {expireAt}");
if (usePtConfig)
{
@@ -214,41 +224,21 @@ namespace Awake
Exit("Received a signal to end the process. Making sure we quit...", 0, _exitSignal, true);
}
}).Start();
TrayHelper.InitializeTray(InternalConstants.FullAppName, new Icon("modules/awake/images/awake.ico"), _exitSignal);
TrayHelper.InitializeTray(InternalConstants.FullAppName, new Icon(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "images/awake.ico")), _exitSignal);
string? settingsPath = _settingsUtils.GetSettingsFilePath(InternalConstants.AppName);
_log.Info($"Reading configuration file: {settingsPath}");
_watcher = new FileSystemWatcher
if (!File.Exists(settingsPath))
{
#pragma warning disable CS8601 // Possible null reference assignment.
Path = Path.GetDirectoryName(settingsPath),
#pragma warning restore CS8601 // Possible null reference assignment.
EnableRaisingEvents = true,
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.CreationTime,
Filter = Path.GetFileName(settingsPath),
};
string? errorString = $"The settings file does not exist. Scaffolding default configuration...";
IObservable<System.Reactive.EventPattern<FileSystemEventArgs>>? changedObservable = Observable.FromEventPattern<FileSystemEventHandler, FileSystemEventArgs>(
h => _watcher.Changed += h,
h => _watcher.Changed -= h);
AwakeSettings scaffoldSettings = new AwakeSettings();
_settingsUtils.SaveSettings(JsonSerializer.Serialize(scaffoldSettings), InternalConstants.AppName);
}
IObservable<System.Reactive.EventPattern<FileSystemEventArgs>>? createdObservable = Observable.FromEventPattern<FileSystemEventHandler, FileSystemEventArgs>(
cre => _watcher.Created += cre,
cre => _watcher.Created -= cre);
IObservable<System.Reactive.EventPattern<FileSystemEventArgs>>? mergedObservable = Observable.Merge(changedObservable, createdObservable);
mergedObservable.Throttle(TimeSpan.FromMilliseconds(25))
.SubscribeOn(TaskPoolScheduler.Default)
.Select(e => e.EventArgs)
.Subscribe(HandleAwakeConfigChange);
TrayHelper.SetTray(InternalConstants.FullAppName, new AwakeSettings(), _startedFromPowerToys);
// Initially the file might not be updated, so we need to start processing
// settings right away.
ProcessSettings();
ScaffoldConfiguration(settingsPath);
}
catch (Exception ex)
{
@@ -259,15 +249,43 @@ namespace Awake
}
else
{
AwakeMode mode = timeLimit <= 0 ? AwakeMode.INDEFINITE : AwakeMode.TIMED;
if (mode == AwakeMode.INDEFINITE)
// Date-based binding takes precedence over timed configuration, so we want to
// check for that first.
if (!string.IsNullOrWhiteSpace(expireAt))
{
SetupIndefiniteKeepAwake(displayOn);
try
{
DateTime expirationDateTime = DateTime.Parse(expireAt);
if (expirationDateTime > DateTime.Now)
{
// We want to have a dedicated expirable keep-awake logic instead of
// converting the target date to seconds and then passing to SetupTimedKeepAwake
// because that way we're accounting for the user potentially changing their clock
// while Awake is running.
SetupExpirableKeepAwake(expirationDateTime, displayOn);
}
else
{
_log.Info($"Target date is not in the future, therefore there is nothing to wait for.");
}
}
catch
{
_log.Error($"Could not parse date string {expireAt} into a viable date.");
}
}
else
{
SetupTimedKeepAwake(timeLimit, displayOn);
AwakeMode mode = timeLimit <= 0 ? AwakeMode.INDEFINITE : AwakeMode.TIMED;
if (mode == AwakeMode.INDEFINITE)
{
SetupIndefiniteKeepAwake(displayOn);
}
else
{
SetupTimedKeepAwake(timeLimit, displayOn);
}
}
}
@@ -283,6 +301,45 @@ namespace Awake
_exitSignal.WaitOne();
}
private static void ScaffoldConfiguration(string settingsPath)
{
try
{
_watcher = new FileSystemWatcher
{
Path = Path.GetDirectoryName(settingsPath)!,
EnableRaisingEvents = true,
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.CreationTime,
Filter = Path.GetFileName(settingsPath),
};
IObservable<System.Reactive.EventPattern<FileSystemEventArgs>>? changedObservable = Observable.FromEventPattern<FileSystemEventHandler, FileSystemEventArgs>(
h => _watcher.Changed += h,
h => _watcher.Changed -= h);
IObservable<System.Reactive.EventPattern<FileSystemEventArgs>>? createdObservable = Observable.FromEventPattern<FileSystemEventHandler, FileSystemEventArgs>(
cre => _watcher.Created += cre,
cre => _watcher.Created -= cre);
IObservable<System.Reactive.EventPattern<FileSystemEventArgs>>? mergedObservable = Observable.Merge(changedObservable, createdObservable);
mergedObservable.Throttle(TimeSpan.FromMilliseconds(25))
.SubscribeOn(TaskPoolScheduler.Default)
.Select(e => e.EventArgs)
.Subscribe(HandleAwakeConfigChange);
TrayHelper.SetTray(InternalConstants.FullAppName, new AwakeSettings(), _startedFromPowerToys);
// Initially the file might not be updated, so we need to start processing
// settings right away.
ProcessSettings();
}
catch (Exception ex)
{
_log.Error($"An error occurred scaffolding the configuration. Error details: {ex.Message}");
}
}
private static void SetupIndefiniteKeepAwake(bool displayOn)
{
APIHelper.SetIndefiniteKeepAwake(LogCompletedKeepAwakeThread, LogUnexpectedOrCancelledKeepAwakeThreadCompletion, displayOn);
@@ -303,7 +360,7 @@ namespace Awake
if (settings != null)
{
_log.Info($"Identified custom time shortcuts for the tray: {settings.Properties.TrayTimeShortcuts.Count}");
_log.Info($"Identified custom time shortcuts for the tray: {settings.Properties.CustomTrayTimes.Count}");
switch (settings.Properties.Mode)
{
@@ -321,12 +378,19 @@ namespace Awake
case AwakeMode.TIMED:
{
uint computedTime = (settings.Properties.Hours * 60 * 60) + (settings.Properties.Minutes * 60);
uint computedTime = (settings.Properties.IntervalHours * 60 * 60) + (settings.Properties.IntervalMinutes * 60);
SetupTimedKeepAwake(computedTime, settings.Properties.KeepDisplayOn);
break;
}
case AwakeMode.EXPIRABLE:
{
SetupExpirableKeepAwake(settings.Properties.ExpirationDateTime, settings.Properties.KeepDisplayOn);
break;
}
default:
{
string? errorMessage = "Unknown mode of operation. Check config file.";
@@ -360,6 +424,13 @@ namespace Awake
APIHelper.SetNoKeepAwake();
}
private static void SetupExpirableKeepAwake(DateTimeOffset expireAt, bool displayOn)
{
_log.Info($"Expirable keep-awake. Expected expiration date/time: {expireAt} with display on setting set to {displayOn}.");
APIHelper.SetExpirableKeepAwake(expireAt, LogCompletedKeepAwakeThread, LogUnexpectedOrCancelledKeepAwakeThreadCompletion, displayOn);
}
private static void SetupTimedKeepAwake(uint time, bool displayOn)
{
_log.Info($"Timed keep-awake. Expected runtime: {time} seconds with display on setting set to {displayOn}.");
@@ -369,14 +440,14 @@ namespace Awake
private static void LogUnexpectedOrCancelledKeepAwakeThreadCompletion()
{
string? errorMessage = "The keep-awake thread was terminated early.";
string? errorMessage = "The keep awake thread was terminated early.";
_log.Info(errorMessage);
_log.Debug(errorMessage);
}
private static void LogCompletedKeepAwakeThread(bool result)
private static void LogCompletedKeepAwakeThread()
{
_log.Info($"Exited keep-awake thread successfully: {result}");
_log.Info($"Exited keep awake thread successfully.");
}
}
}

View File

@@ -0,0 +1,16 @@
// 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.Diagnostics.Tracing;
using Microsoft.PowerToys.Telemetry;
using Microsoft.PowerToys.Telemetry.Events;
namespace Awake.Telemetry
{
[EventData]
public class AwakeExpirableKeepAwakeEvent : EventBase, IEvent
{
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
}
}

View File

@@ -0,0 +1,16 @@
// 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.Diagnostics.Tracing;
using Microsoft.PowerToys.Telemetry;
using Microsoft.PowerToys.Telemetry.Events;
namespace Awake.Telemetry
{
[EventData]
internal class AwakeNoKeepAwakeEvent : EventBase, IEvent
{
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
}
}

View File

@@ -2,6 +2,7 @@
// 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.Text.Json.Serialization;
@@ -13,25 +14,29 @@ namespace Microsoft.PowerToys.Settings.UI.Library
{
KeepDisplayOn = false;
Mode = AwakeMode.PASSIVE;
Hours = 0;
Minutes = 0;
TrayTimeShortcuts = new Dictionary<string, int>();
IntervalHours = 0;
IntervalMinutes = 0;
ExpirationDateTime = DateTimeOffset.MinValue;
CustomTrayTimes = new Dictionary<string, int>();
}
[JsonPropertyName("awake_keep_display_on")]
[JsonPropertyName("keepDisplayOn")]
public bool KeepDisplayOn { get; set; }
[JsonPropertyName("awake_mode")]
[JsonPropertyName("mode")]
public AwakeMode Mode { get; set; }
[JsonPropertyName("awake_hours")]
public uint Hours { get; set; }
[JsonPropertyName("intervalHours")]
public uint IntervalHours { get; set; }
[JsonPropertyName("awake_minutes")]
public uint Minutes { get; set; }
[JsonPropertyName("intervalMinutes")]
public uint IntervalMinutes { get; set; }
[JsonPropertyName("tray_times")]
public Dictionary<string, int> TrayTimeShortcuts { get; set; }
[JsonPropertyName("expirationDateTime")]
public DateTimeOffset ExpirationDateTime { get; set; }
[JsonPropertyName("customTrayTimes")]
public Dictionary<string, int> CustomTrayTimes { get; set; }
}
public enum AwakeMode
@@ -39,5 +44,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
PASSIVE = 0,
INDEFINITE = 1,
TIMED = 2,
EXPIRABLE = 3,
}
}

View File

@@ -2,15 +2,17 @@
// 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.Linq;
using System.Text.Json.Serialization;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library
{
public class AwakeSettings : BasePTModuleSettings, ISettingsConfig
public class AwakeSettings : BasePTModuleSettings, ISettingsConfig, ICloneable
{
public const string ModuleName = "Awake";
public const string ModuleVersion = "0.0.1";
public const string ModuleVersion = "0.0.2";
public AwakeSettings()
{
@@ -22,6 +24,24 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonPropertyName("properties")]
public AwakeProperties Properties { get; set; }
public object Clone()
{
return new AwakeSettings()
{
Name = Name,
Version = Version,
Properties = new AwakeProperties()
{
CustomTrayTimes = Properties.CustomTrayTimes.ToDictionary(entry => entry.Key, entry => entry.Value),
Mode = Properties.Mode,
KeepDisplayOn = Properties.KeepDisplayOn,
IntervalMinutes = Properties.IntervalMinutes,
IntervalHours = Properties.IntervalHours,
ExpirationDateTime = Properties.ExpirationDateTime,
},
};
}
public string GetModuleName()
{
return Name;

View File

@@ -7,5 +7,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library.Interfaces
public interface ISettingsRepository<T>
{
T SettingsConfig { get; set; }
bool ReloadSettings();
}
}

View File

@@ -44,6 +44,23 @@ namespace Microsoft.PowerToys.Settings.UI.Library
{
}
public bool ReloadSettings()
{
try
{
T settingsItem = new T();
settingsConfig = _settingsUtils.GetSettingsOrDefault<T>(settingsItem.GetModuleName());
SettingsConfig = settingsConfig;
return true;
}
catch
{
return false;
}
}
// Settings configurations shared across all viewmodels
public T SettingsConfig
{

View File

@@ -48,6 +48,23 @@ namespace Microsoft.PowerToys.Settings.UI.UnitTests.BackwardsCompatibility
}
}
}
public bool ReloadSettings()
{
try
{
T settingsItem = new T();
_settingsConfig = _settingsUtils.GetSettingsOrDefault<T>(settingsItem.GetModuleName());
SettingsConfig = _settingsConfig;
return true;
}
catch
{
return false;
}
}
}
public static Mock<IFile> GetModuleIOProvider(string version, string module, string fileName)

View File

@@ -1812,35 +1812,41 @@ From there, simply click on one of the supported files in the File Explorer and
<data name="Awake.ModuleDescription" xml:space="preserve">
<value>A convenient way to keep your PC awake on-demand.</value>
</data>
<data name="Awake_EnableAwake.Header" xml:space="preserve">
<data name="Awake_EnableSettingsCard.Header" xml:space="preserve">
<value>Enable Awake</value>
<comment>Awake is a product name, do not loc</comment>
</data>
<data name="Awake_NoKeepAwake.Content" xml:space="preserve">
<data name="Awake_NoKeepAwakeSelector.Content" xml:space="preserve">
<value>Keep using the selected power plan</value>
</data>
<data name="Awake_IndefiniteKeepAwake.Content" xml:space="preserve">
<data name="Awake_IndefiniteKeepAwakeSelector.Content" xml:space="preserve">
<value>Keep awake indefinitely</value>
</data>
<data name="Awake_TemporaryKeepAwake.Content" xml:space="preserve">
<value>Keep awake temporarily</value>
<data name="Awake_TemporaryKeepAwakeSelector.Content" xml:space="preserve">
<value>Keep awake for a time interval</value>
</data>
<data name="Awake_EnableDisplayKeepAwake.Header" xml:space="preserve">
<data name="Awake_ExpirableKeepAwakeSelector.Content" xml:space="preserve">
<value>Keep awake until expiration</value>
</data>
<data name="Awake_DisplaySettingsCard.Header" xml:space="preserve">
<value>Keep screen on</value>
</data>
<data name="Awake_EnableDisplayKeepAwake.Description" xml:space="preserve">
<data name="Awake_DisplaySettingsCard.Description" xml:space="preserve">
<value>This setting is only available when keeping the PC awake</value>
</data>
<data name="Awake_Mode.Header" xml:space="preserve">
<data name="Awake_ExpirationSettingsCard.Description" xml:space="preserve">
<value>Keep custom awakeness state until a specific date and time</value>
</data>
<data name="Awake_ModeSettingsCard.Header" xml:space="preserve">
<value>Mode</value>
</data>
<data name="Awake_Behavior_GroupSettings.Header" xml:space="preserve">
<data name="Awake_BehaviorSettingsGroup.Header" xml:space="preserve">
<value>Behavior</value>
</data>
<data name="Awake_TemporaryKeepAwake_Hours.Header" xml:space="preserve">
<data name="Awake_IntervalHoursInput.Header" xml:space="preserve">
<value>Hours</value>
</data>
<data name="Awake_TemporaryKeepAwake_Minutes.Header" xml:space="preserve">
<data name="Awake_IntervalMinutesInput.Header" xml:space="preserve">
<value>Minutes</value>
</data>
<data name="Oobe_Awake.Title" xml:space="preserve">
@@ -1930,7 +1936,7 @@ From there, simply click on one of the supported files in the File Explorer and
<data name="SeeWhatsNew.Content" xml:space="preserve">
<value>See what's new</value>
</data>
<data name="Awake_Mode.Description" xml:space="preserve">
<data name="Awake_ModeSettingsCard.Description" xml:space="preserve">
<value>Manage the state of your device when Awake is active</value>
</data>
<data name="ExcludedApps.Header" xml:space="preserve">
@@ -2120,8 +2126,11 @@ From there, simply click on one of the supported files in the File Explorer and
<value>New size</value>
<comment>First part of the default name of new sizes that can be added in PT's settings ui.</comment>
</data>
<data name="Awake_TimeBeforeAwake.Header" xml:space="preserve">
<value>Time before returning to the previous awakeness state</value>
<data name="Awake_IntervalSettingsCard.Header" xml:space="preserve">
<value>Interval before returning to the previous awakeness state</value>
</data>
<data name="Awake_ExpirationSettingsCard.Header" xml:space="preserve">
<value>End date and time</value>
</data>
<data name="MouseUtils.ModuleTitle" xml:space="preserve">
<value>Mouse utilities</value>
@@ -3056,4 +3065,4 @@ Activate by holding the key for the character you want to add an accent to, then
<data name="Hosts_Toggle_LoopbackDuplicates.Header" xml:space="preserve">
<value>Consider loopback addresses as duplicates</value>
</data>
</root>
</root>

View File

@@ -4,62 +4,29 @@
using System;
using System.Runtime.CompilerServices;
using global::PowerToys.GPOWrapper;
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.Library.Utilities;
namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
public class AwakeViewModel : Observable
{
private GeneralSettings GeneralSettingsConfig { get; set; }
private AwakeSettings Settings { get; set; }
private Func<string, int> SendConfigMSG { get; }
public AwakeViewModel(ISettingsRepository<GeneralSettings> settingsRepository, ISettingsRepository<AwakeSettings> moduleSettingsRepository, Func<string, int> ipcMSGCallBackFunc)
public AwakeViewModel()
{
// To obtain the general settings configurations of PowerToys Settings.
if (settingsRepository == null)
{
throw new ArgumentNullException(nameof(settingsRepository));
}
GeneralSettingsConfig = settingsRepository.SettingsConfig;
// To obtain the settings configurations of Fancy zones.
if (moduleSettingsRepository == null)
{
throw new ArgumentNullException(nameof(moduleSettingsRepository));
}
Settings = moduleSettingsRepository.SettingsConfig;
InitializeEnabledValue();
_keepDisplayOn = Settings.Properties.KeepDisplayOn;
_mode = Settings.Properties.Mode;
_hours = Settings.Properties.Hours;
_minutes = Settings.Properties.Minutes;
// set the callback functions value to hangle outgoing IPC message.
SendConfigMSG = ipcMSGCallBackFunc;
}
private void InitializeEnabledValue()
public AwakeSettings ModuleSettings
{
_enabledGpoRuleConfiguration = GPOWrapper.GetConfiguredAwakeEnabledValue();
if (_enabledGpoRuleConfiguration == GpoRuleConfigured.Disabled || _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled)
get => _moduleSettings;
set
{
// Get the enabled state from GPO.
_enabledStateIsGPOConfigured = true;
_isEnabled = _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled;
}
else
{
_isEnabled = GeneralSettingsConfig.Enabled.Awake;
if (_moduleSettings != value)
{
_moduleSettings = value;
RefreshModuleSettings();
RefreshEnabledState();
}
}
}
@@ -78,13 +45,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
_isEnabled = value;
GeneralSettingsConfig.Enabled.Awake = value;
OnPropertyChanged(nameof(IsEnabled));
OnPropertyChanged(nameof(IsTimeConfigurationEnabled));
OnPropertyChanged(nameof(IsScreenConfigurationPossibleEnabled));
RefreshEnabledState();
OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(GeneralSettingsConfig);
SendConfigMSG(outgoing.ToString());
NotifyPropertyChanged();
}
}
@@ -93,31 +55,44 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
public bool IsEnabledGpoConfigured
{
get => _enabledStateIsGPOConfigured;
set
{
if (_enabledStateIsGPOConfigured != value)
{
_enabledStateIsGPOConfigured = value;
NotifyPropertyChanged();
}
}
}
public bool IsExpirationConfigurationEnabled
{
get => ModuleSettings.Properties.Mode == AwakeMode.EXPIRABLE && IsEnabled;
}
public bool IsTimeConfigurationEnabled
{
get => _mode == AwakeMode.TIMED && _isEnabled;
get => ModuleSettings.Properties.Mode == AwakeMode.TIMED && IsEnabled;
}
public bool IsScreenConfigurationPossibleEnabled
{
get => _mode != AwakeMode.PASSIVE && _isEnabled;
get => ModuleSettings.Properties.Mode != AwakeMode.PASSIVE && IsEnabled;
}
public AwakeMode Mode
{
get => _mode;
get => ModuleSettings.Properties.Mode;
set
{
if (_mode != value)
if (ModuleSettings.Properties.Mode != value)
{
_mode = value;
OnPropertyChanged(nameof(Mode));
ModuleSettings.Properties.Mode = value;
OnPropertyChanged(nameof(IsTimeConfigurationEnabled));
OnPropertyChanged(nameof(IsScreenConfigurationPossibleEnabled));
OnPropertyChanged(nameof(IsExpirationConfigurationEnabled));
Settings.Properties.Mode = value;
NotifyPropertyChanged();
}
}
@@ -125,79 +100,93 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
public bool KeepDisplayOn
{
get => _keepDisplayOn;
get => ModuleSettings.Properties.KeepDisplayOn;
set
{
if (_keepDisplayOn != value)
if (ModuleSettings.Properties.KeepDisplayOn != value)
{
_keepDisplayOn = value;
OnPropertyChanged(nameof(KeepDisplayOn));
Settings.Properties.KeepDisplayOn = value;
ModuleSettings.Properties.KeepDisplayOn = value;
NotifyPropertyChanged();
}
}
}
public uint Hours
public uint IntervalHours
{
get => _hours;
get => ModuleSettings.Properties.IntervalHours;
set
{
if (_hours != value)
if (ModuleSettings.Properties.IntervalHours != value)
{
_hours = value;
OnPropertyChanged(nameof(Hours));
Settings.Properties.Hours = value;
ModuleSettings.Properties.IntervalHours = value;
NotifyPropertyChanged();
}
}
}
public uint Minutes
public uint IntervalMinutes
{
get => _minutes;
get => ModuleSettings.Properties.IntervalMinutes;
set
{
if (_minutes != value)
if (ModuleSettings.Properties.IntervalMinutes != value)
{
_minutes = value;
OnPropertyChanged(nameof(Minutes));
Settings.Properties.Minutes = value;
ModuleSettings.Properties.IntervalMinutes = value;
NotifyPropertyChanged();
}
}
}
public DateTimeOffset ExpirationDateTime
{
get => ModuleSettings.Properties.ExpirationDateTime;
set
{
if (ModuleSettings.Properties.ExpirationDateTime != value)
{
ModuleSettings.Properties.ExpirationDateTime = value;
NotifyPropertyChanged();
}
}
}
public TimeSpan ExpirationTime
{
get => ExpirationDateTime.TimeOfDay;
set
{
if (ExpirationDateTime.TimeOfDay != value)
{
ExpirationDateTime = new DateTime(ExpirationDateTime.Year, ExpirationDateTime.Month, ExpirationDateTime.Day, value.Hours, value.Minutes, value.Seconds);
}
}
}
public void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
{
Logger.LogInfo($"Changed the property {propertyName}");
OnPropertyChanged(propertyName);
if (SendConfigMSG != null)
{
SndAwakeSettings outsettings = new SndAwakeSettings(Settings);
SndModuleSettings<SndAwakeSettings> ipcMessage = new SndModuleSettings<SndAwakeSettings>(outsettings);
string targetMessage = ipcMessage.ToJsonString();
SendConfigMSG(targetMessage);
}
}
public void RefreshEnabledState()
{
InitializeEnabledValue();
OnPropertyChanged(nameof(IsEnabled));
OnPropertyChanged(nameof(IsTimeConfigurationEnabled));
OnPropertyChanged(nameof(IsScreenConfigurationPossibleEnabled));
OnPropertyChanged(nameof(IsExpirationConfigurationEnabled));
}
public void RefreshModuleSettings()
{
OnPropertyChanged(nameof(Mode));
OnPropertyChanged(nameof(KeepDisplayOn));
OnPropertyChanged(nameof(IntervalHours));
OnPropertyChanged(nameof(IntervalMinutes));
OnPropertyChanged(nameof(ExpirationDateTime));
}
private GpoRuleConfigured _enabledGpoRuleConfiguration;
private bool _enabledStateIsGPOConfigured;
private AwakeSettings _moduleSettings;
private bool _isEnabled;
private uint _hours;
private uint _minutes;
private bool _keepDisplayOn;
private AwakeMode _mode;
}
}

View File

@@ -8,6 +8,8 @@
xmlns:labs="using:CommunityToolkit.Labs.WinUI"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:CommunityToolkit.WinUI.UI"
xmlns:viewmodels="using:Microsoft.PowerToys.Settings.UI.ViewModels"
d:DataContext="{d:DesignInstance Type=viewmodels:AwakeViewModel}"
AutomationProperties.LandmarkType="Main"
mc:Ignorable="d">
@@ -22,7 +24,7 @@
<controls:SettingsPageControl.ModuleContent>
<StackPanel Orientation="Vertical" ChildrenTransitions="{StaticResource SettingsCardsAnimations}">
<labs:SettingsCard
x:Uid="Awake_EnableAwake"
x:Uid="Awake_EnableSettingsCard"
HeaderIcon="{ui:BitmapIcon Source=/Assets/FluentIcons/FluentIconsAwake.png}"
IsEnabled="{x:Bind Mode=OneWay, Path=ViewModel.IsEnabledGpoConfigured, Converter={StaticResource BoolNegationConverter}}">
<ToggleSwitch
@@ -37,37 +39,48 @@
Severity="Informational" />
<controls:SettingsGroup
x:Uid="Awake_Behavior_GroupSettings"
x:Uid="Awake_BehaviorSettingsGroup"
IsEnabled="{x:Bind Mode=OneWay, Path=ViewModel.IsEnabled}">
<labs:SettingsCard
x:Uid="Awake_Mode"
x:Uid="Awake_ModeSettingsCard"
HeaderIcon="{ui:FontIcon FontFamily={StaticResource SymbolThemeFontFamily}, Glyph=&#xE945;}">
<ComboBox
MinWidth="{StaticResource SettingActionControlMinWidth}"
SelectedIndex="{x:Bind Path=ViewModel.Mode, Mode=TwoWay, Converter={StaticResource AwakeModeToIntConverter}}">
<ComboBoxItem x:Uid="Awake_NoKeepAwake" />
<ComboBoxItem x:Uid="Awake_IndefiniteKeepAwake" />
<ComboBoxItem x:Uid="Awake_TemporaryKeepAwake" />
<ComboBoxItem x:Uid="Awake_NoKeepAwakeSelector" />
<ComboBoxItem x:Uid="Awake_IndefiniteKeepAwakeSelector" />
<ComboBoxItem x:Uid="Awake_TemporaryKeepAwakeSelector" />
<ComboBoxItem x:Uid="Awake_ExpirableKeepAwakeSelector" />
</ComboBox>
</labs:SettingsCard>
<labs:SettingsCard
x:Uid="Awake_TimeBeforeAwake"
x:Uid="Awake_ExpirationSettingsCard"
HeaderIcon="{ui:FontIcon FontFamily={StaticResource SymbolThemeFontFamily}, Glyph=&#xEC92;}"
Visibility="{x:Bind ViewModel.IsExpirationConfigurationEnabled, Mode=OneWay}">
<StackPanel Orientation="Horizontal">
<DatePicker Date="{x:Bind ViewModel.ExpirationDateTime, Mode=TwoWay}"></DatePicker>
<TimePicker Margin="8,0,0,0" Time="{x:Bind ViewModel.ExpirationTime, Mode=TwoWay}" ClockIdentifier="24HourClock"></TimePicker>
</StackPanel>
</labs:SettingsCard>
<labs:SettingsCard
x:Uid="Awake_IntervalSettingsCard"
HeaderIcon="{ui:FontIcon FontFamily={StaticResource SymbolThemeFontFamily}, Glyph=&#xE916;}"
Visibility="{x:Bind ViewModel.IsTimeConfigurationEnabled, Mode=OneWay}">
<StackPanel Orientation="Horizontal">
<NumberBox
x:Uid="Awake_TemporaryKeepAwake_Hours"
x:Uid="Awake_IntervalHoursInput"
Width="96"
HorizontalAlignment="Left"
LargeChange="5"
Minimum="0"
SmallChange="1"
SpinButtonPlacementMode="Compact"
Value="{x:Bind ViewModel.Hours, Mode=TwoWay}" />
Value="{x:Bind ViewModel.IntervalHours, Mode=TwoWay}" />
<NumberBox
x:Uid="Awake_TemporaryKeepAwake_Minutes"
x:Uid="Awake_IntervalMinutesInput"
Width="96"
Margin="8,0,0,0"
HorizontalAlignment="Left"
@@ -76,16 +89,15 @@
Minimum="0"
SmallChange="1"
SpinButtonPlacementMode="Compact"
Value="{x:Bind ViewModel.Minutes, Mode=TwoWay}" />
Value="{x:Bind ViewModel.IntervalMinutes, Mode=TwoWay}" />
</StackPanel>
</labs:SettingsCard>
<labs:SettingsCard
x:Uid="Awake_EnableDisplayKeepAwake"
HeaderIcon="{ui:FontIcon FontFamily={StaticResource SymbolThemeFontFamily}, Glyph=&#xE7FB;}"
x:Uid="Awake_DisplaySettingsCard"
HeaderIcon="{ui:FontIcon FontFamily={StaticResource SymbolThemeFontFamily}, Glyph=&#xE7F4;}"
IsEnabled="{x:Bind ViewModel.IsScreenConfigurationPossibleEnabled, Mode=OneWay}">
<ToggleSwitch
x:Uid="ToggleSwitch"
IsOn="{x:Bind ViewModel.KeepDisplayOn, Mode=TwoWay}" />
</labs:SettingsCard>
</controls:SettingsGroup>
@@ -99,7 +111,7 @@
</controls:SettingsPageControl.PrimaryLinks>
<controls:SettingsPageControl.SecondaryLinks>
<controls:PageLink
Link="https://Awake.den.dev"
Link="https://awake.den.dev"
Text="Den Delimarsky's work on creating Awake" />
</controls:SettingsPageControl.SecondaryLinks>
</controls:SettingsPageControl>

View File

@@ -2,25 +2,184 @@
// 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.IO;
using System.IO.Abstractions;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
using Microsoft.PowerToys.Settings.UI.Library.Utilities;
using Microsoft.PowerToys.Settings.UI.ViewModels;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml.Controls;
using PowerToys.GPOWrapper;
namespace Microsoft.PowerToys.Settings.UI.Views
{
public sealed partial class AwakePage : Page, IRefreshablePage
{
private readonly string _appName = "Awake";
private readonly SettingsUtils _settingsUtils;
private readonly SettingsRepository<GeneralSettings> _generalSettingsRepository;
private readonly SettingsRepository<AwakeSettings> _moduleSettingsRepository;
private readonly IFileSystem _fileSystem;
private readonly IFileSystemWatcher _fileSystemWatcher;
private readonly DispatcherQueue _dispatcherQueue;
private readonly Func<string, int> _sendConfigMsg;
private AwakeViewModel ViewModel { get; set; }
public AwakePage()
{
var settingsUtils = new SettingsUtils();
ViewModel = new AwakeViewModel(SettingsRepository<GeneralSettings>.GetInstance(settingsUtils), SettingsRepository<AwakeSettings>.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage);
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
_fileSystem = new FileSystem();
_settingsUtils = new SettingsUtils();
_sendConfigMsg = ShellPage.SendDefaultIPCMessage;
ViewModel = new AwakeViewModel();
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
_generalSettingsRepository = SettingsRepository<GeneralSettings>.GetInstance(_settingsUtils);
_moduleSettingsRepository = SettingsRepository<AwakeSettings>.GetInstance(_settingsUtils);
// We load the view model settings first.
LoadSettings(_generalSettingsRepository, _moduleSettingsRepository);
DataContext = ViewModel;
var settingsPath = _settingsUtils.GetSettingsFilePath(_appName);
_fileSystemWatcher = _fileSystem.FileSystemWatcher.CreateNew();
_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;
InitializeComponent();
}
/// <summary>
/// Triggered whenever a view model property changes. This is done in addition to the baked-in view model changes.
/// </summary>
/// <remarks>
/// TODO: The logic here needs to be optimized since doing string comparison on values is not ideal.
/// </remarks>
/// <param name="sender">Sender of the change.</param>
/// <param name="e">Property parameter.</param>
private void ViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (_sendConfigMsg != null)
{
if (e.PropertyName == "IsEnabled")
{
if (ViewModel.IsEnabled != _generalSettingsRepository.SettingsConfig.Enabled.Awake)
{
_generalSettingsRepository.SettingsConfig.Enabled.Awake = ViewModel.IsEnabled;
var generalSettingsMessage = new OutGoingGeneralSettings(_generalSettingsRepository.SettingsConfig).ToString();
Logger.LogInfo($"Saved general settings from Awake page.");
_sendConfigMsg(generalSettingsMessage);
}
}
else
{
if (ViewModel.ModuleSettings != null)
{
SndAwakeSettings currentSettings = new(_moduleSettingsRepository.SettingsConfig);
SndModuleSettings<SndAwakeSettings> csIpcMessage = new(currentSettings);
SndAwakeSettings outSettings = new(ViewModel.ModuleSettings);
SndModuleSettings<SndAwakeSettings> outIpcMessage = new(outSettings);
string csMessage = csIpcMessage.ToJsonString();
string outMessage = outIpcMessage.ToJsonString();
if (!csMessage.Equals(outMessage))
{
Logger.LogInfo($"Saved Awake settings from Awake page.");
_sendConfigMsg(outMessage);
}
}
}
}
}
private void LoadSettings(ISettingsRepository<GeneralSettings> generalSettingsRepository, ISettingsRepository<AwakeSettings> 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(AwakeSettings awakeSettings, GeneralSettings generalSettings)
{
if (awakeSettings != null)
{
if (generalSettings != null)
{
ViewModel.IsEnabled = generalSettings.Enabled.Awake;
ViewModel.ModuleSettings = (AwakeSettings)awakeSettings.Clone();
UpdateEnabledState(generalSettings.Enabled.Awake);
}
else
{
throw new ArgumentNullException(nameof(generalSettings));
}
}
else
{
throw new ArgumentNullException(nameof(awakeSettings));
}
}
/// <summary>
/// Updates the tool enablement state.
/// </summary>
/// <param name="recommendedState">The state that is recommended for the tool, but can be overridden if a GPO policy is in place.</param>
private void UpdateEnabledState(bool recommendedState)
{
var enabledGpoRuleConfiguration = GPOWrapper.GetConfiguredAwakeEnabledValue();
if (enabledGpoRuleConfiguration == GpoRuleConfigured.Disabled || enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled)
{
// Get the enabled state from GPO.
ViewModel.IsEnabledGpoConfigured = true;
ViewModel.IsEnabled = enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled;
}
else
{
ViewModel.IsEnabled = recommendedState;
}
}
private void Settings_Changed(object sender, FileSystemEventArgs e)
{
bool taskAdded = _dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Normal, () =>
{
_moduleSettingsRepository.ReloadSettings();
LoadSettings(_generalSettingsRepository, _moduleSettingsRepository);
});
}
public void RefreshEnabledState()
{
ViewModel.RefreshEnabledState();