diff --git a/src/Mod/Create/CMakeLists.txt b/src/Mod/Create/CMakeLists.txt index 6ca61d5950..fea7defe9e 100644 --- a/src/Mod/Create/CMakeLists.txt +++ b/src/Mod/Create/CMakeLists.txt @@ -25,6 +25,7 @@ install( silo_document.py silo_objects.py silo_tree.py + silo_viewers.py silo_viewproviders.py update_checker.py ${CMAKE_CURRENT_BINARY_DIR}/version.py diff --git a/src/Mod/Create/silo_viewers.py b/src/Mod/Create/silo_viewers.py new file mode 100644 index 0000000000..a5cd4a5fd4 --- /dev/null +++ b/src/Mod/Create/silo_viewers.py @@ -0,0 +1,151 @@ +""" +silo_viewers.py — Read-only MDI viewer widgets for Silo tree leaf nodes. + +Each viewer is a plain QWidget suitable for embedding in an MDI subwindow. +The ``create_viewer_widget`` factory routes a SiloViewerObject to the +appropriate viewer class based on its SiloPath property. +""" + +import json + +import FreeCAD +from PySide import QtCore, QtWidgets + +# --------------------------------------------------------------------------- +# Manifest Viewer +# --------------------------------------------------------------------------- + +_MANIFEST_FIELDS = [ + ("Part UUID", "part_uuid", True), + ("Silo Instance", "silo_instance", True), + ("Revision Hash", "revision_hash", False), + ("KC Version", "kc_version", False), + ("Created", "created_at", False), + ("Modified", "modified_at", False), + ("Created By", "created_by", False), +] + + +class SiloManifestViewer(QtWidgets.QWidget): + """Read-only form displaying ``silo/manifest.json`` fields.""" + + WINDOW_TITLE = "Silo \u2014 Manifest" + + def __init__(self, obj, parent=None): + super().__init__(parent) + self.setObjectName(f"SiloViewer_{obj.Name}") + self._build_ui(obj.RawContent) + + # -- layout -------------------------------------------------------------- + + def _build_ui(self, raw_content): + try: + data = json.loads(raw_content) + except Exception: + data = {} + + outer = QtWidgets.QVBoxLayout(self) + outer.setContentsMargins(16, 16, 16, 16) + outer.setSpacing(12) + + title = QtWidgets.QLabel("Silo Manifest") + font = title.font() + font.setPointSize(font.pointSize() + 2) + font.setBold(True) + title.setFont(font) + outer.addWidget(title) + + line = QtWidgets.QFrame() + line.setFrameShape(QtWidgets.QFrame.HLine) + line.setFrameShadow(QtWidgets.QFrame.Sunken) + outer.addWidget(line) + + form = QtWidgets.QFormLayout() + form.setLabelAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + form.setHorizontalSpacing(16) + form.setVerticalSpacing(8) + outer.addLayout(form) + + for label_text, key, copyable in _MANIFEST_FIELDS: + raw_val = str(data.get(key, "") or "") + display_val = _format_value(key, raw_val) + _add_row(form, label_text, display_val, raw_val, copyable) + + outer.addStretch() + + # -- no state to serialize ----------------------------------------------- + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _format_value(key, value): + """Format a manifest value for display.""" + if not value: + return "\u2014" # em-dash + if key in ("created_at", "modified_at"): + try: + s = str(value).replace("Z", "").replace("T", " ") + if len(s) >= 16: + s = s[:16] + return s + " UTC" + except Exception: + pass + return str(value) + + +def _add_row(form, label_text, display_val, raw_val, copyable): + """Add a single row to the form layout.""" + value_label = QtWidgets.QLabel(display_val) + value_label.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse) + + if copyable and raw_val: + row = QtWidgets.QWidget() + row_layout = QtWidgets.QHBoxLayout(row) + row_layout.setContentsMargins(0, 0, 0, 0) + row_layout.setSpacing(4) + row_layout.addWidget(value_label) + + btn = QtWidgets.QToolButton() + btn.setText("\u29c9") # ⧉ + btn.setFixedWidth(24) + btn.setToolTip(f"Copy {label_text}") + btn.clicked.connect( + lambda checked=False, v=raw_val: QtWidgets.QApplication.clipboard().setText( + v + ) + ) + row_layout.addWidget(btn) + row_layout.addStretch() + form.addRow(label_text + ":", row) + else: + form.addRow(label_text + ":", value_label) + + +# --------------------------------------------------------------------------- +# Viewer factory +# --------------------------------------------------------------------------- + +_VIEWER_REGISTRY = { + "silo/manifest.json": SiloManifestViewer, +} + + +def create_viewer_widget(obj): + """Route a Silo tree node to the appropriate viewer widget. + + Returns a QWidget ready for MDI embedding, or None if no viewer + is registered for this node's SiloPath. + """ + cls = _VIEWER_REGISTRY.get(obj.SiloPath) + if cls is None: + return None + try: + return cls(obj) + except Exception as exc: + FreeCAD.Console.PrintWarning( + f"silo_viewers: failed to create viewer for {obj.SiloPath!r}: {exc}\n" + ) + return None diff --git a/src/Mod/Create/silo_viewproviders.py b/src/Mod/Create/silo_viewproviders.py index d42e0ff414..f992d067fd 100644 --- a/src/Mod/Create/silo_viewproviders.py +++ b/src/Mod/Create/silo_viewproviders.py @@ -63,8 +63,44 @@ class SiloViewerViewProvider: return "" def doubleClicked(self, vobj): - """Phase 1: no action on double-click.""" - return False + """Open a read-only MDI viewer for this silo node.""" + try: + import FreeCADGui + from PySide import QtWidgets + from silo_viewers import create_viewer_widget + + obj = vobj.Object + widget = create_viewer_widget(obj) + if widget is None: + return False + + mw = FreeCADGui.getMainWindow() + mdi = mw.findChild(QtWidgets.QMdiArea) + if mdi is None: + return False + + # Reuse existing subwindow if already open for this object + target_name = widget.objectName() + for sw in mdi.subWindowList(): + if sw.widget() and sw.widget().objectName() == target_name: + widget.deleteLater() + mdi.setActiveSubWindow(sw) + sw.show() + return True + + sw = mdi.addSubWindow(widget) + sw.setWindowTitle(getattr(widget, "WINDOW_TITLE", "Silo Viewer")) + sw.show() + mdi.setActiveSubWindow(sw) + return True + + except Exception as exc: + import FreeCAD + + FreeCAD.Console.PrintWarning( + f"silo_viewproviders: doubleClicked failed: {exc}\n" + ) + return False def setupContextMenu(self, vobj, menu): """Phase 1: no context menu items."""