From bd866f7f13e0634758c8cedfca4573d4e5904695 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 12 Nov 2021 20:03:27 +0100 Subject: [PATCH 01/18] copied creator to openpype tools --- openpype/tools/creator/__init__.py | 7 + openpype/tools/creator/app.py | 720 +++++++++++++++++++++++++++++ openpype/tools/creator/widgets.py | 79 ++++ 3 files changed, 806 insertions(+) create mode 100644 openpype/tools/creator/__init__.py create mode 100644 openpype/tools/creator/app.py create mode 100644 openpype/tools/creator/widgets.py diff --git a/openpype/tools/creator/__init__.py b/openpype/tools/creator/__init__.py new file mode 100644 index 0000000000..694caf15fe --- /dev/null +++ b/openpype/tools/creator/__init__.py @@ -0,0 +1,7 @@ +from .app import ( + show, +) + +__all__ = [ + "show", +] diff --git a/openpype/tools/creator/app.py b/openpype/tools/creator/app.py new file mode 100644 index 0000000000..83a43556e2 --- /dev/null +++ b/openpype/tools/creator/app.py @@ -0,0 +1,720 @@ +import sys +import inspect +import traceback +import re + +from ...vendor.Qt import QtWidgets, QtCore, QtGui +from ...vendor import qtawesome +from ...vendor import six +from ... import api, io, style + +from .widgets import CreateErrorMessageBox +from .. import lib +from openpype.api import get_current_project_settings + +module = sys.modules[__name__] +module.window = None +module.root = api.registered_root() + +HelpRole = QtCore.Qt.UserRole + 2 +FamilyRole = QtCore.Qt.UserRole + 3 +ExistsRole = QtCore.Qt.UserRole + 4 +PluginRole = QtCore.Qt.UserRole + 5 + +Separator = "---separator---" + +# TODO regex should be defined by schema +SubsetAllowedSymbols = "a-zA-Z0-9_." + + +class SubsetNameValidator(QtGui.QRegExpValidator): + + invalid = QtCore.Signal(set) + pattern = "^[{}]*$".format(SubsetAllowedSymbols) + + def __init__(self): + reg = QtCore.QRegExp(self.pattern) + super(SubsetNameValidator, self).__init__(reg) + + def validate(self, input, pos): + results = super(SubsetNameValidator, self).validate(input, pos) + if results[0] == self.Invalid: + self.invalid.emit(self.invalid_chars(input)) + return results + + def invalid_chars(self, input): + invalid = set() + re_valid = re.compile(self.pattern) + for char in input: + if char == " ": + invalid.add("' '") + continue + if not re_valid.match(char): + invalid.add(char) + return invalid + + +class SubsetNameLineEdit(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(SubsetNameLineEdit, 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 Window(QtWidgets.QDialog): + + stateChanged = QtCore.Signal(bool) + + def __init__(self, parent=None): + super(Window, self).__init__(parent) + self.setWindowTitle("Instance Creator") + self.setFocusPolicy(QtCore.Qt.StrongFocus) + if not parent: + self.setWindowFlags( + self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint + ) + + # Store the widgets for lookup in here + self.data = dict() + + # Store internal states in here + self.state = { + "valid": False + } + # Message dialog when something goes wrong during creation + self.message_dialog = None + + body = QtWidgets.QWidget() + lists = QtWidgets.QWidget() + footer = QtWidgets.QWidget() + + container = QtWidgets.QWidget() + + listing = QtWidgets.QListWidget() + listing.setSortingEnabled(True) + asset = QtWidgets.QLineEdit() + name = SubsetNameLineEdit() + result = QtWidgets.QLineEdit() + result.setStyleSheet("color: gray;") + result.setEnabled(False) + + # region Menu for default subset names + + subset_button = QtWidgets.QPushButton() + subset_button.setFixedWidth(18) + subset_menu = QtWidgets.QMenu(subset_button) + subset_button.setMenu(subset_menu) + + # endregion + + name_layout = QtWidgets.QHBoxLayout() + name_layout.addWidget(name) + name_layout.addWidget(subset_button) + name_layout.setSpacing(3) + name_layout.setContentsMargins(0, 0, 0, 0) + + layout = QtWidgets.QVBoxLayout(container) + + header = FamilyDescriptionWidget(parent=self) + layout.addWidget(header) + + layout.addWidget(QtWidgets.QLabel("Family")) + layout.addWidget(listing) + layout.addWidget(QtWidgets.QLabel("Asset")) + layout.addWidget(asset) + layout.addWidget(QtWidgets.QLabel("Subset")) + layout.addLayout(name_layout) + layout.addWidget(result) + layout.setContentsMargins(0, 0, 0, 0) + + options = QtWidgets.QWidget() + + useselection_chk = QtWidgets.QCheckBox("Use selection") + useselection_chk.setCheckState(QtCore.Qt.Checked) + + layout = QtWidgets.QGridLayout(options) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(useselection_chk, 1, 1) + + layout = QtWidgets.QHBoxLayout(lists) + layout.addWidget(container) + layout.setContentsMargins(0, 0, 0, 0) + + layout = QtWidgets.QVBoxLayout(body) + layout.addWidget(lists) + layout.addWidget(options, 0, QtCore.Qt.AlignLeft) + layout.setContentsMargins(0, 0, 0, 0) + + create_btn = QtWidgets.QPushButton("Create") + # Need to store error_msg to prevent garbage collection. + self.error_msg = QtWidgets.QLabel() + self.error_msg.setFixedHeight(20) + + layout = QtWidgets.QVBoxLayout(footer) + layout.addWidget(create_btn) + layout.addWidget(self.error_msg) + layout.setContentsMargins(0, 0, 0, 0) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(body) + layout.addWidget(footer) + + self.data = { + "Create Button": create_btn, + "Listing": listing, + "Use Selection Checkbox": useselection_chk, + "Subset": name, + "Subset Menu": subset_menu, + "Result": result, + "Asset": asset, + "Error Message": self.error_msg, + } + + for _name, widget in self.data.items(): + widget.setObjectName(_name) + + create_btn.clicked.connect(self.on_create) + name.returnPressed.connect(self.on_create) + name.textChanged.connect(self.on_data_changed) + name.report.connect(self.echo) + asset.textChanged.connect(self.on_data_changed) + listing.currentItemChanged.connect(self.on_selection_changed) + listing.currentItemChanged.connect(header.set_item) + + self.stateChanged.connect(self._on_state_changed) + + # Defaults + self.resize(300, 500) + name.setFocus() + create_btn.setEnabled(False) + + def _on_state_changed(self, state): + self.state["valid"] = state + self.data["Create Button"].setEnabled(state) + + def _build_menu(self, default_names): + """Create optional predefined subset names + + Args: + default_names(list): all predefined names + + Returns: + None + """ + + menu = self.data["Subset Menu"] + button = menu.parent() + + # 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 == Separator: + menu.addSeparator() + continue + action = group.addAction(name) + menu.addAction(action) + + group.triggered.connect(self._on_action_clicked) + + def _on_action_clicked(self, action): + name = self.data["Subset"] + name.setText(action.text()) + + def _on_data_changed(self): + listing = self.data["Listing"] + asset_name = self.data["Asset"] + subset = self.data["Subset"] + result = self.data["Result"] + + item = listing.currentItem() + user_input_text = subset.text() + asset_name = asset_name.text() + + # Early exit if no asset name + if not asset_name.strip(): + self._build_menu([]) + item.setData(ExistsRole, False) + self.echo("Asset name is required ..") + self.stateChanged.emit(False) + return + + # 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 + plugin = item.data(PluginRole) + if asset_doc and plugin: + 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 = 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(SubsetAllowedSymbols), + "", + tmp_subset_name + ) + subset_name = ( + tmp_subset_name + .replace(curly_left, "{") + .replace(curly_right, "}") + ) + result.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 ( + plugin.defaults + and isinstance(plugin.defaults, (list, tuple, set)) + ): + defaults = list(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: + subset.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 + subset.as_exists() + else: + subset.as_new() + + item.setData(ExistsRole, True) + + else: + subset_name = user_input_text + self._build_menu([]) + item.setData(ExistsRole, False) + + if not plugin: + self.echo("No registered families ..") + else: + self.echo("Asset '%s' not found .." % asset_name) + + # Update the valid state + valid = ( + subset_name.strip() != "" and + item.data(QtCore.Qt.ItemIsEnabled) and + item.data(ExistsRole) + ) + self.stateChanged.emit(valid) + + 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.stateChanged.emit(False) + + lib.schedule(self._on_data_changed, 500, channel="gui") + + def on_selection_changed(self, *args): + name = self.data["Subset"] + item = self.data["Listing"].currentItem() + + plugin = item.data(PluginRole) + if plugin is None: + return + + default = None + if hasattr(plugin, "get_default_variant"): + default = plugin.get_default_variant() + + if not default: + if plugin.defaults and isinstance(plugin.defaults, list): + default = plugin.defaults[0] + else: + default = "Default" + + name.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. + + """ + + def refresh(self): + + listing = self.data["Listing"] + asset = self.data["Asset"] + asset.setText(api.Session["AVALON_ASSET"]) + + listing.clear() + + has_families = False + + creators = api.discover(api.Creator) + + for creator in creators: + label = creator.label or creator.family + item = QtWidgets.QListWidgetItem(label) + item.setData(QtCore.Qt.ItemIsEnabled, True) + item.setData(HelpRole, creator.__doc__) + item.setData(FamilyRole, creator.family) + item.setData(PluginRole, creator) + item.setData(ExistsRole, False) + listing.addItem(item) + + has_families = True + + if not has_families: + item = QtWidgets.QListWidgetItem("No registered families") + item.setData(QtCore.Qt.ItemIsEnabled, False) + listing.addItem(item) + + pype_project_setting = ( + get_current_project_settings() + ["global"] + ["tools"] + ["creator"] + ["families_smart_select"] + ) + item = None + family_type = None + task_name = io.Session.get('AVALON_TASK', None) + if task_name: + for key, value in pype_project_setting.items(): + for t_name in value: + if t_name in task_name.lower(): + family_type = key + break + if family_type: + break + if family_type: + items = listing.findItems(family_type, QtCore.Qt.MatchExactly) + if len(items) > 0: + item = items[0] + listing.setCurrentItem(item) + if not item: + listing.setCurrentItem(listing.item(0)) + + def on_create(self): + + # Do not allow creation in an invalid state + if not self.state["valid"]: + return + + asset = self.data["Asset"] + listing = self.data["Listing"] + result = self.data["Result"] + + item = listing.currentItem() + if item is None: + return + + subset_name = result.text() + asset = asset.text() + family = item.data(FamilyRole) + Creator = item.data(PluginRole) + use_selection = self.data["Use Selection Checkbox"].isChecked() + + variant = self.data["Subset"].text() + + error_info = None + try: + api.create( + Creator, + subset_name, + asset, + 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( + family, subset_name, asset, *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 echo(self, message): + widget = self.data["Error Message"] + widget.setText(str(message)) + widget.show() + + lib.schedule(lambda: widget.setText(""), 5000, channel="message") + + +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) + + # Header font + font = QtGui.QFont() + font.setBold(True) + font.setPointSize(14) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + + icon = QtWidgets.QLabel() + icon.setSizePolicy(QtWidgets.QSizePolicy.Maximum, + QtWidgets.QSizePolicy.Maximum) + + # Add 4 pixel padding to avoid icon being cut off + icon.setFixedWidth(self.SIZE + 4) + icon.setFixedHeight(self.SIZE + 4) + icon.setStyleSheet(""" + QLabel { + padding-right: 5px; + } + """) + + label_layout = QtWidgets.QVBoxLayout() + label_layout.setSpacing(0) + + family = QtWidgets.QLabel("family") + family.setFont(font) + family.setAlignment(QtCore.Qt.AlignBottom | QtCore.Qt.AlignLeft) + + help = QtWidgets.QLabel("help") + help.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft) + + label_layout.addWidget(family) + label_layout.addWidget(help) + + layout.addWidget(icon) + layout.addLayout(label_layout) + + self.help = help + self.family = family + self.icon = icon + + def set_item(self, item): + """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 item: + return + + # Support a font-awesome icon + plugin = item.data(PluginRole) + icon_name = getattr(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(plugin) + help = docstring.splitlines()[0] if docstring else "" + + self.icon.setPixmap(pixmap) + self.family.setText(item.data(FamilyRole)) + self.help.setText(help) + + +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 lib.application(): + window = Window(parent) + window.setStyleSheet(style.load_stylesheet()) + window.refresh() + window.show() + + module.window = window + + # Pull window to the front. + module.window.raise_() + module.window.activateWindow() diff --git a/openpype/tools/creator/widgets.py b/openpype/tools/creator/widgets.py new file mode 100644 index 0000000000..dc62cac9ab --- /dev/null +++ b/openpype/tools/creator/widgets.py @@ -0,0 +1,79 @@ +from ...vendor.Qt import QtWidgets, QtCore +from ... import style + + +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 + ) + + self.setStyleSheet(style.load_stylesheet()) + + 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 _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 From 8196c4463a114d043cd7e2afbdc34903b3919013 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 12 Nov 2021 20:03:48 +0100 Subject: [PATCH 02/18] renamed app.py to window.py --- openpype/tools/creator/__init__.py | 2 +- openpype/tools/creator/{app.py => window.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename openpype/tools/creator/{app.py => window.py} (100%) diff --git a/openpype/tools/creator/__init__.py b/openpype/tools/creator/__init__.py index 694caf15fe..eb7aad635a 100644 --- a/openpype/tools/creator/__init__.py +++ b/openpype/tools/creator/__init__.py @@ -1,4 +1,4 @@ -from .app import ( +from .window import ( show, ) diff --git a/openpype/tools/creator/app.py b/openpype/tools/creator/window.py similarity index 100% rename from openpype/tools/creator/app.py rename to openpype/tools/creator/window.py From 459c3d4cb5da5cd3b87d624d42a2f1ebe3a7ff62 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 12 Nov 2021 20:05:18 +0100 Subject: [PATCH 03/18] fixed imports --- openpype/tools/creator/widgets.py | 5 +---- openpype/tools/creator/window.py | 10 +++++----- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/openpype/tools/creator/widgets.py b/openpype/tools/creator/widgets.py index dc62cac9ab..7a370d8700 100644 --- a/openpype/tools/creator/widgets.py +++ b/openpype/tools/creator/widgets.py @@ -1,5 +1,4 @@ -from ...vendor.Qt import QtWidgets, QtCore -from ... import style +from Qt import QtWidgets, QtCore class CreateErrorMessageBox(QtWidgets.QDialog): @@ -19,8 +18,6 @@ class CreateErrorMessageBox(QtWidgets.QDialog): self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint ) - self.setStyleSheet(style.load_stylesheet()) - body_layout = QtWidgets.QVBoxLayout(self) main_label = ( diff --git a/openpype/tools/creator/window.py b/openpype/tools/creator/window.py index 83a43556e2..501cb66bb8 100644 --- a/openpype/tools/creator/window.py +++ b/openpype/tools/creator/window.py @@ -3,13 +3,13 @@ import inspect import traceback import re -from ...vendor.Qt import QtWidgets, QtCore, QtGui -from ...vendor import qtawesome -from ...vendor import six -from ... import api, io, style +from Qt import QtWidgets, QtCore, QtGui +from avalon.vendor import qtawesome +import six +from avalon import api, io, style from .widgets import CreateErrorMessageBox -from .. import lib +from avalon.tools import lib from openpype.api import get_current_project_settings module = sys.modules[__name__] From 96acb06ffd7b5d209cfae6a8a54a087916fe114a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 12 Nov 2021 20:22:24 +0100 Subject: [PATCH 04/18] separated parts from window to widgets and constants --- openpype/tools/creator/constants.py | 11 +++ openpype/tools/creator/widgets.py | 94 ++++++++++++++++++- openpype/tools/creator/window.py | 134 ++++------------------------ 3 files changed, 122 insertions(+), 117 deletions(-) create mode 100644 openpype/tools/creator/constants.py diff --git a/openpype/tools/creator/constants.py b/openpype/tools/creator/constants.py new file mode 100644 index 0000000000..9096224ac1 --- /dev/null +++ b/openpype/tools/creator/constants.py @@ -0,0 +1,11 @@ +from Qt import QtCore + + +FamilyRole = QtCore.Qt.UserRole + 3 +ExistsRole = QtCore.Qt.UserRole + 4 +PluginRole = QtCore.Qt.UserRole + 5 + +Separator = "---separator---" + +# TODO regex should be defined by schema +SubsetAllowedSymbols = "a-zA-Z0-9_." diff --git a/openpype/tools/creator/widgets.py b/openpype/tools/creator/widgets.py index 7a370d8700..f9a104e237 100644 --- a/openpype/tools/creator/widgets.py +++ b/openpype/tools/creator/widgets.py @@ -1,4 +1,7 @@ -from Qt import QtWidgets, QtCore +import re +from Qt import QtWidgets, QtCore, QtGui + +from .constants import SubsetAllowedSymbols class CreateErrorMessageBox(QtWidgets.QDialog): @@ -74,3 +77,92 @@ class CreateErrorMessageBox(QtWidgets.QDialog): line.setFrameShape(QtWidgets.QFrame.HLine) line.setFrameShadow(QtWidgets.QFrame.Sunken) return line + + +class SubsetNameValidator(QtGui.QRegExpValidator): + invalid = QtCore.Signal(set) + pattern = "^[{}]*$".format(SubsetAllowedSymbols) + + def __init__(self): + reg = QtCore.QRegExp(self.pattern) + super(SubsetNameValidator, self).__init__(reg) + + def validate(self, input, pos): + results = super(SubsetNameValidator, self).validate(input, pos) + if results[0] == self.Invalid: + self.invalid.emit(self.invalid_chars(input)) + return results + + def invalid_chars(self, input): + invalid = set() + re_valid = re.compile(self.pattern) + for char in input: + if char == " ": + invalid.add("' '") + continue + if not re_valid.match(char): + invalid.add(char) + return invalid + + +class SubsetNameLineEdit(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(SubsetNameLineEdit, 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 + ) diff --git a/openpype/tools/creator/window.py b/openpype/tools/creator/window.py index 501cb66bb8..8ce0dbff54 100644 --- a/openpype/tools/creator/window.py +++ b/openpype/tools/creator/window.py @@ -4,126 +4,34 @@ import traceback import re from Qt import QtWidgets, QtCore, QtGui -from avalon.vendor import qtawesome -import six -from avalon import api, io, style -from .widgets import CreateErrorMessageBox +from avalon.vendor import qtawesome +from avalon import api, io, style from avalon.tools import lib + from openpype.api import get_current_project_settings +from .widgets import ( + CreateErrorMessageBox, + SubsetNameLineEdit +) +from .constants import ( + FamilyRole, + ExistsRole, + PluginRole, + SubsetAllowedSymbols, + Separator +) + module = sys.modules[__name__] module.window = None -module.root = api.registered_root() - -HelpRole = QtCore.Qt.UserRole + 2 -FamilyRole = QtCore.Qt.UserRole + 3 -ExistsRole = QtCore.Qt.UserRole + 4 -PluginRole = QtCore.Qt.UserRole + 5 - -Separator = "---separator---" - -# TODO regex should be defined by schema -SubsetAllowedSymbols = "a-zA-Z0-9_." -class SubsetNameValidator(QtGui.QRegExpValidator): - - invalid = QtCore.Signal(set) - pattern = "^[{}]*$".format(SubsetAllowedSymbols) - - def __init__(self): - reg = QtCore.QRegExp(self.pattern) - super(SubsetNameValidator, self).__init__(reg) - - def validate(self, input, pos): - results = super(SubsetNameValidator, self).validate(input, pos) - if results[0] == self.Invalid: - self.invalid.emit(self.invalid_chars(input)) - return results - - def invalid_chars(self, input): - invalid = set() - re_valid = re.compile(self.pattern) - for char in input: - if char == " ": - invalid.add("' '") - continue - if not re_valid.match(char): - invalid.add(char) - return invalid - - -class SubsetNameLineEdit(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(SubsetNameLineEdit, 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 Window(QtWidgets.QDialog): - +class CreatorWindow(QtWidgets.QDialog): stateChanged = QtCore.Signal(bool) def __init__(self, parent=None): - super(Window, self).__init__(parent) + super(CreatorWindow, self).__init__(parent) self.setWindowTitle("Instance Creator") self.setFocusPolicy(QtCore.Qt.StrongFocus) if not parent: @@ -131,9 +39,6 @@ class Window(QtWidgets.QDialog): self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint ) - # Store the widgets for lookup in here - self.data = dict() - # Store internal states in here self.state = { "valid": False @@ -459,7 +364,6 @@ class Window(QtWidgets.QDialog): """ def refresh(self): - listing = self.data["Listing"] asset = self.data["Asset"] asset.setText(api.Session["AVALON_ASSET"]) @@ -474,7 +378,6 @@ class Window(QtWidgets.QDialog): label = creator.label or creator.family item = QtWidgets.QListWidgetItem(label) item.setData(QtCore.Qt.ItemIsEnabled, True) - item.setData(HelpRole, creator.__doc__) item.setData(FamilyRole, creator.family) item.setData(PluginRole, creator) item.setData(ExistsRole, False) @@ -708,8 +611,7 @@ def show(debug=False, parent=None): module.project = any_project["name"] with lib.application(): - window = Window(parent) - window.setStyleSheet(style.load_stylesheet()) + window = CreatorWindow(parent) window.refresh() window.show() From 04b5c039b07483d48d13a8da1663ab41d5a868c3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 12 Nov 2021 20:35:46 +0100 Subject: [PATCH 05/18] replaced input variable name with text --- openpype/tools/creator/widgets.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/tools/creator/widgets.py b/openpype/tools/creator/widgets.py index f9a104e237..eb42f42a50 100644 --- a/openpype/tools/creator/widgets.py +++ b/openpype/tools/creator/widgets.py @@ -87,16 +87,16 @@ class SubsetNameValidator(QtGui.QRegExpValidator): reg = QtCore.QRegExp(self.pattern) super(SubsetNameValidator, self).__init__(reg) - def validate(self, input, pos): - results = super(SubsetNameValidator, self).validate(input, pos) + def validate(self, text, pos): + results = super(SubsetNameValidator, self).validate(text, pos) if results[0] == self.Invalid: - self.invalid.emit(self.invalid_chars(input)) + self.invalid.emit(self.invalid_chars(text)) return results - def invalid_chars(self, input): + def invalid_chars(self, text): invalid = set() re_valid = re.compile(self.pattern) - for char in input: + for char in text: if char == " ": invalid.add("' '") continue From 998434cd12872863e346f4a79d3db420fbd2eab3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 12 Nov 2021 21:19:52 +0100 Subject: [PATCH 06/18] small modifications in ui initialization --- openpype/tools/creator/__init__.py | 6 +- openpype/tools/creator/constants.py | 3 +- openpype/tools/creator/widgets.py | 109 ++++++++- openpype/tools/creator/window.py | 337 ++++++++++------------------ 4 files changed, 227 insertions(+), 228 deletions(-) diff --git a/openpype/tools/creator/__init__.py b/openpype/tools/creator/__init__.py index eb7aad635a..585b8bdf80 100644 --- a/openpype/tools/creator/__init__.py +++ b/openpype/tools/creator/__init__.py @@ -1,7 +1,9 @@ from .window import ( show, + CreatorWindow ) -__all__ = [ +__all__ = ( "show", -] + "CreatorWindow" +) diff --git a/openpype/tools/creator/constants.py b/openpype/tools/creator/constants.py index 9096224ac1..36d98284ec 100644 --- a/openpype/tools/creator/constants.py +++ b/openpype/tools/creator/constants.py @@ -5,7 +5,8 @@ FamilyRole = QtCore.Qt.UserRole + 3 ExistsRole = QtCore.Qt.UserRole + 4 PluginRole = QtCore.Qt.UserRole + 5 -Separator = "---separator---" +SEPARATOR = "---" +SEPARATORS = {"---", "---separator---"} # TODO regex should be defined by schema SubsetAllowedSymbols = "a-zA-Z0-9_." diff --git a/openpype/tools/creator/widgets.py b/openpype/tools/creator/widgets.py index eb42f42a50..450e001b00 100644 --- a/openpype/tools/creator/widgets.py +++ b/openpype/tools/creator/widgets.py @@ -1,7 +1,15 @@ import re +import inspect + from Qt import QtWidgets, QtCore, QtGui -from .constants import SubsetAllowedSymbols +from avalon.vendor import qtawesome + +from .constants import ( + PluginRole, + FamilyRole, + SubsetAllowedSymbols +) class CreateErrorMessageBox(QtWidgets.QDialog): @@ -105,7 +113,7 @@ class SubsetNameValidator(QtGui.QRegExpValidator): return invalid -class SubsetNameLineEdit(QtWidgets.QLineEdit): +class VariantLineEdit(QtWidgets.QLineEdit): report = QtCore.Signal(str) colors = { "empty": (QtGui.QColor("#78879b"), ""), @@ -114,7 +122,7 @@ class SubsetNameLineEdit(QtWidgets.QLineEdit): } def __init__(self, *args, **kwargs): - super(SubsetNameLineEdit, self).__init__(*args, **kwargs) + super(VariantLineEdit, self).__init__(*args, **kwargs) validator = SubsetNameValidator() self.setValidator(validator) @@ -166,3 +174,98 @@ class SubsetNameLineEdit(QtWidgets.QLineEdit): 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) + + # Header font + font = QtGui.QFont() + font.setBold(True) + font.setPointSize(14) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + + icon = QtWidgets.QLabel() + icon.setSizePolicy(QtWidgets.QSizePolicy.Maximum, + QtWidgets.QSizePolicy.Maximum) + + # Add 4 pixel padding to avoid icon being cut off + icon.setFixedWidth(self.SIZE + 4) + icon.setFixedHeight(self.SIZE + 4) + icon.setStyleSheet(""" + QLabel { + padding-right: 5px; + } + """) + + label_layout = QtWidgets.QVBoxLayout() + label_layout.setSpacing(0) + + family = QtWidgets.QLabel("family") + family.setFont(font) + family.setAlignment(QtCore.Qt.AlignBottom | QtCore.Qt.AlignLeft) + + help = QtWidgets.QLabel("help") + help.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft) + + label_layout.addWidget(family) + label_layout.addWidget(help) + + layout.addWidget(icon) + layout.addLayout(label_layout) + + self.help = help + self.family = family + self.icon = icon + + def set_item(self, item): + """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 item: + return + + # Support a font-awesome icon + plugin = item.data(PluginRole) + icon_name = getattr(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(plugin) + help = docstring.splitlines()[0] if docstring else "" + + self.icon.setPixmap(pixmap) + self.family.setText(item.data(FamilyRole)) + self.help.setText(help) diff --git a/openpype/tools/creator/window.py b/openpype/tools/creator/window.py index 8ce0dbff54..c889b2e657 100644 --- a/openpype/tools/creator/window.py +++ b/openpype/tools/creator/window.py @@ -1,26 +1,27 @@ import sys -import inspect import traceback import re from Qt import QtWidgets, QtCore, QtGui -from avalon.vendor import qtawesome -from avalon import api, io, style +from avalon import api, io from avalon.tools import lib +from openpype import style from openpype.api import get_current_project_settings from .widgets import ( CreateErrorMessageBox, - SubsetNameLineEdit + VariantLineEdit, + FamilyDescriptionWidget ) from .constants import ( FamilyRole, ExistsRole, PluginRole, SubsetAllowedSymbols, - Separator + SEPARATOR, + SEPARATORS ) module = sys.modules[__name__] @@ -39,26 +40,16 @@ class CreatorWindow(QtWidgets.QDialog): self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint ) - # Store internal states in here - self.state = { - "valid": False - } - # Message dialog when something goes wrong during creation - self.message_dialog = None + creator_info = FamilyDescriptionWidget(parent=self) - body = QtWidgets.QWidget() - lists = QtWidgets.QWidget() - footer = QtWidgets.QWidget() - - container = QtWidgets.QWidget() - - listing = QtWidgets.QListWidget() + listing = QtWidgets.QListWidget(self) listing.setSortingEnabled(True) - asset = QtWidgets.QLineEdit() - name = SubsetNameLineEdit() - result = QtWidgets.QLineEdit() - result.setStyleSheet("color: gray;") - result.setEnabled(False) + + asset_name_input = QtWidgets.QLineEdit(self) + variant_input = VariantLineEdit(self) + subset_name_input = QtWidgets.QLineEdit(self) + subset_name_input.setStyleSheet("color: gray;") + subset_name_input.setEnabled(False) # region Menu for default subset names @@ -70,91 +61,92 @@ class CreatorWindow(QtWidgets.QDialog): # endregion name_layout = QtWidgets.QHBoxLayout() - name_layout.addWidget(name) + name_layout.addWidget(variant_input) name_layout.addWidget(subset_button) name_layout.setSpacing(3) name_layout.setContentsMargins(0, 0, 0, 0) - layout = QtWidgets.QVBoxLayout(container) + body_layout = QtWidgets.QVBoxLayout() + body_layout.setContentsMargins(0, 0, 0, 0) - header = FamilyDescriptionWidget(parent=self) - layout.addWidget(header) + body_layout.addWidget(creator_info, 0) + body_layout.addWidget(QtWidgets.QLabel("Family", self), 0) + body_layout.addWidget(listing, 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) - layout.addWidget(QtWidgets.QLabel("Family")) - layout.addWidget(listing) - layout.addWidget(QtWidgets.QLabel("Asset")) - layout.addWidget(asset) - layout.addWidget(QtWidgets.QLabel("Subset")) - layout.addLayout(name_layout) - layout.addWidget(result) - layout.setContentsMargins(0, 0, 0, 0) - - options = QtWidgets.QWidget() - - useselection_chk = QtWidgets.QCheckBox("Use selection") + useselection_chk = QtWidgets.QCheckBox("Use selection", self) useselection_chk.setCheckState(QtCore.Qt.Checked) - layout = QtWidgets.QGridLayout(options) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(useselection_chk, 1, 1) + create_btn = QtWidgets.QPushButton("Create", self) + # Need to store error_msg to prevent garbage collection + msg_label = QtWidgets.QLabel(self) - layout = QtWidgets.QHBoxLayout(lists) - layout.addWidget(container) - layout.setContentsMargins(0, 0, 0, 0) - - layout = QtWidgets.QVBoxLayout(body) - layout.addWidget(lists) - layout.addWidget(options, 0, QtCore.Qt.AlignLeft) - layout.setContentsMargins(0, 0, 0, 0) - - create_btn = QtWidgets.QPushButton("Create") - # Need to store error_msg to prevent garbage collection. - self.error_msg = QtWidgets.QLabel() - self.error_msg.setFixedHeight(20) - - layout = QtWidgets.QVBoxLayout(footer) - layout.addWidget(create_btn) - layout.addWidget(self.error_msg) - layout.setContentsMargins(0, 0, 0, 0) + 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.addWidget(body) - layout.addWidget(footer) + layout.addLayout(body_layout, 1) + layout.addWidget(useselection_chk, 0, QtCore.Qt.AlignLeft) + layout.addLayout(footer_layout, 0) - self.data = { - "Create Button": create_btn, - "Listing": listing, - "Use Selection Checkbox": useselection_chk, - "Subset": name, - "Subset Menu": subset_menu, - "Result": result, - "Asset": asset, - "Error Message": self.error_msg, - } - - for _name, widget in self.data.items(): - widget.setObjectName(_name) + msg_timer = QtCore.QTimer() + msg_timer.setSingleShot(True) + msg_timer.setInterval(5000) + msg_timer.timeout.connect(self._on_msg_timer) create_btn.clicked.connect(self.on_create) - name.returnPressed.connect(self.on_create) - name.textChanged.connect(self.on_data_changed) - name.report.connect(self.echo) - asset.textChanged.connect(self.on_data_changed) - listing.currentItemChanged.connect(self.on_selection_changed) - listing.currentItemChanged.connect(header.set_item) + 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) + listing.currentItemChanged.connect(self._on_creator_change) self.stateChanged.connect(self._on_state_changed) + # Store internal states in here + self.state = { + "valid": False + } + 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_view = listing + + self._subset_btn = subset_button + self._subset_menu = subset_menu + + self._msg_label = msg_label + + self._msg_timer = msg_timer + # Defaults self.resize(300, 500) - name.setFocus() + variant_input.setFocus() create_btn.setEnabled(False) def _on_state_changed(self, state): self.state["valid"] = state - self.data["Create Button"].setEnabled(state) + self._create_btn.setEnabled(state) - def _build_menu(self, default_names): + def _on_creator_change(self, index): + self.on_selection_changed(index) + self._creator_info.set_item(index) + + def _build_menu(self, default_names=None): """Create optional predefined subset names Args: @@ -163,9 +155,11 @@ class CreatorWindow(QtWidgets.QDialog): Returns: None """ + if not default_names: + default_names = [] - menu = self.data["Subset Menu"] - button = menu.parent() + menu = self._subset_menu + button = self._subset_btn # Get and destroy the action group group = button.findChild(QtWidgets.QActionGroup) @@ -180,7 +174,7 @@ class CreatorWindow(QtWidgets.QDialog): # Build new action group group = QtWidgets.QActionGroup(button) for name in default_names: - if name == Separator: + if name in SEPARATORS: menu.addSeparator() continue action = group.addAction(name) @@ -189,22 +183,16 @@ class CreatorWindow(QtWidgets.QDialog): group.triggered.connect(self._on_action_clicked) def _on_action_clicked(self, action): - name = self.data["Subset"] - name.setText(action.text()) + self._variant_input.setText(action.text()) def _on_data_changed(self): - listing = self.data["Listing"] - asset_name = self.data["Asset"] - subset = self.data["Subset"] - result = self.data["Result"] - - item = listing.currentItem() - user_input_text = subset.text() - asset_name = asset_name.text() + item = self._creators_view.currentItem() + 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._build_menu() item.setData(ExistsRole, False) self.echo("Asset name is required ..") self.stateChanged.emit(False) @@ -249,7 +237,7 @@ class CreatorWindow(QtWidgets.QDialog): .replace(curly_left, "{") .replace(curly_right, "}") ) - result.setText(subset_name) + self._subset_name_input.setText(subset_name) # Get all subsets of the current asset subset_docs = io.find( @@ -287,20 +275,20 @@ class CreatorWindow(QtWidgets.QDialog): if subset_hints: if defaults: - defaults.append(Separator) + defaults.append(SEPARATOR) defaults.extend(subset_hints) self._build_menu(defaults) # Indicate subset existence if not user_input_text: - subset.as_empty() + 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 - subset.as_exists() + self._variant_input.as_exists() else: - subset.as_new() + self._variant_input.as_new() item.setData(ExistsRole, True) @@ -332,8 +320,7 @@ class CreatorWindow(QtWidgets.QDialog): lib.schedule(self._on_data_changed, 500, channel="gui") def on_selection_changed(self, *args): - name = self.data["Subset"] - item = self.data["Listing"].currentItem() + item = self._creators_view.currentItem() plugin = item.data(PluginRole) if plugin is None: @@ -349,7 +336,7 @@ class CreatorWindow(QtWidgets.QDialog): else: default = "Default" - name.setText(default) + self._variant_input.setText(default) self.on_data_changed() @@ -362,11 +349,17 @@ class CreatorWindow(QtWidgets.QDialog): 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): - listing = self.data["Listing"] - asset = self.data["Asset"] - asset.setText(api.Session["AVALON_ASSET"]) + listing = self._creators_view + self._asset_name_input.setText(api.Session["AVALON_ASSET"]) listing.clear() @@ -417,33 +410,28 @@ class CreatorWindow(QtWidgets.QDialog): listing.setCurrentItem(listing.item(0)) def on_create(self): - # Do not allow creation in an invalid state if not self.state["valid"]: return - asset = self.data["Asset"] - listing = self.data["Listing"] - result = self.data["Result"] - - item = listing.currentItem() + item = self._creators_view.currentItem() if item is None: return - subset_name = result.text() - asset = asset.text() + subset_name = self._subset_name_input.text() + asset_name = self._asset_name_input.text() family = item.data(FamilyRole) Creator = item.data(PluginRole) - use_selection = self.data["Use Selection Checkbox"].isChecked() + use_selection = self._useselection_chk.isChecked() - variant = self.data["Subset"].text() + variant = self._variant_input.text() error_info = None try: api.create( Creator, subset_name, - asset, + asset_name, options={"useSelection": use_selection}, data={"variant": variant} ) @@ -463,116 +451,21 @@ class CreatorWindow(QtWidgets.QDialog): if error_info: box = CreateErrorMessageBox( - family, subset_name, asset, *error_info + family, subset_name, asset_name, *error_info ) box.show() # Store dialog so is not garbage collected before is shown - self.message_dialog = box + self._message_dialog = box else: self.echo("Created %s .." % subset_name) + def _on_msg_timer(self): + self._msg_label.setText("") + def echo(self, message): - widget = self.data["Error Message"] - widget.setText(str(message)) - widget.show() - - lib.schedule(lambda: widget.setText(""), 5000, channel="message") - - -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) - - # Header font - font = QtGui.QFont() - font.setBold(True) - font.setPointSize(14) - - layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - - icon = QtWidgets.QLabel() - icon.setSizePolicy(QtWidgets.QSizePolicy.Maximum, - QtWidgets.QSizePolicy.Maximum) - - # Add 4 pixel padding to avoid icon being cut off - icon.setFixedWidth(self.SIZE + 4) - icon.setFixedHeight(self.SIZE + 4) - icon.setStyleSheet(""" - QLabel { - padding-right: 5px; - } - """) - - label_layout = QtWidgets.QVBoxLayout() - label_layout.setSpacing(0) - - family = QtWidgets.QLabel("family") - family.setFont(font) - family.setAlignment(QtCore.Qt.AlignBottom | QtCore.Qt.AlignLeft) - - help = QtWidgets.QLabel("help") - help.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft) - - label_layout.addWidget(family) - label_layout.addWidget(help) - - layout.addWidget(icon) - layout.addLayout(label_layout) - - self.help = help - self.family = family - self.icon = icon - - def set_item(self, item): - """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 item: - return - - # Support a font-awesome icon - plugin = item.data(PluginRole) - icon_name = getattr(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(plugin) - help = docstring.splitlines()[0] if docstring else "" - - self.icon.setPixmap(pixmap) - self.family.setText(item.data(FamilyRole)) - self.help.setText(help) + self._msg_label.setText(str(message)) + self._msg_timer.start() def show(debug=False, parent=None): From 26408c22c2d2c5ff23c610fe4a30253ff74fc81e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 12 Nov 2021 21:20:01 +0100 Subject: [PATCH 07/18] changed usage of creator --- openpype/tools/utils/host_tools.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index a15d12b386..9f2adfdfef 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -109,23 +109,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() From 100eaa6de7585138188c4ca3a46a21d2c7569fcc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 12 Nov 2021 21:36:32 +0100 Subject: [PATCH 08/18] simplified few thinks --- openpype/tools/creator/window.py | 246 ++++++++++++++++--------------- 1 file changed, 125 insertions(+), 121 deletions(-) diff --git a/openpype/tools/creator/window.py b/openpype/tools/creator/window.py index c889b2e657..c17fbba0b9 100644 --- a/openpype/tools/creator/window.py +++ b/openpype/tools/creator/window.py @@ -5,10 +5,10 @@ import re from Qt import QtWidgets, QtCore, QtGui from avalon import api, io -from avalon.tools import lib from openpype import style from openpype.api import get_current_project_settings +from openpype.tools.utils.lib import qt_app_context from .widgets import ( CreateErrorMessageBox, @@ -29,8 +29,6 @@ module.window = None class CreatorWindow(QtWidgets.QDialog): - stateChanged = QtCore.Signal(bool) - def __init__(self, parent=None): super(CreatorWindow, self).__init__(parent) self.setWindowTitle("Instance Creator") @@ -99,20 +97,24 @@ class CreatorWindow(QtWidgets.QDialog): 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) - create_btn.clicked.connect(self.on_create) - variant_input.returnPressed.connect(self.on_create) - variant_input.textChanged.connect(self.on_data_changed) + 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) + asset_name_input.textChanged.connect(self._on_data_changed) listing.currentItemChanged.connect(self._on_creator_change) - self.stateChanged.connect(self._on_state_changed) + # Store valid states and + self._is_valid = False + create_btn.setEnabled(self._is_valid) - # Store internal states in here - self.state = { - "valid": False - } self._first_show = True # Message dialog when something goes wrong during creation @@ -131,16 +133,18 @@ class CreatorWindow(QtWidgets.QDialog): self._msg_label = msg_label + self._validation_timer = validation_timer self._msg_timer = msg_timer # Defaults self.resize(300, 500) variant_input.setFocus() - create_btn.setEnabled(False) - def _on_state_changed(self, state): - self.state["valid"] = state - self._create_btn.setEnabled(state) + def _set_valid_state(self, valid): + if self._is_valid == valid: + return + self._is_valid = valid + self._create_btn.setEnabled(valid) def _on_creator_change(self, index): self.on_selection_changed(index) @@ -185,7 +189,15 @@ class CreatorWindow(QtWidgets.QDialog): def _on_action_clicked(self, action): self._variant_input.setText(action.text()) - def _on_data_changed(self): + 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): item = self._creators_view.currentItem() user_input_text = self._variant_input.text() asset_name = self._asset_name_input.text() @@ -195,7 +207,7 @@ class CreatorWindow(QtWidgets.QDialog): self._build_menu() item.setData(ExistsRole, False) self.echo("Asset name is required ..") - self.stateChanged.emit(False) + self._set_valid_state(False) return # Get the asset from the database which match with the name @@ -205,102 +217,103 @@ class CreatorWindow(QtWidgets.QDialog): ) # Get plugin plugin = item.data(PluginRole) - if asset_doc and plugin: - 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 = 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(SubsetAllowedSymbols), - "", - 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 ( - plugin.defaults - and isinstance(plugin.defaults, (list, tuple, set)) - ): - defaults = list(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() - - item.setData(ExistsRole, True) - - else: + if not asset_doc or not plugin: subset_name = user_input_text - self._build_menu([]) + self._build_menu() item.setData(ExistsRole, False) if not 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 = 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(SubsetAllowedSymbols), + "", + 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 ( + plugin.defaults + and isinstance(plugin.defaults, (list, tuple, set)) + ): + defaults = list(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() + + item.setData(ExistsRole, True) # Update the valid state valid = ( @@ -308,16 +321,7 @@ class CreatorWindow(QtWidgets.QDialog): item.data(QtCore.Qt.ItemIsEnabled) and item.data(ExistsRole) ) - self.stateChanged.emit(valid) - - 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.stateChanged.emit(False) - - lib.schedule(self._on_data_changed, 500, channel="gui") + self._set_valid_state(valid) def on_selection_changed(self, *args): item = self._creators_view.currentItem() @@ -338,7 +342,7 @@ class CreatorWindow(QtWidgets.QDialog): self._variant_input.setText(default) - self.on_data_changed() + self._on_data_changed() def keyPressEvent(self, event): """Custom keyPressEvent. @@ -409,9 +413,9 @@ class CreatorWindow(QtWidgets.QDialog): if not item: listing.setCurrentItem(listing.item(0)) - def on_create(self): + def _on_create(self): # Do not allow creation in an invalid state - if not self.state["valid"]: + if not self._is_valid: return item = self._creators_view.currentItem() @@ -503,7 +507,7 @@ def show(debug=False, parent=None): api.Session["AVALON_PROJECT"] = any_project["name"] module.project = any_project["name"] - with lib.application(): + with qt_app_context(): window = CreatorWindow(parent) window.refresh() window.show() From cb4b01480014c3f06f5ca9daa460494b4b6e6cd1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 12 Nov 2021 22:31:13 +0100 Subject: [PATCH 09/18] moved SUBSET_NAME_ALLOWED_SYMBOLS to pipeline/create/constants --- openpype/pipeline/create/__init__.py | 5 +++++ openpype/pipeline/create/constants.py | 6 ++++++ openpype/tools/publisher/constants.py | 2 -- openpype/tools/publisher/widgets/create_dialog.py | 6 ++++-- openpype/tools/publisher/widgets/widgets.py | 3 +-- 5 files changed, 16 insertions(+), 6 deletions(-) create mode 100644 openpype/pipeline/create/constants.py 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/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 ) From 5e3434a321aa3153c53e64b3ba1b789ae1bdd746 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 12 Nov 2021 22:37:45 +0100 Subject: [PATCH 10/18] added cretors model --- openpype/tools/creator/constants.py | 5 +- openpype/tools/creator/model.py | 57 +++++++++ openpype/tools/creator/widgets.py | 22 ++-- openpype/tools/creator/window.py | 173 +++++++++++++--------------- 4 files changed, 151 insertions(+), 106 deletions(-) create mode 100644 openpype/tools/creator/model.py diff --git a/openpype/tools/creator/constants.py b/openpype/tools/creator/constants.py index 36d98284ec..2a0fe9f8f4 100644 --- a/openpype/tools/creator/constants.py +++ b/openpype/tools/creator/constants.py @@ -1,9 +1,8 @@ from Qt import QtCore -FamilyRole = QtCore.Qt.UserRole + 3 -ExistsRole = QtCore.Qt.UserRole + 4 -PluginRole = QtCore.Qt.UserRole + 5 +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..e68150c09e --- /dev/null +++ b/openpype/tools/creator/model.py @@ -0,0 +1,57 @@ +import uuid +from Qt import QtGui, QtCore + +from avalon import api + +from openpype.api import get_current_project_settings + +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 index 450e001b00..a8b34580f2 100644 --- a/openpype/tools/creator/widgets.py +++ b/openpype/tools/creator/widgets.py @@ -5,11 +5,7 @@ from Qt import QtWidgets, QtCore, QtGui from avalon.vendor import qtawesome -from .constants import ( - PluginRole, - FamilyRole, - SubsetAllowedSymbols -) +from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS class CreateErrorMessageBox(QtWidgets.QDialog): @@ -89,7 +85,7 @@ class CreateErrorMessageBox(QtWidgets.QDialog): class SubsetNameValidator(QtGui.QRegExpValidator): invalid = QtCore.Signal(set) - pattern = "^[{}]*$".format(SubsetAllowedSymbols) + pattern = "^[{}]*$".format(SUBSET_NAME_ALLOWED_SYMBOLS) def __init__(self): reg = QtCore.QRegExp(self.pattern) @@ -236,7 +232,7 @@ class FamilyDescriptionWidget(QtWidgets.QWidget): self.family = family self.icon = icon - def set_item(self, item): + def set_item(self, creator_plugin): """Update elements to display information of a family item. Args: @@ -246,12 +242,14 @@ class FamilyDescriptionWidget(QtWidgets.QWidget): None """ - if not item: + if not creator_plugin: + self.icon.setPixmap(None) + self.family.setText("") + self.help.setText("") return # Support a font-awesome icon - plugin = item.data(PluginRole) - icon_name = getattr(plugin, "icon", None) or "info-circle" + 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) @@ -263,9 +261,9 @@ class FamilyDescriptionWidget(QtWidgets.QWidget): pixmap = pixmap.scaled(self.SIZE, self.SIZE) # Parse a clean line from the Creator's docstring - docstring = inspect.getdoc(plugin) + docstring = inspect.getdoc(creator_plugin) help = docstring.splitlines()[0] if docstring else "" self.icon.setPixmap(pixmap) - self.family.setText(item.data(FamilyRole)) + self.family.setText(creator_plugin.family) self.help.setText(help) diff --git a/openpype/tools/creator/window.py b/openpype/tools/creator/window.py index c17fbba0b9..ca7e1dd1f0 100644 --- a/openpype/tools/creator/window.py +++ b/openpype/tools/creator/window.py @@ -9,17 +9,17 @@ 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 ( - FamilyRole, - ExistsRole, - PluginRole, - SubsetAllowedSymbols, + ITEM_ID_ROLE, + FAMILY_ROLE, SEPARATOR, SEPARATORS ) @@ -40,8 +40,13 @@ class CreatorWindow(QtWidgets.QDialog): creator_info = FamilyDescriptionWidget(parent=self) - listing = QtWidgets.QListWidget(self) - listing.setSortingEnabled(True) + creators_model = CreatorsModel() + + creators_proxy = QtCore.QSortFilterProxyModel() + creators_proxy.setSourceModel(creators_model) + + creators_view = QtWidgets.QListView(self) + creators_view.setModel(creators_proxy) asset_name_input = QtWidgets.QLineEdit(self) variant_input = VariantLineEdit(self) @@ -49,15 +54,11 @@ class CreatorWindow(QtWidgets.QDialog): subset_name_input.setStyleSheet("color: gray;") subset_name_input.setEnabled(False) - # region Menu for default subset names - subset_button = QtWidgets.QPushButton() subset_button.setFixedWidth(18) subset_menu = QtWidgets.QMenu(subset_button) subset_button.setMenu(subset_menu) - # endregion - name_layout = QtWidgets.QHBoxLayout() name_layout.addWidget(variant_input) name_layout.addWidget(subset_button) @@ -69,7 +70,7 @@ class CreatorWindow(QtWidgets.QDialog): body_layout.addWidget(creator_info, 0) body_layout.addWidget(QtWidgets.QLabel("Family", self), 0) - body_layout.addWidget(listing, 1) + 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) @@ -109,7 +110,9 @@ class CreatorWindow(QtWidgets.QDialog): variant_input.textChanged.connect(self._on_data_changed) variant_input.report.connect(self.echo) asset_name_input.textChanged.connect(self._on_data_changed) - listing.currentItemChanged.connect(self._on_creator_change) + creators_view.selectionModel().currentChanged.connect( + self._on_selection_changed + ) # Store valid states and self._is_valid = False @@ -126,7 +129,10 @@ class CreatorWindow(QtWidgets.QDialog): self._variant_input = variant_input self._subset_name_input = subset_name_input self._asset_name_input = asset_name_input - self._creators_view = listing + + 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 @@ -146,10 +152,6 @@ class CreatorWindow(QtWidgets.QDialog): self._is_valid = valid self._create_btn.setEnabled(valid) - def _on_creator_change(self, index): - self.on_selection_changed(index) - self._creator_info.set_item(index) - def _build_menu(self, default_names=None): """Create optional predefined subset names @@ -198,31 +200,33 @@ class CreatorWindow(QtWidgets.QDialog): self._validation_timer.start() def _on_validation_timer(self): - item = self._creators_view.currentItem() + 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() - item.setData(ExistsRole, False) self.echo("Asset name is required ..") self._set_valid_state(False) return - # Get the asset from the database which match with the name - asset_doc = io.find_one( - {"name": asset_name, "type": "asset"}, - projection={"_id": 1} - ) + 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 - plugin = item.data(PluginRole) - if not asset_doc or not plugin: + if not asset_doc or not creator_plugin: subset_name = user_input_text self._build_menu() - item.setData(ExistsRole, False) - if not plugin: + if not creator_plugin: self.echo("No registered families ..") else: self.echo("Asset '%s' not found .." % asset_name) @@ -234,7 +238,7 @@ class CreatorWindow(QtWidgets.QDialog): task_name = io.Session["AVALON_TASK"] # Calculate subset name with Creator plugin - subset_name = plugin.get_subset_name( + subset_name = creator_plugin.get_subset_name( user_input_text, task_name, asset_id, project_name ) # Force replacement of prohibited symbols @@ -251,7 +255,7 @@ class CreatorWindow(QtWidgets.QDialog): ) # Replace prohibited symbols tmp_subset_name = re.sub( - "[^{}]+".format(SubsetAllowedSymbols), + "[^{}]+".format(SUBSET_NAME_ALLOWED_SYMBOLS), "", tmp_subset_name ) @@ -280,10 +284,10 @@ class CreatorWindow(QtWidgets.QDialog): defaults = [] # Check if Creator plugin has set defaults if ( - plugin.defaults - and isinstance(plugin.defaults, (list, tuple, set)) + creator_plugin.defaults + and isinstance(creator_plugin.defaults, (list, tuple, set)) ): - defaults = list(plugin.defaults) + defaults = list(creator_plugin.defaults) # Replace compare_regex = re.compile(re.sub( @@ -313,30 +317,32 @@ class CreatorWindow(QtWidgets.QDialog): else: self._variant_input.as_new() - item.setData(ExistsRole, True) - # Update the valid state - valid = ( - subset_name.strip() != "" and - item.data(QtCore.Qt.ItemIsEnabled) and - item.data(ExistsRole) - ) + valid = subset_name.strip() != "" + self._set_valid_state(valid) - def on_selection_changed(self, *args): - item = self._creators_view.currentItem() + def _on_selection_changed(self, old_idx, new_idx): + index = self._creators_view.currentIndex() + item_id = index.data(ITEM_ID_ROLE) - plugin = item.data(PluginRole) - if plugin is None: + 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(plugin, "get_default_variant"): - default = plugin.get_default_variant() + if hasattr(creator_plugin, "get_default_variant"): + default = creator_plugin.get_default_variant() if not default: - if plugin.defaults and isinstance(plugin.defaults, list): - default = plugin.defaults[0] + if ( + creator_plugin.defaults + and isinstance(creator_plugin.defaults, list) + ): + default = creator_plugin.defaults[0] else: default = "Default" @@ -362,30 +368,9 @@ class CreatorWindow(QtWidgets.QDialog): self.setStyleSheet(style.load_stylesheet()) def refresh(self): - listing = self._creators_view self._asset_name_input.setText(api.Session["AVALON_ASSET"]) - listing.clear() - - has_families = False - - creators = api.discover(api.Creator) - - for creator in creators: - label = creator.label or creator.family - item = QtWidgets.QListWidgetItem(label) - item.setData(QtCore.Qt.ItemIsEnabled, True) - item.setData(FamilyRole, creator.family) - item.setData(PluginRole, creator) - item.setData(ExistsRole, False) - listing.addItem(item) - - has_families = True - - if not has_families: - item = QtWidgets.QListWidgetItem("No registered families") - item.setData(QtCore.Qt.ItemIsEnabled, False) - listing.addItem(item) + self._creators_model.reset() pype_project_setting = ( get_current_project_settings() @@ -394,38 +379,44 @@ class CreatorWindow(QtWidgets.QDialog): ["creator"] ["families_smart_select"] ) - item = None - family_type = None - task_name = io.Session.get('AVALON_TASK', None) + current_index = None + family = None + task_name = io.Session.get("AVALON_TASK", None) + lowered_task_name = task_name.lower() if task_name: - for key, value in pype_project_setting.items(): - for t_name in value: - if t_name in task_name.lower(): - family_type = key + 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_type: + if family: break - if family_type: - items = listing.findItems(family_type, QtCore.Qt.MatchExactly) - if len(items) > 0: - item = items[0] - listing.setCurrentItem(item) - if not item: - listing.setCurrentItem(listing.item(0)) + + 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 - item = self._creators_view.currentItem() - if item is None: + 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() - family = item.data(FamilyRole) - Creator = item.data(PluginRole) use_selection = self._useselection_chk.isChecked() variant = self._variant_input.text() @@ -433,7 +424,7 @@ class CreatorWindow(QtWidgets.QDialog): error_info = None try: api.create( - Creator, + creator_plugin, subset_name, asset_name, options={"useSelection": use_selection}, @@ -455,7 +446,7 @@ class CreatorWindow(QtWidgets.QDialog): if error_info: box = CreateErrorMessageBox( - family, subset_name, asset_name, *error_info + creator_plugin.family, subset_name, asset_name, *error_info ) box.show() # Store dialog so is not garbage collected before is shown From 904704cf694f04326b17d1548156587c6ceb3a06 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Nov 2021 10:48:02 +0100 Subject: [PATCH 11/18] removed unused constant --- openpype/tools/creator/constants.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/openpype/tools/creator/constants.py b/openpype/tools/creator/constants.py index 2a0fe9f8f4..26a25dc010 100644 --- a/openpype/tools/creator/constants.py +++ b/openpype/tools/creator/constants.py @@ -6,6 +6,3 @@ ITEM_ID_ROLE = QtCore.Qt.UserRole + 2 SEPARATOR = "---" SEPARATORS = {"---", "---separator---"} - -# TODO regex should be defined by schema -SubsetAllowedSymbols = "a-zA-Z0-9_." From 16f37c3703ed1ac0c2848c77cb47d876d9f1da0e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Nov 2021 10:48:15 +0100 Subject: [PATCH 12/18] apply style on message box --- openpype/tools/creator/window.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/tools/creator/window.py b/openpype/tools/creator/window.py index ca7e1dd1f0..010cecbdc8 100644 --- a/openpype/tools/creator/window.py +++ b/openpype/tools/creator/window.py @@ -448,6 +448,7 @@ class CreatorWindow(QtWidgets.QDialog): box = CreateErrorMessageBox( creator_plugin.family, subset_name, asset_name, *error_info ) + box.setStyle(self.style()) box.show() # Store dialog so is not garbage collected before is shown self._message_dialog = box From 5b661fd2c750f13eb6955a4cd73d1f7fa6eed089 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Nov 2021 10:48:40 +0100 Subject: [PATCH 13/18] use dbcon session --- openpype/tools/creator/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/creator/window.py b/openpype/tools/creator/window.py index 010cecbdc8..ce9f7ef9dc 100644 --- a/openpype/tools/creator/window.py +++ b/openpype/tools/creator/window.py @@ -368,7 +368,7 @@ class CreatorWindow(QtWidgets.QDialog): self.setStyleSheet(style.load_stylesheet()) def refresh(self): - self._asset_name_input.setText(api.Session["AVALON_ASSET"]) + self._asset_name_input.setText(io.Session["AVALON_ASSET"]) self._creators_model.reset() From 5ebd398fd1c0527743bdda4a3a196bbf435ca115 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Nov 2021 10:50:31 +0100 Subject: [PATCH 14/18] small modification of FamilyDescriptionWidget --- openpype/tools/creator/widgets.py | 54 ++++++++++++++++--------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/openpype/tools/creator/widgets.py b/openpype/tools/creator/widgets.py index a8b34580f2..e1a30e9aba 100644 --- a/openpype/tools/creator/widgets.py +++ b/openpype/tools/creator/widgets.py @@ -196,17 +196,17 @@ class FamilyDescriptionWidget(QtWidgets.QWidget): font.setBold(True) font.setPointSize(14) - layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - - icon = QtWidgets.QLabel() - icon.setSizePolicy(QtWidgets.QSizePolicy.Maximum, - QtWidgets.QSizePolicy.Maximum) + icon_label = QtWidgets.QLabel(self) + icon_label.setObjectName("FamilyIconLabel") + icon_label.setSizePolicy( + QtWidgets.QSizePolicy.Maximum, + QtWidgets.QSizePolicy.Maximum + ) # Add 4 pixel padding to avoid icon being cut off - icon.setFixedWidth(self.SIZE + 4) - icon.setFixedHeight(self.SIZE + 4) - icon.setStyleSheet(""" + icon_label.setFixedWidth(self.SIZE + 4) + icon_label.setFixedHeight(self.SIZE + 4) + icon_label.setStyleSheet(""" QLabel { padding-right: 5px; } @@ -215,22 +215,24 @@ class FamilyDescriptionWidget(QtWidgets.QWidget): label_layout = QtWidgets.QVBoxLayout() label_layout.setSpacing(0) - family = QtWidgets.QLabel("family") - family.setFont(font) - family.setAlignment(QtCore.Qt.AlignBottom | QtCore.Qt.AlignLeft) + family_label = QtWidgets.QLabel(self) + family_label.setFont(font) + family_label.setAlignment(QtCore.Qt.AlignBottom | QtCore.Qt.AlignLeft) - help = QtWidgets.QLabel("help") - help.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft) + help_label = QtWidgets.QLabel(self) + help_label.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft) - label_layout.addWidget(family) + label_layout.addWidget(family_label) label_layout.addWidget(help) - layout.addWidget(icon) + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(icon_label) layout.addLayout(label_layout) - self.help = help - self.family = family - self.icon = icon + 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. @@ -243,9 +245,9 @@ class FamilyDescriptionWidget(QtWidgets.QWidget): """ if not creator_plugin: - self.icon.setPixmap(None) - self.family.setText("") - self.help.setText("") + self._icon_label.setPixmap(None) + self._family_label.setText("") + self._help_label.setText("") return # Support a font-awesome icon @@ -262,8 +264,8 @@ class FamilyDescriptionWidget(QtWidgets.QWidget): # Parse a clean line from the Creator's docstring docstring = inspect.getdoc(creator_plugin) - help = docstring.splitlines()[0] if docstring else "" + creator_help = docstring.splitlines()[0] if docstring else "" - self.icon.setPixmap(pixmap) - self.family.setText(creator_plugin.family) - self.help.setText(help) + self._icon_label.setPixmap(pixmap) + self._family_label.setText(creator_plugin.family) + self._help_label.setText(creator_help) From 41a46980c527795a95030d61740c4946f1c945b6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Nov 2021 11:18:33 +0100 Subject: [PATCH 15/18] moved style changes to openpype stylesheet --- openpype/style/style.css | 7 +++++++ openpype/tools/creator/widgets.py | 16 +++------------- openpype/tools/creator/window.py | 4 +--- 3 files changed, 11 insertions(+), 16 deletions(-) diff --git a/openpype/style/style.css b/openpype/style/style.css index 89458fd117..b253b5de41 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -765,12 +765,19 @@ 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; } +/* Creator */ +#CreatorFamilyLabel { + font-size: 10pt; + font-weight: bold; +} + /* Python console interpreter */ #PythonInterpreterOutput, #PythonCodeEditor { font-family: "Roboto Mono"; diff --git a/openpype/tools/creator/widgets.py b/openpype/tools/creator/widgets.py index e1a30e9aba..d1ac0111b7 100644 --- a/openpype/tools/creator/widgets.py +++ b/openpype/tools/creator/widgets.py @@ -191,13 +191,7 @@ class FamilyDescriptionWidget(QtWidgets.QWidget): def __init__(self, parent=None): super(FamilyDescriptionWidget, self).__init__(parent=parent) - # Header font - font = QtGui.QFont() - font.setBold(True) - font.setPointSize(14) - icon_label = QtWidgets.QLabel(self) - icon_label.setObjectName("FamilyIconLabel") icon_label.setSizePolicy( QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum @@ -206,27 +200,23 @@ class FamilyDescriptionWidget(QtWidgets.QWidget): # Add 4 pixel padding to avoid icon being cut off icon_label.setFixedWidth(self.SIZE + 4) icon_label.setFixedHeight(self.SIZE + 4) - icon_label.setStyleSheet(""" - QLabel { - padding-right: 5px; - } - """) label_layout = QtWidgets.QVBoxLayout() label_layout.setSpacing(0) family_label = QtWidgets.QLabel(self) - family_label.setFont(font) + 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.addWidget(help_label) layout = QtWidgets.QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(5) layout.addWidget(icon_label) layout.addLayout(label_layout) diff --git a/openpype/tools/creator/window.py b/openpype/tools/creator/window.py index ce9f7ef9dc..3bdebfe6d8 100644 --- a/openpype/tools/creator/window.py +++ b/openpype/tools/creator/window.py @@ -19,7 +19,6 @@ from .widgets import ( ) from .constants import ( ITEM_ID_ROLE, - FAMILY_ROLE, SEPARATOR, SEPARATORS ) @@ -38,7 +37,7 @@ class CreatorWindow(QtWidgets.QDialog): self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint ) - creator_info = FamilyDescriptionWidget(parent=self) + creator_info = FamilyDescriptionWidget(self) creators_model = CreatorsModel() @@ -51,7 +50,6 @@ class CreatorWindow(QtWidgets.QDialog): asset_name_input = QtWidgets.QLineEdit(self) variant_input = VariantLineEdit(self) subset_name_input = QtWidgets.QLineEdit(self) - subset_name_input.setStyleSheet("color: gray;") subset_name_input.setEnabled(False) subset_button = QtWidgets.QPushButton() From c787ec0b8fd9c7767c881328ddd8fac04e21c413 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Nov 2021 11:23:47 +0100 Subject: [PATCH 16/18] fixed setting of stylesheet for error message box --- openpype/tools/creator/widgets.py | 5 +++++ openpype/tools/creator/window.py | 1 - 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/tools/creator/widgets.py b/openpype/tools/creator/widgets.py index d1ac0111b7..89c90cc048 100644 --- a/openpype/tools/creator/widgets.py +++ b/openpype/tools/creator/widgets.py @@ -5,6 +5,7 @@ from Qt import QtWidgets, QtCore, QtGui from avalon.vendor import qtawesome +from openpype import style from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS @@ -72,6 +73,10 @@ class CreateErrorMessageBox(QtWidgets.QDialog): 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() diff --git a/openpype/tools/creator/window.py b/openpype/tools/creator/window.py index 3bdebfe6d8..faee78d1e5 100644 --- a/openpype/tools/creator/window.py +++ b/openpype/tools/creator/window.py @@ -446,7 +446,6 @@ class CreatorWindow(QtWidgets.QDialog): box = CreateErrorMessageBox( creator_plugin.family, subset_name, asset_name, *error_info ) - box.setStyle(self.style()) box.show() # Store dialog so is not garbage collected before is shown self._message_dialog = box From fab657c48ac4209f88e8cb0662bfaccb4529c98e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Nov 2021 11:35:29 +0100 Subject: [PATCH 17/18] removed unused imports --- openpype/tools/creator/model.py | 2 -- openpype/tools/creator/window.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/tools/creator/model.py b/openpype/tools/creator/model.py index e68150c09e..6907e8f0aa 100644 --- a/openpype/tools/creator/model.py +++ b/openpype/tools/creator/model.py @@ -3,8 +3,6 @@ from Qt import QtGui, QtCore from avalon import api -from openpype.api import get_current_project_settings - from . constants import ( FAMILY_ROLE, ITEM_ID_ROLE diff --git a/openpype/tools/creator/window.py b/openpype/tools/creator/window.py index faee78d1e5..6f590e887e 100644 --- a/openpype/tools/creator/window.py +++ b/openpype/tools/creator/window.py @@ -2,7 +2,7 @@ import sys import traceback import re -from Qt import QtWidgets, QtCore, QtGui +from Qt import QtWidgets, QtCore from avalon import api, io From 1893b9416f4ef902c1e66931d666a78bc4198b49 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 18 Nov 2021 10:50:40 +0100 Subject: [PATCH 18/18] added padding to creators view --- openpype/style/style.css | 4 ++++ openpype/tools/creator/window.py | 1 + 2 files changed, 5 insertions(+) diff --git a/openpype/style/style.css b/openpype/style/style.css index b253b5de41..467d150bca 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -773,6 +773,10 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { } /* Creator */ +#CreatorsView::item { + padding: 1px 5px; +} + #CreatorFamilyLabel { font-size: 10pt; font-weight: bold; diff --git a/openpype/tools/creator/window.py b/openpype/tools/creator/window.py index 6f590e887e..dca1735121 100644 --- a/openpype/tools/creator/window.py +++ b/openpype/tools/creator/window.py @@ -45,6 +45,7 @@ class CreatorWindow(QtWidgets.QDialog): 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)