@@ -1,11 +1,12 @@
"""
silo_viewers.py — Read-only MDI viewer widgets for Silo tree leaf nodes.
silo_viewers.py — 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 copy
import json
import FreeCAD
@@ -124,12 +125,311 @@ def _add_row(form, label_text, display_val, raw_val, copyable):
form . addRow ( label_text + " : " , value_label )
# ---------------------------------------------------------------------------
# Metadata Editor
# ---------------------------------------------------------------------------
_LIFECYCLE_OPTIONS = [ " draft " , " review " , " released " , " obsolete " ]
class SiloMetadataEditor ( QtWidgets . QWidget ) :
""" Editable form for ``silo/metadata.json`` fields. """
WINDOW_TITLE = " Silo \u2014 Metadata "
def __init__ ( self , obj , parent = None ) :
super ( ) . __init__ ( parent )
self . setObjectName ( f " SiloViewer_ { obj . Name } " )
self . _obj = obj
self . _original_data = { }
self . _field_widgets = { } # key -> QWidget
self . _tags_layout = None
self . _lifecycle_combo = None
self . _save_btn = None
self . _reset_btn = None
self . _schema_name = " "
self . _build_ui ( obj . RawContent )
# -- layout --------------------------------------------------------------
def _build_ui ( self , raw_content ) :
try :
data = json . loads ( raw_content )
except Exception :
data = { }
self . _original_data = copy . deepcopy ( data )
self . _schema_name = data . get ( " schema " , " " )
outer = QtWidgets . QVBoxLayout ( self )
outer . setContentsMargins ( 16 , 16 , 16 , 16 )
outer . setSpacing ( 12 )
# Header: title + lifecycle combo
header = QtWidgets . QHBoxLayout ( )
title = QtWidgets . QLabel ( " Part Metadata " )
font = title . font ( )
font . setPointSize ( font . pointSize ( ) + 2 )
font . setBold ( True )
title . setFont ( font )
header . addWidget ( title )
header . addStretch ( )
self . _lifecycle_combo = QtWidgets . QComboBox ( )
self . _lifecycle_combo . addItems ( _LIFECYCLE_OPTIONS )
current_lc = data . get ( " lifecycle " , " draft " )
idx = self . _lifecycle_combo . findText ( current_lc )
if idx > = 0 :
self . _lifecycle_combo . setCurrentIndex ( idx )
self . _lifecycle_combo . currentTextChanged . connect ( self . _on_edited )
header . addWidget ( QtWidgets . QLabel ( " Lifecycle: " ) )
header . addWidget ( self . _lifecycle_combo )
outer . addLayout ( header )
# Separator
line = QtWidgets . QFrame ( )
line . setFrameShape ( QtWidgets . QFrame . HLine )
line . setFrameShadow ( QtWidgets . QFrame . Sunken )
outer . addWidget ( line )
# Scroll area for form content
scroll = QtWidgets . QScrollArea ( )
scroll . setWidgetResizable ( True )
scroll . setFrameShape ( QtWidgets . QFrame . NoFrame )
content = QtWidgets . QWidget ( )
content_layout = QtWidgets . QVBoxLayout ( content )
content_layout . setContentsMargins ( 0 , 0 , 0 , 0 )
content_layout . setSpacing ( 12 )
# Schema label
schema_text = self . _schema_name or " \u2014 "
content_layout . addWidget ( QtWidgets . QLabel ( f " Schema: { schema_text } " ) )
# Tags row
tags_container = QtWidgets . QWidget ( )
tags_outer = QtWidgets . QHBoxLayout ( tags_container )
tags_outer . setContentsMargins ( 0 , 0 , 0 , 0 )
tags_outer . setSpacing ( 4 )
tags_outer . addWidget ( QtWidgets . QLabel ( " Tags: " ) )
self . _tags_layout = QtWidgets . QHBoxLayout ( )
self . _tags_layout . setContentsMargins ( 0 , 0 , 0 , 0 )
self . _tags_layout . setSpacing ( 4 )
tags_outer . addLayout ( self . _tags_layout )
for tag in data . get ( " tags " , [ ] ) :
self . _add_tag_chip ( tag )
add_btn = QtWidgets . QToolButton ( )
add_btn . setText ( " + " )
add_btn . setFixedWidth ( 24 )
add_btn . setToolTip ( " Add tag " )
add_btn . clicked . connect ( self . _on_add_tag )
tags_outer . addWidget ( add_btn )
tags_outer . addStretch ( )
content_layout . addWidget ( tags_container )
# Fields section
fields = data . get ( " fields " , { } )
if fields :
group = QtWidgets . QGroupBox ( " Fields " )
form = QtWidgets . QFormLayout ( group )
form . setLabelAlignment ( QtCore . Qt . AlignRight | QtCore . Qt . AlignVCenter )
form . setHorizontalSpacing ( 16 )
form . setVerticalSpacing ( 8 )
for key , value in fields . items ( ) :
widget = self . _make_field_widget ( value )
self . _field_widgets [ key ] = widget
form . addRow ( key + " : " , widget )
content_layout . addWidget ( group )
content_layout . addStretch ( )
scroll . setWidget ( content )
outer . addWidget ( scroll , 1 )
# Button bar
btn_bar = QtWidgets . QHBoxLayout ( )
btn_bar . addStretch ( )
self . _save_btn = QtWidgets . QPushButton ( " Save " )
self . _save_btn . setEnabled ( False )
self . _save_btn . clicked . connect ( self . _on_save )
btn_bar . addWidget ( self . _save_btn )
self . _reset_btn = QtWidgets . QPushButton ( " Reset " )
self . _reset_btn . setEnabled ( False )
self . _reset_btn . clicked . connect ( self . _on_reset )
btn_bar . addWidget ( self . _reset_btn )
outer . addLayout ( btn_bar )
# -- field widgets -------------------------------------------------------
def _make_field_widget ( self , value ) :
""" Create an appropriate edit widget based on the value type. """
if isinstance ( value , bool ) :
cb = QtWidgets . QCheckBox ( )
cb . setChecked ( value )
cb . stateChanged . connect ( self . _on_edited )
return cb
if isinstance ( value , ( int , float ) ) :
spin = QtWidgets . QDoubleSpinBox ( )
spin . setDecimals ( 4 )
spin . setRange ( - 1e9 , 1e9 )
spin . setValue ( float ( value ) )
spin . valueChanged . connect ( self . _on_edited )
return spin
le = QtWidgets . QLineEdit ( )
le . setText ( str ( value ) if value is not None else " " )
le . textChanged . connect ( self . _on_edited )
return le
def _read_field_widget ( self , widget ) :
""" Read a value back from a field widget. """
if isinstance ( widget , QtWidgets . QCheckBox ) :
return widget . isChecked ( )
if isinstance ( widget , QtWidgets . QDoubleSpinBox ) :
return widget . value ( )
return widget . text ( )
# -- tag management ------------------------------------------------------
def _add_tag_chip ( self , tag_text ) :
""" Add a removable tag chip to the tags row. """
chip = QtWidgets . QFrame ( )
chip . setStyleSheet (
" QFrame { border: 1px solid palette(mid); "
" border-radius: 8px; padding: 2px 4px; } "
)
chip_layout = QtWidgets . QHBoxLayout ( chip )
chip_layout . setContentsMargins ( 4 , 0 , 0 , 0 )
chip_layout . setSpacing ( 2 )
chip_layout . addWidget ( QtWidgets . QLabel ( tag_text ) )
remove_btn = QtWidgets . QToolButton ( )
remove_btn . setText ( " \u00d7 " ) # ×
remove_btn . setFixedSize ( 16 , 16 )
remove_btn . setStyleSheet ( " QToolButton { border: none; } " )
remove_btn . clicked . connect (
lambda checked = False , c = chip : self . _remove_tag_chip ( c )
)
chip_layout . addWidget ( remove_btn )
self . _tags_layout . addWidget ( chip )
def _remove_tag_chip ( self , chip ) :
""" Remove a tag chip and mark dirty. """
self . _tags_layout . removeWidget ( chip )
chip . deleteLater ( )
self . _on_edited ( )
def _on_add_tag ( self ) :
""" Prompt for a new tag and add it. """
text , ok = QtWidgets . QInputDialog . getText ( self , " Add Tag " , " Tag name: " )
if ok and text . strip ( ) :
self . _add_tag_chip ( text . strip ( ) )
self . _on_edited ( )
def _get_tags ( self ) :
""" Collect current tag strings from the chip widgets. """
tags = [ ]
for i in range ( self . _tags_layout . count ( ) ) :
chip = self . _tags_layout . itemAt ( i ) . widget ( )
if chip is None :
continue
label = chip . findChild ( QtWidgets . QLabel )
if label :
tags . append ( label . text ( ) )
return tags
# -- dirty tracking / save / reset ---------------------------------------
def _on_edited ( self , * args ) :
""" Mark the object dirty and enable Save/Reset. """
self . _obj . Proxy . mark_dirty ( )
self . _save_btn . setEnabled ( True )
self . _reset_btn . setEnabled ( True )
def _collect_data ( self ) :
""" Build a metadata dict from current widget state. """
return {
" schema " : self . _schema_name ,
" lifecycle " : self . _lifecycle_combo . currentText ( ) ,
" tags " : self . _get_tags ( ) ,
" fields " : {
key : self . _read_field_widget ( w )
for key , w in self . _field_widgets . items ( )
} ,
}
def _on_save ( self ) :
""" Write current form state to obj.RawContent. """
new_data = self . _collect_data ( )
self . _obj . RawContent = json . dumps ( new_data , indent = 2 )
self . _obj . Proxy . clear_dirty ( )
self . _original_data = copy . deepcopy ( new_data )
self . _save_btn . setEnabled ( False )
self . _reset_btn . setEnabled ( False )
def _on_reset ( self ) :
""" Revert all fields to last-saved state. """
# Clear existing field widgets and tags
self . _field_widgets . clear ( )
for i in reversed ( range ( self . _tags_layout . count ( ) ) ) :
w = self . _tags_layout . itemAt ( i ) . widget ( )
if w :
w . deleteLater ( )
# Repopulate from original data
data = self . _original_data
# Lifecycle
idx = self . _lifecycle_combo . findText ( data . get ( " lifecycle " , " draft " ) )
if idx > = 0 :
self . _lifecycle_combo . blockSignals ( True )
self . _lifecycle_combo . setCurrentIndex ( idx )
self . _lifecycle_combo . blockSignals ( False )
# Tags
for tag in data . get ( " tags " , [ ] ) :
self . _add_tag_chip ( tag )
# Fields — find the QGroupBox and rebuild its form
for child in self . findChildren ( QtWidgets . QGroupBox ) :
if child . title ( ) == " Fields " :
form = child . layout ( )
# Clear existing rows
while form . count ( ) :
item = form . takeAt ( 0 )
if item . widget ( ) :
item . widget ( ) . deleteLater ( )
# Rebuild
for key , value in data . get ( " fields " , { } ) . items ( ) :
widget = self . _make_field_widget ( value )
self . _field_widgets [ key ] = widget
form . addRow ( key + " : " , widget )
break
self . _obj . Proxy . clear_dirty ( )
self . _save_btn . setEnabled ( False )
self . _reset_btn . setEnabled ( False )
# -- close guard ---------------------------------------------------------
def closeEvent ( self , event ) :
if self . _obj . Proxy . is_dirty ( ) :
reply = QtWidgets . QMessageBox . question (
self ,
" Unsaved Changes " ,
" Metadata has unsaved changes. Discard? " ,
QtWidgets . QMessageBox . Discard | QtWidgets . QMessageBox . Cancel ,
QtWidgets . QMessageBox . Cancel ,
)
if reply == QtWidgets . QMessageBox . Cancel :
event . ignore ( )
return
self . _obj . Proxy . clear_dirty ( )
event . accept ( )
# ---------------------------------------------------------------------------
# Viewer factory
# ---------------------------------------------------------------------------
_VIEWER_REGISTRY = {
" silo/manifest.json " : SiloManifestViewer ,
" silo/metadata.json " : SiloMetadataEditor ,
}