Last active
April 8, 2025 09:16
-
-
Save minimalefforttech/bd7aa3d6eb177421bc357800d9bf5630 to your computer and use it in GitHub Desktop.
An example QTreeView that is styled to look like a form.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from PySide6 import QtCore | |
from types import SimpleNamespace | |
from dataclasses import dataclass | |
from typing import Dict, List, Optional, Union | |
WidgetType = SimpleNamespace( | |
TEXT_INPUT="text_input", | |
DROPDOWN="dropdown", | |
SPINBOX="spinbox", | |
CHECKBOX="checkbox", | |
DATE_PICKER="date_picker", | |
TEXT_AREA="text_area", | |
FILE_PATH="file_path", | |
NUMERIC_INPUT="numeric_input", | |
COLOR_PICKER="color_picker", | |
SLIDER="slider", | |
FRAME_RANGE="frame_range", | |
) | |
LABEL_ROLE = QtCore.Qt.UserRole + 1 | |
CONFIG_ROLE = QtCore.Qt.UserRole + 2 | |
IS_GROUP_ROLE = QtCore.Qt.UserRole + 3 | |
@dataclass | |
class EditorConfig: | |
"""Configuration for editor widgets.""" | |
widget_type: str | |
options: Optional[Union[List, Dict]] = None | |
label: str = "" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from PySide6 import QtWidgets, QtCore, QtGui | |
from .constants import EditorConfig, IS_GROUP_ROLE, LABEL_ROLE, CONFIG_ROLE | |
from .editors import EditorFactory | |
class AdaptiveItemDelegate(QtWidgets.QStyledItemDelegate): | |
"""Delegate for adaptive item rendering in a tree view.""" | |
def __init__(self, parent=None): | |
super().__init__(parent) | |
self._editors = {} | |
def paint(self, painter, option, index): | |
painter.save() | |
is_group = index.data(IS_GROUP_ROLE) | |
widget_label = index.data(LABEL_ROLE) | |
rect = option.rect | |
opt = QtWidgets.QStyleOptionViewItem(option) | |
self.initStyleOption(opt, index) | |
if option.state & QtWidgets.QStyle.State_Selected: | |
painter.fillRect(rect, option.palette.highlight()) | |
painter.setPen(option.palette.color(QtGui.QPalette.HighlightedText)) | |
else: | |
painter.setPen(option.palette.color(QtGui.QPalette.Text)) | |
if is_group: | |
font = painter.font() | |
font.setBold(True) | |
painter.setFont(font) | |
text_rect = QtCore.QRect( | |
rect.left(), rect.top(), rect.width() - 10, rect.height() - 4 | |
) | |
painter.drawText( | |
text_rect, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, widget_label | |
) | |
line_y = rect.bottom() - 2 | |
painter.drawLine(rect.left(), line_y, rect.right(), line_y) | |
else: | |
text_rect = QtCore.QRect(rect.left(), rect.top() + 2, rect.width() - 10, 16) | |
font = painter.font() | |
font.setBold(True) | |
painter.setFont(font) | |
painter.drawText(text_rect, QtCore.Qt.AlignLeft, widget_label) | |
painter.restore() | |
def createEditor(self, parent, option, index): | |
if index.data(IS_GROUP_ROLE): | |
return None | |
config = self._get_editor_config(index) | |
editor = EditorFactory.create_editor(config, parent) | |
self._editors[QtCore.QPersistentModelIndex(index)] = editor | |
return editor | |
def setEditorData(self, editor, index): | |
value = index.data(QtCore.Qt.DisplayRole) | |
editor.blockSignals(True) | |
try: | |
editor.set_value(value) | |
finally: | |
editor.blockSignals(False) | |
def setModelData(self, editor, model, index): | |
value = editor.get_value() | |
model.setData(index, value, QtCore.Qt.EditRole) | |
def _get_editor_config(self, index): | |
"""Create a config object from index data""" | |
config_data = index.data(CONFIG_ROLE) or {} | |
return EditorConfig( | |
widget_type=config_data.get("widget_type", ""), | |
options=config_data.get("options"), | |
label=config_data.get("label", ""), | |
) | |
def sizeHint(self, option, index): | |
is_group = index.data(IS_GROUP_ROLE) | |
if is_group: | |
return QtCore.QSize(option.rect.width(), 30) | |
else: | |
config = self._get_editor_config(index) | |
font_metrics = QtGui.QFontMetrics(option.font) | |
text_height = font_metrics.height() + 4 | |
return QtCore.QSize( | |
option.rect.width(), | |
text_height + EditorFactory.required_size(config.widget_type).height(), | |
) | |
def updateEditorGeometry(self, editor, option, index): | |
font_metrics = QtGui.QFontMetrics(option.font) | |
text_height = font_metrics.height() + 4 | |
editor_rect = QtCore.QRect( | |
option.rect.left(), | |
option.rect.top() + text_height, | |
option.rect.width(), | |
option.rect.height() - text_height, | |
) | |
editor.setGeometry(editor_rect) | |
def _update_model_data(self, index, value): | |
if not index.isValid(): | |
return | |
model = index.model() | |
if model: | |
model.setData(index, value, QtCore.Qt.EditRole) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from PySide6 import QtWidgets, QtCore | |
import traceback | |
from .constants import WidgetType, EditorConfig | |
class IEditor: | |
"""Base interface for all editor widgets.""" | |
valueChanged = QtCore.Signal("QVariant") | |
def __init__(self, config): | |
self.config = config | |
def set_value(self, value): | |
raise NotImplementedError() | |
def get_value(self): | |
raise NotImplementedError() | |
@staticmethod | |
def required_size(config=None): | |
return QtCore.QSize(-1, 20) | |
class TextEditor(IEditor, QtWidgets.QLineEdit): | |
"""Simple text input editor.""" | |
def __init__(self, parent=None, config=None): | |
QtWidgets.QLineEdit.__init__(self, parent) | |
try: | |
IEditor.__init__(self, config) | |
except Exception as e: | |
traceback.print_exc() | |
self.textChanged.connect(self._on_value_changed) | |
def set_value(self, value): | |
if value is not None: | |
self.setText(str(value)) | |
def get_value(self): | |
return self.text() | |
def _on_value_changed(self): | |
try: | |
self.valueChanged.emit(self.get_value()) | |
except Exception as e: | |
traceback.print_exc() | |
@staticmethod | |
def required_size(config=None): | |
return QtCore.QSize(-1, 20) | |
class TextAreaEditor(IEditor, QtWidgets.QTextEdit): | |
"""Multi-line text editor for longer content.""" | |
def __init__(self, parent=None, config=None): | |
QtWidgets.QTextEdit.__init__(self, parent) | |
IEditor.__init__(self, config) | |
self.textChanged.connect(self._on_value_changed) | |
def set_value(self, value): | |
if value is not None: | |
self.setPlainText(str(value)) | |
def get_value(self): | |
return self.toPlainText() | |
def _on_value_changed(self): | |
self.valueChanged.emit(self.get_value()) | |
@staticmethod | |
def required_size(config=None): | |
return QtCore.QSize(-1, 60) | |
class DropdownEditor(IEditor, QtWidgets.QComboBox): | |
"""Dropdown selection editor for choosing from predefined options.""" | |
def __init__(self, parent=None, config=None): | |
QtWidgets.QComboBox.__init__(self, parent) | |
IEditor.__init__(self, config) | |
options = config.options if config else None | |
if options: | |
self.addItems([str(option) for option in options]) | |
self.currentIndexChanged.connect(self._on_value_changed) | |
def set_value(self, value): | |
if isinstance(value, int): | |
self.setCurrentIndex(value) | |
elif isinstance(value, str): | |
items = [self.itemText(i) for i in range(self.count())] | |
if value in items: | |
self.setCurrentText(value) | |
def get_value(self): | |
return self.currentText() | |
def _on_value_changed(self): | |
self.valueChanged.emit(self.get_value()) | |
@staticmethod | |
def required_size(config=None): | |
return QtCore.QSize(-1, 20) | |
class SpinBoxEditor(IEditor, QtWidgets.QSpinBox): | |
"""Integer value editor with increment/decrement buttons.""" | |
def __init__(self, parent=None, config=None): | |
QtWidgets.QSpinBox.__init__(self, parent) | |
IEditor.__init__(self, config) | |
options = config.options if config else None | |
if options and isinstance(options, dict): | |
min_val = options.get("min", 0) | |
max_val = options.get("max", 100) | |
step = options.get("step", 1) | |
self.setRange(min_val, max_val) | |
self.setSingleStep(step) | |
self.valueChanged.connect(self._on_value_changed) | |
def set_value(self, value): | |
if value is not None: | |
self.setValue(int(value)) | |
def get_value(self): | |
return self.value() | |
def _on_value_changed(self): | |
self.valueChanged.emit(self.get_value()) | |
@staticmethod | |
def required_size(config=None): | |
return QtCore.QSize(-1, 20) | |
class CheckBoxEditor(IEditor, QtWidgets.QCheckBox): | |
"""Boolean value editor represented as a checkbox.""" | |
def __init__(self, parent=None, config=None): | |
QtWidgets.QCheckBox.__init__(self, parent) | |
IEditor.__init__(self, config) | |
self.stateChanged.connect(self._on_value_changed) | |
def set_value(self, value): | |
self.setChecked(bool(value)) | |
def get_value(self): | |
return self.isChecked() | |
def _on_value_changed(self): | |
self.valueChanged.emit(self.get_value()) | |
@staticmethod | |
def required_size(config=None): | |
return QtCore.QSize(-1, 20) | |
class DatePickerEditor(IEditor, QtWidgets.QDateEdit): | |
"""Date selection editor with calendar popup.""" | |
def __init__(self, parent=None, config=None): | |
QtWidgets.QDateEdit.__init__(self, parent) | |
IEditor.__init__(self, config) | |
self.setCalendarPopup(True) | |
self.setDate(QtCore.QDate.currentDate()) | |
self.dateChanged.connect(self._on_value_changed) | |
def set_value(self, value): | |
if isinstance(value, QtCore.QDate): | |
self.setDate(value) | |
def get_value(self): | |
return self.date() | |
def _on_value_changed(self): | |
self.valueChanged.emit(self.get_value()) | |
@staticmethod | |
def required_size(config=None): | |
return QtCore.QSize(-1, 20) | |
class FilePathEditor(IEditor, QtWidgets.QWidget): | |
""" File path input editor with browse button.""" | |
def __init__(self, parent=None, config=None): | |
QtWidgets.QWidget.__init__(self, parent) | |
IEditor.__init__(self, config) | |
layout = QtWidgets.QHBoxLayout(self) | |
layout.setContentsMargins(0, 0, 0, 0) | |
self._line_edit = QtWidgets.QLineEdit() | |
browse_button = QtWidgets.QPushButton("...") | |
layout.addWidget(self._line_edit) | |
layout.addWidget(browse_button) | |
self.setFocusProxy(self._line_edit) | |
browse_button.clicked.connect(self._browse_file) | |
self._line_edit.textChanged.connect(self._on_value_changed) | |
def _browse_file(self): | |
path, _ = QtWidgets.QFileDialog.getOpenFileName() | |
if path: | |
self._line_edit.setText(path) | |
def set_value(self, value): | |
if value is not None: | |
self._line_edit.setText(str(value)) | |
def get_value(self): | |
return self._line_edit.text() | |
def _on_value_changed(self): | |
self.valueChanged.emit(self.get_value()) | |
@staticmethod | |
def required_size(config=None): | |
return QtCore.QSize(-1, 20) | |
class NumericInputEditor(IEditor, QtWidgets.QDoubleSpinBox): | |
"""Floating-point value editor with increment/decrement buttons.""" | |
def __init__(self, parent=None, config=None): | |
QtWidgets.QDoubleSpinBox.__init__(self, parent) | |
IEditor.__init__(self, config) | |
options = config.options if config else None | |
if options and isinstance(options, dict): | |
min_val = options.get("min", 0.0) | |
max_val = options.get("max", 100.0) | |
step = options.get("step", 0.1) | |
decimals = options.get("decimals", 2) | |
self.setRange(min_val, max_val) | |
self.setSingleStep(step) | |
self.setDecimals(decimals) | |
self.valueChanged.connect(self._on_value_changed) | |
def set_value(self, value): | |
if value is not None: | |
self.setValue(float(value)) | |
def get_value(self): | |
return self.value() | |
def _on_value_changed(self): | |
self.valueChanged.emit(self.get_value()) | |
@staticmethod | |
def required_size(config=None): | |
return QtCore.QSize(-1, 20) | |
class SliderEditor(IEditor, QtWidgets.QSlider): | |
"""Slider editor for selecting a numeric value within a range.""" | |
def __init__(self, parent=None, config=None): | |
QtWidgets.QSlider.__init__(self, QtCore.Qt.Horizontal, parent) | |
IEditor.__init__(self, config) | |
options = config.options if config else None | |
if options and isinstance(options, dict): | |
min_val = options.get("min", 0) | |
max_val = options.get("max", 100) | |
step = options.get("step", 1) | |
self.setRange(min_val, max_val) | |
self.setSingleStep(step) | |
self.setPageStep(step * 10) | |
self.setTickPosition(QtWidgets.QSlider.TicksBelow) | |
self.setTickInterval( | |
(max_val - min_val) // 10 if max_val != min_val else 10 | |
) | |
self.valueChanged.connect(self._on_value_changed) | |
def set_value(self, value): | |
if value is not None: | |
self.setValue(int(value)) | |
def get_value(self): | |
return self.value() | |
def _on_value_changed(self): | |
self.valueChanged.emit(self.get_value()) | |
@staticmethod | |
def required_size(config=None): | |
return QtCore.QSize(-1, 30) | |
class FrameRangeEditor(IEditor, QtWidgets.QWidget): | |
"""Frame range editor for specifying a start and end frame with increment.""" | |
def __init__(self, parent=None, config=None): | |
QtWidgets.QWidget.__init__(self, parent) | |
IEditor.__init__(self, config) | |
# Focus is required in order for setModelData to be called | |
self.setFocusPolicy(QtCore.Qt.StrongFocus) | |
layout = QtWidgets.QHBoxLayout(self) | |
layout.setContentsMargins(0, 0, 0, 0) | |
start_label = QtWidgets.QLabel("Start:") | |
end_label = QtWidgets.QLabel("End:") | |
incr_label = QtWidgets.QLabel("Increment:") | |
self._start_spin = QtWidgets.QSpinBox() | |
self._end_spin = QtWidgets.QSpinBox() | |
self._incr_spin = QtWidgets.QSpinBox() | |
for spin in [self._start_spin, self._end_spin]: | |
spin.setRange(1, 10000) | |
self._incr_spin.setRange(1, 100) | |
self._start_spin.setValue(1) | |
self._end_spin.setValue(100) | |
self._incr_spin.setValue(1) | |
layout.addWidget(start_label) | |
layout.addWidget(self._start_spin) | |
layout.addWidget(end_label) | |
layout.addWidget(self._end_spin) | |
layout.addWidget(incr_label) | |
layout.addWidget(self._incr_spin) | |
self._start_spin.valueChanged.connect(self._on_value_changed) | |
self._end_spin.valueChanged.connect(self._on_value_changed) | |
self._incr_spin.valueChanged.connect(self._on_value_changed) | |
def set_value(self, value): | |
if isinstance(value, dict): | |
self._start_spin.setValue(value.get("start", 1)) | |
self._end_spin.setValue(value.get("end", 100)) | |
self._incr_spin.setValue(value.get("incr", 1)) | |
def get_value(self): | |
return { | |
"start": self._start_spin.value(), | |
"end": self._end_spin.value(), | |
"incr": self._incr_spin.value(), | |
} | |
def _on_value_changed(self): | |
self.valueChanged.emit(self.get_value()) | |
@staticmethod | |
def required_size(config=None): | |
return QtCore.QSize(-1, 20) | |
class EditorFactory: | |
"""Factory class to create editor widgets based on the widget type.""" | |
_EDITOR_CLASS_MAP = { | |
WidgetType.TEXT_INPUT: TextEditor, | |
WidgetType.TEXT_AREA: TextAreaEditor, | |
WidgetType.DROPDOWN: DropdownEditor, | |
WidgetType.SPINBOX: SpinBoxEditor, | |
WidgetType.CHECKBOX: CheckBoxEditor, | |
WidgetType.DATE_PICKER: DatePickerEditor, | |
WidgetType.FILE_PATH: FilePathEditor, | |
WidgetType.NUMERIC_INPUT: NumericInputEditor, | |
WidgetType.COLOR_PICKER: None, | |
WidgetType.SLIDER: SliderEditor, | |
WidgetType.FRAME_RANGE: FrameRangeEditor, | |
} | |
@staticmethod | |
def create_editor(config:EditorConfig, parent=None): | |
editor_class = EditorFactory._get_editor_class(config.widget_type) | |
if not editor_class: | |
return TextEditor(parent, config) | |
return editor_class(parent, config) | |
@staticmethod | |
def _get_editor_class(widget_type): | |
"""Get the editor class for the widget type""" | |
return EditorFactory._EDITOR_CLASS_MAP.get(widget_type) | |
@staticmethod | |
def required_size(widget_type, value=None): | |
"""Get the required size for the widget type""" | |
if isinstance(widget_type, EditorConfig): | |
config = widget_type | |
widget_type = config.widget_type | |
editor_class = EditorFactory._get_editor_class(widget_type) | |
if editor_class and hasattr(editor_class, "required_size"): | |
return editor_class.required_size(value) | |
if widget_type == WidgetType.TEXT_AREA: | |
return QtCore.QSize(-1, 60) | |
else: | |
return QtCore.QSize(-1, 20) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from PySide6 import QtWidgets, QtGui, QtCore | |
from .delegate import AdaptiveItemDelegate | |
from .constants import IS_GROUP_ROLE, LABEL_ROLE, CONFIG_ROLE, WidgetType | |
class VFXIngestionForm(QtWidgets.QWidget): | |
"""Main form for VFX asset ingestion.""" | |
def __init__(self, parent=None): | |
super().__init__(parent) | |
self.setWindowTitle("VFX Asset Ingestion") | |
self.resize(800, 700) | |
self._apply_dark_pastel_palette() | |
layout = QtWidgets.QVBoxLayout(self) | |
self._tree_view = QtWidgets.QTreeView() | |
model = QtGui.QStandardItemModel() | |
self._tree_view.setModel(model) | |
self._tree_view.setHeaderHidden(True) | |
self._tree_view.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) | |
self._tree_view.setItemDelegate(AdaptiveItemDelegate(self)) | |
self._tree_view.setIndentation(20) | |
self._tree_view.setItemsExpandable(True) | |
self._tree_view.setAnimated(True) | |
self._tree_view.expanded.connect(self._handle_item_expanded) | |
self._tree_view.collapsed.connect(self._handle_item_collapsed) | |
self._tree_view.setStyleSheet( | |
""" | |
QTreeView { | |
background-color: transparent; | |
border: none; | |
} | |
QTreeView::item:hover { | |
background-color: palette(window); | |
} | |
""" | |
) | |
layout.addWidget(self._tree_view) | |
button_layout = QtWidgets.QHBoxLayout() | |
self._submit_button = QtWidgets.QPushButton("Submit") | |
self._cancel_button = QtWidgets.QPushButton("Cancel") | |
self._submit_button.setMinimumHeight(36) | |
self._cancel_button.setMinimumHeight(36) | |
self._submit_button.setProperty("class", "primary-button") | |
button_layout.addStretch() | |
button_layout.addWidget(self._cancel_button) | |
button_layout.addWidget(self._submit_button) | |
layout.addLayout(button_layout) | |
self._setup_form() | |
self._open_all_persistent_editors() | |
self._submit_button.clicked.connect(self.submit_form) | |
self._cancel_button.clicked.connect(self.close) | |
def _apply_dark_pastel_palette(self): | |
"""Apply a dark pastel theme palette to the application""" | |
palette = QtGui.QPalette() | |
dark_color = QtGui.QColor(42, 42, 42) | |
darker_color = QtGui.QColor(34, 34, 34) | |
highlight_color = QtGui.QColor(74, 108, 212) | |
text_color = QtGui.QColor(224, 224, 224) | |
palette.setColor(QtGui.QPalette.Window, dark_color) | |
palette.setColor(QtGui.QPalette.WindowText, text_color) | |
palette.setColor(QtGui.QPalette.Base, darker_color) | |
palette.setColor(QtGui.QPalette.AlternateBase, QtGui.QColor(48, 48, 48)) | |
palette.setColor(QtGui.QPalette.ToolTipBase, dark_color) | |
palette.setColor(QtGui.QPalette.ToolTipText, text_color) | |
palette.setColor(QtGui.QPalette.Text, text_color) | |
palette.setColor(QtGui.QPalette.Button, dark_color) | |
palette.setColor(QtGui.QPalette.ButtonText, text_color) | |
palette.setColor(QtGui.QPalette.BrightText, QtCore.Qt.white) | |
palette.setColor(QtGui.QPalette.Link, QtGui.QColor(92, 179, 255)) | |
palette.setColor(QtGui.QPalette.LinkVisited, QtGui.QColor(180, 120, 220)) | |
palette.setColor(QtGui.QPalette.Highlight, highlight_color) | |
palette.setColor(QtGui.QPalette.HighlightedText, QtCore.Qt.white) | |
palette.setColor( | |
QtGui.QPalette.Disabled, QtGui.QPalette.Text, QtGui.QColor(128, 128, 128) | |
) | |
palette.setColor( | |
QtGui.QPalette.Disabled, | |
QtGui.QPalette.ButtonText, | |
QtGui.QColor(128, 128, 128), | |
) | |
palette.setColor( | |
QtGui.QPalette.Disabled, QtGui.QPalette.Highlight, QtGui.QColor(80, 80, 80) | |
) | |
palette.setColor( | |
QtGui.QPalette.Disabled, | |
QtGui.QPalette.HighlightedText, | |
QtGui.QColor(160, 160, 160), | |
) | |
self.setPalette(palette) | |
QtWidgets.QApplication.setPalette(palette) | |
def _handle_item_expanded(self, index): | |
QtCore.QTimer.singleShot(200, lambda: self._open_editors_for_index(index)) | |
def _handle_item_collapsed(self, index): | |
self._close_editors_for_index(index) | |
def _open_editors_for_index(self, parent_index): | |
row_count = self._tree_view.model().rowCount(parent_index) | |
for row in range(row_count): | |
index = self._tree_view.model().index(row, 0, parent_index) | |
is_group = index.data(IS_GROUP_ROLE) | |
if not is_group: | |
self._tree_view.openPersistentEditor(index) | |
def _close_editors_for_index(self, parent_index): | |
row_count = self._tree_view.model().rowCount(parent_index) | |
for row in range(row_count): | |
index = self._tree_view.model().index(row, 0, parent_index) | |
is_group = index.data(IS_GROUP_ROLE) | |
if not is_group: | |
self._tree_view.closePersistentEditor(index) | |
if self._tree_view.model().hasChildren(index): | |
self._close_editors_for_index(index) | |
def _open_all_persistent_editors(self): | |
def _recursive_open(parent_index): | |
for row in range(self._tree_view.model().rowCount(parent_index)): | |
index = self._tree_view.model().index(row, 0, parent_index) | |
is_group = index.data(IS_GROUP_ROLE) | |
if not is_group: | |
self._tree_view.openPersistentEditor(index) | |
_recursive_open(index) | |
_recursive_open(QtCore.QModelIndex()) | |
def _setup_form(self): | |
self._add_field("Asset Name", WidgetType.TEXT_INPUT, value="New Asset") | |
self._add_field( | |
"Asset Type", | |
WidgetType.DROPDOWN, | |
options=[ | |
"Model", | |
"Texture", | |
"Animation", | |
"Rig", | |
"FX", | |
"Lighting", | |
"Compositing", | |
], | |
value=0, | |
) | |
self._add_field( | |
"Project", | |
WidgetType.DROPDOWN, | |
options=["Project A", "Project B", "Project C"], | |
value=0, | |
) | |
self._add_field( | |
"Department", | |
WidgetType.DROPDOWN, | |
options=[ | |
"Modeling", | |
"Animation", | |
"Rigging", | |
"FX", | |
"Lighting", | |
"Compositing", | |
], | |
value=0, | |
) | |
self._add_field( | |
"Version", | |
WidgetType.SPINBOX, | |
options={"min": 1, "max": 100, "step": 1}, | |
value=1, | |
) | |
metadata_group = self._add_group("Metadata") | |
self._add_field( | |
"Description", | |
WidgetType.TEXT_AREA, | |
value="Enter a detailed description of the asset here...", | |
parent=metadata_group, | |
) | |
self._add_field( | |
"Tags", WidgetType.TEXT_INPUT, value="asset, vfx", parent=metadata_group | |
) | |
self._add_field( | |
"Author", WidgetType.TEXT_INPUT, value="VFX Artist", parent=metadata_group | |
) | |
self._add_field( | |
"Creation Date", | |
WidgetType.DATE_PICKER, | |
value=QtCore.QDate.currentDate(), | |
parent=metadata_group, | |
) | |
self._add_field( | |
"Priority", | |
WidgetType.SLIDER, | |
options={"min": 1, "max": 10, "step": 1}, | |
value=5, | |
parent=metadata_group, | |
) | |
tech_group = self._add_group("Technical Specifications") | |
self._add_field( | |
"File Format", | |
WidgetType.DROPDOWN, | |
options=["FBX", "USD", "Alembic", "OBJ", "EXR", "JPEG", "PNG"], | |
value=0, | |
parent=tech_group, | |
) | |
self._add_field( | |
"Resolution", WidgetType.TEXT_INPUT, value="1920x1080", parent=tech_group | |
) | |
self._add_field( | |
"Frame Range", | |
WidgetType.FRAME_RANGE, | |
value={"start": 1001, "end": 1100, "incr": 1}, | |
parent=tech_group, | |
) | |
self._add_field( | |
"Color Space", | |
WidgetType.DROPDOWN, | |
options=["sRGB", "ACEScg", "Linear", "Rec.709"], | |
value=0, | |
parent=tech_group, | |
) | |
self._add_field( | |
"Detail Level", | |
WidgetType.SLIDER, | |
options={"min": 1, "max": 10, "step": 1}, | |
value=7, | |
parent=tech_group, | |
) | |
pipeline_group = self._add_group("Pipeline Integration") | |
self._add_field( | |
"Target Location", | |
WidgetType.FILE_PATH, | |
value="C:/VFX/Assets/", | |
parent=pipeline_group, | |
) | |
self._add_field( | |
"Dependency Path", | |
WidgetType.TEXT_INPUT, | |
value="//server/assets/dependencies/", | |
parent=pipeline_group, | |
) | |
self._add_field( | |
"Review Status", | |
WidgetType.DROPDOWN, | |
options=["Not Reviewed", "In Progress", "Rejected", "Approved"], | |
value=0, | |
parent=pipeline_group, | |
) | |
self._add_field( | |
"Notes", | |
WidgetType.TEXT_AREA, | |
value="Add any pipeline-specific notes here...", | |
parent=pipeline_group, | |
) | |
self._add_field( | |
"Quality Score", | |
WidgetType.SLIDER, | |
options={"min": 0, "max": 100, "step": 5}, | |
value=75, | |
parent=pipeline_group, | |
) | |
self._add_field( | |
"Auto-publish", WidgetType.CHECKBOX, value=True, parent=pipeline_group | |
) | |
def _add_group(self, label): | |
item = QtGui.QStandardItem(label) | |
item.setData(True, IS_GROUP_ROLE) | |
config = {} | |
item.setData(label, LABEL_ROLE) | |
item.setData(config, CONFIG_ROLE) | |
item.setFlags(QtCore.Qt.ItemIsEnabled) | |
self._tree_view.model().appendRow(item) | |
return item | |
def _add_field(self, label, widget_type, options=None, value=None, parent=None): | |
item = QtGui.QStandardItem() | |
config = {"widget_type": widget_type, "options": options} | |
item.setText(str(value) if value is not None else "") | |
item.setData(value, QtCore.Qt.DisplayRole) | |
item.setData(value, QtCore.Qt.EditRole) | |
item.setData(False, IS_GROUP_ROLE) | |
item.setData(config, CONFIG_ROLE) | |
item.setData(label, LABEL_ROLE) | |
if parent is None: | |
self._tree_view.model().appendRow(item) | |
else: | |
parent.appendRow(item) | |
return item | |
def submit_form(self): | |
form_data = {} | |
for row in range(self._tree_view.model().rowCount()): | |
index = self._tree_view.model().index(row, 0) | |
is_group = index.data(IS_GROUP_ROLE) | |
label = index.data(LABEL_ROLE) or "" | |
if is_group: | |
group_data = {} | |
for child_row in range(self._tree_view.model().rowCount(index)): | |
child_index = self._tree_view.model().index(child_row, 0, index) | |
child_label = child_index.data(LABEL_ROLE) or "" | |
child_value = child_index.data(QtCore.Qt.DisplayRole) | |
group_data[child_label] = child_value | |
form_data[label] = group_data | |
else: | |
value = index.data(QtCore.Qt.DisplayRole) | |
form_data[label] = value | |
html_content = "<h3>Form Submission Details:</h3>" | |
for key, value in form_data.items(): | |
if isinstance(value, dict): | |
html_content += f"<h4>{key}</h4>" | |
for sub_key, sub_value in value.items(): | |
html_content += ( | |
f"{sub_key} = {sub_value}<br>" | |
) | |
else: | |
html_content += f"{key} = {value}<br>" | |
dialog = QtWidgets.QDialog(self) | |
dialog.setWindowTitle("Form Submitted") | |
dialog.resize(600, 400) | |
layout = QtWidgets.QVBoxLayout(dialog) | |
message_label = QtWidgets.QLabel( | |
"Asset ingestion form has been submitted successfully!" | |
) | |
font = message_label.font() | |
font.setBold(True) | |
message_label.setFont(font) | |
result_display = QtWidgets.QTextBrowser() | |
result_display.setHtml(html_content) | |
button_box = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok) | |
button_box.accepted.connect(dialog.accept) | |
layout.addWidget(message_label) | |
layout.addWidget(result_display) | |
layout.addWidget(button_box) | |
dialog.exec() | |
if __name__ == "__main__": | |
import sys | |
app = QtWidgets.QApplication(sys.argv) | |
form = VFXIngestionForm() | |
form.show() | |
sys.exit(app.exec()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment