F1-F5 leave full-screen views; dialogs still block them
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
+27
-8
@@ -58,6 +58,8 @@ type overlay struct {
|
|||||||
page string
|
page string
|
||||||
focus tview.Primitive
|
focus tview.Primitive
|
||||||
hints string
|
hints string
|
||||||
|
full bool // full-screen view (can be left with F1-F5), as opposed to a dialog
|
||||||
|
close func() // optional cleanup when the overlay is removed
|
||||||
}
|
}
|
||||||
|
|
||||||
// New builds the application shell: a tab bar, one page per view and a status bar.
|
// New builds the application shell: a tab bar, one page per view and a status bar.
|
||||||
@@ -185,9 +187,9 @@ func (a *App) renderStatus() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// pushOverlay stacks p above everything and focuses focus.
|
// pushOverlay stacks p above everything and focuses focus.
|
||||||
func (a *App) pushOverlay(p, focus tview.Primitive, hints string) {
|
func (a *App) pushOverlay(p, focus tview.Primitive, hints string, full bool, onClose func()) {
|
||||||
name := fmt.Sprintf("overlay-%d", len(a.overlays))
|
name := fmt.Sprintf("overlay-%d", len(a.overlays))
|
||||||
a.overlays = append(a.overlays, overlay{page: name, focus: a.app.GetFocus(), hints: hints})
|
a.overlays = append(a.overlays, overlay{page: name, focus: a.app.GetFocus(), hints: hints, full: full, close: onClose})
|
||||||
a.pages.AddPage(name, p, true, true)
|
a.pages.AddPage(name, p, true, true)
|
||||||
a.app.SetFocus(focus)
|
a.app.SetFocus(focus)
|
||||||
a.renderStatus()
|
a.renderStatus()
|
||||||
@@ -202,12 +204,12 @@ func (a *App) openModal(p tview.Primitive, width, height int) {
|
|||||||
AddItem(p, height, 0, true).
|
AddItem(p, height, 0, true).
|
||||||
AddItem(nil, 0, 1, false), width, 0, true).
|
AddItem(nil, 0, 1, false), width, 0, true).
|
||||||
AddItem(nil, 0, 1, false)
|
AddItem(nil, 0, 1, false)
|
||||||
a.pushOverlay(box, p, "↑↓ move · ←→ change · Enter select · Esc cancel")
|
a.pushOverlay(box, p, "↑↓ move · ←→ change · Enter select · Esc cancel", false, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// openFull shows p above the whole view area (the tab bar and status bar stay visible).
|
// openFull shows p above the whole view area (the tab bar and status bar stay visible).
|
||||||
func (a *App) openFull(p tview.Primitive, hints string) {
|
func (a *App) openFull(p tview.Primitive, hints string, onClose func()) {
|
||||||
a.pushOverlay(p, p, hints)
|
a.pushOverlay(p, p, hints, true, onClose)
|
||||||
}
|
}
|
||||||
|
|
||||||
// closeModal removes the topmost overlay and restores the previous focus.
|
// closeModal removes the topmost overlay and restores the previous focus.
|
||||||
@@ -218,6 +220,9 @@ func (a *App) closeModal() {
|
|||||||
top := a.overlays[len(a.overlays)-1]
|
top := a.overlays[len(a.overlays)-1]
|
||||||
a.overlays = a.overlays[:len(a.overlays)-1]
|
a.overlays = a.overlays[:len(a.overlays)-1]
|
||||||
a.pages.RemovePage(top.page)
|
a.pages.RemovePage(top.page)
|
||||||
|
if top.close != nil {
|
||||||
|
top.close()
|
||||||
|
}
|
||||||
if top.focus != nil {
|
if top.focus != nil {
|
||||||
a.app.SetFocus(top.focus)
|
a.app.SetFocus(top.focus)
|
||||||
}
|
}
|
||||||
@@ -239,6 +244,16 @@ func (a *App) confirm(text string, yes func()) {
|
|||||||
a.openModal(m, 60, 7+min(lines, 10))
|
a.openModal(m, 60, 7+min(lines, 10))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// onlyFullOverlays is true when no dialog is open (full-screen views may be).
|
||||||
|
func (a *App) onlyFullOverlays() bool {
|
||||||
|
for _, o := range a.overlays {
|
||||||
|
if !o.full {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// typing reports whether keystrokes belong to a text field or dialog and must
|
// typing reports whether keystrokes belong to a text field or dialog and must
|
||||||
// not trigger global shortcuts.
|
// not trigger global shortcuts.
|
||||||
func (a *App) typing() bool {
|
func (a *App) typing() bool {
|
||||||
@@ -254,13 +269,17 @@ func (a *App) handleKey(ev *tcell.EventKey) *tcell.EventKey {
|
|||||||
a.app.Stop()
|
a.app.Stop()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// Shortcuts that also work while typing in a search box. Not while a dialog is open.
|
// Shortcuts that also work while typing in a search box. Dialogs block them;
|
||||||
if len(a.overlays) == 0 {
|
// full-screen views (release list, episodes) are closed by a view switch.
|
||||||
if ev.Key() == tcell.KeyF9 {
|
if a.onlyFullOverlays() {
|
||||||
|
if ev.Key() == tcell.KeyF9 && len(a.overlays) == 0 {
|
||||||
a.showHelp()
|
a.showHelp()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if i := a.viewShortcut(ev); i >= 0 && i < len(a.views) {
|
if i := a.viewShortcut(ev); i >= 0 && i < len(a.views) {
|
||||||
|
for len(a.overlays) > 0 {
|
||||||
|
a.closeModal()
|
||||||
|
}
|
||||||
a.switchTo(i)
|
a.switchTo(i)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ func (a *App) openSeries(c *sonarr.Client, s sonarr.Series, onChange func()) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
v.render()
|
v.render()
|
||||||
a.openFull(v.root, "Enter expand/releases · i releases · s search · t monitor · r reload · Esc back")
|
a.openFull(v.root, "Enter expand/releases · i releases · s search · t monitor · r reload · Esc back", nil)
|
||||||
v.reload()
|
v.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const helpText = `[yellow]Global[-]
|
const helpText = `[yellow]Global[-]
|
||||||
F1-F5 switch view
|
F1-F5 switch view (also from the release and episode lists)
|
||||||
Tab, Shift+Tab switch panel within a view
|
Tab, Shift+Tab switch panel within a view
|
||||||
?, F9 this help (F9 works while typing)
|
?, F9 this help (F9 works while typing)
|
||||||
q, Ctrl+C quit
|
q, Ctrl+C quit
|
||||||
|
|||||||
@@ -24,3 +24,18 @@ func TestToastProgress(t *testing.T) {
|
|||||||
t.Error("progress detection wrong")
|
t.Error("progress detection wrong")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOnlyFullOverlays(t *testing.T) {
|
||||||
|
a := &App{}
|
||||||
|
if !a.onlyFullOverlays() {
|
||||||
|
t.Error("no overlays: view shortcuts must work")
|
||||||
|
}
|
||||||
|
a.overlays = []overlay{{full: true}, {full: true}}
|
||||||
|
if !a.onlyFullOverlays() {
|
||||||
|
t.Error("full-screen views must not block view shortcuts")
|
||||||
|
}
|
||||||
|
a.overlays = append(a.overlays, overlay{})
|
||||||
|
if a.onlyFullOverlays() {
|
||||||
|
t.Error("a dialog must block view shortcuts")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ func (a *App) openReleases(title string, fetch func(context.Context) ([]arr.Rele
|
|||||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
|
||||||
v.cancel = cancel
|
v.cancel = cancel
|
||||||
v.render() // shows the "searching" placeholder
|
v.render() // shows the "searching" placeholder
|
||||||
a.openFull(v.root, "Enter/g grab · s sort · f hide rejected · Esc back")
|
a.openFull(v.root, "Enter/g grab · s sort · f hide rejected · Esc back", v.cancel)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
releases, err := fetch(ctx)
|
releases, err := fetch(ctx)
|
||||||
@@ -102,8 +102,7 @@ func (a *App) openReleases(title string, fetch func(context.Context) ([]arr.Rele
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (v *releasesView) close() {
|
func (v *releasesView) close() {
|
||||||
v.cancel()
|
v.app.closeModal() // cancels the running search via the overlay's cleanup
|
||||||
v.app.closeModal()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *releasesView) setReleases(releases []arr.Release) {
|
func (v *releasesView) setReleases(releases []arr.Release) {
|
||||||
|
|||||||
Reference in New Issue
Block a user