basic recursive build functionality

This commit is contained in:
2026-05-12 09:41:00 +02:00
parent 2c072e2252
commit 3561752d33

View File

@@ -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)