mirror of
https://github.com/wailsapp/wails.git
synced 2025-05-03 15:00:21 +08:00
304 lines
7.1 KiB
Go
304 lines
7.1 KiB
Go
package application
|
|
|
|
import (
|
|
"github.com/wailsapp/wails/v3/pkg/w32"
|
|
"unsafe"
|
|
)
|
|
|
|
const (
|
|
MenuItemMsgID = w32.WM_APP + 1024
|
|
)
|
|
|
|
type RadioGroupMember struct {
|
|
ID int
|
|
MenuItem *MenuItem
|
|
}
|
|
|
|
type RadioGroup []*RadioGroupMember
|
|
|
|
func (r *RadioGroup) Add(id int, item *MenuItem) {
|
|
*r = append(*r, &RadioGroupMember{
|
|
ID: id,
|
|
MenuItem: item,
|
|
})
|
|
}
|
|
|
|
func (r *RadioGroup) Bounds() (int, int) {
|
|
p := *r
|
|
return p[0].ID, p[len(p)-1].ID
|
|
}
|
|
|
|
func (r *RadioGroup) MenuID(item *MenuItem) int {
|
|
for _, member := range *r {
|
|
if member.MenuItem == item {
|
|
return member.ID
|
|
}
|
|
}
|
|
panic("RadioGroup.MenuID: item not found:")
|
|
}
|
|
|
|
type Win32Menu struct {
|
|
isPopup bool
|
|
menu w32.HMENU
|
|
parentWindow *windowsWebviewWindow
|
|
parent w32.HWND
|
|
menuMapping map[int]*MenuItem
|
|
checkboxItems map[*MenuItem][]int
|
|
radioGroups map[*MenuItem][]*RadioGroup
|
|
menuData *Menu
|
|
currentMenuID int
|
|
onMenuClose func()
|
|
onMenuOpen func()
|
|
}
|
|
|
|
func (p *Win32Menu) newMenu() w32.HMENU {
|
|
if p.isPopup {
|
|
return w32.NewPopupMenu()
|
|
}
|
|
return w32.CreateMenu()
|
|
}
|
|
|
|
func (p *Win32Menu) buildMenu(parentMenu w32.HMENU, inputMenu *Menu) {
|
|
currentRadioGroup := RadioGroup{}
|
|
for _, item := range inputMenu.items {
|
|
p.currentMenuID++
|
|
itemID := p.currentMenuID
|
|
p.menuMapping[itemID] = item
|
|
|
|
menuItemImpl := newMenuItemImpl(item, parentMenu, itemID)
|
|
menuItemImpl.parent = inputMenu
|
|
item.impl = menuItemImpl
|
|
|
|
if item.Hidden() {
|
|
if item.accelerator != nil {
|
|
if p.parentWindow != nil {
|
|
// Remove the accelerator from the keybindings
|
|
p.parentWindow.parent.removeMenuBinding(item.accelerator)
|
|
} else {
|
|
// Remove the global keybindings
|
|
globalApplication.removeKeyBinding(item.accelerator.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
flags := uint32(w32.MF_STRING)
|
|
if item.disabled {
|
|
flags = flags | w32.MF_GRAYED
|
|
}
|
|
if item.checked {
|
|
flags = flags | w32.MF_CHECKED
|
|
}
|
|
if item.IsSeparator() {
|
|
flags = flags | w32.MF_SEPARATOR
|
|
}
|
|
|
|
if item.checked && item.IsRadio() {
|
|
flags = flags | w32.MFT_RADIOCHECK
|
|
}
|
|
|
|
if item.IsCheckbox() {
|
|
p.checkboxItems[item] = append(p.checkboxItems[item], itemID)
|
|
}
|
|
if item.IsRadio() {
|
|
currentRadioGroup.Add(itemID, item)
|
|
} else {
|
|
if len(currentRadioGroup) > 0 {
|
|
for _, radioMember := range currentRadioGroup {
|
|
currentRadioGroup := currentRadioGroup
|
|
p.radioGroups[radioMember.MenuItem] = append(p.radioGroups[radioMember.MenuItem], ¤tRadioGroup)
|
|
}
|
|
currentRadioGroup = RadioGroup{}
|
|
}
|
|
}
|
|
|
|
if item.submenu != nil {
|
|
flags = flags | w32.MF_POPUP
|
|
newSubmenu := p.newMenu()
|
|
p.buildMenu(newSubmenu, item.submenu)
|
|
itemID = int(newSubmenu)
|
|
menuItemImpl.submenu = newSubmenu
|
|
}
|
|
|
|
var menuText = item.Label()
|
|
if item.accelerator != nil {
|
|
menuText = menuText + "\t" + item.accelerator.String()
|
|
if item.callback != nil {
|
|
if p.parentWindow != nil {
|
|
p.parentWindow.parent.addMenuBinding(item.accelerator, item)
|
|
} else {
|
|
globalApplication.addKeyBinding(item.accelerator.String(), func(w *WebviewWindow) {
|
|
item.handleClick()
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// If the item is hidden, don't append
|
|
if item.Hidden() {
|
|
continue
|
|
}
|
|
|
|
ok := w32.AppendMenu(parentMenu, flags, uintptr(itemID), w32.MustStringToUTF16Ptr(menuText))
|
|
if !ok {
|
|
globalApplication.fatal("error adding menu item '%s'", menuText)
|
|
}
|
|
if item.bitmap != nil {
|
|
err := w32.SetMenuIcons(parentMenu, itemID, item.bitmap, nil)
|
|
if err != nil {
|
|
globalApplication.fatal("error setting menu icons: %w", err)
|
|
}
|
|
}
|
|
}
|
|
if len(currentRadioGroup) > 0 {
|
|
for _, radioMember := range currentRadioGroup {
|
|
currentRadioGroup := currentRadioGroup
|
|
p.radioGroups[radioMember.MenuItem] = append(p.radioGroups[radioMember.MenuItem], ¤tRadioGroup)
|
|
}
|
|
currentRadioGroup = RadioGroup{}
|
|
}
|
|
}
|
|
|
|
func (p *Win32Menu) Update() {
|
|
p.menu = p.newMenu()
|
|
p.menuMapping = make(map[int]*MenuItem)
|
|
p.currentMenuID = MenuItemMsgID
|
|
p.buildMenu(p.menu, p.menuData)
|
|
p.updateRadioGroups()
|
|
}
|
|
|
|
func NewPopupMenu(parent w32.HWND, inputMenu *Menu) *Win32Menu {
|
|
result := &Win32Menu{
|
|
isPopup: true,
|
|
parent: parent,
|
|
menuData: inputMenu,
|
|
checkboxItems: make(map[*MenuItem][]int),
|
|
radioGroups: make(map[*MenuItem][]*RadioGroup),
|
|
}
|
|
result.Update()
|
|
return result
|
|
}
|
|
func NewApplicationMenu(parent *windowsWebviewWindow, inputMenu *Menu) *Win32Menu {
|
|
result := &Win32Menu{
|
|
parentWindow: parent,
|
|
parent: parent.hwnd,
|
|
menuData: inputMenu,
|
|
checkboxItems: make(map[*MenuItem][]int),
|
|
radioGroups: make(map[*MenuItem][]*RadioGroup),
|
|
}
|
|
result.Update()
|
|
return result
|
|
}
|
|
|
|
func (p *Win32Menu) ShowAt(x int, y int) {
|
|
|
|
w32.SetForegroundWindow(p.parent)
|
|
|
|
if p.onMenuOpen != nil {
|
|
p.onMenuOpen()
|
|
}
|
|
|
|
// Get screen dimensions to determine menu positioning
|
|
monitor := w32.MonitorFromWindow(p.parent, w32.MONITOR_DEFAULTTONEAREST)
|
|
var monitorInfo w32.MONITORINFO
|
|
monitorInfo.CbSize = uint32(unsafe.Sizeof(monitorInfo))
|
|
if !w32.GetMonitorInfo(monitor, &monitorInfo) {
|
|
globalApplication.fatal("GetMonitorInfo failed")
|
|
}
|
|
|
|
// Set flags to always position the menu above the cursor
|
|
menuFlags := uint32(w32.TPM_LEFTALIGN | w32.TPM_BOTTOMALIGN)
|
|
|
|
// Check if we're close to the right edge of the screen
|
|
// If so, right-align the menu with some padding
|
|
if x > int(monitorInfo.RcWork.Right)-200 { // Assuming 200px as a reasonable menu width
|
|
menuFlags = uint32(w32.TPM_RIGHTALIGN | w32.TPM_BOTTOMALIGN)
|
|
// Add a small padding (10px) from the right edge
|
|
x = int(monitorInfo.RcWork.Right) - 10
|
|
}
|
|
|
|
if !w32.TrackPopupMenuEx(p.menu, menuFlags, int32(x), int32(y), p.parent, nil) {
|
|
globalApplication.fatal("TrackPopupMenu failed")
|
|
}
|
|
|
|
if p.onMenuClose != nil {
|
|
p.onMenuClose()
|
|
}
|
|
|
|
if !w32.PostMessage(p.parent, w32.WM_NULL, 0, 0) {
|
|
globalApplication.fatal("PostMessage failed")
|
|
}
|
|
|
|
}
|
|
|
|
func (p *Win32Menu) ShowAtCursor() {
|
|
x, y, ok := w32.GetCursorPos()
|
|
if ok == false {
|
|
globalApplication.fatal("GetCursorPos failed")
|
|
}
|
|
|
|
p.ShowAt(x, y)
|
|
}
|
|
|
|
func (p *Win32Menu) ProcessCommand(cmdMsgID int) bool {
|
|
item := p.menuMapping[cmdMsgID]
|
|
if item == nil {
|
|
return false
|
|
}
|
|
if item.IsRadio() {
|
|
if item.checked {
|
|
return true
|
|
}
|
|
item.checked = true
|
|
p.updateRadioGroup(item)
|
|
}
|
|
if item.callback != nil {
|
|
item.handleClick()
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (p *Win32Menu) Destroy() {
|
|
w32.DestroyMenu(p.menu)
|
|
}
|
|
|
|
func (p *Win32Menu) UpdateMenuItem(item *MenuItem) {
|
|
if item.IsCheckbox() {
|
|
for _, itemID := range p.checkboxItems[item] {
|
|
var checkState uint = w32.MF_UNCHECKED
|
|
if item.checked {
|
|
checkState = w32.MF_CHECKED
|
|
}
|
|
w32.CheckMenuItem(p.menu, uintptr(itemID), checkState)
|
|
}
|
|
return
|
|
}
|
|
if item.IsRadio() && item.checked == true {
|
|
p.updateRadioGroup(item)
|
|
}
|
|
}
|
|
|
|
func (p *Win32Menu) updateRadioGroups() {
|
|
for menuItem := range p.radioGroups {
|
|
if menuItem.checked {
|
|
p.updateRadioGroup(menuItem)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *Win32Menu) updateRadioGroup(item *MenuItem) {
|
|
for _, radioGroup := range p.radioGroups[item] {
|
|
thisMenuID := radioGroup.MenuID(item)
|
|
startID, endID := radioGroup.Bounds()
|
|
w32.CheckRadio(p.menu, startID, endID, thisMenuID)
|
|
|
|
}
|
|
}
|
|
|
|
func (p *Win32Menu) OnMenuOpen(fn func()) {
|
|
p.onMenuOpen = fn
|
|
}
|
|
|
|
func (p *Win32Menu) OnMenuClose(fn func()) {
|
|
p.onMenuClose = fn
|
|
}
|