5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-03 15:00:21 +08:00
wails/v3/pkg/application/popupmenu_windows.go
Ian VanSchooten b733b1d3c4
Fix hidden menuItem on windows
And bring menu_windows into alignment with popupmenu_windows
2025-03-08 11:25:06 +11:00

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], &currentRadioGroup)
}
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], &currentRadioGroup)
}
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
}