diff --git a/v3/examples/menu/main.go b/v3/examples/menu/main.go index b98393a8..d049525e 100644 --- a/v3/examples/menu/main.go +++ b/v3/examples/menu/main.go @@ -3,6 +3,7 @@ package main import ( _ "embed" "log" + "runtime" "github.com/wailsapp/wails/v3/pkg/application" ) @@ -19,7 +20,9 @@ func main() { // Create a custom menu menu := app.NewMenu() - menu.AddRole(application.AppMenu) + if runtime.GOOS == "darwin" { + menu.AddRole(application.AppMenu) + } // Let's make a "Demo" menu myMenu := menu.AddSubmenu("Demo") @@ -89,11 +92,11 @@ func main() { }) myMenu.Add("Hide the beatles").OnClick(func(ctx *application.Context) { if beatles.Hidden() { - ctx.ClickedMenuItem().SetLabel("Unhide the beatles!") + ctx.ClickedMenuItem().SetLabel("Hide the beatles!") beatles.SetHidden(false) } else { beatles.SetHidden(true) - ctx.ClickedMenuItem().SetLabel("Hide the beatles!") + ctx.ClickedMenuItem().SetLabel("Unhide the beatles!") } }) app.SetMenu(menu) diff --git a/v3/pkg/application/application_windows.go b/v3/pkg/application/application_windows.go index 37598fb9..429b230b 100644 --- a/v3/pkg/application/application_windows.go +++ b/v3/pkg/application/application_windows.go @@ -7,6 +7,7 @@ import ( "golang.org/x/sys/windows" "os" "strconv" + "sync" "syscall" "unsafe" @@ -23,8 +24,9 @@ type windowsApp struct { instance w32.HINSTANCE - windowMap map[w32.HWND]*windowsWebviewWindow - systrayMap map[w32.HMENU]*windowsSystemTray + windowMap map[w32.HWND]*windowsWebviewWindow + windowMapLock sync.RWMutex + systrayMap map[w32.HMENU]*windowsSystemTray mainThreadID w32.HANDLE mainThreadWindowHWND w32.HWND @@ -34,7 +36,14 @@ type windowsApp struct { focusedWindow w32.HWND // system theme - isDarkMode bool + isDarkMode bool + currentWindowID uint +} + +func (m *windowsApp) getWindowForHWND(hwnd w32.HWND) *windowsWebviewWindow { + m.windowMapLock.RLock() + defer m.windowMapLock.RUnlock() + return m.windowMap[hwnd] } func getNativeApplication() *windowsApp { @@ -131,8 +140,7 @@ func (m *windowsApp) name() string { } func (m *windowsApp) getCurrentWindowID() uint { - //return uint(C.getCurrentWindowID()) - return uint(0) + return m.currentWindowID } func (m *windowsApp) setApplicationMenu(menu *Menu) { @@ -142,9 +150,7 @@ func (m *windowsApp) setApplicationMenu(menu *Menu) { } menu.Update() - // Convert impl to macosMenu object - //m.applicationMenu = (menu.impl).(*macosMenu).nsMenu - //C.setApplicationMenu(m.applicationMenu) + m.parent.ApplicationMenu = menu } func (m *windowsApp) run() error { @@ -250,7 +256,9 @@ func (m *windowsApp) wndProc(hwnd w32.HWND, msg uint32, wParam, lParam uintptr) } func (m *windowsApp) registerWindow(result *windowsWebviewWindow) { + m.windowMapLock.Lock() m.windowMap[result.hwnd] = result + m.windowMapLock.Unlock() } func (m *windowsApp) registerSystemTray(result *windowsSystemTray) { @@ -258,7 +266,9 @@ func (m *windowsApp) registerSystemTray(result *windowsSystemTray) { } func (m *windowsApp) unregisterWindow(w *windowsWebviewWindow) { + m.windowMapLock.Lock() delete(m.windowMap, w.hwnd) + m.windowMapLock.Unlock() // If this was the last window... if len(m.windowMap) == 0 && !m.parent.options.Windows.DisableQuitOnLastWindowClosed { diff --git a/v3/pkg/application/menuitem_windows.go b/v3/pkg/application/menuitem_windows.go index 9cec67a8..26509976 100644 --- a/v3/pkg/application/menuitem_windows.go +++ b/v3/pkg/application/menuitem_windows.go @@ -8,20 +8,53 @@ import ( ) type windowsMenuItem struct { + parent *Menu menuItem *MenuItem - hMenu w32.HMENU - id int - label string - disabled bool - checked bool - itemType menuItemType - hidden bool - submenu w32.HMENU + hMenu w32.HMENU + id int + label string + disabled bool + checked bool + itemType menuItemType + hidden bool + submenu w32.HMENU + itemAfter *MenuItem } func (m *windowsMenuItem) setHidden(hidden bool) { m.hidden = hidden + if m.hidden { + // iterate the parent items and find the menu item before us + for i, item := range m.parent.items { + if item == m.menuItem { + if i < len(m.parent.items)-1 { + m.itemAfter = m.parent.items[i+1] + } else { + m.itemAfter = nil + } + break + } + } + // Get the position of this menu item in the parent menu + // m.pos = w32.GetMenuItemPosition(m.hMenu, uint32(m.id)) + // Remove from parent menu + w32.RemoveMenu(m.hMenu, m.id, w32.MF_BYCOMMAND) + } else { + // Add to parent menu + // Get the position of the item before us + var pos int + if m.itemAfter != nil { + for i, item := range m.parent.items { + if item == m.itemAfter { + pos = i - 1 + break + } + } + m.itemAfter = nil + } + w32.InsertMenuItem(m.hMenu, uint32(pos), true, m.getMenuInfo()) + } } func (m *windowsMenuItem) Checked() bool { @@ -41,43 +74,7 @@ func (m *windowsMenuItem) Enabled() bool { } func (m *windowsMenuItem) update() { - var mii w32.MENUITEMINFO - mii.CbSize = uint32(unsafe.Sizeof(mii)) - mii.FMask = w32.MIIM_FTYPE | w32.MIIM_ID | w32.MIIM_STATE | w32.MIIM_STRING - if m.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 = w32.MustStringToUTF16Ptr(m.label) - mii.Cch = uint32(len([]rune(m.label))) - } - mii.WID = uint32(m.id) - if m.Enabled() { - mii.FState &^= w32.MFS_DISABLED - } else { - mii.FState |= w32.MFS_DISABLED - } - - if m.IsCheckbox() { - mii.FMask |= w32.MIIM_CHECKMARKS - } - if m.Checked() { - mii.FState |= w32.MFS_CHECKED - } - - if m.menuItem.submenu != nil { - mii.FMask |= w32.MIIM_SUBMENU - mii.HSubMenu = m.submenu - } - - w32.SetMenuItemInfo(m.hMenu, uint32(m.id), false, &mii) + w32.SetMenuItemInfo(m.hMenu, uint32(m.id), false, m.getMenuInfo()) } func (m *windowsMenuItem) setLabel(label string) { @@ -249,3 +246,42 @@ func newFullScreenMenuItem() *MenuItem { func (m *windowsMenuItem) setTooltip(_ string) { // Unsupported } + +func (m *windowsMenuItem) getMenuInfo() *w32.MENUITEMINFO { + var mii w32.MENUITEMINFO + mii.CbSize = uint32(unsafe.Sizeof(mii)) + mii.FMask = w32.MIIM_FTYPE | w32.MIIM_ID | w32.MIIM_STATE | w32.MIIM_STRING + if m.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 = w32.MustStringToUTF16Ptr(m.label) + mii.Cch = uint32(len([]rune(m.label))) + } + mii.WID = uint32(m.id) + if m.Enabled() { + mii.FState &^= w32.MFS_DISABLED + } else { + mii.FState |= w32.MFS_DISABLED + } + + if m.IsCheckbox() { + mii.FMask |= w32.MIIM_CHECKMARKS + } + if m.Checked() { + mii.FState |= w32.MFS_CHECKED + } + + if m.menuItem.submenu != nil { + mii.FMask |= w32.MIIM_SUBMENU + mii.HSubMenu = m.submenu + } + return &mii +} diff --git a/v3/pkg/application/options_webview_window.go b/v3/pkg/application/options_webview_window.go index 3367ccd2..36c10d73 100644 --- a/v3/pkg/application/options_webview_window.go +++ b/v3/pkg/application/options_webview_window.go @@ -42,6 +42,7 @@ type WebviewWindowOptions struct { Mac MacWindow Windows WindowsWindow Focused bool + Menu *Menu } var WebviewWindowDefaults = &WebviewWindowOptions{ diff --git a/v3/pkg/application/options_win.go b/v3/pkg/application/options_win.go index cefd3219..1dcd4f3d 100644 --- a/v3/pkg/application/options_win.go +++ b/v3/pkg/application/options_win.go @@ -52,6 +52,9 @@ type WindowsWindow struct { // ResizeDebounceMS is the amount of time to debounce redraws of webview2 // when resizing the window ResizeDebounceMS uint16 + + // Disable the menu bar for this window + DisableMenu bool } type Theme int diff --git a/v3/pkg/application/popupmenu_windows.go b/v3/pkg/application/popupmenu_windows.go index dd77042a..d7880a9f 100644 --- a/v3/pkg/application/popupmenu_windows.go +++ b/v3/pkg/application/popupmenu_windows.go @@ -37,8 +37,9 @@ func (r *RadioGroup) MenuID(item *MenuItem) int { panic("RadioGroup.MenuID: item not found:") } -type PopupMenu struct { - menu w32.PopupMenu +type Win32Menu struct { + isPopup bool + menu w32.HMENU parent w32.HWND menuMapping map[int]*MenuItem checkboxItems map[*MenuItem][]int @@ -49,7 +50,14 @@ type PopupMenu struct { onMenuOpen func() } -func (p *PopupMenu) buildMenu(parentMenu w32.PopupMenu, inputMenu *Menu) { +func (p *Win32Menu) newMenu() w32.HMENU { + if p.isPopup { + return w32.NewPopupMenu() + } + return w32.CreateMenu() +} + +func (p *Win32Menu) buildMenu(parentMenu w32.HMENU, inputMenu *Menu) { var currentRadioGroup RadioGroup for _, item := range inputMenu.items { if item.Hidden() { @@ -59,7 +67,8 @@ func (p *PopupMenu) buildMenu(parentMenu w32.PopupMenu, inputMenu *Menu) { itemID := p.currentMenuID p.menuMapping[itemID] = item - menuItemImpl := newMenuItemImpl(item, w32.HWND(parentMenu), itemID) + menuItemImpl := newMenuItemImpl(item, parentMenu, itemID) + menuItemImpl.parent = inputMenu flags := uint32(w32.MF_STRING) if item.disabled { @@ -89,15 +98,14 @@ func (p *PopupMenu) buildMenu(parentMenu w32.PopupMenu, inputMenu *Menu) { if item.submenu != nil { flags = flags | w32.MF_POPUP - newSubmenu := w32.CreatePopupMenu() + newSubmenu := p.newMenu() p.buildMenu(newSubmenu, item.submenu) itemID = int(newSubmenu) - menuItemImpl.submenu = w32.HWND(newSubmenu) + menuItemImpl.submenu = newSubmenu } var menuText = item.Label() - - ok := parentMenu.Append(flags, uintptr(itemID), menuText) + ok := w32.AppendMenu(parentMenu, flags, uintptr(itemID), w32.MustStringToUTF16Ptr(menuText)) if !ok { w32.Fatal(fmt.Sprintf("Error adding menu item: %s", menuText)) } @@ -113,16 +121,27 @@ func (p *PopupMenu) buildMenu(parentMenu w32.PopupMenu, inputMenu *Menu) { } } -func (p *PopupMenu) Update() { - p.menu = w32.CreatePopupMenu() +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) *PopupMenu { - result := &PopupMenu{ +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 w32.HWND, inputMenu *Menu) *Win32Menu { + result := &Win32Menu{ parent: parent, menuData: inputMenu, checkboxItems: make(map[*MenuItem][]int), @@ -132,7 +151,7 @@ func NewPopupMenu(parent w32.HWND, inputMenu *Menu) *PopupMenu { return result } -func (p *PopupMenu) ShowAtCursor() { +func (p *Win32Menu) ShowAtCursor() { x, y, ok := w32.GetCursorPos() if ok == false { w32.Fatal("GetCursorPos failed") @@ -144,7 +163,7 @@ func (p *PopupMenu) ShowAtCursor() { p.onMenuOpen() } - if p.menu.Track(p.parent, w32.TPM_LEFTALIGN, int32(x), int32(y-5)) == false { + if !w32.TrackPopupMenuEx(p.menu, w32.TPM_LEFTALIGN, int32(x), int32(y-5), p.parent, nil) { w32.Fatal("TrackPopupMenu failed") } @@ -158,7 +177,7 @@ func (p *PopupMenu) ShowAtCursor() { } -func (p *PopupMenu) ProcessCommand(cmdMsgID int) { +func (p *Win32Menu) ProcessCommand(cmdMsgID int) { item := p.menuMapping[cmdMsgID] if item == nil { return @@ -172,14 +191,18 @@ func (p *PopupMenu) ProcessCommand(cmdMsgID int) { } } -func (p *PopupMenu) Destroy() { - p.menu.Destroy() +func (p *Win32Menu) Destroy() { + w32.DestroyMenu(p.menu) } -func (p *PopupMenu) UpdateMenuItem(item *MenuItem) { +func (p *Win32Menu) UpdateMenuItem(item *MenuItem) { if item.IsCheckbox() { for _, itemID := range p.checkboxItems[item] { - p.menu.Check(uintptr(itemID), item.checked) + var checkState uint = w32.MF_UNCHECKED + if item.checked { + checkState = w32.MF_CHECKED + } + w32.CheckMenuItem(p.menu, uintptr(itemID), checkState) } return } @@ -188,7 +211,7 @@ func (p *PopupMenu) UpdateMenuItem(item *MenuItem) { } } -func (p *PopupMenu) updateRadioGroups() { +func (p *Win32Menu) updateRadioGroups() { for menuItem := range p.radioGroups { if menuItem.checked { p.updateRadioGroup(menuItem) @@ -196,18 +219,19 @@ func (p *PopupMenu) updateRadioGroups() { } } -func (p *PopupMenu) updateRadioGroup(item *MenuItem) { +func (p *Win32Menu) updateRadioGroup(item *MenuItem) { for _, radioGroup := range p.radioGroups[item] { thisMenuID := radioGroup.MenuID(item) startID, endID := radioGroup.Bounds() - p.menu.CheckRadio(startID, endID, thisMenuID) + w32.CheckRadio(p.menu, startID, endID, thisMenuID) + } } -func (p *PopupMenu) OnMenuOpen(fn func()) { +func (p *Win32Menu) OnMenuOpen(fn func()) { p.onMenuOpen = fn } -func (p *PopupMenu) OnMenuClose(fn func()) { +func (p *Win32Menu) OnMenuClose(fn func()) { p.onMenuClose = fn } diff --git a/v3/pkg/application/systemtray_windows.go b/v3/pkg/application/systemtray_windows.go index 513a6763..89e4399f 100644 --- a/v3/pkg/application/systemtray_windows.go +++ b/v3/pkg/application/systemtray_windows.go @@ -19,7 +19,7 @@ const ( type windowsSystemTray struct { parent *SystemTray - menu *PopupMenu + menu *Win32Menu // Platform specific implementation uid uint32 diff --git a/v3/pkg/application/webview_window_windows.go b/v3/pkg/application/webview_window_windows.go index 701bb01e..93d1691b 100644 --- a/v3/pkg/application/webview_window_windows.go +++ b/v3/pkg/application/webview_window_windows.go @@ -30,6 +30,7 @@ type windowsWebviewWindow struct { windowImpl unsafe.Pointer parent *WebviewWindow hwnd w32.HWND + menu *Win32Menu // Fullscreen flags isCurrentlyFullscreen bool @@ -117,6 +118,21 @@ func (w *windowsWebviewWindow) run() { var startX, _ = lo.Coalesce(options.X, w32.CW_USEDEFAULT) var startY, _ = lo.Coalesce(options.Y, w32.CW_USEDEFAULT) + var appMenu w32.HMENU + + // Process Menu + if !options.Windows.DisableMenu { + theMenu := globalApplication.ApplicationMenu + // Create the menu if we have one + if w.parent.options.Menu != nil { + theMenu = w.parent.options.Menu + } + if theMenu != nil { + w.menu = NewApplicationMenu(w.hwnd, theMenu) + appMenu = w.menu.menu + } + } + w.hwnd = w32.CreateWindowEx( exStyle, windowClassName, @@ -127,7 +143,7 @@ func (w *windowsWebviewWindow) run() { options.Width, options.Height, 0, - 0, + appMenu, w32.GetModuleHandle(""), nil) @@ -612,6 +628,10 @@ func (w *windowsWebviewWindow) isActive() bool { func (w *windowsWebviewWindow) WndProc(msg uint32, wparam, lparam uintptr) uintptr { switch msg { + case w32.WM_ACTIVATE: + if wparam == w32.WA_ACTIVE || wparam == w32.WA_CLICKACTIVE { + getNativeApplication().currentWindowID = w.parent.id + } case w32.WM_SIZE: return 0 case w32.WM_CLOSE: @@ -698,6 +718,17 @@ func (w *windowsWebviewWindow) WndProc(msg uint32, wparam, lparam uintptr) uintp } } + if w.menu != nil { + switch msg { + case w32.WM_COMMAND: + cmdMsgID := int(wparam & 0xffff) + switch cmdMsgID { + default: + w.menu.ProcessCommand(cmdMsgID) + } + } + } + if options := w.parent.options; options.Frameless { switch msg { case w32.WM_ACTIVATE: diff --git a/v3/pkg/w32/popupmenu.go b/v3/pkg/w32/popupmenu.go index 267b3af4..926071b8 100644 --- a/v3/pkg/w32/popupmenu.go +++ b/v3/pkg/w32/popupmenu.go @@ -22,6 +22,14 @@ func (p PopupMenu) Track(hwnd HWND, flags uint32, x, y int32) bool { nil) } +func RemoveMenu(m HMENU, pos, flags int) bool { + ret, _, _ := procRemoveMenu.Call( + uintptr(m), + uintptr(pos), + uintptr(flags)) + return ret != 0 +} + func (p PopupMenu) Append(flags uint32, id uintptr, text string) bool { return Menu(p).Append(flags, id, text) } @@ -42,6 +50,16 @@ func (m Menu) Check(id uintptr, check bool) bool { return CheckMenuItem(HMENU(m), id, checkState) != 0 } +func CheckRadio(m HMENU, startID int, endID int, selectedID int) bool { + ret, _, _ := procCheckMenuRadioItem.Call( + m, + uintptr(startID), + uintptr(endID), + uintptr(selectedID), + MF_BYCOMMAND) + return ret != 0 +} + func (m Menu) CheckRadio(startID int, endID int, selectedID int) bool { ret, _, _ := procCheckMenuRadioItem.Call( uintptr(m), diff --git a/v3/pkg/w32/user32.go b/v3/pkg/w32/user32.go index 18c8836d..65f4058d 100644 --- a/v3/pkg/w32/user32.go +++ b/v3/pkg/w32/user32.go @@ -86,6 +86,8 @@ var ( procGetDlgItem = moduser32.NewProc("GetDlgItem") procDrawIcon = moduser32.NewProc("DrawIcon") procCreateMenu = moduser32.NewProc("CreateMenu") + procRemoveMenu = moduser32.NewProc("RemoveMenu") + procGetMenuItemPosition = moduser32.NewProc("GetMenuItemPosition") procDestroyMenu = moduser32.NewProc("DestroyMenu") procCreatePopupMenu = moduser32.NewProc("CreatePopupMenu") procCheckMenuRadioItem = moduser32.NewProc("CheckMenuRadioItem")