diff --git a/main.go b/main.go index f255449..b9f094d 100644 --- a/main.go +++ b/main.go @@ -70,8 +70,9 @@ func main() { barFocused := false - // TODO: load menus in another function - bar.AddMenu(ui.NewMenu("File", &theme, []ui.Item{&ui.ItemEntry{Name: "New File", Callback: func() { + fileMenu := ui.NewMenu("File", &theme) + + fileMenu.AddItems([]ui.Item{&ui.ItemEntry{Name: "New File", Callback: func() { textEdit := ui.NewTextEdit(&s, "", "", &theme) // No file path, no contents tabContainer.AddTab("noname", textEdit) }}, &ui.ItemEntry{Name: "Open...", Callback: func() { @@ -144,9 +145,11 @@ func main() { }}, &ui.ItemSeparator{}, &ui.ItemEntry{Name: "Exit", Callback: func() { s.Fini() 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 { tab := tabContainer.GetTab(tabContainer.GetSelectedTabIdx()) te := tab.Child.(*ui.TextEdit) @@ -177,11 +180,17 @@ func main() { } 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() - }}})) + }}}) + + bar.AddMenu(fileMenu) + bar.AddMenu(editMenu) + bar.AddMenu(searchMenu) changeFocus(tabContainer) // TabContainer is focused by default diff --git a/ui/menu.go b/ui/menu.go index faadd48..d180eba 100644 --- a/ui/menu.go +++ b/ui/menu.go @@ -51,7 +51,8 @@ type MenuBar struct { x, y int width, height int 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 } @@ -65,7 +66,8 @@ func NewMenuBar(theme *Theme) *MenuBar { func (b *MenuBar) AddMenu(menu *Menu) { menu.itemSelectedCallback = func() { - // TODO: figure out what im doing here + b.menusVisible = false + menu.SetFocused(false) } b.Menus = append(b.Menus, menu) } @@ -84,7 +86,7 @@ func (b *MenuBar) Draw(s tcell.Screen) { normalStyle := b.Theme.GetOrDefault("MenuBar") // 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 for i, item := range b.Menus { 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) } - if b.Menus[b.selected].Visible { + if b.menusVisible { menu := b.Menus[b.selected] 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. func (b *MenuBar) SetFocused(v bool) { b.focused = v + b.Menus[b.selected].SetFocused(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 { switch ev := event.(type) { case *tcell.EventKey: - if ev.Key() == tcell.KeyEnter && !b.Menus[b.selected].Visible { - menu := &b.Menus[b.selected] - (*menu).SetPos(b.GetMenuXPos(b.selected), b.y+1) - (*menu).SetFocused(true) // Makes .Visible true for the Menu - } else if ev.Key() == tcell.KeyLeft { + switch ev.Key() { + case tcell.KeyEnter: + if !b.menusVisible { // If menus are not visible... + b.menusVisible = true + + 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 { b.selected = len(b.Menus) - 1 // Wrap to end } else { @@ -157,7 +171,9 @@ func (b *MenuBar) HandleEvent(event tcell.Event) bool { } // Update position of new menu after changing menu selection 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 { b.selected = 0 // Wrap to beginning } else { @@ -165,8 +181,26 @@ func (b *MenuBar) HandleEvent(event tcell.Event) bool { } // Update position of new menu after changing menu selection b.Menus[b.selected].SetPos(b.GetMenuXPos(b.selected), b.y+1) - } else { - if b.Menus[b.selected].Visible { + b.Menus[b.selected].SetFocused(true) // Focus new menu + + 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) } else { return false // Nobody to propogate our event to @@ -181,7 +215,6 @@ func (b *MenuBar) HandleEvent(event tcell.Event) bool { type Menu struct { Name string Items []Item - Visible bool // True when focused x, y int width, height int // Size may not be settable @@ -192,18 +225,40 @@ type Menu struct { } // New creates a new Menu. `items` can be `nil`. -func NewMenu(name string, theme *Theme, items []Item) *Menu { - if items == nil { - items = make([]Item, 0, 6) - } - +func NewMenu(name string, theme *Theme) *Menu { return &Menu{ Name: name, - Items: items, + Items: make([]Item, 0, 6), 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() { if m.selected <= 0 { 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. 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. @@ -299,22 +357,28 @@ func (m *Menu) HandleEvent(event tcell.Event) bool { // TODO: simplify this function switch ev := event.(type) { case *tcell.EventKey: - if ev.Key() == tcell.KeyEnter { - m.SetFocused(false) // Hides the menu - switch item := m.Items[m.selected].(type) { - case *ItemEntry: - item.Callback() - case *Menu: - // TODO: implement sub-menus ... - } - return true - } else if ev.Key() == tcell.KeyUp { + switch ev.Key() { + case tcell.KeyEnter: + m.ActivateItemUnderCursor() + case tcell.KeyUp: m.CursorUp() - return true - } else if ev.Key() == tcell.KeyDown { + case tcell.KeyDown: 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 } diff --git a/ui/util.go b/ui/util.go index 4cb7d44..0fdcb4b 100644 --- a/ui/util.go +++ b/ui/util.go @@ -1,5 +1,25 @@ 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. func Max(a, b int) int { if a > b {