remove operação remove, corrige bug de focus
This commit is contained in:
+5
-1
@@ -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() {
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user