From 76aab2271cfa534526d93bbebf2d2fb2aa61252f Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Thu, 8 Jul 2021 23:34:56 +1000 Subject: [PATCH] [windows] Menu checkboxes and radio groups now fully in sync when using Go menus mutltiple times. --- v2/internal/ffenestri/ffenestri_windows.go | 330 +++----------------- v2/internal/ffenestri/windows_checkboxes.go | 69 ++++ v2/internal/ffenestri/windows_menu.go | 183 +++++++++++ v2/internal/ffenestri/windows_menu_cache.go | 61 ++++ v2/internal/ffenestri/windows_radiogroup.go | 137 ++++++++ v2/internal/ffenestri/windows_win32.go | 99 ++++++ 6 files changed, 598 insertions(+), 281 deletions(-) create mode 100644 v2/internal/ffenestri/windows_checkboxes.go create mode 100644 v2/internal/ffenestri/windows_menu.go create mode 100644 v2/internal/ffenestri/windows_menu_cache.go create mode 100644 v2/internal/ffenestri/windows_radiogroup.go create mode 100644 v2/internal/ffenestri/windows_win32.go diff --git a/v2/internal/ffenestri/ffenestri_windows.go b/v2/internal/ffenestri/ffenestri_windows.go index df08f3b7b..36ae93b83 100644 --- a/v2/internal/ffenestri/ffenestri_windows.go +++ b/v2/internal/ffenestri/ffenestri_windows.go @@ -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 -} diff --git a/v2/internal/ffenestri/windows_checkboxes.go b/v2/internal/ffenestri/windows_checkboxes.go new file mode 100644 index 000000000..92d28a8ac --- /dev/null +++ b/v2/internal/ffenestri/windows_checkboxes.go @@ -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 +} diff --git a/v2/internal/ffenestri/windows_menu.go b/v2/internal/ffenestri/windows_menu.go new file mode 100644 index 000000000..97d5dbeae --- /dev/null +++ b/v2/internal/ffenestri/windows_menu.go @@ -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 +} diff --git a/v2/internal/ffenestri/windows_menu_cache.go b/v2/internal/ffenestri/windows_menu_cache.go new file mode 100644 index 000000000..e139c0237 --- /dev/null +++ b/v2/internal/ffenestri/windows_menu_cache.go @@ -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] +} diff --git a/v2/internal/ffenestri/windows_radiogroup.go b/v2/internal/ffenestri/windows_radiogroup.go new file mode 100644 index 000000000..b5cce8eef --- /dev/null +++ b/v2/internal/ffenestri/windows_radiogroup.go @@ -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) + } +} diff --git a/v2/internal/ffenestri/windows_win32.go b/v2/internal/ffenestri/windows_win32.go new file mode 100644 index 000000000..b3cd18068 --- /dev/null +++ b/v2/internal/ffenestri/windows_win32.go @@ -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) + } +}