mirror of
https://github.com/wailsapp/wails.git
synced 2026-03-14 22:55:48 +01:00
[windows] Menu checkboxes and radio groups now fully in sync when using Go menus mutltiple times.
This commit is contained in:
parent
3192026e6d
commit
76aab2271c
6 changed files with 597 additions and 280 deletions
|
|
@ -14,89 +14,25 @@ extern void DisableWindowIcon(struct Application* app);
|
|||
*/
|
||||
import "C"
|
||||
import (
|
||||
"github.com/wailsapp/wails/v2/internal/menumanager"
|
||||
"github.com/wailsapp/wails/v2/pkg/menu"
|
||||
"golang.org/x/sys/windows"
|
||||
"log"
|
||||
"strconv"
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
"github.com/wailsapp/wails/v2/pkg/menu"
|
||||
)
|
||||
|
||||
var (
|
||||
// DLL stuff
|
||||
user32 = windows.NewLazySystemDLL("User32.dll")
|
||||
win32CreateMenu = user32.NewProc("CreateMenu")
|
||||
win32DestroyMenu = user32.NewProc("DestroyMenu")
|
||||
win32CreatePopupMenu = user32.NewProc("CreatePopupMenu")
|
||||
win32AppendMenuW = user32.NewProc("AppendMenuW")
|
||||
win32SetMenu = user32.NewProc("SetMenu")
|
||||
win32CheckMenuItem = user32.NewProc("CheckMenuItem")
|
||||
win32GetMenuState = user32.NewProc("GetMenuState")
|
||||
win32CheckMenuRadioItem = user32.NewProc("CheckMenuRadioItem")
|
||||
applicationMenu *menumanager.WailsMenu
|
||||
menuManager *menumanager.Manager
|
||||
)
|
||||
// Setup the global caches
|
||||
var globalCheckboxCache = NewCheckboxCache()
|
||||
var globalRadioGroupCache = NewRadioGroupCache()
|
||||
var globalRadioGroupMap = NewRadioGroupMap()
|
||||
var globalApplicationMenu *Menu
|
||||
|
||||
const MF_BITMAP uint32 = 0x00000004
|
||||
const MF_CHECKED uint32 = 0x00000008
|
||||
const MF_DISABLED uint32 = 0x00000002
|
||||
const MF_ENABLED uint32 = 0x00000000
|
||||
const MF_GRAYED uint32 = 0x00000001
|
||||
const MF_MENUBARBREAK uint32 = 0x00000020
|
||||
const MF_MENUBREAK uint32 = 0x00000040
|
||||
const MF_OWNERDRAW uint32 = 0x00000100
|
||||
const MF_POPUP uint32 = 0x00000010
|
||||
const MF_SEPARATOR uint32 = 0x00000800
|
||||
const MF_STRING uint32 = 0x00000000
|
||||
const MF_UNCHECKED uint32 = 0x00000000
|
||||
const MF_BYCOMMAND uint32 = 0x00000000
|
||||
const MF_BYPOSITION uint32 = 0x00000400
|
||||
|
||||
type menuType int
|
||||
type menuType string
|
||||
|
||||
const (
|
||||
appMenuType menuType = iota
|
||||
appMenuType menuType = "ApplicationMenu"
|
||||
contextMenuType
|
||||
trayMenuType
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
MenuCache
|
||||
---------
|
||||
When windows calls back to Go (when an item is clicked), we need to
|
||||
be able to retrieve information about the menu item:
|
||||
- The menu that the menuitem is part of (parent)
|
||||
- The original processed menu item
|
||||
- The type of the menu (application, context or tray)
|
||||
|
||||
This cache is built up when a menu is created.
|
||||
|
||||
*/
|
||||
|
||||
type menuCacheEntry struct {
|
||||
parent uintptr
|
||||
menuType menuType
|
||||
item *menumanager.ProcessedMenuItem
|
||||
}
|
||||
|
||||
var menubar uintptr
|
||||
var menuCache = map[uint32]menuCacheEntry{}
|
||||
var menuCacheLock sync.RWMutex
|
||||
|
||||
func addMenuCacheEntry(id uint32, entry menuCacheEntry) {
|
||||
menuCacheLock.Lock()
|
||||
defer menuCacheLock.Unlock()
|
||||
menuCache[id] = entry
|
||||
}
|
||||
|
||||
func getMenuCacheEntry(id uint32) menuCacheEntry {
|
||||
menuCacheLock.Lock()
|
||||
defer menuCacheLock.Unlock()
|
||||
return menuCache[id]
|
||||
}
|
||||
|
||||
func (a *Application) processPlatformSettings() error {
|
||||
|
||||
menuManager = a.menuManager
|
||||
|
|
@ -154,257 +90,89 @@ func (a *Application) processPlatformSettings() error {
|
|||
|
||||
func (c *Client) updateApplicationMenu() {
|
||||
applicationMenu = c.app.menuManager.GetProcessedApplicationMenu()
|
||||
radioGroupCache = map[uint32]*radioGroupCacheEntry{}
|
||||
menuCache = map[uint32]menuCacheEntry{}
|
||||
createApplicationMenu(uintptr(C.GetWindowHandle(c.app.app)))
|
||||
}
|
||||
|
||||
func createMenu() (uintptr, error) {
|
||||
res, _, err := win32CreateMenu.Call()
|
||||
if res == 0 {
|
||||
return 0, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
/* ---------------------------------------------------------------------------------
|
||||
|
||||
func destroyMenu(menu uintptr) error {
|
||||
res, _, err := win32CreateMenu.Call(menu)
|
||||
if res == 0 {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func createPopupMenu() (uintptr, error) {
|
||||
res, _, err := win32CreatePopupMenu.Call()
|
||||
if res == 0 {
|
||||
return 0, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func appendMenuItem(menu uintptr, flags uintptr, id uintptr, label string) error {
|
||||
menuText, err := windows.UTF16PtrFromString(label)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res, _, err := win32AppendMenuW.Call(
|
||||
menu,
|
||||
flags,
|
||||
id,
|
||||
uintptr(unsafe.Pointer(menuText)),
|
||||
)
|
||||
if res == 0 {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
Radio Groups
|
||||
------------
|
||||
Radio groups are stored by the ProcessedMenu as a list of menu ids.
|
||||
Windows only cares about the start and end ids of the group so we
|
||||
preprocess the radio groups and store this data in a radioGroupCache.
|
||||
When a radio button is clicked, we use the menu id to read in the
|
||||
radio group data and call CheckMenuRadioItem to update the group.
|
||||
*/
|
||||
type radioGroupCacheEntry struct {
|
||||
startID uint32
|
||||
endID uint32
|
||||
}
|
||||
|
||||
var radioGroupCache = map[uint32]*radioGroupCacheEntry{}
|
||||
var radioGroupCacheLock sync.RWMutex
|
||||
|
||||
func addRadioGroupCacheEntry(id uint32, entry *radioGroupCacheEntry) {
|
||||
radioGroupCacheLock.Lock()
|
||||
defer radioGroupCacheLock.Unlock()
|
||||
radioGroupCache[id] = entry
|
||||
}
|
||||
|
||||
func getRadioGroupCacheEntry(id uint32) *radioGroupCacheEntry {
|
||||
radioGroupCacheLock.Lock()
|
||||
defer radioGroupCacheLock.Unlock()
|
||||
return radioGroupCache[id]
|
||||
}
|
||||
|
||||
func mustAtoi(input string) int {
|
||||
result, err := strconv.Atoi(input)
|
||||
if err != nil {
|
||||
log.Fatal("invalid string value for mustAtoi: %s", input)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/*
|
||||
Application Menu
|
||||
----------------
|
||||
There's only 1 application menu and this is where we create it. This method
|
||||
is called from C after the window is created and the WM_CREATE message has
|
||||
been sent.
|
||||
|
||||
*/
|
||||
|
||||
func checkFatal(err error) {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
//export createApplicationMenu
|
||||
func createApplicationMenu(hwnd uintptr) {
|
||||
|
||||
if applicationMenu == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Process Radio groups
|
||||
for _, rg := range applicationMenu.RadioGroups {
|
||||
startID := uint32(mustAtoi(rg.Members[0]))
|
||||
endID := uint32(mustAtoi(rg.Members[len(rg.Members)-1]))
|
||||
thisRG := &radioGroupCacheEntry{
|
||||
startID: startID,
|
||||
endID: endID,
|
||||
}
|
||||
// Set this for each member
|
||||
for _, member := range rg.Members {
|
||||
id := uint32(mustAtoi(member))
|
||||
addRadioGroupCacheEntry(id, thisRG)
|
||||
}
|
||||
if globalApplicationMenu != nil {
|
||||
checkFatal(globalApplicationMenu.Destroy())
|
||||
}
|
||||
|
||||
// Delete current menu if it exists
|
||||
var err error
|
||||
if menubar != 0 {
|
||||
err = destroyMenu(menubar)
|
||||
if err != nil {
|
||||
log.Fatal("destroyMenu:", err.Error())
|
||||
}
|
||||
}
|
||||
globalApplicationMenu, err = createMenu(applicationMenu, appMenuType)
|
||||
checkFatal(err)
|
||||
|
||||
// Create top level menu bar
|
||||
menubar, err = createMenu()
|
||||
if err != nil {
|
||||
log.Fatal("createMenu:", err.Error())
|
||||
}
|
||||
|
||||
// Process top level menus
|
||||
for _, toplevelmenu := range applicationMenu.Menu.Items {
|
||||
err = processMenuItem(menubar, toplevelmenu, appMenuType)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
res, _, err := win32SetMenu.Call(hwnd, menubar)
|
||||
if res == 0 {
|
||||
log.Fatal("setmenu", err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustSelectRadioItem(id uint32, parent uintptr) {
|
||||
rg := getRadioGroupCacheEntry(id)
|
||||
res, _, err := win32CheckMenuRadioItem.Call(parent, uintptr(rg.startID), uintptr(rg.endID), uintptr(id), uintptr(MF_BYCOMMAND))
|
||||
if int(res) == 0 {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = setWindowMenu(win32Window(hwnd), globalApplicationMenu.menu)
|
||||
checkFatal(err)
|
||||
}
|
||||
|
||||
/*
|
||||
This method is called by C when a menu item is pressed
|
||||
*/
|
||||
|
||||
//export menuClicked
|
||||
func menuClicked(id uint32) {
|
||||
win32MenuID := win32MenuItemID(id)
|
||||
//println("Got click from menu id", win32MenuID)
|
||||
|
||||
// Get the menu from the cache
|
||||
menuitem := getMenuCacheEntry(id)
|
||||
menuItemDetails := getMenuCacheEntry(win32MenuID)
|
||||
wailsMenuID := wailsMenuItemID(menuItemDetails.item.ID)
|
||||
|
||||
switch menuitem.item.Type {
|
||||
//println("Got click from menu id", win32MenuID, "- wails menu ID", wailsMenuID)
|
||||
//spew.Dump(menuItemDetails)
|
||||
|
||||
switch menuItemDetails.item.Type {
|
||||
case menu.CheckboxType:
|
||||
res, _, err := win32GetMenuState.Call(menuitem.parent, uintptr(id), uintptr(MF_BYCOMMAND))
|
||||
|
||||
// Determine if the menu is set or not
|
||||
res, _, err := win32GetMenuState.Call(uintptr(menuItemDetails.parent), uintptr(id), uintptr(MF_BYCOMMAND))
|
||||
if int(res) == -1 {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
flag := MF_CHECKED
|
||||
if uint32(res) == MF_CHECKED {
|
||||
res, _, err = win32CheckMenuItem.Call(menuitem.parent, uintptr(id), uintptr(MF_UNCHECKED))
|
||||
} else {
|
||||
res, _, err = win32CheckMenuItem.Call(menuitem.parent, uintptr(id), uintptr(MF_CHECKED))
|
||||
flag = MF_UNCHECKED
|
||||
}
|
||||
if int(res) == -1 {
|
||||
log.Fatal(err)
|
||||
|
||||
for _, menuid := range globalCheckboxCache.win32MenuIDsForWailsMenuID(wailsMenuID) {
|
||||
//println("setting menuid", menuid, "with flag", flag)
|
||||
menuItemDetails := getMenuCacheEntry(menuid)
|
||||
res, _, err = win32CheckMenuItem.Call(uintptr(menuItemDetails.parent), uintptr(menuid), uintptr(flag))
|
||||
if int(res) == -1 {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
case menu.RadioType:
|
||||
mustSelectRadioItem(id, menuitem.parent)
|
||||
selectRadioItemFromWailsMenuID(wailsMenuID, win32MenuID)
|
||||
}
|
||||
|
||||
// Print the click error - it's not fatal
|
||||
err := menuManager.ProcessClick(menuitem.item.ID, "", "ApplicationMenu", "")
|
||||
err := menuManager.ProcessClick(menuItemDetails.item.ID, "", string(menuItemDetails.menuType), "")
|
||||
if err != nil {
|
||||
println(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
var flagMap = map[menu.Type]uint32{
|
||||
menu.TextType: MF_STRING,
|
||||
menu.SeparatorType: MF_SEPARATOR,
|
||||
menu.SubmenuType: MF_STRING | MF_POPUP,
|
||||
menu.CheckboxType: MF_STRING,
|
||||
menu.RadioType: MF_STRING,
|
||||
}
|
||||
|
||||
func calculateFlags(menuItem *menumanager.ProcessedMenuItem) uint32 {
|
||||
result := flagMap[menuItem.Type]
|
||||
|
||||
if menuItem.Disabled {
|
||||
result |= MF_DISABLED
|
||||
}
|
||||
|
||||
if menuItem.Type == menu.CheckboxType && menuItem.Checked {
|
||||
result |= MF_CHECKED
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func processMenuItem(parent uintptr, menuItem *menumanager.ProcessedMenuItem, menuType menuType) error {
|
||||
|
||||
// Ignore hidden items
|
||||
if menuItem.Hidden {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Calculate the flags for this menu item
|
||||
flags := uintptr(calculateFlags(menuItem))
|
||||
|
||||
switch menuItem.Type {
|
||||
case menu.SubmenuType:
|
||||
submenu, err := createPopupMenu()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, submenuItem := range menuItem.SubMenu.Items {
|
||||
err = processMenuItem(submenu, submenuItem, menuType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
err = appendMenuItem(parent, flags, submenu, menuItem.Label)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case menu.TextType, menu.CheckboxType, menu.RadioType:
|
||||
ID := uint32(mustAtoi(menuItem.ID))
|
||||
err := appendMenuItem(parent, flags, uintptr(ID), menuItem.Label)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
menuCacheItem := menuCacheEntry{
|
||||
parent: parent,
|
||||
menuType: menuType,
|
||||
item: menuItem,
|
||||
}
|
||||
addMenuCacheEntry(ID, menuCacheItem)
|
||||
if menuItem.Type == menu.RadioType && menuItem.Checked {
|
||||
mustSelectRadioItem(ID, parent)
|
||||
}
|
||||
case menu.SeparatorType:
|
||||
err := appendMenuItem(parent, flags, 0, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
69
v2/internal/ffenestri/windows_checkboxes.go
Normal file
69
v2/internal/ffenestri/windows_checkboxes.go
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
//+build windows
|
||||
|
||||
package ffenestri
|
||||
|
||||
import (
|
||||
"github.com/wailsapp/wails/v2/internal/menumanager"
|
||||
"sync"
|
||||
)
|
||||
|
||||
/* ---------------------------------------------------------------------------------
|
||||
|
||||
Checkbox Cache
|
||||
--------------
|
||||
The checkbox cache keeps a list of IDs that are associated with the same checkbox menu item.
|
||||
This can happen when a checkbox is used in an application menu and a tray menu, eg "start at login".
|
||||
The cache is used to bulk toggle the menu items when one is clicked.
|
||||
|
||||
*/
|
||||
|
||||
type CheckboxCache struct {
|
||||
cache map[*menumanager.ProcessedMenu]map[wailsMenuItemID][]win32MenuItemID
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
func NewCheckboxCache() *CheckboxCache {
|
||||
return &CheckboxCache{
|
||||
cache: make(map[*menumanager.ProcessedMenu]map[wailsMenuItemID][]win32MenuItemID),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CheckboxCache) addToCheckboxCache(menu *menumanager.ProcessedMenu, item wailsMenuItemID, menuID win32MenuItemID) {
|
||||
|
||||
// Get map for menu
|
||||
if c.cache[menu] == nil {
|
||||
c.cache[menu] = make(map[wailsMenuItemID][]win32MenuItemID)
|
||||
}
|
||||
menuMap := c.cache[menu]
|
||||
|
||||
// Ensure we have a slice
|
||||
if menuMap[item] == nil {
|
||||
menuMap[item] = []win32MenuItemID{}
|
||||
}
|
||||
|
||||
c.mutex.Lock()
|
||||
menuMap[item] = append(menuMap[item], menuID)
|
||||
c.mutex.Unlock()
|
||||
|
||||
}
|
||||
|
||||
func (c *CheckboxCache) removeMenuFromChechboxCache(menu *menumanager.ProcessedMenu) {
|
||||
c.mutex.Lock()
|
||||
delete(c.cache, menu)
|
||||
c.mutex.Unlock()
|
||||
}
|
||||
|
||||
// win32MenuIDsForWailsMenuID returns all win32menuids that are used for a wails menu item id across
|
||||
// all menus
|
||||
func (c *CheckboxCache) win32MenuIDsForWailsMenuID(item wailsMenuItemID) []win32MenuItemID {
|
||||
c.mutex.Lock()
|
||||
result := []win32MenuItemID{}
|
||||
for _, menu := range c.cache {
|
||||
ids := menu[item]
|
||||
if ids != nil {
|
||||
result = append(result, ids...)
|
||||
}
|
||||
}
|
||||
c.mutex.Unlock()
|
||||
return result
|
||||
}
|
||||
183
v2/internal/ffenestri/windows_menu.go
Normal file
183
v2/internal/ffenestri/windows_menu.go
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
//+build windows
|
||||
|
||||
package ffenestri
|
||||
|
||||
import (
|
||||
"github.com/wailsapp/wails/v2/internal/menumanager"
|
||||
"github.com/wailsapp/wails/v2/pkg/menu"
|
||||
)
|
||||
|
||||
//-------------------- Types ------------------------
|
||||
|
||||
type win32MenuItemID uint32
|
||||
type win32Menu uintptr
|
||||
type win32Window uintptr
|
||||
type wailsMenuItemID string // The internal menu ID
|
||||
|
||||
type Menu struct {
|
||||
wailsMenu *menumanager.WailsMenu
|
||||
menu win32Menu
|
||||
menuType menuType
|
||||
|
||||
// A list of all checkbox and radio menuitems we
|
||||
// create for this menu
|
||||
checkboxes []win32MenuItemID
|
||||
radioboxes []win32MenuItemID
|
||||
initiallySelectedRadioItems []win32MenuItemID
|
||||
}
|
||||
|
||||
func createMenu(wailsMenu *menumanager.WailsMenu, menuType menuType) (*Menu, error) {
|
||||
|
||||
mainMenu, err := createWin32Menu()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &Menu{
|
||||
wailsMenu: wailsMenu,
|
||||
menu: mainMenu,
|
||||
menuType: menuType,
|
||||
}
|
||||
|
||||
// Process top level menus
|
||||
for _, toplevelmenu := range applicationMenu.Menu.Items {
|
||||
err := result.processMenuItem(result.menu, toplevelmenu)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
err = result.processRadioGroups()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (m *Menu) processMenuItem(parent win32Menu, menuItem *menumanager.ProcessedMenuItem) error {
|
||||
|
||||
// Ignore hidden items
|
||||
if menuItem.Hidden {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Calculate the flags for this menu item
|
||||
flags := uintptr(calculateFlags(menuItem))
|
||||
|
||||
switch menuItem.Type {
|
||||
case menu.SubmenuType:
|
||||
submenu, err := createWin32PopupMenu()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, submenuItem := range menuItem.SubMenu.Items {
|
||||
err = m.processMenuItem(submenu, submenuItem)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
err = appendWin32MenuItem(parent, flags, uintptr(submenu), menuItem.Label)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case menu.TextType, menu.CheckboxType, menu.RadioType:
|
||||
win32ID := addMenuCacheEntry(parent, m.menuType, menuItem, m.wailsMenu.Menu)
|
||||
//label := fmt.Sprintf("%s (%d)", menuItem.Label, win32ID)
|
||||
label := menuItem.Label
|
||||
err := appendWin32MenuItem(parent, flags, uintptr(win32ID), label)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if menuItem.Type == menu.CheckboxType {
|
||||
// We need to maintain a list of this menu's checkboxes
|
||||
m.checkboxes = append(m.checkboxes, win32ID)
|
||||
globalCheckboxCache.addToCheckboxCache(m.wailsMenu.Menu, wailsMenuItemID(menuItem.ID), win32ID)
|
||||
}
|
||||
if menuItem.Type == menu.RadioType {
|
||||
// We need to maintain a list of this menu's radioitems
|
||||
m.radioboxes = append(m.radioboxes, win32ID)
|
||||
globalRadioGroupMap.addRadioGroupMapping(m.wailsMenu.Menu, wailsMenuItemID(menuItem.ID), win32ID)
|
||||
if menuItem.Checked {
|
||||
m.initiallySelectedRadioItems = append(m.initiallySelectedRadioItems, win32ID)
|
||||
}
|
||||
}
|
||||
case menu.SeparatorType:
|
||||
err := appendWin32MenuItem(parent, flags, 0, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Menu) processRadioGroups() error {
|
||||
|
||||
for _, rg := range applicationMenu.RadioGroups {
|
||||
startWailsMenuID := wailsMenuItemID(rg.Members[0])
|
||||
endWailsMenuID := wailsMenuItemID(rg.Members[len(rg.Members)-1])
|
||||
|
||||
startIDs := globalRadioGroupMap.getRadioGroupMapping(startWailsMenuID)
|
||||
endIDs := globalRadioGroupMap.getRadioGroupMapping(endWailsMenuID)
|
||||
|
||||
var radioGroupMaps = []*radioGroupStartEnd{}
|
||||
for index := range startIDs {
|
||||
startID := startIDs[index]
|
||||
endID := endIDs[index]
|
||||
thisRadioGroup := &radioGroupStartEnd{
|
||||
startID: startID,
|
||||
endID: endID,
|
||||
}
|
||||
radioGroupMaps = append(radioGroupMaps, thisRadioGroup)
|
||||
}
|
||||
|
||||
// Set this for each member
|
||||
for _, member := range rg.Members {
|
||||
id := wailsMenuItemID(member)
|
||||
globalRadioGroupCache.addToRadioGroupCache(m.wailsMenu.Menu, id, radioGroupMaps)
|
||||
}
|
||||
}
|
||||
|
||||
// Enable all initially checked radio items
|
||||
for _, win32MenuID := range m.initiallySelectedRadioItems {
|
||||
menuItemDetails := getMenuCacheEntry(win32MenuID)
|
||||
wailsMenuID := wailsMenuItemID(menuItemDetails.item.ID)
|
||||
selectRadioItemFromWailsMenuID(wailsMenuID, win32MenuID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Menu) Destroy() error {
|
||||
|
||||
// Unload this menu's checkboxes from the cache
|
||||
globalCheckboxCache.removeMenuFromChechboxCache(m.wailsMenu.Menu)
|
||||
|
||||
// Unload this menu's radio groups from the cache
|
||||
globalRadioGroupCache.removeMenuFromRadioBoxCache(m.wailsMenu.Menu)
|
||||
|
||||
// Delete menu
|
||||
return destroyWin32Menu(m.menu)
|
||||
}
|
||||
|
||||
var flagMap = map[menu.Type]uint32{
|
||||
menu.TextType: MF_STRING,
|
||||
menu.SeparatorType: MF_SEPARATOR,
|
||||
menu.SubmenuType: MF_STRING | MF_POPUP,
|
||||
menu.CheckboxType: MF_STRING,
|
||||
menu.RadioType: MF_STRING,
|
||||
}
|
||||
|
||||
func calculateFlags(menuItem *menumanager.ProcessedMenuItem) uint32 {
|
||||
result := flagMap[menuItem.Type]
|
||||
|
||||
if menuItem.Disabled {
|
||||
result |= MF_DISABLED
|
||||
}
|
||||
|
||||
if menuItem.Type == menu.CheckboxType && menuItem.Checked {
|
||||
result |= MF_CHECKED
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
61
v2/internal/ffenestri/windows_menu_cache.go
Normal file
61
v2/internal/ffenestri/windows_menu_cache.go
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
//+build windows
|
||||
|
||||
package ffenestri
|
||||
|
||||
import (
|
||||
"github.com/wailsapp/wails/v2/internal/menumanager"
|
||||
"sync"
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
MenuCache
|
||||
---------
|
||||
When windows calls back to Go (when an item is clicked), we need to
|
||||
be able to retrieve information about the menu item:
|
||||
- The menu that the menuitem is part of (parent)
|
||||
- The original processed menu item
|
||||
- The type of the menu (application, context or tray)
|
||||
|
||||
This cache is built up when a menu is created.
|
||||
|
||||
*/
|
||||
|
||||
// TODO: Make this like the other caches
|
||||
|
||||
type menuCacheEntry struct {
|
||||
parent win32Menu
|
||||
menuType menuType
|
||||
item *menumanager.ProcessedMenuItem
|
||||
processedMenu *menumanager.ProcessedMenu
|
||||
}
|
||||
|
||||
// windowsMenuIDCounter keeps track of the unique windows menu IDs
|
||||
var windowsMenuIDCounter uint32
|
||||
|
||||
var menuCache = map[win32MenuItemID]*menuCacheEntry{}
|
||||
var menuCacheLock sync.RWMutex
|
||||
var wailsMenuIDtoWin32IDMap = map[wailsMenuItemID]win32MenuItemID{}
|
||||
|
||||
func addMenuCacheEntry(parent win32Menu, typ menuType, wailsMenuItem *menumanager.ProcessedMenuItem, processedMenu *menumanager.ProcessedMenu) win32MenuItemID {
|
||||
menuCacheLock.Lock()
|
||||
defer menuCacheLock.Unlock()
|
||||
menuID := win32MenuItemID(windowsMenuIDCounter)
|
||||
windowsMenuIDCounter++
|
||||
menuCache[menuID] = &menuCacheEntry{
|
||||
parent: parent,
|
||||
menuType: typ,
|
||||
item: wailsMenuItem,
|
||||
processedMenu: processedMenu,
|
||||
}
|
||||
// save the mapping
|
||||
wailsMenuIDtoWin32IDMap[wailsMenuItemID(wailsMenuItem.ID)] = menuID
|
||||
return menuID
|
||||
|
||||
}
|
||||
|
||||
func getMenuCacheEntry(id win32MenuItemID) *menuCacheEntry {
|
||||
menuCacheLock.Lock()
|
||||
defer menuCacheLock.Unlock()
|
||||
return menuCache[id]
|
||||
}
|
||||
137
v2/internal/ffenestri/windows_radiogroup.go
Normal file
137
v2/internal/ffenestri/windows_radiogroup.go
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
//+build windows
|
||||
|
||||
package ffenestri
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/menumanager"
|
||||
)
|
||||
|
||||
/* ---------------------------------------------------------------------------------
|
||||
|
||||
Radio Groups
|
||||
------------
|
||||
Radio groups are stored by the ProcessedMenu as a list of menu ids.
|
||||
Windows only cares about the start and end ids of the group so we
|
||||
preprocess the radio groups and store this data in a radioGroupMap.
|
||||
When a radio button is clicked, we use the menu id to read in the
|
||||
radio group data and call CheckMenuRadioItem to update the group.
|
||||
|
||||
*/
|
||||
|
||||
type radioGroupStartEnd struct {
|
||||
startID win32MenuItemID
|
||||
endID win32MenuItemID
|
||||
}
|
||||
|
||||
type RadioGroupCache struct {
|
||||
cache map[*menumanager.ProcessedMenu]map[wailsMenuItemID][]*radioGroupStartEnd
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
func NewRadioGroupCache() *RadioGroupCache {
|
||||
return &RadioGroupCache{
|
||||
cache: make(map[*menumanager.ProcessedMenu]map[wailsMenuItemID][]*radioGroupStartEnd),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *RadioGroupCache) addToRadioGroupCache(menu *menumanager.ProcessedMenu, item wailsMenuItemID, radioGroupMaps []*radioGroupStartEnd) {
|
||||
|
||||
c.mutex.Lock()
|
||||
|
||||
// Get map for menu
|
||||
if c.cache[menu] == nil {
|
||||
c.cache[menu] = make(map[wailsMenuItemID][]*radioGroupStartEnd)
|
||||
}
|
||||
menuMap := c.cache[menu]
|
||||
|
||||
// Ensure we have a slice
|
||||
if menuMap[item] == nil {
|
||||
menuMap[item] = []*radioGroupStartEnd{}
|
||||
}
|
||||
|
||||
menuMap[item] = radioGroupMaps
|
||||
|
||||
c.mutex.Unlock()
|
||||
|
||||
}
|
||||
|
||||
func (c *RadioGroupCache) removeMenuFromRadioBoxCache(menu *menumanager.ProcessedMenu) {
|
||||
c.mutex.Lock()
|
||||
delete(c.cache, menu)
|
||||
c.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (c *RadioGroupCache) getRadioGroupMappings(wailsMenuID wailsMenuItemID) []*radioGroupStartEnd {
|
||||
c.mutex.Lock()
|
||||
result := []*radioGroupStartEnd{}
|
||||
for _, menugroups := range c.cache {
|
||||
groups := menugroups[wailsMenuID]
|
||||
if groups != nil {
|
||||
result = append(result, groups...)
|
||||
}
|
||||
}
|
||||
c.mutex.Unlock()
|
||||
return result
|
||||
}
|
||||
|
||||
type RadioGroupMap struct {
|
||||
cache map[*menumanager.ProcessedMenu]map[wailsMenuItemID][]win32MenuItemID
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
func NewRadioGroupMap() *RadioGroupMap {
|
||||
return &RadioGroupMap{
|
||||
cache: make(map[*menumanager.ProcessedMenu]map[wailsMenuItemID][]win32MenuItemID),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *RadioGroupMap) addRadioGroupMapping(menu *menumanager.ProcessedMenu, item wailsMenuItemID, win32ID win32MenuItemID) {
|
||||
m.mutex.Lock()
|
||||
|
||||
// Get map for menu
|
||||
if m.cache[menu] == nil {
|
||||
m.cache[menu] = make(map[wailsMenuItemID][]win32MenuItemID)
|
||||
}
|
||||
menuMap := m.cache[menu]
|
||||
|
||||
// Ensure we have a slice
|
||||
if menuMap[item] == nil {
|
||||
menuMap[item] = []win32MenuItemID{}
|
||||
}
|
||||
|
||||
menuMap[item] = append(menuMap[item], win32ID)
|
||||
|
||||
m.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (m *RadioGroupMap) getRadioGroupMapping(wailsMenuID wailsMenuItemID) []win32MenuItemID {
|
||||
m.mutex.Lock()
|
||||
result := []win32MenuItemID{}
|
||||
for _, menuids := range m.cache {
|
||||
ids := menuids[wailsMenuID]
|
||||
if ids != nil {
|
||||
result = append(result, ids...)
|
||||
}
|
||||
}
|
||||
m.mutex.Unlock()
|
||||
return result
|
||||
}
|
||||
|
||||
func selectRadioItemFromWailsMenuID(wailsMenuID wailsMenuItemID, win32MenuID win32MenuItemID) {
|
||||
radioItemGroups := globalRadioGroupCache.getRadioGroupMappings(wailsMenuID)
|
||||
// Figure out offset into group
|
||||
var offset win32MenuItemID = 0
|
||||
for _, radioItemGroup := range radioItemGroups {
|
||||
if win32MenuID >= radioItemGroup.startID && win32MenuID <= radioItemGroup.endID {
|
||||
offset = win32MenuID - radioItemGroup.startID
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, radioItemGroup := range radioItemGroups {
|
||||
selectedMenuID := radioItemGroup.startID + offset
|
||||
menuItemDetails := getMenuCacheEntry(selectedMenuID)
|
||||
selectRadioItem(selectedMenuID, radioItemGroup.startID, radioItemGroup.endID, menuItemDetails.parent)
|
||||
}
|
||||
}
|
||||
99
v2/internal/ffenestri/windows_win32.go
Normal file
99
v2/internal/ffenestri/windows_win32.go
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
//+build windows
|
||||
|
||||
package ffenestri
|
||||
|
||||
import (
|
||||
"log"
|
||||
"unsafe"
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/menumanager"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var (
|
||||
// DLL stuff
|
||||
user32 = windows.NewLazySystemDLL("User32.dll")
|
||||
win32CreateMenu = user32.NewProc("CreateMenu")
|
||||
win32DestroyMenu = user32.NewProc("DestroyMenu")
|
||||
win32CreatePopupMenu = user32.NewProc("CreatePopupMenu")
|
||||
win32AppendMenuW = user32.NewProc("AppendMenuW")
|
||||
win32SetMenu = user32.NewProc("SetMenu")
|
||||
win32CheckMenuItem = user32.NewProc("CheckMenuItem")
|
||||
win32GetMenuState = user32.NewProc("GetMenuState")
|
||||
win32CheckMenuRadioItem = user32.NewProc("CheckMenuRadioItem")
|
||||
applicationMenu *menumanager.WailsMenu
|
||||
menuManager *menumanager.Manager
|
||||
)
|
||||
|
||||
const MF_BITMAP uint32 = 0x00000004
|
||||
const MF_CHECKED uint32 = 0x00000008
|
||||
const MF_DISABLED uint32 = 0x00000002
|
||||
const MF_ENABLED uint32 = 0x00000000
|
||||
const MF_GRAYED uint32 = 0x00000001
|
||||
const MF_MENUBARBREAK uint32 = 0x00000020
|
||||
const MF_MENUBREAK uint32 = 0x00000040
|
||||
const MF_OWNERDRAW uint32 = 0x00000100
|
||||
const MF_POPUP uint32 = 0x00000010
|
||||
const MF_SEPARATOR uint32 = 0x00000800
|
||||
const MF_STRING uint32 = 0x00000000
|
||||
const MF_UNCHECKED uint32 = 0x00000000
|
||||
const MF_BYCOMMAND uint32 = 0x00000000
|
||||
const MF_BYPOSITION uint32 = 0x00000400
|
||||
|
||||
// ------------------- win32 calls -----------------------
|
||||
|
||||
func createWin32Menu() (win32Menu, error) {
|
||||
res, _, err := win32CreateMenu.Call()
|
||||
if res == 0 {
|
||||
return 0, err
|
||||
}
|
||||
return win32Menu(res), nil
|
||||
}
|
||||
|
||||
func destroyWin32Menu(menu win32Menu) error {
|
||||
res, _, err := win32CreateMenu.Call(uintptr(menu))
|
||||
if res == 0 {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func createWin32PopupMenu() (win32Menu, error) {
|
||||
res, _, err := win32CreatePopupMenu.Call()
|
||||
if res == 0 {
|
||||
return 0, err
|
||||
}
|
||||
return win32Menu(res), nil
|
||||
}
|
||||
|
||||
func appendWin32MenuItem(menu win32Menu, flags uintptr, submenuOrID uintptr, label string) error {
|
||||
menuText, err := windows.UTF16PtrFromString(label)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res, _, err := win32AppendMenuW.Call(
|
||||
uintptr(menu),
|
||||
flags,
|
||||
submenuOrID,
|
||||
uintptr(unsafe.Pointer(menuText)),
|
||||
)
|
||||
if res == 0 {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setWindowMenu(window win32Window, menu win32Menu) error {
|
||||
res, _, err := win32SetMenu.Call(uintptr(window), uintptr(menu))
|
||||
if res == 0 {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func selectRadioItem(selectedMenuID, startMenuItemID, endMenuItemID win32MenuItemID, parent win32Menu) {
|
||||
res, _, err := win32CheckMenuRadioItem.Call(uintptr(parent), uintptr(startMenuItemID), uintptr(endMenuItemID), uintptr(selectedMenuID), uintptr(MF_BYCOMMAND))
|
||||
if int(res) == 0 {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue