[Awake]Fix DAISY build issues (#34054)

This PR addresses some post-merge issues caught by @davidegiacometti,
including:

1. Separator in the context menu shown when not running from inside
PowerToys.
2. "Keep display on" setting not persisting across switches between
modes.
3. Awake not launching in standalone mode.

Additionally:

1. Exits are now properly handled in **timed** and **expirable**
keep-awake modes when running standalone. This ensures that Awake exists
after completion and doesn't switch to an in-actionable passive mode.
2. Tray tooltips now cover how much time is left on the timer.
3. Fixes #29354
4. Avoids a nasty memory leak because of re-instantiating of `Icon`
objects for every tray update.
5. Adds DPI awareness to the context menu (#16123)
This commit is contained in:
Den Delimarsky 2024-07-30 08:08:37 -07:00 committed by GitHub
parent 5d77874382
commit e8ad4fa804
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 255 additions and 113 deletions

View File

@ -35,6 +35,7 @@
<CsWinRTIncludes>PowerToys.GPOWrapper</CsWinRTIncludes>
<CsWinRTGeneratedFilesDir>$(OutDir)</CsWinRTGeneratedFilesDir>
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Debug'">

View File

@ -19,5 +19,17 @@ namespace Awake.Core
target.Add(element);
}
}
public static string ToHumanReadableString(this TimeSpan timeSpan)
{
// Get days, hours, minutes, and seconds from the TimeSpan
int days = timeSpan.Days;
int hours = timeSpan.Hours;
int minutes = timeSpan.Minutes;
int seconds = timeSpan.Seconds;
// Format the string based on the presence of days, hours, minutes, and seconds
return $"{days:D2}{Properties.Resources.AWAKE_LABEL_DAYS} {hours:D2}{Properties.Resources.AWAKE_LABEL_HOURS} {minutes:D2}{Properties.Resources.AWAKE_LABEL_MINUTES} {seconds:D2}{Properties.Resources.AWAKE_LABEL_SECONDS}";
}
}
}

View File

@ -39,6 +39,12 @@ namespace Awake.Core
private static readonly BlockingCollection<ExecutionState> _stateQueue;
// Core icons used for the tray
private static readonly Icon _timedIcon = new(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets/Awake/timed.ico"));
private static readonly Icon _expirableIcon = new(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets/Awake/expirable.ico"));
private static readonly Icon _indefiniteIcon = new(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets/Awake/indefinite.ico"));
private static readonly Icon _disabledIcon = new(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets/Awake/disabled.ico"));
private static CancellationTokenSource _tokenSource;
private static SettingsUtils? _moduleSettings;
@ -135,7 +141,7 @@ namespace Awake.Core
_stateQueue.Add(ComputeAwakeState(keepDisplayOn));
TrayHelper.SetShellIcon(TrayHelper.HiddenWindowHandle, $"{Constants.FullAppName} [{Resources.AWAKE_TRAY_TEXT_INDEFINITE}]", new Icon(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets/Awake/indefinite.ico")), TrayIconAction.Update);
TrayHelper.SetShellIcon(TrayHelper.HiddenWindowHandle, $"{Constants.FullAppName} [{Resources.AWAKE_TRAY_TEXT_INDEFINITE}]", _indefiniteIcon, TrayIconAction.Update);
if (IsUsingPowerToysConfig)
{
@ -172,14 +178,23 @@ namespace Awake.Core
Logger.LogInfo($"Starting expirable log for {expireAt}");
_stateQueue.Add(ComputeAwakeState(keepDisplayOn));
TrayHelper.SetShellIcon(TrayHelper.HiddenWindowHandle, $"{Constants.FullAppName} [{Resources.AWAKE_TRAY_TEXT_EXPIRATION}]", new Icon(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets/Awake/expirable.ico")), TrayIconAction.Update);
TrayHelper.SetShellIcon(TrayHelper.HiddenWindowHandle, $"{Constants.FullAppName} [{Resources.AWAKE_TRAY_TEXT_EXPIRATION} - {expireAt}]", _expirableIcon, TrayIconAction.Update);
Observable.Timer(expireAt - DateTimeOffset.Now).Subscribe(
_ =>
{
Logger.LogInfo($"Completed expirable keep-awake.");
CancelExistingThread();
SetPassiveKeepAwake();
if (IsUsingPowerToysConfig)
{
SetPassiveKeepAwake();
}
else
{
Logger.LogInfo("Exiting after expirable keep awake.");
CompleteExit(Environment.ExitCode);
}
},
_tokenSource.Token);
}
@ -224,16 +239,40 @@ namespace Awake.Core
Logger.LogInfo($"Timed keep awake started for {seconds} seconds.");
_stateQueue.Add(ComputeAwakeState(keepDisplayOn));
TrayHelper.SetShellIcon(TrayHelper.HiddenWindowHandle, $"{Constants.FullAppName} [{Resources.AWAKE_TRAY_TEXT_TIMED}]", new Icon(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets/Awake/timed.ico")), TrayIconAction.Update);
TrayHelper.SetShellIcon(TrayHelper.HiddenWindowHandle, $"{Constants.FullAppName} [{Resources.AWAKE_TRAY_TEXT_TIMED}]", _timedIcon, TrayIconAction.Update);
Observable.Timer(TimeSpan.FromSeconds(seconds)).Subscribe(
_ =>
{
Logger.LogInfo($"Completed timed thread.");
CancelExistingThread();
SetPassiveKeepAwake();
},
_tokenSource.Token);
var timerObservable = Observable.Timer(TimeSpan.FromSeconds(seconds));
var intervalObservable = Observable.Interval(TimeSpan.FromSeconds(1)).TakeUntil(timerObservable);
var combinedObservable = Observable.CombineLatest(intervalObservable, timerObservable.StartWith(0), (elapsedSeconds, _) => elapsedSeconds + 1);
combinedObservable.Subscribe(
elapsedSeconds =>
{
var timeRemaining = seconds - (uint)elapsedSeconds;
if (timeRemaining >= 0)
{
TrayHelper.SetShellIcon(TrayHelper.HiddenWindowHandle, $"{Constants.FullAppName} [{Resources.AWAKE_TRAY_TEXT_TIMED}]\n{TimeSpan.FromSeconds(timeRemaining).ToHumanReadableString()}", _timedIcon, TrayIconAction.Update);
}
},
() =>
{
Console.WriteLine("Completed timed thread.");
CancelExistingThread();
if (IsUsingPowerToysConfig)
{
// If we're using PowerToys settings, we need to make sure that
// we just switch over the Passive Keep-Awake.
SetPassiveKeepAwake();
}
else
{
Logger.LogInfo("Exiting after timed keep-awake.");
CompleteExit(Environment.ExitCode);
}
},
_tokenSource.Token);
if (IsUsingPowerToysConfig)
{
@ -264,9 +303,7 @@ namespace Awake.Core
/// Performs a clean exit from Awake.
/// </summary>
/// <param name="exitCode">Exit code to exit with.</param>
/// <param name="exitSignal">Exit signal tracking the state.</param>
/// <param name="force">Determines whether to force exit and post a quitting message.</param>
internal static void CompleteExit(int exitCode, ManualResetEvent? exitSignal, bool force = false)
internal static void CompleteExit(int exitCode)
{
SetPassiveKeepAwake(updateSettings: false);
@ -277,22 +314,12 @@ namespace Awake.Core
// Close the message window that we used for the tray.
Bridge.SendMessage(TrayHelper.HiddenWindowHandle, Native.Constants.WM_CLOSE, 0, 0);
}
if (force)
{
Bridge.PostQuitMessage(exitCode);
}
try
{
exitSignal?.Set();
Bridge.DestroyWindow(TrayHelper.HiddenWindowHandle);
}
catch (Exception ex)
{
Logger.LogError($"Exit signal error ${ex}");
}
Bridge.PostQuitMessage(exitCode);
Environment.Exit(exitCode);
}
/// <summary>
@ -350,7 +377,7 @@ namespace Awake.Core
CancelExistingThread();
TrayHelper.SetShellIcon(TrayHelper.HiddenWindowHandle, $"{Constants.FullAppName} [{Resources.AWAKE_TRAY_TEXT_OFF}]", new Icon(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets/Awake/disabled.ico")), TrayIconAction.Update);
TrayHelper.SetShellIcon(TrayHelper.HiddenWindowHandle, $"{Constants.FullAppName} [{Resources.AWAKE_TRAY_TEXT_OFF}]", _disabledIcon, TrayIconAction.Update);
if (IsUsingPowerToysConfig && updateSettings)
{

View File

@ -28,7 +28,6 @@ namespace Awake.Core
internal static class TrayHelper
{
private static NotifyIconData _notifyIconData;
private static ManualResetEvent? _exitSignal;
private static IntPtr _trayMenu;
@ -44,41 +43,49 @@ namespace Awake.Core
HiddenWindowHandle = IntPtr.Zero;
}
public static void InitializeTray(string text, Icon icon, ManualResetEvent? exitSignal)
public static void InitializeTray(string text, Icon icon)
{
_exitSignal = exitSignal;
CreateHiddenWindow(icon, text);
}
private static void ShowContextMenu(IntPtr hWnd)
{
Bridge.SetForegroundWindow(hWnd);
// Get the handle to the context menu associated with the tray icon
IntPtr hMenu = TrayMenu;
// Get the current cursor position
Bridge.GetCursorPos(out Models.Point cursorPos);
Bridge.ScreenToClient(hWnd, ref cursorPos);
MenuInfo menuInfo = new()
if (TrayMenu != IntPtr.Zero)
{
CbSize = (uint)Marshal.SizeOf(typeof(MenuInfo)),
FMask = Native.Constants.MIM_STYLE,
DwStyle = Native.Constants.MNS_AUTO_DISMISS,
};
Bridge.SetMenuInfo(hMenu, ref menuInfo);
Bridge.SetForegroundWindow(hWnd);
// Display the context menu at the cursor position
Bridge.TrackPopupMenuEx(
hMenu,
Native.Constants.TPM_LEFT_ALIGN | Native.Constants.TPM_BOTTOMALIGN | Native.Constants.TPM_LEFT_BUTTON,
cursorPos.X,
cursorPos.Y,
hWnd,
IntPtr.Zero);
// Get the handle to the context menu associated with the tray icon
IntPtr hMenu = TrayMenu;
// Get the current cursor position
Bridge.GetCursorPos(out Models.Point cursorPos);
Bridge.ScreenToClient(hWnd, ref cursorPos);
MenuInfo menuInfo = new()
{
CbSize = (uint)Marshal.SizeOf(typeof(MenuInfo)),
FMask = Native.Constants.MIM_STYLE,
DwStyle = Native.Constants.MNS_AUTO_DISMISS,
};
Bridge.SetMenuInfo(hMenu, ref menuInfo);
// Display the context menu at the cursor position
Bridge.TrackPopupMenuEx(
hMenu,
Native.Constants.TPM_LEFT_ALIGN | Native.Constants.TPM_BOTTOMALIGN | Native.Constants.TPM_LEFT_BUTTON,
cursorPos.X,
cursorPos.Y,
hWnd,
IntPtr.Zero);
}
else
{
// Tray menu was not initialized. Log the issue.
// This is normal when operating in "standalone mode" - that is, detached
// from the PowerToys configuration file.
Logger.LogError("Tried to create a context menu while the TrayMenu object is a null pointer. Normal when used in standalone mode.");
}
}
private static void CreateHiddenWindow(Icon icon, string text)
@ -159,30 +166,40 @@ namespace Awake.Core
break;
}
_notifyIconData = action == TrayIconAction.Add || action == TrayIconAction.Update
? new NotifyIconData
if (action == TrayIconAction.Add || action == TrayIconAction.Update)
{
_notifyIconData = new NotifyIconData
{
CbSize = Marshal.SizeOf(typeof(NotifyIconData)),
HWnd = hWnd,
UId = 1000,
UFlags = Native.Constants.NIF_ICON | Native.Constants.NIF_TIP | Native.Constants.NIF_MESSAGE,
UCallbackMessage = (int)Native.Constants.WM_USER,
HIcon = icon!.Handle,
HIcon = icon?.Handle ?? IntPtr.Zero,
SzTip = text,
}
: new NotifyIconData
};
}
else if (action == TrayIconAction.Delete)
{
_notifyIconData = new NotifyIconData
{
CbSize = Marshal.SizeOf(typeof(NotifyIconData)),
HWnd = hWnd,
UId = 1000,
UFlags = 0,
};
}
if (!Bridge.Shell_NotifyIcon(message, ref _notifyIconData))
{
int errorCode = Marshal.GetLastWin32Error();
throw new Win32Exception(errorCode, $"Failed to change tray icon. Action: {action} and error code: {errorCode}");
}
if (action == TrayIconAction.Delete)
{
_notifyIconData = default;
}
}
private static void RunMessageLoop()
@ -218,32 +235,47 @@ namespace Awake.Core
switch (targetCommandIndex)
{
case (uint)TrayCommands.TC_EXIT:
Manager.CompleteExit(0, _exitSignal, true);
break;
case (uint)TrayCommands.TC_DISPLAY_SETTING:
Manager.SetDisplay();
break;
case (uint)TrayCommands.TC_MODE_INDEFINITE:
Manager.SetIndefiniteKeepAwake();
break;
case (uint)TrayCommands.TC_MODE_PASSIVE:
Manager.SetPassiveKeepAwake();
break;
default:
if (targetCommandIndex >= trayCommandsSize)
{
AwakeSettings settings = Manager.ModuleSettings!.GetSettings<AwakeSettings>(Constants.AppName);
if (settings.Properties.CustomTrayTimes.Count == 0)
{
settings.Properties.CustomTrayTimes.AddRange(Manager.GetDefaultTrayOptions());
}
int index = (int)targetCommandIndex - (int)TrayCommands.TC_TIME;
uint targetTime = (uint)settings.Properties.CustomTrayTimes.ElementAt(index).Value;
Manager.SetTimedKeepAwake(targetTime);
Manager.CompleteExit(Environment.ExitCode);
break;
}
break;
case (uint)TrayCommands.TC_DISPLAY_SETTING:
{
Manager.SetDisplay();
break;
}
case (uint)TrayCommands.TC_MODE_INDEFINITE:
{
AwakeSettings settings = Manager.ModuleSettings!.GetSettings<AwakeSettings>(Constants.AppName);
Manager.SetIndefiniteKeepAwake(keepDisplayOn: settings.Properties.KeepDisplayOn);
break;
}
case (uint)TrayCommands.TC_MODE_PASSIVE:
{
Manager.SetPassiveKeepAwake();
break;
}
default:
{
if (targetCommandIndex >= trayCommandsSize)
{
AwakeSettings settings = Manager.ModuleSettings!.GetSettings<AwakeSettings>(Constants.AppName);
if (settings.Properties.CustomTrayTimes.Count == 0)
{
settings.Properties.CustomTrayTimes.AddRange(Manager.GetDefaultTrayOptions());
}
int index = (int)targetCommandIndex - (int)TrayCommands.TC_TIME;
uint targetTime = (uint)settings.Properties.CustomTrayTimes.ElementAt(index).Value;
Manager.SetTimedKeepAwake(targetTime, keepDisplayOn: settings.Properties.KeepDisplayOn);
}
break;
}
}
break;
@ -300,7 +332,7 @@ namespace Awake.Core
InsertAwakeModeMenuItems(mode);
EnsureDefaultTrayTimeShortcuts(trayTimeShortcuts);
CreateAwakeTimeSubMenu(trayTimeShortcuts);
CreateAwakeTimeSubMenu(trayTimeShortcuts, mode == AwakeMode.TIMED);
}
private static void ClearExistingTrayMenu()
@ -326,7 +358,11 @@ namespace Awake.Core
}
InsertMenuItem(0, TrayCommands.TC_DISPLAY_SETTING, Resources.AWAKE_KEEP_SCREEN_ON, keepDisplayOn, mode == AwakeMode.PASSIVE);
InsertSeparator(1);
if (!startedFromPowerToys)
{
InsertSeparator(1);
}
}
private static void InsertMenuItem(int position, TrayCommands command, string text, bool checkedState = false, bool disabled = false)
@ -351,7 +387,7 @@ namespace Awake.Core
}
}
private static void CreateAwakeTimeSubMenu(Dictionary<string, int> trayTimeShortcuts)
private static void CreateAwakeTimeSubMenu(Dictionary<string, int> trayTimeShortcuts, bool isChecked = false)
{
var awakeTimeMenu = Bridge.CreatePopupMenu();
for (int i = 0; i < trayTimeShortcuts.Count; i++)
@ -359,7 +395,7 @@ namespace Awake.Core
Bridge.InsertMenu(awakeTimeMenu, (uint)i, Native.Constants.MF_BYPOSITION | Native.Constants.MF_STRING, (uint)TrayCommands.TC_TIME + (uint)i, trayTimeShortcuts.ElementAt(i).Key);
}
Bridge.InsertMenu(TrayMenu, 0, Native.Constants.MF_BYPOSITION | Native.Constants.MF_POPUP, (uint)awakeTimeMenu, Resources.AWAKE_KEEP_ON_INTERVAL);
Bridge.InsertMenu(TrayMenu, 0, Native.Constants.MF_BYPOSITION | Native.Constants.MF_POPUP | (isChecked == true ? Native.Constants.MF_CHECKED : Native.Constants.MF_UNCHECKED), (uint)awakeTimeMenu, Resources.AWAKE_KEEP_ON_INTERVAL);
}
private static void InsertAwakeModeMenuItems(AwakeMode mode)

View File

@ -26,8 +26,6 @@ namespace Awake
{
internal sealed class Program
{
private static readonly ManualResetEvent _exitSignal = new(false);
private static Mutex? _mutex;
private static FileSystemWatcher? _watcher;
private static SettingsUtils? _settingsUtils;
@ -47,6 +45,8 @@ namespace Awake
internal static readonly string[] AliasesPidOption = ["--pid", "-p"];
internal static readonly string[] AliasesExpireAtOption = ["--expire-at", "-e"];
private static readonly Icon _defaultAwakeIcon = new(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets/Awake/awake.ico"));
private static int Main(string[] args)
{
_settingsUtils = new SettingsUtils();
@ -54,15 +54,17 @@ namespace Awake
Logger.InitializeLogger(Path.Combine("\\", Core.Constants.AppName, "Logs"));
AppDomain.CurrentDomain.UnhandledException += AwakeUnhandledExceptionCatcher;
if (PowerToys.GPOWrapper.GPOWrapper.GetConfiguredAwakeEnabledValue() == PowerToys.GPOWrapper.GpoRuleConfigured.Disabled)
{
Exit("PowerToys.Awake tried to start with a group policy setting that disables the tool. Please contact your system 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);
return 0;
}
if (!instantiated)
{
Exit(Core.Constants.AppName + " is already running! Exiting the application.", 1, _exitSignal, true);
Exit(Core.Constants.AppName + " is already running! Exiting the application.", 1);
}
Logger.LogInfo($"Launching {Core.Constants.AppName}...");
@ -129,18 +131,26 @@ namespace Awake
return rootCommand.InvokeAsync(args).Result;
}
private static void AwakeUnhandledExceptionCatcher(object sender, UnhandledExceptionEventArgs e)
{
if (e.ExceptionObject is Exception exception)
{
Logger.LogError(exception.ToString());
Logger.LogError(exception.StackTrace);
}
}
private static bool ExitHandler(ControlType ctrlType)
{
Logger.LogInfo($"Exited through handler with control type: {ctrlType}");
Exit(Resources.AWAKE_EXIT_MESSAGE, Environment.ExitCode, _exitSignal);
Exit(Resources.AWAKE_EXIT_MESSAGE, Environment.ExitCode);
return false;
}
private static void Exit(string message, int exitCode, ManualResetEvent exitSignal, bool force = false)
private static void Exit(string message, int exitCode)
{
Logger.LogInfo(message);
Manager.CompleteExit(exitCode, exitSignal, force);
Manager.CompleteExit(exitCode);
}
private static void HandleCommandLineArguments(bool usePtConfig, bool displayOn, uint timeLimit, int pid, string expireAt)
@ -169,6 +179,15 @@ namespace Awake
// Start the monitor thread that will be used to track the current state.
Manager.StartMonitor();
TrayHelper.InitializeTray(Core.Constants.FullAppName, _defaultAwakeIcon);
var eventHandle = new EventWaitHandle(false, EventResetMode.ManualReset, interop.Constants.AwakeExitEvent());
new Thread(() =>
{
WaitHandle.WaitAny([eventHandle]);
Exit(Resources.AWAKE_EXIT_SIGNAL_MESSAGE, 0);
}).Start();
if (usePtConfig)
{
// Configuration file is used, therefore we disregard any other command-line parameter
@ -177,17 +196,6 @@ namespace Awake
try
{
var eventHandle = new EventWaitHandle(false, EventResetMode.ManualReset, interop.Constants.AwakeExitEvent());
new Thread(() =>
{
if (WaitHandle.WaitAny([_exitSignal, eventHandle]) == 1)
{
Exit(Resources.AWAKE_EXIT_SIGNAL_MESSAGE, 0, _exitSignal, true);
}
}).Start();
TrayHelper.InitializeTray(Core.Constants.FullAppName, new Icon(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets/Awake/awake.ico")), _exitSignal);
string? settingsPath = _settingsUtils!.GetSettingsFilePath(Core.Constants.AppName);
Logger.LogInfo($"Reading configuration file: {settingsPath}");
@ -245,11 +253,9 @@ namespace Awake
RunnerHelper.WaitForPowerToysRunner(pid, () =>
{
Logger.LogInfo($"Triggered PID-based exit handler for PID {pid}.");
Exit(Resources.AWAKE_EXIT_BINDING_HOOK_MESSAGE, 0, _exitSignal, true);
Exit(Resources.AWAKE_EXIT_BINDING_HOOK_MESSAGE, 0);
});
}
_exitSignal.WaitOne();
}
private static void ScaffoldConfiguration(string settingsPath)

View File

@ -195,6 +195,42 @@ namespace Awake.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to d.
/// </summary>
internal static string AWAKE_LABEL_DAYS {
get {
return ResourceManager.GetString("AWAKE_LABEL_DAYS", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to h.
/// </summary>
internal static string AWAKE_LABEL_HOURS {
get {
return ResourceManager.GetString("AWAKE_LABEL_HOURS", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to m.
/// </summary>
internal static string AWAKE_LABEL_MINUTES {
get {
return ResourceManager.GetString("AWAKE_LABEL_MINUTES", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to s.
/// </summary>
internal static string AWAKE_LABEL_SECONDS {
get {
return ResourceManager.GetString("AWAKE_LABEL_SECONDS", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} minutes.
/// </summary>
@ -241,7 +277,7 @@ namespace Awake.Properties {
}
/// <summary>
/// Looks up a localized string similar to Timed.
/// Looks up a localized string similar to Interval.
/// </summary>
internal static string AWAKE_TRAY_TEXT_TIMED {
get {

View File

@ -187,6 +187,22 @@
<value>Passive</value>
</data>
<data name="AWAKE_TRAY_TEXT_TIMED" xml:space="preserve">
<value>Timed</value>
<value>Interval</value>
</data>
<data name="AWAKE_LABEL_DAYS" xml:space="preserve">
<value>d</value>
<comment>Used to display number of days in the system tray tooltip.</comment>
</data>
<data name="AWAKE_LABEL_HOURS" xml:space="preserve">
<value>h</value>
<comment>Used to display number of hours in the system tray tooltip.</comment>
</data>
<data name="AWAKE_LABEL_MINUTES" xml:space="preserve">
<value>m</value>
<comment>Used to display number of minutes in the system tray tooltip.</comment>
</data>
<data name="AWAKE_LABEL_SECONDS" xml:space="preserve">
<value>s</value>
<comment>Used to display number of seconds in the system tray tooltip.</comment>
</data>
</root>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<asmv3:application>
<asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
<dpiAware>true</dpiAware>
</asmv3:windowsSettings>
</asmv3:application>
</assembly>