Make menu clickable
This commit is contained in:
@@ -6,8 +6,10 @@ from pathlib import Path
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from rich.text import Text
|
from rich.text import Text
|
||||||
|
from textual import events
|
||||||
from textual.app import App, ComposeResult
|
from textual.app import App, ComposeResult
|
||||||
from textual.binding import Binding
|
from textual.binding import Binding
|
||||||
|
from textual.coordinate import Coordinate
|
||||||
from textual.widgets import DataTable, Footer, Header, Static
|
from textual.widgets import DataTable, Footer, Header, Static
|
||||||
|
|
||||||
from .bsipa import BSIPA_PLUGIN_ID
|
from .bsipa import BSIPA_PLUGIN_ID
|
||||||
@@ -32,6 +34,43 @@ class InstallationChoice:
|
|||||||
state_root: Path
|
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]):
|
class PluginHelperTui(App[int]):
|
||||||
CSS = """
|
CSS = """
|
||||||
#title {
|
#title {
|
||||||
@@ -77,7 +116,7 @@ class PluginHelperTui(App[int]):
|
|||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
yield Header(show_clock=False)
|
yield Header(show_clock=False)
|
||||||
yield Static("", id="title")
|
yield Static("", id="title")
|
||||||
yield DataTable(id="table")
|
yield ActivatableDataTable(id="table")
|
||||||
yield Static("", id="status")
|
yield Static("", id="status")
|
||||||
yield Footer()
|
yield Footer()
|
||||||
|
|
||||||
@@ -90,6 +129,13 @@ class PluginHelperTui(App[int]):
|
|||||||
return
|
return
|
||||||
self._show_installations()
|
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:
|
def action_select(self) -> None:
|
||||||
if self.mode != "installations":
|
if self.mode != "installations":
|
||||||
return
|
return
|
||||||
@@ -312,7 +358,7 @@ class PluginHelperTui(App[int]):
|
|||||||
if self.setup_hint:
|
if self.setup_hint:
|
||||||
self._set_status(self.setup_hint)
|
self._set_status(self.setup_hint)
|
||||||
else:
|
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:
|
def _show_plugins(self, *, preserve_status: bool = False) -> None:
|
||||||
if self.selected_installation is None:
|
if self.selected_installation is None:
|
||||||
@@ -357,7 +403,7 @@ class PluginHelperTui(App[int]):
|
|||||||
if self.plugin_rows:
|
if self.plugin_rows:
|
||||||
back_hint = "" if len(self.choices) == 1 else " b returns to installations."
|
back_hint = "" if len(self.choices) == 1 else " b returns to installations."
|
||||||
self._set_status(
|
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}"
|
f"s saves known-good. g restores known-good.{back_hint}"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -2636,6 +2636,43 @@ class PluginHelperTuiTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
self.assertNotIn("example", updated["plugins"])
|
self.assertNotIn("example", updated["plugins"])
|
||||||
self.assertIn("example", updated["disabledPlugins"])
|
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:
|
async def test_space_toggle_keeps_cursor_on_selected_plugin(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
app, instance, _state = _make_two_plugin_tui_fixture(Path(tmp))
|
app, instance, _state = _make_two_plugin_tui_fixture(Path(tmp))
|
||||||
|
|||||||
Reference in New Issue
Block a user