Skip to content

V3/mac menu fixes#4312

Merged
atterpac merged 4 commits intowailsapp:v3-alpha-chore/menu-fixes-2from
atterpac:v3/mac-menu-fixes
May 29, 2025
Merged

V3/mac menu fixes#4312
atterpac merged 4 commits intowailsapp:v3-alpha-chore/menu-fixes-2from
atterpac:v3/mac-menu-fixes

Conversation

@atterpac
Copy link
Copy Markdown
Member

@atterpac atterpac commented May 27, 2025

Fixes infinite loop on startup and fixes example to match changes to API

Summary by CodeRabbit

  • New Features

    • Introduced a unified interface for menu elements, enabling consistent handling of menus and menu items with unique IDs and global registration.
    • Added advanced menu element management methods for insertion, removal, lookup, cloning, and recursive updates.
    • Extended menu items with unique IDs, parent references, and fluent API methods for improved customization and lifecycle management.
  • Bug Fixes

    • Improved submenu label updates to ensure changes are only applied when necessary, with better error handling and status messages.
    • Enhanced application menu setup on macOS to ensure menus are fully initialized before being set, with added error reporting for unexpected states.
  • Refactor

    • Refined native menu construction on macOS to differentiate submenu and item processing, improving platform-specific menu handling.
    • Updated method calls on menu items to use new fluent chaining methods for accelerators, roles, and click handlers, ensuring API consistency.

atterpac added 3 commits May 27, 2025 09:28
still bugs present but this fixes the window not rendering due to an update loop on startup
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 27, 2025

Walkthrough

The changes improve submenu label updates in the menu example by using stored submenu references with error handling and detailed output. The macOS application menu setup is refactored for explicit initialization, error reporting, and safer native menu assignment. The Menu and MenuItem types are refactored to implement a shared interface with unique IDs, enabling unified element management, lookup, insertion, removal, and cloning. Method calls on MenuItem are updated to use new fluent interface methods.

Changes

File(s) Change Summary
v3/examples/menu/main.go Replaced label lookups with stored submenu references for label updates; added error handling and detailed status output
v3/pkg/application/application_darwin.go Refactored macOS menu setup: explicit menu implementation initialization, error handling, and safer native menu assignment
v3/pkg/application/menu.go Introduced MenuElementInterface; unified Menu and MenuItem handling; added unique IDs, global registry, recursive updates, lookup, insertion, removal, cloning
v3/pkg/application/menu_darwin.go Updated native menu update logic to distinguish Menu and MenuItem types explicitly; used exported Label() method
v3/pkg/application/menuitem.go Added unique element IDs and parent references to MenuItem; implemented MenuElementInterface; added cloning, destruction, and fluent API methods
v3/pkg/application/menuitem_dev.go Updated method calls on MenuItem to use fluent interface methods (SetAcceleratorItem, OnClickItem)
v3/pkg/application/menuitem_roles.go Replaced calls to SetAccelerator, SetRole, and OnClick with new fluent interface methods on MenuItem
v3/pkg/application/menuitem_test.go Updated tests to use SetAcceleratorItem instead of SetAccelerator on MenuItem

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant MainApp
    participant Submenu

    User->>MainApp: Request submenu label update
    MainApp->>Submenu: Check submenu reference and current label
    alt Submenu reference is valid and label differs
        Submenu->>Submenu: Set new label
        MainApp->>MainApp: Call menu.Update()
        MainApp->>User: Print success and verification messages
    else Submenu reference is nil
        MainApp->>User: Print error message
    else Label already set
        MainApp->>User: Print no update needed message
    end
Loading
sequenceDiagram
    participant macosApp
    participant Menu
    participant macosMenuImpl
    participant MainThread

    macosApp->>Menu: Check if menu is nil
    alt menu is nil
        macosApp->>Menu: Create default menu
        Menu->>Menu: Update to initialize impl and nsMenu
        Menu->>macosApp: Return impl
        alt impl is nil
            macosApp->>macosApp: Print error and abort
        else
            macosApp->>MainThread: Set native menu pointer
        end
    else
        macosApp->>Menu: Check impl
        alt impl is nil
            macosApp->>Menu: Initialize impl (process radio groups, create macosMenu)
            macosMenuImpl->>macosMenuImpl: update()
        end
        alt impl not macosMenuImpl
            macosApp->>macosApp: Print critical error and abort
        else
            macosApp->>MainThread: Set native menu pointer
        end
    end
Loading

Possibly related PRs

  • More control over menus #4031: Related enhancements to menu management methods such as Clear(), Destroy(), Append(), and Prepend() in the menu package.

Suggested reviewers

  • leaanthony

Poem

🐇
A menu’s label, crisp and neat,
Now updates with a nimble feat.
Submenus checked, errors caught,
Mac’s menu logic finely wrought.
With Label() new and shining bright,
The code hops forward, pure delight!
—✨

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@atterpac
Copy link
Copy Markdown
Member Author

This PR has been expanded to beyond just Mac fixes and is a rewrite of how menus are organized

@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
3.9% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
v3/pkg/application/menu.go (1)

118-127: Consider optimizing label-based lookups.

The current implementation performs a linear search through all elements, which could be slow with many menu items. Consider maintaining a secondary map for label-to-element mapping.

For better performance with frequent label lookups, you could maintain a secondary index:

var (
    globalMenuRegistry      = make(map[string]MenuElementInterface)
    globalLabelRegistry     = make(map[string][]MenuElementInterface) // labels can be non-unique
    globalMenuRegistryMutex sync.RWMutex
    globalElementIDCounter  uint64
)

Then update registration/unregistration to maintain both indexes.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 642b6ea and ae4a217.

📒 Files selected for processing (6)
  • v3/pkg/application/menu.go (8 hunks)
  • v3/pkg/application/menu_darwin.go (2 hunks)
  • v3/pkg/application/menuitem.go (3 hunks)
  • v3/pkg/application/menuitem_dev.go (1 hunks)
  • v3/pkg/application/menuitem_roles.go (19 hunks)
  • v3/pkg/application/menuitem_test.go (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
v3/pkg/application/menuitem_roles.go (3)
v3/pkg/application/roles.go (5)
  • Hide (21-21)
  • HideOthers (22-22)
  • Quit (36-36)
  • ToggleFullscreen (45-45)
  • Open (52-52)
v3/pkg/application/menuitem.go (2)
  • MenuItem (51-72)
  • NewMenuItem (74-84)
v3/pkg/application/context.go (1)
  • Context (3-6)
v3/pkg/application/menu_darwin.go (3)
v3/pkg/application/menu.go (1)
  • Menu (63-86)
v3/pkg/application/menuitem.go (2)
  • NewMenuItem (74-84)
  • MenuItem (51-72)
v3/pkg/application/roles.go (1)
  • ServicesMenu (18-18)
🔇 Additional comments (18)
v3/pkg/application/menuitem_dev.go (1)

7-13: LGTM!

The method calls have been correctly updated to use the new fluent interface methods (SetAcceleratorItem and OnClickItem) as part of the broader MenuItem API refactoring.

v3/pkg/application/menuitem_test.go (1)

17-17: Test updates correctly reflect the API changes.

The test cases have been properly updated to use SetAcceleratorItem instead of SetAccelerator, maintaining test coverage for the refactored MenuItem API.

Also applies to: 44-44

v3/pkg/application/menuitem_roles.go (1)

16-17: All method calls have been correctly updated to the new API.

The systematic replacement of SetAcceleratorSetAcceleratorItem, SetRoleSetRoleItem, and OnClickOnClickItem has been applied consistently throughout all menu item factory functions. Platform-specific logic and accelerator overrides have been preserved correctly.

Also applies to: 22-23, 36-36, 38-38, 51-51, 53-53, 65-65, 68-68, 80-80, 83-83, 95-95, 98-98, 110-110, 115-115, 118-118, 136-137, 144-144, 147-147, 163-163, 170-171, 181-182, 192-193, 203-204, 211-211, 219-220, 230-231, 241-242, 252-253, 263-263, 273-273, 283-283, 288-288, 301-301, 306-307, 312-312, 317-317, 322-322, 327-327, 332-332, 337-337, 342-342, 347-347, 352-352, 357-357

v3/pkg/application/menu_darwin.go (3)

77-77: Good encapsulation improvement.

Using the Label() method instead of direct field access aligns with the new interface-based design.


87-100: Menu-as-submenu handling looks correct.

The explicit type checking and handling for Menu items is a good improvement for type safety. The temporary MenuItem creation pattern (lines 93-96) appears necessary to bridge between the Go menu structure and the native NSMenu API.


103-136: MenuItem processing is well-structured.

The refactored code properly handles different menu item types with clear separation of concerns. Good improvements:

  • Using Hidden() method instead of direct field access (line 119)
  • Proper bitmap handling within the MenuItem context (lines 127-134)
  • Preserved ServicesMenu role handling (lines 113-115)
v3/pkg/application/menuitem.go (6)

18-38: Thread-safe implementation looks good!

The global menu item registry is properly protected with mutex locks, and the ID generation uses atomic operations for thread safety.


53-72: Good struct enhancements for interface implementation.

The addition of elementID and parentMenu fields properly supports the new MenuElementInterface design and hierarchical menu structure.


139-232: Excellent implementation of MenuElementInterface!

The interface methods are well-implemented with:

  • Proper nil checks for platform implementations
  • Consistent label synchronization between menu items and their submenus
  • Good error handling in SetAccelerator
  • Recursive update propagation for submenus

329-355: Well-implemented deep copy functionality.

The Clone method properly:

  • Generates new unique IDs to avoid conflicts
  • Performs deep copies of all nested structures
  • Registers the cloned item in the global registries

357-380: Thorough cleanup in Destroy method.

The method properly handles all cleanup tasks:

  • Unregisters from both global registries
  • Destroys platform implementations
  • Recursively destroys submenus
  • Clears all references to prevent memory leaks

523-557: Good convenience methods for fluent chaining.

These wrapper methods maintain backward compatibility while allowing fluent method chaining by returning *MenuItem instead of the interface type.

v3/pkg/application/menu.go (6)

14-37: Well-designed interface for unified menu element handling.

The MenuElementInterface provides a clean abstraction that enables:

  • Uniform handling of both Menu and MenuItem types
  • Method chaining with the interface return type
  • Clear separation of concerns (identity, state, behavior)

63-86: Excellent improvements to Menu struct design.

The changes enable:

  • Flexible element types through interface usage
  • Fast O(1) lookups with local item registry
  • Proper thread safety with read-write mutex
  • Support for mixed menu hierarchies

260-303: Well-structured update propagation logic.

The method properly:

  • Recursively updates all child elements
  • Handles radio group processing
  • Safely updates attached windows with proper locking

Note: The convertToOldMenu() method currently returns self - ensure platform-specific implementations override this if needed.


396-423: Robust implementation of InsertAtIndex.

The method properly:

  • Handles bounds checking with clamping
  • Correctly inserts elements in the slice
  • Updates the registry
  • Sets parent references for both Menu and MenuItem types

596-635: Well-implemented radio group processing.

The method correctly:

  • Groups consecutive radio menu items
  • Handles type conversions for backward compatibility
  • Recursively processes submenus
  • Properly closes radio groups when encountering non-radio items

677-685: Correct implementation of Prepend.

The method properly inserts all items at the beginning of the menu while maintaining their relative order.

Comment on lines 86 to 94
func NewMenuItemSeparator() *MenuItem {
result := &MenuItem{
id: uint(atomic.AddUintptr(&menuItemID, 1)),
itemType: separator,
id: uint(atomic.AddUintptr(&menuItemID, 1)),
elementID: generateElementID(),
itemType: separator,
}
registerElement(result)
return result
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Missing menu item map registration for separators.

The separator constructor doesn't call addToMenuItemMap(result), which is inconsistent with other menu item constructors. This could cause issues when trying to look up separators by ID using getMenuItemByID().

Apply this diff to fix the registration:

 func NewMenuItemSeparator() *MenuItem {
 	result := &MenuItem{
 		id:        uint(atomic.AddUintptr(&menuItemID, 1)),
 		elementID: generateElementID(),
 		itemType:  separator,
 	}
+	addToMenuItemMap(result)
 	registerElement(result)
 	return result
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func NewMenuItemSeparator() *MenuItem {
result := &MenuItem{
id: uint(atomic.AddUintptr(&menuItemID, 1)),
itemType: separator,
id: uint(atomic.AddUintptr(&menuItemID, 1)),
elementID: generateElementID(),
itemType: separator,
}
registerElement(result)
return result
}
func NewMenuItemSeparator() *MenuItem {
result := &MenuItem{
id: uint(atomic.AddUintptr(&menuItemID, 1)),
elementID: generateElementID(),
itemType: separator,
}
addToMenuItemMap(result)
registerElement(result)
return result
}
🤖 Prompt for AI Agents
In v3/pkg/application/menuitem.go around lines 86 to 94, the
NewMenuItemSeparator function creates a separator MenuItem but does not register
it in the menu item map by calling addToMenuItemMap(result). To fix this, add a
call to addToMenuItemMap(result) after creating the MenuItem and before
returning it, ensuring separators are properly registered and can be looked up
by ID.

Comment on lines +268 to +270
func (m *MenuItem) RemoveAccelerator() {
m.accelerator = nil
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Platform implementation not updated when removing accelerator.

The method sets the accelerator to nil but doesn't propagate this change to the platform implementation, which could cause the accelerator to still function.

Apply this diff to fix the issue:

 func (m *MenuItem) RemoveAccelerator() {
 	m.accelerator = nil
+	if m.impl != nil {
+		m.impl.setAccelerator(nil)
+	}
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (m *MenuItem) RemoveAccelerator() {
m.accelerator = nil
}
func (m *MenuItem) RemoveAccelerator() {
m.accelerator = nil
if m.impl != nil {
m.impl.setAccelerator(nil)
}
}
🤖 Prompt for AI Agents
In v3/pkg/application/menuitem.go around lines 268 to 270, the RemoveAccelerator
method sets the accelerator field to nil but does not update the
platform-specific implementation, causing the accelerator to remain active.
Modify the method to also call the appropriate platform implementation function
to remove or clear the accelerator, ensuring the change is fully propagated.

Comment on lines +210 to +223
func (m *Menu) AddElement(element MenuElementInterface) *Menu {
m.registryMutex.Lock()
defer m.registryMutex.Unlock()

m.items = append(m.items, element)
m.itemRegistry[element.ID()] = element

// Set parent reference
if menu, ok := element.(*Menu); ok {
menu.parentMenu = m
}

return m
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Missing parent reference for MenuItem elements.

The method sets the parent reference for Menu elements but not for MenuItem elements, even though MenuItem has a parentMenu field.

Apply this diff to fix the parent reference:

 func (m *Menu) AddElement(element MenuElementInterface) *Menu {
 	m.registryMutex.Lock()
 	defer m.registryMutex.Unlock()
 
 	m.items = append(m.items, element)
 	m.itemRegistry[element.ID()] = element
 
 	// Set parent reference
 	if menu, ok := element.(*Menu); ok {
 		menu.parentMenu = m
+	} else if item, ok := element.(*MenuItem); ok {
+		item.parentMenu = m
 	}
 
 	return m
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (m *Menu) AddElement(element MenuElementInterface) *Menu {
m.registryMutex.Lock()
defer m.registryMutex.Unlock()
m.items = append(m.items, element)
m.itemRegistry[element.ID()] = element
// Set parent reference
if menu, ok := element.(*Menu); ok {
menu.parentMenu = m
}
return m
}
func (m *Menu) AddElement(element MenuElementInterface) *Menu {
m.registryMutex.Lock()
defer m.registryMutex.Unlock()
m.items = append(m.items, element)
m.itemRegistry[element.ID()] = element
// Set parent reference
if menu, ok := element.(*Menu); ok {
menu.parentMenu = m
} else if item, ok := element.(*MenuItem); ok {
item.parentMenu = m
}
return m
}
🤖 Prompt for AI Agents
In v3/pkg/application/menu.go around lines 210 to 223, the AddElement method
sets the parentMenu field only for elements of type Menu but not for MenuItem
elements, which also have a parentMenu field. To fix this, add a similar check
for MenuItem type elements and set their parentMenu field to the current Menu
instance (m) inside the method, ensuring both Menu and MenuItem elements have
their parent references correctly assigned.

Comment on lines +525 to +533
func (m *Menu) Clear() {
for _, item := range m.items {
if menuItem, ok := item.(*MenuItem); ok {
removeMenuItemByID(menuItem.id)
}
unregisterElement(item.ID())
}
m.items = nil
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Incomplete cleanup - submenus are not destroyed.

The Clear method only cleans up MenuItem elements but ignores Menu elements (submenus), which could lead to resource leaks.

Apply this diff to properly clean up all elements:

 func (m *Menu) Clear() {
 	for _, item := range m.items {
 		if menuItem, ok := item.(*MenuItem); ok {
 			removeMenuItemByID(menuItem.id)
+		} else if submenu, ok := item.(*Menu); ok {
+			submenu.Destroy()
 		}
 		unregisterElement(item.ID())
 	}
 	m.items = nil
+	m.itemRegistry = make(map[string]MenuElementInterface)
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (m *Menu) Clear() {
for _, item := range m.items {
if menuItem, ok := item.(*MenuItem); ok {
removeMenuItemByID(menuItem.id)
}
unregisterElement(item.ID())
}
m.items = nil
}
func (m *Menu) Clear() {
for _, item := range m.items {
if menuItem, ok := item.(*MenuItem); ok {
removeMenuItemByID(menuItem.id)
} else if submenu, ok := item.(*Menu); ok {
submenu.Destroy()
}
unregisterElement(item.ID())
}
m.items = nil
m.itemRegistry = make(map[string]MenuElementInterface)
}
🤖 Prompt for AI Agents
In v3/pkg/application/menu.go around lines 525 to 533, the Clear method
currently only cleans up MenuItem elements and does not handle Menu elements
(submenus). To fix this, update the Clear method to check if each item is a
MenuItem or a Menu. For MenuItem, call removeMenuItemByID with its id; for Menu
(submenu), call its Clear method recursively to ensure proper cleanup. Then
unregister the element by its ID as before and finally set m.items to nil.

@atterpac atterpac merged commit a3c0024 into wailsapp:v3-alpha-chore/menu-fixes-2 May 29, 2025
7 of 9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant