Skip to content

Commit

Permalink
Various improvements.
Browse files Browse the repository at this point in the history
* Added "auto" option for normal battery colour.
* Added black and white colour to tray icon colour selection.
* Made minimum refresh interval from 1 second to 5 seconds, 1 second was really for debug purposes.
* Removed double refresh of the battery status.
* All events are debounced before triggering the battery status refresh.
* Details page now uses the same refersh interval as set in the settings page instead of 1 second.
  • Loading branch information
leonzhou-smokeball committed Dec 15, 2024
1 parent 124d491 commit 30131e0
Show file tree
Hide file tree
Showing 10 changed files with 116 additions and 74 deletions.
1 change: 1 addition & 0 deletions App/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 3 additions & 1 deletion App/Controls/AccentColourPicker.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 23 additions & 6 deletions App/Controls/BatteryInformation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -79,17 +81,35 @@ public BatteryInformation()

Update();

updateSubscription = Observable.Interval(TimeSpan.FromSeconds(1))
.ObserveOn(AsyncOperationManager.SynchronizationContext).Subscribe(_ => Update());
SetupUpdateSubscription();

Observable.FromEventPattern<PropertyChangedEventHandler, PropertyChangedEventArgs>(
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");
Expand Down Expand Up @@ -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<string>(icon)
{
Expand Down
24 changes: 18 additions & 6 deletions App/Pages/SettingsPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -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">
<StackPanel>

<!-- Auto start disabled warning -->
Expand Down Expand Up @@ -64,7 +69,7 @@
</ui:CardControl.Header>
<ComboBox ItemStringFormat="{}{0} seconds"
SelectedValue="{Binding RefreshSeconds, Source={x:Static properties:Settings.Default}, Mode=TwoWay}">
<system:Int32>1</system:Int32>
<system:Int32>5</system:Int32>
<system:Int32>10</system:Int32>
<system:Int32>30</system:Int32>
<system:Int32>60</system:Int32>
Expand Down Expand Up @@ -174,10 +179,17 @@
Grid.Column="2"
Text="Normal"
VerticalAlignment="Center" />
<controls:AccentColourPicker Grid.Row="2"
Grid.Column="3"
Margin="5,0"
SelectedItem="{Binding BatteryNormalColour, Source={x:Static properties:Settings.Default}}" />
<StackPanel Grid.Row="2"
Grid.Column="3"
Orientation="Horizontal">
<controls:AccentColourPicker
Margin="5,0,12,0"
SelectedItem="{Binding BatteryNormalColour, Source={x:Static properties:Settings.Default}}"
IsEnabled="{Binding IsAutoBatteryNormalColour, Source={x:Static properties:Settings.Default}, Converter={x:Static data:BooleanConverter.Instance}, ConverterParameter=invert}" />
<CheckBox Content="Auto"
ToolTip="Use system default text colour"
IsChecked="{Binding IsAutoBatteryNormalColour, Source={x:Static properties:Settings.Default}}" />
</StackPanel>
</Grid>
</ui:CardExpander>

Expand Down
1 change: 1 addition & 0 deletions App/Pages/SettingsPage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
12 changes: 12 additions & 0 deletions App/Properties/Settings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions App/Properties/Settings.settings
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@
<Setting Name="TrayIconFontUnderline" Type="System.Boolean" Scope="User">
<Value Profile="(Default)">False</Value>
</Setting>
<Setting Name="IsAutoBatteryNormalColour" Type="System.Boolean" Scope="User">
<Value Profile="(Default)">True</Value>
</Setting>
</Settings>
</SettingsFile>

112 changes: 53 additions & 59 deletions App/TrayIconWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<MainWindow>().FirstOrDefault();
if (window != null)
{
window.Activate();
return window;
}

window = new MainWindow();
window.Show();
return window;
}

private static SolidColorBrush GetBrushFromColourHexString(string hexString, Color fallbackColour)
Expand Down Expand Up @@ -71,24 +86,12 @@ private static RenderTargetBitmap GetImageSource(FrameworkElement element)
return renderTargetBitmap;
}

private static MainWindow ActivateMainWindow()
{
var window = Application.Current.Windows.OfType<MainWindow>().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)
Expand All @@ -112,43 +115,50 @@ private void OnLoaded(object sender, RoutedEventArgs args)

if (!Default.HideAtStartup) ActivateMainWindow().NavigateToPage<DetailsPage>();

// 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<PowerModeChangedEventHandler, PowerModeChangedEventArgs>(
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<EventHandler, EventArgs>(
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<UserPreferenceChangedEventHandler, UserPreferenceChangedEventArgs>(
Observable.FromEventPattern<UserPreferenceChangedEventHandler, UserPreferenceChangedEventArgs>(
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<PropertyChangedEventHandler, PropertyChangedEventArgs>(
Observable.FromEventPattern<PropertyChangedEventHandler, PropertyChangedEventArgs>(
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<DetailsPage>();
}

private void OnSettingsMenuItemClick(object sender, RoutedEventArgs e)
Expand All @@ -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)
Expand Down Expand Up @@ -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;
Expand All @@ -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()
Expand Down Expand Up @@ -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<DetailsPage>();
}
}
2 changes: 1 addition & 1 deletion Pack/Pack.wapproj
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
<GenerateTemporaryStoreCertificate>True</GenerateTemporaryStoreCertificate>
<GenerateAppInstallerFile>False</GenerateAppInstallerFile>
<AppxPackageSigningTimestampDigestAlgorithm>SHA256</AppxPackageSigningTimestampDigestAlgorithm>
<AppxAutoIncrementPackageRevision>True</AppxAutoIncrementPackageRevision>
<AppxAutoIncrementPackageRevision>False</AppxAutoIncrementPackageRevision>
<GenerateTestArtifacts>True</GenerateTestArtifacts>
<HoursBetweenUpdateChecks>0</HoursBetweenUpdateChecks>
<EntryPointProjectUniqueName>..\App\App.csproj</EntryPointProjectUniqueName>
Expand Down
2 changes: 1 addition & 1 deletion Pack/Package.appxmanifest
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<Identity
Name="61867SoleonInnovation.BatteryPercentageIcon"
Publisher="CN=B0B1FE5B-CC73-4F71-BD3F-7B809647826C"
Version="2.0.8.0" />
Version="2.0.9.0" />

<Properties>
<DisplayName>Battery Percentage Icon</DisplayName>
Expand Down

0 comments on commit 30131e0

Please sign in to comment.