Files
website/tools/content/tui/dialogs.py
T

109 lines
3.8 KiB
Python

from dataclasses import dataclass, field
from textual import on
from textual.app import ComposeResult
from textual.containers import Horizontal, Vertical
from textual.screen import ModalScreen
from textual.widgets import Button, Input, Label, Select
@dataclass
class FormField:
name: str
label: str
kind: str = "text"
placeholder: str = ""
required: bool = False
value: str = ""
options: list[str] = field(default_factory=list)
class FormDialog(ModalScreen[dict | None]):
BINDINGS = [("escape", "cancel", "Cancel")]
def __init__(self, title: str, fields: list[FormField], submit_label: str = "Save"):
super().__init__()
self.form_title = title
self.fields = fields
self.submit_label = submit_label
def compose(self) -> ComposeResult:
with Vertical(classes="dialog"):
yield Label(self.form_title, classes="dialog-title")
for form_field in self.fields:
yield Label(form_field.label, classes="field-label")
yield self.field_widget(form_field)
yield Label("", id="dialog-error")
with Horizontal(classes="dialog-buttons"):
yield Button(self.submit_label, variant="primary", id="submit")
yield Button("Cancel", id="cancel")
def field_widget(self, form_field: FormField):
widget_id = f"field-{form_field.name}"
if form_field.kind == "select":
options = [(option, option) for option in form_field.options]
return Select(options, value=form_field.value or form_field.options[0], allow_blank=False, id=widget_id)
input_type = "integer" if form_field.kind == "rating" else "text"
return Input(value=form_field.value, placeholder=form_field.placeholder, type=input_type, id=widget_id)
def show_error(self, message: str):
self.query_one("#dialog-error", Label).update(message)
def read_value(self, form_field: FormField):
text = str(self.query_one(f"#field-{form_field.name}").value or "").strip()
if form_field.required and not text:
raise ValueError(f"{form_field.label} is required")
if form_field.kind != "rating":
return text or None
if not text:
return None
rating = int(text)
if not 0 <= rating <= 10:
raise ValueError("Rating must be between 0 and 10")
return rating
def submit(self):
try:
values = {form_field.name: self.read_value(form_field) for form_field in self.fields}
except ValueError as error:
self.show_error(str(error))
return
self.dismiss(values)
@on(Input.Submitted)
def submit_from_input(self):
self.submit()
@on(Button.Pressed, "#submit")
def submit_from_button(self):
self.submit()
@on(Button.Pressed, "#cancel")
def action_cancel(self):
self.dismiss(None)
class ConfirmDialog(ModalScreen[bool]):
BINDINGS = [("y", "confirm", "Yes"), ("n", "cancel", "No"), ("escape", "cancel", "No")]
def __init__(self, message: str, confirm_label: str = "Delete", confirm_variant: str = "error"):
super().__init__()
self.message = message
self.confirm_label = confirm_label
self.confirm_variant = confirm_variant
def compose(self) -> ComposeResult:
with Vertical(classes="dialog"):
yield Label(self.message, classes="dialog-title")
with Horizontal(classes="dialog-buttons"):
yield Button(self.confirm_label, variant=self.confirm_variant, id="confirm")
yield Button("Cancel", id="cancel")
@on(Button.Pressed, "#confirm")
def action_confirm(self):
self.dismiss(True)
@on(Button.Pressed, "#cancel")
def action_cancel(self):
self.dismiss(False)