diff --git a/v2/internal/ffenestri/ffenestri_windows.cpp b/v2/internal/ffenestri/ffenestri_windows.cpp index e45baa3d..8c8a0418 100644 --- a/v2/internal/ffenestri/ffenestri_windows.cpp +++ b/v2/internal/ffenestri/ffenestri_windows.cpp @@ -142,6 +142,14 @@ LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch(msg) { + case WM_CREATE: { + createApplicationMenu(hwnd); + break; + } + case WM_COMMAND: + menuClicked(LOWORD(wParam)); + break; + case WM_CLOSE: { DestroyWindow( app->window ); break; diff --git a/v2/internal/ffenestri/ffenestri_windows.go b/v2/internal/ffenestri/ffenestri_windows.go index eceadd70..ec0275f5 100644 --- a/v2/internal/ffenestri/ffenestri_windows.go +++ b/v2/internal/ffenestri/ffenestri_windows.go @@ -13,9 +13,73 @@ 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" + "unsafe" +) + +var ( + // DLL stuff + user32 = windows.NewLazySystemDLL("User32.dll") + win32CreateMenu = user32.NewProc("CreateMenu") + win32CreatePopupMenu = user32.NewProc("CreatePopupMenu") + win32AppendMenuW = user32.NewProc("AppendMenuW") + win32SetMenu = user32.NewProc("SetMenu") + win32CheckMenuItem = user32.NewProc("CheckMenuItem") + win32GetMenuState = user32.NewProc("GetMenuState") + 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 + +type menuType int + +// Credit: https://github.com/getlantern/systray/blob/2c0986dda9aea361e925f90e848d9036be7b5367/systray_windows.go +type menuItemInfo struct { + Size, Mask, Type, State uint32 + ID uint32 + SubMenu, Checked, Unchecked windows.Handle + ItemData uintptr + TypeData *uint16 + Cch uint32 + BMPItem windows.Handle +} + +const ( + appMenuType menuType = iota + contextMenuType + trayMenuType +) + +type menuCacheEntry struct { + parent uintptr + menuType menuType + index int + item *menumanager.ProcessedMenuItem +} + +var menuCache = []menuCacheEntry{} func (a *Application) processPlatformSettings() error { + menuManager = a.menuManager config := a.config.Windows if config == nil { return nil @@ -34,13 +98,9 @@ func (a *Application) processPlatformSettings() error { C.DisableWindowIcon(a.app) } - // Process menu - //applicationMenu := options.GetApplicationMenu(a.config) - applicationMenu := a.menuManager.GetApplicationMenuJSON() - println("Appmenu =", applicationMenu) - if applicationMenu != "" { - C.SetApplicationMenu(a.app, a.string2CString(applicationMenu)) - } + // Unfortunately, we need to store this in the package variable so the C callback can see it + applicationMenu = a.menuManager.GetProcessedApplicationMenu() + // //// Process tray //trays, err := a.menuManager.GetTrayMenus() @@ -71,3 +131,163 @@ func (a *Application) processPlatformSettings() error { return nil } + +func createMenu() (uintptr, error) { + res, _, err := win32CreateMenu.Call() + if res == 0 { + return 0, err + } + return res, 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 +} + +//export createApplicationMenu +func createApplicationMenu(hwnd uintptr) { + if applicationMenu == nil { + return + } + + // Create top level menu bar + menubar, err := createMenu() + if err != nil { + log.Fatal("createMenu:", err.Error()) + } + + // Process top level menus + for index, toplevelmenu := range applicationMenu.Menu.Items { + err = processMenuItem(menubar, toplevelmenu, appMenuType, index) + if err != nil { + log.Fatal(err) + } + } + + res, _, err := win32SetMenu.Call(hwnd, menubar) + if res == 0 { + log.Fatal("setmenu", err) + } +} + +//export menuClicked +func menuClicked(id uint32) { + + // Get the menu from the cache + menuitem := menuCache[id] + + if menuitem.item.Type == menu.CheckboxType { + + res, _, err := win32GetMenuState.Call(menuitem.parent, uintptr(id), uintptr(MF_BYCOMMAND)) + if int(res) == -1 { + log.Fatal(err) + } + + 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)) + } + if int(res) == -1 { + log.Fatal(err) + } + } + + // Print the click error - it's not fatal + err := menuManager.ProcessClick(menuitem.item.ID, "", "ApplicationMenu", "") + 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, +} + +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, index int) error { + + // Ignore hidden items + if menuItem.Hidden { + return nil + } + + // Add menuitem to cache + ID := len(menuCache) + + // 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 index, submenuItem := range menuItem.SubMenu.Items { + err = processMenuItem(submenu, submenuItem, menuType, index) + if err != nil { + return err + } + } + err = appendMenuItem(parent, flags, submenu, menuItem.Label) + if err != nil { + return err + } + case menu.TextType, menu.CheckboxType: + err := appendMenuItem(parent, flags, uintptr(ID), menuItem.Label) + if err != nil { + return err + } + menuCacheItem := menuCacheEntry{ + parent: parent, + menuType: menuType, + index: index, + item: menuItem, + } + menuCache = append(menuCache, menuCacheItem) + case menu.SeparatorType: + err := appendMenuItem(parent, flags, 0, "") + if err != nil { + return err + } + } + return nil +} diff --git a/v2/internal/ffenestri/ffenestri_windows.h b/v2/internal/ffenestri/ffenestri_windows.h index 5147b1f0..35d36feb 100644 --- a/v2/internal/ffenestri/ffenestri_windows.h +++ b/v2/internal/ffenestri/ffenestri_windows.h @@ -80,6 +80,8 @@ extern "C" { void DisableWindowIcon(struct Application* app); void messageFromWindowCallback(const char *); void* GetWindowHandle(struct Application*); + void createApplicationMenu(HWND hwnd); + void menuClicked(UINT id); } #endif \ No newline at end of file diff --git a/v2/internal/menumanager/applicationmenu.go b/v2/internal/menumanager/applicationmenu.go index 3ebb8f93..3e6ba83e 100644 --- a/v2/internal/menumanager/applicationmenu.go +++ b/v2/internal/menumanager/applicationmenu.go @@ -23,6 +23,10 @@ func (m *Manager) GetApplicationMenuJSON() string { return m.applicationMenuJSON } +func (m *Manager) GetProcessedApplicationMenu() *WailsMenu { + return m.processedApplicationMenu +} + // UpdateApplicationMenu reprocesses the application menu to pick up structure // changes etc // Returns the JSON representation of the updated menu @@ -36,8 +40,8 @@ func (m *Manager) UpdateApplicationMenu() (string, error) { func (m *Manager) processApplicationMenu() error { // Process the menu - processedApplicationMenu := NewWailsMenu(m.applicationMenuItemMap, m.applicationMenu) - applicationMenuJSON, err := processedApplicationMenu.AsJSON() + m.processedApplicationMenu = NewWailsMenu(m.applicationMenuItemMap, m.applicationMenu) + applicationMenuJSON, err := m.processedApplicationMenu.AsJSON() if err != nil { return err } diff --git a/v2/internal/menumanager/menumanager.go b/v2/internal/menumanager/menumanager.go index 862143bb..76fe9b49 100644 --- a/v2/internal/menumanager/menumanager.go +++ b/v2/internal/menumanager/menumanager.go @@ -8,8 +8,9 @@ import ( type Manager struct { // The application menu. - applicationMenu *menu.Menu - applicationMenuJSON string + applicationMenu *menu.Menu + applicationMenuJSON string + processedApplicationMenu *WailsMenu // Our application menu mappings applicationMenuItemMap *MenuItemMap