[v3 windows] Support Application Menu, hiding menu items

This commit is contained in:
Lea Anthony
2023-06-01 22:52:04 +10:00
parent 82ec56bc7d
commit 9b686b5417
10 changed files with 210 additions and 82 deletions
+6 -3
View File
@@ -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)
+18 -8
View File
@@ -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 {
+81 -45
View File
@@ -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
}
@@ -42,6 +42,7 @@ type WebviewWindowOptions struct {
Mac MacWindow
Windows WindowsWindow
Focused bool
Menu *Menu
}
var WebviewWindowDefaults = &WebviewWindowOptions{
+3
View File
@@ -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
+48 -24
View File
@@ -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
}
+1 -1
View File
@@ -19,7 +19,7 @@ const (
type windowsSystemTray struct {
parent *SystemTray
menu *PopupMenu
menu *Win32Menu
// Platform specific implementation
uid uint32
+32 -1
View File
@@ -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:
+18
View File
@@ -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),
+2
View File
@@ -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")