Add update-available badge to system tray icon (#47030)

When an update is available (readyToDownload or readyToInstall), the
tray icon switches to a badged variant with an orange dot. Works for
both default mode (color icon.ico) and theme-adaptive mode (light/dark
variants).

Closes: #19222
Closes: #25497

## Changes

### Tray icon update badge
- Add 3 badged icon variants
- Extend get_icon() to select badged variant based on update_available
state
- Add set_tray_icon_update_available() for runtime icon switching
- Hook into UpdateUtils.cpp state transitions via
dispatch_run_on_main_ui_thread
- Check UpdateState at startup to show badge immediately if update
pending
- Add 'Update available' context menu item at top of tray menu when
active

### Fix: update icons not deployed (bug fix)
- Add APPICON_UPDATE icon resource to runner.base.rc (iconUpdate.ico was
missing)
- Add CopyFileToFolders entries for iconUpdate.ico,
PowerToysDarkUpdate.ico, and PowerToysWhiteUpdate.ico in runner.vcxproj
- Add all update icon files to installer Core.wxs so they ship in
releases

### UX improvements
- 'Update available' tray menu item now navigates to General page
(Overview) instead of opening Settings to Dashboard
- Update InfoBar severity changed from Success/Informational to Warning
across GeneralPage, LaunchPage, and CheckUpdateControl
- Dashboard update badge gradient and icon refreshed (orange theme,
exclamation glyph)
- AccentButtonStyle applied to 'Install Now' button
- Fixed casing: 'Update Available' to 'Update available'
- Added UpdateAvailableInfoBar.Title resource string
- Add orange update dot to the General navview item

### Screenshots

Before:
<img width="146" height="78" alt="image"
src="https://github.com/user-attachments/assets/c80b8b5f-da94-4cba-92c9-3fcca685653c"
/>

After:

<img width="184" height="104" alt="image"
src="https://github.com/user-attachments/assets/13fc6b34-6e2a-4060-a2f7-f0b6b0d15363"
/>

<img width="150" height="84" alt="image"
src="https://github.com/user-attachments/assets/2673239c-8ce3-437b-947a-1d66803a87ec"
/>

<img width="150" height="100" alt="image"
src="https://github.com/user-attachments/assets/c321deda-770d-47ff-9600-c395f466d444"
/>

<img width="189" height="104" alt="image"
src="https://github.com/user-attachments/assets/2c56d1b7-6615-4d85-80b9-a1cee6413b75"
/>


<img width="473" height="218" alt="image"
src="https://github.com/user-attachments/assets/b0fb59ed-f8bd-40a0-aefd-816a71fc231f"
/>

<img width="1048" height="288" alt="image"
src="https://github.com/user-attachments/assets/29d34e01-f6a9-46c3-a56e-2c50a07718a1"
/>

<img width="206" height="155" alt="image"
src="https://github.com/user-attachments/assets/80e9f77e-aae5-429a-b6be-f0e9f296e929"
/>

<img width="434" height="163" alt="image"
src="https://github.com/user-attachments/assets/7c9d6cd5-fdaa-4b70-a2c0-cff87f5fcf1c"
/>

<img width="379" height="270" alt="image"
src="https://github.com/user-attachments/assets/03e0f60d-a901-45e7-a03a-18be28ec87ed"
/>


## How to test

Since local dev builds use version `0.0.1` which blocks update checks,
you need to temporarily fake an older version:

1. In `src/Version.props`, change `<Version>0.0.1</Version>` to
`<Version>0.87.0</Version>`
2. Optionally, in `src/runner/UpdateUtils.cpp`, change both interval
constants to `1` (minute) for faster testing:
   ```cpp
   constexpr int64_t UPDATE_CHECK_INTERVAL_MINUTES = 1;
   constexpr int64_t UPDATE_CHECK_AFTER_FAILED_INTERVAL_MINUTES = 1;
   ```
3. Build and run the runner
4. Within ~1 minute (with the interval change) or after clicking 'Check
for updates' in Settings > General, the runner will query GitHub and
find a newer version

### Verify
- [ ] Tray icon changes to the update variant (badged with orange dot)
- [ ] Right-clicking the tray icon shows 'Update available' at the top
of the context menu
- [ ] Clicking 'Update available' opens Settings directly to the General
page
- [ ] Settings General page shows the update InfoBar with Warning
severity
- [ ] Dashboard shows the update badge with orange gradient and
exclamation icon
- [ ] Quick Access flyout shows update InfoBar with Warning severity

**Remember to revert Version.props and UpdateUtils.cpp before
committing!**

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Niels Laute
2026-04-22 10:48:03 +02:00
committed by GitHub
parent c8ffcb73c3
commit b0ccc2394a
21 changed files with 168 additions and 13 deletions

View File

@@ -67,8 +67,11 @@
<RegistryValue Type="string" Name="svgs_icons" Value="" KeyPath="yes" /> <RegistryValue Type="string" Name="svgs_icons" Value="" KeyPath="yes" />
</RegistryKey> </RegistryKey>
<File Id="icon.ico" Source="$(var.BinDir)svgs\icon.ico" /> <File Id="icon.ico" Source="$(var.BinDir)svgs\icon.ico" />
<File Id="iconUpdate.ico" Source="$(var.BinDir)svgs\iconUpdate.ico" />
<File Id="PowerToysWhite.ico" Source="$(var.BinDir)svgs\PowerToysWhite.ico" /> <File Id="PowerToysWhite.ico" Source="$(var.BinDir)svgs\PowerToysWhite.ico" />
<File Id="PowerToysWhiteUpdate.ico" Source="$(var.BinDir)svgs\PowerToysWhiteUpdate.ico" />
<File Id="PowerToysDark.ico" Source="$(var.BinDir)svgs\PowerToysDark.ico" /> <File Id="PowerToysDark.ico" Source="$(var.BinDir)svgs\PowerToysDark.ico" />
<File Id="PowerToysDarkUpdate.ico" Source="$(var.BinDir)svgs\PowerToysDarkUpdate.ico" />
</Component> </Component>
</Directory> </Directory>
</DirectoryRef> </DirectoryRef>

View File

@@ -197,4 +197,7 @@
<value>Close</value> <value>Close</value>
<comment>Close as a verb, as in Close the application</comment> <comment>Close as a verb, as in Close the application</comment>
</data> </data>
<data name="UPDATE_AVAILABLE_MENU_TEXT" xml:space="preserve">
<value>Update available</value>
</data>
</root> </root>

View File

@@ -5,6 +5,7 @@
#include "ActionRunnerUtils.h" #include "ActionRunnerUtils.h"
#include "general_settings.h" #include "general_settings.h"
#include "trace.h" #include "trace.h"
#include "tray_icon.h"
#include "UpdateUtils.h" #include "UpdateUtils.h"
#include <common/utils/gpo.h> #include <common/utils/gpo.h>
@@ -130,6 +131,7 @@ void ProcessNewVersionInfo(const github_version_info& version_info,
state.releasePageUrl = {}; state.releasePageUrl = {};
state.downloadedInstallerFilename = {}; state.downloadedInstallerFilename = {};
Logger::trace(L"Version is up to date"); Logger::trace(L"Version is up to date");
dispatch_run_on_main_ui_thread([](PVOID) { set_tray_icon_update_available(false); }, nullptr);
return; return;
} }
const auto new_version_info = std::get<new_version_download_info>(version_info); const auto new_version_info = std::get<new_version_download_info>(version_info);
@@ -179,6 +181,7 @@ void ProcessNewVersionInfo(const github_version_info& version_info,
state.state = UpdateState::readyToInstall; state.state = UpdateState::readyToInstall;
state.downloadedInstallerFilename = new_version_info.installer_filename; state.downloadedInstallerFilename = new_version_info.installer_filename;
Trace::UpdateDownloadCompleted(true, new_version_info.version.toWstring()); Trace::UpdateDownloadCompleted(true, new_version_info.version.toWstring());
dispatch_run_on_main_ui_thread([](PVOID) { set_tray_icon_update_available(true); }, nullptr);
if (show_notifications) if (show_notifications)
{ {
ShowNewVersionAvailable(new_version_info); ShowNewVersionAvailable(new_version_info);
@@ -197,6 +200,7 @@ void ProcessNewVersionInfo(const github_version_info& version_info,
Logger::trace(L"New version is ready to download, showing notification"); Logger::trace(L"New version is ready to download, showing notification");
state.state = UpdateState::readyToDownload; state.state = UpdateState::readyToDownload;
state.downloadedInstallerFilename = {}; state.downloadedInstallerFilename = {};
dispatch_run_on_main_ui_thread([](PVOID) { set_tray_icon_update_available(true); }, nullptr);
if (show_notifications) if (show_notifications)
{ {
ShowOpenSettingsForUpdate(); ShowOpenSettingsForUpdate();

View File

@@ -14,6 +14,7 @@
#define APPICON 101 #define APPICON 101
#define ID_TRAY_MENU 102 #define ID_TRAY_MENU 102
#define APPICON_UPDATE 103
#define ID_CLOSE_MENU_COMMAND 40001 #define ID_CLOSE_MENU_COMMAND 40001
#define ID_SETTINGS_MENU_COMMAND 40002 #define ID_SETTINGS_MENU_COMMAND 40002
@@ -21,3 +22,4 @@
#define ID_REPORT_BUG_COMMAND 40004 #define ID_REPORT_BUG_COMMAND 40004
#define ID_DOCUMENTATION_MENU_COMMAND 40005 #define ID_DOCUMENTATION_MENU_COMMAND 40005
#define ID_QUICK_ACCESS_MENU_COMMAND 40006 #define ID_QUICK_ACCESS_MENU_COMMAND 40006
#define ID_UPDATE_MENU_COMMAND 40007

Binary file not shown.

View File

@@ -111,6 +111,11 @@
<FileType>Document</FileType> <FileType>Document</FileType>
<DestinationFolders>$(OutDir)\svgs</DestinationFolders> <DestinationFolders>$(OutDir)\svgs</DestinationFolders>
</CopyFileToFolders> </CopyFileToFolders>
<CopyFileToFolders Include="svgs\iconUpdate.ico">
<DeploymentContent>true</DeploymentContent>
<FileType>Document</FileType>
<DestinationFolders>$(OutDir)\svgs</DestinationFolders>
</CopyFileToFolders>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\common\COMUtils\COMUtils.vcxproj"> <ProjectReference Include="..\common\COMUtils\COMUtils.vcxproj">
@@ -152,6 +157,16 @@
<DestinationFolders Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(OutDir)\svgs</DestinationFolders> <DestinationFolders Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(OutDir)\svgs</DestinationFolders>
<DestinationFolders Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(OutDir)\svgs</DestinationFolders> <DestinationFolders Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(OutDir)\svgs</DestinationFolders>
</CopyFileToFolders> </CopyFileToFolders>
<CopyFileToFolders Include="svgs\PowerToysDarkUpdate.ico">
<DeploymentContent Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">true</DeploymentContent>
<DeploymentContent Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">true</DeploymentContent>
<DeploymentContent Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</DeploymentContent>
<DeploymentContent Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</DeploymentContent>
<DestinationFolders Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">$(OutDir)\svgs</DestinationFolders>
<DestinationFolders Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">$(OutDir)\svgs</DestinationFolders>
<DestinationFolders Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(OutDir)\svgs</DestinationFolders>
<DestinationFolders Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(OutDir)\svgs</DestinationFolders>
</CopyFileToFolders>
<CopyFileToFolders Include="svgs\PowerToysWhite.ico"> <CopyFileToFolders Include="svgs\PowerToysWhite.ico">
<DeploymentContent Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">true</DeploymentContent> <DeploymentContent Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">true</DeploymentContent>
<DeploymentContent Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">true</DeploymentContent> <DeploymentContent Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">true</DeploymentContent>
@@ -162,6 +177,16 @@
<DestinationFolders Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(OutDir)\svgs</DestinationFolders> <DestinationFolders Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(OutDir)\svgs</DestinationFolders>
<DestinationFolders Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(OutDir)\svgs</DestinationFolders> <DestinationFolders Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(OutDir)\svgs</DestinationFolders>
</CopyFileToFolders> </CopyFileToFolders>
<CopyFileToFolders Include="svgs\PowerToysWhiteUpdate.ico">
<DeploymentContent Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">true</DeploymentContent>
<DeploymentContent Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">true</DeploymentContent>
<DeploymentContent Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</DeploymentContent>
<DeploymentContent Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</DeploymentContent>
<DestinationFolders Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">$(OutDir)\svgs</DestinationFolders>
<DestinationFolders Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">$(OutDir)\svgs</DestinationFolders>
<DestinationFolders Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(OutDir)\svgs</DestinationFolders>
<DestinationFolders Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(OutDir)\svgs</DestinationFolders>
</CopyFileToFolders>
</ItemGroup> </ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" /> <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Import Project="$(RepoRoot)deps\spdlog.props" /> <Import Project="$(RepoRoot)deps\spdlog.props" />

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

@@ -17,6 +17,7 @@
#include <common/Themes/theme_listener.h> #include <common/Themes/theme_listener.h>
#include <common/Themes/theme_helpers.h> #include <common/Themes/theme_helpers.h>
#include "bug_report.h" #include "bug_report.h"
#include <common/updating/updateState.h>
namespace namespace
{ {
@@ -45,6 +46,7 @@ namespace
static ThemeListener theme_listener; static ThemeListener theme_listener;
static bool theme_adaptive_enabled = false; static bool theme_adaptive_enabled = false;
static bool update_available = false;
} }
// Struct to fill with callback and the data. The window_proc is responsible for cleaning it. // Struct to fill with callback and the data. The window_proc is responsible for cleaning it.
@@ -124,6 +126,11 @@ void handle_tray_command(HWND window, const WPARAM command_id, LPARAM lparam)
open_quick_access_flyout_window(); open_quick_access_flyout_window();
break; break;
} }
case ID_UPDATE_MENU_COMMAND:
{
open_settings_window(std::wstring{ L"Overview" });
break;
}
} }
} }
@@ -260,6 +267,24 @@ LRESULT __stdcall tray_icon_window_proc(HWND window, UINT message, WPARAM wparam
{ {
h_sub_menu = GetSubMenu(h_menu, 0); h_sub_menu = GetSubMenu(h_menu, 0);
} }
// Dynamically add/remove "Update available" menu item and its separator
DeleteMenu(h_sub_menu, ID_UPDATE_MENU_COMMAND, MF_BYCOMMAND);
// Remove the separator right after the update item (position 0 after deletion)
if (GetMenuItemCount(h_sub_menu) > 0)
{
MENUITEMINFOW mii = { .cbSize = sizeof(mii), .fMask = MIIM_FTYPE };
if (GetMenuItemInfoW(h_sub_menu, 0, TRUE, &mii) && (mii.fType & MFT_SEPARATOR))
{
DeleteMenu(h_sub_menu, 0, MF_BYPOSITION);
}
}
if (update_available)
{
InsertMenuW(h_sub_menu, 0, MF_BYPOSITION | MF_STRING, ID_UPDATE_MENU_COMMAND, GET_RESOURCE_STRING(IDS_UPDATE_AVAILABLE_MENU_TEXT).c_str());
InsertMenuW(h_sub_menu, 1, MF_BYPOSITION | MF_SEPARATOR, 0, nullptr);
}
POINT mouse_pointer; POINT mouse_pointer;
GetCursorPos(&mouse_pointer); GetCursorPos(&mouse_pointer);
SetForegroundWindow(window); // Needed for the context menu to disappear. SetForegroundWindow(window); // Needed for the context menu to disappear.
@@ -318,7 +343,14 @@ LRESULT __stdcall tray_icon_window_proc(HWND window, UINT message, WPARAM wparam
static HICON get_icon(Theme theme) static HICON get_icon(Theme theme)
{ {
std::wstring icon_path = get_module_folderpath(); std::wstring icon_path = get_module_folderpath();
icon_path += theme == Theme::Dark ? L"\\svgs\\PowerToysWhite.ico" : L"\\svgs\\PowerToysDark.ico"; if (theme == Theme::Dark)
{
icon_path += update_available ? L"\\svgs\\PowerToysWhiteUpdate.ico" : L"\\svgs\\PowerToysWhite.ico";
}
else
{
icon_path += update_available ? L"\\svgs\\PowerToysDarkUpdate.ico" : L"\\svgs\\PowerToysDark.ico";
}
Logger::trace(L"get_icon: Loading icon from path: {}", icon_path); Logger::trace(L"get_icon: Loading icon from path: {}", icon_path);
HICON icon = static_cast<HICON>(LoadImage(NULL, HICON icon = static_cast<HICON>(LoadImage(NULL,
@@ -356,7 +388,14 @@ void start_tray_icon(bool isProcessElevated, bool theme_adaptive)
{ {
theme_adaptive_enabled = theme_adaptive; theme_adaptive_enabled = theme_adaptive;
auto h_instance = reinterpret_cast<HINSTANCE>(&__ImageBase); auto h_instance = reinterpret_cast<HINSTANCE>(&__ImageBase);
HICON const icon = theme_adaptive ? get_icon(ThemeHelpers::GetSystemTheme()) : LoadIcon(h_instance, MAKEINTRESOURCE(APPICON));
// Check if an update is available at startup
auto state = UpdateState::read();
update_available = (state.state == UpdateState::readyToDownload || state.state == UpdateState::readyToInstall);
HICON const icon = theme_adaptive
? get_icon(ThemeHelpers::GetSystemTheme())
: LoadIcon(h_instance, MAKEINTRESOURCE(update_available ? APPICON_UPDATE : APPICON));
if (icon) if (icon)
{ {
UINT id_tray_icon = 1; UINT id_tray_icon = 1;
@@ -425,6 +464,29 @@ void set_tray_icon_visible(bool shouldIconBeVisible)
Shell_NotifyIcon(NIM_MODIFY, &tray_icon_data); Shell_NotifyIcon(NIM_MODIFY, &tray_icon_data);
} }
void set_tray_icon_update_available(bool available)
{
if (update_available == available)
{
return;
}
update_available = available;
Logger::info(L"set_tray_icon_update_available: update_available={}", update_available);
if (theme_adaptive_enabled)
{
tray_icon_data.hIcon = get_icon(ThemeHelpers::GetSystemTheme());
}
else
{
auto h_instance = reinterpret_cast<HINSTANCE>(&__ImageBase);
tray_icon_data.hIcon = LoadIcon(h_instance, MAKEINTRESOURCE(available ? APPICON_UPDATE : APPICON));
}
Shell_NotifyIcon(NIM_MODIFY, &tray_icon_data);
}
void set_tray_icon_theme_adaptive(bool theme_adaptive) void set_tray_icon_theme_adaptive(bool theme_adaptive)
{ {
Logger::info(L"set_tray_icon_theme_adaptive: Called with theme_adaptive={}, current theme_adaptive_enabled={}", Logger::info(L"set_tray_icon_theme_adaptive: Called with theme_adaptive={}, current theme_adaptive_enabled={}",
@@ -445,7 +507,7 @@ void set_tray_icon_theme_adaptive(bool theme_adaptive)
// If not requesting adaptive icon, or if adaptive icon failed to load, use default icon // If not requesting adaptive icon, or if adaptive icon failed to load, use default icon
if (!icon) if (!icon)
{ {
icon = LoadIcon(h_instance, MAKEINTRESOURCE(APPICON)); icon = LoadIcon(h_instance, MAKEINTRESOURCE(update_available ? APPICON_UPDATE : APPICON));
if (theme_adaptive && icon) if (theme_adaptive && icon)
{ {
// We requested adaptive but had to fall back, so update the flag // We requested adaptive but had to fall back, so update the flag

View File

@@ -9,6 +9,8 @@ void start_tray_icon(bool isProcessElevated, bool theme_adaptive);
void set_tray_icon_visible(bool shouldIconBeVisible); void set_tray_icon_visible(bool shouldIconBeVisible);
// Enable or disable theme adaptive tray icon at runtime // Enable or disable theme adaptive tray icon at runtime
void set_tray_icon_theme_adaptive(bool theme_adaptive); void set_tray_icon_theme_adaptive(bool theme_adaptive);
// Show or hide the update-available badge on the tray icon
void set_tray_icon_update_available(bool available);
// Stop the Tray Icon // Stop the Tray Icon
void stop_tray_icon(); void stop_tray_icon();
// Open the Settings Window // Open the Settings Window

View File

@@ -77,7 +77,7 @@
x:Uid="UpdateAvailableInfoBar" x:Uid="UpdateAvailableInfoBar"
IsClosable="False" IsClosable="False"
IsOpen="{x:Bind ViewModel.IsUpdateAvailable, Mode=OneWay}" IsOpen="{x:Bind ViewModel.IsUpdateAvailable, Mode=OneWay}"
Severity="Success" Severity="Warning"
Tapped="UpdateInfoBar_Tapped" /> Tapped="UpdateInfoBar_Tapped" />
<StackPanel <StackPanel

View File

@@ -24,15 +24,15 @@
CornerRadius="10"> CornerRadius="10">
<Border.Background> <Border.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="0.5,1"> <LinearGradientBrush StartPoint="0,0" EndPoint="0.5,1">
<GradientStop Offset="0.0" Color="#239DE0" /> <GradientStop Offset="0.0" Color="#FFC328" />
<GradientStop Offset="1.0" Color="#037CD6" /> <GradientStop Offset="1.0" Color="#FC9A03" />
</LinearGradientBrush> </LinearGradientBrush>
</Border.Background> </Border.Background>
<FontIcon <FontIcon
AutomationProperties.AccessibilityView="Raw" AutomationProperties.AccessibilityView="Raw"
FontSize="11" FontSize="11"
Foreground="White" Foreground="Black"
Glyph="&#xE895;" /> Glyph="&#xEDB1;" />
</Border> </Border>
<StackPanel Grid.Column="1" Orientation="Vertical"> <StackPanel Grid.Column="1" Orientation="Vertical">
<TextBlock x:Uid="UpdateAvailableTextBlock" FontWeight="SemiBold" /> <TextBlock x:Uid="UpdateAvailableTextBlock" FontWeight="SemiBold" />

View File

@@ -20,4 +20,21 @@
</Setter.Value> </Setter.Value>
</Setter> </Setter>
</Style> </Style>
<Style x:Key="UpdateInfoBadgeStyle" TargetType="InfoBadge">
<Setter Property="Padding" Value="0" />
<Setter Property="Width" Value="8" />
<Setter Property="Height" Value="8" />
<Setter Property="Background" Value="#F09A2A" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="InfoBadge">
<Border
x:Name="RootGrid"
Padding="{TemplateBinding Padding}"
Background="{TemplateBinding Background}"
CornerRadius="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.InfoBadgeCornerRadius}" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary> </ResourceDictionary>

View File

@@ -109,7 +109,7 @@
IsOpen="{x:Bind ViewModel.PowerToysUpdatingState, Mode=OneWay, Converter={StaticResource UpdateStateToBoolConverter}, ConverterParameter=ReadyToDownload}" IsOpen="{x:Bind ViewModel.PowerToysUpdatingState, Mode=OneWay, Converter={StaticResource UpdateStateToBoolConverter}, ConverterParameter=ReadyToDownload}"
IsTabStop="{x:Bind ViewModel.PowerToysUpdatingState, Mode=OneWay, Converter={StaticResource UpdateStateToBoolConverter}, ConverterParameter=ReadyToDownload}" IsTabStop="{x:Bind ViewModel.PowerToysUpdatingState, Mode=OneWay, Converter={StaticResource UpdateStateToBoolConverter}, ConverterParameter=ReadyToDownload}"
Message="{x:Bind ViewModel.PowerToysNewAvailableVersion, Mode=OneWay}" Message="{x:Bind ViewModel.PowerToysNewAvailableVersion, Mode=OneWay}"
Severity="Informational"> Severity="Warning">
<InfoBar.Content> <InfoBar.Content>
<StackPanel Spacing="16"> <StackPanel Spacing="16">
@@ -150,13 +150,14 @@
IsOpen="{x:Bind ViewModel.PowerToysUpdatingState, Mode=OneWay, Converter={StaticResource UpdateStateToBoolConverter}, ConverterParameter=ReadyToInstall}" IsOpen="{x:Bind ViewModel.PowerToysUpdatingState, Mode=OneWay, Converter={StaticResource UpdateStateToBoolConverter}, ConverterParameter=ReadyToInstall}"
IsTabStop="{x:Bind ViewModel.PowerToysUpdatingState, Mode=OneWay, Converter={StaticResource UpdateStateToBoolConverter}, ConverterParameter=ReadyToInstall}" IsTabStop="{x:Bind ViewModel.PowerToysUpdatingState, Mode=OneWay, Converter={StaticResource UpdateStateToBoolConverter}, ConverterParameter=ReadyToInstall}"
Message="{x:Bind ViewModel.PowerToysNewAvailableVersion, Mode=OneWay}" Message="{x:Bind ViewModel.PowerToysNewAvailableVersion, Mode=OneWay}"
Severity="Success"> Severity="Warning">
<InfoBar.Content> <InfoBar.Content>
<Button <Button
x:Uid="General_InstallNow" x:Uid="General_InstallNow"
Margin="0,0,0,16" Margin="0,0,0,16"
Command="{Binding UpdateNowButtonEventHandler}" Command="{Binding UpdateNowButtonEventHandler}"
IsEnabled="{Binding IsDownloadAllowed}" /> IsEnabled="{Binding IsDownloadAllowed}"
Style="{StaticResource AccentButtonStyle}" />
</InfoBar.Content> </InfoBar.Content>
<InfoBar.ActionButton> <InfoBar.ActionButton>
<HyperlinkButton <HyperlinkButton

View File

@@ -168,6 +168,13 @@
</AnimatedIcon.FallbackIconSource> </AnimatedIcon.FallbackIconSource>
</AnimatedIcon> </AnimatedIcon>
</NavigationViewItem.Icon> </NavigationViewItem.Icon>
<NavigationViewItem.InfoBadge>
<InfoBadge
x:Name="UpdateInfoBadge"
Margin="0,0,2,0"
Style="{StaticResource UpdateInfoBadgeStyle}"
Visibility="Collapsed" />
</NavigationViewItem.InfoBadge>
</NavigationViewItem> </NavigationViewItem>
<NavigationViewItemSeparator /> <NavigationViewItemSeparator />

View File

@@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO.Abstractions;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -13,6 +14,7 @@ using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Controls; using Microsoft.PowerToys.Settings.UI.Controls;
using Microsoft.PowerToys.Settings.UI.Helpers; using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Utilities;
using Microsoft.PowerToys.Settings.UI.Services; using Microsoft.PowerToys.Settings.UI.Services;
using Microsoft.PowerToys.Settings.UI.ViewModels; using Microsoft.PowerToys.Settings.UI.ViewModels;
using Microsoft.UI.Windowing; using Microsoft.UI.Windowing;
@@ -100,6 +102,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
private CancellationTokenSource _searchDebounceCts; private CancellationTokenSource _searchDebounceCts;
private const int SearchDebounceMs = 500; private const int SearchDebounceMs = 500;
private bool _disposed; private bool _disposed;
private IFileSystemWatcher _updateStateWatcher;
// Removed trace id counter per cleanup // Removed trace id counter per cleanup
@@ -137,6 +140,12 @@ namespace Microsoft.PowerToys.Settings.UI.Views
_searchSuggestions.Add(child.Content?.ToString()); _searchSuggestions.Add(child.Content?.ToString());
} }
} }
UpdateGeneralInfoBadge();
_updateStateWatcher = Helper.GetFileWatcher(string.Empty, UpdatingSettings.SettingsFile, () =>
{
DispatcherQueue.TryEnqueue(UpdateGeneralInfoBadge);
});
} }
public static int SendDefaultIPCMessage(string msg) public static int SendDefaultIPCMessage(string msg)
@@ -629,11 +638,28 @@ namespace Microsoft.PowerToys.Settings.UI.Views
return; return;
} }
_updateStateWatcher?.Dispose();
_searchDebounceCts?.Cancel(); _searchDebounceCts?.Cancel();
_searchDebounceCts?.Dispose(); _searchDebounceCts?.Dispose();
_searchDebounceCts = null; _searchDebounceCts = null;
_disposed = true; _disposed = true;
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
private void UpdateGeneralInfoBadge()
{
try
{
var config = UpdatingSettings.LoadSettings();
bool updateAvailable = config != null &&
(config.State == UpdatingSettings.UpdatingState.ReadyToDownload ||
config.State == UpdatingSettings.UpdatingState.ReadyToInstall);
UpdateInfoBadge.Visibility = updateAvailable ? Visibility.Visible : Visibility.Collapsed;
}
catch (Exception)
{
UpdateInfoBadge.Visibility = Visibility.Collapsed;
}
}
} }
} }

View File

@@ -5061,9 +5061,12 @@ The break timer font matches the text font.</value>
<value>You're up to date</value> <value>You're up to date</value>
</data> </data>
<data name="UpdateAvailableTextBlock.Text" xml:space="preserve"> <data name="UpdateAvailableTextBlock.Text" xml:space="preserve">
<value>Update Available</value> <value>Update available</value>
</data> </data>
<data name="GeneralVersion.Text" xml:space="preserve"> <data name="UpdateAvailableInfoBar.Title" xml:space="preserve">
<value>Update available</value>
</data>
<data name="GeneralVersion.Text" xml:space="preserve">
<value>Version</value> <value>Version</value>
</data> </data>
<data name="LearnWhatsNew.Text" xml:space="preserve"> <data name="LearnWhatsNew.Text" xml:space="preserve">