5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-02 20:03:01 +08:00
wails/v2/internal/frontend/desktop/windows/winc/menu.go
2022-03-29 21:45:16 +11:00

338 lines
8.2 KiB
Go

/*
* Copyright (C) 2019 The Winc Authors. All Rights Reserved.
*/
package winc
import (
"fmt"
"syscall"
"unsafe"
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32"
)
var (
nextMenuItemID uint16 = 3
actionsByID = make(map[uint16]*MenuItem)
shortcut2Action = make(map[Shortcut]*MenuItem)
menuItems = make(map[w32.HMENU][]*MenuItem)
radioGroups = make(map[*MenuItem]*RadioGroup)
initialised bool
)
var NoShortcut = Shortcut{}
// Menu for main window and context menus on controls.
// Most methods used for both main window menu and context menu.
type Menu struct {
hMenu w32.HMENU
hwnd w32.HWND // hwnd might be nil if it is context menu.
}
type MenuItem struct {
hMenu w32.HMENU
hSubMenu w32.HMENU // Non zero if this item is in itself a submenu.
text string
toolTip string
image *Bitmap
shortcut Shortcut
enabled bool
checkable bool
checked bool
isRadio bool
id uint16
onClick EventManager
}
type RadioGroup struct {
members []*MenuItem
hwnd w32.HWND
}
func NewContextMenu() *MenuItem {
hMenu := w32.CreatePopupMenu()
if hMenu == 0 {
panic("failed CreateMenu")
}
item := &MenuItem{
hMenu: hMenu,
hSubMenu: hMenu,
}
return item
}
func (m *Menu) Dispose() {
if m.hMenu != 0 {
w32.DestroyMenu(m.hMenu)
m.hMenu = 0
}
}
func (m *Menu) IsDisposed() bool {
return m.hMenu == 0
}
func initMenuItemInfoFromAction(mii *w32.MENUITEMINFO, a *MenuItem) {
mii.CbSize = uint32(unsafe.Sizeof(*mii))
mii.FMask = w32.MIIM_FTYPE | w32.MIIM_ID | w32.MIIM_STATE | w32.MIIM_STRING
if a.image != nil {
mii.FMask |= w32.MIIM_BITMAP
mii.HbmpItem = a.image.handle
}
if a.IsSeparator() {
mii.FType = w32.MFT_SEPARATOR
} else {
mii.FType = w32.MFT_STRING
var text string
if s := a.shortcut; s.Key != 0 {
text = fmt.Sprintf("%s\t%s", a.text, s.String())
shortcut2Action[a.shortcut] = a
} else {
text = a.text
}
mii.DwTypeData = syscall.StringToUTF16Ptr(text)
mii.Cch = uint32(len([]rune(a.text)))
}
mii.WID = uint32(a.id)
if a.Enabled() {
mii.FState &^= w32.MFS_DISABLED
} else {
mii.FState |= w32.MFS_DISABLED
}
if a.Checkable() {
mii.FMask |= w32.MIIM_CHECKMARKS
}
if a.Checked() {
mii.FState |= w32.MFS_CHECKED
}
if a.hSubMenu != 0 {
mii.FMask |= w32.MIIM_SUBMENU
mii.HSubMenu = a.hSubMenu
}
}
// Show menu on the main window.
func (m *Menu) Show() {
initialised = true
updateRadioGroups()
if !w32.DrawMenuBar(m.hwnd) {
panic("DrawMenuBar failed")
}
}
// AddSubMenu returns item that is used as submenu to perform AddItem(s).
func (m *Menu) AddSubMenu(text string) *MenuItem {
hSubMenu := w32.CreateMenu()
if hSubMenu == 0 {
panic("failed CreateMenu")
}
return addMenuItem(m.hMenu, hSubMenu, text, Shortcut{}, nil, false)
}
// This method will iterate through the menu items, group radio items together, build a
// quick access map and set the initial items
func updateRadioGroups() {
if !initialised {
return
}
radioItemsChecked := []*MenuItem{}
radioGroups = make(map[*MenuItem]*RadioGroup)
var currentRadioGroupMembers []*MenuItem
// Iterate the menus
for _, menu := range menuItems {
menuLength := len(menu)
for index, menuItem := range menu {
if menuItem.isRadio {
currentRadioGroupMembers = append(currentRadioGroupMembers, menuItem)
if menuItem.checked {
radioItemsChecked = append(radioItemsChecked, menuItem)
}
// If end of menu
if index == menuLength-1 {
radioGroup := &RadioGroup{
members: currentRadioGroupMembers,
hwnd: menuItem.hMenu,
}
// Save the group to each member iin the radiomap
for _, member := range currentRadioGroupMembers {
radioGroups[member] = radioGroup
}
currentRadioGroupMembers = []*MenuItem{}
}
continue
}
// Not a radio item
if len(currentRadioGroupMembers) > 0 {
radioGroup := &RadioGroup{
members: currentRadioGroupMembers,
hwnd: menuItem.hMenu,
}
// Save the group to each member iin the radiomap
for _, member := range currentRadioGroupMembers {
radioGroups[member] = radioGroup
}
currentRadioGroupMembers = []*MenuItem{}
}
}
}
// Enable the checked items
for _, item := range radioItemsChecked {
radioGroup := radioGroups[item]
startID := radioGroup.members[0].id
endID := radioGroup.members[len(radioGroup.members)-1].id
w32.SelectRadioMenuItem(item.id, startID, endID, radioGroup.hwnd)
}
}
func (mi *MenuItem) OnClick() *EventManager {
return &mi.onClick
}
func (mi *MenuItem) AddSeparator() {
addMenuItem(mi.hSubMenu, 0, "-", Shortcut{}, nil, false)
}
// AddItem adds plain menu item.
func (mi *MenuItem) AddItem(text string, shortcut Shortcut) *MenuItem {
return addMenuItem(mi.hSubMenu, 0, text, shortcut, nil, false)
}
// AddItemCheckable adds plain menu item that can have a checkmark.
func (mi *MenuItem) AddItemCheckable(text string, shortcut Shortcut) *MenuItem {
return addMenuItem(mi.hSubMenu, 0, text, shortcut, nil, true)
}
// AddItemRadio adds plain menu item that can have a checkmark and is part of a radio group.
func (mi *MenuItem) AddItemRadio(text string, shortcut Shortcut) *MenuItem {
menuItem := addMenuItem(mi.hSubMenu, 0, text, shortcut, nil, true)
menuItem.isRadio = true
return menuItem
}
// AddItemWithBitmap adds menu item with shortcut and bitmap.
func (mi *MenuItem) AddItemWithBitmap(text string, shortcut Shortcut, image *Bitmap) *MenuItem {
return addMenuItem(mi.hSubMenu, 0, text, shortcut, image, false)
}
// AddSubMenu adds a submenu.
func (mi *MenuItem) AddSubMenu(text string) *MenuItem {
hSubMenu := w32.CreatePopupMenu()
if hSubMenu == 0 {
panic("failed CreatePopupMenu")
}
return addMenuItem(mi.hSubMenu, hSubMenu, text, Shortcut{}, nil, false)
}
// AddItem to the menu, set text to "-" for separators.
func addMenuItem(hMenu, hSubMenu w32.HMENU, text string, shortcut Shortcut, image *Bitmap, checkable bool) *MenuItem {
item := &MenuItem{
hMenu: hMenu,
hSubMenu: hSubMenu,
text: text,
shortcut: shortcut,
image: image,
enabled: true,
id: nextMenuItemID,
checkable: checkable,
isRadio: false,
//visible: true,
}
nextMenuItemID++
actionsByID[item.id] = item
menuItems[hMenu] = append(menuItems[hMenu], item)
var mii w32.MENUITEMINFO
initMenuItemInfoFromAction(&mii, item)
index := -1
if !w32.InsertMenuItem(hMenu, uint32(index), true, &mii) {
panic("InsertMenuItem failed")
}
return item
}
func indexInObserver(a *MenuItem) int {
var idx int
for _, mi := range menuItems[a.hMenu] {
if mi == a {
return idx
}
idx++
}
return -1
}
func findMenuItemByID(id int) *MenuItem {
return actionsByID[uint16(id)]
}
func (mi *MenuItem) update() {
var mii w32.MENUITEMINFO
initMenuItemInfoFromAction(&mii, mi)
if !w32.SetMenuItemInfo(mi.hMenu, uint32(indexInObserver(mi)), true, &mii) {
panic("SetMenuItemInfo failed")
}
if mi.isRadio {
mi.updateRadioGroup()
}
}
func (mi *MenuItem) IsSeparator() bool { return mi.text == "-" }
func (mi *MenuItem) SetSeparator() { mi.text = "-" }
func (mi *MenuItem) Enabled() bool { return mi.enabled }
func (mi *MenuItem) SetEnabled(b bool) { mi.enabled = b; mi.update() }
func (mi *MenuItem) Checkable() bool { return mi.checkable }
func (mi *MenuItem) SetCheckable(b bool) { mi.checkable = b; mi.update() }
func (mi *MenuItem) Checked() bool { return mi.checked }
func (mi *MenuItem) SetChecked(b bool) {
if mi.isRadio {
radioGroup := radioGroups[mi]
if radioGroup != nil {
for _, member := range radioGroup.members {
member.checked = false
}
}
}
mi.checked = b
mi.update()
}
func (mi *MenuItem) Text() string { return mi.text }
func (mi *MenuItem) SetText(s string) { mi.text = s; mi.update() }
func (mi *MenuItem) Image() *Bitmap { return mi.image }
func (mi *MenuItem) SetImage(b *Bitmap) { mi.image = b; mi.update() }
func (mi *MenuItem) ToolTip() string { return mi.toolTip }
func (mi *MenuItem) SetToolTip(s string) { mi.toolTip = s; mi.update() }
func (mi *MenuItem) updateRadioGroup() {
radioGroup := radioGroups[mi]
if radioGroup == nil {
return
}
startID := radioGroup.members[0].id
endID := radioGroup.members[len(radioGroup.members)-1].id
w32.SelectRadioMenuItem(mi.id, startID, endID, radioGroup.hwnd)
}