mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge pull request #2244 from pypeclub/feature/creator_in_openpype
Tools: Creator in OpenPype
This commit is contained in:
commit
d141384bc2
12 changed files with 877 additions and 13 deletions
|
|
@ -1,3 +1,6 @@
|
|||
from .constants import (
|
||||
SUBSET_NAME_ALLOWED_SYMBOLS
|
||||
)
|
||||
from .creator_plugins import (
|
||||
CreatorError,
|
||||
|
||||
|
|
@ -13,6 +16,8 @@ from .context import (
|
|||
|
||||
|
||||
__all__ = (
|
||||
"SUBSET_NAME_ALLOWED_SYMBOLS",
|
||||
|
||||
"CreatorError",
|
||||
|
||||
"BaseCreator",
|
||||
|
|
|
|||
6
openpype/pipeline/create/constants.py
Normal file
6
openpype/pipeline/create/constants.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
SUBSET_NAME_ALLOWED_SYMBOLS = "a-zA-Z0-9_."
|
||||
|
||||
|
||||
__all__ = (
|
||||
"SUBSET_NAME_ALLOWED_SYMBOLS",
|
||||
)
|
||||
|
|
@ -755,13 +755,24 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
|
|||
border: 1px solid {color:border};
|
||||
border-radius: 0.1em;
|
||||
}
|
||||
|
||||
/* Subset Manager */
|
||||
#SubsetManagerDetailsText {}
|
||||
#SubsetManagerDetailsText[state="invalid"] {
|
||||
border: 1px solid #ff0000;
|
||||
}
|
||||
|
||||
/* Scene Inventory*/
|
||||
/* Creator */
|
||||
#CreatorsView::item {
|
||||
padding: 1px 5px;
|
||||
}
|
||||
|
||||
#CreatorFamilyLabel {
|
||||
font-size: 10pt;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Scene Inventory */
|
||||
#ButtonWithMenu {
|
||||
padding-right: 16px;
|
||||
border: 1px solid #4A4949;
|
||||
|
|
|
|||
9
openpype/tools/creator/__init__.py
Normal file
9
openpype/tools/creator/__init__.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
from .window import (
|
||||
show,
|
||||
CreatorWindow
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
"show",
|
||||
"CreatorWindow"
|
||||
)
|
||||
8
openpype/tools/creator/constants.py
Normal file
8
openpype/tools/creator/constants.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
from Qt import QtCore
|
||||
|
||||
|
||||
FAMILY_ROLE = QtCore.Qt.UserRole + 1
|
||||
ITEM_ID_ROLE = QtCore.Qt.UserRole + 2
|
||||
|
||||
SEPARATOR = "---"
|
||||
SEPARATORS = {"---", "---separator---"}
|
||||
55
openpype/tools/creator/model.py
Normal file
55
openpype/tools/creator/model.py
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import uuid
|
||||
from Qt import QtGui, QtCore
|
||||
|
||||
from avalon import api
|
||||
|
||||
from . constants import (
|
||||
FAMILY_ROLE,
|
||||
ITEM_ID_ROLE
|
||||
)
|
||||
|
||||
|
||||
class CreatorsModel(QtGui.QStandardItemModel):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CreatorsModel, self).__init__(*args, **kwargs)
|
||||
|
||||
self._creators_by_id = {}
|
||||
|
||||
def reset(self):
|
||||
# TODO change to refresh when clearing is not needed
|
||||
self.clear()
|
||||
self._creators_by_id = {}
|
||||
|
||||
items = []
|
||||
creators = api.discover(api.Creator)
|
||||
for creator in creators:
|
||||
item_id = str(uuid.uuid4())
|
||||
self._creators_by_id[item_id] = creator
|
||||
|
||||
label = creator.label or creator.family
|
||||
item = QtGui.QStandardItem(label)
|
||||
item.setEditable(False)
|
||||
item.setData(item_id, ITEM_ID_ROLE)
|
||||
item.setData(creator.family, FAMILY_ROLE)
|
||||
items.append(item)
|
||||
|
||||
if not items:
|
||||
item = QtGui.QStandardItem("No registered families")
|
||||
item.setEnabled(False)
|
||||
item.setData(QtCore.Qt.ItemIsEnabled, False)
|
||||
items.append(item)
|
||||
|
||||
self.invisibleRootItem().appendRows(items)
|
||||
|
||||
def get_creator_by_id(self, item_id):
|
||||
return self._creators_by_id.get(item_id)
|
||||
|
||||
def get_indexes_by_family(self, family):
|
||||
indexes = []
|
||||
for row in range(self.rowCount()):
|
||||
index = self.index(row, 0)
|
||||
item_id = index.data(ITEM_ID_ROLE)
|
||||
creator_plugin = self._creators_by_id.get(item_id)
|
||||
if creator_plugin and creator_plugin.family == family:
|
||||
indexes.append(index)
|
||||
return indexes
|
||||
266
openpype/tools/creator/widgets.py
Normal file
266
openpype/tools/creator/widgets.py
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
import re
|
||||
import inspect
|
||||
|
||||
from Qt import QtWidgets, QtCore, QtGui
|
||||
|
||||
from avalon.vendor import qtawesome
|
||||
|
||||
from openpype import style
|
||||
from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS
|
||||
|
||||
|
||||
class CreateErrorMessageBox(QtWidgets.QDialog):
|
||||
def __init__(
|
||||
self,
|
||||
family,
|
||||
subset_name,
|
||||
asset_name,
|
||||
exc_msg,
|
||||
formatted_traceback,
|
||||
parent=None
|
||||
):
|
||||
super(CreateErrorMessageBox, self).__init__(parent)
|
||||
self.setWindowTitle("Creation failed")
|
||||
self.setFocusPolicy(QtCore.Qt.StrongFocus)
|
||||
self.setWindowFlags(
|
||||
self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint
|
||||
)
|
||||
|
||||
body_layout = QtWidgets.QVBoxLayout(self)
|
||||
|
||||
main_label = (
|
||||
"<span style='font-size:18pt;'>Failed to create</span>"
|
||||
)
|
||||
main_label_widget = QtWidgets.QLabel(main_label, self)
|
||||
body_layout.addWidget(main_label_widget)
|
||||
|
||||
item_name_template = (
|
||||
"<span style='font-weight:bold;'>Family:</span> {}<br>"
|
||||
"<span style='font-weight:bold;'>Subset:</span> {}<br>"
|
||||
"<span style='font-weight:bold;'>Asset:</span> {}<br>"
|
||||
)
|
||||
exc_msg_template = "<span style='font-weight:bold'>{}</span>"
|
||||
|
||||
line = self._create_line()
|
||||
body_layout.addWidget(line)
|
||||
|
||||
item_name = item_name_template.format(family, subset_name, asset_name)
|
||||
item_name_widget = QtWidgets.QLabel(
|
||||
item_name.replace("\n", "<br>"), self
|
||||
)
|
||||
body_layout.addWidget(item_name_widget)
|
||||
|
||||
exc_msg = exc_msg_template.format(exc_msg.replace("\n", "<br>"))
|
||||
message_label_widget = QtWidgets.QLabel(exc_msg, self)
|
||||
body_layout.addWidget(message_label_widget)
|
||||
|
||||
if formatted_traceback:
|
||||
tb_widget = QtWidgets.QLabel(
|
||||
formatted_traceback.replace("\n", "<br>"), self
|
||||
)
|
||||
tb_widget.setTextInteractionFlags(
|
||||
QtCore.Qt.TextBrowserInteraction
|
||||
)
|
||||
body_layout.addWidget(tb_widget)
|
||||
|
||||
footer_widget = QtWidgets.QWidget(self)
|
||||
footer_layout = QtWidgets.QHBoxLayout(footer_widget)
|
||||
button_box = QtWidgets.QDialogButtonBox(QtCore.Qt.Vertical)
|
||||
button_box.setStandardButtons(
|
||||
QtWidgets.QDialogButtonBox.StandardButton.Ok
|
||||
)
|
||||
button_box.accepted.connect(self._on_accept)
|
||||
footer_layout.addWidget(button_box, alignment=QtCore.Qt.AlignRight)
|
||||
body_layout.addWidget(footer_widget)
|
||||
|
||||
def showEvent(self, event):
|
||||
self.setStyleSheet(style.load_stylesheet())
|
||||
super(CreateErrorMessageBox, self).showEvent(event)
|
||||
|
||||
def _on_accept(self):
|
||||
self.close()
|
||||
|
||||
def _create_line(self):
|
||||
line = QtWidgets.QFrame(self)
|
||||
line.setFixedHeight(2)
|
||||
line.setFrameShape(QtWidgets.QFrame.HLine)
|
||||
line.setFrameShadow(QtWidgets.QFrame.Sunken)
|
||||
return line
|
||||
|
||||
|
||||
class SubsetNameValidator(QtGui.QRegExpValidator):
|
||||
invalid = QtCore.Signal(set)
|
||||
pattern = "^[{}]*$".format(SUBSET_NAME_ALLOWED_SYMBOLS)
|
||||
|
||||
def __init__(self):
|
||||
reg = QtCore.QRegExp(self.pattern)
|
||||
super(SubsetNameValidator, self).__init__(reg)
|
||||
|
||||
def validate(self, text, pos):
|
||||
results = super(SubsetNameValidator, self).validate(text, pos)
|
||||
if results[0] == self.Invalid:
|
||||
self.invalid.emit(self.invalid_chars(text))
|
||||
return results
|
||||
|
||||
def invalid_chars(self, text):
|
||||
invalid = set()
|
||||
re_valid = re.compile(self.pattern)
|
||||
for char in text:
|
||||
if char == " ":
|
||||
invalid.add("' '")
|
||||
continue
|
||||
if not re_valid.match(char):
|
||||
invalid.add(char)
|
||||
return invalid
|
||||
|
||||
|
||||
class VariantLineEdit(QtWidgets.QLineEdit):
|
||||
report = QtCore.Signal(str)
|
||||
colors = {
|
||||
"empty": (QtGui.QColor("#78879b"), ""),
|
||||
"exists": (QtGui.QColor("#4E76BB"), "border-color: #4E76BB;"),
|
||||
"new": (QtGui.QColor("#7AAB8F"), "border-color: #7AAB8F;"),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(VariantLineEdit, self).__init__(*args, **kwargs)
|
||||
|
||||
validator = SubsetNameValidator()
|
||||
self.setValidator(validator)
|
||||
self.setToolTip("Only alphanumeric characters (A-Z a-z 0-9), "
|
||||
"'_' and '.' are allowed.")
|
||||
|
||||
self._status_color = self.colors["empty"][0]
|
||||
|
||||
anim = QtCore.QPropertyAnimation()
|
||||
anim.setTargetObject(self)
|
||||
anim.setPropertyName(b"status_color")
|
||||
anim.setEasingCurve(QtCore.QEasingCurve.InCubic)
|
||||
anim.setDuration(300)
|
||||
anim.setStartValue(QtGui.QColor("#C84747")) # `Invalid` status color
|
||||
self.animation = anim
|
||||
|
||||
validator.invalid.connect(self.on_invalid)
|
||||
|
||||
def on_invalid(self, invalid):
|
||||
message = "Invalid character: %s" % ", ".join(invalid)
|
||||
self.report.emit(message)
|
||||
self.animation.stop()
|
||||
self.animation.start()
|
||||
|
||||
def as_empty(self):
|
||||
self._set_border("empty")
|
||||
self.report.emit("Empty subset name ..")
|
||||
|
||||
def as_exists(self):
|
||||
self._set_border("exists")
|
||||
self.report.emit("Existing subset, appending next version.")
|
||||
|
||||
def as_new(self):
|
||||
self._set_border("new")
|
||||
self.report.emit("New subset, creating first version.")
|
||||
|
||||
def _set_border(self, status):
|
||||
qcolor, style = self.colors[status]
|
||||
self.animation.setEndValue(qcolor)
|
||||
self.setStyleSheet(style)
|
||||
|
||||
def _get_status_color(self):
|
||||
return self._status_color
|
||||
|
||||
def _set_status_color(self, color):
|
||||
self._status_color = color
|
||||
self.setStyleSheet("border-color: %s;" % color.name())
|
||||
|
||||
status_color = QtCore.Property(
|
||||
QtGui.QColor, _get_status_color, _set_status_color
|
||||
)
|
||||
|
||||
|
||||
class FamilyDescriptionWidget(QtWidgets.QWidget):
|
||||
"""A family description widget.
|
||||
|
||||
Shows a family icon, family name and a help description.
|
||||
Used in creator header.
|
||||
|
||||
_________________
|
||||
| ____ |
|
||||
| |icon| FAMILY |
|
||||
| |____| help |
|
||||
|_________________|
|
||||
|
||||
"""
|
||||
|
||||
SIZE = 35
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(FamilyDescriptionWidget, self).__init__(parent=parent)
|
||||
|
||||
icon_label = QtWidgets.QLabel(self)
|
||||
icon_label.setSizePolicy(
|
||||
QtWidgets.QSizePolicy.Maximum,
|
||||
QtWidgets.QSizePolicy.Maximum
|
||||
)
|
||||
|
||||
# Add 4 pixel padding to avoid icon being cut off
|
||||
icon_label.setFixedWidth(self.SIZE + 4)
|
||||
icon_label.setFixedHeight(self.SIZE + 4)
|
||||
|
||||
label_layout = QtWidgets.QVBoxLayout()
|
||||
label_layout.setSpacing(0)
|
||||
|
||||
family_label = QtWidgets.QLabel(self)
|
||||
family_label.setObjectName("CreatorFamilyLabel")
|
||||
family_label.setAlignment(QtCore.Qt.AlignBottom | QtCore.Qt.AlignLeft)
|
||||
|
||||
help_label = QtWidgets.QLabel(self)
|
||||
help_label.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft)
|
||||
|
||||
label_layout.addWidget(family_label)
|
||||
label_layout.addWidget(help_label)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(5)
|
||||
layout.addWidget(icon_label)
|
||||
layout.addLayout(label_layout)
|
||||
|
||||
self._help_label = help_label
|
||||
self._family_label = family_label
|
||||
self._icon_label = icon_label
|
||||
|
||||
def set_item(self, creator_plugin):
|
||||
"""Update elements to display information of a family item.
|
||||
|
||||
Args:
|
||||
item (dict): A family item as registered with name, help and icon
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
if not creator_plugin:
|
||||
self._icon_label.setPixmap(None)
|
||||
self._family_label.setText("")
|
||||
self._help_label.setText("")
|
||||
return
|
||||
|
||||
# Support a font-awesome icon
|
||||
icon_name = getattr(creator_plugin, "icon", None) or "info-circle"
|
||||
try:
|
||||
icon = qtawesome.icon("fa.{}".format(icon_name), color="white")
|
||||
pixmap = icon.pixmap(self.SIZE, self.SIZE)
|
||||
except Exception:
|
||||
print("BUG: Couldn't load icon \"fa.{}\"".format(str(icon_name)))
|
||||
# Create transparent pixmap
|
||||
pixmap = QtGui.QPixmap()
|
||||
pixmap.fill(QtCore.Qt.transparent)
|
||||
pixmap = pixmap.scaled(self.SIZE, self.SIZE)
|
||||
|
||||
# Parse a clean line from the Creator's docstring
|
||||
docstring = inspect.getdoc(creator_plugin)
|
||||
creator_help = docstring.splitlines()[0] if docstring else ""
|
||||
|
||||
self._icon_label.setPixmap(pixmap)
|
||||
self._family_label.setText(creator_plugin.family)
|
||||
self._help_label.setText(creator_help)
|
||||
509
openpype/tools/creator/window.py
Normal file
509
openpype/tools/creator/window.py
Normal file
|
|
@ -0,0 +1,509 @@
|
|||
import sys
|
||||
import traceback
|
||||
import re
|
||||
|
||||
from Qt import QtWidgets, QtCore
|
||||
|
||||
from avalon import api, io
|
||||
|
||||
from openpype import style
|
||||
from openpype.api import get_current_project_settings
|
||||
from openpype.tools.utils.lib import qt_app_context
|
||||
from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS
|
||||
|
||||
from .model import CreatorsModel
|
||||
from .widgets import (
|
||||
CreateErrorMessageBox,
|
||||
VariantLineEdit,
|
||||
FamilyDescriptionWidget
|
||||
)
|
||||
from .constants import (
|
||||
ITEM_ID_ROLE,
|
||||
SEPARATOR,
|
||||
SEPARATORS
|
||||
)
|
||||
|
||||
module = sys.modules[__name__]
|
||||
module.window = None
|
||||
|
||||
|
||||
class CreatorWindow(QtWidgets.QDialog):
|
||||
def __init__(self, parent=None):
|
||||
super(CreatorWindow, self).__init__(parent)
|
||||
self.setWindowTitle("Instance Creator")
|
||||
self.setFocusPolicy(QtCore.Qt.StrongFocus)
|
||||
if not parent:
|
||||
self.setWindowFlags(
|
||||
self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint
|
||||
)
|
||||
|
||||
creator_info = FamilyDescriptionWidget(self)
|
||||
|
||||
creators_model = CreatorsModel()
|
||||
|
||||
creators_proxy = QtCore.QSortFilterProxyModel()
|
||||
creators_proxy.setSourceModel(creators_model)
|
||||
|
||||
creators_view = QtWidgets.QListView(self)
|
||||
creators_view.setObjectName("CreatorsView")
|
||||
creators_view.setModel(creators_proxy)
|
||||
|
||||
asset_name_input = QtWidgets.QLineEdit(self)
|
||||
variant_input = VariantLineEdit(self)
|
||||
subset_name_input = QtWidgets.QLineEdit(self)
|
||||
subset_name_input.setEnabled(False)
|
||||
|
||||
subset_button = QtWidgets.QPushButton()
|
||||
subset_button.setFixedWidth(18)
|
||||
subset_menu = QtWidgets.QMenu(subset_button)
|
||||
subset_button.setMenu(subset_menu)
|
||||
|
||||
name_layout = QtWidgets.QHBoxLayout()
|
||||
name_layout.addWidget(variant_input)
|
||||
name_layout.addWidget(subset_button)
|
||||
name_layout.setSpacing(3)
|
||||
name_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
body_layout = QtWidgets.QVBoxLayout()
|
||||
body_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
body_layout.addWidget(creator_info, 0)
|
||||
body_layout.addWidget(QtWidgets.QLabel("Family", self), 0)
|
||||
body_layout.addWidget(creators_view, 1)
|
||||
body_layout.addWidget(QtWidgets.QLabel("Asset", self), 0)
|
||||
body_layout.addWidget(asset_name_input, 0)
|
||||
body_layout.addWidget(QtWidgets.QLabel("Subset", self), 0)
|
||||
body_layout.addLayout(name_layout, 0)
|
||||
body_layout.addWidget(subset_name_input, 0)
|
||||
|
||||
useselection_chk = QtWidgets.QCheckBox("Use selection", self)
|
||||
useselection_chk.setCheckState(QtCore.Qt.Checked)
|
||||
|
||||
create_btn = QtWidgets.QPushButton("Create", self)
|
||||
# Need to store error_msg to prevent garbage collection
|
||||
msg_label = QtWidgets.QLabel(self)
|
||||
|
||||
footer_layout = QtWidgets.QVBoxLayout()
|
||||
footer_layout.addWidget(create_btn, 0)
|
||||
footer_layout.addWidget(msg_label, 0)
|
||||
footer_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.addLayout(body_layout, 1)
|
||||
layout.addWidget(useselection_chk, 0, QtCore.Qt.AlignLeft)
|
||||
layout.addLayout(footer_layout, 0)
|
||||
|
||||
msg_timer = QtCore.QTimer()
|
||||
msg_timer.setSingleShot(True)
|
||||
msg_timer.setInterval(5000)
|
||||
|
||||
validation_timer = QtCore.QTimer()
|
||||
validation_timer.setSingleShot(True)
|
||||
validation_timer.setInterval(300)
|
||||
|
||||
msg_timer.timeout.connect(self._on_msg_timer)
|
||||
validation_timer.timeout.connect(self._on_validation_timer)
|
||||
|
||||
create_btn.clicked.connect(self._on_create)
|
||||
variant_input.returnPressed.connect(self._on_create)
|
||||
variant_input.textChanged.connect(self._on_data_changed)
|
||||
variant_input.report.connect(self.echo)
|
||||
asset_name_input.textChanged.connect(self._on_data_changed)
|
||||
creators_view.selectionModel().currentChanged.connect(
|
||||
self._on_selection_changed
|
||||
)
|
||||
|
||||
# Store valid states and
|
||||
self._is_valid = False
|
||||
create_btn.setEnabled(self._is_valid)
|
||||
|
||||
self._first_show = True
|
||||
|
||||
# Message dialog when something goes wrong during creation
|
||||
self._message_dialog = None
|
||||
|
||||
self._creator_info = creator_info
|
||||
self._create_btn = create_btn
|
||||
self._useselection_chk = useselection_chk
|
||||
self._variant_input = variant_input
|
||||
self._subset_name_input = subset_name_input
|
||||
self._asset_name_input = asset_name_input
|
||||
|
||||
self._creators_model = creators_model
|
||||
self._creators_proxy = creators_proxy
|
||||
self._creators_view = creators_view
|
||||
|
||||
self._subset_btn = subset_button
|
||||
self._subset_menu = subset_menu
|
||||
|
||||
self._msg_label = msg_label
|
||||
|
||||
self._validation_timer = validation_timer
|
||||
self._msg_timer = msg_timer
|
||||
|
||||
# Defaults
|
||||
self.resize(300, 500)
|
||||
variant_input.setFocus()
|
||||
|
||||
def _set_valid_state(self, valid):
|
||||
if self._is_valid == valid:
|
||||
return
|
||||
self._is_valid = valid
|
||||
self._create_btn.setEnabled(valid)
|
||||
|
||||
def _build_menu(self, default_names=None):
|
||||
"""Create optional predefined subset names
|
||||
|
||||
Args:
|
||||
default_names(list): all predefined names
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
if not default_names:
|
||||
default_names = []
|
||||
|
||||
menu = self._subset_menu
|
||||
button = self._subset_btn
|
||||
|
||||
# Get and destroy the action group
|
||||
group = button.findChild(QtWidgets.QActionGroup)
|
||||
if group:
|
||||
group.deleteLater()
|
||||
|
||||
state = any(default_names)
|
||||
button.setEnabled(state)
|
||||
if state is False:
|
||||
return
|
||||
|
||||
# Build new action group
|
||||
group = QtWidgets.QActionGroup(button)
|
||||
for name in default_names:
|
||||
if name in SEPARATORS:
|
||||
menu.addSeparator()
|
||||
continue
|
||||
action = group.addAction(name)
|
||||
menu.addAction(action)
|
||||
|
||||
group.triggered.connect(self._on_action_clicked)
|
||||
|
||||
def _on_action_clicked(self, action):
|
||||
self._variant_input.setText(action.text())
|
||||
|
||||
def _on_data_changed(self, *args):
|
||||
# Set invalid state until it's reconfirmed to be valid by the
|
||||
# scheduled callback so any form of creation is held back until
|
||||
# valid again
|
||||
self._set_valid_state(False)
|
||||
|
||||
self._validation_timer.start()
|
||||
|
||||
def _on_validation_timer(self):
|
||||
index = self._creators_view.currentIndex()
|
||||
item_id = index.data(ITEM_ID_ROLE)
|
||||
creator_plugin = self._creators_model.get_creator_by_id(item_id)
|
||||
user_input_text = self._variant_input.text()
|
||||
asset_name = self._asset_name_input.text()
|
||||
|
||||
# Early exit if no asset name
|
||||
if not asset_name.strip():
|
||||
self._build_menu()
|
||||
self.echo("Asset name is required ..")
|
||||
self._set_valid_state(False)
|
||||
return
|
||||
|
||||
asset_doc = None
|
||||
if creator_plugin:
|
||||
# Get the asset from the database which match with the name
|
||||
asset_doc = io.find_one(
|
||||
{"name": asset_name, "type": "asset"},
|
||||
projection={"_id": 1}
|
||||
)
|
||||
|
||||
# Get plugin
|
||||
if not asset_doc or not creator_plugin:
|
||||
subset_name = user_input_text
|
||||
self._build_menu()
|
||||
|
||||
if not creator_plugin:
|
||||
self.echo("No registered families ..")
|
||||
else:
|
||||
self.echo("Asset '%s' not found .." % asset_name)
|
||||
self._set_valid_state(False)
|
||||
return
|
||||
|
||||
project_name = io.Session["AVALON_PROJECT"]
|
||||
asset_id = asset_doc["_id"]
|
||||
task_name = io.Session["AVALON_TASK"]
|
||||
|
||||
# Calculate subset name with Creator plugin
|
||||
subset_name = creator_plugin.get_subset_name(
|
||||
user_input_text, task_name, asset_id, project_name
|
||||
)
|
||||
# Force replacement of prohibited symbols
|
||||
# QUESTION should Creator care about this and here should be only
|
||||
# validated with schema regex?
|
||||
|
||||
# Allow curly brackets in subset name for dynamic keys
|
||||
curly_left = "__cbl__"
|
||||
curly_right = "__cbr__"
|
||||
tmp_subset_name = (
|
||||
subset_name
|
||||
.replace("{", curly_left)
|
||||
.replace("}", curly_right)
|
||||
)
|
||||
# Replace prohibited symbols
|
||||
tmp_subset_name = re.sub(
|
||||
"[^{}]+".format(SUBSET_NAME_ALLOWED_SYMBOLS),
|
||||
"",
|
||||
tmp_subset_name
|
||||
)
|
||||
subset_name = (
|
||||
tmp_subset_name
|
||||
.replace(curly_left, "{")
|
||||
.replace(curly_right, "}")
|
||||
)
|
||||
self._subset_name_input.setText(subset_name)
|
||||
|
||||
# Get all subsets of the current asset
|
||||
subset_docs = io.find(
|
||||
{
|
||||
"type": "subset",
|
||||
"parent": asset_id
|
||||
},
|
||||
{"name": 1}
|
||||
)
|
||||
existing_subset_names = set(subset_docs.distinct("name"))
|
||||
existing_subset_names_low = set(
|
||||
_name.lower()
|
||||
for _name in existing_subset_names
|
||||
)
|
||||
|
||||
# Defaults to dropdown
|
||||
defaults = []
|
||||
# Check if Creator plugin has set defaults
|
||||
if (
|
||||
creator_plugin.defaults
|
||||
and isinstance(creator_plugin.defaults, (list, tuple, set))
|
||||
):
|
||||
defaults = list(creator_plugin.defaults)
|
||||
|
||||
# Replace
|
||||
compare_regex = re.compile(re.sub(
|
||||
user_input_text, "(.+)", subset_name, flags=re.IGNORECASE
|
||||
))
|
||||
subset_hints = set()
|
||||
if user_input_text:
|
||||
for _name in existing_subset_names:
|
||||
_result = compare_regex.search(_name)
|
||||
if _result:
|
||||
subset_hints |= set(_result.groups())
|
||||
|
||||
if subset_hints:
|
||||
if defaults:
|
||||
defaults.append(SEPARATOR)
|
||||
defaults.extend(subset_hints)
|
||||
self._build_menu(defaults)
|
||||
|
||||
# Indicate subset existence
|
||||
if not user_input_text:
|
||||
self._variant_input.as_empty()
|
||||
elif subset_name.lower() in existing_subset_names_low:
|
||||
# validate existence of subset name with lowered text
|
||||
# - "renderMain" vs. "rensermain" mean same path item for
|
||||
# windows
|
||||
self._variant_input.as_exists()
|
||||
else:
|
||||
self._variant_input.as_new()
|
||||
|
||||
# Update the valid state
|
||||
valid = subset_name.strip() != ""
|
||||
|
||||
self._set_valid_state(valid)
|
||||
|
||||
def _on_selection_changed(self, old_idx, new_idx):
|
||||
index = self._creators_view.currentIndex()
|
||||
item_id = index.data(ITEM_ID_ROLE)
|
||||
|
||||
creator_plugin = self._creators_model.get_creator_by_id(item_id)
|
||||
|
||||
self._creator_info.set_item(creator_plugin)
|
||||
|
||||
if creator_plugin is None:
|
||||
return
|
||||
|
||||
default = None
|
||||
if hasattr(creator_plugin, "get_default_variant"):
|
||||
default = creator_plugin.get_default_variant()
|
||||
|
||||
if not default:
|
||||
if (
|
||||
creator_plugin.defaults
|
||||
and isinstance(creator_plugin.defaults, list)
|
||||
):
|
||||
default = creator_plugin.defaults[0]
|
||||
else:
|
||||
default = "Default"
|
||||
|
||||
self._variant_input.setText(default)
|
||||
|
||||
self._on_data_changed()
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""Custom keyPressEvent.
|
||||
|
||||
Override keyPressEvent to do nothing so that Maya's panels won't
|
||||
take focus when pressing "SHIFT" whilst mouse is over viewport or
|
||||
outliner. This way users don't accidently perform Maya commands
|
||||
whilst trying to name an instance.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
def showEvent(self, event):
|
||||
super(CreatorWindow, self).showEvent(event)
|
||||
if self._first_show:
|
||||
self._first_show = False
|
||||
self.setStyleSheet(style.load_stylesheet())
|
||||
|
||||
def refresh(self):
|
||||
self._asset_name_input.setText(io.Session["AVALON_ASSET"])
|
||||
|
||||
self._creators_model.reset()
|
||||
|
||||
pype_project_setting = (
|
||||
get_current_project_settings()
|
||||
["global"]
|
||||
["tools"]
|
||||
["creator"]
|
||||
["families_smart_select"]
|
||||
)
|
||||
current_index = None
|
||||
family = None
|
||||
task_name = io.Session.get("AVALON_TASK", None)
|
||||
lowered_task_name = task_name.lower()
|
||||
if task_name:
|
||||
for _family, _task_names in pype_project_setting.items():
|
||||
_low_task_names = {name.lower() for name in _task_names}
|
||||
for _task_name in _low_task_names:
|
||||
if _task_name in lowered_task_name:
|
||||
family = _family
|
||||
break
|
||||
if family:
|
||||
break
|
||||
|
||||
if family:
|
||||
indexes = self._creators_model.get_indexes_by_family(family)
|
||||
if indexes:
|
||||
index = indexes[0]
|
||||
current_index = self._creators_proxy.mapFromSource(index)
|
||||
|
||||
if current_index is None or not current_index.isValid():
|
||||
current_index = self._creators_proxy.index(0, 0)
|
||||
|
||||
self._creators_view.setCurrentIndex(current_index)
|
||||
|
||||
def _on_create(self):
|
||||
# Do not allow creation in an invalid state
|
||||
if not self._is_valid:
|
||||
return
|
||||
|
||||
index = self._creators_view.currentIndex()
|
||||
item_id = index.data(ITEM_ID_ROLE)
|
||||
creator_plugin = self._creators_model.get_creator_by_id(item_id)
|
||||
if creator_plugin is None:
|
||||
return
|
||||
|
||||
subset_name = self._subset_name_input.text()
|
||||
asset_name = self._asset_name_input.text()
|
||||
use_selection = self._useselection_chk.isChecked()
|
||||
|
||||
variant = self._variant_input.text()
|
||||
|
||||
error_info = None
|
||||
try:
|
||||
api.create(
|
||||
creator_plugin,
|
||||
subset_name,
|
||||
asset_name,
|
||||
options={"useSelection": use_selection},
|
||||
data={"variant": variant}
|
||||
)
|
||||
|
||||
except api.CreatorError as exc:
|
||||
self.echo("Creator error: {}".format(str(exc)))
|
||||
error_info = (str(exc), None)
|
||||
|
||||
except Exception as exc:
|
||||
self.echo("Program error: %s" % str(exc))
|
||||
|
||||
exc_type, exc_value, exc_traceback = sys.exc_info()
|
||||
formatted_traceback = "".join(traceback.format_exception(
|
||||
exc_type, exc_value, exc_traceback
|
||||
))
|
||||
error_info = (str(exc), formatted_traceback)
|
||||
|
||||
if error_info:
|
||||
box = CreateErrorMessageBox(
|
||||
creator_plugin.family, subset_name, asset_name, *error_info
|
||||
)
|
||||
box.show()
|
||||
# Store dialog so is not garbage collected before is shown
|
||||
self._message_dialog = box
|
||||
|
||||
else:
|
||||
self.echo("Created %s .." % subset_name)
|
||||
|
||||
def _on_msg_timer(self):
|
||||
self._msg_label.setText("")
|
||||
|
||||
def echo(self, message):
|
||||
self._msg_label.setText(str(message))
|
||||
self._msg_timer.start()
|
||||
|
||||
|
||||
def show(debug=False, parent=None):
|
||||
"""Display asset creator GUI
|
||||
|
||||
Arguments:
|
||||
debug (bool, optional): Run loader in debug-mode,
|
||||
defaults to False
|
||||
parent (QtCore.QObject, optional): When provided parent the interface
|
||||
to this QObject.
|
||||
|
||||
"""
|
||||
|
||||
try:
|
||||
module.window.close()
|
||||
del(module.window)
|
||||
except (AttributeError, RuntimeError):
|
||||
pass
|
||||
|
||||
if debug:
|
||||
from avalon import mock
|
||||
for creator in mock.creators:
|
||||
api.register_plugin(api.Creator, creator)
|
||||
|
||||
import traceback
|
||||
sys.excepthook = lambda typ, val, tb: traceback.print_last()
|
||||
|
||||
io.install()
|
||||
|
||||
any_project = next(
|
||||
project for project in io.projects()
|
||||
if project.get("active", True) is not False
|
||||
)
|
||||
|
||||
api.Session["AVALON_PROJECT"] = any_project["name"]
|
||||
module.project = any_project["name"]
|
||||
|
||||
with qt_app_context():
|
||||
window = CreatorWindow(parent)
|
||||
window.refresh()
|
||||
window.show()
|
||||
|
||||
module.window = window
|
||||
|
||||
# Pull window to the front.
|
||||
module.window.raise_()
|
||||
module.window.activateWindow()
|
||||
|
|
@ -6,7 +6,6 @@ CONTEXT_LABEL = "Options"
|
|||
|
||||
# Allowed symbols for subset name (and variant)
|
||||
# - characters, numbers, unsercore and dash
|
||||
SUBSET_NAME_ALLOWED_SYMBOLS = "a-zA-Z0-9_."
|
||||
VARIANT_TOOLTIP = (
|
||||
"Variant may contain alphabetical characters (a-Z)"
|
||||
"\nnumerical characters (0-9) dot (\".\") or underscore (\"_\")."
|
||||
|
|
@ -23,7 +22,6 @@ FAMILY_ROLE = QtCore.Qt.UserRole + 5
|
|||
__all__ = (
|
||||
"CONTEXT_ID",
|
||||
|
||||
"SUBSET_NAME_ALLOWED_SYMBOLS",
|
||||
"VARIANT_TOOLTIP",
|
||||
|
||||
"INSTANCE_ID_ROLE",
|
||||
|
|
|
|||
|
|
@ -9,11 +9,13 @@ except Exception:
|
|||
commonmark = None
|
||||
from Qt import QtWidgets, QtCore, QtGui
|
||||
|
||||
from openpype.pipeline.create import CreatorError
|
||||
from openpype.pipeline.create import (
|
||||
CreatorError,
|
||||
SUBSET_NAME_ALLOWED_SYMBOLS
|
||||
)
|
||||
|
||||
from .widgets import IconValuePixmapLabel
|
||||
from ..constants import (
|
||||
SUBSET_NAME_ALLOWED_SYMBOLS,
|
||||
VARIANT_TOOLTIP,
|
||||
CREATOR_IDENTIFIER_ROLE,
|
||||
FAMILY_ROLE
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from avalon.vendor import qtawesome
|
|||
|
||||
from openpype.widgets.attribute_defs import create_widget_for_attr_def
|
||||
from openpype.tools.flickcharm import FlickCharm
|
||||
|
||||
from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS
|
||||
from .models import (
|
||||
AssetsHierarchyModel,
|
||||
TasksModel,
|
||||
|
|
@ -21,7 +21,6 @@ from .icons import (
|
|||
)
|
||||
|
||||
from ..constants import (
|
||||
SUBSET_NAME_ALLOWED_SYMBOLS,
|
||||
VARIANT_TOOLTIP
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -108,23 +108,19 @@ class HostToolsHelper:
|
|||
def get_creator_tool(self, parent):
|
||||
"""Create, cache and return creator tool window."""
|
||||
if self._creator_tool is None:
|
||||
from avalon.tools.creator.app import Window
|
||||
from openpype.tools.creator import CreatorWindow
|
||||
|
||||
creator_window = Window(parent=parent or self._parent)
|
||||
creator_window = CreatorWindow(parent=parent or self._parent)
|
||||
self._creator_tool = creator_window
|
||||
|
||||
return self._creator_tool
|
||||
|
||||
def show_creator(self, parent=None):
|
||||
"""Show tool to create new instantes for publishing."""
|
||||
from avalon import style
|
||||
|
||||
creator_tool = self.get_creator_tool(parent)
|
||||
creator_tool.refresh()
|
||||
creator_tool.show()
|
||||
|
||||
creator_tool.setStyleSheet(style.load_stylesheet())
|
||||
|
||||
# Pull window to the front.
|
||||
creator_tool.raise_()
|
||||
creator_tool.activateWindow()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue