diff --git a/docs/src/content/docs/changelog.mdx b/docs/src/content/docs/changelog.mdx
index 6ad587ab4..7ad0571f4 100644
--- a/docs/src/content/docs/changelog.mdx
+++ b/docs/src/content/docs/changelog.mdx
@@ -36,6 +36,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add window to context when calling a service method by [@leaanthony](https://github.com/leaanthony)
- Add `window-call` example to demonstrate how to know which window is calling a service by [@leaanthony](https://github.com/leaanthony)
- Better panic handling by [@leaanthony](https://github.com/leaanthony)
+- New Menu guide by [@leaanthony](https://github.com/leaanthony)
### Fixed
@@ -50,6 +51,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Removed `application.WindowIDKey` and `application.WindowNameKey` (replaced by `application.WindowKey`) by [@leaanthony](https://github.com/leaanthony)
- In JS/TS bindings, class fields of fixed-length array types are now initialized with their expected length instead of being empty by [@fbbdev](https://github.com/fbbdev) in [#4001](https://github.com/wailsapp/wails/pull/4001)
+- ContextMenuData now returns a string instead of any by [@leaanthony](https://github.com/leaanthony)
## v3.0.0-alpha.9 - 2025-01-13
diff --git a/docs/src/content/docs/guides/menus.mdx b/docs/src/content/docs/guides/menus.mdx
new file mode 100644
index 000000000..ed2de62d8
--- /dev/null
+++ b/docs/src/content/docs/guides/menus.mdx
@@ -0,0 +1,491 @@
+---
+title: Menus
+description: A guide to creating and customising menus in Wails v3
+---
+
+Wails v3 provides a powerful menu system that allows you to create both application menus and context menus. This guide will walk you through the various features and capabilities of the menu system.
+
+### Creating a Menu
+
+To create a new menu, use the `NewMenu()` method from your application instance:
+
+```go
+menu := application.NewMenu()
+```
+
+### Adding Menu Items
+
+Wails supports several types of menu items, each serving a specific purpose:
+
+#### Regular Menu Items
+Regular menu items are the basic building blocks of menus. They display text and can trigger actions when clicked:
+
+```go
+menuItem := menu.Add("Click Me")
+```
+
+#### Checkboxes
+Checkbox menu items provide a toggleable state, useful for enabling/disabling features or settings:
+
+```go
+checkbox := menu.AddCheckbox("My checkbox", true) // true = initially checked
+```
+
+#### Radio Groups
+Radio groups allow users to select one option from a set of mutually exclusive choices. They are automatically created when radio items are placed next to each other:
+
+```go
+menu.AddRadio("Option 1", true) // true = initially selected
+menu.AddRadio("Option 2", false)
+menu.AddRadio("Option 3", false)
+```
+
+#### Separators
+Separators are horizontal lines that help organise menu items into logical groups:
+
+```go
+menu.AddSeparator()
+```
+
+#### Submenus
+Submenus are nested menus that appear when hovering over or clicking a menu item. They're useful for organizing complex menu structures:
+
+```go
+submenu := menu.AddSubmenu("File")
+submenu.Add("Open")
+submenu.Add("Save")
+```
+
+### Menu Item Properties
+
+Menu items have several properties that can be configured:
+
+| Property | Method | Description |
+|-------------|--------------------------|-----------------------------------------------------|
+| Label | `SetLabel(string)` | Sets the display text |
+| Enabled | `SetEnabled(bool)` | Enables/disables the item |
+| Checked | `SetChecked(bool)` | Sets the checked state (for checkboxes/radio items) |
+| Tooltip | `SetTooltip(string)` | Sets the tooltip text |
+| Hidden | `SetHidden(bool)` | Shows/hides the item |
+| Accelerator | `SetAccelerator(string)` | Sets the keyboard shortcut |
+
+### Menu Item States
+
+Menu items can be in different states that control their visibility and interactivity:
+
+#### Visibility
+
+Menu items can be shown or hidden dynamically using the `SetHidden()` method:
+
+```go
+menuItem := menu.Add("Dynamic Item")
+
+// Hide the menu item
+menuItem.SetHidden(true)
+
+// Show the menu item
+menuItem.SetHidden(false)
+
+// Check current visibility
+isHidden := menuItem.Hidden()
+```
+
+Hidden menu items are completely removed from the menu until shown again. This is useful for contextual menu items that should only appear in certain application states.
+
+#### Enabled State
+
+Menu items can be enabled or disabled using the `SetEnabled()` method:
+
+```go
+menuItem := menu.Add("Save")
+
+// Disable the menu item
+menuItem.SetEnabled(false) // Item appears grayed out and cannot be clicked
+
+// Enable the menu item
+menuItem.SetEnabled(true) // Item becomes clickable again
+
+// Check current enabled state
+isEnabled := menuItem.Enabled()
+```
+
+Disabled menu items remain visible but appear grayed out and cannot be clicked. This is commonly used to indicate that an action is currently unavailable, such as:
+- Disabling "Save" when there are no changes to save
+- Disabling "Copy" when nothing is selected
+- Disabling "Undo" when there's no action to undo
+
+#### Dynamic State Management
+
+You can combine these states with event handlers to create dynamic menus:
+
+```go
+saveMenuItem := menu.Add("Save")
+
+// Initially disable the Save menu item
+saveMenuItem.SetEnabled(false)
+
+// Enable Save only when there are unsaved changes
+documentChanged := func() {
+ saveMenuItem.SetEnabled(true)
+ menu.Update() // Remember to update the menu after changing states
+}
+
+// Disable Save after saving
+documentSaved := func() {
+ saveMenuItem.SetEnabled(false)
+ menu.Update()
+}
+```
+
+### Event Handling
+
+Menu items can respond to click events using the `OnClick` method:
+
+```go
+menuItem.OnClick(func(ctx *application.Context) {
+ // Handle the click event
+ println("Menu item clicked!")
+})
+```
+
+The context provides information about the clicked menu item:
+
+```go
+menuItem.OnClick(func(ctx *application.Context) {
+ // Get the clicked menu item
+ clickedItem := ctx.ClickedMenuItem()
+ // Get its current state
+ isChecked := clickedItem.Checked()
+})
+```
+
+### Role-Based Menu Items
+
+Wails provides a set of predefined menu roles that automatically create menu items with standard functionality. Here are the supported menu roles:
+
+#### Complete Menu Structures
+These roles create entire menu structures with common functionality:
+
+| Role | Description | Platform Notes |
+|------|-------------|----------------|
+| `AppMenu` | Application menu with About, Services, Hide/Show, and Quit | macOS only |
+| `EditMenu` | Standard Edit menu with Undo, Redo, Cut, Copy, Paste, etc. | All platforms |
+| `ViewMenu` | View menu with Reload, Zoom, and Fullscreen controls | All platforms |
+| `WindowMenu` | Window controls (Minimise, Zoom, etc.) | All platforms |
+| `HelpMenu` | Help menu with "Learn More" link to Wails website | All platforms |
+
+#### Individual Menu Items
+These roles can be used to add individual menu items:
+
+| Role | Description | Platform Notes |
+|------|-------------|----------------|
+| `About` | Show application About dialog | All platforms |
+| `Hide` | Hide application | macOS only |
+| `HideOthers` | Hide other applications | macOS only |
+| `UnHide` | Show hidden application | macOS only |
+| `CloseWindow` | Close current window | All platforms |
+| `Minimise` | Minimise window | All platforms |
+| `Zoom` | Zoom window | macOS only |
+| `Front` | Bring window to front | macOS only |
+| `Quit` | Quit application | All platforms |
+| `Undo` | Undo last action | All platforms |
+| `Redo` | Redo last action | All platforms |
+| `Cut` | Cut selection | All platforms |
+| `Copy` | Copy selection | All platforms |
+| `Paste` | Paste from clipboard | All platforms |
+| `PasteAndMatchStyle` | Paste and match style | macOS only |
+| `SelectAll` | Select all | All platforms |
+| `Delete` | Delete selection | All platforms |
+| `Reload` | Reload current page | All platforms |
+| `ForceReload` | Force reload current page | All platforms |
+| `ToggleFullscreen` | Toggle fullscreen mode | All platforms |
+| `ResetZoom` | Reset zoom level | All platforms |
+| `ZoomIn` | Increase zoom | All platforms |
+| `ZoomOut` | Decrease zoom | All platforms |
+
+Here's an example showing how to use both complete menus and individual roles:
+
+```go
+menu := application.NewMenu()
+
+// Add complete menu structures
+menu.AddRole(application.AppMenu) // macOS only
+menu.AddRole(application.EditMenu) // Common edit operations
+menu.AddRole(application.ViewMenu) // View controls
+menu.AddRole(application.WindowMenu) // Window controls
+
+// Add individual role-based items to a custom menu
+fileMenu := menu.AddSubmenu("File")
+fileMenu.AddRole(application.CloseWindow)
+fileMenu.AddSeparator()
+fileMenu.AddRole(application.Quit)
+```
+
+## Application Menus
+
+Application menus are the menus that appear at the top of your application window (Windows/Linux) or at the top of the screen (macOS).
+
+
+### Application Menu Behaviour
+
+When you set an application menu using `app.SetMenu()`, it becomes the default menu for all windows in your application. However, there are a few important behaviours to note:
+
+1. **Global Application Menu**: The menu set via `app.SetMenu()` acts as the default menu for all windows.
+
+2. **Per-Window Menu Override**: Individual windows can override the application menu by setting their own menu through window options:
+```go
+app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
+ Title: "Custom Menu Window",
+ Windows: application.WindowsWindow{
+ Menu: customMenu, // Override application menu for this window
+ },
+})
+```
+
+3. **Disable Window Menu**: On Windows, you can disable a window's menu completely even when there's a global application menu:
+```go
+app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
+ Title: "No Menu Window",
+ Windows: application.WindowsWindow{
+ DisableMenu: true, // Disable menu for this window
+ },
+})
+```
+
+Here's a complete example showing these different menu behaviours:
+
+```go
+func main() {
+ app := application.New(application.Options{})
+
+ // Create application menu
+ appMenu := application.NewMenu()
+ fileMenu := appMenu.AddSubmenu("File")
+ fileMenu.Add("New").OnClick(func(ctx *application.Context) {
+ // This will be available in all windows unless overridden
+ window := app.CurrentWindow()
+ window.SetTitle("New Window")
+ })
+
+ // Set as application menu - default for all windows
+ app.SetMenu(appMenu)
+
+ // Window with default application menu
+ app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
+ Title: "Default Menu",
+ })
+
+ // Window with custom menu
+ customMenu := application.NewMenu()
+ customMenu.Add("Custom Action")
+ app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
+ Title: "Custom Menu",
+ Windows: application.WindowsWindow{
+ Menu: customMenu,
+ },
+ })
+
+ // Window with no menu
+ app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
+ Title: "No Menu",
+ Windows: application.WindowsWindow{
+ DisableMenu: true,
+ },
+ })
+
+ app.Run()
+}
+```
+
+## Context Menus
+
+Context menus are popup menus that appear when right-clicking elements in your application. They provide quick access to relevant actions for the clicked element.
+
+### Default Context Menu
+
+The default context menu is the webview's built-in context menu that provides system-level operations such as:
+- Copy, Cut, and Paste for text manipulation
+- Text selection controls
+- Spell checking options
+
+#### Controlling the Default Context Menu
+
+You can control when the default context menu appears using the `--default-contextmenu` CSS property:
+
+```html
+
+
+
+
+
+
+
+
+
Custom context menu only
+
+
+
+
+
+
Select this text to see the default menu
+
+
+```
+
+#### Nested Context Menu Behavior
+
+When using the `--default-contextmenu` property on nested elements, the following rules apply:
+
+1. Child elements inherit their parent's context menu setting unless explicitly overridden
+2. The most specific (closest) setting takes precedence
+3. The `auto` value can be used to reset to default behaviour
+
+Example of nested context menu behaviour:
+
+```html
+
+
+
+
No context menu here
+
+
+
+
Context menu shown here
+
+
+ Also has context menu
+
+
+
+
Shows menu only when text is selected
+
+
+
+```
+
+### Custom Context Menus
+
+Custom context menus allow you to provide application-specific actions that are relevant to the element being clicked. They're particularly useful for:
+- File operations in a document manager
+- Image manipulation tools
+- Custom actions in a data grid
+- Component-specific operations
+
+#### Creating a Custom Context Menu
+
+When creating a custom context menu, you provide a unique identifier (name) that links the menu to HTML elements:
+
+```go
+// Create a context menu with identifier "imageMenu"
+contextMenu := application.NewContextMenu("imageMenu")
+```
+
+The name parameter ("imageMenu" in this example) serves as a unique identifier that will be used to:
+1. Link HTML elements to this specific context menu
+2. Identify which menu should be shown when right-clicking
+3. Allow menu updates and cleanup
+
+#### Context Data
+
+When handling context menu events, you can access both the clicked menu item and its associated context data:
+
+```go
+contextMenu.Add("Process").OnClick(func(ctx *application.Context) {
+ // Get the clicked menu item
+ menuItem := ctx.ClickedMenuItem()
+
+ // Get the context data as a string
+ contextData := ctx.ContextMenuData()
+
+ // Check if the menu item is checked (for checkbox/radio items)
+ isChecked := ctx.IsChecked()
+
+ // Use the data
+ if contextData != "" {
+ processItem(contextData)
+ }
+})
+```
+
+The context data is passed from the HTML element's `--custom-contextmenu-data` property and is available in the click handler through `ctx.ContextMenuData()`. This is particularly useful when:
+
+- Working with lists or grids where each item needs unique identification
+- Handling operations on specific components or elements
+- Passing state or metadata from the frontend to the backend
+
+#### Context Menu Management
+
+After making changes to a context menu, call the `Update()` method to apply the changes:
+
+```go
+contextMenu.Update()
+```
+
+When you no longer need a context menu, you can destroy it:
+
+```go
+contextMenu.Destroy()
+```
+:::danger[Warning]
+After calling `Destroy()`, using the context menu reference again will result in a panic.
+:::
+
+### Real-World Example: Image Gallery
+
+Here's a complete example of implementing a custom context menu for an image gallery:
+
+```go
+// Backend: Create the context menu
+imageMenu := application.NewContextMenu("imageMenu")
+
+// Add relevant operations
+imageMenu.Add("View Full Size").OnClick(func(ctx *application.Context) {
+ // Get the image ID from context data
+ if imageID := ctx.ContextMenuData(); imageID != "" {
+ openFullSizeImage(imageID)
+ }
+})
+
+imageMenu.Add("Download").OnClick(func(ctx *application.Context) {
+ if imageID := ctx.ContextMenuData(); imageID != "" {
+ downloadImage(imageID)
+ }
+})
+
+imageMenu.Add("Share").OnClick(func(ctx *application.Context) {
+ if imageID := ctx.ContextMenuData(); imageID != "" {
+ showShareDialog(imageID)
+ }
+})
+```
+
+```html
+
+
+
+
+
+ Nature Photo
+
+
+
+
+ City Photo
+
+
+```
+
+In this example:
+1. The context menu is created with the identifier "imageMenu"
+2. Each image container is linked to the menu using `--custom-contextmenu: imageMenu`
+3. Each container provides its image ID as context data using `--custom-contextmenu-data`
+4. The backend receives the image ID in click handlers and can perform specific operations
+5. The same menu is reused for all images, but the context data tells us which image to operate on
+
+This pattern is particularly powerful for:
+- Data grids where rows need specific operations
+- File managers where files need context-specific actions
+- Design tools where different elements need different operations
+- Any component where the same operations apply to multiple instances
diff --git a/v3/examples/contextmenus/main.go b/v3/examples/contextmenus/main.go
index 8aa15d37b..50aa0a132 100644
--- a/v3/examples/contextmenus/main.go
+++ b/v3/examples/contextmenus/main.go
@@ -24,7 +24,7 @@ func main() {
},
})
- mainWindow := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
+ app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Title: "Context Menu Demo",
Width: 1024,
Height: 1024,
@@ -35,22 +35,15 @@ func main() {
},
})
- contextMenu := app.NewMenu()
- contextMenu.Add("Click Me").OnClick(func(data *application.Context) {
+ contextMenu := application.NewContextMenu("test")
+ clickMe := contextMenu.Add("Set Menuitem label to Context Data")
+ contextDataMenuItem := contextMenu.Add("No Context Data")
+ clickMe.OnClick(func(data *application.Context) {
app.Logger.Info("Context menu", "context data", data.ContextMenuData())
+ contextDataMenuItem.SetLabel("Current context data: " + data.ContextMenuData())
+ contextMenu.Update()
})
- globalContextMenu := app.NewMenu()
- globalContextMenu.Add("Default context menu item").OnClick(func(data *application.Context) {
- app.Logger.Info("Context menu", "context data", data.ContextMenuData())
- })
-
- // Registering the menu with a window will make it available to that window only
- mainWindow.RegisterContextMenu("test", contextMenu)
-
- // Registering the menu with the app will make it available to all windows
- app.RegisterContextMenu("test", globalContextMenu)
-
err := app.Run()
if err != nil {
diff --git a/v3/examples/window-menu/README.md b/v3/examples/window-menu/README.md
new file mode 100644
index 000000000..7b3a517e4
--- /dev/null
+++ b/v3/examples/window-menu/README.md
@@ -0,0 +1,24 @@
+# Window Menu Example
+
+*** Windows Only ***
+
+This example demonstrates how to create a window with a menu bar that can be toggled using the window.ToggleMenuBar() method.
+
+## Features
+
+- Default menu bar with File, Edit, and Help menus
+- F1 key to toggle menu bar visibility
+- Simple HTML interface with instructions
+
+## Running the Example
+
+```bash
+cd v3/examples/window-menu
+go run .
+```
+
+## How it Works
+
+The example creates a window with a default menu and binds the F10 key to toggle the menu bar's visibility. The menu bar will hide when F10 is pressed and show when F10 is released.
+
+Note: The menu bar toggling functionality only works on Windows. On other platforms, the F10 key binding will have no effect.
diff --git a/v3/examples/window-menu/assets/about.html b/v3/examples/window-menu/assets/about.html
new file mode 100644
index 000000000..e887a84ce
--- /dev/null
+++ b/v3/examples/window-menu/assets/about.html
@@ -0,0 +1,14 @@
+
+
+ Window Menu Demo
+
+
+
+
+
About Window Menu Demo
+
Press F1 to toggle menu bar visibility
+
Press F2 to show menu bar
+
Press F3 to hide menu bar
+
+
+
\ No newline at end of file
diff --git a/v3/examples/window-menu/assets/index.html b/v3/examples/window-menu/assets/index.html
new file mode 100644
index 000000000..b18f601e0
--- /dev/null
+++ b/v3/examples/window-menu/assets/index.html
@@ -0,0 +1,48 @@
+
+
+ Window Menu Demo
+
+
+
+
+
Window Menu Demo
+
This example demonstrates the menu bar visibility toggle feature.
+
Press F1 to toggle the menu bar.
+
Press F2 to show the menu bar.
+
Press F3 to hide the menu bar.
+
The menu includes:
+
+
File menu with Exit option
+
MenuBar menu with Hide options
+
Help menu with About
+
+
+
+
\ No newline at end of file
diff --git a/v3/examples/window-menu/assets/style.css b/v3/examples/window-menu/assets/style.css
new file mode 100644
index 000000000..c7fc71f39
--- /dev/null
+++ b/v3/examples/window-menu/assets/style.css
@@ -0,0 +1,26 @@
+body {
+ font-family: system-ui, -apple-system, sans-serif;
+ margin: 0;
+ padding: 2rem;
+ background: #f5f5f5;
+ color: #333;
+}
+.container {
+ max-width: 600px;
+ margin: 0 auto;
+ background: white;
+ padding: 2rem;
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+}
+h1 {
+ margin-top: 0;
+ color: #2d2d2d;
+}
+.key {
+ background: #e9e9e9;
+ padding: 2px 8px;
+ border-radius: 4px;
+ border: 1px solid #ccc;
+ font-family: monospace;
+}
diff --git a/v3/examples/window-menu/main.go b/v3/examples/window-menu/main.go
new file mode 100644
index 000000000..03c2acb35
--- /dev/null
+++ b/v3/examples/window-menu/main.go
@@ -0,0 +1,64 @@
+package main
+
+import (
+ "embed"
+ _ "embed"
+ "github.com/wailsapp/wails/v3/pkg/application"
+ "log"
+)
+
+//go:embed assets/*
+var assets embed.FS
+
+func main() {
+ app := application.New(application.Options{
+ Name: "Window Menu Demo",
+ Description: "A demo of menu bar toggling",
+ Assets: application.AssetOptions{
+ Handler: application.BundledAssetFileServer(assets),
+ },
+ })
+
+ // Create a menu
+ menu := app.NewMenu()
+ fileMenu := menu.AddSubmenu("File")
+ fileMenu.Add("Exit").OnClick(func(ctx *application.Context) {
+ app.Quit()
+ })
+
+ editMenu := menu.AddSubmenu("MenuBar")
+ editMenu.Add("Hide MenuBar").OnClick(func(ctx *application.Context) {
+ app.CurrentWindow().HideMenuBar()
+ })
+
+ helpMenu := menu.AddSubmenu("Help")
+ helpMenu.Add("About").OnClick(func(ctx *application.Context) {
+ app.CurrentWindow().SetURL("/about.html")
+ })
+
+ // Create window with menu
+ app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
+ Title: "Window Menu Demo",
+ Width: 800,
+ Height: 600,
+ Windows: application.WindowsWindow{
+ Menu: menu,
+ },
+ KeyBindings: map[string]func(window *application.WebviewWindow){
+ "F1": func(window *application.WebviewWindow) {
+ window.ToggleMenuBar()
+ },
+ "F2": func(window *application.WebviewWindow) {
+ window.ShowMenuBar()
+ },
+ "F3": func(window *application.WebviewWindow) {
+ window.HideMenuBar()
+ },
+ },
+ })
+
+ err := app.Run()
+ if err != nil {
+ log.Fatal(err)
+ }
+}
diff --git a/v3/pkg/application/application.go b/v3/pkg/application/application.go
index edd504024..ec9e34b27 100644
--- a/v3/pkg/application/application.go
+++ b/v3/pkg/application/application.go
@@ -332,7 +332,7 @@ type App struct {
customEventProcessor *EventProcessor
Logger *slog.Logger
- contextMenus map[string]*Menu
+ contextMenus map[string]*ContextMenu
contextMenusLock sync.Mutex
assets *assetserver.AssetServer
@@ -436,7 +436,7 @@ func (a *App) init() {
a.applicationEventListeners = make(map[uint][]*EventListener)
a.windows = make(map[uint]Window)
a.systemTrays = make(map[uint]*SystemTray)
- a.contextMenus = make(map[string]*Menu)
+ a.contextMenus = make(map[string]*ContextMenu)
a.keyBindings = make(map[string]func(window *WebviewWindow))
a.Logger = a.options.Logger
a.pid = os.Getpid()
@@ -955,13 +955,19 @@ func (a *App) Show() {
}
}
-func (a *App) RegisterContextMenu(name string, menu *Menu) {
+func (a *App) registerContextMenu(menu *ContextMenu) {
a.contextMenusLock.Lock()
defer a.contextMenusLock.Unlock()
- a.contextMenus[name] = menu
+ a.contextMenus[menu.name] = menu
}
-func (a *App) getContextMenu(name string) (*Menu, bool) {
+func (a *App) unregisterContextMenu(name string) {
+ a.contextMenusLock.Lock()
+ defer a.contextMenusLock.Unlock()
+ delete(a.contextMenus, name)
+}
+
+func (a *App) getContextMenu(name string) (*ContextMenu, bool) {
a.contextMenusLock.Lock()
defer a.contextMenusLock.Unlock()
menu, ok := a.contextMenus[name]
diff --git a/v3/pkg/application/context.go b/v3/pkg/application/context.go
index 56b213350..16d6d7dbc 100644
--- a/v3/pkg/application/context.go
+++ b/v3/pkg/application/context.go
@@ -32,8 +32,17 @@ func (c *Context) IsChecked() bool {
}
return result.(bool)
}
-func (c *Context) ContextMenuData() any {
- return c.data[contextMenuData]
+func (c *Context) ContextMenuData() string {
+ result := c.data[contextMenuData]
+ if result == nil {
+ return ""
+ }
+ str, ok := result.(string)
+ if !ok {
+ return ""
+ }
+ return str
+}
}
func (c *Context) withClickedMenuItem(menuItem *MenuItem) *Context {
diff --git a/v3/pkg/application/menu.go b/v3/pkg/application/menu.go
index 948c95a9c..5a8673ea7 100644
--- a/v3/pkg/application/menu.go
+++ b/v3/pkg/application/menu.go
@@ -4,6 +4,29 @@ type menuImpl interface {
update()
}
+type ContextMenu struct {
+ *Menu
+ name string
+}
+
+func NewContextMenu(name string) *ContextMenu {
+ result := &ContextMenu{
+ Menu: NewMenu(),
+ name: name,
+ }
+ result.Update()
+ return result
+}
+
+func (m *ContextMenu) Update() {
+ m.Menu.Update()
+ globalApplication.registerContextMenu(m)
+}
+
+func (m *ContextMenu) Destroy() {
+ globalApplication.unregisterContextMenu(m.name)
+}
+
type Menu struct {
items []*MenuItem
label string
diff --git a/v3/pkg/application/messageprocessor_contextmenu.go b/v3/pkg/application/messageprocessor_contextmenu.go
index 0d7f2e894..84b0ea458 100644
--- a/v3/pkg/application/messageprocessor_contextmenu.go
+++ b/v3/pkg/application/messageprocessor_contextmenu.go
@@ -8,7 +8,7 @@ type ContextMenuData struct {
Id string `json:"id"`
X int `json:"x"`
Y int `json:"y"`
- Data any `json:"data"`
+ Data string `json:"data"`
}
func (d ContextMenuData) clone() *ContextMenuData {
diff --git a/v3/pkg/application/webview_window.go b/v3/pkg/application/webview_window.go
index 866b8fa70..7776f7280 100644
--- a/v3/pkg/application/webview_window.go
+++ b/v3/pkg/application/webview_window.go
@@ -104,6 +104,9 @@ type (
delete()
selectAll()
redo()
+ showMenuBar()
+ hideMenuBar()
+ toggleMenuBar()
}
)
@@ -138,9 +141,6 @@ type WebviewWindow struct {
eventHooks map[uint][]*WindowEventListener
eventHooksLock sync.RWMutex
- contextMenus map[string]*Menu
- contextMenusLock sync.RWMutex
-
// A map of listener cancellation functions
cancellersLock sync.RWMutex
cancellers []func()
@@ -247,7 +247,6 @@ func NewWindow(options WebviewWindowOptions) *WebviewWindow {
id: thisWindowID,
options: options,
eventListeners: make(map[uint][]*WindowEventListener),
- contextMenus: make(map[string]*Menu),
eventHooks: make(map[uint][]*WindowEventListener),
menuBindings: make(map[string]*MenuItem),
}
@@ -1176,35 +1175,21 @@ func (w *WebviewWindow) HandleDragAndDropMessage(filenames []string) {
}
func (w *WebviewWindow) OpenContextMenu(data *ContextMenuData) {
- menu, ok := w.contextMenus[data.Id]
+ // try application level context menu
+ menu, ok := globalApplication.getContextMenu(data.Id)
if !ok {
- // try application level context menu
- menu, ok = globalApplication.getContextMenu(data.Id)
- if !ok {
- w.Error("No context menu found for id: %s", data.Id)
- return
- }
+ w.Error("No context menu found for id: %s", data.Id)
+ return
}
menu.setContextData(data)
if w.impl == nil || w.isDestroyed() {
return
}
InvokeSync(func() {
- w.impl.openContextMenu(menu, data)
+ w.impl.openContextMenu(menu.Menu, data)
})
}
-// RegisterContextMenu registers a context menu and assigns it the given name.
-func (w *WebviewWindow) RegisterContextMenu(name string, menu *Menu) {
- if menu == nil {
- w.Error("RegisterContextMenu called with nil menu")
- return
- }
- w.contextMenusLock.Lock()
- defer w.contextMenusLock.Unlock()
- w.contextMenus[name] = menu
-}
-
// NativeWindowHandle returns the platform native window handle for the window.
func (w *WebviewWindow) NativeWindowHandle() (uintptr, error) {
if w.impl == nil || w.isDestroyed() {
@@ -1368,3 +1353,27 @@ func (w *WebviewWindow) redo() {
}
w.impl.redo()
}
+
+// ShowMenuBar shows the menu bar for the window.
+func (w *WebviewWindow) ShowMenuBar() {
+ if w.impl == nil || w.isDestroyed() {
+ return
+ }
+ InvokeSync(w.impl.showMenuBar)
+}
+
+// HideMenuBar hides the menu bar for the window.
+func (w *WebviewWindow) HideMenuBar() {
+ if w.impl == nil || w.isDestroyed() {
+ return
+ }
+ InvokeSync(w.impl.hideMenuBar)
+}
+
+// ToggleMenuBar toggles the menu bar for the window.
+func (w *WebviewWindow) ToggleMenuBar() {
+ if w.impl == nil || w.isDestroyed() {
+ return
+ }
+ InvokeSync(w.impl.toggleMenuBar)
+}
diff --git a/v3/pkg/application/webview_window_darwin.go b/v3/pkg/application/webview_window_darwin.go
index 1c855f749..bd787007a 100644
--- a/v3/pkg/application/webview_window_darwin.go
+++ b/v3/pkg/application/webview_window_darwin.go
@@ -1438,3 +1438,7 @@ func (w *macosWebviewWindow) delete() {
func (w *macosWebviewWindow) redo() {
}
+
+func (w *macosWebviewWindow) showMenuBar() {}
+func (w *macosWebviewWindow) hideMenuBar() {}
+func (w *macosWebviewWindow) toggleMenuBar() {}
diff --git a/v3/pkg/application/webview_window_linux.go b/v3/pkg/application/webview_window_linux.go
index b83d2e81b..f5c604a05 100644
--- a/v3/pkg/application/webview_window_linux.go
+++ b/v3/pkg/application/webview_window_linux.go
@@ -422,3 +422,7 @@ func (w *linuxWebviewWindow) isIgnoreMouseEvents() bool {
func (w *linuxWebviewWindow) setIgnoreMouseEvents(ignore bool) {
w.ignoreMouse(w.ignoreMouseEvents)
}
+
+func (w *linuxWebviewWindow) showMenuBar() {}
+func (w *linuxWebviewWindow) hideMenuBar() {}
+func (w *linuxWebviewWindow) toggleMenuBar() {}
diff --git a/v3/pkg/application/webview_window_windows.go b/v3/pkg/application/webview_window_windows.go
index e724dabe6..ef2243f16 100644
--- a/v3/pkg/application/webview_window_windows.go
+++ b/v3/pkg/application/webview_window_windows.go
@@ -1169,9 +1169,37 @@ func (w *windowsWebviewWindow) WndProc(msg uint32, wparam, lparam uintptr) uintp
w.parent.emit(events.Windows.WindowBackgroundErase)
return 1 // Let WebView2 handle background erasing
// Check for keypress
- case w32.WM_KEYDOWN:
- w.processKeyBinding(uint(wparam))
+ case w32.WM_SYSCOMMAND:
+ switch wparam {
+ case w32.SC_KEYMENU:
+ if lparam == 0 {
+ // F10 or plain Alt key
+ if w.processKeyBinding(w32.VK_F10) {
+ return 0
+ }
+ } else {
+ // Alt + key combination
+ // The character code is in the low word of lparam
+ char := byte(lparam & 0xFF)
+ // Convert ASCII to virtual key code if needed
+ vkey := w32.VkKeyScan(uint16(char))
+ if w.processKeyBinding(uint(vkey)) {
+ return 0
+ }
+ }
+ }
+ case w32.WM_SYSKEYDOWN:
+ globalApplication.info("w32.WM_SYSKEYDOWN: %v", uint(wparam))
w.parent.emit(events.Windows.WindowKeyDown)
+ if w.processKeyBinding(uint(wparam)) {
+ return 0
+ }
+ case w32.WM_SYSKEYUP:
+ w.parent.emit(events.Windows.WindowKeyUp)
+ case w32.WM_KEYDOWN:
+ globalApplication.info("w32.WM_KEYDOWN: %v", uint(wparam))
+ w.parent.emit(events.Windows.WindowKeyDown)
+ w.processKeyBinding(uint(wparam))
case w32.WM_KEYUP:
w.parent.emit(events.Windows.WindowKeyUp)
case w32.WM_SIZE:
@@ -1917,6 +1945,43 @@ func (w *windowsWebviewWindow) setMinimiseButtonEnabled(enabled bool) {
w.setStyle(enabled, w32.WS_MINIMIZEBOX)
}
+func (w *windowsWebviewWindow) toggleMenuBar() {
+ if w.menu != nil {
+ if w32.GetMenu(w.hwnd) == 0 {
+ w32.SetMenu(w.hwnd, w.menu.menu)
+ } else {
+ w32.SetMenu(w.hwnd, 0)
+ }
+
+ // Get the bounds of the client area
+ //bounds := w32.GetClientRect(w.hwnd)
+
+ // Resize the webview
+ w.chromium.Resize()
+
+ // Update size of webview
+ w.update()
+ // Restore focus to the webview after toggling menu
+ w.focus()
+ }
+}
+
+func (w *windowsWebviewWindow) enableRedraw() {
+ w32.SendMessage(w.hwnd, w32.WM_SETREDRAW, 1, 0)
+ w32.RedrawWindow(w.hwnd, nil, 0, w32.RDW_ERASE|w32.RDW_FRAME|w32.RDW_INVALIDATE|w32.RDW_ALLCHILDREN)
+}
+
+func (w *windowsWebviewWindow) disableRedraw() {
+ w32.SendMessage(w.hwnd, w32.WM_SETREDRAW, 0, 0)
+}
+
+func (w *windowsWebviewWindow) disableRedrawWithCallback(callback func()) {
+ w.disableRedraw()
+ callback()
+ w.enableRedraw()
+
+}
+
func NewIconFromResource(instance w32.HINSTANCE, resId uint16) (w32.HICON, error) {
var err error
var result w32.HICON
@@ -1986,3 +2051,15 @@ func (w *windowsWebviewWindow) setPadding(padding edge.Rect) {
}
w.chromium.SetPadding(padding)
}
+
+func (w *windowsWebviewWindow) showMenuBar() {
+ if w.menu != nil {
+ w32.SetMenu(w.hwnd, w.menu.menu)
+ }
+}
+
+func (w *windowsWebviewWindow) hideMenuBar() {
+ if w.menu != nil {
+ w32.SetMenu(w.hwnd, 0)
+ }
+}
diff --git a/v3/pkg/application/window.go b/v3/pkg/application/window.go
index 5669e54d5..3f3dea64a 100644
--- a/v3/pkg/application/window.go
+++ b/v3/pkg/application/window.go
@@ -32,6 +32,7 @@ type Window interface {
HandleWindowEvent(id uint)
Height() int
Hide() Window
+ HideMenuBar()
ID() uint
Info(message string, args ...any)
IsFocused() bool
@@ -46,7 +47,6 @@ type Window interface {
OnWindowEvent(eventType events.WindowEventType, callback func(event *WindowEvent)) func()
OpenContextMenu(data *ContextMenuData)
Position() (int, int)
- RegisterContextMenu(name string, menu *Menu)
RelativePosition() (int, int)
Reload()
Resizable() bool
@@ -70,10 +70,12 @@ type Window interface {
SetURL(s string) Window
SetZoom(magnification float64) Window
Show() Window
+ ShowMenuBar()
Size() (width int, height int)
OpenDevTools()
ToggleFullscreen()
ToggleMaximise()
+ ToggleMenuBar()
UnFullscreen()
UnMaximise()
UnMinimise()
diff --git a/v3/pkg/w32/constants.go b/v3/pkg/w32/constants.go
index a76848517..234933ba6 100644
--- a/v3/pkg/w32/constants.go
+++ b/v3/pkg/w32/constants.go
@@ -608,6 +608,36 @@ const (
WM_DPICHANGED = 0x02E0
)
+const (
+ SC_SIZE = 0xF000 // Resize the window
+ SC_MOVE = 0xF010 // Move the window
+ SC_MINIMIZE = 0xF020 // Minimize the window
+ SC_MAXIMIZE = 0xF030 // Maximize the window
+ SC_NEXTWINDOW = 0xF040 // Move to next window
+ SC_PREVWINDOW = 0xF050 // Move to previous window
+ SC_CLOSE = 0xF060 // Close the window
+ SC_VSCROLL = 0xF070 // Vertical scroll
+ SC_HSCROLL = 0xF080 // Horizontal scroll
+ SC_MOUSEMENU = 0xF090 // Mouse menu
+ SC_KEYMENU = 0xF100 // Key menu (triggered by Alt or F10)
+ SC_ARRANGE = 0xF110 // Arrange windows
+ SC_RESTORE = 0xF120 // Restore window from minimized/maximized
+ SC_TASKLIST = 0xF130 // Task list
+ SC_SCREENSAVE = 0xF140 // Screen saver
+ SC_HOTKEY = 0xF150 // Hotkey
+ SC_DEFAULT = 0xF160 // Default command
+ SC_MONITORPOWER = 0xF170 // Monitor power
+ SC_CONTEXTHELP = 0xF180 // Context help
+ SC_SEPARATOR = 0xF00F // Separator
+)
+
+const (
+ // Remove the Close option from the window menu
+ SC_MASK_CLOSE = ^uint16(SC_CLOSE)
+ // Mask for extracting the system command
+ SC_MASK_CMD = 0xFFF0
+)
+
// WM_ACTIVATE
const (
WA_INACTIVE = 0
diff --git a/v3/pkg/w32/window.go b/v3/pkg/w32/window.go
index 542971db7..3b4cefa79 100644
--- a/v3/pkg/w32/window.go
+++ b/v3/pkg/w32/window.go
@@ -12,23 +12,24 @@ import (
"unsafe"
)
-const (
- SC_CLOSE = 0xF060
- SC_MOVE = 0xF010
- SC_MAXIMIZE = 0xF030
- SC_MINIMIZE = 0xF020
- SC_SIZE = 0xF000
- SC_RESTORE = 0xF120
-)
-
var (
user32 = syscall.NewLazyDLL("user32.dll")
getSystemMenu = user32.NewProc("GetSystemMenu")
+ getMenuProc = user32.NewProc("GetMenu")
enableMenuItem = user32.NewProc("EnableMenuItem")
findWindow = user32.NewProc("FindWindowW")
sendMessage = user32.NewProc("SendMessageW")
+ vkKeyScan = user32.NewProc("VkKeyScanW") // Use W version for Unicode
)
+func VkKeyScan(ch uint16) uint16 {
+ ret, _, _ := syscall.SyscallN(
+ vkKeyScan.Addr(),
+ uintptr(ch),
+ )
+ return uint16(ret)
+}
+
const (
WMCOPYDATA_SINGLE_INSTANCE_DATA = 1542
)
@@ -345,3 +346,9 @@ func SendMessageToWindow(hwnd HWND, msg string) {
uintptr(unsafe.Pointer(&cds)),
)
}
+
+// GetMenu retrieves a handle to the menu assigned to the specified window
+func GetMenu(hwnd HWND) HMENU {
+ ret, _, _ := getMenuProc.Call(hwnd)
+ return ret
+}