diff --git a/prototypes/t_qt_2.py b/prototypes/t_qt_2.py index c9d9840..a6e3419 100644 --- a/prototypes/t_qt_2.py +++ b/prototypes/t_qt_2.py @@ -6,6 +6,8 @@ import sys import time import uuid from collections.abc import Sequence +from pprint import pprint +from typing import TypeAlias, TypedDict from PySide6.QtCore import QDate, QModelIndex, QStringListModel, Qt, QTimer, Signal from PySide6.QtGui import QAction, QStandardItem, QStandardItemModel @@ -22,6 +24,7 @@ from PySide6.QtWidgets import ( QGroupBox, QHBoxLayout, QLabel, + QLayout, QLineEdit, QListWidget, QMainWindow, @@ -395,18 +398,22 @@ class ContactPersonForm_Search(QWidget): class FormFieldType(enum.StrEnum): + GROUP = enum.auto() TEXT = enum.auto() LONGTEXT = enum.auto() DATE = enum.auto() DATETIME = enum.auto() DROPDOWN = enum.auto() + DYNAMIC_LIST = enum.auto() @dc.dataclass(slots=True) class FormField: label: str type: FormFieldType - required: bool + children: Sequence[FormField] = dc.field(default_factory=list) + parent: FormField | None = None + required: bool = False placeholder: str | None = None fill_value: str | None = None readonly: bool = False @@ -419,7 +426,7 @@ class FormField: self.key = str(uuid.uuid4()) self.label = self.label.strip() - if not self.label.endswith(":"): + if not self.label.endswith(":") and self.type is not FormFieldType.GROUP: self.label += ":" if self.required: self.label += "*" @@ -427,6 +434,9 @@ class FormField: if self.type is FormFieldType.DROPDOWN and self.options is None: raise ValueError("Invalid field definition: Dropdown requires options") + for child in self.children: + child.parent = self + @dc.dataclass(slots=True) class FormFieldDynList: @@ -440,105 +450,119 @@ class FormFieldGroup: fields: Sequence[FormField] | FormFieldDynList -FORM_FIELD_DEF = [ - FormField( - "test", - FormFieldType.DROPDOWN, - required=False, - options=["test1", "test2"], - ), - FormField( - "test", - FormFieldType.DROPDOWN, - required=False, - options=["test1", "test2"], - fill_value="test1", - ), - FormField( - "test", - FormFieldType.DROPDOWN, - required=False, - options=["test1", "test2"], - fill_value="test2", - readonly=True, - ), - FormField( - "test", - FormFieldType.DROPDOWN, - required=False, - options=["test1", "test2", "test3"], - fill_value="test3", - ), - FormField("Projektname", FormFieldType.TEXT, True, "Bitte füllen..."), - FormField("Beschreibung", FormFieldType.LONGTEXT, False), - FormField( - "Externe Daten", - FormFieldType.TEXT, - True, - fill_value="Lorem ipsum und so weiter...", - readonly=True, - ), - FormField("Auftragsdatum", FormFieldType.DATE, True), - FormField("Startdatum", FormFieldType.DATE, True), - FormField( - "MS Datum 1", - FormFieldType.DATE, - False, - "", - fill_value="26.07.2026", - readonly=True, - ), - FormField( - "MS Datum 2", - FormFieldType.DATE, - False, - "", - fill_value="30.08.2026", - readonly=False, - ), - FormField( - "Wichtige Notizen", - FormFieldType.LONGTEXT, - True, - "Text eingeben...", - ), -] +# @dc.dataclass(slots=True) +# class FormFieldRegistry: +# widget: QWidget +# form_field: FormField -FORM_FIELD_DEF2 = [ - FormField("Projektname", FormFieldType.TEXT, True, "Bitte füllen..."), - FormField("Beschreibung", FormFieldType.LONGTEXT, False), - FormField( - "Externe Daten", - FormFieldType.TEXT, - True, - fill_value="Lorem ipsum und so weiter...", - readonly=True, - ), - FormField("Auftragsdatum", FormFieldType.DATE, True), - FormField("Startdatum", FormFieldType.DATE, True), - FormField( - "MS Datum 1", - FormFieldType.DATE, - False, - "", - fill_value="26.07.2026", - readonly=True, - ), - FormField( - "MS Datum 2", - FormFieldType.DATE, - False, - "", - fill_value="30.08.2026", - readonly=False, - ), - FormField( - "Wichtige Notizen", - FormFieldType.LONGTEXT, - True, - "Text eingeben...", - ), -] + +class WidgetRegistryEntry(TypedDict): + widget: QWidget + form_field: FormField + + +WidgetRegistry: TypeAlias = dict[str, WidgetRegistryEntry] + + +# FORM_FIELD_DEF = [ +# FormField( +# "test", +# FormFieldType.DROPDOWN, +# required=False, +# options=["test1", "test2"], +# ), +# FormField( +# "test", +# FormFieldType.DROPDOWN, +# required=False, +# options=["test1", "test2"], +# fill_value="test1", +# ), +# FormField( +# "test", +# FormFieldType.DROPDOWN, +# required=False, +# options=["test1", "test2"], +# fill_value="test2", +# readonly=True, +# ), +# FormField( +# "test", +# FormFieldType.DROPDOWN, +# required=False, +# options=["test1", "test2", "test3"], +# fill_value="test3", +# ), +# FormField("Projektname", FormFieldType.TEXT, True, "Bitte füllen..."), +# FormField("Beschreibung", FormFieldType.LONGTEXT, False), +# FormField( +# "Externe Daten", +# FormFieldType.TEXT, +# True, +# fill_value="Lorem ipsum und so weiter...", +# readonly=True, +# ), +# FormField("Auftragsdatum", FormFieldType.DATE, True), +# FormField("Startdatum", FormFieldType.DATE, True), +# FormField( +# "MS Datum 1", +# FormFieldType.DATE, +# False, +# "", +# fill_value="26.07.2026", +# readonly=True, +# ), +# FormField( +# "MS Datum 2", +# FormFieldType.DATE, +# False, +# "", +# fill_value="30.08.2026", +# readonly=False, +# ), +# FormField( +# "Wichtige Notizen", +# FormFieldType.LONGTEXT, +# True, +# "Text eingeben...", +# ), +# ] + +# FORM_FIELD_DEF2 = [ +# FormField("Projektname", FormFieldType.TEXT, True, "Bitte füllen..."), +# FormField("Beschreibung", FormFieldType.LONGTEXT, False), +# FormField( +# "Externe Daten", +# FormFieldType.TEXT, +# True, +# fill_value="Lorem ipsum und so weiter...", +# readonly=True, +# ), +# FormField("Auftragsdatum", FormFieldType.DATE, True), +# FormField("Startdatum", FormFieldType.DATE, True), +# FormField( +# "MS Datum 1", +# FormFieldType.DATE, +# False, +# "", +# fill_value="26.07.2026", +# readonly=True, +# ), +# FormField( +# "MS Datum 2", +# FormFieldType.DATE, +# False, +# "", +# fill_value="30.08.2026", +# readonly=False, +# ), +# FormField( +# "Wichtige Notizen", +# FormFieldType.LONGTEXT, +# True, +# "Text eingeben...", +# ), +# ] FORM_FIELDS_CONTACT_PERSON = [ @@ -991,13 +1015,27 @@ FORM_FIELDS_LANGUAGES = [ ), ] -FORM_DYN_LIST = FormFieldDynList("Dynamische Liste", FORM_FIELDS_SCHOOL) +# FORM_DYN_LIST = FormFieldDynList("Dynamische Liste", FORM_FIELDS_SCHOOL) -FORM_FIELD_GROUPS = [ - FormFieldGroup( +FORM_FIELDS = [ + # FormFieldGroup( + # "Status && Projektrelevanz", + # [ + # FormField( + # "Projektrelevanz", + # FormFieldType.DROPDOWN, + # required=True, + # options=["ja", "nein"], + # fill_value="nein", + # ), + # ], + # ), + FormField( "Status && Projektrelevanz", - [ + FormFieldType.GROUP, + key="state_relevance", + children=[ FormField( "Projektrelevanz", FormFieldType.DROPDOWN, @@ -1007,14 +1045,20 @@ FORM_FIELD_GROUPS = [ ), ], ), - FormFieldGroup("Daten Kontaktperson", FORM_FIELDS_CONTACT_PERSON), - FormFieldGroup("Stammdaten (ERGÄNZUNG KINDERALTER DYNAMISCH)", FORM_FIELDS_MASTER_DATA), - FormFieldGroup("weitere Informationen", FORM_FIELDS_ADDITIONAL_DATA), - FormFieldGroup("Schule (MIT PLUS-ERWEITERUNG)", FORM_FIELDS_SCHOOL), - FormFieldGroup("Studium/Ausbildung (MIT PLUS-ERWEITERUNG)", FORM_FIELDS_HIGHER_EDUCATION), - FormFieldGroup("Arbeitserfahrung (MIT PLUS-ERWEITERUNG)", FORM_FIELDS_WORK_EXPERIENCE), - FormFieldGroup("Sprachkenntnisse (MIT PLUS-ERWEITERUNG)", FORM_FIELDS_LANGUAGES), - FormFieldGroup("Dynamische Liste", FORM_DYN_LIST), + FormField( + "Daten Kontaktperson", + FormFieldType.GROUP, + key="data_contact_person", + children=FORM_FIELDS_CONTACT_PERSON, + ), + # FormFieldGroup("Daten Kontaktperson", FORM_FIELDS_CONTACT_PERSON), + # FormFieldGroup("Stammdaten (ERGÄNZUNG KINDERALTER DYNAMISCH)", FORM_FIELDS_MASTER_DATA), + # FormFieldGroup("weitere Informationen", FORM_FIELDS_ADDITIONAL_DATA), + # FormFieldGroup("Schule (MIT PLUS-ERWEITERUNG)", FORM_FIELDS_SCHOOL), + # FormFieldGroup("Studium/Ausbildung (MIT PLUS-ERWEITERUNG)", FORM_FIELDS_HIGHER_EDUCATION), + # FormFieldGroup("Arbeitserfahrung (MIT PLUS-ERWEITERUNG)", FORM_FIELDS_WORK_EXPERIENCE), + # FormFieldGroup("Sprachkenntnisse (MIT PLUS-ERWEITERUNG)", FORM_FIELDS_LANGUAGES), + # FormFieldGroup("Dynamische Liste", FORM_DYN_LIST), # FormFieldGroup("Test-2", FORM_FIELD_DEF2), ] @@ -1022,7 +1066,7 @@ FORM_FIELD_GROUPS = [ class AutoForm(QWidget): def __init__( self, - form_field_groups: Sequence[FormFieldGroup], + form_fields: Sequence[FormField], add_buttons: bool = True, ) -> None: super().__init__() @@ -1042,25 +1086,52 @@ class AutoForm(QWidget): padding: 0 5px; /* Etwas Luft links und rechts vom Text */ color: #334155; /* Dunkelgraue Schrift */ } + QComboBox { + border: 1px solid #cbd5e1; + border-radius: 4px; + padding: 5px; + } + QComboBox:disabled { + background-color: #f1f5f9; /* Helles System-Grau */ + color: #333D4B; /* Gut lesbare, aber gedeckte Schrift */ + border: 1px dashed #cbd5e1; /* Gestrichelter Rand wie beim Datum */ + } + QComboBox::drop-down:disabled { + border: none; + image: none; + } """) # --- LAYOUT --- self.main_layout = QVBoxLayout(self) self.main_layout.setContentsMargins(0, 0, 0, 0) - self.form_field_groups = form_field_groups + self.top_level_form_layout = QFormLayout() + self.main_layout.addLayout(self.top_level_form_layout) + self.top_level_form_layout.setSpacing(10) - self.widgets: dict[str, QWidget] = {} + self.form_fields = form_fields - for fg in form_field_groups: - _sub = fg.fields - if isinstance(_sub, FormFieldDynList): - widget = DynamicListWidget(_sub.fields, _sub.label) - else: - widget = FormGroupWidget(_sub, fg.label) - self.main_layout.addWidget(widget) - # TODO widget list is not updated with dynamic lists - # self._add_widgets(widget.widgets) - # self.widgets.update(form_group.widgets) + self.widget_registry: WidgetRegistry = {} + + self._build_ui_recursively( + self.form_fields, + self.top_level_form_layout, + self.widget_registry, + ) + + # TODO: REMOVE + # for fg in form_field_groups: + # _sub = fg.fields + # if isinstance(_sub, FormFieldDynList): + # widget = DynamicListWidget(_sub.fields, _sub.label) + # else: + # widget = FormGroupWidget(_sub, fg.label) + # self.main_layout.addWidget(widget) + # TODO widget list is not updated with dynamic lists + # self._add_widgets(widget.widgets) + # self.widgets.update(form_group.widgets) + # print("------------->>> Widget Registry") + # pprint(self.widget_registry) # buttons self.add_buttons = add_buttons @@ -1086,18 +1157,174 @@ class AutoForm(QWidget): self.reset_btn.clicked.connect(self.reset_form) self.layout_btn.addWidget(self.reset_btn) - def _add_widgets( + # def _add_widgets( + # self, + # widgets: dict[str, QWidget], + # ) -> None: + # current_keys = set(self.widget_registry.keys()) + # new_keys = set(widgets.keys()) + # shared_keys = current_keys.intersection(new_keys) + + # if shared_keys: + # raise ValueError(f"Tried to add fields with already assigned keys: {shared_keys}") + + # self.widget_registry.update(widgets) + + def _build_ui_recursively( self, - widgets: dict[str, QWidget], + schema: Sequence[FormField], + parent_layout: QFormLayout, + widget_registry: WidgetRegistry, + prefix: str = "", ) -> None: - current_keys = set(self.widgets.keys()) - new_keys = set(widgets.keys()) - shared_keys = current_keys.intersection(new_keys) + for field in schema: + full_key = f"{prefix}.{field.key}" if prefix else field.key - if shared_keys: - raise ValueError(f"Tried to add fields with already assigned keys: {shared_keys}") + widget: QWidget | None = None - self.widgets.update(widgets) + match field.type: + case FormFieldType.GROUP: + group_box = QGroupBox(field.label) + group_layout = QFormLayout(group_box) + self._build_ui_recursively( + schema=field.children, + parent_layout=group_layout, + widget_registry=widget_registry, + prefix=f"{full_key}", + ) + parent_layout.addRow(group_box) + + case FormFieldType.TEXT: + widget = QLineEdit() + if field.placeholder: + widget.setPlaceholderText(field.placeholder) + if field.fill_value: + widget.setText(field.fill_value) + if field.readonly: + widget.setReadOnly(True) # Falls es ein Fixwert ist + widget.setProperty("styleClass", "stempel") + + widget_registry[full_key] = { + "widget": widget, + "form_field": field, + } + + if field.tooltip: + tooltip_layout = self._add_tooltip(widget, field.tooltip) + parent_layout.addRow(field.label, tooltip_layout) + else: + parent_layout.addRow(field.label, widget) + + case FormFieldType.LONGTEXT: + widget = QPlainTextEdit() + widget.setMaximumHeight(80) # Kompakte Höhe für Formulare + if field.placeholder: + widget.setPlaceholderText(field.placeholder) + if field.readonly: + widget.setReadOnly(True) # Falls es ein Fixwert ist + widget.setProperty("styleClass", "stempel") + + widget_registry[full_key] = { + "widget": widget, + "form_field": field, + } + + if field.tooltip: + tooltip_layout = self._add_tooltip(widget, field.tooltip) + parent_layout.addRow(field.label, tooltip_layout) + else: + parent_layout.addRow(field.label, widget) + + case FormFieldType.DATE: + widget = QDateEdit() # Oder QDateEdit + widget.setCalendarPopup(True) + cal = widget.calendarWidget() + if cal: + cal.setFirstDayOfWeek(Qt.DayOfWeek.Monday) + cal.setGridVisible(True) + cal.setStyleSheet(""" + QCalendarWidget QAbstractItemView { + selection-background-color: #2980b9; + selection-color: white; + } + """) + if field.fill_value: + set_date = QDate.fromString(field.fill_value, "dd.MM.yyyy") + if not set_date.isValid(): + raise ValueError( + f"Could not parse date field value >{field.fill_value}<" + ) + widget.setDate(set_date) + else: + widget.setDate(QDate.currentDate()) + + if field.readonly: + widget = QLineEdit() + widget.setReadOnly(True) # Falls es ein Fixwert ist + widget.setProperty("styleClass", "stempel") + if field.fill_value: + widget.setText(field.fill_value) + + widget_registry[full_key] = { + "widget": widget, + "form_field": field, + } + + if field.tooltip: + tooltip_layout = self._add_tooltip(widget, field.tooltip) + parent_layout.addRow(field.label, tooltip_layout) + else: + parent_layout.addRow(field.label, widget) + + case FormFieldType.DROPDOWN: + widget = QComboBox() + assert field.options + widget.addItem("--- Bitte wählen ---") + widget.addItems(field.options) + if field.placeholder: + widget.setPlaceholderText(field.placeholder) + if field.fill_value: + widget.setCurrentText(field.fill_value) + else: + widget.setCurrentIndex(0) + if field.readonly: + widget.setEnabled(False) + widget.setProperty("styleClass", "stempel") + + widget_registry[full_key] = { + "widget": widget, + "form_field": field, + } + + if field.tooltip: + tooltip_layout = self._add_tooltip(widget, field.tooltip) + parent_layout.addRow(field.label, tooltip_layout) + else: + parent_layout.addRow(field.label, widget) + + # case FormFieldType.DYNAMIC_LIST: + # widget = DynamicListWidget(field.label) + + case _: + raise NotImplementedError(f"Not supported field type: {field.type.value}") + + @staticmethod + def _add_tooltip( + widget: QWidget, + tooltip: str, + ) -> QHBoxLayout: + field_layout = QHBoxLayout() + field_layout.setContentsMargins(0, 0, 0, 0) + field_layout.setSpacing(5) + info_btn = QPushButton("ℹ️") + info_btn.setFixedSize(28, 27) + info_btn.setFlat(True) + info_btn.setCursor(Qt.CursorShape.PointingHandCursor) + info_btn.setToolTip(tooltip) + field_layout.addWidget(widget) + field_layout.addWidget(info_btn) + + return field_layout def _disable_save(self) -> None: self.save_btn.setEnabled(False) @@ -1118,46 +1345,48 @@ class AutoForm(QWidget): # time.sleep(0.5) # Wir gehen unsere Feld-Definitionen durch - for fg in self.form_field_groups: - if isinstance(fg.fields, DynamicListWidget): + # for fg in self.form_fields: + # if isinstance(fg.fields, DynamicListWidget): + # continue + + for key, registry_entry in self.widget_registry.items(): # type: ignore + # widget = self.widget_registry[field.key] + widget = registry_entry["widget"] + form_field = registry_entry["form_field"] + + # 1. Zuerst setzen wir das Design des Feldes wieder auf "Normal" zurück. + # Falls der Nutzer den Fehler vorher schon korrigiert hat, muss der rote Rand weg! + if not form_field.readonly: + widget.setStyleSheet("") + + # 2. Ist es überhaupt ein Pflichtfeld? + if not form_field.required: continue - for field in fg.fields: # type: ignore - widget = self.widgets[field.key] + is_empty = False + if isinstance(widget, (QLineEdit, QDateEdit)): + if not widget.text().strip(): + is_empty = True - # 1. Zuerst setzen wir das Design des Feldes wieder auf "Normal" zurück. - # Falls der Nutzer den Fehler vorher schon korrigiert hat, muss der rote Rand weg! - if not field.readonly: - widget.setStyleSheet("") + elif isinstance(widget, QPlainTextEdit): + if not widget.toPlainText().strip(): + is_empty = True - # 2. Ist es überhaupt ein Pflichtfeld? - if not field.required: - continue + if not is_empty: + continue - is_empty = False - if isinstance(widget, (QLineEdit, QDateEdit)): - if not widget.text().strip(): - is_empty = True + error = form_field.label.replace("*", "").replace(":", "") + if form_field.parent is not None: + error = f"{form_field.parent.label}: {error}" + errors.append(error) - elif isinstance(widget, QPlainTextEdit): - if not widget.toPlainText().strip(): - is_empty = True - - if not is_empty: - continue - - error = field.label.replace("*", "").replace(":", "") - if fg.label: - error = f"{fg.label}: {error}" - errors.append(error) - - # Optisches Feedback: Heller roter Hintergrund und roter Rand - widget.setStyleSheet(""" - border: 1px solid #ef4444; - background-color: #ffe9e9; - padding: 4px; - border-radius: 4px; - """) + # Optisches Feedback: Heller roter Hintergrund und roter Rand + widget.setStyleSheet(""" + border: 1px solid #ef4444; + background-color: #ffe9e9; + padding: 4px; + border-radius: 4px; + """) # --- ERGEBNIS AUSWERTEN --- if errors: @@ -1177,43 +1406,50 @@ class AutoForm(QWidget): self._enable_save() def reset_form(self) -> None: - for fg in self.form_field_groups: - if isinstance(fg.fields, DynamicListWidget): + for key, registry_entry in self.widget_registry.items(): # type: ignore + # widget = self.widget_registry[field.key] + widget = registry_entry["widget"] + form_field = registry_entry["form_field"] + + # for fg in self.form_fields: + # if isinstance(fg.fields, DynamicListWidget): + # continue + + # for field in fg.fields: # type: ignore + # widget = self.widget_registry[field.key] + + if form_field.readonly: continue - for field in fg.fields: # type: ignore - widget = self.widgets[field.key] + if isinstance(widget, QLineEdit): + widget.clear() + if form_field.fill_value: + widget.setText(form_field.fill_value) + elif isinstance(widget, QPlainTextEdit): + widget.clear() + elif isinstance(widget, QDateEdit): + if form_field.fill_value: + set_date = QDate.fromString(form_field.fill_value, "dd.MM.yyyy") + if not set_date.isValid(): + raise ValueError( + f"Could not parse date field value >{form_field.fill_value}<" + ) + widget.setDate(set_date) + else: + widget.setDate(QDate.currentDate()) + elif isinstance(widget, QComboBox): + if form_field.fill_value: + widget.setCurrentText(form_field.fill_value) + else: + widget.setCurrentIndex(0) - if field.readonly: - continue - - if isinstance(widget, QLineEdit): - widget.clear() - if field.fill_value: - widget.setText(field.fill_value) - elif isinstance(widget, QPlainTextEdit): - widget.clear() - elif isinstance(widget, QDateEdit): - if field.fill_value: - set_date = QDate.fromString(field.fill_value, "dd.MM.yyyy") - if not set_date.isValid(): - raise ValueError( - f"Could not parse date field value >{field.fill_value}<" - ) - widget.setDate(set_date) - else: - widget.setDate(QDate.currentDate()) - elif isinstance(widget, QComboBox): - if field.fill_value: - widget.setCurrentText(field.fill_value) - - widget.setStyleSheet("") + widget.setStyleSheet("") def get_form_data(self) -> ...: raise NotImplementedError() data = {} - for key, widget in self.widgets.items(): + for key, widget in self.widget_registry.items(): if isinstance(widget, (QLineEdit, QDateEdit)): data[key] = widget.text() elif isinstance(widget, QPlainTextEdit): @@ -1839,7 +2075,7 @@ class SearchFormPage(QWidget): title.setStyleSheet("font-size: 14px; font-style: italic;") # font-weight: bold; container_layout.addWidget(title) # container_layout.addWidget(MyFormPart(FORM_FIELD_DEF, "Test-Gruppe")) - container_layout.addWidget(AutoForm(FORM_FIELD_GROUPS)) + container_layout.addWidget(AutoForm(FORM_FIELDS)) container_layout.addSpacing(30)