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()