728 lines
17 KiB
Text
728 lines
17 KiB
Text
---
|
|
title: Context Menus
|
|
description: Create right-click context menus for your application
|
|
sidebar:
|
|
order: 2
|
|
---
|
|
|
|
import { Card, CardGrid, Tabs, TabItem } from "@astrojs/starlight/components";
|
|
|
|
## The Problem
|
|
|
|
Users expect right-click menus with context-specific actions. Different elements need different menus:
|
|
- **Text**: Cut, Copy, Paste
|
|
- **Images**: Save, Copy, Open
|
|
- **Custom elements**: Application-specific actions
|
|
|
|
Building context menus manually means handling mouse events, positioning, and platform differences.
|
|
|
|
## The Wails Solution
|
|
|
|
Wails provides **declarative context menus** using CSS properties. Associate menus with HTML elements, pass data, and handle clicks—all with native platform behaviour.
|
|
|
|
## Quick Start
|
|
|
|
**Go code:**
|
|
|
|
```go
|
|
// Create context menu
|
|
contextMenu := app.NewContextMenu()
|
|
contextMenu.Add("Cut").OnClick(handleCut)
|
|
contextMenu.Add("Copy").OnClick(handleCopy)
|
|
contextMenu.Add("Paste").OnClick(handlePaste)
|
|
|
|
// Register with ID
|
|
app.RegisterContextMenu("editor-menu", contextMenu)
|
|
```
|
|
|
|
**HTML:**
|
|
|
|
```html
|
|
<textarea style="--custom-contextmenu: editor-menu">
|
|
Right-click me!
|
|
</textarea>
|
|
```
|
|
|
|
**That's it!** Right-clicking the textarea shows your custom menu.
|
|
|
|
## Creating Context Menus
|
|
|
|
### Basic Context Menu
|
|
|
|
```go
|
|
// Create menu
|
|
contextMenu := app.NewContextMenu()
|
|
|
|
// Add items
|
|
contextMenu.Add("Cut").SetAccelerator("CmdOrCtrl+X").OnClick(func(ctx *application.Context) {
|
|
// Handle cut
|
|
})
|
|
|
|
contextMenu.Add("Copy").SetAccelerator("CmdOrCtrl+C").OnClick(func(ctx *application.Context) {
|
|
// Handle copy
|
|
})
|
|
|
|
contextMenu.Add("Paste").SetAccelerator("CmdOrCtrl+V").OnClick(func(ctx *application.Context) {
|
|
// Handle paste
|
|
})
|
|
|
|
// Register with unique ID
|
|
app.RegisterContextMenu("text-menu", contextMenu)
|
|
```
|
|
|
|
**Menu ID:** Must be unique. Used to associate menu with HTML elements.
|
|
|
|
### With Submenus
|
|
|
|
```go
|
|
contextMenu := app.NewContextMenu()
|
|
|
|
// Add regular items
|
|
contextMenu.Add("Open").OnClick(handleOpen)
|
|
contextMenu.Add("Delete").OnClick(handleDelete)
|
|
|
|
contextMenu.AddSeparator()
|
|
|
|
// Add submenu
|
|
exportMenu := contextMenu.AddSubmenu("Export As")
|
|
exportMenu.Add("PNG").OnClick(exportPNG)
|
|
exportMenu.Add("JPEG").OnClick(exportJPEG)
|
|
exportMenu.Add("SVG").OnClick(exportSVG)
|
|
|
|
app.RegisterContextMenu("image-menu", contextMenu)
|
|
```
|
|
|
|
### With Checkboxes and Radio Groups
|
|
|
|
```go
|
|
contextMenu := app.NewContextMenu()
|
|
|
|
// Checkbox
|
|
contextMenu.AddCheckbox("Show Grid", true).OnClick(func(ctx *application.Context) {
|
|
showGrid := ctx.ClickedMenuItem().Checked()
|
|
// Toggle grid
|
|
})
|
|
|
|
contextMenu.AddSeparator()
|
|
|
|
// Radio group
|
|
contextMenu.AddRadio("Small", false).OnClick(handleSize)
|
|
contextMenu.AddRadio("Medium", true).OnClick(handleSize)
|
|
contextMenu.AddRadio("Large", false).OnClick(handleSize)
|
|
|
|
app.RegisterContextMenu("view-menu", contextMenu)
|
|
```
|
|
|
|
**For all menu item types**, see [Menu Reference](/features/menus/reference).
|
|
|
|
## Associating with HTML Elements
|
|
|
|
Use CSS custom properties to attach context menus:
|
|
|
|
### Basic Association
|
|
|
|
```html
|
|
<div style="--custom-contextmenu: menu-id">
|
|
Right-click me!
|
|
</div>
|
|
```
|
|
|
|
**CSS property:** `--custom-contextmenu: <menu-id>`
|
|
|
|
### With Context Data
|
|
|
|
Pass data from HTML to Go:
|
|
|
|
```html
|
|
<div style="--custom-contextmenu: file-menu; --custom-contextmenu-data: file-123">
|
|
Right-click this file
|
|
</div>
|
|
```
|
|
|
|
**Go handler:**
|
|
|
|
```go
|
|
contextMenu := app.NewContextMenu()
|
|
contextMenu.Add("Open").OnClick(func(ctx *application.Context) {
|
|
fileID := ctx.ContextMenuData() // "file-123"
|
|
openFile(fileID)
|
|
})
|
|
|
|
app.RegisterContextMenu("file-menu", contextMenu)
|
|
```
|
|
|
|
**CSS properties:**
|
|
- `--custom-contextmenu: <menu-id>` - Which menu to show
|
|
- `--custom-contextmenu-data: <data>` - Data to pass to handlers
|
|
|
|
### Dynamic Data
|
|
|
|
Generate data dynamically in JavaScript:
|
|
|
|
```html
|
|
<div id="file-item" style="--custom-contextmenu: file-menu">
|
|
File.txt
|
|
</div>
|
|
|
|
<script>
|
|
// Set data dynamically
|
|
const fileItem = document.getElementById('file-item')
|
|
fileItem.style.setProperty('--custom-contextmenu-data', 'file-' + fileId)
|
|
</script>
|
|
```
|
|
|
|
### Multiple Elements, Same Menu
|
|
|
|
```html
|
|
<div class="file-item" style="--custom-contextmenu: file-menu; --custom-contextmenu-data: file-1">
|
|
Document.pdf
|
|
</div>
|
|
|
|
<div class="file-item" style="--custom-contextmenu: file-menu; --custom-contextmenu-data: file-2">
|
|
Image.png
|
|
</div>
|
|
|
|
<div class="file-item" style="--custom-contextmenu: file-menu; --custom-contextmenu-data: file-3">
|
|
Video.mp4
|
|
</div>
|
|
```
|
|
|
|
**One menu, different data for each element.**
|
|
|
|
## Context Data
|
|
|
|
### Accessing Context Data
|
|
|
|
```go
|
|
contextMenu.Add("Process").OnClick(func(ctx *application.Context) {
|
|
data := ctx.ContextMenuData() // Get data from HTML
|
|
|
|
// Use the data
|
|
processItem(data)
|
|
})
|
|
```
|
|
|
|
**Data type:** Always `string`. Parse as needed.
|
|
|
|
### Passing Complex Data
|
|
|
|
Use JSON for complex data:
|
|
|
|
```html
|
|
<div style="--custom-contextmenu: item-menu; --custom-contextmenu-data: {"id":123,"type":"image"}">
|
|
Image.png
|
|
</div>
|
|
```
|
|
|
|
**Go handler:**
|
|
|
|
```go
|
|
import "encoding/json"
|
|
|
|
type ItemData struct {
|
|
ID int `json:"id"`
|
|
Type string `json:"type"`
|
|
}
|
|
|
|
contextMenu.Add("Process").OnClick(func(ctx *application.Context) {
|
|
dataStr := ctx.ContextMenuData()
|
|
|
|
var data ItemData
|
|
if err := json.Unmarshal([]byte(dataStr), &data); err != nil {
|
|
log.Printf("Invalid data: %v", err)
|
|
return
|
|
}
|
|
|
|
processItem(data.ID, data.Type)
|
|
})
|
|
```
|
|
|
|
:::caution[Security]
|
|
**Always validate context data** from the frontend. Users can manipulate CSS properties, so treat data as untrusted input.
|
|
:::
|
|
|
|
### Validation Example
|
|
|
|
```go
|
|
contextMenu.Add("Delete").OnClick(func(ctx *application.Context) {
|
|
fileID := ctx.ContextMenuData()
|
|
|
|
// Validate
|
|
if !isValidFileID(fileID) {
|
|
log.Printf("Invalid file ID: %s", fileID)
|
|
return
|
|
}
|
|
|
|
// Check permissions
|
|
if !canDeleteFile(fileID) {
|
|
showError("Permission denied")
|
|
return
|
|
}
|
|
|
|
// Safe to proceed
|
|
deleteFile(fileID)
|
|
})
|
|
```
|
|
|
|
## Default Context Menu
|
|
|
|
The WebView provides a built-in context menu for standard operations (copy, paste, inspect). Control it with `--default-contextmenu`:
|
|
|
|
### Hide Default Menu
|
|
|
|
```html
|
|
<div style="--default-contextmenu: hide">
|
|
No default menu here
|
|
</div>
|
|
```
|
|
|
|
**Use case:** Custom UI elements where default menu doesn't make sense.
|
|
|
|
### Show Default Menu
|
|
|
|
```html
|
|
<div style="--default-contextmenu: show">
|
|
Default menu always shown
|
|
</div>
|
|
```
|
|
|
|
**Use case:** Text areas, input fields, editable content.
|
|
|
|
### Auto (Smart) Mode
|
|
|
|
```html
|
|
<div style="--default-contextmenu: auto">
|
|
Smart context menu
|
|
</div>
|
|
```
|
|
|
|
**Default behaviour.** Shows default menu when:
|
|
- Text is selected
|
|
- In text input fields
|
|
- In editable content (`contenteditable`)
|
|
|
|
Hides default menu otherwise.
|
|
|
|
### Combining Custom and Default
|
|
|
|
```html
|
|
<!-- Custom menu + default menu -->
|
|
<textarea style="--custom-contextmenu: editor-menu; --default-contextmenu: show">
|
|
Both menus available
|
|
</textarea>
|
|
```
|
|
|
|
**Behaviour:**
|
|
1. Custom menu shows first
|
|
2. If custom menu is empty or not found, default menu shows
|
|
3. Both can coexist (platform-dependent)
|
|
|
|
## Dynamic Context Menus
|
|
|
|
Update menus based on application state:
|
|
|
|
### Enable/Disable Items
|
|
|
|
```go
|
|
var cutMenuItem *application.MenuItem
|
|
var copyMenuItem *application.MenuItem
|
|
|
|
func createContextMenu() {
|
|
contextMenu := app.NewContextMenu()
|
|
|
|
cutMenuItem = contextMenu.Add("Cut")
|
|
cutMenuItem.SetEnabled(false) // Initially disabled
|
|
cutMenuItem.OnClick(handleCut)
|
|
|
|
copyMenuItem = contextMenu.Add("Copy")
|
|
copyMenuItem.SetEnabled(false)
|
|
copyMenuItem.OnClick(handleCopy)
|
|
|
|
app.RegisterContextMenu("editor-menu", contextMenu)
|
|
}
|
|
|
|
func onSelectionChanged(hasSelection bool) {
|
|
cutMenuItem.SetEnabled(hasSelection)
|
|
copyMenuItem.SetEnabled(hasSelection)
|
|
contextMenu.Update() // Important!
|
|
}
|
|
```
|
|
|
|
:::caution[Always Call Update()]
|
|
After changing menu state, **call `contextMenu.Update()`**. This is critical on Windows.
|
|
|
|
See [Menu Reference](/features/menus/reference#enabled-state) for details.
|
|
:::
|
|
|
|
### Change Labels
|
|
|
|
```go
|
|
playMenuItem := contextMenu.Add("Play")
|
|
|
|
playMenuItem.OnClick(func(ctx *application.Context) {
|
|
if isPlaying {
|
|
playMenuItem.SetLabel("Pause")
|
|
} else {
|
|
playMenuItem.SetLabel("Play")
|
|
}
|
|
contextMenu.Update()
|
|
})
|
|
```
|
|
|
|
### Rebuild Menus
|
|
|
|
For major changes, rebuild the entire menu:
|
|
|
|
```go
|
|
func rebuildContextMenu(fileType string) {
|
|
contextMenu := app.NewContextMenu()
|
|
|
|
// Common items
|
|
contextMenu.Add("Open").OnClick(handleOpen)
|
|
contextMenu.Add("Delete").OnClick(handleDelete)
|
|
|
|
contextMenu.AddSeparator()
|
|
|
|
// Type-specific items
|
|
switch fileType {
|
|
case "image":
|
|
contextMenu.Add("Edit Image").OnClick(editImage)
|
|
contextMenu.Add("Set as Wallpaper").OnClick(setWallpaper)
|
|
case "video":
|
|
contextMenu.Add("Play").OnClick(playVideo)
|
|
contextMenu.Add("Extract Audio").OnClick(extractAudio)
|
|
case "document":
|
|
contextMenu.Add("Print").OnClick(printDocument)
|
|
contextMenu.Add("Export PDF").OnClick(exportPDF)
|
|
}
|
|
|
|
app.RegisterContextMenu("file-menu", contextMenu)
|
|
}
|
|
```
|
|
|
|
## Platform Behaviour
|
|
|
|
Context menus are **platform-native**:
|
|
|
|
<Tabs syncKey="platform">
|
|
<TabItem label="macOS" icon="apple">
|
|
**Native macOS context menus:**
|
|
- System animations and transitions
|
|
- Right-click = Control+Click (automatic)
|
|
- Adapts to system appearance (light/dark)
|
|
- Standard text operations in default menu
|
|
- Native scrolling for long menus
|
|
|
|
**macOS conventions:**
|
|
- Use sentence case for menu items
|
|
- Use ellipsis (...) for items that open dialogs
|
|
- Common shortcuts: ⌘C (Copy), ⌘V (Paste)
|
|
</TabItem>
|
|
|
|
<TabItem label="Windows" icon="seti:windows">
|
|
**Native Windows context menus:**
|
|
- Windows native style
|
|
- Follows Windows theme
|
|
- Standard Windows operations in default menu
|
|
- Touch and pen input support
|
|
|
|
**Windows conventions:**
|
|
- Use title case for menu items
|
|
- Use ellipsis (...) for items that open dialogs
|
|
- Common shortcuts: Ctrl+C (Copy), Ctrl+V (Paste)
|
|
</TabItem>
|
|
|
|
<TabItem label="Linux" icon="linux">
|
|
**Desktop environment integration:**
|
|
- Adapts to desktop theme (GTK, Qt, etc.)
|
|
- Right-click behaviour follows system settings
|
|
- Default menu content varies by environment
|
|
- Positioning follows DE conventions
|
|
|
|
**Linux considerations:**
|
|
- Test on target desktop environments
|
|
- GTK and Qt have different behaviours
|
|
- Some DEs customise context menus
|
|
</TabItem>
|
|
</Tabs>
|
|
|
|
## Complete Example
|
|
|
|
**Go code:**
|
|
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"log"
|
|
"github.com/wailsapp/wails/v3/pkg/application"
|
|
)
|
|
|
|
type FileData struct {
|
|
ID string `json:"id"`
|
|
Type string `json:"type"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
func main() {
|
|
app := application.New(application.Options{
|
|
Name: "Context Menu Demo",
|
|
})
|
|
|
|
// Create file context menu
|
|
fileMenu := createFileMenu(app)
|
|
app.RegisterContextMenu("file-menu", fileMenu)
|
|
|
|
// Create image context menu
|
|
imageMenu := createImageMenu(app)
|
|
app.RegisterContextMenu("image-menu", imageMenu)
|
|
|
|
// Create text context menu
|
|
textMenu := createTextMenu(app)
|
|
app.RegisterContextMenu("text-menu", textMenu)
|
|
|
|
app.Window.New()
|
|
app.Run()
|
|
}
|
|
|
|
func createFileMenu(app *application.Application) *application.ContextMenu {
|
|
menu := app.NewContextMenu()
|
|
|
|
menu.Add("Open").OnClick(func(ctx *application.Context) {
|
|
data := parseFileData(ctx.ContextMenuData())
|
|
openFile(data.ID)
|
|
})
|
|
|
|
menu.Add("Rename").OnClick(func(ctx *application.Context) {
|
|
data := parseFileData(ctx.ContextMenuData())
|
|
renameFile(data.ID)
|
|
})
|
|
|
|
menu.AddSeparator()
|
|
|
|
menu.Add("Delete").OnClick(func(ctx *application.Context) {
|
|
data := parseFileData(ctx.ContextMenuData())
|
|
deleteFile(data.ID)
|
|
})
|
|
|
|
return menu
|
|
}
|
|
|
|
func createImageMenu(app *application.Application) *application.ContextMenu {
|
|
menu := app.NewContextMenu()
|
|
|
|
menu.Add("View").OnClick(func(ctx *application.Context) {
|
|
data := parseFileData(ctx.ContextMenuData())
|
|
viewImage(data.ID)
|
|
})
|
|
|
|
menu.Add("Edit").OnClick(func(ctx *application.Context) {
|
|
data := parseFileData(ctx.ContextMenuData())
|
|
editImage(data.ID)
|
|
})
|
|
|
|
menu.AddSeparator()
|
|
|
|
exportMenu := menu.AddSubmenu("Export As")
|
|
exportMenu.Add("PNG").OnClick(exportPNG)
|
|
exportMenu.Add("JPEG").OnClick(exportJPEG)
|
|
exportMenu.Add("WebP").OnClick(exportWebP)
|
|
|
|
return menu
|
|
}
|
|
|
|
func createTextMenu(app *application.Application) *application.ContextMenu {
|
|
menu := app.NewContextMenu()
|
|
|
|
menu.Add("Cut").SetAccelerator("CmdOrCtrl+X").OnClick(handleCut)
|
|
menu.Add("Copy").SetAccelerator("CmdOrCtrl+C").OnClick(handleCopy)
|
|
menu.Add("Paste").SetAccelerator("CmdOrCtrl+V").OnClick(handlePaste)
|
|
|
|
return menu
|
|
}
|
|
|
|
func parseFileData(dataStr string) FileData {
|
|
var data FileData
|
|
if err := json.Unmarshal([]byte(dataStr), &data); err != nil {
|
|
log.Printf("Invalid file data: %v", err)
|
|
}
|
|
return data
|
|
}
|
|
|
|
// Handler implementations...
|
|
func openFile(id string) { /* ... */ }
|
|
func renameFile(id string) { /* ... */ }
|
|
func deleteFile(id string) { /* ... */ }
|
|
func viewImage(id string) { /* ... */ }
|
|
func editImage(id string) { /* ... */ }
|
|
func exportPNG(ctx *application.Context) { /* ... */ }
|
|
func exportJPEG(ctx *application.Context) { /* ... */ }
|
|
func exportWebP(ctx *application.Context) { /* ... */ }
|
|
func handleCut(ctx *application.Context) { /* ... */ }
|
|
func handleCopy(ctx *application.Context) { /* ... */ }
|
|
func handlePaste(ctx *application.Context) { /* ... */ }
|
|
```
|
|
|
|
**HTML:**
|
|
|
|
```html
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<style>
|
|
.file-item {
|
|
padding: 10px;
|
|
margin: 5px;
|
|
border: 1px solid #ccc;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.file-item:hover {
|
|
background: #f0f0f0;
|
|
}
|
|
|
|
textarea {
|
|
width: 100%;
|
|
height: 200px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h2>Files</h2>
|
|
|
|
<!-- Regular file -->
|
|
<div class="file-item"
|
|
style="--custom-contextmenu: file-menu;
|
|
--custom-contextmenu-data: {"id":"file-1","type":"document","name":"Report.pdf"}">
|
|
📄 Report.pdf
|
|
</div>
|
|
|
|
<!-- Image file -->
|
|
<div class="file-item"
|
|
style="--custom-contextmenu: image-menu;
|
|
--custom-contextmenu-data: {"id":"file-2","type":"image","name":"Photo.jpg"}">
|
|
🖼️ Photo.jpg
|
|
</div>
|
|
|
|
<h2>Text Editor</h2>
|
|
|
|
<!-- Text area with custom menu + default menu -->
|
|
<textarea
|
|
style="--custom-contextmenu: text-menu; --default-contextmenu: show"
|
|
placeholder="Type here, then right-click...">
|
|
</textarea>
|
|
|
|
<h2>No Context Menu</h2>
|
|
|
|
<!-- Disable default menu -->
|
|
<div style="--default-contextmenu: hide; padding: 20px; border: 1px solid #ccc;">
|
|
Right-click here - no menu appears
|
|
</div>
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
### ✅ Do
|
|
|
|
- **Keep menus focused** - Only relevant actions for the element
|
|
- **Validate context data** - Treat as untrusted input
|
|
- **Use clear labels** - "Delete File" not "Delete"
|
|
- **Call menu.Update()** - After changing menu state
|
|
- **Test on all platforms** - Behaviour varies
|
|
- **Provide keyboard shortcuts** - For common actions
|
|
- **Group related items** - Use separators
|
|
|
|
### ❌ Don't
|
|
|
|
- **Don't trust context data** - Always validate
|
|
- **Don't make menus too long** - 7-10 items maximum
|
|
- **Don't forget menu.Update()** - Menus won't work properly
|
|
- **Don't nest too deeply** - 2 levels maximum
|
|
- **Don't use jargon** - Keep labels user-friendly
|
|
- **Don't block handlers** - Keep them fast
|
|
|
|
## Troubleshooting
|
|
|
|
### Context Menu Not Appearing
|
|
|
|
**Possible causes:**
|
|
1. Menu ID mismatch
|
|
2. CSS property typo
|
|
3. Runtime not initialised
|
|
|
|
**Solution:**
|
|
|
|
```go
|
|
// Check menu is registered
|
|
app.RegisterContextMenu("my-menu", contextMenu)
|
|
```
|
|
|
|
```html
|
|
<!-- Check ID matches -->
|
|
<div style="--custom-contextmenu: my-menu">
|
|
```
|
|
|
|
### Context Data Not Received
|
|
|
|
**Possible causes:**
|
|
1. CSS property not set
|
|
2. Data contains special characters
|
|
|
|
**Solution:**
|
|
|
|
```html
|
|
<!-- Escape quotes in JSON -->
|
|
<div style="--custom-contextmenu-data: {"id":123}">
|
|
```
|
|
|
|
Or use JavaScript:
|
|
|
|
```javascript
|
|
element.style.setProperty('--custom-contextmenu-data', JSON.stringify(data))
|
|
```
|
|
|
|
### Menu Items Not Responding
|
|
|
|
**Cause:** Forgot to call `menu.Update()` after enabling
|
|
|
|
**Solution:**
|
|
|
|
```go
|
|
menuItem.SetEnabled(true)
|
|
contextMenu.Update() // Add this!
|
|
```
|
|
|
|
## Next Steps
|
|
|
|
<CardGrid>
|
|
<Card title="Menu Reference" icon="document">
|
|
Complete reference for menu item types and properties.
|
|
|
|
[Learn More →](/features/menus/reference)
|
|
</Card>
|
|
|
|
<Card title="Application Menus" icon="list-format">
|
|
Create application menu bars.
|
|
|
|
[Learn More →](/features/menus/application)
|
|
</Card>
|
|
|
|
<Card title="System Tray Menus" icon="star">
|
|
Add system tray/menu bar integration.
|
|
|
|
[Learn More →](/features/menus/systray)
|
|
</Card>
|
|
|
|
<Card title="Menu Patterns" icon="open-book">
|
|
Common menu patterns and best practices.
|
|
|
|
[Learn More →](/guides/patterns/menus)
|
|
</Card>
|
|
</CardGrid>
|
|
|
|
---
|
|
|
|
**Questions?** Ask in [Discord](https://discord.gg/JDdSxwjhGf) or check the [context menu example](https://github.com/wailsapp/wails/tree/v3-alpha/v3/examples/contextmenus).
|