Навигация стрелками по полям диалогов
Оба диалога наследуются от FormScreen с action_walk: у Screen нет готового action_focus_next, поэтому биндинг стрелок на него молча ничего не делал. Вверх и вниз ходят по полям, влево и вправо остаются за курсором внутри поля ввода — биндинги намеренно без priority.
This commit is contained in:
@@ -88,6 +88,10 @@ matches(text, query) ; общий фильтр для pick() и обоих с
|
||||
установленном textual: TUI посреди CLI-команды сломал бы pipe-сценарии.
|
||||
- Текст `StepError` — `«{what} завершился с кодом {N}»` — менять нельзя: на него завязано
|
||||
глушение ошибок `pg_restore`.
|
||||
- Диалоги наследуются от `FormScreen` и подмешивают `FORM_NAV`: у `Screen` нет готового
|
||||
`action_focus_next`, поэтому обход полей стрелками сделан своим `action_walk`. Биндинги без
|
||||
`priority` — внутри `Input` стрелки вверх/вниз свободны и всплывают до экрана сами, а
|
||||
влево/вправо остаются за курсором.
|
||||
- Новый `Ctrl`-хоткей добавляется только через `with_ru_layout([...])`: при русской раскладке
|
||||
терминал присылает `ctrl+в` вместо `ctrl+d`, и голый биндинг молча не срабатывает. Полноту
|
||||
дублей проверяет тест, перебирающий `PgSyncApp.BINDINGS`.
|
||||
|
||||
@@ -67,6 +67,9 @@ copy config.example.json config.json
|
||||
| `F1` / `F2` / `F3` | помощь / тема / показывать ли вывод утилит |
|
||||
| `Ctrl+C` | выход |
|
||||
|
||||
В окне операции: `↑` `↓` (и `Tab`) — переход по полям, `Space` — переключить галочку,
|
||||
`Ctrl+Enter` — начать, `Esc` — отмена.
|
||||
|
||||
Раскладка клавиатуры значения не имеет: каждый `Ctrl`-хоткей продублирован кириллическим
|
||||
двойником по ЙЦУКЕН (`Ctrl+В` = `Ctrl+D`, `Ctrl+Ч` = `Ctrl+X` и так далее).
|
||||
|
||||
|
||||
+22
-4
@@ -283,6 +283,24 @@ class DumpList(FilterList):
|
||||
# --------------------------------------------------------------------------- модалки
|
||||
|
||||
|
||||
# Стрелки как способ ходить по полям диалога: Tab знают не все, а мышь в консоли не всегда
|
||||
# под рукой. Без priority — внутри Input стрелки вверх/вниз свободны и всплывают сюда сами.
|
||||
FORM_NAV = [
|
||||
Binding("down", "walk(1)", "ниже", show=False),
|
||||
Binding("up", "walk(-1)", "выше", show=False),
|
||||
]
|
||||
|
||||
|
||||
class FormScreen(ModalScreen):
|
||||
"""Диалог, по полям которого ходят стрелками: у Screen нет готового action для фокуса."""
|
||||
|
||||
def action_walk(self, step: int) -> None:
|
||||
if step > 0:
|
||||
self.focus_next()
|
||||
else:
|
||||
self.focus_previous()
|
||||
|
||||
|
||||
class PasswordModal(ModalScreen):
|
||||
"""getpass под запущенным Textual невозможен: пароль спрашивается только отсюда."""
|
||||
|
||||
@@ -310,11 +328,11 @@ class PasswordModal(ModalScreen):
|
||||
self.dismiss(None)
|
||||
|
||||
|
||||
class ConfirmModal(ModalScreen):
|
||||
class ConfirmModal(FormScreen):
|
||||
"""Последний экран перед деструктивной операцией: имя базы, режим и прямая формулировка."""
|
||||
|
||||
BINDINGS = [Binding("escape", "cancel", "отмена"),
|
||||
Binding("ctrl+enter", "start", "начать", priority=True)]
|
||||
Binding("ctrl+enter", "start", "начать", priority=True), *FORM_NAV]
|
||||
|
||||
TITLES = {"sync": "Dump + Restore", "dump": "Dump", "restore": "Restore"}
|
||||
|
||||
@@ -391,7 +409,7 @@ def _split_csv(text: str) -> list:
|
||||
return [part.strip() for part in text.split(",") if part.strip()]
|
||||
|
||||
|
||||
class DbOpModal(ModalScreen):
|
||||
class DbOpModal(FormScreen):
|
||||
"""Что делаем с выбранной базой: параметры дампа и галочка автоподнятия локально.
|
||||
|
||||
Заменяет собой выбор действия: снятый флаг — просто dump, поднятый — dump + restore.
|
||||
@@ -399,7 +417,7 @@ class DbOpModal(ModalScreen):
|
||||
"""
|
||||
|
||||
BINDINGS = [Binding("escape", "cancel", "отмена"),
|
||||
Binding("ctrl+enter", "start", "начать", priority=True)]
|
||||
Binding("ctrl+enter", "start", "начать", priority=True), *FORM_NAV]
|
||||
|
||||
def __init__(self, dbs: list, cfg: dict, defaults: dict) -> None:
|
||||
super().__init__()
|
||||
|
||||
@@ -998,3 +998,39 @@ def test_op_dialog_fits_and_keeps_buttons_visible(cfg, monkeypatch):
|
||||
await pilot.press("escape")
|
||||
await pilot.pause()
|
||||
asyncio.run(scenario())
|
||||
|
||||
|
||||
def test_arrows_walk_dialog_fields(cfg, monkeypatch):
|
||||
"""В диалоге операции стрелки должны ходить по полям, а не только Tab."""
|
||||
app = make_app(cfg, monkeypatch)
|
||||
|
||||
async def scenario():
|
||||
async with app.run_test(size=(120, 40)) as pilot:
|
||||
await settle(app, pilot)
|
||||
app.dashboard.open_db_op(autorestore=True)
|
||||
for _ in range(40):
|
||||
await pilot.pause()
|
||||
if isinstance(app.screen, tui.DbOpModal):
|
||||
break
|
||||
seen = []
|
||||
for _ in range(6):
|
||||
await pilot.press("down")
|
||||
await pilot.pause()
|
||||
seen.append(app.focused.id)
|
||||
assert len(set(seen)) > 1, f"фокус не двигается: {seen}"
|
||||
|
||||
back = app.focused.id
|
||||
await pilot.press("up")
|
||||
await pilot.pause()
|
||||
assert app.focused.id != back # вверх возвращает на предыдущее поле
|
||||
|
||||
# стрелки внутри поля ввода остаются за курсором: значение не должно меняться
|
||||
field = app.screen.query_one("#compress", Input)
|
||||
field.focus()
|
||||
await pilot.pause()
|
||||
await pilot.press("left", "right")
|
||||
await pilot.pause()
|
||||
assert field.value == "6"
|
||||
await pilot.press("escape")
|
||||
await pilot.pause()
|
||||
asyncio.run(scenario())
|
||||
|
||||
Reference in New Issue
Block a user