From be8159d41009a998508a04ab17011025e87ee518 Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Fri, 17 Jan 2025 21:19:23 +1100 Subject: [PATCH] # Conflicts: # docs/src/content/docs/changelog.mdx # v3/pkg/application/menuitem.go --- docs/src/content/docs/changelog.mdx | 2 + docs/src/content/docs/guides/menus.mdx | 491 ++++++++++++++++++ v3/examples/contextmenus/main.go | 21 +- v3/examples/window-menu/README.md | 24 + v3/examples/window-menu/assets/about.html | 14 + v3/examples/window-menu/assets/index.html | 48 ++ v3/examples/window-menu/assets/style.css | 26 + v3/examples/window-menu/main.go | 64 +++ v3/pkg/application/application.go | 16 +- v3/pkg/application/context.go | 13 +- v3/pkg/application/menu.go | 23 + .../messageprocessor_contextmenu.go | 2 +- v3/pkg/application/webview_window.go | 55 +- v3/pkg/application/webview_window_darwin.go | 4 + v3/pkg/application/webview_window_linux.go | 4 + v3/pkg/application/webview_window_windows.go | 79 ++- v3/pkg/application/window.go | 4 +- v3/pkg/w32/constants.go | 30 ++ v3/pkg/w32/window.go | 25 +- 19 files changed, 889 insertions(+), 56 deletions(-) create mode 100644 docs/src/content/docs/guides/menus.mdx create mode 100644 v3/examples/window-menu/README.md create mode 100644 v3/examples/window-menu/assets/about.html create mode 100644 v3/examples/window-menu/assets/index.html create mode 100644 v3/examples/window-menu/assets/style.css create mode 100644 v3/examples/window-menu/main.go diff --git a/docs/src/content/docs/changelog.mdx b/docs/src/content/docs/changelog.mdx index 6ad587ab42d..7ad0571f437 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 00000000000..ed2de62d8ff --- /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 + + +``` + +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 8aa15d37b23..50aa0a132a7 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 00000000000..7b3a517e410 --- /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 00000000000..e887a84ceb4 --- /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 00000000000..b18f601e064 --- /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:

+ +
+ + \ 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 00000000000..c7fc71f390e --- /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 00000000000..03c2acb35a0 --- /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 edd50402456..ec9e34b275c 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 56b2133501f..16d6d7dbcf7 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 948c95a9c62..5a8673ea7b6 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 0d7f2e8948b..84b0ea45833 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 866b8fa7054..7776f728026 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 1c855f74906..bd787007ace 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 b83d2e81b76..f5c604a051a 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 e724dabe63e..ef2243f1641 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_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: - w.processKeyBinding(uint(wparam)) + 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 5669e54d5c0..3f3dea64a8f 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 a7684851700..234933ba615 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 542971db7d9..3b4cefa7971 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 +}