diff --git a/App/App.xaml.cs b/App/App.xaml.cs index c146748..61d2963 100644 --- a/App/App.xaml.cs +++ b/App/App.xaml.cs @@ -23,6 +23,7 @@ public partial class App internal const int DefaultBatteryLowNotificationValue = 20; internal const string DefaultBatteryNormalColour = null; internal const bool DefaultHideAtStartup = false; + internal const bool DefaultIsAutoBatteryNormalColour = true; internal const int DefaultRefreshSeconds = 60; internal const bool DefaultTrayIconFontBold = false; internal const bool DefaultTrayIconFontUnderline = false; diff --git a/App/Controls/AccentColourPicker.xaml.cs b/App/Controls/AccentColourPicker.xaml.cs index 30cb357..5c2f9b4 100644 --- a/App/Controls/AccentColourPicker.xaml.cs +++ b/App/Controls/AccentColourPicker.xaml.cs @@ -2,8 +2,10 @@ namespace Percentage.App.Controls; public partial class AccentColourPicker { - public static string[] AccentBrushes = + public static readonly string[] AccentBrushes = [ + "#000000", // Black + "#FFFFFF", // White "#FFB900", // Gold "#FF8C00", // Dark Orange "#F7630C", // Orange diff --git a/App/Controls/BatteryInformation.cs b/App/Controls/BatteryInformation.cs index 0c9e669..c2dd2da 100644 --- a/App/Controls/BatteryInformation.cs +++ b/App/Controls/BatteryInformation.cs @@ -5,9 +5,11 @@ using System.Text.RegularExpressions; using System.Windows; using System.Windows.Controls; +using System.Windows.Forms; using System.Windows.Media; using Windows.Devices.Power; using Windows.System.Power; +using Percentage.App.Properties; using Wpf.Ui.Controls; using PowerLineStatus = System.Windows.Forms.PowerLineStatus; @@ -79,17 +81,35 @@ public BatteryInformation() Update(); - updateSubscription = Observable.Interval(TimeSpan.FromSeconds(1)) - .ObserveOn(AsyncOperationManager.SynchronizationContext).Subscribe(_ => Update()); + SetupUpdateSubscription(); + + Observable.FromEventPattern( + handler => Settings.Default.PropertyChanged += handler, + handler => Settings.Default.PropertyChanged -= handler) + .Throttle(TimeSpan.FromMilliseconds(500)) + .ObserveOn(AsyncOperationManager.SynchronizationContext) + .Subscribe(_ => SetupUpdateSubscription()); + + return; + + void SetupUpdateSubscription() + { + updateSubscription?.Dispose(); + updateSubscription = Observable.Interval(TimeSpan.FromSeconds(Settings.Default.RefreshSeconds)) + .ObserveOn(AsyncOperationManager.SynchronizationContext).Subscribe(_ => Update()); + } }; Unloaded += (_, _) => { updateSubscription?.Dispose(); }; } + [GeneratedRegex(@"\B[A-Z]")] + private static partial Regex WordStartLetterRegex(); + private void Update() { var report = Battery.AggregateBattery.GetReport(); - var powerStatus = System.Windows.Forms.SystemInformation.PowerStatus; + var powerStatus = SystemInformation.PowerStatus; _batteryLifePercent.Value = report.Status == BatteryStatus.NotPresent ? "Unknown" : powerStatus.BatteryLifePercent.ToString("P"); @@ -154,9 +174,6 @@ private void Update() _batteryStatus.Value = WordStartLetterRegex().Replace(report.Status.ToString(), " $0"); } - [GeneratedRegex(@"\B[A-Z]")] - private static partial Regex WordStartLetterRegex(); - private sealed partial class BatteryInformationObservableValue(SymbolRegular icon, string name) : SymbolIconObservableValue(icon) { diff --git a/App/Pages/SettingsPage.xaml b/App/Pages/SettingsPage.xaml index ba648d0..9db82e8 100644 --- a/App/Pages/SettingsPage.xaml +++ b/App/Pages/SettingsPage.xaml @@ -4,7 +4,12 @@ xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml" xmlns:properties="clr-namespace:Percentage.App.Properties" xmlns:system="clr-namespace:System;assembly=System.Runtime" - xmlns:controls="clr-namespace:Percentage.App.Controls"> + xmlns:controls="clr-namespace:Percentage.App.Controls" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:data="clr-namespace:Codify.System.Windows.Data;assembly=Codify.System.Windows" + mc:Ignorable="d" + d:DesignWidth="400"> @@ -64,7 +69,7 @@ - 1 + 5 10 30 60 @@ -174,10 +179,17 @@ Grid.Column="2" Text="Normal" VerticalAlignment="Center" /> - + + + + diff --git a/App/Pages/SettingsPage.xaml.cs b/App/Pages/SettingsPage.xaml.cs index aca50bc..12c80e6 100644 --- a/App/Pages/SettingsPage.xaml.cs +++ b/App/Pages/SettingsPage.xaml.cs @@ -123,6 +123,7 @@ private void OnResetButtonClick(object sender, RoutedEventArgs e) Default.BatteryHighNotification = App.DefaultBatteryHighNotification; Default.BatteryCriticalNotification = App.DefaultBatteryCriticalNotification; Default.HideAtStartup = App.DefaultHideAtStartup; + Default.IsAutoBatteryNormalColour = App.DefaultIsAutoBatteryNormalColour; _ = EnableAutoStart(); } diff --git a/App/Properties/Settings.Designer.cs b/App/Properties/Settings.Designer.cs index 3c5e049..afe3e85 100644 --- a/App/Properties/Settings.Designer.cs +++ b/App/Properties/Settings.Designer.cs @@ -225,5 +225,17 @@ public bool TrayIconFontUnderline { this["TrayIconFontUnderline"] = value; } } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("True")] + public bool IsAutoBatteryNormalColour { + get { + return ((bool)(this["IsAutoBatteryNormalColour"])); + } + set { + this["IsAutoBatteryNormalColour"] = value; + } + } } } diff --git a/App/Properties/Settings.settings b/App/Properties/Settings.settings index a09191c..ff75850 100644 --- a/App/Properties/Settings.settings +++ b/App/Properties/Settings.settings @@ -53,6 +53,9 @@ False + + True + diff --git a/App/TrayIconWindow.xaml.cs b/App/TrayIconWindow.xaml.cs index fadb34c..cdef9ce 100644 --- a/App/TrayIconWindow.xaml.cs +++ b/App/TrayIconWindow.xaml.cs @@ -12,7 +12,6 @@ using Microsoft.Toolkit.Uwp.Notifications; using Microsoft.Win32; using Percentage.App.Pages; -using Percentage.App.Properties; using Wpf.Ui.Appearance; using Wpf.Ui.Markup; using Application = System.Windows.Application; @@ -27,18 +26,34 @@ namespace Percentage.App; public partial class TrayIconWindow { - private DispatcherTimer _batteryStatusRefreshTimer; - - // Setup variables used in the repetitively ran "Update" local function. + private static readonly TimeSpan DebounceTimeSpan = TimeSpan.FromMilliseconds(500); + private readonly DispatcherTimer _refreshTimer; private (NotificationType Type, DateTime DateTime) _lastNotification = (default, default); private string _notificationText; private string _notificationTitle; - private IDisposable _refreshSubscription; public TrayIconWindow() { SystemThemeWatcher.Watch(this); InitializeComponent(); + + // Setup timer to update the tray icon. + _refreshTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(Default.RefreshSeconds) }; + _refreshTimer.Tick += (_, _) => UpdateBatteryStatus(); + } + + private static MainWindow ActivateMainWindow() + { + var window = Application.Current.Windows.OfType().FirstOrDefault(); + if (window != null) + { + window.Activate(); + return window; + } + + window = new MainWindow(); + window.Show(); + return window; } private static SolidColorBrush GetBrushFromColourHexString(string hexString, Color fallbackColour) @@ -71,24 +86,12 @@ private static RenderTargetBitmap GetImageSource(FrameworkElement element) return renderTargetBitmap; } - private static MainWindow ActivateMainWindow() - { - var window = Application.Current.Windows.OfType().FirstOrDefault(); - if (window != null) - { - window.Activate(); - return window; - } - - window = new MainWindow(); - window.Show(); - return window; - } - private SolidColorBrush GetNormalBrush() { - return GetBrushFromColourHexString(Default.BatteryNormalColour, - (Color)FindResource(nameof(ThemeResource.TextFillColorPrimary))); + return Default.IsAutoBatteryNormalColour + ? new SolidColorBrush((Color)FindResource(nameof(ThemeResource.TextFillColorPrimary))) + : GetBrushFromColourHexString(Default.BatteryNormalColour, + (Color)FindResource(nameof(ThemeResource.TextFillColorPrimary))); } private void OnAboutMenuItemClick(object sender, RoutedEventArgs e) @@ -112,43 +115,50 @@ private void OnLoaded(object sender, RoutedEventArgs args) if (!Default.HideAtStartup) ActivateMainWindow().NavigateToPage(); - // Update battery status when the computer resumes or when the power status changes. - SystemEvents.PowerModeChanged += (_, e) => - { - if (e.Mode is PowerModes.Resume or PowerModes.StatusChange) UpdateBatteryStatus(); - }; + // Update battery status when the computer resumes or when the power status changes with debounce. + Observable.FromEventPattern( + handler => SystemEvents.PowerModeChanged += handler, + handler => SystemEvents.PowerModeChanged -= handler) + .Throttle(DebounceTimeSpan) + .ObserveOn(AsyncOperationManager.SynchronizationContext) + .Subscribe(_ => UpdateBatteryStatus()); - // Update battery status when the display settings change. + // Update battery status when the display settings change with debounce. // This will redraw the tray icon to ensure optimal icon resolution under the current display settings. - SystemEvents.DisplaySettingsChanged += (_, _) => UpdateBatteryStatus(); + Observable.FromEventPattern( + handler => SystemEvents.DisplaySettingsChanged += handler, + handler => SystemEvents.DisplaySettingsChanged -= handler) + .Throttle(DebounceTimeSpan) + .ObserveOn(AsyncOperationManager.SynchronizationContext) + .Subscribe(_ => UpdateBatteryStatus()); // This event can be triggered multiple times when Windows changes between dark and light theme. // Update tray icon colour when user preference changes settled down. - Observable - .FromEventPattern( + Observable.FromEventPattern( handler => SystemEvents.UserPreferenceChanged += handler, handler => SystemEvents.UserPreferenceChanged -= handler) - .Throttle(TimeSpan.FromMilliseconds(500)) + .Throttle(DebounceTimeSpan) .ObserveOn(AsyncOperationManager.SynchronizationContext) .Subscribe(_ => UpdateBatteryStatus()); // Handle user settings change with debouncing. - Observable - .FromEventPattern( + Observable.FromEventPattern( handler => Default.PropertyChanged += handler, handler => Default.PropertyChanged -= handler) - .Throttle(TimeSpan.FromMilliseconds(500)) + .Throttle(DebounceTimeSpan) .ObserveOn(AsyncOperationManager.SynchronizationContext) .Subscribe(pattern => OnUserSettingsPropertyChanged(pattern.EventArgs.PropertyName)); - // Initial updates. + // Initial update. UpdateBatteryStatus(); - UpdateRefreshSubscription(); - // Setup timer to update the tray icon. - _batteryStatusRefreshTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(Default.RefreshSeconds) }; - _batteryStatusRefreshTimer.Tick += (_, _) => UpdateBatteryStatus(); - _batteryStatusRefreshTimer.Start(); + // Kick off to update the tray icon. + _refreshTimer.Start(); + } + + private void OnNotifyIconLeftDoubleClick(NotifyIcon sender, RoutedEventArgs e) + { + ActivateMainWindow().NavigateToPage(); } private void OnSettingsMenuItemClick(object sender, RoutedEventArgs e) @@ -160,11 +170,11 @@ private void OnUserSettingsPropertyChanged(string propertyName) { // Always save settings change immediately in case the app crashes losing all changes. Default.Save(); - + switch (propertyName) { case nameof(Default.RefreshSeconds): - _batteryStatusRefreshTimer.Interval = TimeSpan.FromSeconds(Default.RefreshSeconds); + _refreshTimer.Interval = TimeSpan.FromSeconds(Default.RefreshSeconds); break; case nameof(Default.BatteryCriticalNotificationValue): if (Default.BatteryLowNotificationValue < Default.BatteryCriticalNotificationValue) @@ -208,11 +218,10 @@ private void SetNotifyIconText(string text, Brush foreground, string fontFamily if (Default.TrayIconFontUnderline) textBlock.TextDecorations = TextDecorations.Underline; var iconImageSource = GetImageSource(textBlock); - + // There's a chance that some native exception may be thrown when setting the notify icon's image. // Catch any exception here and retry a few times then fail silently with logs. for (var i = 0; i < 5; i++) - { try { NotifyIcon.Icon = iconImageSource; @@ -221,13 +230,10 @@ private void SetNotifyIconText(string text, Brush foreground, string fontFamily catch (Exception e) { if (i == 4) - { // Retried maximum number of times. // Log error and continue. App.SetTrayIconUpdateError(e); - } } - } } private void UpdateBatteryStatus() @@ -405,16 +411,4 @@ void CheckAndSendNotification() _lastNotification = (notificationType, utcNow); } } - - private void UpdateRefreshSubscription() - { - _refreshSubscription?.Dispose(); - _refreshSubscription = Observable.Interval(TimeSpan.FromSeconds(Default.RefreshSeconds)) - .ObserveOn(AsyncOperationManager.SynchronizationContext).Subscribe(_ => UpdateBatteryStatus()); - } - - private void OnNotifyIconLeftDoubleClick(NotifyIcon sender, RoutedEventArgs e) - { - ActivateMainWindow().NavigateToPage(); - } } \ No newline at end of file diff --git a/Pack/Pack.wapproj b/Pack/Pack.wapproj index 113d630..6ad9633 100644 --- a/Pack/Pack.wapproj +++ b/Pack/Pack.wapproj @@ -51,7 +51,7 @@ True False SHA256 - True + False True 0 ..\App\App.csproj diff --git a/Pack/Package.appxmanifest b/Pack/Package.appxmanifest index e0a1452..0cf78cf 100644 --- a/Pack/Package.appxmanifest +++ b/Pack/Package.appxmanifest @@ -10,7 +10,7 @@ + Version="2.0.9.0" /> Battery Percentage Icon