Make menu clickable
This commit is contained in:
@@ -6,8 +6,10 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from rich.text import Text
|
||||
from textual import events
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.binding import Binding
|
||||
from textual.coordinate import Coordinate
|
||||
from textual.widgets import DataTable, Footer, Header, Static
|
||||
|
||||
from .bsipa import BSIPA_PLUGIN_ID
|
||||
@@ -32,6 +34,43 @@ class InstallationChoice:
|
||||
state_root: Path
|
||||
|
||||
|
||||
class ActivatableDataTable(DataTable):
|
||||
"""DataTable that activates a row on every click, not only a re-click."""
|
||||
|
||||
async def _on_click(self, event: events.Click) -> None:
|
||||
# Textual dispatches `_on_click` for every class in the MRO, so we fully
|
||||
# handle the click here and prevent DataTable's handler from also running.
|
||||
self._set_hover_cursor(True)
|
||||
meta = event.style.meta
|
||||
if "row" not in meta or "column" not in meta:
|
||||
event.prevent_default()
|
||||
return
|
||||
if self.cursor_type != "row" and meta.get("out_of_bounds", False):
|
||||
event.prevent_default()
|
||||
return
|
||||
|
||||
row_index = meta["row"]
|
||||
column_index = meta["column"]
|
||||
is_header_click = self.show_header and row_index == -1
|
||||
is_row_label_click = self.show_row_labels and column_index == -1
|
||||
if is_header_click:
|
||||
column = self.ordered_columns[column_index]
|
||||
self.post_message(
|
||||
DataTable.HeaderSelected(self, column.key, column_index, label=column.label)
|
||||
)
|
||||
elif is_row_label_click:
|
||||
row = self.ordered_rows[row_index]
|
||||
self.post_message(
|
||||
DataTable.RowLabelSelected(self, row.key, row_index, label=row.label)
|
||||
)
|
||||
elif self.show_cursor and self.cursor_type != "none":
|
||||
self.cursor_coordinate = Coordinate(row_index, column_index)
|
||||
self._post_selected_message()
|
||||
self._scroll_cursor_into_view(animate=True)
|
||||
event.stop()
|
||||
event.prevent_default()
|
||||
|
||||
|
||||
class PluginHelperTui(App[int]):
|
||||
CSS = """
|
||||
#title {
|
||||
@@ -77,7 +116,7 @@ class PluginHelperTui(App[int]):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header(show_clock=False)
|
||||
yield Static("", id="title")
|
||||
yield DataTable(id="table")
|
||||
yield ActivatableDataTable(id="table")
|
||||
yield Static("", id="status")
|
||||
yield Footer()
|
||||
|
||||
@@ -90,6 +129,13 @@ class PluginHelperTui(App[int]):
|
||||
return
|
||||
self._show_installations()
|
||||
|
||||
async def on_data_table_row_selected(self, _event: DataTable.RowSelected) -> None:
|
||||
if self.mode == "installations":
|
||||
self.action_select()
|
||||
return
|
||||
if self.mode == "plugins":
|
||||
await self.action_toggle_plugin()
|
||||
|
||||
def action_select(self) -> None:
|
||||
if self.mode != "installations":
|
||||
return
|
||||
@@ -312,7 +358,7 @@ class PluginHelperTui(App[int]):
|
||||
if self.setup_hint:
|
||||
self._set_status(self.setup_hint)
|
||||
else:
|
||||
self._set_status("Enter selects an installation. q quits.")
|
||||
self._set_status("Click or Enter selects an installation. q quits.")
|
||||
|
||||
def _show_plugins(self, *, preserve_status: bool = False) -> None:
|
||||
if self.selected_installation is None:
|
||||
@@ -357,7 +403,7 @@ class PluginHelperTui(App[int]):
|
||||
if self.plugin_rows:
|
||||
back_hint = "" if len(self.choices) == 1 else " b returns to installations."
|
||||
self._set_status(
|
||||
"Space toggles selected. d disables all. e enables all. "
|
||||
"Click or Space toggles selected. d disables all. e enables all. "
|
||||
f"s saves known-good. g restores known-good.{back_hint}"
|
||||
)
|
||||
else:
|
||||
|
||||
@@ -2636,6 +2636,43 @@ class PluginHelperTuiTests(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertNotIn("example", updated["plugins"])
|
||||
self.assertIn("example", updated["disabledPlugins"])
|
||||
|
||||
async def test_click_toggles_plugin_like_space(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
app, instance, state = _make_two_plugin_tui_fixture(Path(tmp))
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
table = app.query_one(DataTable)
|
||||
self.assertEqual(table.row_count, 2)
|
||||
self.assertEqual(app.plugin_rows[0]["status"], "enabled")
|
||||
# Header is y=0; first data row is y=1. Click the second plugin row.
|
||||
await pilot.click(DataTable, offset=(2, 2))
|
||||
await pilot.pause()
|
||||
self.assertEqual(table.cursor_row, 1)
|
||||
self.assertEqual(app.plugin_rows[1]["id"], "beta")
|
||||
self.assertEqual(app.plugin_rows[1]["status"], "disabled")
|
||||
|
||||
self.assertFalse((instance / "Plugins" / "Beta.dll").exists())
|
||||
self.assertTrue((instance / "Plugins" / "Alpha.dll").exists())
|
||||
updated = load_installed_state(state, "1.40.8", install_id=app.choices[0].install_id)
|
||||
self.assertNotIn("beta", updated["plugins"])
|
||||
self.assertIn("beta", updated["disabledPlugins"])
|
||||
self.assertIn("alpha", updated["plugins"])
|
||||
|
||||
async def test_click_toggles_already_selected_plugin(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
app, instance, state = _make_tui_fixture(Path(tmp))
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
self.assertEqual(app.plugin_rows[0]["status"], "enabled")
|
||||
await pilot.click(DataTable, offset=(2, 1))
|
||||
await pilot.pause()
|
||||
self.assertEqual(app.plugin_rows[0]["status"], "disabled")
|
||||
|
||||
self.assertFalse((instance / "Plugins" / "Example.dll").exists())
|
||||
updated = load_installed_state(state, "1.40.8", install_id=app.choices[0].install_id)
|
||||
self.assertNotIn("example", updated["plugins"])
|
||||
self.assertIn("example", updated["disabledPlugins"])
|
||||
|
||||
async def test_space_toggle_keeps_cursor_on_selected_plugin(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
app, instance, _state = _make_two_plugin_tui_fixture(Path(tmp))
|
||||
|
||||
Reference in New Issue
Block a user