From 7ec74c40ad956d8776ca675bea789b9f3f7f7daf Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 5 Nov 2026 23:12:51 -0500 Subject: [PATCH] Every view declares what it shows as a class keyword; prepared choices declare their editors Pairs with OpenHCSDev/formbridge#19, where DeclaredViews stops probing class bodies (`vars(cls).get("shows", ())`) and the family becomes the inheritance tree. The 14 `shows = (...)` body lines become `shows=(...)` class keywords. FieldView lists the DeclaredViews mixin first, so the keyword is taken before Textual's DOMNode.__init_subclass__ sees it. comms_command_dialog wrote `ValueEditor.shown_by[PreparedChoices] = lambda ...` from module scope: a registry entry outside any class. PreparedChoices now inherits DeclaredMetadata and declares its parameter's editor declarations before its own lineage, so the same editor is chosen and the line is deleted. Proof (with formbridge#19 on the path): test_forms, test_settings_screen and test_agent_configuration 16 passed; the 2 failures are the checkbox 'ellipsis' == 'clip' tests already red on main. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XbFMSqJMarXn2ZE8iAL7Sd --- src/toad/forms/editors.py | 28 +++++++----------------- src/toad/forms/form_view.py | 10 +++------ src/toad/screens/settings.py | 4 +--- src/toad/widgets/comms_command_dialog.py | 19 +++++++--------- tests/textual/test_settings_screen.py | 3 +-- 5 files changed, 21 insertions(+), 43 deletions(-) diff --git a/src/toad/forms/editors.py b/src/toad/forms/editors.py index b5e410bf3..30b8af9db 100644 --- a/src/toad/forms/editors.py +++ b/src/toad/forms/editors.py @@ -101,9 +101,7 @@ def watch_feedback(self, text: str) -> None: self.post_message(self.Feedback(text)) -class BoolEditor(ValueEditor, FormCheckbox): - shows = (bool,) - +class BoolEditor(ValueEditor, FormCheckbox, shows=(bool,)): def control_arguments(self) -> dict[str, object]: return {"value": bool(self.field.value)} @@ -165,23 +163,19 @@ def paint(self) -> None: self.value = self.value_text(self.field.value) -class IntEditor(LineEditor): - shows = (int,) +class IntEditor(LineEditor, shows=(int,)): accepts = "integer" -class FloatEditor(LineEditor): - shows = (float,) +class FloatEditor(LineEditor, shows=(float,)): accepts = "number" -class StrEditor(LineEditor): - shows = (str,) +class StrEditor(LineEditor, shows=(str,)): accepts = "text" -class MultilineEditor(ValueEditor, UntintedTonePaint, FramedControl, TextArea): - shows = (Multiline,) +class MultilineEditor(ValueEditor, UntintedTonePaint, FramedControl, TextArea, shows=(Multiline,)): height = Scalar.from_number(10) padding = Spacing(0, 1, 0, 1) @@ -255,9 +249,7 @@ def paint(self) -> None: self.value = value if self.lists(value) else Select.NULL -class ChoicesEditor(SelectEditor): - shows = (AnnotationChoices,) - +class ChoicesEditor(SelectEditor, shows=(AnnotationChoices,)): def paint(self) -> None: with self.prevent(Select.Changed): self.set_options(self.options()) @@ -273,20 +265,16 @@ def options(self) -> Iterable[tuple[str, object]]: return ((self.choices.label(choice), choice) for choice in self.choices.choices()) -class EnumEditor(SelectEditor): - shows = (Enum,) - +class EnumEditor(SelectEditor, shows=(Enum,)): def options(self) -> Iterable[tuple[str, object]]: members: type[Enum] = resolve_annotated(self.field.annotation) # type: ignore[assignment] return ((format_enum_display(member), member) for member in members) -class OptionalEditor(ValueEditor, WidgetView, containers.VerticalGroup): +class OptionalEditor(ValueEditor, WidgetView, containers.VerticalGroup, shows=(Optional,)): """A field that may hold nothing: a box says whether it holds a value, and the editor of that value is shown while it does.""" - shows = (Optional,) - def bind_control(self) -> None: self.holds_value = FormCheckbox("Set", value=self.field.value is not None) self.member = ValueEditor.declared_for(self.field.member) diff --git a/src/toad/forms/form_view.py b/src/toad/forms/form_view.py index a46e70ffd..2947eb18d 100644 --- a/src/toad/forms/form_view.py +++ b/src/toad/forms/form_view.py @@ -54,7 +54,7 @@ def paint_foreground(self): return self.app.colours.colour(PrimaryColour) -class FieldView(WidgetView, containers.VerticalGroup, DeclaredViews): +class FieldView(DeclaredViews, WidgetView, containers.VerticalGroup): """One parameter of a form, shown the way its formbridge kind asks for.""" def __init__(self, field: DeclaredField) -> None: @@ -84,11 +84,9 @@ def filter(self, term: str) -> bool: raise NotImplementedError -class ValueView(FieldView): +class ValueView(FieldView, shows=(ParameterInfo,)): """A field with one value: its label, its help and the editor for its kind.""" - shows = (ParameterInfo,) - CHANGED_MARK = " ●" def __init__(self, field: DeclaredField) -> None: @@ -132,11 +130,9 @@ def mark(self, changed: bool) -> None: ) -class GroupView(FocusWithinView, FieldView): +class GroupView(FocusWithinView, FieldView, shows=(DirectDataclassInfo,)): """A nested dataclass: a heading and the views of its fields.""" - shows = (DirectDataclassInfo,) - padding = Spacing(0, 1, 0, 1) @property diff --git a/src/toad/screens/settings.py b/src/toad/screens/settings.py index 51029e470..7f27830ef 100644 --- a/src/toad/screens/settings.py +++ b/src/toad/screens/settings.py @@ -27,11 +27,9 @@ from toad.app_node import ToadNode -class SchemeEditor(SelectEditor, ToadNode): +class SchemeEditor(SelectEditor, ToadNode, shows=(SchemeName,)): """A colour scheme's name, picked from the schemes the running app has.""" - shows = (SchemeName,) - def options(self) -> Iterable[tuple[str, object]]: return ((name, name) for name in self.app.colours.schemes) diff --git a/src/toad/widgets/comms_command_dialog.py b/src/toad/widgets/comms_command_dialog.py index bb75cd38e..01b33099b 100644 --- a/src/toad/widgets/comms_command_dialog.py +++ b/src/toad/widgets/comms_command_dialog.py @@ -23,7 +23,7 @@ from toad.widgets.checkbox import Checkbox from toad.forms.editors import MultilineEditor, ValueEditor from toad.forms.form_view import InlineFormView -from formbridge.forms.declared_fields import Multiline +from formbridge.forms.declared_fields import DeclaredMetadata, Multiline from toad.native_style import AutoWidth, EllipsisOverflow, SideMargin from toad.screens.modal import CenteredScreen, DismissibleScreen, ModalPanel, ModalScreen, ShadedScreen from toad.widget_actions import WidgetAction @@ -32,7 +32,7 @@ @dataclass(eq=False) -class PreparedChoices(AnnotationChoices): +class PreparedChoices(AnnotationChoices, DeclaredMetadata): """The catalog last prepared for a draft, not another copy of its value.""" parameter: ChoiceTargetField @@ -42,24 +42,22 @@ def choices(self): def label(self, choice): return next((label for label, value in self.parameter.choices if value == choice), 'Choose…') - -ValueEditor.shown_by[PreparedChoices] = lambda field: ValueEditor.showing( - (*field.metadata(PreparedChoices).parameter.editor_declarations, AnnotationChoices) -)(field) + def declares(self): + """The prepared parameter names its own editors first; any choices editor follows.""" + yield from self.parameter.editor_declarations + yield from super().declares() class CommandText(Multiline): """A command parameter of several lines; the command form shows it five rows tall.""" -class CommandTextEditor(MultilineEditor): - shows = (CommandText,) +class CommandTextEditor(MultilineEditor, shows=(CommandText,)): height = Scalar.from_number(5) -class CatalogField(ValueEditor, Button): +class CatalogField(ValueEditor, Button, shows=(ModelConfigOption,)): """The shared model selector edits the form's canonical draft value.""" - shows = (ModelConfigOption,) def bind_control(self): self.paint() @@ -244,7 +242,6 @@ def draft_changed(self, paths): group='command-catalogs', exclusive=True) - def arguments(self): return {name: self.state.get_resolved_value(name) for name in self.form.values} diff --git a/tests/textual/test_settings_screen.py b/tests/textual/test_settings_screen.py index b3d811e7f..7570b1e26 100644 --- a/tests/textual/test_settings_screen.py +++ b/tests/textual/test_settings_screen.py @@ -29,8 +29,7 @@ class Shouted: """Declares text that is kept in capitals.""" -class ShoutedEditor(editors.StrEditor): - shows = (Shouted,) +class ShoutedEditor(editors.StrEditor, shows=(Shouted,)): def read(self, text: str) -> object: return text.upper()