# Conflicts:

#	docs/src/content/docs/changelog.mdx
#	v3/pkg/application/menuitem.go
This commit is contained in:
Lea Anthony 2025-01-17 21:19:23 +11:00
commit be8159d410
No known key found for this signature in database
GPG key ID: 33DAF7BB90A58405
19 changed files with 889 additions and 56 deletions

View file

@ -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

View file

@ -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
<!-- Always show default context menu -->
<div style="--default-contextmenu: show">
<input type="text" placeholder="Right-click for text operations"/>
<textarea>Standard text operations available here</textarea>
</div>
<!-- Hide default context menu -->
<div style="--default-contextmenu: hide">
<div class="custom-component">Custom context menu only</div>
</div>
<!-- Smart context menu behaviour (default) -->
<div style="--default-contextmenu: auto">
<!-- Shows default menu when text is selected or in input fields -->
<p>Select this text to see the default menu</p>
<input type="text" placeholder="Default menu for input operations"/>
</div>
```
#### 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
<!-- Parent sets hide -->
<div style="--default-contextmenu: hide">
<!-- This inherits hide -->
<p>No context menu here</p>
<!-- This overrides to show -->
<div style="--default-contextmenu: show">
<p>Context menu shown here</p>
<!-- This inherits show -->
<span>Also has context menu</span>
<!-- This resets to automatic behaviour -->
<div style="--default-contextmenu: auto">
<p>Shows menu only when text is selected</p>
</div>
</div>
</div>
```
### 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
<!-- Frontend: Image gallery implementation -->
<div class="gallery">
<!-- Each image container with context menu -->
<div class="image-container"
style="--custom-contextmenu: imageMenu; --custom-contextmenu-data: img_123">
<img src="/images/img_123.jpg" alt="Gallery Image"/>
<span class="caption">Nature Photo</span>
</div>
<div class="image-container"
style="--custom-contextmenu: imageMenu; --custom-contextmenu-data: img_124">
<img src="/images/img_124.jpg" alt="Gallery Image"/>
<span class="caption">City Photo</span>
</div>
</div>
```
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

View file

@ -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 {

View file

@ -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.

View file

@ -0,0 +1,14 @@
<html>
<head>
<title>Window Menu Demo</title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<div class="container">
<h1>About Window Menu Demo</h1>
<p>Press F1 to toggle menu bar visibility</p>
<p>Press F2 to show menu bar</p>
<p>Press F3 to hide menu bar</p>
</div>
</body>
</html>

View file

@ -0,0 +1,48 @@
<html>
<head>
<title>Window Menu Demo</title>
<style>
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;
}
</style>
</head>
<body>
<div class="container">
<h1>Window Menu Demo</h1>
<p>This example demonstrates the menu bar visibility toggle feature.</p>
<p>Press <span class="key">F1</span> to toggle the menu bar.</p>
<p>Press <span class="key">F2</span> to show the menu bar.</p>
<p>Press <span class="key">F3</span> to hide the menu bar.</p>
<p>The menu includes:</p>
<ul>
<li>File menu with Exit option</li>
<li>MenuBar menu with Hide options</li>
<li>Help menu with About</li>
</ul>
</div>
</body>
</html>

View file

@ -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;
}

View file

@ -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)
}
}

View file

@ -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]

View file

@ -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 {

View file

@ -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

View file

@ -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 {

View file

@ -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)
}

View file

@ -1438,3 +1438,7 @@ func (w *macosWebviewWindow) delete() {
func (w *macosWebviewWindow) redo() {
}
func (w *macosWebviewWindow) showMenuBar() {}
func (w *macosWebviewWindow) hideMenuBar() {}
func (w *macosWebviewWindow) toggleMenuBar() {}

View file

@ -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() {}

View file

@ -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)
}
}

View file

@ -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()

View file

@ -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

View file

@ -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
}