From 1161888660482f12cf38c756c0d7c0ba1f7b3713 Mon Sep 17 00:00:00 2001 From: "Luke I. Wilson" Date: Thu, 25 Mar 2021 11:27:43 -0500 Subject: [PATCH] Made quick chars use an index --- main.go | 50 ++++++++++++++++++++++----------------------- ui/drawfunctions.go | 39 ++++++++++++++++++++--------------- ui/menu.go | 50 ++++++++++++++++++++++++++++++--------------- ui/util.go | 38 +++++++++++++++++++--------------- 4 files changed, 102 insertions(+), 75 deletions(-) diff --git a/main.go b/main.go index c6be316..d5e09cc 100644 --- a/main.go +++ b/main.go @@ -85,12 +85,12 @@ func main() { menuBar = ui.NewMenuBar(&theme) - fileMenu := ui.NewMenu("_File", &theme) + fileMenu := ui.NewMenu("File", 0, &theme) - fileMenu.AddItems([]ui.Item{&ui.ItemEntry{Name: "_New File", Shortcut: "Ctrl+N", Callback: func() { + fileMenu.AddItems([]ui.Item{&ui.ItemEntry{Name: "New File", Shortcut: "Ctrl+N", Callback: func() { textEdit := ui.NewTextEdit(&s, "", []byte{}, &theme) // No file path, no contents tabContainer.AddTab("noname", textEdit) - }}, &ui.ItemEntry{Name: "_Open...", Shortcut: "Ctrl+O", Callback: func() { + }}, &ui.ItemEntry{Name: "Open...", Shortcut: "Ctrl+O", Callback: func() { callback := func(filePaths []string) { for _, path := range filePaths { file, err := os.Open(path) @@ -124,7 +124,7 @@ func main() { }, ) changeFocus(dialog) - }}, &ui.ItemEntry{Name: "_Save", Shortcut: "Ctrl+S", Callback: func() { + }}, &ui.ItemEntry{Name: "Save", Shortcut: "Ctrl+S", Callback: func() { if tabContainer.GetTabCount() > 0 { tab := tabContainer.GetTab(tabContainer.GetSelectedTabIdx()) te := tab.Child.(*ui.TextEdit) @@ -144,7 +144,7 @@ func main() { } changeFocus(tabContainer) } - }}, &ui.ItemEntry{Name: "Save _As...", Callback: func() { + }}, &ui.ItemEntry{Name: "Save As...", QuickChar: 5, Callback: func() { // TODO: implement a "Save as" dialog system, and show that when trying to save noname files callback := func(filePaths []string) { dialog = nil // Hide the file selector @@ -163,7 +163,7 @@ func main() { }, ) changeFocus(dialog) - }}, &ui.ItemSeparator{}, &ui.ItemEntry{Name: "_Close", Shortcut: "Ctrl+Q", Callback: func() { + }}, &ui.ItemSeparator{}, &ui.ItemEntry{Name: "Close", Shortcut: "Ctrl+Q", Callback: func() { if tabContainer.GetTabCount() > 0 { tabContainer.RemoveTab(tabContainer.GetSelectedTabIdx()) } else { // No tabs open; close the editor @@ -172,35 +172,35 @@ func main() { } }}}) - panelMenu := ui.NewMenu("_Panel", &theme) + panelMenu := ui.NewMenu("Panel", 0, &theme) - panelMenu.AddItems([]ui.Item{&ui.ItemEntry{Name: "Focus Up", Shortcut: "Alt+Up", Callback: func() { + panelMenu.AddItems([]ui.Item{&ui.ItemEntry{Name: "Focus Up", QuickChar: -1, Shortcut: "Alt+Up", Callback: func() { - }}, &ui.ItemEntry{Name: "Focus Down", Shortcut: "Alt+Down", Callback: func() { + }}, &ui.ItemEntry{Name: "Focus Down", QuickChar: -1, Shortcut: "Alt+Down", Callback: func() { - }}, &ui.ItemEntry{Name: "Focus Left", Shortcut: "Alt+Left", Callback: func() { + }}, &ui.ItemEntry{Name: "Focus Left", QuickChar: -1, Shortcut: "Alt+Left", Callback: func() { - }}, &ui.ItemEntry{Name: "Focus Right", Shortcut: "Alt+Right", Callback: func() { + }}, &ui.ItemEntry{Name: "Focus Right", QuickChar: -1, Shortcut: "Alt+Right", Callback: func() { - }}, &ui.ItemSeparator{}, &ui.ItemEntry{Name: "Split _Top", Callback: func() { + }}, &ui.ItemSeparator{}, &ui.ItemEntry{Name: "Split Top", QuickChar: 6, Callback: func() { - }}, &ui.ItemEntry{Name: "Split _Bottom", Callback: func() { + }}, &ui.ItemEntry{Name: "Split Bottom", QuickChar: 6, Callback: func() { - }}, &ui.ItemEntry{Name: "Split _Left", Callback: func() { + }}, &ui.ItemEntry{Name: "Split Left", QuickChar: 6, Callback: func() { - }}, &ui.ItemEntry{Name: "Split _Right", Callback: func() { + }}, &ui.ItemEntry{Name: "Split Right", QuickChar: 6, Callback: func() { - }}, &ui.ItemSeparator{}, &ui.ItemEntry{Name: "_Move", Shortcut: "Ctrl+M", Callback: func() { + }}, &ui.ItemSeparator{}, &ui.ItemEntry{Name: "Move", Shortcut: "Ctrl+M", Callback: func() { - }}, &ui.ItemEntry{Name: "_Resize", Shortcut: "Ctrl+R", Callback: func() { + }}, &ui.ItemEntry{Name: "Resize", Shortcut: "Ctrl+R", Callback: func() { - }}, &ui.ItemEntry{Name: "_Float", Callback: func() { + }}, &ui.ItemEntry{Name: "Float", Callback: func() { }}}) - editMenu := ui.NewMenu("_Edit", &theme) + editMenu := ui.NewMenu("Edit", 0, &theme) - editMenu.AddItems([]ui.Item{&ui.ItemEntry{Name: "_Cut", Shortcut: "Ctrl+X", Callback: func() { + editMenu.AddItems([]ui.Item{&ui.ItemEntry{Name: "Cut", Shortcut: "Ctrl+X", Callback: func() { if tabContainer.GetTabCount() > 0 { tab := tabContainer.GetTab(tabContainer.GetSelectedTabIdx()) te := tab.Child.(*ui.TextEdit) @@ -212,7 +212,7 @@ func main() { } changeFocus(tabContainer) } - }}, &ui.ItemEntry{Name: "_Copy", Shortcut: "Ctrl+C", Callback: func() { + }}, &ui.ItemEntry{Name: "Copy", Shortcut: "Ctrl+C", Callback: func() { if tabContainer.GetTabCount() > 0 { tab := tabContainer.GetTab(tabContainer.GetSelectedTabIdx()) te := tab.Child.(*ui.TextEdit) @@ -222,7 +222,7 @@ func main() { } changeFocus(tabContainer) } - }}, &ui.ItemEntry{Name: "_Paste", Shortcut: "Ctrl+V", Callback: func() { + }}, &ui.ItemEntry{Name: "Paste", Shortcut: "Ctrl+V", Callback: func() { if tabContainer.GetTabCount() > 0 { tab := tabContainer.GetTab(tabContainer.GetSelectedTabIdx()) te := tab.Child.(*ui.TextEdit) @@ -235,13 +235,13 @@ func main() { changeFocus(tabContainer) } - }}, &ui.ItemSeparator{}, &ui.ItemEntry{Name: "Select _All", Shortcut: "Ctrl+A", Callback: func() { + }}, &ui.ItemSeparator{}, &ui.ItemEntry{Name: "Select All", QuickChar: 7, Shortcut: "Ctrl+A", Callback: func() { - }}, &ui.ItemEntry{Name: "Select _Line", Callback: func() { + }}, &ui.ItemEntry{Name: "Select Line", QuickChar: 7, Callback: func() { }}}) - searchMenu := ui.NewMenu("_Search", &theme) + searchMenu := ui.NewMenu("Search", 0, &theme) searchMenu.AddItems([]ui.Item{&ui.ItemEntry{Name: "New", Callback: func() { s.Beep() diff --git a/ui/drawfunctions.go b/ui/drawfunctions.go index 574fa76..eab6c1c 100644 --- a/ui/drawfunctions.go +++ b/ui/drawfunctions.go @@ -1,6 +1,10 @@ package ui -import "github.com/gdamore/tcell/v2" +import ( + "unicode/utf8" + + "github.com/gdamore/tcell/v2" +) // DrawRect renders a filled box at `x` and `y`, of size `width` and `height`. // Will not call `Show()`. @@ -21,25 +25,26 @@ func DrawStr(s tcell.Screen, x, y int, str string, style tcell.Style) { } // DrawQuickCharStr renders a string very similar to how DrawStr works, but stylizes the -// quick char (any rune after an underscore) with an underline. Returned is the number of -// columns that were drawn to the screen. This is useful to know the length of the string -// drawn, minus the underscore. -func DrawQuickCharStr(s tcell.Screen, x, y int, str string, style tcell.Style) int { - runes := []rune(str) - col := 0 - for i := 0; i < len(runes); i++ { - r := runes[i] - if r == '_' && i+1 < len(runes) { - i++ - sty := style.Underline(true) - - s.SetContent(x+col, y, runes[i], nil, sty) - } else { - s.SetContent(x+col, y, r, nil, style) +// quick char (the rune at `quickCharIdx`) with an underline. Returned is the number of +// columns that were drawn to the screen. +func DrawQuickCharStr(s tcell.Screen, x, y int, str string, quickCharIdx int, style tcell.Style) int { + var col int + var runeIdx int + + bytes := []byte(str) + for i := 0; i < len(bytes); runeIdx++ { // i is a byte index + r, size := utf8.DecodeRune(bytes[i:]) + + sty := style + if runeIdx == quickCharIdx { + sty = style.Underline(true) } + s.SetContent(x+col, y, r, nil, sty) + + i += size col++ } - return col + return col // TODO: use mattn/runewidth } // DrawRectOutline draws only the outline of a rectangle, using `ul`, `ur`, `bl`, and `br` diff --git a/ui/menu.go b/ui/menu.go index 3acd42d..0ec1aca 100644 --- a/ui/menu.go +++ b/ui/menu.go @@ -11,6 +11,8 @@ import ( // Item is an interface implemented by ItemEntry and ItemMenu to be listed in Menus. type Item interface { GetName() string + // Returns a character/rune index of the name of the item. + GetQuickCharIdx() int // A Shortcut is a string of the modifiers+key name of the action that must be pressed // to trigger the shortcut. For example: "Ctrl+Alt+X". The order of the modifiers is // very important. Letters are case-sensitive. See the KeyEvent.Name() function of tcell @@ -27,15 +29,20 @@ func (i *ItemSeparator) GetName() string { return "" } +func (i *ItemSeparator) GetQuickCharIdx() int { + return 0 +} + func (i *ItemSeparator) GetShortcut() string { return "" } // ItemEntry is a listing in a Menu with a name and callback. type ItemEntry struct { - Name string - Shortcut string - Callback func() + Name string + QuickChar int // Character/rune index of Name + Shortcut string + Callback func() } // GetName returns the name of the ItemEntry. @@ -43,6 +50,10 @@ func (i *ItemEntry) GetName() string { return i.Name } +func (i *ItemEntry) GetQuickCharIdx() int { + return i.QuickChar +} + func (i *ItemEntry) GetShortcut() string { return i.Shortcut } @@ -52,6 +63,10 @@ func (m *Menu) GetName() string { return m.Name } +func (m *Menu) GetQuickCharIdx() int { + return m.QuickChar +} + func (m *Menu) GetShortcut() string { return "" } @@ -143,16 +158,15 @@ func (b *MenuBar) Draw(s tcell.Screen) { 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 - sty := normalStyle if b.focused && b.selected == i { sty = b.Theme.GetOrDefault("MenuBarSelected") // Use special style for selected item } - DrawQuickCharStr(s, col, b.y, str, sty) + str := fmt.Sprintf(" %s ", item.Name) + cols := DrawQuickCharStr(s, col, b.y, str, item.QuickChar+1, sty) - col += len(str) + col += cols } if b.menusVisible { @@ -245,8 +259,8 @@ func (b *MenuBar) HandleEvent(event tcell.Event) bool { case tcell.KeyRune: // Search for the matching quick char in menu names if !b.menusVisible { // If the selected Menu is not open/visible for i, m := range b.menus { - found, r := QuickCharInString(m.Name) - if found && r == ev.Rune() { + r := QuickCharInString(m.Name, m.QuickChar) + if r != 0 && r == ev.Rune() { b.selected = i // Select menu at i b.ActivateMenuUnderCursor() // Show menu break @@ -270,8 +284,9 @@ func (b *MenuBar) HandleEvent(event tcell.Event) bool { // A Menu contains one or more ItemEntry or ItemMenus. type Menu struct { - Name string - Items []Item + Name string + QuickChar int // Character/rune index of Name + Items []Item x, y int width, height int // Size may not be settable @@ -282,9 +297,10 @@ type Menu struct { } // New creates a new Menu. `items` can be `nil`. -func NewMenu(name string, theme *Theme) *Menu { +func NewMenu(name string, quickChar int, theme *Theme) *Menu { return &Menu{ Name: name, + QuickChar: quickChar, Items: make([]Item, 0, 6), Theme: theme, } @@ -364,10 +380,10 @@ func (m *Menu) Draw(s tcell.Screen) { sty = defaultStyle } - nameLen := DrawQuickCharStr(s, m.x+1, m.y+1+i, item.GetName(), sty) + nameCols := DrawQuickCharStr(s, m.x+1, m.y+1+i, item.GetName(), item.GetQuickCharIdx(), sty) - str := strings.Repeat(" ", m.width-2-nameLen) // Fill space after menu names to border - DrawStr(s, m.x+1+nameLen, m.y+1+i, str, sty) + str := strings.Repeat(" ", m.width-2-nameCols) // Fill space after menu names to border + DrawStr(s, m.x+1+nameCols, m.y+1+i, str, sty) if shortcut := item.GetShortcut(); len(shortcut) > 0 { // If the item has a shortcut... str := " " + shortcut + " " @@ -470,8 +486,8 @@ func (m *Menu) HandleEvent(event tcell.Event) bool { if m.selected == i { continue // Skip the item we're on } - found, r := QuickCharInString(item.GetName()) - if found && r == ev.Rune() { + r := QuickCharInString(item.GetName(), item.GetQuickCharIdx()) + if r != 0 && r == ev.Rune() { m.selected = i break } diff --git a/ui/util.go b/ui/util.go index 0fdcb4b..32da4e1 100644 --- a/ui/util.go +++ b/ui/util.go @@ -1,23 +1,29 @@ package ui -import "unicode" +import ( + "unicode" + "unicode/utf8" +) -// 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, ' ' - } - } +// QuickCharInString is used for finding the "quick char" in a string. The rune +// is always made lowercase. A rune of value zero is returned if the index was +// less than zero, or greater or equal to, the number of runes in s. +func QuickCharInString(s string, idx int) rune { + if idx < 0 { + return 0 } - return false, ' ' + + var runeIdx int + + bytes := []byte(s) + for i := 0; i < len(bytes); runeIdx++ { // i is a byte index + r, size := utf8.DecodeRune(bytes[i:]) + if runeIdx == idx { + return unicode.ToLower(r) + } + i += size + } + return 0 } // Max returns the larger integer.