remove operação remove, corrige bug de focus

This commit is contained in:
2026-09-08 11:16:15 -03:00
parent bf46c56b36
commit 94833d6f43
5 changed files with 128 additions and 38 deletions
+5 -1
View File
@@ -272,7 +272,11 @@ func (a *App) buildControls(ctx context.Context) {
func (a *App) globalKeys(ctx context.Context) func(*tcell.EventKey) *tcell.EventKey {
return func(event *tcell.EventKey) *tcell.EventKey {
if name, _ := a.pages.GetFrontPage(); name != pageMain {
// A dialog is open; it handles its own keys.
// A dialog is open. Restore focus to its modal layer before
// dispatch so keys can never reach a page underneath it.
if page := a.pages.GetPage(name); page != nil && !page.HasFocus() {
a.app.SetFocus(page)
}
return event
}
switch event.Key() {
+48
View File
@@ -240,6 +240,54 @@ func TestAppCancelledDialogLeavesNoOperation(t *testing.T) {
})
}
func TestOperationDialogTrapsKeyboardAndMouseFocus(t *testing.T) {
dir := makeDir(t, "one.txt", "two.txt")
app, screen, onLoop := startApp(t, dir)
ctx := context.Background()
onLoop(func() {
app.openOperationDialog(ctx, ops.KindReplace, ops.DefaultConfig(ops.KindReplace), -1)
// Simulate focus being stolen by an underlying widget. The global key
// guard must restore it before dispatching this key.
app.app.SetFocus(app.filePane)
})
screen.InjectKey(tcell.KeyRune, 'x', tcell.ModNone)
waitForState(t, onLoop, "keyboard focus to return to the operation dialog", func() bool {
page := app.pages.GetPage(pageOperation)
return page != nil && page.HasFocus() && len(app.names) == 2
})
// The folder field lies outside the centered form. Clicking it must be
// consumed by the modal layer rather than focusing the underlying input.
screen.InjectMouse(5, 1, tcell.Button1, tcell.ModNone)
screen.InjectMouse(5, 1, tcell.ButtonNone, tcell.ModNone)
waitForState(t, onLoop, "mouse focus to remain in the operation dialog", func() bool {
page := app.pages.GetPage(pageOperation)
return page != nil && page.HasFocus() && !app.dirInput.HasFocus()
})
}
func TestMessageDialogTrapsFocusUntilDismissed(t *testing.T) {
dir := makeDir(t, "one.txt")
app, screen, onLoop := startApp(t, dir)
onLoop(func() {
app.showMessage("Notice", "Stay modal")
app.app.SetFocus(app.filePane)
})
screen.InjectKey(tcell.KeyRune, 'x', tcell.ModNone)
waitForState(t, onLoop, "focus to return to the message", func() bool {
page := app.pages.GetPage(pageMessage)
return page != nil && page.HasFocus() && len(app.names) == 1
})
screen.InjectKey(tcell.KeyEscape, 0, tcell.ModNone)
waitForState(t, onLoop, "the message to be dismissed", func() bool {
name, _ := app.pages.GetFrontPage()
return name == pageMain
})
}
func TestAppRemovesFilesAndOperations(t *testing.T) {
dir := makeDir(t, "one.txt", "two.txt")
app, screen, onLoop := startApp(t, dir)
+43 -18
View File
@@ -67,19 +67,13 @@ func (a *App) openOperationDialog(ctx context.Context, kind ops.Kind, cfg ops.Co
}
switch kind {
case ops.KindReplace, ops.KindRemove:
target := "Replace target"
if kind == ops.KindRemove {
target = "Remove target"
}
text(target, cfg.Target, func(v string) { cfg.Target = v })
if kind == ops.KindReplace {
text("Replacement", cfg.Replacement, func(v string) { cfg.Replacement = v })
}
case ops.KindReplace:
text("Replace target", cfg.Target, func(v string) { cfg.Target = v })
text("Replacement", cfg.Replacement, func(v string) { cfg.Replacement = v })
number("Limit (0 = all)", cfg.Limit, acceptInt, func(v string) { cfg.Limit = v })
check("From start", cfg.FromStart, func(v bool) { cfg.FromStart = v })
check("Case sensitive", cfg.CaseSensitive, func(v bool) { cfg.CaseSensitive = v })
check(strings.ToLower(string(kind))+" target is regex", cfg.Regex, func(v bool) { cfg.Regex = v })
check("Replace target is regex", cfg.Regex, func(v bool) { cfg.Regex = v })
check("Preserve extension", cfg.PreserveExt, func(v bool) { cfg.PreserveExt = v })
case ops.KindInsert:
@@ -202,9 +196,7 @@ func (a *App) confirmApply(ctx context.Context) {
})
modal.SetInputCapture(dismissOnEscape(func() { a.closeDialog(pageConfirm) }))
a.dialogFocus[pageConfirm] = a.app.GetFocus()
a.pages.AddPage(pageConfirm, modal, true, true)
a.app.SetFocus(modal)
a.showOverlay(pageConfirm, modal, modal)
}
// applyRename performs the rename on disk and reloads the folder.
@@ -230,9 +222,7 @@ func (a *App) showMessage(title, body string) {
SetDoneFunc(func(int, string) { a.closeDialog(pageMessage) })
modal.SetInputCapture(dismissOnEscape(func() { a.closeDialog(pageMessage) }))
a.dialogFocus[pageMessage] = a.app.GetFocus()
a.pages.AddPage(pageMessage, modal, true, true)
a.app.SetFocus(modal)
a.showOverlay(pageMessage, modal, modal)
}
// helpText lists every key binding of the interface.
@@ -292,9 +282,44 @@ func dismissOnEscape(close func()) func(*tcell.EventKey) *tcell.EventKey {
// showDialog puts p on top of the main layout as a centred floating window.
func (a *App) showDialog(name string, p tview.Primitive, width, height int) {
a.showOverlay(name, p, center(p, width, height))
}
// showOverlay adds a full-screen modal layer and focuses its interactive
// content. Mouse events not handled by the content are consumed by the layer
// instead of falling through to the main page.
func (a *App) showOverlay(name string, focus, layout tview.Primitive) {
a.dialogFocus[name] = a.app.GetFocus()
a.pages.AddPage(name, center(p, width, height), true, true)
a.app.SetFocus(p)
a.pages.AddPage(name, newFocusTrap(layout, focus), true, true)
a.app.SetFocus(focus)
}
// focusTrap is a modal primitive which delegates rendering and input to its
// layout while preventing unhandled mouse events from reaching lower pages.
type focusTrap struct {
tview.Primitive
focus tview.Primitive
}
// newFocusTrap returns a modal wrapper around layout.
func newFocusTrap(layout, focus tview.Primitive) *focusTrap {
return &focusTrap{Primitive: layout, focus: focus}
}
// MouseHandler forwards events to the overlay and consumes any event it does
// not handle. Clicking outside the content restores focus inside the overlay.
func (t *focusTrap) MouseHandler() func(tview.MouseAction, *tcell.EventMouse, func(tview.Primitive)) (bool, tview.Primitive) {
return func(action tview.MouseAction, event *tcell.EventMouse, setFocus func(tview.Primitive)) (bool, tview.Primitive) {
if handler := t.Primitive.MouseHandler(); handler != nil {
if consumed, capture := handler(action, event, setFocus); consumed {
return true, capture
}
}
if action == tview.MouseLeftDown && !t.focus.HasFocus() {
setFocus(t.focus)
}
return true, nil
}
}
// closeDialog removes a floating window and restores the previous focus.