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  }