Menu & MenuBar experience improvements
This commit is contained in:
parent
1d77aae277
commit
83567c1341
23
main.go
23
main.go
@ -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
|
||||||
|
|
||||||
|
132
ui/menu.go
132
ui/menu.go
@ -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
|
||||||
}
|
}
|
||||||
|
20
ui/util.go
20
ui/util.go
@ -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 {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user