Improve action menu escape handling
Make the F11 action menu close through a shared close action so Escape and the explicit Close actions menu item follow the same path. Treat Escape-prefixed Up and Down events as close inputs while the action menu is active, which handles termbox InputAlt behavior after pressing Escape. Preserve and restore the user/channel tree selection across action menu open and close, and preserve selection across live tree rebuilds. Tested with: GOCACHE=/tmp/barnard-go-cache go test ./...
This commit is contained in:
@@ -50,19 +50,21 @@ func (b *Barnard) OnAdminEscape(ui *uiterm.Ui, key uiterm.Key) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if b.Ui.Active() == uiViewAdmin {
|
if b.Ui.Active() == uiViewAdmin {
|
||||||
b.Ui.SetActive(uiViewTree)
|
b.CloseAdminMenu(ui)
|
||||||
width, height := termboxSize()
|
|
||||||
b.OnUiResize(ui, width, height)
|
|
||||||
b.AddOutputLine("Admin: closed")
|
b.AddOutputLine("Admin: closed")
|
||||||
ui.Refresh()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *Barnard) closeAdminAction() {
|
||||||
|
b.OnAdminEscape(b.Ui, uiterm.KeyEsc)
|
||||||
|
}
|
||||||
|
|
||||||
func (b *Barnard) OpenAdminMenu() {
|
func (b *Barnard) OpenAdminMenu() {
|
||||||
if b.Client == nil || b.Client.Self == nil {
|
if b.Client == nil || b.Client.Self == nil {
|
||||||
b.AddOutputLine("Admin: not connected")
|
b.AddOutputLine("Admin: not connected")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
b.adminReturnItem = b.UiTree.ActiveItem()
|
||||||
b.adminTargetUser = b.selectedUser
|
b.adminTargetUser = b.selectedUser
|
||||||
b.adminTargetChan = b.Client.Self.Channel
|
b.adminTargetChan = b.Client.Self.Channel
|
||||||
if b.Ui.Active() == uiViewTree {
|
if b.Ui.Active() == uiViewTree {
|
||||||
@@ -98,6 +100,15 @@ func (b *Barnard) OpenAdminMenu() {
|
|||||||
b.Ui.Refresh()
|
b.Ui.Refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *Barnard) CloseAdminMenu(ui *uiterm.Ui) {
|
||||||
|
b.Ui.SetActive(uiViewTree)
|
||||||
|
b.UiTree.SetActiveItem(b.adminReturnItem, sameUserChannelTreeItem)
|
||||||
|
b.adminReturnItem = nil
|
||||||
|
width, height := termboxSize()
|
||||||
|
b.OnUiResize(ui, width, height)
|
||||||
|
ui.Refresh()
|
||||||
|
}
|
||||||
|
|
||||||
func termboxSize() (int, int) {
|
func termboxSize() (int, int) {
|
||||||
width, height := termbox.Size()
|
width, height := termbox.Size()
|
||||||
return width, height
|
return width, height
|
||||||
@@ -140,11 +151,15 @@ func (b *Barnard) AdminItemBuild(item uiterm.TreeItem) []uiterm.TreeItem {
|
|||||||
if children := b.adminContextActionItems(); len(children) > 0 {
|
if children := b.adminContextActionItems(); len(children) > 0 {
|
||||||
items = append(items, adminItem{label: "Context actions", children: children})
|
items = append(items, adminItem{label: "Context actions", children: children})
|
||||||
}
|
}
|
||||||
items = append(items, adminItem{label: "Close actions menu", action: func() { b.OnAdminEscape(b.Ui, uiterm.KeyEsc) }})
|
items = append(items, adminItem{label: "Close actions menu", action: b.closeAdminAction})
|
||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) AdminItemKeyPress(ui *uiterm.Ui, tree *uiterm.Tree, item uiterm.TreeItem, key uiterm.Key) {
|
func (b *Barnard) AdminItemKeyPress(ui *uiterm.Ui, tree *uiterm.Tree, item uiterm.TreeItem, key uiterm.Key) {
|
||||||
|
if isAdminEscapeKey(key) {
|
||||||
|
b.OnAdminEscape(ui, key)
|
||||||
|
return
|
||||||
|
}
|
||||||
if key != uiterm.KeyEnter {
|
if key != uiterm.KeyEnter {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -158,6 +173,22 @@ func (b *Barnard) AdminItemKeyPress(ui *uiterm.Ui, tree *uiterm.Tree, item uiter
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) AdminItemCharacter(ui *uiterm.Ui, tree *uiterm.Tree, item uiterm.TreeItem, ch rune) {
|
func (b *Barnard) AdminItemCharacter(ui *uiterm.Ui, tree *uiterm.Tree, item uiterm.TreeItem, ch rune) {
|
||||||
|
if isAdminEscapeCharacter(ch) {
|
||||||
|
b.OnAdminEscape(ui, uiterm.KeyEsc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isAdminEscapeKey(key uiterm.Key) bool {
|
||||||
|
switch key {
|
||||||
|
case uiterm.KeyEsc, uiterm.KeyAltEsc, uiterm.KeyAltArrowUp, uiterm.KeyAltArrowDown:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isAdminEscapeCharacter(ch rune) bool {
|
||||||
|
return ch == rune(uiterm.KeyEsc)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) informationItems() []uiterm.TreeItem {
|
func (b *Barnard) informationItems() []uiterm.TreeItem {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.stormux.org/storm/barnard/gumble/gumble"
|
"git.stormux.org/storm/barnard/gumble/gumble"
|
||||||
|
"git.stormux.org/storm/barnard/uiterm"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseToggleState(t *testing.T) {
|
func TestParseToggleState(t *testing.T) {
|
||||||
@@ -63,3 +64,27 @@ func TestPermissionList(t *testing.T) {
|
|||||||
t.Fatalf("expected write,ban, got %q", got)
|
t.Fatalf("expected write,ban, got %q", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAdminEscapeInputs(t *testing.T) {
|
||||||
|
if !isAdminEscapeKey(uiterm.KeyEsc) {
|
||||||
|
t.Fatal("expected escape key to close admin menu")
|
||||||
|
}
|
||||||
|
if !isAdminEscapeKey(uiterm.KeyAltEsc) {
|
||||||
|
t.Fatal("expected alt escape key to close admin menu")
|
||||||
|
}
|
||||||
|
if !isAdminEscapeKey(uiterm.KeyAltArrowUp) {
|
||||||
|
t.Fatal("expected escape-prefixed up arrow to close admin menu")
|
||||||
|
}
|
||||||
|
if !isAdminEscapeKey(uiterm.KeyAltArrowDown) {
|
||||||
|
t.Fatal("expected escape-prefixed down arrow to close admin menu")
|
||||||
|
}
|
||||||
|
if isAdminEscapeKey(uiterm.KeyEnter) {
|
||||||
|
t.Fatal("expected enter key not to close admin menu")
|
||||||
|
}
|
||||||
|
if !isAdminEscapeCharacter(rune(uiterm.KeyEsc)) {
|
||||||
|
t.Fatal("expected escape character to close admin menu")
|
||||||
|
}
|
||||||
|
if isAdminEscapeCharacter('x') {
|
||||||
|
t.Fatal("expected non-escape character not to close admin menu")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+3
-2
@@ -43,6 +43,7 @@ type Barnard struct {
|
|||||||
selectedUser *gumble.User
|
selectedUser *gumble.User
|
||||||
adminTargetUser *gumble.User
|
adminTargetUser *gumble.User
|
||||||
adminTargetChan *gumble.Channel
|
adminTargetChan *gumble.Channel
|
||||||
|
adminReturnItem uiterm.TreeItem
|
||||||
statusText string
|
statusText string
|
||||||
statusNotice bool
|
statusNotice bool
|
||||||
|
|
||||||
@@ -144,7 +145,7 @@ func (b *Barnard) TreeItemKeyPress(ui *uiterm.Ui, tree *uiterm.Tree, item uiterm
|
|||||||
delete(b.MutedChannels, treeItem.Channel.ID)
|
delete(b.MutedChannels, treeItem.Channel.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
b.UiTree.Rebuild()
|
b.RebuildUserChannelTreePreservingSelection()
|
||||||
b.Ui.Refresh()
|
b.Ui.Refresh()
|
||||||
}
|
}
|
||||||
if key == *b.Hotkeys.VolumeDown {
|
if key == *b.Hotkeys.VolumeDown {
|
||||||
@@ -169,7 +170,7 @@ func (b *Barnard) TreeItemKeyPress(ui *uiterm.Ui, tree *uiterm.Tree, item uiterm
|
|||||||
treeItem.User.AudioSource.SetGain(treeItem.User.Volume)
|
treeItem.User.AudioSource.SetGain(treeItem.User.Volume)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
b.UiTree.Rebuild()
|
b.RebuildUserChannelTreePreservingSelection()
|
||||||
b.Ui.Refresh()
|
b.Ui.Refresh()
|
||||||
}
|
}
|
||||||
if key == *b.Hotkeys.VolumeDown {
|
if key == *b.Hotkeys.VolumeDown {
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ func (b *Barnard) OnUserChange(e *gumble.UserChangeEvent) {
|
|||||||
b.AddOutputLine(formatUserStats(e.User))
|
b.AddOutputLine(formatUserStats(e.User))
|
||||||
}
|
}
|
||||||
b.updateUserChannel(e)
|
b.updateUserChannel(e)
|
||||||
b.UiTree.Rebuild()
|
b.RebuildUserChannelTreePreservingSelection()
|
||||||
b.Ui.Refresh()
|
b.Ui.Refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,7 +317,7 @@ func (b *Barnard) OnChannelChange(e *gumble.ChannelChangeEvent) {
|
|||||||
b.AddOutputLine(fmt.Sprintf("Channel permissions for %s: %s", e.Channel.Name, permissionList(*permission)))
|
b.AddOutputLine(fmt.Sprintf("Channel permissions for %s: %s", e.Channel.Name, permissionList(*permission)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
b.UiTree.Rebuild()
|
b.RebuildUserChannelTreePreservingSelection()
|
||||||
b.Ui.Refresh()
|
b.Ui.Refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ func (b *Barnard) UpdateInputStatus(status string) {
|
|||||||
status = status[:17] + "..." + "]"
|
status = status[:17] + "..." + "]"
|
||||||
}
|
}
|
||||||
b.UiInputStatus.Text = status
|
b.UiInputStatus.Text = status
|
||||||
b.UiTree.Rebuild()
|
b.RebuildUserChannelTreePreservingSelection()
|
||||||
b.Ui.Refresh()
|
b.Ui.Refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+150
-128
@@ -1,155 +1,177 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"git.stormux.org/storm/barnard/gumble/gumble"
|
"git.stormux.org/storm/barnard/gumble/gumble"
|
||||||
"git.stormux.org/storm/barnard/uiterm"
|
"git.stormux.org/storm/barnard/uiterm"
|
||||||
"sort"
|
"sort"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ti TreeItem) String() string {
|
func (ti TreeItem) String() string {
|
||||||
if ti.User != nil {
|
if ti.User != nil {
|
||||||
if ti.User.LocallyMuted {
|
if ti.User.LocallyMuted {
|
||||||
return "[MUTED] " + ti.User.Name
|
return "[MUTED] " + ti.User.Name
|
||||||
}
|
}
|
||||||
// Calculate total volume as percentage
|
// Calculate total volume as percentage
|
||||||
boostPercent := float32(ti.User.Boost-1) * 10
|
boostPercent := float32(ti.User.Boost-1) * 10
|
||||||
totalVolume := ti.User.Volume*100 + boostPercent
|
totalVolume := ti.User.Volume*100 + boostPercent
|
||||||
return fmt.Sprintf("%s [%.0f%%]", ti.User.Name, totalVolume)
|
return fmt.Sprintf("%s [%.0f%%]", ti.User.Name, totalVolume)
|
||||||
}
|
}
|
||||||
if ti.Channel != nil {
|
if ti.Channel != nil {
|
||||||
return "#" + ti.Channel.Name
|
return "#" + ti.Channel.Name
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ti TreeItem) TreeItemStyle(fg, bg uiterm.Attribute, active bool) (uiterm.Attribute, uiterm.Attribute) {
|
func (ti TreeItem) TreeItemStyle(fg, bg uiterm.Attribute, active bool) (uiterm.Attribute, uiterm.Attribute) {
|
||||||
if ti.Channel != nil {
|
if ti.Channel != nil {
|
||||||
fg |= uiterm.AttrBold
|
fg |= uiterm.AttrBold
|
||||||
}
|
}
|
||||||
if active {
|
if active {
|
||||||
fg, bg = bg, fg
|
fg, bg = bg, fg
|
||||||
}
|
}
|
||||||
return fg, bg
|
return fg, bg
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) changeVolume(users []*gumble.User, change float32) {
|
func (b *Barnard) changeVolume(users []*gumble.User, change float32) {
|
||||||
for _, u := range users {
|
for _, u := range users {
|
||||||
au := u.AudioSource
|
au := u.AudioSource
|
||||||
if au == nil {
|
if au == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
var boost uint16
|
var boost uint16
|
||||||
var cv float32
|
var cv float32
|
||||||
var ng float32
|
var ng float32
|
||||||
var curboost float32
|
var curboost float32
|
||||||
curboost = float32((u.Boost - 1)) / 10
|
curboost = float32((u.Boost - 1)) / 10
|
||||||
cv = au.GetGain() + curboost
|
cv = au.GetGain() + curboost
|
||||||
ng = cv + change
|
ng = cv + change
|
||||||
boost = uint16(1)
|
boost = uint16(1)
|
||||||
if ng > 1.0 {
|
if ng > 1.0 {
|
||||||
perc := uint16((ng * 10)) - 10
|
perc := uint16((ng * 10)) - 10
|
||||||
perc += 1
|
perc += 1
|
||||||
boost = perc
|
boost = perc
|
||||||
ng = 1.0
|
ng = 1.0
|
||||||
}
|
}
|
||||||
if ng < 0 {
|
if ng < 0 {
|
||||||
ng = 0.0
|
ng = 0.0
|
||||||
}
|
}
|
||||||
u.Boost = boost
|
u.Boost = boost
|
||||||
u.Volume = ng
|
u.Volume = ng
|
||||||
if !u.LocallyMuted {
|
if !u.LocallyMuted {
|
||||||
au.SetGain(ng)
|
au.SetGain(ng)
|
||||||
}
|
}
|
||||||
b.UserConfig.UpdateConfig(u)
|
b.UserConfig.UpdateConfig(u)
|
||||||
}
|
}
|
||||||
b.UserConfig.SaveConfig()
|
b.UserConfig.SaveConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) resetVolume(users []*gumble.User) {
|
func (b *Barnard) resetVolume(users []*gumble.User) {
|
||||||
for _, u := range users {
|
for _, u := range users {
|
||||||
au := u.AudioSource
|
au := u.AudioSource
|
||||||
if au == nil {
|
if au == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Reset to original volume (1.0) and boost (1)
|
// Reset to original volume (1.0) and boost (1)
|
||||||
u.Boost = uint16(1)
|
u.Boost = uint16(1)
|
||||||
u.Volume = 1.0
|
u.Volume = 1.0
|
||||||
if !u.LocallyMuted {
|
if !u.LocallyMuted {
|
||||||
au.SetGain(1.0)
|
au.SetGain(1.0)
|
||||||
}
|
}
|
||||||
b.UserConfig.UpdateConfig(u)
|
b.UserConfig.UpdateConfig(u)
|
||||||
}
|
}
|
||||||
b.UserConfig.SaveConfig()
|
b.UserConfig.SaveConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeUsersArray(users gumble.Users) []*gumble.User {
|
func makeUsersArray(users gumble.Users) []*gumble.User {
|
||||||
t := make([]*gumble.User, 0, len(users))
|
t := make([]*gumble.User, 0, len(users))
|
||||||
for _, u := range users {
|
for _, u := range users {
|
||||||
t = append(t, u)
|
t = append(t, u)
|
||||||
}
|
}
|
||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Barnard) TreeItemBuild(item uiterm.TreeItem) []uiterm.TreeItem {
|
func (b *Barnard) TreeItemBuild(item uiterm.TreeItem) []uiterm.TreeItem {
|
||||||
if b.Client == nil {
|
if b.Client == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var treeItem TreeItem
|
var treeItem TreeItem
|
||||||
if ti, ok := item.(TreeItem); !ok {
|
if ti, ok := item.(TreeItem); !ok {
|
||||||
root := b.Client.Channels[0]
|
root := b.Client.Channels[0]
|
||||||
if root == nil {
|
if root == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return []uiterm.TreeItem{
|
return []uiterm.TreeItem{
|
||||||
TreeItem{
|
TreeItem{
|
||||||
Channel: root,
|
Channel: root,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
treeItem = ti
|
treeItem = ti
|
||||||
}
|
}
|
||||||
|
|
||||||
if treeItem.User != nil {
|
if treeItem.User != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
users := []uiterm.TreeItem{}
|
users := []uiterm.TreeItem{}
|
||||||
ul := []*gumble.User{}
|
ul := []*gumble.User{}
|
||||||
for _, user := range treeItem.Channel.Users {
|
for _, user := range treeItem.Channel.Users {
|
||||||
ul = append(ul, user)
|
ul = append(ul, user)
|
||||||
var u = ul[len(ul)-1]
|
var u = ul[len(ul)-1]
|
||||||
_ = u
|
_ = u
|
||||||
}
|
}
|
||||||
sort.Slice(ul, func(i, j int) bool {
|
sort.Slice(ul, func(i, j int) bool {
|
||||||
return ul[i].Name < ul[j].Name
|
return ul[i].Name < ul[j].Name
|
||||||
})
|
})
|
||||||
for _, user := range ul {
|
for _, user := range ul {
|
||||||
users = append(users, TreeItem{
|
users = append(users, TreeItem{
|
||||||
User: user,
|
User: user,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
channels := []uiterm.TreeItem{}
|
channels := []uiterm.TreeItem{}
|
||||||
cl := []*gumble.Channel{}
|
cl := []*gumble.Channel{}
|
||||||
for _, subchannel := range treeItem.Channel.Children {
|
for _, subchannel := range treeItem.Channel.Children {
|
||||||
cl = append(cl, subchannel)
|
cl = append(cl, subchannel)
|
||||||
}
|
}
|
||||||
sort.Slice(cl, func(i, j int) bool {
|
sort.Slice(cl, func(i, j int) bool {
|
||||||
return cl[i].Name < cl[j].Name
|
return cl[i].Name < cl[j].Name
|
||||||
})
|
})
|
||||||
for _, subchannel := range cl {
|
for _, subchannel := range cl {
|
||||||
displayName := subchannel.Name
|
displayName := subchannel.Name
|
||||||
if b.MutedChannels[subchannel.ID] {
|
if b.MutedChannels[subchannel.ID] {
|
||||||
displayName = "[MUTED] #" + displayName
|
displayName = "[MUTED] #" + displayName
|
||||||
} else {
|
} else {
|
||||||
displayName = "#" + displayName
|
displayName = "#" + displayName
|
||||||
}
|
}
|
||||||
channels = append(channels, TreeItem{
|
channels = append(channels, TreeItem{
|
||||||
Channel: subchannel,
|
Channel: subchannel,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return append(users, channels...)
|
return append(users, channels...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Barnard) RebuildUserChannelTreePreservingSelection() {
|
||||||
|
b.UiTree.RebuildPreservingActiveItem(sameUserChannelTreeItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sameUserChannelTreeItem(previous, current uiterm.TreeItem) bool {
|
||||||
|
prev, ok := previous.(TreeItem)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
cur, ok := current.(TreeItem)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if prev.User != nil && cur.User != nil {
|
||||||
|
return prev.User.Session == cur.User.Session
|
||||||
|
}
|
||||||
|
if prev.Channel != nil && cur.Channel != nil {
|
||||||
|
return prev.Channel.ID == cur.Channel.ID
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
+40
-4
@@ -59,11 +59,34 @@ func (t *Tree) uiSetBounds(x0, y0, x1, y1 int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tree) Rebuild() {
|
func (t *Tree) Rebuild() {
|
||||||
|
t.rebuild(false, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tree) RebuildPreservingActiveItem(sameItem func(previous, current TreeItem) bool) {
|
||||||
|
t.rebuild(true, sameItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tree) SetActiveItem(target TreeItem, sameItem func(previous, current TreeItem) bool) bool {
|
||||||
|
if target == nil || sameItem == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for line, item := range t.lines {
|
||||||
|
if sameItem(target, item.Item) {
|
||||||
|
t.SetActiveLine(line, false)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tree) rebuild(preserveActive bool, sameItem func(previous, current TreeItem) bool) {
|
||||||
if t.Generator == nil {
|
if t.Generator == nil {
|
||||||
t.lines = []renderedTreeItem{}
|
t.lines = []renderedTreeItem{}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
previousItem := t.ActiveItem()
|
||||||
|
previousLine := t.activeLine
|
||||||
lines := []renderedTreeItem{}
|
lines := []renderedTreeItem{}
|
||||||
for _, item := range t.Generator(nil) {
|
for _, item := range t.Generator(nil) {
|
||||||
children := t.rebuild_rec(item, 0)
|
children := t.rebuild_rec(item, 0)
|
||||||
@@ -72,8 +95,22 @@ func (t *Tree) Rebuild() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
t.lines = lines
|
t.lines = lines
|
||||||
t.SetActiveLine(0, false)
|
if preserveActive {
|
||||||
t.uiDraw()
|
t.SetActiveLine(previousLine, false)
|
||||||
|
if previousItem != nil && sameItem != nil {
|
||||||
|
for line, item := range t.lines {
|
||||||
|
if sameItem(previousItem, item.Item) {
|
||||||
|
t.SetActiveLine(line, false)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t.SetActiveLine(0, false)
|
||||||
|
}
|
||||||
|
if t.ui != nil {
|
||||||
|
t.uiDraw()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tree) rebuild_rec(parent TreeItem, level int) []renderedTreeItem {
|
func (t *Tree) rebuild_rec(parent TreeItem, level int) []renderedTreeItem {
|
||||||
@@ -138,11 +175,10 @@ func (t *Tree) uiDraw() {
|
|||||||
fg := t.Fg
|
fg := t.Fg
|
||||||
bg := t.Bg
|
bg := t.Bg
|
||||||
dx := x - t.x0
|
dx := x - t.x0
|
||||||
dy := y - t.y0
|
|
||||||
if reader != nil && level*2 <= dx {
|
if reader != nil && level*2 <= dx {
|
||||||
if ch, _, err := reader.ReadRune(); err == nil {
|
if ch, _, err := reader.ReadRune(); err == nil {
|
||||||
chr = ch
|
chr = ch
|
||||||
fg, bg = item.TreeItemStyle(fg, bg, t.active && t.activeLine == dy)
|
fg, bg = item.TreeItemStyle(fg, bg, t.active && t.activeLine == line)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
termbox.SetCell(x, y, chr, termbox.Attribute(fg), termbox.Attribute(bg))
|
termbox.SetCell(x, y, chr, termbox.Attribute(fg), termbox.Attribute(bg))
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package uiterm
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
type testTreeItem string
|
||||||
|
|
||||||
|
func (i testTreeItem) String() string {
|
||||||
|
return string(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i testTreeItem) TreeItemStyle(fg, bg Attribute, active bool) (Attribute, Attribute) {
|
||||||
|
return fg, bg
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTreeRebuildResetsActiveLine(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tree := Tree{
|
||||||
|
Generator: func(item TreeItem) []TreeItem {
|
||||||
|
if item != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []TreeItem{testTreeItem("one"), testTreeItem("two"), testTreeItem("three")}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
tree.Rebuild()
|
||||||
|
tree.SetActiveLine(2, false)
|
||||||
|
tree.Rebuild()
|
||||||
|
|
||||||
|
if tree.activeLine != 0 {
|
||||||
|
t.Fatalf("expected active line to reset to 0, got %d", tree.activeLine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTreeRebuildPreservingActiveItemKeepsMatchingItem(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
items := []TreeItem{testTreeItem("one"), testTreeItem("two"), testTreeItem("three")}
|
||||||
|
tree := Tree{
|
||||||
|
Generator: func(item TreeItem) []TreeItem {
|
||||||
|
if item != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
tree.Rebuild()
|
||||||
|
tree.SetActiveLine(1, false)
|
||||||
|
items = []TreeItem{testTreeItem("three"), testTreeItem("one"), testTreeItem("two")}
|
||||||
|
tree.RebuildPreservingActiveItem(func(previous, current TreeItem) bool {
|
||||||
|
return previous.String() == current.String()
|
||||||
|
})
|
||||||
|
|
||||||
|
if tree.activeLine != 2 {
|
||||||
|
t.Fatalf("expected active line to follow matching item to line 2, got %d", tree.activeLine)
|
||||||
|
}
|
||||||
|
if got := tree.ActiveItem().String(); got != "two" {
|
||||||
|
t.Fatalf("expected active item two, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTreeSetActiveItemSelectsMatchingItem(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tree := Tree{
|
||||||
|
Generator: func(item TreeItem) []TreeItem {
|
||||||
|
if item != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []TreeItem{testTreeItem("one"), testTreeItem("two"), testTreeItem("three")}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
tree.Rebuild()
|
||||||
|
if ok := tree.SetActiveItem(testTreeItem("three"), func(previous, current TreeItem) bool {
|
||||||
|
return previous.String() == current.String()
|
||||||
|
}); !ok {
|
||||||
|
t.Fatal("expected matching item to be selected")
|
||||||
|
}
|
||||||
|
|
||||||
|
if tree.activeLine != 2 {
|
||||||
|
t.Fatalf("expected active line 2, got %d", tree.activeLine)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user