package ui // The keyboard and the mouse on the menu bar. import "github.com/gdamore/tcell/v2" // HandleKey drives the bar: F10 and Alt-letter open it, and once open the // arrow keys, Enter and Escape work it. func (b *MenuBar) HandleKey(ev *tcell.EventKey) bool { if !b.Open() { return b.handleClosedKey(ev) } return b.handleOpenKey(ev) } // handleClosedKey opens the bar on F10 or on the Alt-letter of a menu. func (b *MenuBar) handleClosedKey(ev *tcell.EventKey) bool { if ev.Key() == tcell.KeyF10 { b.OpenMenu(0) return true } if ev.Modifiers()&tcell.ModAlt == 0 || ev.Key() != tcell.KeyRune { return false } for i, menu := range b.menus { if MatchesHotKey(menu.Label, ev.Rune()) { b.OpenMenu(i) return true } } return false } // handleOpenKey works whichever drop-down the keyboard is in. func (b *MenuBar) handleOpenKey(ev *tcell.EventKey) bool { if b.submenuOpen() { return b.handleSubmenuKey(ev) } switch ev.Key() { case tcell.KeyEscape: b.Close() case tcell.KeyLeft: b.OpenMenu((b.openMenu - 1 + len(b.menus)) % len(b.menus)) case tcell.KeyRight: b.stepRight() case tcell.KeyUp: b.highlight = b.nextUsableIn(b.menus[b.openMenu].Items, b.highlight, -1) case tcell.KeyDown: b.highlight = b.nextUsableIn(b.menus[b.openMenu].Items, b.highlight, +1) case tcell.KeyEnter: b.activate(b.highlight) case tcell.KeyRune: return b.selectByHotKey(ev.Rune()) default: return false } return true } // selectByHotKey runs the drop-down item whose hot key was typed. func (b *MenuBar) selectByHotKey(key rune) bool { for i, item := range b.menus[b.openMenu].Items { if item.enabled() && MatchesHotKey(item.Label, key) { b.activate(i) return true } } return true // an open menu swallows stray typing rather than letting it through } // activate opens item i's submenu, or closes the menu and runs its action. func (b *MenuBar) activate(i int) { items := b.menus[b.openMenu].Items if i < 0 || i >= len(items) || !items[i].enabled() { return } if items[i].hasSubmenu() { b.openSubmenu(i) return } action := items[i].Action b.Close() action() } // nextUsableIn returns the index of the next item of a list that can be // chosen, walking in the given direction and wrapping round. It returns from // where it started if the list holds nothing usable at all. func (b *MenuBar) nextUsableIn(items []*MenuItem, from, step int) int { if len(items) == 0 { return 0 } index := from for range len(items) { index = (index + step + len(items)) % len(items) if items[index].enabled() { return index } } return max(from, 0) } // HandleMouse opens, walks and runs menus with the pointer. func (b *MenuBar) HandleMouse(ev *tcell.EventMouse) bool { x, y := ev.Position() pressed := ev.Buttons() == tcell.Button1 if b.Bounds().Contains(x, y) { return b.handleBarMouse(x, pressed) } if !b.Open() { return false } if b.submenuOpen() && b.submenuBounds().Contains(x, y) { return b.handleSubmenuMouse(x, y, pressed) } return b.handleDropdownMouse(x, y, pressed) } // handleBarMouse reacts to the pointer on the row of titles. func (b *MenuBar) handleBarMouse(x int, pressed bool) bool { for i, menu := range b.menus { start := b.menuX(i) if x < start-1 || x > start+LabelWidth(menu.Label) { continue } if pressed { b.toggle(i) } return true } if pressed { b.Close() } return true } // toggle opens menu i, or closes it when it is already the open one. func (b *MenuBar) toggle(i int) { if b.openMenu == i { b.Close() return } b.OpenMenu(i) } // handleDropdownMouse reacts to the pointer over an open drop-down. func (b *MenuBar) handleDropdownMouse(x, y int, pressed bool) bool { bounds := b.dropdownBounds() if !bounds.Contains(x, y) { if pressed { b.Close() // a click anywhere else puts the menu away } return pressed } index := y - bounds.Y - 1 items := b.menus[b.openMenu].Items if index < 0 || index >= len(items) || !items[index].enabled() { return true } b.highlight = index // Moving onto a branch opens it and moving off it closes it, so the pointer // never leaves a submenu showing for an item it has left. switch { case items[index].hasSubmenu(): b.openSubmenu(index) case b.submenuOpen(): b.closeSubmenu() } if pressed { b.activate(index) } return true }