Add taskbar progress indicator support (#2695)#20621
Add taskbar progress indicator support (#2695)#20621mikasoukhov wants to merge 6 commits intoAvaloniaUI:masterfrom
Conversation
Implement cross-platform taskbar/dock progress indicator: - Windows: ITaskbarList3 COM (SetProgressValue/SetProgressState) - Linux: Unity Launcher API via DBus (com.canonical.Unity.LauncherEntry) - macOS: NSDockTile with NSProgressIndicator New API on Window class: - TaskbarProgressState property (None/Indeterminate/Normal/Error/Paused) - TaskbarProgressValue property (double, 0.0-1.0) Includes Sandbox demo app for testing.
There was a problem hiding this comment.
Pull request overview
This pull request implements cross-platform taskbar/dock progress indicator support for Avalonia windows, allowing applications to display progress information in the taskbar (Windows), dock (macOS), or launcher (Linux/Unity).
Changes:
- Added
TaskbarProgressStateenum with states: None, Indeterminate, Normal, Error, and Paused - Added
TaskbarProgressStateandTaskbarProgressValueproperties to theWindowclass with platform implementation bindings - Implemented platform-specific support for Windows (via ITaskbarList3), macOS (via NSProgressIndicator in dock), and Linux (via Unity LauncherEntry DBus protocol)
- Added stub implementations for headless and designer support environments
- Included a sample application in Sandbox to demonstrate the feature
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Avalonia.Controls/TaskbarProgressState.cs | New enum defining progress indicator states |
| src/Avalonia.Controls/Window.cs | Added properties and platform bindings for taskbar progress |
| src/Avalonia.Controls/Platform/IWindowImpl.cs | Extended interface with SetTaskbarProgressState and SetTaskbarProgressValue methods |
| src/Windows/Avalonia.Win32/WindowImpl.cs | Windows implementation using ITaskbarList3 COM interface |
| src/Windows/Avalonia.Win32/Interop/TaskBarList.cs | Windows interop wrapper for taskbar progress methods |
| src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs | P/Invoke delegate definitions for ITaskbarList3 |
| src/Avalonia.Native/WindowImpl.cs | macOS C# wrapper calling native implementation |
| src/Avalonia.Native/avn.idl | Native interface additions for dock progress |
| native/Avalonia.Native/src/OSX/WindowImpl.h | macOS native header with static dock progress fields |
| native/Avalonia.Native/src/OSX/WindowImpl.mm | macOS native implementation using NSProgressIndicator |
| src/Avalonia.X11/X11Window.cs | Linux/X11 implementation using Unity LauncherEntry via DBus |
| src/Avalonia.FreeDesktop/DBusUnityLauncher.cs | DBus handler for Unity launcher progress updates |
| src/Avalonia.FreeDesktop/DBusXml/com.canonical.Unity.LauncherEntry.xml | DBus interface definition |
| src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj | Project configuration for DBus code generation |
| src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs | Empty stub implementation for headless testing |
| src/Avalonia.DesignerSupport/Remote/Stubs.cs | Empty stub implementation for designer |
| src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs | Empty stub implementation for previewer |
| samples/Sandbox/MainWindow.axaml | Sample UI demonstrating progress indicator controls |
| samples/Sandbox/MainWindow.axaml.cs | Sample code-behind with progress animation demo |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
src/Avalonia.X11/X11Window.cs
Outdated
| private static DBusUnityLauncher? s_unityLauncher; | ||
| private TaskbarProgressState _taskbarProgressState; |
There was a problem hiding this comment.
The static DBusUnityLauncher field is accessed from multiple X11Window instances without synchronization. If SetTaskbarProgressState or SetTaskbarProgressValue are called concurrently from different windows (or threads), this could result in a race condition where TryCreate() is called multiple times simultaneously, potentially creating multiple launcher instances or causing issues with DBus connection registration. Consider using lazy initialization with proper thread safety, such as a Lazy<T> or adding a lock around the initialization check.
| static NSProgressIndicator* s_dockProgressIndicator; | ||
| static NSView* s_dockContentView; |
There was a problem hiding this comment.
The static dock progress indicator is shared across all WindowImpl instances, which means the progress indicator will be global to the entire application rather than per-window. This is inconsistent with the Windows implementation where each window can have its own taskbar progress indicator. While this might be a limitation of the macOS dock API, it should be documented, and consideration should be given to tracking which window last set the progress to avoid conflicts between multiple windows trying to set different progress values.
| static void EnsureDockProgressIndicator() { | ||
| if (WindowImpl::s_dockProgressIndicator == nullptr) { | ||
| NSDockTile *dockTile = [[NSApplication sharedApplication] dockTile]; | ||
| NSImageView *iconView = [[NSImageView alloc] init]; | ||
| [iconView setImage:[NSApplication sharedApplication].applicationIconImage]; | ||
|
|
||
| NSRect frame = NSMakeRect(0, 0, dockTile.size.width, 15); | ||
| WindowImpl::s_dockProgressIndicator = [[NSProgressIndicator alloc] initWithFrame:frame]; | ||
| [WindowImpl::s_dockProgressIndicator setStyle:NSProgressIndicatorStyleBar]; | ||
| [WindowImpl::s_dockProgressIndicator setMinValue:0.0]; | ||
| [WindowImpl::s_dockProgressIndicator setMaxValue:1.0]; | ||
| [WindowImpl::s_dockProgressIndicator setDoubleValue:0.0]; | ||
| [WindowImpl::s_dockProgressIndicator setHidden:YES]; | ||
|
|
||
| WindowImpl::s_dockContentView = [[NSView alloc] init]; | ||
| [WindowImpl::s_dockContentView addSubview:iconView]; | ||
| [WindowImpl::s_dockContentView addSubview:WindowImpl::s_dockProgressIndicator]; | ||
|
|
||
| [iconView setFrame:NSMakeRect(0, 0, dockTile.size.width, dockTile.size.height)]; | ||
|
|
||
| [dockTile setContentView:WindowImpl::s_dockContentView]; | ||
| } | ||
| } |
There was a problem hiding this comment.
The EnsureDockProgressIndicator() function is not thread-safe. If multiple threads call SetDockProgressState or SetDockProgressValue simultaneously, there's a race condition where the initialization check could pass for multiple threads, potentially causing multiple allocations or inconsistent state. Consider using dispatch_once or another synchronization mechanism to ensure thread-safe initialization.
| AvaloniaProperty.Register<Window, TaskbarProgressState>(nameof(TaskbarProgressState), TaskbarProgressState.None); | ||
|
|
||
| /// <summary> | ||
| /// Defines the <see cref="TaskbarProgressValue"/> property. |
There was a problem hiding this comment.
The TaskbarProgressValue property doesn't validate that the input is within the expected range of 0.0 to 1.0. While the documentation states this range, accepting values outside this range could lead to unexpected behavior in platform implementations. Consider adding validation to clamp or throw an exception for out-of-range values, or document that values outside this range have undefined behavior.
| /// Defines the <see cref="TaskbarProgressValue"/> property. | |
| /// Defines the <see cref="TaskbarProgressValue"/> property. | |
| /// The value is expected to be in the range from 0.0 to 1.0. Values outside this range | |
| /// have undefined behavior and may lead to unexpected results in platform implementations. |
src/Avalonia.Controls/Window.cs
Outdated
| CreatePlatformImplBinding(CanMaximizeProperty, canMaximize => PlatformImpl!.SetCanMaximize(canMaximize)); | ||
| CreatePlatformImplBinding(ShowInTaskbarProperty, show => PlatformImpl!.ShowTaskbarIcon(show)); | ||
| CreatePlatformImplBinding(TaskbarProgressStateProperty, state => PlatformImpl!.SetTaskbarProgressState(state)); | ||
| CreatePlatformImplBinding(TaskbarProgressValueProperty, value => PlatformImpl!.SetTaskbarProgressValue((ulong)(value * 1000), 1000)); |
There was a problem hiding this comment.
The conversion from double (0.0-1.0) to ulong uses a fixed scale factor of 1000. This means the progress value only has 1000 discrete steps. While this is likely sufficient for visual progress indicators, it's an arbitrary precision loss that might not match the underlying platform APIs' capabilities. Consider documenting this precision limitation or using a larger scale factor if the platform APIs support higher precision.
| public static readonly StyledProperty<TaskbarProgressState> TaskbarProgressStateProperty = | ||
| AvaloniaProperty.Register<Window, TaskbarProgressState>(nameof(TaskbarProgressState), TaskbarProgressState.None); | ||
|
|
||
| /// <summary> | ||
| /// Defines the <see cref="TaskbarProgressValue"/> property. | ||
| /// </summary> | ||
| public static readonly StyledProperty<double> TaskbarProgressValueProperty = | ||
| AvaloniaProperty.Register<Window, double>(nameof(TaskbarProgressValue), 0.0); |
There was a problem hiding this comment.
The new TaskbarProgressState and TaskbarProgressValue properties lack test coverage. Other Window properties like Title have corresponding tests in WindowTests.cs that verify the property values are correctly passed to the platform implementation. Consider adding tests similar to Setting_Title_Should_Set_Impl_Title to verify that these properties correctly invoke SetTaskbarProgressState and SetTaskbarProgressValue on the platform implementation.
| { | ||
| EmitUpdate(_desktopUri, new Dictionary<string, VariantValue> | ||
| { | ||
| ["progress"] = (double)progress, |
There was a problem hiding this comment.
The explicit cast to double is redundant since 'progress' is already of type double. This adds unnecessary noise to the code.
| ["progress"] = (double)progress, | |
| ["progress"] = progress, |
src/Avalonia.X11/X11Window.cs
Outdated
| private static DBusUnityLauncher? s_unityLauncher; | ||
| private TaskbarProgressState _taskbarProgressState; |
There was a problem hiding this comment.
The static DBusUnityLauncher instance is shared across all X11Window instances, which means the progress indicator will be global to the entire application rather than per-window. This is inconsistent with the Windows and macOS implementations where each window can have its own progress indicator. Consider making this an instance field instead, or document that this is a known limitation of the Unity launcher protocol.
| public void SetTaskbarProgressState(TaskbarProgressState state) | ||
| { | ||
| _taskbarProgressState = state; | ||
| s_unityLauncher ??= DBusUnityLauncher.TryCreate(); | ||
| if (s_unityLauncher is null) | ||
| return; | ||
|
|
||
| if (state == TaskbarProgressState.None) | ||
| s_unityLauncher.SetProgress(0, false); | ||
| } |
There was a problem hiding this comment.
When TaskbarProgressState is set to any value other than None, this method only updates the state but doesn't update the visibility or progress value. This means if the state changes from None to Normal/Error/Paused, the progress indicator won't become visible until SetTaskbarProgressValue is called. Consider calling SetProgress with the current stored progress value to ensure the indicator becomes visible immediately when the state changes.
| static void EnsureDockProgressIndicator() { | ||
| if (WindowImpl::s_dockProgressIndicator == nullptr) { | ||
| NSDockTile *dockTile = [[NSApplication sharedApplication] dockTile]; | ||
| NSImageView *iconView = [[NSImageView alloc] init]; | ||
| [iconView setImage:[NSApplication sharedApplication].applicationIconImage]; | ||
|
|
||
| NSRect frame = NSMakeRect(0, 0, dockTile.size.width, 15); | ||
| WindowImpl::s_dockProgressIndicator = [[NSProgressIndicator alloc] initWithFrame:frame]; | ||
| [WindowImpl::s_dockProgressIndicator setStyle:NSProgressIndicatorStyleBar]; | ||
| [WindowImpl::s_dockProgressIndicator setMinValue:0.0]; | ||
| [WindowImpl::s_dockProgressIndicator setMaxValue:1.0]; | ||
| [WindowImpl::s_dockProgressIndicator setDoubleValue:0.0]; | ||
| [WindowImpl::s_dockProgressIndicator setHidden:YES]; | ||
|
|
||
| WindowImpl::s_dockContentView = [[NSView alloc] init]; | ||
| [WindowImpl::s_dockContentView addSubview:iconView]; | ||
| [WindowImpl::s_dockContentView addSubview:WindowImpl::s_dockProgressIndicator]; | ||
|
|
||
| [iconView setFrame:NSMakeRect(0, 0, dockTile.size.width, dockTile.size.height)]; | ||
|
|
||
| [dockTile setContentView:WindowImpl::s_dockContentView]; | ||
| } |
There was a problem hiding this comment.
The memory allocated for iconView, s_dockProgressIndicator, and s_dockContentView is never released. While these are intended to be long-lived static objects, there's no cleanup mechanism if the application needs to reset or release these resources. In Objective-C with ARC, these objects will be retained as long as the static pointers hold references to them, but consider whether a cleanup method might be needed for cases where the application wants to restore the default dock icon appearance.
|
Why are all non-Windows implementations global and not tied to a Window? |
|
Please read the following Contributor License Agreement (CLA). If you agree with the CLA, please reply with the following: Contributor License AgreementContribution License AgreementThis Contribution License Agreement ( “Agreement” ) is agreed to by the party signing below ( “You” ), 1. Definitions. “Code” means the computer software code, whether in human-readable or machine-executable form, “Project” means any of the projects owned or managed by AvaloniaUI OÜ and offered under a license “Submit” is the act of uploading, submitting, transmitting, or distributing code or other content to any “Submission” means the Code and any other copyrightable material Submitted by You, including any 2. Your Submission. You must agree to the terms of this Agreement before making a Submission to any 3. Originality of Work. You represent that each of Your Submissions is entirely Your 4. Your Employer. References to “employer” in this Agreement include Your employer or anyone else 5. Licenses. a. Copyright License. You grant AvaloniaUI OÜ, and those who receive the Submission directly b. Patent License. You grant AvaloniaUI OÜ, and those who receive the Submission directly or c. Other Rights Reserved. Each party reserves all rights not expressly granted in this Agreement. 6. Representations and Warranties. You represent that You are legally entitled to grant the above 7. Notice to AvaloniaUI OÜ. You agree to notify AvaloniaUI OÜ in writing of any facts or 8. Information about Submissions. You agree that contributions to Projects and information about 9. Governing Law/Jurisdiction. This Agreement is governed by the laws of the Republic of Estonia, and 10. Entire Agreement/Assignment. This Agreement is the entire agreement between the parties, and AvaloniaUI OÜ dedicates this Contribution License Agreement to the public domain according to the Creative Commons CC0 1. |
- Move dock progress statics to file-scope in WindowImpl.mm to fix private access error in free function - Use dispatch_once for thread-safe initialization (macOS) - Use Lazy<T> for thread-safe DBus launcher initialization (Linux) - Add API compatibility suppression entries for new IWindowImpl methods - Clamp TaskbarProgressValue to 0.0-1.0 range - Show progress bar immediately when state changes to non-None (Linux)
The TaskBarList COM object on Windows is also static/global - it's a single COM instance per process (private static IntPtr s_taskBarList). The difference is that the Windows ITaskbarList3 API accepts an HWND parameter to target a specific window, while macOS (NSDockTile) and Linux (Unity Launcher DBus API) don't have a per-window identifier. They operate at the application level. So the static approach is consistent across all platforms; Windows just happens to support per-window dispatch through the HWND parameter. |
@cla-avalonia agree |
| if (connection is null) | ||
| return null; | ||
|
|
||
| var appId = Assembly.GetEntryAssembly()?.GetName().Name |
There was a problem hiding this comment.
There is zero guarantee that assembly name / process name would match a desktop file.
There was a problem hiding this comment.
I. e. it's not uncommon to auto-create a .desktop entry in ~/.local/share/applications named like org.telegram.desktop._d9f9f629990a1d1bb0bb3ac9e74581d0.desktop
|
Notes from the API review meeting: We decided to create a class solely to store dock-related properties for this PR and future work. The property used for the progress will be Please wait for #20634 to be merged first. |
Detect desktop file name from GIO_LAUNCHED_DESKTOP_FILE or BAMF_DESKTOP_FILE_HINT environment variables set by desktop environments, with fallback to assembly name and a warning log.
Implement cross-platform taskbar/dock progress indicator