from abc import abstractmethod
from dataclasses import dataclass
from typing import TypeVar, Generic, Optional, List
from textual.app import ComposeResult
from textual.binding import Binding
from textual.containers import Vertical, Horizontal
from textual.reactive import reactive, Reactive
from textual.screen import ModalScreen
from textual.widgets import Button, Label, Checkbox, Input, TextArea
T = TypeVar('T')
class QuestionDialog(Generic[T], ModalScreen[Optional[T]]):
BINDINGS = [
Binding("escape", "cancel", "Cancel", show=True),
]
DEFAULT_CSS = """
QuestionDialog {
align: center middle;
height: auto;
background: $background 1%;
}
#dialog {
width: 75%;
max-width: 60;
height: auto;
padding: 0 1;
border: thick $accent 50%;
background: $panel;
}
#question {
width: 100%;
content-align: center middle;
margin-bottom: 1;
text-style: bold;
}
CheckBox, ToggleButton {
background: $panel;
}
#buttons {
align: right middle;
height: auto;
margin-top: 1;
}
"""
valid: Reactive[bool] = reactive(True)
def __init__(self, prompt: str, yes: str, no: str) -> None:
# NB: push_screen must be run from a worker when `wait_for_dismiss` is True
super().__init__()
self._prompt: str = prompt
self._yes: str = yes
self._no: str = no
@abstractmethod
def _compose_dialog(self) -> ComposeResult:
raise NotImplementedError
yield
@abstractmethod
def _get_result(self) -> T:
raise NotImplementedError
def compose(self) -> ComposeResult:
with Vertical(id="dialog"):
yield Label(self._prompt, id="question")
yield from self._compose_dialog()
yield Horizontal(Button(self._no, variant="error", id="no"),
Button(self._yes, variant="primary", id="yes"), id="buttons")
def watch_valid(self, valid: bool) -> None:
self.query_one("#yes", Button).disabled = not valid
def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id == "yes":
self.dismiss(self._get_result())
else:
self.dismiss(None)
def action_cancel(self) -> None:
self.dismiss(None)
@dataclass(frozen=True)
class CreateAnswer:
parent: bool
summary: List[str]
class CreateDialog(QuestionDialog[CreateAnswer]):
DEFAULT_CSS = """
TextArea {
height: auto;
max-height: 7;
}
"""
def __init__(self, parent_id: Optional[str], parent_summary: Optional[str]) -> None:
super().__init__(prompt="Create task(s)?", yes="Create", no="Cancel")
self._parent_id: Optional[str] = parent_id
self._parent_summary: Optional[str] = parent_summary
def _compose_dialog(self) -> ComposeResult:
yield TextArea()
if self._parent_id is not None:
yield Checkbox(f"Create as subtask of '{self._parent_summary or ''}'")
def on_mount(self) -> None:
self.valid = False
def _parse_text_area(self) -> List[str]:
return list(filter(None, [_.strip() for _ in self.query_one(TextArea).text.splitlines(keepends=False)]))
def on_text_area_changed(self, evt: Input.Changed) -> None:
self.valid = len(self._parse_text_area()) > 0
def _get_result(self) -> CreateAnswer:
return CreateAnswer(
parent=self._parent_id is not None and self.query_one(Checkbox).value,
summary=self._parse_text_area(),
)
class CreateListDialog(QuestionDialog[str]):
def __init__(self) -> None:
super().__init__(prompt="Create task list?", yes="Create", no="Cancel")
self._input: Input = Input()
def _compose_dialog(self) -> ComposeResult:
yield self._input
def on_mount(self) -> None:
self.valid = False
def _parse_input(self) -> str:
return self._input.value.strip()
def on_input_changed(self, evt: Input.Changed) -> None:
self.valid = len(self._parse_input()) > 0
def _get_result(self) -> str:
return self._parse_input()
class DeleteDialog(QuestionDialog[bool]):
def __init__(self, summary: str, count: int) -> None:
super().__init__(prompt=f"Delete task '{summary}'?", yes="Delete", no="Cancel")
self._count: int = count
def _compose_dialog(self) -> ComposeResult:
if self._count > 1:
yield Checkbox(f"Confirm recursive deletion of {self._count} tasks")
def on_mount(self) -> None:
self.valid = self._count <= 1
def on_checkbox_changed(self, evt: Checkbox.Changed) -> None:
self.valid = evt.value
def _get_result(self) -> bool:
return self.valid
class DeleteListDialog(QuestionDialog[bool]):
def __init__(self, summary: str) -> None:
super().__init__(prompt=f"Delete task list '{summary}'?", yes="Delete", no="Cancel")
def _compose_dialog(self) -> ComposeResult:
yield Checkbox("Confirm recursive deletion of all tasks")
def on_mount(self) -> None:
self.valid = False
def on_checkbox_changed(self, evt: Checkbox.Changed) -> None:
self.valid = evt.value
def _get_result(self) -> bool:
return self.valid