From 972dc07157f8de9820ae6ca1348c640088d30c60 Mon Sep 17 00:00:00 2001 From: forbes-0023 Date: Wed, 11 Feb 2026 15:14:38 -0600 Subject: [PATCH] feat(silo): replace modal new-item dialog with MDI pre-document tab Extract SchemaFormWidget from SchemaFormDialog so the creation form can be embedded as a plain QWidget in an MDI subwindow tab. Each Ctrl+N invocation opens a new tab alongside document tabs. On successful creation the pre-document tab closes and the real document opens in its place. - SchemaFormWidget emits item_created/cancelled signals - SchemaFormDialog preserved as thin modal wrapper for backward compat - Inline error display replaces modal QMessageBox - Live tab title updates from part number preview --- freecad/schema_form.py | 89 ++++++++++++++++++++++++++++++-------- freecad/silo_commands.py | 93 +++++++++++++++++++++++----------------- 2 files changed, 124 insertions(+), 58 deletions(-) diff --git a/freecad/schema_form.py b/freecad/schema_form.py index 4f30f84..a6cba20 100644 --- a/freecad/schema_form.py +++ b/freecad/schema_form.py @@ -1,8 +1,12 @@ -"""Schema-driven new-item dialog for Kindred Create. +"""Schema-driven new-item form for Kindred Create. Fetches schema data from the Silo REST API and builds a dynamic Qt form that mirrors the React ``CreateItemPane`` — category picker, property fields grouped by domain, live part number preview, and project tagging. + +The primary widget is :class:`SchemaFormWidget` (a plain ``QWidget``) +which can be embedded in an MDI tab, dock panel, or wrapped in the +backward-compatible :class:`SchemaFormDialog` modal. """ import json @@ -211,30 +215,29 @@ def _read_field(widget: QtWidgets.QWidget, prop_def: dict): # --------------------------------------------------------------------------- -# Main form dialog +# Embeddable form widget # --------------------------------------------------------------------------- -class SchemaFormDialog(QtWidgets.QDialog): - """Schema-driven new-item dialog. +class SchemaFormWidget(QtWidgets.QWidget): + """Schema-driven new-item form widget. - Fetches schema and property data from the Silo API, builds the form - dynamically, and returns the creation result on accept. + A plain ``QWidget`` that can be embedded in an MDI subwindow, dock + panel, or dialog. Emits :pyqt:`item_created` on successful creation + and :pyqt:`cancelled` when the user clicks Cancel. """ + item_created = QtCore.Signal(dict) + cancelled = QtCore.Signal() + def __init__(self, client, parent=None): super().__init__(parent) self._client = client - self._result = None self._prop_widgets = {} # {key: (widget, prop_def)} self._prop_groups = [] # list of _CollapsibleGroup to clear on category change self._categories = {} self._projects = [] - self.setWindowTitle("New Item") - self.setMinimumSize(600, 500) - self.resize(680, 700) - self._load_schema_data() self._build_ui() @@ -307,8 +310,18 @@ class SchemaFormDialog(QtWidgets.QDialog): root = QtWidgets.QVBoxLayout(self) root.setSpacing(8) + # Inline error label (hidden by default) + self._error_label = QtWidgets.QLabel() + self._error_label.setStyleSheet( + "background-color: #f38ba8; color: #1e1e2e; " + "padding: 8px; border-radius: 4px; font-weight: bold;" + ) + self._error_label.setAlignment(QtCore.Qt.AlignCenter) + self._error_label.hide() + root.addWidget(self._error_label) + # Part number preview banner - self._pn_label = QtWidgets.QLabel("Part Number: —") + self._pn_label = QtWidgets.QLabel("Part Number: \u2014") self._pn_label.setStyleSheet("font-size: 16px; font-weight: bold; padding: 8px;") self._pn_label.setAlignment(QtCore.Qt.AlignCenter) root.addWidget(self._pn_label) @@ -401,7 +414,7 @@ class SchemaFormDialog(QtWidgets.QDialog): btn_layout.addStretch() self._cancel_btn = QtWidgets.QPushButton("Cancel") - self._cancel_btn.clicked.connect(self.reject) + self._cancel_btn.clicked.connect(self.cancelled.emit) btn_layout.addWidget(self._cancel_btn) self._create_btn = QtWidgets.QPushButton("Create") @@ -506,8 +519,10 @@ class SchemaFormDialog(QtWidgets.QDialog): pn = self._generate_pn_preview(category) if pn: self._pn_label.setText(f"Part Number: {pn}") + self.setWindowTitle(f"New: {pn}") else: self._pn_label.setText(f"Part Number: {category}-????") + self.setWindowTitle(f"New: {category}-????") # -- submission --------------------------------------------------------- @@ -550,10 +565,12 @@ class SchemaFormDialog(QtWidgets.QDialog): def _on_create(self): """Validate and submit the form.""" + self._error_label.hide() data = self._collect_form_data() if not data["category"]: - QtWidgets.QMessageBox.warning(self, "Validation", "Category is required.") + self._error_label.setText("Category is required.") + self._error_label.show() return try: @@ -563,11 +580,47 @@ class SchemaFormDialog(QtWidgets.QDialog): data["description"], projects=data["projects"], ) - self._result = result - self._result["_form_data"] = data - self.accept() + result["_form_data"] = data + self.item_created.emit(result) except Exception as e: - QtWidgets.QMessageBox.critical(self, "Error", f"Failed to create item:\n{e}") + self._error_label.setText(f"Failed to create item: {e}") + self._error_label.show() + + +# --------------------------------------------------------------------------- +# Modal dialog wrapper (backward compatibility) +# --------------------------------------------------------------------------- + + +class SchemaFormDialog(QtWidgets.QDialog): + """Modal dialog wrapper around :class:`SchemaFormWidget`. + + Provides the same ``exec_and_create()`` API as the original + implementation for callers that still need blocking modal behavior. + """ + + def __init__(self, client, parent=None): + super().__init__(parent) + self.setWindowTitle("New Item") + self.setMinimumSize(600, 500) + self.resize(680, 700) + self._result = None + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + self._form = SchemaFormWidget(client, parent=self) + self._form.item_created.connect(self._on_created) + self._form.cancelled.connect(self.reject) + layout.addWidget(self._form) + + @property + def _desc_edit(self): + """Expose description field for pre-fill by callers.""" + return self._form._desc_edit + + def _on_created(self, result): + self._result = result + self.accept() def exec_and_create(self): """Show dialog and return the creation result, or None if cancelled.""" diff --git a/freecad/silo_commands.py b/freecad/silo_commands.py index bc976ef..2df4899 100644 --- a/freecad/silo_commands.py +++ b/freecad/silo_commands.py @@ -736,7 +736,13 @@ class Silo_Open: class Silo_New: - """Create new item with part number.""" + """Create new item with part number. + + Opens a pre-document MDI tab containing the schema-driven creation + form. Each invocation opens a new tab so multiple items can be + prepared in parallel. On successful creation the tab closes and + the real document opens in its place. + """ def GetResources(self): return { @@ -746,53 +752,60 @@ class Silo_New: } def Activated(self): - from PySide import QtGui + from PySide import QtGui, QtWidgets - from schema_form import SchemaFormDialog + from schema_form import SchemaFormWidget - sel = FreeCADGui.Selection.getSelection() - - dlg = SchemaFormDialog(_client, parent=FreeCADGui.getMainWindow()) - - # Pre-fill description from selected object - if sel: - dlg._desc_edit.setText(sel[0].Label) - - result = dlg.exec_and_create() - if result is None: + mw = FreeCADGui.getMainWindow() + mdi = mw.findChild(QtWidgets.QMdiArea) + if not mdi: return - part_number = result["part_number"] - form_data = result.get("_form_data", {}) - selected_projects = form_data.get("projects") or [] + # Each invocation creates a new pre-document tab + form = SchemaFormWidget(_client) - try: - if sel: - # Tag selected object - obj = sel[0] - set_silo_properties( - obj, - { - "SiloItemId": result.get("id", ""), - "SiloPartNumber": part_number, - "SiloRevision": 1, - }, - ) - obj.Label = part_number - _sync.save_to_canonical_path(FreeCAD.ActiveDocument, force_rename=True) - else: - # Create new document - _sync.create_document_for_item(result, save=True) + # Pre-fill description from current selection + sel = FreeCADGui.Selection.getSelection() + if sel: + form._desc_edit.setText(sel[0].Label) - msg = f"Part number: {part_number}" - if selected_projects: - msg += f"\nTagged with projects: {', '.join(selected_projects)}" + # Add as MDI subwindow (appears as a tab alongside documents) + sw = mdi.addSubWindow(form) + sw.setWindowTitle("New Item") + sw.setWindowIcon(QtGui.QIcon(_icon("new"))) + sw.show() + mdi.setActiveSubWindow(sw) - FreeCAD.Console.PrintMessage(f"Created: {part_number}\n") - QtGui.QMessageBox.information(None, "Item Created", msg) + # On creation: process result, close tab, open real document + def _on_created(result): + part_number = result["part_number"] - except Exception as e: - QtGui.QMessageBox.critical(None, "Error", str(e)) + try: + sel_now = FreeCADGui.Selection.getSelection() + if sel_now: + obj = sel_now[0] + set_silo_properties( + obj, + { + "SiloItemId": result.get("id", ""), + "SiloPartNumber": part_number, + "SiloRevision": 1, + }, + ) + obj.Label = part_number + _sync.save_to_canonical_path(FreeCAD.ActiveDocument, force_rename=True) + else: + _sync.create_document_for_item(result, save=True) + + FreeCAD.Console.PrintMessage(f"Created: {part_number}\n") + except Exception as e: + FreeCAD.Console.PrintError(f"Failed to process created item: {e}\n") + + # Close the pre-document tab + sw.close() + + form.item_created.connect(_on_created) + form.cancelled.connect(sw.close) def IsActive(self): return _server_mode == "normal"