mirror of
https://github.com/wailsapp/wails.git
synced 2025-05-02 03:51:18 +08:00
Support context menus
This commit is contained in:
parent
23d2c9a741
commit
9b25e639f5
@ -59,3 +59,14 @@ TBD
|
||||
|
||||
Dialogs are now available in JavaScript!
|
||||
|
||||
## Drag and Drop
|
||||
|
||||
TBD
|
||||
|
||||
## Context Menus
|
||||
|
||||
Context menus are contextual menus that are shown when the user right clicks on an element. Creating a context menu is the same as creating a standard menu , by using `app.NewMenu()`. To make the context menu available to a window, call `window.RegisterContextMenu(name, menu)`. The name will be the id of the context menu and used by the frontend.
|
||||
|
||||
To indicate that an element has a context menu, add the `data-contextmenu-id` attribute to the element. The value of this attribute should be the name of a context menu previously registered with the window.
|
||||
|
||||
It is possible to register a context menu at the application level, making it available to all windows. This can be done using `app.RegisterContextMenu(name, menu)`. If a context menu cannot be found at the window level, the application context menus will be checked. A demo of this can be found in `v3/examples/contextmenus`.
|
30
v3/examples/contextmenus/assets/index.html
Normal file
30
v3/examples/contextmenus/assets/index.html
Normal file
@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Title</title>
|
||||
<style>body{ text-align: center; color: white; background-color: rgba(0,0,0,0); user-select: none; -ms-user-select: none; -webkit-user-select: none; }</style>
|
||||
<style>.file{ width: 100px; height: 100px; border: 3px solid black; }</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Events Demo</h1>
|
||||
<br/>
|
||||
<div class="file" id="123abc" data-contextmenu-id="test" data-contextmenu-data="1" draggable="true" ondragstart="dragstart()" ondragend="dragend()">
|
||||
<h1>1</h1>
|
||||
</div>
|
||||
<div class="file" id="234abc" data-contextmenu-id="test" data-contextmenu-data="2" draggable="true" ondragstart="dragstart()" ondragend="dragend()">
|
||||
<h1>2</h1>
|
||||
</div>
|
||||
<div class="file" id="345abc" data-contextmenu-id="test" data-contextmenu-data="3" draggable="true" ondragstart="dragstart()" ondragend="dragend()">
|
||||
<h1>3</h1>
|
||||
</div>
|
||||
<div id="results"></div>
|
||||
</body>
|
||||
|
||||
<script>
|
||||
// window.addEventListener("dragstart", (event) =>
|
||||
// event.dataTransfer.setData("text/plain", "This text may be dragged")
|
||||
// );
|
||||
</script>
|
||||
|
||||
</html>
|
70
v3/examples/contextmenus/main.go
Normal file
70
v3/examples/contextmenus/main.go
Normal file
@ -0,0 +1,70 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
//go:embed assets
|
||||
var assets embed.FS
|
||||
|
||||
func main() {
|
||||
|
||||
app := application.New(application.Options{
|
||||
Name: "Context Menu Demo",
|
||||
Description: "A demo of the Context Menu API",
|
||||
Mac: application.MacOptions{
|
||||
ApplicationShouldTerminateAfterLastWindowClosed: true,
|
||||
},
|
||||
})
|
||||
|
||||
mainWindow := app.NewWebviewWindowWithOptions(&application.WebviewWindowOptions{
|
||||
Title: "Context Menu Demo",
|
||||
Assets: application.AssetOptions{
|
||||
FS: assets,
|
||||
},
|
||||
Mac: application.MacWindow{
|
||||
Backdrop: application.MacBackdropTranslucent,
|
||||
TitleBar: application.MacTitleBarHiddenInsetUnified,
|
||||
InvisibleTitleBarHeight: 50,
|
||||
},
|
||||
})
|
||||
|
||||
app.NewWebviewWindowWithOptions(&application.WebviewWindowOptions{
|
||||
Title: "Context Menu Demo",
|
||||
Assets: application.AssetOptions{
|
||||
FS: assets,
|
||||
},
|
||||
Mac: application.MacWindow{
|
||||
Backdrop: application.MacBackdropTranslucent,
|
||||
TitleBar: application.MacTitleBarHiddenInsetUnified,
|
||||
InvisibleTitleBarHeight: 50,
|
||||
},
|
||||
})
|
||||
|
||||
contextMenu := app.NewMenu()
|
||||
contextMenu.Add("Click Me").OnClick(func(data *application.Context) {
|
||||
fmt.Printf("Context menu data: %+v\n", data.ContextMenuData())
|
||||
})
|
||||
|
||||
globalContextMenu := app.NewMenu()
|
||||
globalContextMenu.Add("Default context menu item").OnClick(func(data *application.Context) {
|
||||
fmt.Printf("Context menu data: %+v\n", 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 {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
}
|
@ -23,17 +23,6 @@
|
||||
window.addEventListener("dragstart", (event) =>
|
||||
event.dataTransfer.setData("text/plain", "This text may be dragged")
|
||||
);
|
||||
window.addEventListener("contextmenu", (event) => {
|
||||
console.log({event})
|
||||
let element = event.target;
|
||||
let contextMenuId = element.getAttribute("data-contextmenu-id");
|
||||
if (contextMenuId) {
|
||||
let contextMenuData = element.getAttribute("data-contextmenu-data");
|
||||
console.log({contextMenuId, contextMenuData, x: event.clientX, y: event.clientY});
|
||||
_wails.openContextMenu(contextMenuId, event.clientX, event.clientY, contextMenuData);
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
</html>
|
@ -6,7 +6,7 @@ function openContextMenu(id, x, y, data) {
|
||||
return call("OpenContextMenu", {id, x, y, data});
|
||||
}
|
||||
|
||||
function enableContextMenus(enabled) {
|
||||
export function enableContextMenus(enabled) {
|
||||
if (enabled) {
|
||||
window.addEventListener('contextmenu', contextMenuHandler);
|
||||
} else {
|
||||
@ -14,15 +14,19 @@ function enableContextMenus(enabled) {
|
||||
}
|
||||
}
|
||||
|
||||
function contextMenuHandler(e) {
|
||||
let element = e.target;
|
||||
let contextMenuId = element.getAttribute("data-contextmenu-id");
|
||||
if (contextMenuId) {
|
||||
let contextMenuData = element.getAttribute("data-contextmenu-data");
|
||||
console.log({contextMenuId, contextMenuData, x: e.clientX, y: e.clientY});
|
||||
e.preventDefault();
|
||||
return openContextMenu(contextMenuId, e.clientX, e.clientY, contextMenuData);
|
||||
}
|
||||
function contextMenuHandler(event) {
|
||||
processContextMenu(event.target, event);
|
||||
}
|
||||
|
||||
enableContextMenus(true);
|
||||
function processContextMenu(element, event) {
|
||||
let id = element.getAttribute('data-contextmenu-id');
|
||||
if (id) {
|
||||
event.preventDefault();
|
||||
openContextMenu(id, event.clientX, event.clientY, element.getAttribute('data-contextmenu-data'));
|
||||
} else {
|
||||
let parent = element.parentElement;
|
||||
if (parent) {
|
||||
processContextMenu(parent, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ import * as Log from './log';
|
||||
import {newWindow} from "./window";
|
||||
import {dispatchCustomEvent, Emit, Off, OffAll, On, Once, OnMultiple} from "./events";
|
||||
import {dialogCallback, dialogErrorCallback, Error, Info, OpenFile, Question, SaveFile, Warning,} from "./dialogs";
|
||||
import {enableContextMenus} from "./contextmenu";
|
||||
|
||||
window.wails = {
|
||||
...newRuntime(-1),
|
||||
@ -63,4 +64,4 @@ if (DEBUG) {
|
||||
console.log("Wails v3.0.0 Debug Mode Enabled");
|
||||
}
|
||||
|
||||
|
||||
enableContextMenus(true);
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -31,6 +31,7 @@ func New(appOptions Options) *App {
|
||||
applicationEventListeners: make(map[uint][]func()),
|
||||
systemTrays: make(map[uint]*SystemTray),
|
||||
log: logger.New(appOptions.Logger.CustomLoggers...),
|
||||
contextMenus: make(map[string]*Menu),
|
||||
}
|
||||
|
||||
if !appOptions.Logger.Silent {
|
||||
@ -122,6 +123,9 @@ type App struct {
|
||||
clipboard *Clipboard
|
||||
Events *EventProcessor
|
||||
log *logger.Logger
|
||||
|
||||
contextMenus map[string]*Menu
|
||||
contextMenusLock sync.Mutex
|
||||
}
|
||||
|
||||
func (a *App) getSystemTrayID() uint {
|
||||
@ -487,3 +491,17 @@ func (a *App) Show() {
|
||||
func (a *App) Log(message *logger.Message) {
|
||||
a.log.Log(message)
|
||||
}
|
||||
|
||||
func (a *App) RegisterContextMenu(name string, menu *Menu) {
|
||||
a.contextMenusLock.Lock()
|
||||
defer a.contextMenusLock.Unlock()
|
||||
a.contextMenus[name] = menu
|
||||
}
|
||||
|
||||
func (a *App) getContextMenu(name string) (*Menu, bool) {
|
||||
a.contextMenusLock.Lock()
|
||||
defer a.contextMenusLock.Unlock()
|
||||
menu, ok := a.contextMenus[name]
|
||||
return menu, ok
|
||||
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ func newContext() *Context {
|
||||
const (
|
||||
clickedMenuItem string = "clickedMenuItem"
|
||||
menuItemIsChecked string = "menuItemIsChecked"
|
||||
contextMenuData string = "contextMenuData"
|
||||
)
|
||||
|
||||
func (c *Context) ClickedMenuItem() *MenuItem {
|
||||
@ -31,6 +32,9 @@ func (c *Context) IsChecked() bool {
|
||||
}
|
||||
return result.(bool)
|
||||
}
|
||||
func (c *Context) ContextMenuData() any {
|
||||
return c.data[contextMenuData]
|
||||
}
|
||||
|
||||
func (c *Context) withClickedMenuItem(menuItem *MenuItem) *Context {
|
||||
c.data[clickedMenuItem] = menuItem
|
||||
@ -40,3 +44,11 @@ func (c *Context) withClickedMenuItem(menuItem *MenuItem) *Context {
|
||||
func (c *Context) withChecked(checked bool) {
|
||||
c.data[menuItemIsChecked] = checked
|
||||
}
|
||||
|
||||
func (c *Context) withContextMenuData(data *ContextMenuData) *Context {
|
||||
if data == nil {
|
||||
return c
|
||||
}
|
||||
c.data[contextMenuData] = data.Data
|
||||
return c
|
||||
}
|
||||
|
@ -87,6 +87,12 @@ func (m *Menu) SetLabel(label string) {
|
||||
m.label = label
|
||||
}
|
||||
|
||||
func (m *Menu) setContextData(data *ContextMenuData) {
|
||||
for _, item := range m.items {
|
||||
item.setContextData(data)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) NewMenu() *Menu {
|
||||
return &Menu{}
|
||||
}
|
||||
|
@ -41,16 +41,17 @@ type menuItemImpl interface {
|
||||
}
|
||||
|
||||
type MenuItem struct {
|
||||
id uint
|
||||
label string
|
||||
tooltip string
|
||||
disabled bool
|
||||
checked bool
|
||||
submenu *Menu
|
||||
callback func(*Context)
|
||||
itemType menuItemType
|
||||
accelerator *accelerator
|
||||
role Role
|
||||
id uint
|
||||
label string
|
||||
tooltip string
|
||||
disabled bool
|
||||
checked bool
|
||||
submenu *Menu
|
||||
callback func(*Context)
|
||||
itemType menuItemType
|
||||
accelerator *accelerator
|
||||
role Role
|
||||
contextMenuData *ContextMenuData
|
||||
|
||||
impl menuItemImpl
|
||||
radioGroupMembers []*MenuItem
|
||||
@ -187,7 +188,9 @@ func newServicesMenu() *MenuItem {
|
||||
}
|
||||
|
||||
func (m *MenuItem) handleClick() {
|
||||
var ctx = newContext().withClickedMenuItem(m)
|
||||
var ctx = newContext().
|
||||
withClickedMenuItem(m).
|
||||
withContextMenuData(m.contextMenuData)
|
||||
if m.itemType == checkbox {
|
||||
m.checked = !m.checked
|
||||
ctx.withChecked(m.checked)
|
||||
@ -272,3 +275,10 @@ func (m *MenuItem) Tooltip() string {
|
||||
func (m *MenuItem) Enabled() bool {
|
||||
return !m.disabled
|
||||
}
|
||||
|
||||
func (m *MenuItem) setContextData(data *ContextMenuData) {
|
||||
m.contextMenuData = data
|
||||
if m.submenu != nil {
|
||||
m.submenu.setContextData(data)
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ func (m *MessageProcessor) processContextMenuMethod(method string, rw http.Respo
|
||||
m.httpError(rw, "error parsing contextmenu message: %s", err.Error())
|
||||
return
|
||||
}
|
||||
window.openContextMenu(data)
|
||||
window.openContextMenu(&data)
|
||||
m.ok(rw)
|
||||
default:
|
||||
m.httpError(rw, "Unknown clipboard method: %s", method)
|
||||
|
@ -2,11 +2,9 @@ package application
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/options"
|
||||
)
|
||||
|
||||
func (m *MessageProcessor) processWindowMethod(method string, rw http.ResponseWriter, r *http.Request, window *WebviewWindow, params QueryParams) {
|
||||
func (m *MessageProcessor) processWindowMethod(method string, rw http.ResponseWriter, _ *http.Request, window *WebviewWindow, params QueryParams) {
|
||||
|
||||
args, err := params.Args()
|
||||
if err != nil {
|
||||
@ -104,7 +102,7 @@ func (m *MessageProcessor) processWindowMethod(method string, rw http.ResponseWr
|
||||
m.Error("Invalid SetBackgroundColour Message: 'a' value required")
|
||||
return
|
||||
}
|
||||
window.SetBackgroundColour(&options.RGBA{
|
||||
window.SetBackgroundColour(&RGBA{
|
||||
Red: *r,
|
||||
Green: *g,
|
||||
Blue: *b,
|
||||
|
@ -29,9 +29,8 @@ extern void processDragItems(unsigned int windowId, char** arr, int length);
|
||||
}
|
||||
|
||||
|
||||
- (NSDragOperation)draggingExited:(id<NSDraggingInfo>)sender {
|
||||
- (void)draggingExited:(id<NSDraggingInfo>)sender {
|
||||
NSLog(@"I am here!!!!");
|
||||
return NSDragOperationCopy;
|
||||
}
|
||||
|
||||
- (BOOL)prepareForDragOperation:(id<NSDraggingInfo>)sender {
|
||||
|
@ -62,6 +62,7 @@ type (
|
||||
hide()
|
||||
getScreen() (*Screen, error)
|
||||
setFrameless(bool)
|
||||
openContextMenu(menu *Menu, data *ContextMenuData)
|
||||
}
|
||||
)
|
||||
|
||||
@ -76,6 +77,9 @@ type WebviewWindow struct {
|
||||
|
||||
eventListeners map[uint][]func()
|
||||
eventListenersLock sync.RWMutex
|
||||
|
||||
contextMenus map[string]*Menu
|
||||
contextMenusLock sync.RWMutex
|
||||
}
|
||||
|
||||
var windowID uint
|
||||
@ -110,6 +114,7 @@ func NewWindow(options *WebviewWindowOptions) *WebviewWindow {
|
||||
id: getWindowID(),
|
||||
options: options,
|
||||
eventListeners: make(map[uint][]func()),
|
||||
contextMenus: make(map[string]*Menu),
|
||||
|
||||
assets: srv,
|
||||
}
|
||||
@ -640,6 +645,16 @@ func (w *WebviewWindow) info(message string, args ...any) {
|
||||
Time: time.Now(),
|
||||
})
|
||||
}
|
||||
func (w *WebviewWindow) error(message string, args ...any) {
|
||||
|
||||
globalApplication.Log(&logger.Message{
|
||||
Level: "ERROR",
|
||||
Message: message,
|
||||
Data: args,
|
||||
Sender: w.Name(),
|
||||
Time: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
func (w *WebviewWindow) handleDragAndDropMessage(event *dragAndDropMessage) {
|
||||
println("Drag and drop message received for " + w.Name())
|
||||
@ -649,6 +664,25 @@ func (w *WebviewWindow) handleDragAndDropMessage(event *dragAndDropMessage) {
|
||||
}
|
||||
}
|
||||
|
||||
func (w *WebviewWindow) openContextMenu(data ContextMenuData) {
|
||||
fmt.Printf("Opening context menu for %+v\n", data)
|
||||
func (w *WebviewWindow) openContextMenu(data *ContextMenuData) {
|
||||
menu, ok := w.contextMenus[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
|
||||
}
|
||||
}
|
||||
menu.setContextData(data)
|
||||
if w.impl == nil {
|
||||
return
|
||||
}
|
||||
w.impl.openContextMenu(menu, data)
|
||||
}
|
||||
|
||||
func (w *WebviewWindow) RegisterContextMenu(name string, menu *Menu) {
|
||||
w.contextMenusLock.Lock()
|
||||
defer w.contextMenusLock.Unlock()
|
||||
w.contextMenus[name] = menu
|
||||
}
|
||||
|
@ -756,6 +756,24 @@ static void windowHide(void *window) {
|
||||
});
|
||||
}
|
||||
|
||||
// windowShowMenu opens an NSMenu at the given coordinates
|
||||
static void windowShowMenu(void *window, void *menu, int x, int y) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
// get main window
|
||||
WebviewWindow* nsWindow = (WebviewWindow*)window;
|
||||
// get menu
|
||||
NSMenu* nsMenu = (NSMenu*)menu;
|
||||
// get webview
|
||||
WebviewWindowDelegate* windowDelegate = (WebviewWindowDelegate*)[nsWindow delegate];
|
||||
// get webview
|
||||
WKWebView* webView = (WKWebView*)windowDelegate.webView;
|
||||
NSPoint point = NSMakePoint(x, y);
|
||||
[nsMenu popUpMenuPositioningItem:nil atLocation:point inView:webView];
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Make the given window frameless
|
||||
static void windowSetFrameless(void *window, bool frameless) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
@ -786,6 +804,13 @@ type macosWebviewWindow struct {
|
||||
parent *WebviewWindow
|
||||
}
|
||||
|
||||
func (w *macosWebviewWindow) openContextMenu(menu *Menu, data *ContextMenuData) {
|
||||
// Create the menu
|
||||
thisMenu := newMenuImpl(menu)
|
||||
thisMenu.update()
|
||||
C.windowShowMenu(w.nsWindow, thisMenu.nsMenu, C.int(data.X), C.int(data.Y))
|
||||
}
|
||||
|
||||
func (w *macosWebviewWindow) getZoom() float64 {
|
||||
return float64(C.windowZoomGet(w.nsWindow))
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user