mirror of
https://github.com/FuzzingLabs/fuzzforge_ai.git
synced 2026-03-13 23:26:39 +00:00
tui: fix single-click buttons and double-modal push
This commit is contained in:
@@ -56,12 +56,16 @@ class SingleClickDataTable(DataTable):
|
||||
return self.data_table
|
||||
|
||||
async def _on_click(self, event: events.Click) -> None: # type: ignore[override]
|
||||
"""Forward to parent, then post RowClicked for single-click detection."""
|
||||
"""Forward to parent, then post RowClicked on every mouse click.
|
||||
|
||||
The hub table is handled exclusively via RowClicked. RowSelected is
|
||||
intentionally NOT used for the hub table to avoid double-dispatch.
|
||||
"""
|
||||
await super()._on_click(event)
|
||||
meta = event.style.meta
|
||||
if "row" in meta and self.cursor_type == "row":
|
||||
row_index: int = meta["row"]
|
||||
if row_index >= 0: # skip header row
|
||||
if meta and "row" in meta and self.cursor_type == "row":
|
||||
row_index: int = int(meta["row"])
|
||||
if row_index >= 0:
|
||||
self.post_message(SingleClickDataTable.RowClicked(self, row_index))
|
||||
|
||||
|
||||
@@ -371,11 +375,14 @@ class FuzzForgeApp(App[None]):
|
||||
self._hub_rows.append((name, image, hub_name, is_ready))
|
||||
|
||||
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
|
||||
"""Handle Enter-key row selection on the agents table."""
|
||||
"""Handle Enter-key row selection (agents table only).
|
||||
|
||||
Hub table uses RowClicked exclusively — wiring it to RowSelected too
|
||||
would cause a double push on every click since Textual 8 fires
|
||||
RowSelected on ALL clicks, not just second-click-on-same-row.
|
||||
"""
|
||||
if event.data_table.id == "agents-table":
|
||||
self._handle_agent_row(event.cursor_row)
|
||||
elif event.data_table.id == "hub-table":
|
||||
self._handle_hub_row(event.cursor_row)
|
||||
|
||||
def on_single_click_data_table_row_clicked(
|
||||
self, event: SingleClickDataTable.RowClicked
|
||||
@@ -408,6 +415,10 @@ class FuzzForgeApp(App[None]):
|
||||
|
||||
def _handle_hub_row(self, idx: int) -> None:
|
||||
"""Handle a click on a hub table row."""
|
||||
# Guard: never push two build dialogs at once (double-click protection)
|
||||
if getattr(self, "_build_dialog_open", False):
|
||||
return
|
||||
|
||||
if idx < 0 or idx >= len(self._hub_rows):
|
||||
return
|
||||
row_data = self._hub_rows[idx]
|
||||
@@ -419,7 +430,11 @@ class FuzzForgeApp(App[None]):
|
||||
# If a build is already running, open the live log viewer
|
||||
if image in self._active_builds:
|
||||
from fuzzforge_cli.tui.screens.build_log import BuildLogScreen
|
||||
self.push_screen(BuildLogScreen(image))
|
||||
self._build_dialog_open = True
|
||||
self.push_screen(
|
||||
BuildLogScreen(image),
|
||||
callback=lambda _: setattr(self, "_build_dialog_open", False),
|
||||
)
|
||||
return
|
||||
|
||||
if is_ready:
|
||||
@@ -432,10 +447,15 @@ class FuzzForgeApp(App[None]):
|
||||
|
||||
from fuzzforge_cli.tui.screens.build_image import BuildImageScreen
|
||||
|
||||
self._build_dialog_open = True
|
||||
|
||||
def _on_build_dialog_done(confirmed: bool, sn: str = server_name, im: str = image, hn: str = hub_name) -> None:
|
||||
self._build_dialog_open = False
|
||||
self._on_build_confirmed(confirmed, sn, im, hn)
|
||||
|
||||
self.push_screen(
|
||||
BuildImageScreen(server_name, image, hub_name),
|
||||
callback=lambda confirmed, sn=server_name, im=image, hn=hub_name:
|
||||
self._on_build_confirmed(confirmed, sn, im, hn),
|
||||
callback=_on_build_dialog_done,
|
||||
)
|
||||
|
||||
def _on_build_confirmed(self, confirmed: bool, server_name: str, image: str, hub_name: str) -> None:
|
||||
|
||||
@@ -14,6 +14,10 @@ from textual.screen import ModalScreen
|
||||
from textual.widgets import Button, Label
|
||||
|
||||
|
||||
class _NoFocusButton(Button):
|
||||
can_focus = False
|
||||
|
||||
|
||||
class BuildImageScreen(ModalScreen[bool]):
|
||||
"""Quick confirmation before starting a background Docker/Podman build."""
|
||||
|
||||
@@ -38,16 +42,10 @@ class BuildImageScreen(ModalScreen[bool]):
|
||||
id="confirm-text",
|
||||
)
|
||||
with Horizontal(classes="dialog-buttons"):
|
||||
yield Button("Build", variant="success", id="btn-build")
|
||||
yield Button("Cancel", variant="default", id="btn-cancel")
|
||||
|
||||
def on_mount(self) -> None:
|
||||
# Ensure a widget is focused so both buttons respond to a single click.
|
||||
# Default to Cancel so Build is never pre-selected.
|
||||
self.query_one("#btn-cancel", Button).focus()
|
||||
yield _NoFocusButton("Build", variant="primary", id="btn-build")
|
||||
yield _NoFocusButton("Cancel", variant="default", id="btn-cancel")
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
event.stop()
|
||||
if event.button.id == "btn-build":
|
||||
self.dismiss(True)
|
||||
elif event.button.id == "btn-cancel":
|
||||
|
||||
@@ -14,6 +14,10 @@ from textual.screen import ModalScreen
|
||||
from textual.widgets import Button, Label, Log
|
||||
|
||||
|
||||
class _NoFocusButton(Button):
|
||||
can_focus = False
|
||||
|
||||
|
||||
class BuildLogScreen(ModalScreen[None]):
|
||||
"""Live log viewer for a background build job managed by the app."""
|
||||
|
||||
@@ -30,10 +34,9 @@ class BuildLogScreen(ModalScreen[None]):
|
||||
yield Label("", id="build-status")
|
||||
yield Log(id="build-log", auto_scroll=True)
|
||||
with Horizontal(classes="dialog-buttons"):
|
||||
yield Button("Close", variant="default", id="btn-close")
|
||||
yield _NoFocusButton("Close", variant="default", id="btn-close")
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.query_one("#btn-close", Button).focus()
|
||||
self._flush_log()
|
||||
self.set_interval(0.5, self._poll_log)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user