Menu & MenuBar experience improvements

This commit is contained in:
Luke I. Wilson 2021-03-18 23:37:42 -05:00
parent 1d77aae277
commit 83567c1341
3 changed files with 134 additions and 41 deletions

23
main.go
View File

@ -70,8 +70,9 @@ func main() {
barFocused := false barFocused := false
// TODO: load menus in another function fileMenu := ui.NewMenu("File", &theme)
bar.AddMenu(ui.NewMenu("File", &theme, []ui.Item{&ui.ItemEntry{Name: "New File", Callback: func() {
fileMenu.AddItems([]ui.Item{&ui.ItemEntry{Name: "New File", Callback: func() {
textEdit := ui.NewTextEdit(&s, "", "", &theme) // No file path, no contents textEdit := ui.NewTextEdit(&s, "", "", &theme) // No file path, no contents
tabContainer.AddTab("noname", textEdit) tabContainer.AddTab("noname", textEdit)
}}, &ui.ItemEntry{Name: "Open...", Callback: func() { }}, &ui.ItemEntry{Name: "Open...", Callback: func() {
@ -144,9 +145,11 @@ func main() {
}}, &ui.ItemSeparator{}, &ui.ItemEntry{Name: "Exit", Callback: func() { }}, &ui.ItemSeparator{}, &ui.ItemEntry{Name: "Exit", Callback: func() {
s.Fini() s.Fini()
os.Exit(0) os.Exit(0)
}}})) }}})
bar.AddMenu(ui.NewMenu("Edit", &theme, []ui.Item{&ui.ItemEntry{Name: "Cut", Callback: func() { editMenu := ui.NewMenu("Edit", &theme)
editMenu.AddItems([]ui.Item{&ui.ItemEntry{Name: "Cut", Callback: func() {
if tabContainer.GetTabCount() > 0 { if tabContainer.GetTabCount() > 0 {
tab := tabContainer.GetTab(tabContainer.GetSelectedTabIdx()) tab := tabContainer.GetTab(tabContainer.GetSelectedTabIdx())
te := tab.Child.(*ui.TextEdit) te := tab.Child.(*ui.TextEdit)
@ -177,11 +180,17 @@ func main() {
} }
te.Insert(contents) te.Insert(contents)
} }
}}})) }}})
bar.AddMenu(ui.NewMenu("Search", &theme, []ui.Item{&ui.ItemEntry{Name: "New", Callback: func() { searchMenu := ui.NewMenu("Search", &theme)
searchMenu.AddItems([]ui.Item{&ui.ItemEntry{Name: "New", Callback: func() {
s.Beep() s.Beep()
}}})) }}})
bar.AddMenu(fileMenu)
bar.AddMenu(editMenu)
bar.AddMenu(searchMenu)
changeFocus(tabContainer) // TabContainer is focused by default changeFocus(tabContainer) // TabContainer is focused by default

View File

@ -51,7 +51,8 @@ type MenuBar struct {
x, y int x, y int
width, height int width, height int
focused bool focused bool
selected int // Index of selection in MenuBar selected int // Index of selection in MenuBar
menusVisible bool // Whether to draw the selected menu
Theme *Theme Theme *Theme
} }
@ -65,7 +66,8 @@ func NewMenuBar(theme *Theme) *MenuBar {
func (b *MenuBar) AddMenu(menu *Menu) { func (b *MenuBar) AddMenu(menu *Menu) {
menu.itemSelectedCallback = func() { menu.itemSelectedCallback = func() {
// TODO: figure out what im doing here b.menusVisible = false
menu.SetFocused(false)
} }
b.Menus = append(b.Menus, menu) b.Menus = append(b.Menus, menu)
} }
@ -84,7 +86,7 @@ func (b *MenuBar) Draw(s tcell.Screen) {
normalStyle := b.Theme.GetOrDefault("MenuBar") normalStyle := b.Theme.GetOrDefault("MenuBar")
// Draw menus based on whether b.focused and which is selected // Draw menus based on whether b.focused and which is selected
DrawRect(s, b.x, b.y, 200, 1, ' ', normalStyle) // TODO: calculate actual width DrawRect(s, b.x, b.y, b.width, 1, ' ', normalStyle)
col := b.x + 1 col := b.x + 1
for i, item := range b.Menus { for i, item := range b.Menus {
str := fmt.Sprintf(" %s ", item.Name) // Surround the name in spaces str := fmt.Sprintf(" %s ", item.Name) // Surround the name in spaces
@ -98,7 +100,7 @@ func (b *MenuBar) Draw(s tcell.Screen) {
col += len(str) col += len(str)
} }
if b.Menus[b.selected].Visible { if b.menusVisible {
menu := b.Menus[b.selected] menu := b.Menus[b.selected]
menu.Draw(s) // Draw menu when it is expanded / visible menu.Draw(s) // Draw menu when it is expanded / visible
} }
@ -107,8 +109,12 @@ func (b *MenuBar) Draw(s tcell.Screen) {
// SetFocused highlights the MenuBar and focuses any sub-menus. // SetFocused highlights the MenuBar and focuses any sub-menus.
func (b *MenuBar) SetFocused(v bool) { func (b *MenuBar) SetFocused(v bool) {
b.focused = v b.focused = v
b.Menus[b.selected].SetFocused(v)
if !v { if !v {
b.Menus[b.selected].SetFocused(false) b.selected = 0 // Reset cursor position every time component is unfocused
if b.menusVisible {
b.menusVisible = false
}
} }
} }
@ -145,11 +151,19 @@ func (b *MenuBar) SetSize(width, height int) {
func (b *MenuBar) HandleEvent(event tcell.Event) bool { func (b *MenuBar) HandleEvent(event tcell.Event) bool {
switch ev := event.(type) { switch ev := event.(type) {
case *tcell.EventKey: case *tcell.EventKey:
if ev.Key() == tcell.KeyEnter && !b.Menus[b.selected].Visible { switch ev.Key() {
menu := &b.Menus[b.selected] case tcell.KeyEnter:
(*menu).SetPos(b.GetMenuXPos(b.selected), b.y+1) if !b.menusVisible { // If menus are not visible...
(*menu).SetFocused(true) // Makes .Visible true for the Menu b.menusVisible = true
} else if ev.Key() == tcell.KeyLeft {
menu := &b.Menus[b.selected]
(*menu).SetPos(b.GetMenuXPos(b.selected), b.y+1)
(*menu).SetFocused(true) // Makes .Visible true for the Menu
} else { // The selected Menu is visible, send the event to it
return b.Menus[b.selected].HandleEvent(event)
}
case tcell.KeyLeft:
b.Menus[b.selected].SetFocused(false) // Unfocus current menu
if b.selected <= 0 { if b.selected <= 0 {
b.selected = len(b.Menus) - 1 // Wrap to end b.selected = len(b.Menus) - 1 // Wrap to end
} else { } else {
@ -157,7 +171,9 @@ func (b *MenuBar) HandleEvent(event tcell.Event) bool {
} }
// Update position of new menu after changing menu selection // Update position of new menu after changing menu selection
b.Menus[b.selected].SetPos(b.GetMenuXPos(b.selected), b.y+1) b.Menus[b.selected].SetPos(b.GetMenuXPos(b.selected), b.y+1)
} else if ev.Key() == tcell.KeyRight { b.Menus[b.selected].SetFocused(true) // Focus new menu
case tcell.KeyRight:
b.Menus[b.selected].SetFocused(false)
if b.selected >= len(b.Menus)-1 { if b.selected >= len(b.Menus)-1 {
b.selected = 0 // Wrap to beginning b.selected = 0 // Wrap to beginning
} else { } else {
@ -165,8 +181,26 @@ func (b *MenuBar) HandleEvent(event tcell.Event) bool {
} }
// Update position of new menu after changing menu selection // Update position of new menu after changing menu selection
b.Menus[b.selected].SetPos(b.GetMenuXPos(b.selected), b.y+1) b.Menus[b.selected].SetPos(b.GetMenuXPos(b.selected), b.y+1)
} else { b.Menus[b.selected].SetFocused(true) // Focus new menu
if b.Menus[b.selected].Visible {
case tcell.KeyRune: // Search for the matching quick char in menu names
if !b.menusVisible { // If the selected Menu is not open/visible
for _, m := range b.Menus {
found, r := QuickCharInString(m.Name)
if found && r == ev.Rune() {
b.menusVisible = true
menu := &b.Menus[b.selected]
(*menu).SetPos(b.GetMenuXPos(b.selected), b.y+1)
(*menu).SetFocused(true)
}
}
} else {
return b.Menus[b.selected].HandleEvent(event) // Have menu handle quick char event
}
default:
if b.menusVisible {
return b.Menus[b.selected].HandleEvent(event) return b.Menus[b.selected].HandleEvent(event)
} else { } else {
return false // Nobody to propogate our event to return false // Nobody to propogate our event to
@ -181,7 +215,6 @@ func (b *MenuBar) HandleEvent(event tcell.Event) bool {
type Menu struct { type Menu struct {
Name string Name string
Items []Item Items []Item
Visible bool // True when focused
x, y int x, y int
width, height int // Size may not be settable width, height int // Size may not be settable
@ -192,18 +225,40 @@ type Menu struct {
} }
// New creates a new Menu. `items` can be `nil`. // New creates a new Menu. `items` can be `nil`.
func NewMenu(name string, theme *Theme, items []Item) *Menu { func NewMenu(name string, theme *Theme) *Menu {
if items == nil {
items = make([]Item, 0, 6)
}
return &Menu{ return &Menu{
Name: name, Name: name,
Items: items, Items: make([]Item, 0, 6),
Theme: theme, Theme: theme,
} }
} }
func (m *Menu) AddItem(item Item) {
switch typ := item.(type) {
case *Menu:
typ.itemSelectedCallback = func() {
m.itemSelectedCallback()
}
}
m.Items = append(m.Items, item)
}
func (m *Menu) AddItems(items []Item) {
for _, item := range items {
m.AddItem(item)
}
}
func (m *Menu) ActivateItemUnderCursor() {
switch item := m.Items[m.selected].(type) {
case *ItemEntry:
item.Callback()
m.itemSelectedCallback()
case *Menu:
// TODO: implement sub-menus ...
}
}
func (m *Menu) CursorUp() { func (m *Menu) CursorUp() {
if m.selected <= 0 { if m.selected <= 0 {
m.selected = len(m.Items) - 1 // Wrap to end m.selected = len(m.Items) - 1 // Wrap to end
@ -260,7 +315,10 @@ func (m *Menu) Draw(s tcell.Screen) {
// SetFocused does not do anything for a Menu. // SetFocused does not do anything for a Menu.
func (m *Menu) SetFocused(v bool) { func (m *Menu) SetFocused(v bool) {
m.Visible = v // TODO: when adding sub-menus, set all focus to v
if !v {
m.selected = 0
}
} }
// GetPos returns the position of the Menu. // GetPos returns the position of the Menu.
@ -299,22 +357,28 @@ func (m *Menu) HandleEvent(event tcell.Event) bool {
// TODO: simplify this function // TODO: simplify this function
switch ev := event.(type) { switch ev := event.(type) {
case *tcell.EventKey: case *tcell.EventKey:
if ev.Key() == tcell.KeyEnter { switch ev.Key() {
m.SetFocused(false) // Hides the menu case tcell.KeyEnter:
switch item := m.Items[m.selected].(type) { m.ActivateItemUnderCursor()
case *ItemEntry: case tcell.KeyUp:
item.Callback()
case *Menu:
// TODO: implement sub-menus ...
}
return true
} else if ev.Key() == tcell.KeyUp {
m.CursorUp() m.CursorUp()
return true case tcell.KeyDown:
} else if ev.Key() == tcell.KeyDown {
m.CursorDown() m.CursorDown()
return true
case tcell.KeyRune:
// TODO: support quick chars for sub-menus
for i, item := range m.Items {
found, r := QuickCharInString(item.GetName())
if found && r == ev.Rune() {
m.selected = i
m.ActivateItemUnderCursor()
}
}
default:
return false
} }
return true
} }
return false return false
} }

View File

@ -1,5 +1,25 @@
package ui package ui
import "unicode"
// QuickCharInString is used for finding the "quick char" in a string. A quick char
// suffixes a '_' (underscore). So basically, this function returns any rune after
// an underscore. The rune is always made lowercase. The bool returned is whether
// the rune was found.
func QuickCharInString(s string) (bool, rune) {
runes := []rune(s)
for i, r := range runes {
if r == '_' {
if i+1 < len(runes) {
return true, unicode.ToLower(runes[i+1])
} else {
return false, ' '
}
}
}
return false, ' '
}
// Max returns the larger integer. // Max returns the larger integer.
func Max(a, b int) int { func Max(a, b int) int {
if a > b { if a > b {