diff --git a/prototypes/t_qt_2.py b/prototypes/t_qt_2.py index a6e3419..478b0dc 100644 --- a/prototypes/t_qt_2.py +++ b/prototypes/t_qt_2.py @@ -7,7 +7,7 @@ import time import uuid from collections.abc import Sequence from pprint import pprint -from typing import TypeAlias, TypedDict +from typing import Any, Protocol, TypeAlias, TypedDict from PySide6.QtCore import QDate, QModelIndex, QStringListModel, Qt, QTimer, Signal from PySide6.QtGui import QAction, QStandardItem, QStandardItemModel @@ -1051,6 +1051,12 @@ FORM_FIELDS = [ key="data_contact_person", children=FORM_FIELDS_CONTACT_PERSON, ), + FormField( + "Schulbildung", + FormFieldType.DYNAMIC_LIST, + children=FORM_FIELDS_SCHOOL, + key="Schulbildung", + ), # FormFieldGroup("Daten Kontaktperson", FORM_FIELDS_CONTACT_PERSON), # FormFieldGroup("Stammdaten (ERGÄNZUNG KINDERALTER DYNAMISCH)", FORM_FIELDS_MASTER_DATA), # FormFieldGroup("weitere Informationen", FORM_FIELDS_ADDITIONAL_DATA), @@ -1063,6 +1069,225 @@ FORM_FIELDS = [ ] +class CustomForm(Protocol): + def get_form_data(self) -> dict[str, Any]: ... + + def reset_form(self) -> None: ... + + +def _build_ui_recursively( + schema: Sequence[FormField], + parent_layout: QFormLayout, + widget_registry: WidgetRegistry, + prefix: str = "", +) -> None: + for field in schema: + full_key = f"{prefix}.{field.key}" if prefix else field.key + widget: QWidget + + match field.type: + case FormFieldType.GROUP: + group_box = QGroupBox(field.label) + group_layout = QFormLayout(group_box) + _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) + widget.setProperty("styleClass", "stempel") + + widget_registry[full_key] = { + "widget": widget, + "form_field": field, + } + + if field.tooltip: + tooltip_layout = _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) + if field.placeholder: + widget.setPlaceholderText(field.placeholder) + if field.readonly: + widget.setReadOnly(True) + widget.setProperty("styleClass", "stempel") + + widget_registry[full_key] = { + "widget": widget, + "form_field": field, + } + + if field.tooltip: + tooltip_layout = _add_tooltip(widget, field.tooltip) + parent_layout.addRow(field.label, tooltip_layout) + else: + parent_layout.addRow(field.label, widget) + + case FormFieldType.DATE: + widget = 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) + 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 = _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 = _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.children, + field.label, + prefix=f"{full_key}", + ) + widget_registry[full_key] = { + "widget": widget, + "form_field": field, + } + parent_layout.addRow(widget) + + case _: + raise NotImplementedError(f"Not supported field type: {field.type.value}") + + +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 reset_form( + widget_registry: WidgetRegistry, +) -> None: + for registry_entry in widget_registry.values(): # type: ignore + widget = registry_entry["widget"] + form_field = registry_entry["form_field"] + + if form_field.readonly: + continue + + 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) + elif isinstance(widget, DynamicListWidget): + # dynamic list widget manages its widgets by itself + widget.reset_form() + + widget.setStyleSheet("") + + +def _insert_nested( + target_dict: dict[str, Any], + key_path: Sequence[str], + value: Any, +): + # keys 'a.b.c = 1' --> {'a': {'b': {'c': 1}}} + for part in key_path[:-1]: + target_dict = target_dict.setdefault(part, {}) + target_dict[key_path[-1]] = value + + class AutoForm(QWidget): def __init__( self, @@ -1110,30 +1335,15 @@ class AutoForm(QWidget): self.top_level_form_layout.setSpacing(10) self.form_fields = form_fields - self.widget_registry: WidgetRegistry = {} - self._build_ui_recursively( + _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 + # buttons (save and reset) self.add_buttons = add_buttons if self.add_buttons: self.layout_btn = QHBoxLayout() @@ -1157,175 +1367,6 @@ class AutoForm(QWidget): self.reset_btn.clicked.connect(self.reset_form) self.layout_btn.addWidget(self.reset_btn) - # 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, - schema: Sequence[FormField], - parent_layout: QFormLayout, - widget_registry: WidgetRegistry, - prefix: str = "", - ) -> None: - for field in schema: - full_key = f"{prefix}.{field.key}" if prefix else field.key - - widget: QWidget | None = None - - 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) self.save_btn.setText(self.save_btn_txt_disabled) @@ -1342,45 +1383,28 @@ class AutoForm(QWidget): self._disable_save() errors: list[str] = [] - # time.sleep(0.5) - - # Wir gehen unsere Feld-Definitionen durch - # 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] + for registry_entry in self.widget_registry.values(): 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 - is_empty = False if isinstance(widget, (QLineEdit, QDateEdit)): - if not widget.text().strip(): - is_empty = True - + if widget.text().strip(): + continue elif isinstance(widget, QPlainTextEdit): - if not widget.toPlainText().strip(): - is_empty = True - - if not is_empty: - continue + if widget.toPlainText().strip(): + continue error = form_field.label.replace("*", "").replace(":", "") if form_field.parent is not None: error = f"{form_field.parent.label}: {error}" errors.append(error) - - # Optisches Feedback: Heller roter Hintergrund und roter Rand + # optical feedback to highlight erroneous cells widget.setStyleSheet(""" border: 1px solid #ef4444; background-color: #ffe9e9; @@ -1388,9 +1412,8 @@ class AutoForm(QWidget): border-radius: 4px; """) - # --- ERGEBNIS AUSWERTEN --- if errors: - # Es gibt Fehler! Speichern abbrechen und Pop-up anzeigen. + # errors: abort saving and show pop up window error_text = ( "Bitte füllen Sie die folgenden Pflichtfelder aus:\n\n- " + "\n- ".join(errors) @@ -1399,89 +1422,118 @@ class AutoForm(QWidget): self._enable_save() return - # Wenn wir hier ankommen, ist die Liste 'errors' leer. Alles ist korrekt ausgefüllt! - # time.sleep(0.5) + # no errors: data can be saved + # TODO: change routine to trigger data saving in the backend print("Erfolg! Alle Daten sind valide.") + print("Get form data call...") + form_data = self.get_form_data() + print("------------>>>>>>>>> Get form data") + pprint(form_data) + # ------------------------------------------------------------ + + # !! keep this code must be called again self.reset_form() self._enable_save() def reset_form(self) -> None: - for key, registry_entry in self.widget_registry.items(): # type: ignore - # widget = self.widget_registry[field.key] + reset_form(self.widget_registry) + + def get_form_data(self) -> dict[str, Any]: + # raise NotImplementedError() + + raw_data = {} + for key, registry_entry in self.widget_registry.items(): 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 - if isinstance(widget, QLineEdit): - widget.clear() - if form_field.fill_value: - widget.setText(form_field.fill_value) + value = widget.text() elif isinstance(widget, QPlainTextEdit): - widget.clear() + value = widget.toPlainText() 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()) + qt_date = widget.date() + value = qt_date.toPython() elif isinstance(widget, QComboBox): - if form_field.fill_value: - widget.setCurrentText(form_field.fill_value) - else: - widget.setCurrentIndex(0) + # TODO add + ... + # value = widget.toPlainText() + elif isinstance(widget, DynamicListWidget): # Unser Custom Widget + # TODO add method + # value = widget.get_data() + value = "test" + else: + value = None - widget.setStyleSheet("") + _insert_nested(raw_data, key.split("."), value) - def get_form_data(self) -> ...: - raise NotImplementedError() + return raw_data - data = {} - for key, widget in self.widget_registry.items(): - if isinstance(widget, (QLineEdit, QDateEdit)): - data[key] = widget.text() - elif isinstance(widget, QPlainTextEdit): - data[key] = widget.toPlainText() - return data +@dc.dataclass(slots=True) +class SubForm: + entry_box: QGroupBox + prefix_parent: str + index: int + prefix: str = "" + + def __post_init__(self) -> None: + self.prefix = f"{self.prefix_parent}-[{self.index}]" + + def update_index(self, new_idx: int) -> None: + self.index = new_idx + self.prefix = f"{self.prefix_parent}-[{self.index}]" + + +def change_sub_form_widget_registry( + widget_registry: WidgetRegistry, + sub_form: SubForm, + new_idx: int, +) -> None: + if sub_form.index == new_idx: + return + + old_key_part = sub_form.prefix + new_key_part = f"{sub_form.prefix_parent}-[{new_idx}]" + + for key in tuple(widget_registry.keys()): + splitted = key.split(".") + key_part, rest = splitted[0], splitted[1:] + + if key_part == old_key_part: + old_key = ".".join([old_key_part] + rest) + new_key = ".".join([new_key_part] + rest) + widget_registry[new_key] = widget_registry[old_key] + del widget_registry[key] + + sub_form.update_index(new_idx) class DynamicListWidget(QWidget): """ - Ein Widget, das dynamisch beliebig viele Sub-Formulare - (z.B. Adressen) erzeugen und verwalten kann. + A Widget, which can generate and manage an arbitrary number of sub forms. """ - def __init__(self, fields: Sequence[FormField], title="Eintrag"): + def __init__( + self, + form_fields: Sequence[FormField], + label: str = "Eintrag", + prefix: str = "", + ): super().__init__() - self.fields = fields - self.title = title - self.widgets: dict[str, QWidget] = {} + self.form_fields = form_fields + self.label = label + self.prefix = prefix + self.widget_registry: WidgetRegistry = {} - # Das Hauptlayout für diese Liste - self.group_box = QGroupBox(title) + # layout for group component + self.group_box = QGroupBox(label) self.main_layout = QVBoxLayout(self) self.main_layout.setContentsMargins(0, 0, 0, 0) + # inner layout which contains sub forms and buttons self.inner_layout = QVBoxLayout() self.main_layout.addWidget(self.group_box) self.group_box.setLayout(self.inner_layout) - - # Liste, in der wir uns die generierten Sub-Formulare merken - self.sub_forms = [] - - # Der "+ Hinzufügen" Button + # sub forms + self.sub_forms: list[SubForm] = [] + # button to add more sub forms self.add_btn = QPushButton("+ Weitere hinzufügen") self.add_btn.setStyleSheet( "color: #0369a1; font-weight: bold; border: 1px dashed #0369a1; padding: 5px;" @@ -1490,64 +1542,68 @@ class DynamicListWidget(QWidget): self.inner_layout.addWidget(self.add_btn) - # Direkt beim Start ein leeres Formular anzeigen (optional) + # add empty sub form as initial value self.add_entry() def add_entry(self): - """Erzeugt ein neues Sub-Formular und fügt es VOR dem Plus-Button ein.""" - - # 1. Den Rahmen für das einzelne Sub-Formular bauen - entry_box = QGroupBox(f"{self.title} {len(self.sub_forms) + 1}") + number_form = len(self.sub_forms) + 1 + entry_box = QGroupBox(f"{self.label} {number_form}") entry_layout = QHBoxLayout(entry_box) + sub_form = SubForm(entry_box, prefix_parent=self.prefix, index=number_form) - # TEST auto form generator - # TODO: keys may not be fixed - form_widget = FormGroupWidget(self.fields, None) - self.widgets.update(form_widget.widgets) + form_layout = QFormLayout() + _build_ui_recursively( + schema=self.form_fields, + parent_layout=form_layout, + widget_registry=self.widget_registry, + prefix=f"{self.prefix}-[{number_form}]", + ) - # 2. Das eigentliche Formular (hier könntest du auch deinen automatisierten - # Form-Generator rekursiv aufrufen!) - # target_layout = QFormLayout() - # street_input = QLineEdit() - # city_input = QLineEdit() - # target_layout.addRow("Straße:", street_input) - # target_layout.addRow("Stadt:", city_input) - - # 3. Den "Löschen" (-) Button bauen del_btn = QPushButton("🗑️") del_btn.setFixedSize(30, 30) - # Lambda mit default-Parameter, um genau DIESE entry_box zu löschen - del_btn.clicked.connect(lambda checked=False, box=entry_box: self.remove_entry(box)) + # Lambda with default parameter to delete exactly this(!) box + del_btn.clicked.connect(lambda checked=False, form=sub_form: self.remove_entry(form)) - # 4. Alles zusammensetzen - # entry_layout.addLayout(target_layout) - entry_layout.addWidget(form_widget) + entry_layout.addLayout(form_layout) entry_layout.addWidget(del_btn) - # 5. Daten merken, um sie später auszulesen - self.sub_forms.append({"box": entry_box, "form_widget": form_widget}) + self.sub_forms.append(sub_form) + self.inner_layout.insertWidget(self.inner_layout.count() - 1, entry_box) + self.update_sub_forms() - # 6. In das Hauptlayout einfügen (Index - 1 bedeutet: VOR dem "+" Button) - self.inner_layout.insertWidget(self.main_layout.count() - 1, entry_box) - self.update_titles() + # print("------------>>>>>>>>> Added entry, length widget registry:") + # pprint(len(self.widget_registry)) + # pprint(list(self.widget_registry.keys())) - def remove_entry(self, box_to_remove: QGroupBox): - """Löscht ein Sub-Formular wieder.""" - # Das Widget aus dem Layout entfernen und zerstören - self.main_layout.removeWidget(box_to_remove) - box_to_remove.deleteLater() + def remove_entry( + self, + subform_to_remove: SubForm, + ): + self.inner_layout.removeWidget(subform_to_remove.entry_box) + subform_to_remove.entry_box.deleteLater() + self.sub_forms.remove(subform_to_remove) + self.update_sub_forms() + # print("------------>>>>>>>>> Removed entry, length widget registry:") + # pprint(len(self.widget_registry)) + # pprint(list(self.widget_registry.keys())) - # Aus unserer internen Liste filtern - self.sub_forms = [f for f in self.sub_forms if f["box"] != box_to_remove] - self.update_titles() + def update_sub_forms(self): + for index, sub_form in enumerate(self.sub_forms, start=1): + sub_form.entry_box.setTitle(f"{self.label} {index}") + change_sub_form_widget_registry( + self.widget_registry, + sub_form, + index, + ) - def update_titles(self): - """Nummeriert die Boxen nach dem Löschen neu durch.""" - for index, form in enumerate(self.sub_forms): - form["box"].setTitle(f"{self.title} {index + 1}") + def reset_form(self) -> None: + reset_form(self.widget_registry) - def get_data(self): - """Liest die Daten aller dynamisch erzeugten Formulare aus!""" + def load_form_data(self) -> None: + # TODO add way to load data when initialised (probably with click) + ... + + def get_form_data(self): raise NotImplementedError() results = [] @@ -1561,184 +1617,10 @@ class DynamicListWidget(QWidget): return results -class FormGroupWidget(QWidget): - def __init__( - self, - form_fields: Sequence[FormField], - group_name: str | None = None, - ): - super().__init__() - self.setStyleSheet(""" - QGroupBox { - font-size: 16px; - font-weight: bold; - border: 1px solid #cbd5e1; /* Heller, moderner Rahmen */ - border-radius: 8px; /* Abgerundete Ecken */ - margin-top: 15px; /* Platz für die Überschrift schaffen */ - padding-top: 15px; /* Abstand zwischen Rahmen und erstem Feld */ - } - QGroupBox::title { - subcontrol-origin: margin; - subcontrol-position: top left; /* Überschrift oben links */ - 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.inner_layout = QVBoxLayout() - # self.inner_layout.setContentsMargins(0, 0, 0, 0) - self.form_layout = QFormLayout() - self.inner_layout.addLayout(self.form_layout) - self.group_box: QGroupBox | None = None - self.group_name = group_name - - if self.group_name: - self.group_box = QGroupBox(group_name) - self.main_layout.addWidget(self.group_box) - self.group_box.setLayout(self.inner_layout) - # self.group_box.setLayout(self.form_layout) - else: - # self.main_layout.addLayout(self.form_layout) - self.main_layout.addLayout(self.inner_layout) - - self.form_layout.setSpacing(10) # Abstand zwischen den Zeilen - - self.form_fields = form_fields - self.widgets: dict[str, QWidget] = {} - # automatic build - self.create_form_fields() - - def create_form_fields(self) -> None: - for field in self.form_fields: - widget: QWidget | None = None - - match field.type: - 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") - - 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") - - 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) - - 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") - - # case FormFieldType.DYNAMIC_LIST: - # widget = DynamicListWidget(field.label) - - case _: - raise NotImplementedError(f"Not supported field type: {field.type.value}") - - self.widgets[field.key] = widget - - # if field.type is FormFieldType.DYNAMIC_LIST: - # self.inner_layout.addWidget(widget) - # continue - - if not field.tooltip: - self.form_layout.addRow(field.label, widget) - continue - - field_layout = QHBoxLayout() - field_layout.setContentsMargins(0, 0, 0, 0) - field_layout.setSpacing(5) - info_btn = QPushButton("ℹ️") - # info_btn = QPushButton("?") - info_btn.setFixedSize(28, 27) - info_btn.setFlat(True) - info_btn.setCursor(Qt.CursorShape.PointingHandCursor) - info_btn.setToolTip(field.tooltip) - field_layout.addWidget(widget) - field_layout.addWidget(info_btn) - - self.form_layout.addRow(field.label, field_layout) - - -# from PySide6.QtWidgets import ( -# QFormLayout, -# QGroupBox, -# QHBoxLayout, -# QLineEdit, -# QPushButton, -# QVBoxLayout, -# QWidget, -# ) - - class ClickableCell(QFrame): - # Wir definieren ein Signal, das ein Dictionary (die Daten) mitschickt - clicked = Signal(dict) + """cell in the table on the startup screen""" + + clicked = Signal(dict) # Signal which also sends data as dictionary def __init__(self, text, data_record): super().__init__() @@ -1785,51 +1667,9 @@ class HeaderCell(QLabel): """) -class NewEntryDialog(QDialog): - def __init__(self, parent=None): - super().__init__(parent) - self.setWindowTitle("Neuer Eintrag") - self.setMinimumWidth(350) - - # Ein FormLayout richtet Labels und Eingabefelder automatisch sauber aus - layout = QFormLayout(self) - - # Eingabefelder erstellen - self.input_c1 = QLineEdit() - self.input_c2 = QLineEdit() - self.input_c3 = QLineEdit() - self.input_c4 = QLineEdit() - self.input_date = QLineEdit() - - # Felder zum Layout hinzufügen - layout.addRow("Name:", self.input_c1) - layout.addRow("Beschreibung:", self.input_c2) - layout.addRow("Abteilung (optional):", self.input_c3) - layout.addRow("Status:", self.input_c4) - layout.addRow("Datum:", self.input_date) - - # Standard-Buttons (OK und Abbrechen) - buttons = QDialogButtonBox( - QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel - ) - buttons.accepted.connect(self.accept) # Schließt Dialog und meldet "Erfolg" - buttons.rejected.connect(self.reject) # Schließt Dialog und meldet "Abbruch" - layout.addWidget(buttons) - - def get_data(self): - # Liest die Textfelder aus und gibt sie als Dictionary zurück - return { - "c1": self.input_c1.text(), - "c2": self.input_c2.text(), - # Wenn das Feld leer ist, speichern wir None (für unseren Platzhalter-Effekt) - "c3": self.input_c3.text() if self.input_c3.text().strip() else None, - "c4": self.input_c4.text(), - "date": self.input_date.text(), - } - - -# --- 2. DIE DETAIL-ANSICHT (SEITE 2) --- class DetailView(QWidget): + """example only: show what happens if a cell was clicked on the start up page""" + back_requested = Signal() # Signal für den Zurück-Button def __init__(self): @@ -1867,9 +1707,11 @@ class DetailView(QWidget): class NewEntrySelect_view(QWidget): - back_requested = Signal() # Signal für den Zurück-Button - company_requested = Signal() # Signal Unternehmen - person_requested = Signal() # Signal Unternehmen + """view for new entry for initial recording ("Grunderfassung")""" + + back_requested = Signal() # Signal back button + company_requested = Signal() # Signal "Unternehmen" + person_requested = Signal() # Signal "Individualperson" def __init__(self): super().__init__() @@ -2079,68 +1921,12 @@ class SearchFormPage(QWidget): container_layout.addSpacing(30) - # container_layout.addWidget(DropdownSearch()) - - container_layout.addSpacing(30) - - # --- SUCH-FORMULAR --- - form_layout = ( - QHBoxLayout() - ) # Horizontal, damit Suchen und Filtern nebeneinander stehen - - # Texteingabe für die Suche - self.search_input = QLineEdit() - self.search_input.setPlaceholderText("Tippe zum Suchen...") - self.search_input.textChanged.connect(self.perform_search) # LIVE-SUCHE! - - # Ein Dropdown als Filter-Beispiel - self.status_filter = QComboBox() - self.status_filter.addItems(["Alle", "Aktiv", "Wartend", "Abgeschlossen"]) - self.status_filter.currentTextChanged.connect(self.perform_search) - - form_layout.addWidget(QLabel("Suchbegriff:")) - form_layout.addWidget(self.search_input, stretch=2) - form_layout.addWidget(QLabel("Status:")) - form_layout.addWidget(self.status_filter, stretch=1) - - container_layout.addLayout(form_layout) - - # --- 3. ERGEBNIS-BEREICH --- - # Für den Anfang ein einfaches Listen-Widget - self.results_list = QListWidget() - container_layout.addWidget( - self.results_list, stretch=100 - ) # Nimmt den restlichen Platz ein - - # Dummy-Datenbank für das Beispiel - self.database = [ - "Projekt Alpha (Aktiv)", - "Projekt Beta (Abgeschlossen)", - "Projekt Gamma (Wartend)", - ] - self.perform_search() # Initiale Ansicht laden - def update_contact_persons( self, company_id: int, ) -> None: self.contact_person_search.update_search_data(company_id) - def perform_search(self): - # 1. Eingaben auslesen - query = self.search_input.text().lower() - status = self.status_filter.currentText() - - # 2. Alte Ergebnisse löschen - self.results_list.clear() - - # 3. Daten filtern und anzeigen - for item in self.database: - # Einfache Filter-Logik - if query in item.lower(): - if status == "Alle" or status in item: - self.results_list.addItem(item) - # 2. Das Hauptfenster mit dem Grid-Layout class MainWindow(QMainWindow): @@ -2335,36 +2121,17 @@ class MainWindow(QMainWindow): # --- MENÜ LOGIK --- def create_menu(self): menu_bar = self.menuBar() + # file menu file_menu = menu_bar.addMenu("Datei") - - new_action = QAction("Neuer Eintrag...", self) - new_action.setShortcut("Ctrl+N") - # VERKNÜPFUNG: Wenn geklickt, rufe die Methode zum Öffnen des Dialogs auf - new_action.triggered.connect(self.open_new_entry_dialog) - file_menu.addAction(new_action) - - file_menu.addSeparator() - exit_action = QAction("Beenden", self) exit_action.setShortcut("Ctrl+Q") exit_action.triggered.connect(self.close) file_menu.addAction(exit_action) - - # 2. Hauptmenü-Punkt: "Hilfe" + # help menu help_menu = menu_bar.addMenu("Hilfe") about_action = QAction("Über", self) help_menu.addAction(about_action) - # --- DIALOG AUFRUFEN & DATEN ÜBERNEHMEN --- - def open_new_entry_dialog(self): - dialog = NewEntryDialog(self) - - # .exec() pausiert das Programm, bis der Dialog geschlossen wird. - # Es gibt True zurück, wenn der Nutzer "OK" geklickt hat. - if dialog.exec(): - new_data = dialog.get_data() # Dictionary aus dem Dialog holen - self.add_row_to_grid(new_data) # Ins Grid zeichnen - if __name__ == "__main__": app = QApplication(sys.argv)