diff --git a/src/wce_crm/gui.py b/src/wce_crm/gui.py index 97f168b..a83d396 100644 --- a/src/wce_crm/gui.py +++ b/src/wce_crm/gui.py @@ -35,6 +35,7 @@ from PySide6.QtGui import QAction, QFont from PySide6.QtWidgets import ( QAbstractItemView, QApplication, + QBoxLayout, QButtonGroup, QCalendarWidget, QComboBox, @@ -272,19 +273,244 @@ class CustomWidget(QWidget): def validate_form_data(self) -> list[str]: ... +def _add_widget_to_layout( + form_field: FormField, + widget: QWidget, + target_layout: QLayout, + widget_only: bool = False, +) -> None: + + if isinstance(target_layout, QFormLayout): + if widget_only: + target_layout.addRow(widget) + elif form_field.tooltip: + tooltip_layout = _add_tooltip(widget, form_field.tooltip) + target_layout.addRow(form_field.label, tooltip_layout) + else: + target_layout.addRow(form_field.label, widget) + elif isinstance(target_layout, QBoxLayout): + if widget_only: + target_layout.addWidget(widget) + elif form_field.tooltip: + tooltip_layout = _add_tooltip(widget, form_field.tooltip) + target_layout.addLayout(tooltip_layout) + else: + target_layout.addWidget(widget) + + +def build_and_add_widget( + form_field: FormField, + parent_layout: QLayout, + prefix: str = "", +) -> tuple[str, QWidget]: + no_scroll_filter = NoScrollFilter(parent_layout) + if prefix and form_field.key: + full_key = f"{prefix}{COLUMN_SEP}{form_field.key}" if prefix else form_field.key + elif prefix and not form_field.key: + full_key = f"{prefix}" + else: + full_key = form_field.key + + widget: QWidget + layout_widget_only: bool = False + install_no_scroll_filter: bool = True + + match form_field.type: + case ( + FormFieldType.DYNAMIC_LIST + | FormFieldType.DYNAMIC_DROPDOWN_NUMERIC + | FormFieldType.DYNAMIC_DROPDOWN_OPTION + | FormFieldType.CUSTOM + ): + layout_widget_only = True + install_no_scroll_filter = False + + match form_field.type: + case FormFieldType.GROUP: + raise TypeError("FormField Groups are not supported for direct building") + + case FormFieldType.TEXT | FormFieldType.TEXT_DATE | FormFieldType.TEXT_DATETIME: + widget = QLineEdit() + if form_field.placeholder: + widget.setPlaceholderText(form_field.placeholder) + if form_field.fill_value: + widget.setText(form_field.fill_value) + if form_field.readonly: + widget.setReadOnly(True) + widget.setProperty("styleClass", "stempel") + + case FormFieldType.TEXT_SEARCH: + widget = QLineEdit() + if form_field.placeholder: + widget.setPlaceholderText(form_field.placeholder) + else: + widget.setPlaceholderText("Tippen zum Suchen...") + + search_completer = QCompleter() + search_completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive) + search_completer.setFilterMode(Qt.MatchFlag.MatchContains) + widget.setCompleter(search_completer) + + case FormFieldType.LONGTEXT: + widget = QPlainTextEdit() + widget.setMaximumHeight(80) + if form_field.placeholder: + widget.setPlaceholderText(form_field.placeholder) + if form_field.readonly: + widget.setReadOnly(True) + widget.setProperty("styleClass", "stempel") + + 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 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()) + + if form_field.readonly: + widget = QLineEdit() + widget.setReadOnly(True) + widget.setProperty("styleClass", "stempel") + if form_field.fill_value: + widget.setText(form_field.fill_value) + + case FormFieldType.DATE_FLEXIBLE: + widget = FlexibleDateInput() + 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.set_date(set_date) + + if form_field.readonly: + widget.setReadOnly(True) + + case FormFieldType.DATETIME_FLEXIBLE: + widget = FlexibleDateTimeInput() + if form_field.fill_value: + set_datetime = QDateTime.fromString(form_field.fill_value, "dd.MM.yyyy hh:mm") + if not set_datetime.isValid(): + raise ValueError( + f"Could not parse date field value >{form_field.fill_value}<" + ) + widget.set_datetime(set_datetime) + + if form_field.readonly: + widget.setReadOnly(True) + + case FormFieldType.DROPDOWN: + widget = QComboBox() + widget.addItem(DROPDOWN_DEFAULT, None) + for option in form_field.dropdown_options: + widget.addItem(option.label, option.data) + + if form_field.placeholder: + widget.setPlaceholderText(form_field.placeholder) + if form_field.fill_value: + widget.setCurrentText(form_field.fill_value) + else: + widget.setCurrentIndex(0) + if form_field.readonly: + widget.setEnabled(False) + widget.setProperty("styleClass", "stempel") + + case FormFieldType.EXTENDED_DROPDOWN: + widget = QComboBox() + widget.setEditable(True) + widget.setInsertPolicy(QComboBox.InsertPolicy.NoInsert) + completer = widget.completer() + assert completer + completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive) + completer.setFilterMode(Qt.MatchFlag.MatchContains) + completer.setCompletionMode(QCompleter.CompletionMode.PopupCompletion) + + widget.addItem(DROPDOWN_DEFAULT, None) + for option in form_field.dropdown_options: + widget.addItem(option.label, option.data) + + if form_field.placeholder: + line_edit = widget.lineEdit() + assert line_edit + line_edit.setPlaceholderText(form_field.placeholder) + if form_field.fill_value: + widget.setCurrentText(form_field.fill_value) + else: + widget.setCurrentIndex(-1) + if form_field.readonly: + widget.setEnabled(False) + widget.setProperty("styleClass", "stempel") + + case FormFieldType.DYNAMIC_LIST: + widget = DynamicListWidget( + form_field.children, + form_field.label, + prefix=f"{full_key}", + ) + + case FormFieldType.DYNAMIC_DROPDOWN_NUMERIC: + widget = DynamicDropdownWidgetNumeric( + form_field.children, + form_field.label, + prefix=f"{full_key}", + ) + + case FormFieldType.DYNAMIC_DROPDOWN_OPTION: + widget = DynamicDropdownWidgetOption( + form_field.children, + form_field.trigger_value, + form_field.label, + prefix=f"{full_key}", + ) + + case FormFieldType.CUSTOM: + widget_class = CUSTOM_WIDGETS[form_field.custom_widget] + widget = widget_class( + form_fields=form_field.children, + label=form_field.label, + prefix=f"{full_key}", + ) + + case _: + raise NotImplementedError(f"Not supported field type: {form_field.type.value}") + + if install_no_scroll_filter: + widget.installEventFilter(no_scroll_filter) + + _add_widget_to_layout(form_field, widget, parent_layout, widget_only=layout_widget_only) + + return full_key, widget + + def _build_ui_recursively( schema: Sequence[FormField], parent_layout: QFormLayout, widget_registry: WidgetRegistry, prefix: str = "", ) -> list[str]: - no_scroll_filter = NoScrollFilter(parent_layout) keys: list[str] = [] for field in schema: if prefix and field.key: full_key = f"{prefix}{COLUMN_SEP}{field.key}" if prefix else field.key elif prefix and not field.key: - full_key = f"{prefix}" + full_key = prefix else: full_key = field.key @@ -302,283 +528,13 @@ def _build_ui_recursively( ) parent_layout.addRow(group_box) - case FormFieldType.TEXT | FormFieldType.TEXT_DATE | FormFieldType.TEXT_DATETIME: - 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, - } - widget.installEventFilter(no_scroll_filter) - - 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.TEXT_SEARCH: - widget = QLineEdit() - if field.placeholder: - widget.setPlaceholderText(field.placeholder) - else: - widget.setPlaceholderText("Tippen zum Suchen...") - - search_completer = QCompleter() - search_completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive) - search_completer.setFilterMode(Qt.MatchFlag.MatchContains) - widget.setCompleter(search_completer) - - widget_registry[full_key] = { - "widget": widget, - "form_field": field, - } - widget.installEventFilter(no_scroll_filter) - - 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, - } - widget.installEventFilter(no_scroll_filter) - - 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, - } - widget.installEventFilter(no_scroll_filter) - - 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_FLEXIBLE: - widget = FlexibleDateInput() - 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.set_date(set_date) - - if field.readonly: - widget.setReadOnly(True) - - 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.DATETIME_FLEXIBLE: - widget = FlexibleDateTimeInput() - if field.fill_value: - set_datetime = QDateTime.fromString(field.fill_value, "dd.MM.yyyy hh:mm") - if not set_datetime.isValid(): - raise ValueError( - f"Could not parse date field value >{field.fill_value}<" - ) - widget.set_datetime(set_datetime) - - if field.readonly: - widget.setReadOnly(True) - - 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() - widget.addItem(DROPDOWN_DEFAULT, None) - for option in field.dropdown_options: - widget.addItem(option.label, option.data) - - 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, - } - widget.installEventFilter(no_scroll_filter) - - 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.EXTENDED_DROPDOWN: - widget = QComboBox() - widget.setEditable(True) - widget.setInsertPolicy(QComboBox.InsertPolicy.NoInsert) - completer = widget.completer() - assert completer - completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive) - completer.setFilterMode(Qt.MatchFlag.MatchContains) - completer.setCompletionMode(QCompleter.CompletionMode.PopupCompletion) - - widget.addItem(DROPDOWN_DEFAULT, None) - for option in field.dropdown_options: - widget.addItem(option.label, option.data) - - if field.placeholder: - line_edit = widget.lineEdit() - assert line_edit - line_edit.setPlaceholderText(field.placeholder) - if field.fill_value: - widget.setCurrentText(field.fill_value) - else: - widget.setCurrentIndex(-1) - if field.readonly: - widget.setEnabled(False) - widget.setProperty("styleClass", "stempel") - - widget_registry[full_key] = { - "widget": widget, - "form_field": field, - } - widget.installEventFilter(no_scroll_filter) - - 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 FormFieldType.DYNAMIC_DROPDOWN_NUMERIC: - widget = DynamicDropdownWidgetNumeric( - field.children, - field.label, - prefix=f"{full_key}", - ) - widget_registry[full_key] = { - "widget": widget, - "form_field": field, - } - parent_layout.addRow(widget) - - case FormFieldType.DYNAMIC_DROPDOWN_OPTION: - widget = DynamicDropdownWidgetOption( - field.children, - field.trigger_value, - field.label, - prefix=f"{full_key}", - ) - widget_registry[full_key] = { - "widget": widget, - "form_field": field, - } - parent_layout.addRow(widget) - - case FormFieldType.CUSTOM: - widget_class = CUSTOM_WIDGETS[field.custom_widget] - widget = widget_class( - form_fields=field.children, - label=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}") - - keys.append(full_key) + full_key, widget = build_and_add_widget(field, parent_layout, prefix=prefix) + widget_registry[full_key] = { + "widget": widget, + "form_field": field, + } + keys.append(full_key) return keys @@ -1589,6 +1545,11 @@ class FlexibleDateInput(QWidget): self.button = QPushButton("📅") self.button.setFixedWidth(30) + self.line_edit.setSizePolicy( + QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding + ) + self.button.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Expanding) + layout.addWidget(self.line_edit) layout.addWidget(self.button) @@ -1748,6 +1709,11 @@ class FlexibleDateTimeInput(QWidget): self.button = QPushButton("📅") self.button.setFixedWidth(30) + self.line_edit.setSizePolicy( + QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding + ) + self.button.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Expanding) + layout.addWidget(self.line_edit) layout.addWidget(self.button)