github.com/secoba/wails/v2@v2.6.4/internal/frontend/desktop/windows/winc/menu.go (about) 1 //go:build windows 2 3 /* 4 * Copyright (C) 2019 The Winc Authors. All Rights Reserved. 5 */ 6 7 package winc 8 9 import ( 10 "fmt" 11 "syscall" 12 "unsafe" 13 14 "github.com/secoba/wails/v2/internal/frontend/desktop/windows/winc/w32" 15 ) 16 17 var ( 18 nextMenuItemID uint16 = 3 19 actionsByID = make(map[uint16]*MenuItem) 20 shortcut2Action = make(map[Shortcut]*MenuItem) 21 menuItems = make(map[w32.HMENU][]*MenuItem) 22 radioGroups = make(map[*MenuItem]*RadioGroup) 23 initialised bool 24 ) 25 26 var NoShortcut = Shortcut{} 27 28 // Menu for main window and context menus on controls. 29 // Most methods used for both main window menu and context menu. 30 type Menu struct { 31 hMenu w32.HMENU 32 hwnd w32.HWND // hwnd might be nil if it is context menu. 33 } 34 35 type MenuItem struct { 36 hMenu w32.HMENU 37 hSubMenu w32.HMENU // Non zero if this item is in itself a submenu. 38 39 text string 40 toolTip string 41 image *Bitmap 42 shortcut Shortcut 43 enabled bool 44 45 checkable bool 46 checked bool 47 isRadio bool 48 49 id uint16 50 51 onClick EventManager 52 } 53 54 type RadioGroup struct { 55 members []*MenuItem 56 hwnd w32.HWND 57 } 58 59 func NewContextMenu() *MenuItem { 60 hMenu := w32.CreatePopupMenu() 61 if hMenu == 0 { 62 panic("failed CreateMenu") 63 } 64 65 item := &MenuItem{ 66 hMenu: hMenu, 67 hSubMenu: hMenu, 68 } 69 return item 70 } 71 72 func (m *Menu) Dispose() { 73 if m.hMenu != 0 { 74 w32.DestroyMenu(m.hMenu) 75 m.hMenu = 0 76 } 77 } 78 79 func (m *Menu) IsDisposed() bool { 80 return m.hMenu == 0 81 } 82 83 func initMenuItemInfoFromAction(mii *w32.MENUITEMINFO, a *MenuItem) { 84 mii.CbSize = uint32(unsafe.Sizeof(*mii)) 85 mii.FMask = w32.MIIM_FTYPE | w32.MIIM_ID | w32.MIIM_STATE | w32.MIIM_STRING 86 if a.image != nil { 87 mii.FMask |= w32.MIIM_BITMAP 88 mii.HbmpItem = a.image.handle 89 } 90 if a.IsSeparator() { 91 mii.FType = w32.MFT_SEPARATOR 92 } else { 93 mii.FType = w32.MFT_STRING 94 var text string 95 if s := a.shortcut; s.Key != 0 { 96 text = fmt.Sprintf("%s\t%s", a.text, s.String()) 97 shortcut2Action[a.shortcut] = a 98 } else { 99 text = a.text 100 } 101 mii.DwTypeData = syscall.StringToUTF16Ptr(text) 102 mii.Cch = uint32(len([]rune(a.text))) 103 } 104 mii.WID = uint32(a.id) 105 106 if a.Enabled() { 107 mii.FState &^= w32.MFS_DISABLED 108 } else { 109 mii.FState |= w32.MFS_DISABLED 110 } 111 112 if a.Checkable() { 113 mii.FMask |= w32.MIIM_CHECKMARKS 114 } 115 if a.Checked() { 116 mii.FState |= w32.MFS_CHECKED 117 } 118 119 if a.hSubMenu != 0 { 120 mii.FMask |= w32.MIIM_SUBMENU 121 mii.HSubMenu = a.hSubMenu 122 } 123 } 124 125 // Show menu on the main window. 126 func (m *Menu) Show() { 127 initialised = true 128 updateRadioGroups() 129 if !w32.DrawMenuBar(m.hwnd) { 130 panic("DrawMenuBar failed") 131 } 132 } 133 134 // AddSubMenu returns item that is used as submenu to perform AddItem(s). 135 func (m *Menu) AddSubMenu(text string) *MenuItem { 136 hSubMenu := w32.CreateMenu() 137 if hSubMenu == 0 { 138 panic("failed CreateMenu") 139 } 140 return addMenuItem(m.hMenu, hSubMenu, text, Shortcut{}, nil, false) 141 } 142 143 // This method will iterate through the menu items, group radio items together, build a 144 // quick access map and set the initial items 145 func updateRadioGroups() { 146 147 if !initialised { 148 return 149 } 150 151 radioItemsChecked := []*MenuItem{} 152 radioGroups = make(map[*MenuItem]*RadioGroup) 153 var currentRadioGroupMembers []*MenuItem 154 // Iterate the menus 155 for _, menu := range menuItems { 156 menuLength := len(menu) 157 for index, menuItem := range menu { 158 if menuItem.isRadio { 159 currentRadioGroupMembers = append(currentRadioGroupMembers, menuItem) 160 if menuItem.checked { 161 radioItemsChecked = append(radioItemsChecked, menuItem) 162 } 163 164 // If end of menu 165 if index == menuLength-1 { 166 radioGroup := &RadioGroup{ 167 members: currentRadioGroupMembers, 168 hwnd: menuItem.hMenu, 169 } 170 // Save the group to each member iin the radiomap 171 for _, member := range currentRadioGroupMembers { 172 radioGroups[member] = radioGroup 173 } 174 currentRadioGroupMembers = []*MenuItem{} 175 } 176 continue 177 } 178 179 // Not a radio item 180 if len(currentRadioGroupMembers) > 0 { 181 radioGroup := &RadioGroup{ 182 members: currentRadioGroupMembers, 183 hwnd: menuItem.hMenu, 184 } 185 // Save the group to each member iin the radiomap 186 for _, member := range currentRadioGroupMembers { 187 radioGroups[member] = radioGroup 188 } 189 currentRadioGroupMembers = []*MenuItem{} 190 } 191 } 192 } 193 194 // Enable the checked items 195 for _, item := range radioItemsChecked { 196 radioGroup := radioGroups[item] 197 startID := radioGroup.members[0].id 198 endID := radioGroup.members[len(radioGroup.members)-1].id 199 w32.SelectRadioMenuItem(item.id, startID, endID, radioGroup.hwnd) 200 } 201 202 } 203 204 func (mi *MenuItem) OnClick() *EventManager { 205 return &mi.onClick 206 } 207 208 func (mi *MenuItem) AddSeparator() { 209 addMenuItem(mi.hSubMenu, 0, "-", Shortcut{}, nil, false) 210 } 211 212 // AddItem adds plain menu item. 213 func (mi *MenuItem) AddItem(text string, shortcut Shortcut) *MenuItem { 214 return addMenuItem(mi.hSubMenu, 0, text, shortcut, nil, false) 215 } 216 217 // AddItemCheckable adds plain menu item that can have a checkmark. 218 func (mi *MenuItem) AddItemCheckable(text string, shortcut Shortcut) *MenuItem { 219 return addMenuItem(mi.hSubMenu, 0, text, shortcut, nil, true) 220 } 221 222 // AddItemRadio adds plain menu item that can have a checkmark and is part of a radio group. 223 func (mi *MenuItem) AddItemRadio(text string, shortcut Shortcut) *MenuItem { 224 menuItem := addMenuItem(mi.hSubMenu, 0, text, shortcut, nil, true) 225 menuItem.isRadio = true 226 return menuItem 227 } 228 229 // AddItemWithBitmap adds menu item with shortcut and bitmap. 230 func (mi *MenuItem) AddItemWithBitmap(text string, shortcut Shortcut, image *Bitmap) *MenuItem { 231 return addMenuItem(mi.hSubMenu, 0, text, shortcut, image, false) 232 } 233 234 // AddSubMenu adds a submenu. 235 func (mi *MenuItem) AddSubMenu(text string) *MenuItem { 236 hSubMenu := w32.CreatePopupMenu() 237 if hSubMenu == 0 { 238 panic("failed CreatePopupMenu") 239 } 240 return addMenuItem(mi.hSubMenu, hSubMenu, text, Shortcut{}, nil, false) 241 } 242 243 // AddItem to the menu, set text to "-" for separators. 244 func addMenuItem(hMenu, hSubMenu w32.HMENU, text string, shortcut Shortcut, image *Bitmap, checkable bool) *MenuItem { 245 item := &MenuItem{ 246 hMenu: hMenu, 247 hSubMenu: hSubMenu, 248 text: text, 249 shortcut: shortcut, 250 image: image, 251 enabled: true, 252 id: nextMenuItemID, 253 checkable: checkable, 254 isRadio: false, 255 //visible: true, 256 } 257 nextMenuItemID++ 258 actionsByID[item.id] = item 259 menuItems[hMenu] = append(menuItems[hMenu], item) 260 261 var mii w32.MENUITEMINFO 262 initMenuItemInfoFromAction(&mii, item) 263 264 index := -1 265 if !w32.InsertMenuItem(hMenu, uint32(index), true, &mii) { 266 panic("InsertMenuItem failed") 267 } 268 return item 269 } 270 271 func indexInObserver(a *MenuItem) int { 272 var idx int 273 for _, mi := range menuItems[a.hMenu] { 274 if mi == a { 275 return idx 276 } 277 idx++ 278 } 279 return -1 280 } 281 282 func findMenuItemByID(id int) *MenuItem { 283 return actionsByID[uint16(id)] 284 } 285 286 func (mi *MenuItem) update() { 287 var mii w32.MENUITEMINFO 288 initMenuItemInfoFromAction(&mii, mi) 289 290 if !w32.SetMenuItemInfo(mi.hMenu, uint32(indexInObserver(mi)), true, &mii) { 291 panic("SetMenuItemInfo failed") 292 } 293 if mi.isRadio { 294 mi.updateRadioGroup() 295 } 296 } 297 298 func (mi *MenuItem) IsSeparator() bool { return mi.text == "-" } 299 func (mi *MenuItem) SetSeparator() { mi.text = "-" } 300 301 func (mi *MenuItem) Enabled() bool { return mi.enabled } 302 func (mi *MenuItem) SetEnabled(b bool) { mi.enabled = b; mi.update() } 303 304 func (mi *MenuItem) Checkable() bool { return mi.checkable } 305 func (mi *MenuItem) SetCheckable(b bool) { mi.checkable = b; mi.update() } 306 307 func (mi *MenuItem) Checked() bool { return mi.checked } 308 func (mi *MenuItem) SetChecked(b bool) { 309 if mi.isRadio { 310 radioGroup := radioGroups[mi] 311 if radioGroup != nil { 312 for _, member := range radioGroup.members { 313 member.checked = false 314 } 315 } 316 317 } 318 mi.checked = b 319 mi.update() 320 } 321 322 func (mi *MenuItem) Text() string { return mi.text } 323 func (mi *MenuItem) SetText(s string) { mi.text = s; mi.update() } 324 325 func (mi *MenuItem) Image() *Bitmap { return mi.image } 326 func (mi *MenuItem) SetImage(b *Bitmap) { mi.image = b; mi.update() } 327 328 func (mi *MenuItem) ToolTip() string { return mi.toolTip } 329 func (mi *MenuItem) SetToolTip(s string) { mi.toolTip = s; mi.update() } 330 331 func (mi *MenuItem) updateRadioGroup() { 332 radioGroup := radioGroups[mi] 333 if radioGroup == nil { 334 return 335 } 336 startID := radioGroup.members[0].id 337 endID := radioGroup.members[len(radioGroup.members)-1].id 338 w32.SelectRadioMenuItem(mi.id, startID, endID, radioGroup.hwnd) 339 }