Make menu clickable

This commit is contained in:
pleb
2026-07-11 16:02:38 -07:00
parent 861ce587f3
commit bcfada09f9
2 changed files with 86 additions and 3 deletions
+49 -3
View File
@@ -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:
+37
View File
@@ -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))