diff --git a/openpype/pipeline/create/__init__.py b/openpype/pipeline/create/__init__.py
index 610ef6d8e2..948b719851 100644
--- a/openpype/pipeline/create/__init__.py
+++ b/openpype/pipeline/create/__init__.py
@@ -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",
diff --git a/openpype/pipeline/create/constants.py b/openpype/pipeline/create/constants.py
new file mode 100644
index 0000000000..bfbbccfd12
--- /dev/null
+++ b/openpype/pipeline/create/constants.py
@@ -0,0 +1,6 @@
+SUBSET_NAME_ALLOWED_SYMBOLS = "a-zA-Z0-9_."
+
+
+__all__ = (
+ "SUBSET_NAME_ALLOWED_SYMBOLS",
+)
diff --git a/openpype/style/style.css b/openpype/style/style.css
index 9d56e85084..fa5b41cd07 100644
--- a/openpype/style/style.css
+++ b/openpype/style/style.css
@@ -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;
diff --git a/openpype/tools/creator/__init__.py b/openpype/tools/creator/__init__.py
new file mode 100644
index 0000000000..585b8bdf80
--- /dev/null
+++ b/openpype/tools/creator/__init__.py
@@ -0,0 +1,9 @@
+from .window import (
+ show,
+ CreatorWindow
+)
+
+__all__ = (
+ "show",
+ "CreatorWindow"
+)
diff --git a/openpype/tools/creator/constants.py b/openpype/tools/creator/constants.py
new file mode 100644
index 0000000000..26a25dc010
--- /dev/null
+++ b/openpype/tools/creator/constants.py
@@ -0,0 +1,8 @@
+from Qt import QtCore
+
+
+FAMILY_ROLE = QtCore.Qt.UserRole + 1
+ITEM_ID_ROLE = QtCore.Qt.UserRole + 2
+
+SEPARATOR = "---"
+SEPARATORS = {"---", "---separator---"}
diff --git a/openpype/tools/creator/model.py b/openpype/tools/creator/model.py
new file mode 100644
index 0000000000..6907e8f0aa
--- /dev/null
+++ b/openpype/tools/creator/model.py
@@ -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
diff --git a/openpype/tools/creator/widgets.py b/openpype/tools/creator/widgets.py
new file mode 100644
index 0000000000..89c90cc048
--- /dev/null
+++ b/openpype/tools/creator/widgets.py
@@ -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 = (
+ "Failed to create"
+ )
+ main_label_widget = QtWidgets.QLabel(main_label, self)
+ body_layout.addWidget(main_label_widget)
+
+ item_name_template = (
+ "Family: {}
"
+ "Subset: {}
"
+ "Asset: {}
"
+ )
+ exc_msg_template = "{}"
+
+ 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", "
"), self
+ )
+ body_layout.addWidget(item_name_widget)
+
+ exc_msg = exc_msg_template.format(exc_msg.replace("\n", "
"))
+ 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", "
"), 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)
diff --git a/openpype/tools/creator/window.py b/openpype/tools/creator/window.py
new file mode 100644
index 0000000000..dca1735121
--- /dev/null
+++ b/openpype/tools/creator/window.py
@@ -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()
diff --git a/openpype/tools/publisher/constants.py b/openpype/tools/publisher/constants.py
index cf0850bde8..dc44aade45 100644
--- a/openpype/tools/publisher/constants.py
+++ b/openpype/tools/publisher/constants.py
@@ -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",
diff --git a/openpype/tools/publisher/widgets/create_dialog.py b/openpype/tools/publisher/widgets/create_dialog.py
index 0206f038fb..84fc6d4e97 100644
--- a/openpype/tools/publisher/widgets/create_dialog.py
+++ b/openpype/tools/publisher/widgets/create_dialog.py
@@ -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
diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py
index 606985c058..fe00ee78d3 100644
--- a/openpype/tools/publisher/widgets/widgets.py
+++ b/openpype/tools/publisher/widgets/widgets.py
@@ -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
)
diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py
index 1cc23fcf97..ef1cd3cf5c 100644
--- a/openpype/tools/utils/host_tools.py
+++ b/openpype/tools/utils/host_tools.py
@@ -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()