5
0
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:
Lea Anthony 2023-02-10 08:25:52 +11:00
parent 23d2c9a741
commit 9b25e639f5
No known key found for this signature in database
GPG Key ID: 33DAF7BB90A58405
21 changed files with 319 additions and 412 deletions

View File

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

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

View 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())
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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