Навигация стрелками по полям диалогов
Оба диалога наследуются от 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-сценарии.
|
установленном textual: TUI посреди CLI-команды сломал бы pipe-сценарии.
|
||||||
- Текст `StepError` — `«{what} завершился с кодом {N}»` — менять нельзя: на него завязано
|
- Текст `StepError` — `«{what} завершился с кодом {N}»` — менять нельзя: на него завязано
|
||||||
глушение ошибок `pg_restore`.
|
глушение ошибок `pg_restore`.
|
||||||
|
- Диалоги наследуются от `FormScreen` и подмешивают `FORM_NAV`: у `Screen` нет готового
|
||||||
|
`action_focus_next`, поэтому обход полей стрелками сделан своим `action_walk`. Биндинги без
|
||||||
|
`priority` — внутри `Input` стрелки вверх/вниз свободны и всплывают до экрана сами, а
|
||||||
|
влево/вправо остаются за курсором.
|
||||||
- Новый `Ctrl`-хоткей добавляется только через `with_ru_layout([...])`: при русской раскладке
|
- Новый `Ctrl`-хоткей добавляется только через `with_ru_layout([...])`: при русской раскладке
|
||||||
терминал присылает `ctrl+в` вместо `ctrl+d`, и голый биндинг молча не срабатывает. Полноту
|
терминал присылает `ctrl+в` вместо `ctrl+d`, и голый биндинг молча не срабатывает. Полноту
|
||||||
дублей проверяет тест, перебирающий `PgSyncApp.BINDINGS`.
|
дублей проверяет тест, перебирающий `PgSyncApp.BINDINGS`.
|
||||||
|
|||||||
@@ -67,6 +67,9 @@ copy config.example.json config.json
|
|||||||
| `F1` / `F2` / `F3` | помощь / тема / показывать ли вывод утилит |
|
| `F1` / `F2` / `F3` | помощь / тема / показывать ли вывод утилит |
|
||||||
| `Ctrl+C` | выход |
|
| `Ctrl+C` | выход |
|
||||||
|
|
||||||
|
В окне операции: `↑` `↓` (и `Tab`) — переход по полям, `Space` — переключить галочку,
|
||||||
|
`Ctrl+Enter` — начать, `Esc` — отмена.
|
||||||
|
|
||||||
Раскладка клавиатуры значения не имеет: каждый `Ctrl`-хоткей продублирован кириллическим
|
Раскладка клавиатуры значения не имеет: каждый `Ctrl`-хоткей продублирован кириллическим
|
||||||
двойником по ЙЦУКЕН (`Ctrl+В` = `Ctrl+D`, `Ctrl+Ч` = `Ctrl+X` и так далее).
|
двойником по ЙЦУКЕН (`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):
|
class PasswordModal(ModalScreen):
|
||||||
"""getpass под запущенным Textual невозможен: пароль спрашивается только отсюда."""
|
"""getpass под запущенным Textual невозможен: пароль спрашивается только отсюда."""
|
||||||
|
|
||||||
@@ -310,11 +328,11 @@ class PasswordModal(ModalScreen):
|
|||||||
self.dismiss(None)
|
self.dismiss(None)
|
||||||
|
|
||||||
|
|
||||||
class ConfirmModal(ModalScreen):
|
class ConfirmModal(FormScreen):
|
||||||
"""Последний экран перед деструктивной операцией: имя базы, режим и прямая формулировка."""
|
"""Последний экран перед деструктивной операцией: имя базы, режим и прямая формулировка."""
|
||||||
|
|
||||||
BINDINGS = [Binding("escape", "cancel", "отмена"),
|
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"}
|
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()]
|
return [part.strip() for part in text.split(",") if part.strip()]
|
||||||
|
|
||||||
|
|
||||||
class DbOpModal(ModalScreen):
|
class DbOpModal(FormScreen):
|
||||||
"""Что делаем с выбранной базой: параметры дампа и галочка автоподнятия локально.
|
"""Что делаем с выбранной базой: параметры дампа и галочка автоподнятия локально.
|
||||||
|
|
||||||
Заменяет собой выбор действия: снятый флаг — просто dump, поднятый — dump + restore.
|
Заменяет собой выбор действия: снятый флаг — просто dump, поднятый — dump + restore.
|
||||||
@@ -399,7 +417,7 @@ class DbOpModal(ModalScreen):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
BINDINGS = [Binding("escape", "cancel", "отмена"),
|
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:
|
def __init__(self, dbs: list, cfg: dict, defaults: dict) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|||||||
@@ -998,3 +998,39 @@ def test_op_dialog_fits_and_keeps_buttons_visible(cfg, monkeypatch):
|
|||||||
await pilot.press("escape")
|
await pilot.press("escape")
|
||||||
await pilot.pause()
|
await pilot.pause()
|
||||||
asyncio.run(scenario())
|
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