From 09cc2e9e595231dc5122a89c54047a3d53ca178d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 16 Apr 2019 14:24:41 +0200 Subject: [PATCH] added widgets for family selection --- .../standalonepublish/widgets/__init__.py | 2 + .../widgets/widget_family.py | 290 ++++++++++++++++++ .../widgets/widget_family_desc.py | 101 ++++++ 3 files changed, 393 insertions(+) create mode 100644 pype/tools/standalonepublish/widgets/widget_family.py create mode 100644 pype/tools/standalonepublish/widgets/widget_family_desc.py diff --git a/pype/tools/standalonepublish/widgets/__init__.py b/pype/tools/standalonepublish/widgets/__init__.py index 426fa3d33c..4cf8a238e0 100644 --- a/pype/tools/standalonepublish/widgets/__init__.py +++ b/pype/tools/standalonepublish/widgets/__init__.py @@ -19,3 +19,5 @@ from .model_tree_view_deselectable import DeselectableTreeView from .widget_asset_view import AssetView from .widget_asset import AssetWidget +from .widget_family_desc import FamilyDescriptionWidget +from .widget_family import FamilyWidget diff --git a/pype/tools/standalonepublish/widgets/widget_family.py b/pype/tools/standalonepublish/widgets/widget_family.py new file mode 100644 index 0000000000..a0786b358d --- /dev/null +++ b/pype/tools/standalonepublish/widgets/widget_family.py @@ -0,0 +1,290 @@ +import os +import sys +import inspect +import json +from collections import namedtuple + +from . import QtWidgets, QtCore, QtGui +from . import HelpRole, FamilyRole, ExistsRole, PluginRole +from . import FamilyDescriptionWidget +from pype.vendor import six + +from avalon import api, io, style +from pype import lib as pypelib + + +class FamilyWidget(QtWidgets.QWidget): + + stateChanged = QtCore.Signal(bool) + data = dict() + _jobs = dict() + Separator = "---separator---" + + def __init__(self, parent): + super().__init__(parent) + # Store internal states in here + self.state = {"valid": False} + self.parent_widget = parent + + body = QtWidgets.QWidget() + lists = QtWidgets.QWidget() + + container = QtWidgets.QWidget() + + list_families = QtWidgets.QListWidget() + input_asset = QtWidgets.QLineEdit() + input_asset.setEnabled(False) + input_asset.setStyleSheet("color: #BBBBBB;") + input_subset = QtWidgets.QLineEdit() + input_result = QtWidgets.QLineEdit() + input_result.setStyleSheet("color: gray;") + input_result.setEnabled(False) + + # region Menu for default subset names + btn_subset = QtWidgets.QPushButton() + btn_subset.setFixedWidth(18) + btn_subset.setFixedHeight(20) + menu_subset = QtWidgets.QMenu(btn_subset) + btn_subset.setMenu(menu_subset) + + # endregion + name_layout = QtWidgets.QHBoxLayout() + name_layout.addWidget(input_subset) + name_layout.addWidget(btn_subset) + name_layout.setContentsMargins(0, 0, 0, 0) + + layout = QtWidgets.QVBoxLayout(container) + + header = FamilyDescriptionWidget(self) + layout.addWidget(header) + + layout.addWidget(QtWidgets.QLabel("Family")) + layout.addWidget(list_families) + layout.addWidget(QtWidgets.QLabel("Asset")) + layout.addWidget(input_asset) + layout.addWidget(QtWidgets.QLabel("Subset")) + layout.addLayout(name_layout) + layout.addWidget(input_result) + layout.setContentsMargins(0, 0, 0, 0) + + options = QtWidgets.QWidget() + + layout = QtWidgets.QGridLayout(options) + layout.setContentsMargins(0, 0, 0, 0) + + 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) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(body) + + input_subset.textChanged.connect(self.on_data_changed) + input_asset.textChanged.connect(self.on_data_changed) + list_families.currentItemChanged.connect(self.on_selection_changed) + list_families.currentItemChanged.connect(header.set_item) + + self.stateChanged.connect(self._on_state_changed) + + self.input_subset = input_subset + self.menu_subset = menu_subset + self.btn_subset = btn_subset + self.list_families = list_families + self.input_asset = input_asset + self.input_result = input_result + + self.refresh() + + @property + def db(self): + return self.parent_widget.db + + def change_asset(self, name): + self.input_asset.setText(name) + + def _on_state_changed(self, state): + self.state['valid'] = state + + def _build_menu(self, default_names): + """Create optional predefined subset names + + Args: + default_names(list): all predefined names + + Returns: + None + """ + + # Get and destroy the action group + group = self.btn_subset.findChild(QtWidgets.QActionGroup) + if group: + group.deleteLater() + + state = any(default_names) + self.btn_subset.setEnabled(state) + if state is False: + return + + # Build new action group + group = QtWidgets.QActionGroup(self.btn_subset) + for name in default_names: + if name == self.Separator: + self.menu_subset.addSeparator() + continue + action = group.addAction(name) + self.menu_subset.addAction(action) + + group.triggered.connect(self._on_action_clicked) + + def _on_action_clicked(self, action): + self.input_subset.setText(action.text()) + + def _on_data_changed(self): + item = self.list_families.currentItem() + subset_name = self.input_subset.text() + asset_name = self.input_asset.text() + + # Get the assets from the database which match with the name + assets_db = self.db.find(filter={"type": "asset"}, projection={"name": 1}) + assets = [asset for asset in assets_db if asset_name in asset["name"]] + if item is None: + return + if assets: + # Get plugin and family + plugin = item.data(PluginRole) + if plugin is None: + return + family = plugin.family.rsplit(".", 1)[-1] + + # Get all subsets of the current asset + asset_ids = [asset["_id"] for asset in assets] + subsets = self.db.find(filter={"type": "subset", + "name": {"$regex": "{}*".format(family), + "$options": "i"}, + "parent": {"$in": asset_ids}}) or [] + + # Get all subsets' their subset name, "Default", "High", "Low" + existed_subsets = [sub["name"].split(family)[-1] + for sub in subsets] + + if plugin.defaults and isinstance(plugin.defaults, list): + defaults = plugin.defaults[:] + [self.Separator] + lowered = [d.lower() for d in plugin.defaults] + for sub in [s for s in existed_subsets + if s.lower() not in lowered]: + defaults.append(sub) + else: + defaults = existed_subsets + + self._build_menu(defaults) + + # Update the result + if subset_name: + subset_name = subset_name[0].upper() + subset_name[1:] + self.input_result.setText("{}{}".format(family, subset_name)) + + item.setData(ExistsRole, True) + self.echo("Ready ..") + else: + self._build_menu([]) + item.setData(ExistsRole, False) + if asset_name != self.parent_widget.NOT_SELECTED: + self.echo("'%s' not found .." % asset_name) + + # Update the valid state + valid = ( + subset_name.strip() != "" and + asset_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) + self.schedule(self._on_data_changed, 500, channel="gui") + + def on_selection_changed(self, *args): + plugin = self.list_families.currentItem().data(PluginRole) + if plugin is None: + return + + if plugin.defaults and isinstance(plugin.defaults, list): + default = plugin.defaults[0] + else: + default = "Default" + + self.input_subset.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): + has_families = False + + path_items = [ + pypelib.get_presets_path(), 'tools', 'standalone_publish.json' + ] + filepath = os.path.sep.join(path_items) + presets = dict() + with open(filepath) as data_file: + presets = json.load(data_file) + + for creator in presets.get('families', {}).values(): + creator = namedtuple("Creator", creator.keys())(*creator.values()) + + label = creator.label or creator.family + item = QtWidgets.QListWidgetItem(label) + item.setData(QtCore.Qt.ItemIsEnabled, True) + item.setData(HelpRole, creator.help or "") + item.setData(FamilyRole, creator.family) + item.setData(PluginRole, creator) + item.setData(ExistsRole, False) + self.list_families.addItem(item) + + has_families = True + + if not has_families: + item = QtWidgets.QListWidgetItem("No registered families") + item.setData(QtCore.Qt.ItemIsEnabled, False) + self.list_families.addItem(item) + + presets_path = pypelib.get_presets_path() + config_file = os.path.sep.join([presets_path, 'tools', 'creator.json']) + + self.list_families.setCurrentItem(self.list_families.item(0)) + + def echo(self, message): + if hasattr(self.parent_widget, 'echo'): + self.parent_widget.echo(message) + + def schedule(self, func, time, channel="default"): + try: + self._jobs[channel].stop() + except (AttributeError, KeyError): + pass + + timer = QtCore.QTimer() + timer.setSingleShot(True) + timer.timeout.connect(func) + timer.start(time) + + self._jobs[channel] = timer diff --git a/pype/tools/standalonepublish/widgets/widget_family_desc.py b/pype/tools/standalonepublish/widgets/widget_family_desc.py new file mode 100644 index 0000000000..e329f28ba6 --- /dev/null +++ b/pype/tools/standalonepublish/widgets/widget_family_desc.py @@ -0,0 +1,101 @@ +import os +import sys +import inspect +import json + +from . import QtWidgets, QtCore, QtGui +from . import HelpRole, FamilyRole, ExistsRole, PluginRole +from . import awesome +from pype.vendor import six +from pype import lib as pypelib + + +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: + family (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 = getattr(plugin, "icon", "info-circle") + assert isinstance(icon, six.string_types) + icon = awesome.icon("fa.{}".format(icon), color="white") + pixmap = icon.pixmap(self.SIZE, self.SIZE) + pixmap = pixmap.scaled(self.SIZE, self.SIZE) + + # Parse a clean line from the Creator's docstring + docstring = plugin.help or "" + + help = docstring.splitlines()[0] if docstring else "" + + self.icon.setPixmap(pixmap) + self.family.setText(item.data(FamilyRole)) + self.help.setText(help)