Skip to content

Comments

[macOS] Add NativeDock.Menu API for adding menu items to macOS dock icon#20634

Merged
MrJul merged 30 commits intomasterfrom
dev/timill/DockMenu
Feb 19, 2026
Merged

[macOS] Add NativeDock.Menu API for adding menu items to macOS dock icon#20634
MrJul merged 30 commits intomasterfrom
dev/timill/DockMenu

Conversation

@drasticactions
Copy link
Contributor

@drasticactions drasticactions commented Feb 7, 2026

What does the pull request do?

This PR adds support for a native Dock menu interop for macOS, allowing apps to customize the menu shown when right-clicking the app icon in the dock.

For this, I added some new APIs to the Avalonia.Native codebase and sample usage in ControlGallery. If you right click the dock in ControlGallery, you can open a new window or pull up the main window.

As this adds new APIs, we'll need to do an API review.

What is the current behavior?

There is currently no way to add dock menu items in Avalonia, without pulling in the .NET MacOS SDK bindings and doing it yourself.

What is the updated/expected behavior with this PR?

In Application.xaml

  <NativeDock.Menu>
    <NativeMenu>
      <NativeMenuItem Header="New Window" Click="OnDockNewWindowClicked"/>
      <NativeMenuItemSeparator/>
      <NativeMenuItem Header="Show Main Window" Click="OnDockShowMainWindowClicked"/>
    </NativeMenu>
  </NativeDock.Menu>
        public void OnDockNewWindowClicked(object? sender, EventArgs e)
        {
            if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime)
            {
                var window = new MainWindow();
                window.Show();
            }
        }

        public void OnDockShowMainWindowClicked(object? sender, EventArgs e)
        {
            if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime)
            {
                desktopLifetime.MainWindow?.Activate();
            }
        }

NativeDock.Menu lets you add menu items to the macOS dock icon.

How was the solution implemented (if it's not obvious)?

I refactored the NativeMenu API to be usable for both the Native taskbar menu and dock for macOS and added the interop code to the Avalonia.Native dylib. The API should function similar to what is done for NativeMenu, only it's for the dock. This only supports macOS.

Checklist

  • Added unit tests (if possible)? (tests/Avalonia.Controls.UnitTests/NativeMenuTests.cs)
  • Added XML documentation to any related classes?
  • Consider submitting a PR to https://github.com/AvaloniaUI/avalonia-docs with user documentation

Breaking changes

Obsoletions / Deprecations

Fixed issues

@timunie timunie added the customer-priority Issue reported by a customer with a support agreement. label Feb 7, 2026
@avaloniaui-bot
Copy link

You can test this PR using the following package version. 12.0.999-cibuild0062029-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@MrJul MrJul added feature needs-api-review The PR adds new public APIs that should be reviewed. backport-candidate-11.3.x Consider this PR for backporting to 11.3 branch os-macos labels Feb 7, 2026

namespace Avalonia.Native
{
internal class AvaloniaNativeDockMenuExporter : INativeMenuExporter, INativeMenuExporterResetHandler
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need a separate exporter when tray menu reuses the shared one?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MenuExporter = new AvaloniaNativeMenuExporter(_native, factory);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest to just change that ctor of AvaloniaNativeMenuExporter to accept Action<IAvnMenu> and reuse the existing class

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my head I was thinking about NativeMenu.Menu and didn't consider what TrayIcon did. I'll try changing it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated with 3cdf0d4

using Avalonia.UnitTests;
using Xunit;

namespace Avalonia.Controls.UnitTests
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have 6 tests for an attached property registration.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what happens when you use Claude for writing tests and don't actually look at it. Sorry, should have caught that these are basically useless.

Removed and replaced with Appium tests, 464ad50. These just validate that the dock items can be updated, checking the dock itself with Appium seemed very flakey on my machine so I want to validate it doesn't throw before going too crazy.




virtual HRESULT SetDockMenu(IAvnMenu* dockMenu) override
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if this is called after initial startup?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It updates the dock menu. You can dynamically update the dock menu with new menu items (Although IMO it's a bad idea to do it because it won't be obvious to users that the items have changed). I updated the tests and the ControlGallery app (cc90f5c) to show it.

スクリーンショット 2026-02-08 10 47 21

return s_appMenuItem;
}

static IAvnMenu* s_dockMenu = nullptr;
Copy link
Member

@kekekeks kekekeks Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, you can just save NSMenu* reference somewhere in app.mm, no need to keep a COM reference here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was following the pattern with what was used for NativeMenu Menu's (Basically copy/paste it but with "Dock" instead.) Yeah, this could be simplified, changed with f47b0d6

@avaloniaui-bot
Copy link

You can test this PR using the following package version. 12.0.999-cibuild0062031-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@maxkatz6
Copy link
Member

maxkatz6 commented Feb 8, 2026

Related #7487
And #8567

@drasticactions
Copy link
Contributor Author

Related #7487 And #8567

For #8567, this could be used as a starting point for a feature like that. I don't want to implement that wholesale for this PR, since this needs to be back ported to 11.x and I rather not add more APIs than needed for that, but for 12.x I could see it.

@@ -0,0 +1,8 @@
namespace Avalonia.Native
{
internal interface INativeMenuExporterResetHandler
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part of the change set isn't needed anymore

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, will revert.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted with 6582192

Comment on lines 48 to 49
private Action _queueReset = null!;
private Action _updateIfNeeded = null!;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since I got rid of the common interface, I changed the internal calls in IAvnMenu to Actions so they could be called by both Menu and DockMenu calls. If you feel this should be different I'm open to it.

@avaloniaui-bot
Copy link

You can test this PR using the following package version. 12.0.999-cibuild0062035-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@avaloniaui-bot
Copy link

You can test this PR using the following package version. 12.0.999-cibuild0062037-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@drasticactions
Copy link
Contributor Author

Re. d8c34cc

Between Github CI runners and Appium, finding a way to interact with the Dock in macOS has proven very hard. Looking at the TrayIcon tests, those seemed to have been turned off for similar reasons. I'm still thinking of how to solve that, but the test that invokes the dock menu is running and working.

@avaloniaui-bot
Copy link

You can test this PR using the following package version. 12.0.999-cibuild0062152-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@avaloniaui-bot
Copy link

You can test this PR using the following package version. 12.0.999-cibuild0062201-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

{
internal class AvaloniaNativeMenuExporter : ITopLevelNativeMenuExporter
{
internal enum MenuTarget { Application, Window, TrayIcon, Dock }
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic before was checking against which object in the exporter was null. Adding an enum to check against this when it's created and using a switch seemed safer to me, but I'm open to changes to this.

@avaloniaui-bot
Copy link

You can test this PR using the following package version. 12.0.999-cibuild0062211-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@avaloniaui-bot
Copy link

You can test this PR using the following package version. 12.0.999-cibuild0062275-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@avaloniaui-bot
Copy link

You can test this PR using the following package version. 12.0.999-cibuild0062299-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

Copy link
Member

@MrJul MrJul left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@MrJul MrJul enabled auto-merge February 17, 2026 11:07
@avaloniaui-bot
Copy link

You can test this PR using the following package version. 12.0.999-cibuild0062350-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@MrJul MrJul dismissed kekekeks’s stale review February 19, 2026 11:01

Requested changes have been made.

@MrJul MrJul added this pull request to the merge queue Feb 19, 2026
Merged via the queue into master with commit ef70264 Feb 19, 2026
11 checks passed
@MrJul MrJul deleted the dev/timill/DockMenu branch February 19, 2026 13:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api-approved The new public APIs have been approved. backport-candidate-11.3.x Consider this PR for backporting to 11.3 branch customer-priority Issue reported by a customer with a support agreement. feature os-macos

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants