5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-02 22:13:36 +08:00
This commit is contained in:
Lea Anthony 2025-03-13 07:17:51 +11:00
parent 8abcd81f30
commit b879742571
No known key found for this signature in database
GPG Key ID: 33DAF7BB90A58405
9 changed files with 892 additions and 72 deletions

View File

@ -43,6 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add doc comments for Service API by [@fbbdev](https://github.com/fbbdev) in [#4024](https://github.com/wailsapp/wails/pull/4024) - Add doc comments for Service API by [@fbbdev](https://github.com/fbbdev) in [#4024](https://github.com/wailsapp/wails/pull/4024)
- Add function `application.NewServiceWithOptions` to initialise services with additional configuration by [@leaanthony](https://github.com/leaanthony) in [#4024](https://github.com/wailsapp/wails/pull/4024) - Add function `application.NewServiceWithOptions` to initialise services with additional configuration by [@leaanthony](https://github.com/leaanthony) in [#4024](https://github.com/wailsapp/wails/pull/4024)
- Improved menu control by [@FalcoG](https://github.com/FalcoG) and [@leaanthony](https://github.com/leaanthony) in [#4031](https://github.com/wailsapp/wails/pull/4031) - Improved menu control by [@FalcoG](https://github.com/FalcoG) and [@leaanthony](https://github.com/leaanthony) in [#4031](https://github.com/wailsapp/wails/pull/4031)
- Added index-based menu operations (`ItemAt`, `InsertAt`, etc.) and consistent visibility control for all menu item types by [@leaanthony](https://github.com/leaanthony)
- More documentation by [@leaanthony](https://github.com/leaanthony) - More documentation by [@leaanthony](https://github.com/leaanthony)
- Support cancellation of events in standard event listeners by [@leaanthony](https://github.com/leaanthony) - Support cancellation of events in standard event listeners by [@leaanthony](https://github.com/leaanthony)
- Systray `Hide`, `Show` and `Destroy` support by [@leaanthony](https://github.com/leaanthony) - Systray `Hide`, `Show` and `Destroy` support by [@leaanthony](https://github.com/leaanthony)

View File

@ -91,6 +91,21 @@ checkbox.SetChecked(true)
isChecked := checkbox.Checked() isChecked := checkbox.Checked()
``` ```
### Visibility
Control whether a menu item is visible using the `SetHidden` method, which works for all menu item types including submenus:
```go
menuItem := menu.Add("Regular Item")
menuItem.SetHidden(true) // Hide the item
menuItem.SetHidden(false) // Show the item
// For submenus, use the same method
submenuItem := menu.AddSubmenu("Advanced Options")
submenuItem.SetHidden(true) // Hide the submenu
submenuItem.SetHidden(false) // Show the submenu
```
### Accelerators ### Accelerators
Add keyboard shortcuts to menu items: Add keyboard shortcuts to menu items:
@ -106,6 +121,71 @@ Common accelerator modifiers:
- `Alt`: Option on macOS - `Alt`: Option on macOS
- `Ctrl`: Control key on all platforms - `Ctrl`: Control key on all platforms
## Menu Operations
### Adding Items
Standard methods for adding items to the end of a menu:
```go
menu.Add("Regular Item")
menu.AddCheckbox("Checkbox Item", false)
menu.AddRadio("Radio Item", false)
menu.AddSubmenu("Submenu")
menu.AddSeparator()
```
### Accessing Items by Index
Retrieve menu items using their index position:
```go
firstItem := menu.ItemAt(0)
secondItem := menu.ItemAt(1)
// Check if an item exists at the given index
if item := menu.ItemAt(5); item != nil {
// Item exists, use it
}
```
### Inserting Items by Index
Insert items at specific positions in the menu. If the index is out of bounds, the item will be inserted at the beginning (for negative indices) or appended to the end (for indices greater than the menu length):
```go
// Insert a regular item at index 0 (beginning of the menu)
menu.InsertAt(0, "First Item")
// Insert a separator at index 2
menu.InsertSeparatorAt(2)
// Insert a checkbox at index 3
menu.InsertCheckboxAt(3, "Enable Feature", false)
// Insert a radio button at index 4
menu.InsertRadioAt(4, "Option", true)
// Insert a submenu at index 5
submenu := menu.InsertSubmenuAt(5, "More Options")
// Insert an existing menu item at index 6
existingItem := NewMenuItem("Existing Item")
menu.InsertItemAt(6, existingItem)
// Handling out-of-bounds indices
menu.InsertAt(-1, "Will be inserted at the beginning")
menu.InsertAt(999, "Will be appended to the end")
```
### Menu Size
Get the number of items in a menu:
```go
count := menu.Count()
```
## Event Handling ## Event Handling
### Click Events ### Click Events
@ -151,6 +231,7 @@ menuItem := menu.Add("Initial State")
menuItem.SetLabel("New Label") menuItem.SetLabel("New Label")
menuItem.SetEnabled(false) menuItem.SetEnabled(false)
menuItem.SetChecked(true) menuItem.SetChecked(true)
menuItem.SetHidden(true) // Hide the item
// Apply changes // Apply changes
menu.Update() menu.Update()
@ -169,6 +250,8 @@ After modifying menu items, call `Update()` on the parent menu to apply the chan
5. Keep radio groups focused on a single choice 5. Keep radio groups focused on a single choice
6. Update menu items to reflect application state 6. Update menu items to reflect application state
7. Handle all possible states in click handlers 7. Handle all possible states in click handlers
8. Use index-based operations for dynamic menu restructuring
9. Use `SetHidden` consistently for all menu item types, including submenus
:::tip[Pro Tip] :::tip[Pro Tip]
When sharing event handlers, use the `ctx.ClickedMenuItem()` method to determine which item triggered the event and handle it accordingly. When sharing event handlers, use the `ctx.ClickedMenuItem()` method to determine which item triggered the event and handle it accordingly.
@ -186,3 +269,37 @@ Menu appearance and behaviour varies by platform:
:::danger[Warning] :::danger[Warning]
Always test menu functionality across all supported platforms, as behaviour and appearance may vary significantly. Always test menu functionality across all supported platforms, as behaviour and appearance may vary significantly.
::: :::
## Example: Dynamic Menu Management
This example demonstrates how to dynamically manage menu items using the index-based operations:
```go
// Create a menu with some initial items
menu := application.NewMenu()
menu.Add("Item 1")
menu.Add("Item 3") // Intentionally skipping Item 2
// Later, insert the missing item at the correct position
menu.InsertAt(1, "Item 2")
// Get the count of items
fmt.Printf("Menu has %d items\n", menu.Count())
// Access items by index
for i := 0; i < menu.Count(); i++ {
item := menu.ItemAt(i)
fmt.Printf("Item at index %d: %s\n", i, item.Label())
}
// Show/hide a submenu based on application state
submenu := menu.AddSubmenu("Advanced Options")
if userIsAdvanced {
submenu.SetHidden(false) // Show the submenu
} else {
submenu.SetHidden(true) // Hide the submenu
}
// Apply all changes
menu.Update()
```

View File

@ -113,6 +113,120 @@ func main() {
ctx.ClickedMenuItem().SetLabel("Unhide the beatles!") ctx.ClickedMenuItem().SetLabel("Unhide the beatles!")
} }
}) })
// ---- New index-based menu operations demo ----
indexMenu := menu.AddSubmenu("Index Operations")
// Add some initial items
indexMenu.Add("Item 0")
indexMenu.Add("Item 2")
indexMenu.Add("Item 4")
// Demonstrate inserting items at specific indices
indexMenu.InsertAt(1, "Item 1").OnClick(func(*application.Context) {
println("Item 1 clicked")
})
indexMenu.InsertAt(3, "Item 3").OnClick(func(*application.Context) {
println("Item 3 clicked")
})
// Demonstrate inserting different types of items at specific indices
indexMenu.AddSeparator()
indexMenu.InsertCheckboxAt(6, "Checkbox at index 6", true).OnClick(func(ctx *application.Context) {
println("Checkbox at index 6 clicked, checked:", ctx.ClickedMenuItem().Checked())
})
indexMenu.InsertRadioAt(7, "Radio at index 7", true).OnClick(func(ctx *application.Context) {
println("Radio at index 7 clicked")
})
indexMenu.InsertSeparatorAt(8)
// Create a submenu and insert it at a specific index
submenuAtIndex := indexMenu.InsertSubmenuAt(9, "Inserted Submenu")
submenuAtIndex.Add("Submenu Item 1")
submenuAtIndex.Add("Submenu Item 2")
// Demonstrate ItemAt to access items by index
indexMenu.AddSeparator()
indexMenu.Add("Get Item at Index").OnClick(func(*application.Context) {
// Get the item at index 2 and change its label
if item := indexMenu.ItemAt(2); item != nil {
println("Item at index 2:", item.Label())
item.SetLabel("Item 2 (Modified)")
}
})
// Demonstrate Count method
indexMenu.Add("Count Items").OnClick(func(*application.Context) {
println("Menu has", indexMenu.Count(), "items")
})
// Demonstrate visibility control for different item types
visibilityMenu := menu.AddSubmenu("Visibility Control")
// Regular menu item
regularItem := visibilityMenu.Add("Regular Item")
// Checkbox menu item
checkboxItem := visibilityMenu.AddCheckbox("Checkbox Item", true)
// Radio menu item
radioItem := visibilityMenu.AddRadio("Radio Item", true)
// Separator
visibilityMenu.AddSeparator()
separatorIndex := visibilityMenu.Count() - 1
separatorItem := visibilityMenu.ItemAt(separatorIndex)
// Submenu - get the MenuItem for the submenu to control visibility
submenuMenuItem := application.NewSubMenuItem("Submenu")
visibilityMenu.InsertItemAt(visibilityMenu.Count(), submenuMenuItem)
submenuContent := submenuMenuItem.GetSubmenu()
submenuContent.Add("Submenu Content")
// Controls for toggling visibility
visibilityMenu.AddSeparator()
visibilityMenu.Add("Toggle Regular Item").OnClick(func(*application.Context) {
regularItem.SetHidden(!regularItem.Hidden())
println("Regular item hidden:", regularItem.Hidden())
})
visibilityMenu.Add("Toggle Checkbox Item").OnClick(func(*application.Context) {
checkboxItem.SetHidden(!checkboxItem.Hidden())
println("Checkbox item hidden:", checkboxItem.Hidden())
})
visibilityMenu.Add("Toggle Radio Item").OnClick(func(*application.Context) {
radioItem.SetHidden(!radioItem.Hidden())
println("Radio item hidden:", radioItem.Hidden())
})
visibilityMenu.Add("Toggle Separator").OnClick(func(*application.Context) {
separatorItem.SetHidden(!separatorItem.Hidden())
println("Separator hidden:", separatorItem.Hidden())
})
// For submenu visibility, we need to toggle the visibility of the MenuItem that contains the submenu
visibilityMenu.Add("Toggle Submenu").OnClick(func(ctx *application.Context) {
// Log the current state before toggling
println("Submenu hidden before toggle:", submenuMenuItem.Hidden())
// Toggle the visibility
submenuMenuItem.SetHidden(!submenuMenuItem.Hidden())
// Log the new state after toggling
println("Submenu hidden after toggle:", submenuMenuItem.Hidden())
// Update the menu item label to reflect the current state
if submenuMenuItem.Hidden() {
ctx.ClickedMenuItem().SetLabel("Show Submenu")
} else {
ctx.ClickedMenuItem().SetLabel("Hide Submenu")
}
})
app.SetMenu(menu) app.SetMenu(menu)
window := app.NewWebviewWindow().SetBackgroundColour(application.NewRGB(33, 37, 41)) window := app.NewWebviewWindow().SetBackgroundColour(application.NewRGB(33, 37, 41))

View File

@ -190,6 +190,78 @@ func (m *Menu) ItemAt(index int) *MenuItem {
return m.items[index] return m.items[index]
} }
// InsertAt inserts a menu item at the specified index.
// If index is out of bounds, the item will be appended to the end of the menu.
// Returns the newly created menu item for further customisation.
func (m *Menu) InsertAt(index int, label string) *MenuItem {
result := NewMenuItem(label)
m.InsertItemAt(index, result)
return result
}
// InsertItemAt inserts an existing menu item at the specified index.
// If index is negative, the item will be inserted at the beginning of the menu.
// If index is greater than the current length, the item will be appended to the end of the menu.
// Returns the menu for method chaining.
func (m *Menu) InsertItemAt(index int, item *MenuItem) *Menu {
if index < 0 {
index = 0
}
if index > len(m.items) {
index = len(m.items)
}
if index == 0 {
m.items = append([]*MenuItem{item}, m.items...)
} else if index == len(m.items) {
m.items = append(m.items, item)
} else {
m.items = append(m.items[:index], append([]*MenuItem{item}, m.items[index:]...)...)
}
return m
}
// InsertSeparatorAt inserts a separator at the specified index.
// If index is out of bounds, the separator will be appended to the end of the menu.
// Returns the menu for method chaining.
func (m *Menu) InsertSeparatorAt(index int) *Menu {
result := NewMenuItemSeparator()
m.InsertItemAt(index, result)
return m
}
// InsertCheckboxAt inserts a checkbox menu item at the specified index.
// If index is out of bounds, the item will be appended to the end of the menu.
// Returns the newly created menu item for further customisation.
func (m *Menu) InsertCheckboxAt(index int, label string, enabled bool) *MenuItem {
result := NewMenuItemCheckbox(label, enabled)
m.InsertItemAt(index, result)
return result
}
// InsertRadioAt inserts a radio menu item at the specified index.
// If index is out of bounds, the item will be appended to the end of the menu.
// Returns the newly created menu item for further customisation.
func (m *Menu) InsertRadioAt(index int, label string, enabled bool) *MenuItem {
result := NewMenuItemRadio(label, enabled)
m.InsertItemAt(index, result)
return result
}
// InsertSubmenuAt inserts a submenu at the specified index.
// If index is out of bounds, the submenu will be appended to the end of the menu.
// Returns the newly created submenu for further customisation.
func (m *Menu) InsertSubmenuAt(index int, label string) *Menu {
result := NewSubMenuItem(label)
m.InsertItemAt(index, result)
return result.submenu
}
// Count returns the number of items in the menu.
func (m *Menu) Count() int {
return len(m.items)
}
// Clone recursively clones the menu and all its submenus. // Clone recursively clones the menu and all its submenus.
func (m *Menu) Clone() *Menu { func (m *Menu) Clone() *Menu {
result := &Menu{ result := &Menu{

View File

@ -62,20 +62,31 @@ func TestMenu_FindByLabel(t *testing.T) {
func TestMenu_ItemAt(t *testing.T) { func TestMenu_ItemAt(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
menu *application.Menu menu *application.Menu
index int index int
shouldError bool expectedLabel string
shouldBeNil bool
}{ }{
{ {
name: "Valid index", name: "Get first item",
menu: application.NewMenuFromItems( menu: application.NewMenuFromItems(
application.NewMenuItem("Item 1"), application.NewMenuItem("Item 1"),
application.NewMenuItem("Item 2"), application.NewMenuItem("Item 2"),
application.NewMenuItem("Target"),
), ),
index: 2, index: 0,
shouldError: false, expectedLabel: "Item 1",
shouldBeNil: false,
},
{
name: "Get last item",
menu: application.NewMenuFromItems(
application.NewMenuItem("Item 1"),
application.NewMenuItem("Item 2"),
),
index: 1,
expectedLabel: "Item 2",
shouldBeNil: false,
}, },
{ {
name: "Index out of bounds (negative)", name: "Index out of bounds (negative)",
@ -84,7 +95,7 @@ func TestMenu_ItemAt(t *testing.T) {
application.NewMenuItem("Item 2"), application.NewMenuItem("Item 2"),
), ),
index: -1, index: -1,
shouldError: true, shouldBeNil: true,
}, },
{ {
name: "Index out of bounds (too large)", name: "Index out of bounds (too large)",
@ -93,18 +104,379 @@ func TestMenu_ItemAt(t *testing.T) {
application.NewMenuItem("Item 2"), application.NewMenuItem("Item 2"),
), ),
index: 2, index: 2,
shouldError: true, shouldBeNil: true,
},
{
name: "Empty menu",
menu: application.NewMenu(),
index: 0,
shouldBeNil: true,
}, },
} }
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
item := test.menu.ItemAt(test.index) item := test.menu.ItemAt(test.index)
if test.shouldError && item != nil {
t.Errorf("Expected error, but found %v", item) if test.shouldBeNil {
if item != nil {
t.Errorf("Expected nil item, but got item with label: %s", item.Label())
}
} else {
if item == nil {
t.Errorf("Expected item with label %s, but got nil", test.expectedLabel)
} else if item.Label() != test.expectedLabel {
t.Errorf("Expected item with label %s, but got %s", test.expectedLabel, item.Label())
}
} }
if !test.shouldError && item == nil { })
t.Errorf("Expected item, but found none") }
}
func TestMenu_InsertAt(t *testing.T) {
tests := []struct {
name string
setupMenu func() *application.Menu
index int
label string
expectedLabels []string
}{
{
name: "Insert at beginning",
setupMenu: func() *application.Menu {
return application.NewMenuFromItems(
application.NewMenuItem("Item 1"),
application.NewMenuItem("Item 2"),
)
},
index: 0,
label: "New Item",
expectedLabels: []string{"New Item", "Item 1", "Item 2"},
},
{
name: "Insert in middle",
setupMenu: func() *application.Menu {
return application.NewMenuFromItems(
application.NewMenuItem("Item 1"),
application.NewMenuItem("Item 2"),
)
},
index: 1,
label: "New Item",
expectedLabels: []string{"Item 1", "New Item", "Item 2"},
},
{
name: "Insert at end",
setupMenu: func() *application.Menu {
return application.NewMenuFromItems(
application.NewMenuItem("Item 1"),
application.NewMenuItem("Item 2"),
)
},
index: 2,
label: "New Item",
expectedLabels: []string{"Item 1", "Item 2", "New Item"},
},
{
name: "Insert with negative index (should insert at beginning)",
setupMenu: func() *application.Menu {
return application.NewMenuFromItems(
application.NewMenuItem("Item 1"),
application.NewMenuItem("Item 2"),
)
},
index: -1,
label: "New Item",
expectedLabels: []string{"New Item", "Item 1", "Item 2"},
},
{
name: "Insert with index too large (should append)",
setupMenu: func() *application.Menu {
return application.NewMenuFromItems(
application.NewMenuItem("Item 1"),
application.NewMenuItem("Item 2"),
)
},
index: 10,
label: "New Item",
expectedLabels: []string{"Item 1", "Item 2", "New Item"},
},
{
name: "Insert into empty menu",
setupMenu: func() *application.Menu {
return application.NewMenu()
},
index: 0,
label: "New Item",
expectedLabels: []string{"New Item"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
menu := test.setupMenu()
menu.InsertAt(test.index, test.label)
// Verify the menu has the correct number of items
if menu.Count() != len(test.expectedLabels) {
t.Errorf("Expected menu to have %d items, but got %d", len(test.expectedLabels), menu.Count())
}
// Verify each item has the expected label
for i, expectedLabel := range test.expectedLabels {
item := menu.ItemAt(i)
if item == nil {
t.Errorf("Expected item at index %d, but got nil", i)
} else if item.Label() != expectedLabel {
t.Errorf("Expected item at index %d to have label %s, but got %s", i, expectedLabel, item.Label())
}
}
})
}
}
func TestMenu_InsertItemAt(t *testing.T) {
tests := []struct {
name string
setupMenu func() *application.Menu
index int
itemLabel string
expectedLabels []string
}{
{
name: "Insert existing item at beginning",
setupMenu: func() *application.Menu {
return application.NewMenuFromItems(
application.NewMenuItem("Item 1"),
application.NewMenuItem("Item 2"),
)
},
index: 0,
itemLabel: "Existing Item",
expectedLabels: []string{"Existing Item", "Item 1", "Item 2"},
},
{
name: "Insert existing item in middle",
setupMenu: func() *application.Menu {
return application.NewMenuFromItems(
application.NewMenuItem("Item 1"),
application.NewMenuItem("Item 2"),
)
},
index: 1,
itemLabel: "Existing Item",
expectedLabels: []string{"Item 1", "Existing Item", "Item 2"},
},
{
name: "Insert existing item at end",
setupMenu: func() *application.Menu {
return application.NewMenuFromItems(
application.NewMenuItem("Item 1"),
application.NewMenuItem("Item 2"),
)
},
index: 2,
itemLabel: "Existing Item",
expectedLabels: []string{"Item 1", "Item 2", "Existing Item"},
},
{
name: "Insert with negative index (should insert at beginning)",
setupMenu: func() *application.Menu {
return application.NewMenuFromItems(
application.NewMenuItem("Item 1"),
application.NewMenuItem("Item 2"),
)
},
index: -1,
itemLabel: "Existing Item",
expectedLabels: []string{"Existing Item", "Item 1", "Item 2"},
},
{
name: "Insert with index too large (should append)",
setupMenu: func() *application.Menu {
return application.NewMenuFromItems(
application.NewMenuItem("Item 1"),
application.NewMenuItem("Item 2"),
)
},
index: 10,
itemLabel: "Existing Item",
expectedLabels: []string{"Item 1", "Item 2", "Existing Item"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
menu := test.setupMenu()
existingItem := application.NewMenuItem(test.itemLabel)
menu.InsertItemAt(test.index, existingItem)
// Verify the menu has the correct number of items
if menu.Count() != len(test.expectedLabels) {
t.Errorf("Expected menu to have %d items, but got %d", len(test.expectedLabels), menu.Count())
}
// Verify each item has the expected label
for i, expectedLabel := range test.expectedLabels {
item := menu.ItemAt(i)
if item == nil {
t.Errorf("Expected item at index %d, but got nil", i)
} else if item.Label() != expectedLabel {
t.Errorf("Expected item at index %d to have label %s, but got %s", i, expectedLabel, item.Label())
}
}
})
}
}
func TestMenu_SpecializedInsertFunctions(t *testing.T) {
t.Run("InsertSeparatorAt", func(t *testing.T) {
menu := application.NewMenuFromItems(
application.NewMenuItem("Item 1"),
application.NewMenuItem("Item 2"),
)
menu.InsertSeparatorAt(1)
// Verify the separator was inserted
if menu.Count() != 3 {
t.Errorf("Expected menu to have 3 items, but got %d", menu.Count())
}
separator := menu.ItemAt(1)
if separator == nil {
t.Errorf("Expected separator at index 1, but got nil")
} else if !separator.IsSeparator() {
t.Errorf("Expected item at index 1 to be a separator, but it wasn't")
}
})
t.Run("InsertCheckboxAt", func(t *testing.T) {
menu := application.NewMenuFromItems(
application.NewMenuItem("Item 1"),
application.NewMenuItem("Item 2"),
)
menu.InsertCheckboxAt(1, "Checkbox", true)
// Verify the checkbox was inserted
if menu.Count() != 3 {
t.Errorf("Expected menu to have 3 items, but got %d", menu.Count())
}
checkbox := menu.ItemAt(1)
if checkbox == nil {
t.Errorf("Expected checkbox at index 1, but got nil")
} else if !checkbox.IsCheckbox() {
t.Errorf("Expected item at index 1 to be a checkbox, but it wasn't")
} else if checkbox.Label() != "Checkbox" {
t.Errorf("Expected checkbox to have label 'Checkbox', but got '%s'", checkbox.Label())
} else if !checkbox.Checked() {
t.Errorf("Expected checkbox to be checked, but it wasn't")
}
})
t.Run("InsertRadioAt", func(t *testing.T) {
menu := application.NewMenuFromItems(
application.NewMenuItem("Item 1"),
application.NewMenuItem("Item 2"),
)
menu.InsertRadioAt(1, "Radio", true)
// Verify the radio button was inserted
if menu.Count() != 3 {
t.Errorf("Expected menu to have 3 items, but got %d", menu.Count())
}
radio := menu.ItemAt(1)
if radio == nil {
t.Errorf("Expected radio button at index 1, but got nil")
} else if !radio.IsRadio() {
t.Errorf("Expected item at index 1 to be a radio button, but it wasn't")
} else if radio.Label() != "Radio" {
t.Errorf("Expected radio button to have label 'Radio', but got '%s'", radio.Label())
} else if !radio.Checked() {
t.Errorf("Expected radio button to be checked, but it wasn't")
}
})
t.Run("InsertSubmenuAt", func(t *testing.T) {
menu := application.NewMenuFromItems(
application.NewMenuItem("Item 1"),
application.NewMenuItem("Item 2"),
)
submenu := menu.InsertSubmenuAt(1, "Submenu")
submenu.Add("Submenu Item")
// Verify the submenu was inserted
if menu.Count() != 3 {
t.Errorf("Expected menu to have 3 items, but got %d", menu.Count())
}
submenuItem := menu.ItemAt(1)
if submenuItem == nil {
t.Errorf("Expected submenu at index 1, but got nil")
} else if !submenuItem.IsSubmenu() {
t.Errorf("Expected item at index 1 to be a submenu, but it wasn't")
} else if submenuItem.Label() != "Submenu" {
t.Errorf("Expected submenu to have label 'Submenu', but got '%s'", submenuItem.Label())
}
// Verify the submenu has the expected item
submenuFromItem := submenuItem.GetSubmenu()
if submenuFromItem == nil {
t.Errorf("Expected to get submenu from item, but got nil")
} else if submenuFromItem.Count() != 1 {
t.Errorf("Expected submenu to have 1 item, but got %d", submenuFromItem.Count())
} else {
submenuItemFromSubmenu := submenuFromItem.ItemAt(0)
if submenuItemFromSubmenu == nil {
t.Errorf("Expected item in submenu, but got nil")
} else if submenuItemFromSubmenu.Label() != "Submenu Item" {
t.Errorf("Expected submenu item to have label 'Submenu Item', but got '%s'", submenuItemFromSubmenu.Label())
}
}
})
}
func TestMenu_Count(t *testing.T) {
tests := []struct {
name string
menu *application.Menu
expectedCount int
}{
{
name: "Menu with items",
menu: application.NewMenuFromItems(
application.NewMenuItem("Item 1"),
application.NewMenuItem("Item 2"),
application.NewMenuItem("Item 3"),
),
expectedCount: 3,
},
{
name: "Empty menu",
menu: application.NewMenu(),
expectedCount: 0,
},
{
name: "Menu with separator",
menu: application.NewMenuFromItems(
application.NewMenuItem("Item 1"),
application.NewMenuItemSeparator(),
application.NewMenuItem("Item 2"),
),
expectedCount: 3,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
count := test.menu.Count()
if count != test.expectedCount {
t.Errorf("Expected count to be %d, but got %d", test.expectedCount, count)
} }
}) })
} }

View File

@ -52,6 +52,7 @@ func (w *windowsMenu) processMenu(parentMenu w32.HMENU, inputMenu *Menu) {
globalApplication.removeKeyBinding(item.accelerator.String()) globalApplication.removeKeyBinding(item.accelerator.String())
} }
} }
continue
} }
flags := uint32(w32.MF_STRING) flags := uint32(w32.MF_STRING)
@ -88,11 +89,6 @@ func (w *windowsMenu) processMenu(parentMenu w32.HMENU, inputMenu *Menu) {
} }
var menuText = w32.MustStringToUTF16Ptr(thisText) var menuText = w32.MustStringToUTF16Ptr(thisText)
// If the item is hidden, don't append
if item.Hidden() {
continue
}
w32.AppendMenu(parentMenu, flags, uintptr(itemID), menuText) w32.AppendMenu(parentMenu, flags, uintptr(itemID), menuText)
if item.bitmap != nil { if item.bitmap != nil {
w32.SetMenuIcons(parentMenu, itemID, item.bitmap, nil) w32.SetMenuIcons(parentMenu, itemID, item.bitmap, nil)

View File

@ -341,44 +341,22 @@ func (m *MenuItem) SetChecked(checked bool) *MenuItem {
return m return m
} }
func (m *MenuItem) SetHidden(hidden bool) *MenuItem {
m.hidden = hidden
if m.impl != nil {
m.impl.setHidden(m.hidden)
}
return m
}
// GetSubmenu returns the submenu of the MenuItem.
// If the MenuItem is not a submenu, it returns nil.
func (m *MenuItem) GetSubmenu() *Menu {
return m.submenu
}
func (m *MenuItem) Checked() bool {
return m.checked
}
func (m *MenuItem) IsSeparator() bool {
return m.itemType == separator
}
func (m *MenuItem) IsSubmenu() bool {
return m.itemType == submenu
}
func (m *MenuItem) IsCheckbox() bool {
return m.itemType == checkbox
}
func (m *MenuItem) IsRadio() bool {
return m.itemType == radio
}
func (m *MenuItem) Hidden() bool { func (m *MenuItem) Hidden() bool {
return m.hidden return m.hidden
} }
// SetHidden sets whether the menu item is hidden.
// This works for all menu item types, including submenus.
// Returns the menu item for method chaining.
func (m *MenuItem) SetHidden(hidden bool) *MenuItem {
m.hidden = hidden
if m.impl != nil {
m.impl.setHidden(hidden)
}
return m
}
// OnClick sets the callback function for when the menu item is clicked.
func (m *MenuItem) OnClick(f func(*Context)) *MenuItem { func (m *MenuItem) OnClick(f func(*Context)) *MenuItem {
m.callback = f m.callback = f
return m return m
@ -454,3 +432,27 @@ func (m *MenuItem) Destroy() {
m.radioGroupMembers = nil m.radioGroupMembers = nil
} }
func (m *MenuItem) GetSubmenu() *Menu {
return m.submenu
}
func (m *MenuItem) IsSeparator() bool {
return m.itemType == separator
}
func (m *MenuItem) IsSubmenu() bool {
return m.itemType == submenu
}
func (m *MenuItem) IsCheckbox() bool {
return m.itemType == checkbox
}
func (m *MenuItem) IsRadio() bool {
return m.itemType == radio
}
func (m *MenuItem) Checked() bool {
return m.checked
}

View File

@ -59,3 +59,121 @@ func TestMenuItem_RemoveAccelerator(t *testing.T) {
}) })
} }
} }
func TestMenuItem_SetHidden(t *testing.T) {
tests := []struct {
name string
setupMenuItem func() *application.MenuItem
setHidden bool
expectedState bool
}{
{
name: "Hide regular menu item",
setupMenuItem: func() *application.MenuItem {
return application.NewMenuItem("Regular Item")
},
setHidden: true,
expectedState: true,
},
{
name: "Show regular menu item",
setupMenuItem: func() *application.MenuItem {
return application.NewMenuItem("Regular Item").SetHidden(true)
},
setHidden: false,
expectedState: false,
},
{
name: "Hide checkbox menu item",
setupMenuItem: func() *application.MenuItem {
return application.NewMenuItemCheckbox("Checkbox Item", true)
},
setHidden: true,
expectedState: true,
},
{
name: "Hide radio menu item",
setupMenuItem: func() *application.MenuItem {
return application.NewMenuItemRadio("Radio Item", true)
},
setHidden: true,
expectedState: true,
},
{
name: "Hide separator",
setupMenuItem: func() *application.MenuItem {
return application.NewMenuItemSeparator()
},
setHidden: true,
expectedState: true,
},
{
name: "Hide submenu",
setupMenuItem: func() *application.MenuItem {
submenu := application.NewSubmenu("Submenu", application.NewMenu())
return submenu
},
setHidden: true,
expectedState: true,
},
{
name: "Show submenu",
setupMenuItem: func() *application.MenuItem {
submenu := application.NewSubmenu("Submenu", application.NewMenu())
submenu.SetHidden(true)
return submenu
},
setHidden: false,
expectedState: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
menuItem := test.setupMenuItem()
// Set the hidden state
menuItem.SetHidden(test.setHidden)
// Verify the hidden state
if menuItem.Hidden() != test.expectedState {
t.Errorf("Expected hidden state to be %v, but got %v", test.expectedState, menuItem.Hidden())
}
})
}
}
func TestMenuItem_SubmenuVisibility(t *testing.T) {
t.Run("Submenu visibility through SetHidden", func(t *testing.T) {
// Create a menu with a submenu
menu := application.NewMenu()
// Create a submenu using NewSubMenuItem directly to get the MenuItem
submenuItem := application.NewSubMenuItem("Submenu")
menu.InsertItemAt(0, submenuItem)
// Get the submenu from the menu item
submenu := submenuItem.GetSubmenu()
// Add some items to the submenu
submenu.Add("Submenu Item 1")
submenu.Add("Submenu Item 2")
// Initially, the submenu should be visible
if submenuItem.Hidden() {
t.Errorf("Expected submenu to be visible initially, but it was hidden")
}
// Hide the submenu
submenuItem.SetHidden(true)
if !submenuItem.Hidden() {
t.Errorf("Expected submenu to be hidden after SetHidden(true), but it was visible")
}
// Show the submenu again
submenuItem.SetHidden(false)
if submenuItem.Hidden() {
t.Errorf("Expected submenu to be visible after SetHidden(false), but it was hidden")
}
})
}

View File

@ -24,35 +24,63 @@ type windowsMenuItem struct {
} }
func (m *windowsMenuItem) setHidden(hidden bool) { func (m *windowsMenuItem) setHidden(hidden bool) {
if hidden && !m.hidden { // Only process if the visibility state is changing
m.hidden = true if hidden == m.hidden {
// iterate the parent items and find the menu item after us return
}
m.hidden = hidden
// For submenus, we need special handling
if m.menuItem.submenu != nil {
// Find our position in the parent menu
var position int = -1
for i, item := range m.parent.items { for i, item := range m.parent.items {
if item == m.menuItem { if item == m.menuItem {
if i < len(m.parent.items)-1 { position = i
m.itemAfter = m.parent.items[i+1]
} else {
m.itemAfter = nil
}
break break
} }
} }
// Remove from parent menu
w32.RemoveMenu(m.hMenu, m.id, w32.MF_BYCOMMAND) if position == -1 {
} else if !hidden && m.hidden { // Can't find our position, can't proceed
m.hidden = false return
// Add to parent menu before the "itemAfter" }
var pos int
if m.itemAfter != nil { if hidden {
// When hiding, we need to remove the menu item by position
w32.RemoveMenu(m.hMenu, position, w32.MF_BYPOSITION)
} else {
// When showing, we need to insert the menu item at the correct position
// Create a new menu info for this item
menuInfo := m.getMenuInfo()
w32.InsertMenuItem(m.hMenu, uint32(position), true, menuInfo)
}
} else {
// For regular menu items, we can use the command ID
if hidden {
w32.RemoveMenu(m.hMenu, int(m.id), w32.MF_BYCOMMAND)
} else {
// Find the position to insert at
var position int = 0
for i, item := range m.parent.items { for i, item := range m.parent.items {
if item == m.itemAfter { if item == m.menuItem {
pos = i - 1 position = i
break break
} }
} }
m.itemAfter = nil menuInfo := m.getMenuInfo()
w32.InsertMenuItem(m.hMenu, uint32(position), true, menuInfo)
}
}
// If we have a parent window, redraw the menu
if m.parent.impl != nil {
if windowsImpl, ok := m.parent.impl.(*windowsMenu); ok {
if windowsImpl.parentWindow != nil {
w32.DrawMenuBar(windowsImpl.parentWindow.hwnd)
}
} }
w32.InsertMenuItem(m.hMenu, uint32(pos), true, m.getMenuInfo())
} }
} }