From 67f314c276e25b029ed871a39de2c36d4cf76a4b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 16 Apr 2019 14:17:50 +0200 Subject: [PATCH 01/65] initial commit --- pype/tools/standalonepublish/__init__.py | 8 ++++++++ pype/tools/standalonepublish/__main__.py | 5 +++++ 2 files changed, 13 insertions(+) create mode 100644 pype/tools/standalonepublish/__init__.py create mode 100644 pype/tools/standalonepublish/__main__.py diff --git a/pype/tools/standalonepublish/__init__.py b/pype/tools/standalonepublish/__init__.py new file mode 100644 index 0000000000..29a4e52904 --- /dev/null +++ b/pype/tools/standalonepublish/__init__.py @@ -0,0 +1,8 @@ +from .app import ( + show, + cli +) +__all__ = [ + "show", + "cli" +] diff --git a/pype/tools/standalonepublish/__main__.py b/pype/tools/standalonepublish/__main__.py new file mode 100644 index 0000000000..d77bc585c5 --- /dev/null +++ b/pype/tools/standalonepublish/__main__.py @@ -0,0 +1,5 @@ +from . import cli + +if __name__ == '__main__': + import sys + sys.exit(cli(sys.argv[1:])) From e6eb2a25e70548f7fafa3229d305ccc2310a2309 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 16 Apr 2019 14:18:14 +0200 Subject: [PATCH 02/65] added resources for widget icons --- .../standalonepublish/resources/__init__.py | 14 +++++ .../standalonepublish/resources/edit.svg | 9 +++ .../resources/image_sequence.png | Bin 0 -> 5092 bytes .../resources/information.svg | 14 +++++ .../resources/original/delete-button.svg | 55 ++++++++++++++++++ .../resources/original/information.svg | 44 ++++++++++++++ .../resources/original/picture.svg | 48 +++++++++++++++ .../resources/original/play_icon.svg | 19 ++++++ .../standalonepublish/resources/preview.svg | 19 ++++++ .../standalonepublish/resources/thumbnail.svg | 19 ++++++ .../standalonepublish/resources/trash.svg | 23 ++++++++ 11 files changed, 264 insertions(+) create mode 100644 pype/tools/standalonepublish/resources/__init__.py create mode 100644 pype/tools/standalonepublish/resources/edit.svg create mode 100644 pype/tools/standalonepublish/resources/image_sequence.png create mode 100644 pype/tools/standalonepublish/resources/information.svg create mode 100644 pype/tools/standalonepublish/resources/original/delete-button.svg create mode 100644 pype/tools/standalonepublish/resources/original/information.svg create mode 100644 pype/tools/standalonepublish/resources/original/picture.svg create mode 100644 pype/tools/standalonepublish/resources/original/play_icon.svg create mode 100644 pype/tools/standalonepublish/resources/preview.svg create mode 100644 pype/tools/standalonepublish/resources/thumbnail.svg create mode 100644 pype/tools/standalonepublish/resources/trash.svg diff --git a/pype/tools/standalonepublish/resources/__init__.py b/pype/tools/standalonepublish/resources/__init__.py new file mode 100644 index 0000000000..ce329ee585 --- /dev/null +++ b/pype/tools/standalonepublish/resources/__init__.py @@ -0,0 +1,14 @@ +import os + + +resource_path = os.path.dirname(__file__) + + +def get_resource(*args): + """ Serves to simple resources access + + :param \*args: should contain *subfolder* names and *filename* of + resource from resources folder + :type \*args: list + """ + return os.path.normpath(os.path.join(resource_path, *args)) diff --git a/pype/tools/standalonepublish/resources/edit.svg b/pype/tools/standalonepublish/resources/edit.svg new file mode 100644 index 0000000000..26451b4a9d --- /dev/null +++ b/pype/tools/standalonepublish/resources/edit.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/pype/tools/standalonepublish/resources/image_sequence.png b/pype/tools/standalonepublish/resources/image_sequence.png new file mode 100644 index 0000000000000000000000000000000000000000..51639b07d0f94612a64a8521fff0509660debe9b GIT binary patch literal 5092 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 literal 0 HcmV?d00001 diff --git a/pype/tools/standalonepublish/resources/information.svg b/pype/tools/standalonepublish/resources/information.svg new file mode 100644 index 0000000000..e0f73a7eb1 --- /dev/null +++ b/pype/tools/standalonepublish/resources/information.svg @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/pype/tools/standalonepublish/resources/original/delete-button.svg b/pype/tools/standalonepublish/resources/original/delete-button.svg new file mode 100644 index 0000000000..48b09ac787 --- /dev/null +++ b/pype/tools/standalonepublish/resources/original/delete-button.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pype/tools/standalonepublish/resources/original/information.svg b/pype/tools/standalonepublish/resources/original/information.svg new file mode 100644 index 0000000000..c040bab773 --- /dev/null +++ b/pype/tools/standalonepublish/resources/original/information.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pype/tools/standalonepublish/resources/original/picture.svg b/pype/tools/standalonepublish/resources/original/picture.svg new file mode 100644 index 0000000000..35f912ce80 --- /dev/null +++ b/pype/tools/standalonepublish/resources/original/picture.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pype/tools/standalonepublish/resources/original/play_icon.svg b/pype/tools/standalonepublish/resources/original/play_icon.svg new file mode 100644 index 0000000000..e9bab5a251 --- /dev/null +++ b/pype/tools/standalonepublish/resources/original/play_icon.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + diff --git a/pype/tools/standalonepublish/resources/preview.svg b/pype/tools/standalonepublish/resources/preview.svg new file mode 100644 index 0000000000..4a9810c1d5 --- /dev/null +++ b/pype/tools/standalonepublish/resources/preview.svg @@ -0,0 +1,19 @@ + + + + + PREVIEW + + diff --git a/pype/tools/standalonepublish/resources/thumbnail.svg b/pype/tools/standalonepublish/resources/thumbnail.svg new file mode 100644 index 0000000000..dbc228f8c8 --- /dev/null +++ b/pype/tools/standalonepublish/resources/thumbnail.svg @@ -0,0 +1,19 @@ + + + + + THUMBNAIL + + diff --git a/pype/tools/standalonepublish/resources/trash.svg b/pype/tools/standalonepublish/resources/trash.svg new file mode 100644 index 0000000000..07905024c0 --- /dev/null +++ b/pype/tools/standalonepublish/resources/trash.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + From 0d73f32d26338a330ca05d5248a5589b4ddf5d5b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 16 Apr 2019 14:20:52 +0200 Subject: [PATCH 03/65] created svg button --- .../standalonepublish/widgets/__init__.py | 11 ++ .../widgets/button_from_svgs.py | 109 ++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 pype/tools/standalonepublish/widgets/__init__.py create mode 100644 pype/tools/standalonepublish/widgets/button_from_svgs.py diff --git a/pype/tools/standalonepublish/widgets/__init__.py b/pype/tools/standalonepublish/widgets/__init__.py new file mode 100644 index 0000000000..f6b3dc2fff --- /dev/null +++ b/pype/tools/standalonepublish/widgets/__init__.py @@ -0,0 +1,11 @@ +from avalon.vendor.Qt import * +from avalon.vendor import qtawesome as awesome +from avalon import style + +HelpRole = QtCore.Qt.UserRole + 2 +FamilyRole = QtCore.Qt.UserRole + 3 +ExistsRole = QtCore.Qt.UserRole + 4 +PluginRole = QtCore.Qt.UserRole + 5 + +from ..resources import get_resource +from .button_from_svgs import SvgResizable, SvgButton diff --git a/pype/tools/standalonepublish/widgets/button_from_svgs.py b/pype/tools/standalonepublish/widgets/button_from_svgs.py new file mode 100644 index 0000000000..aeeeae5c7a --- /dev/null +++ b/pype/tools/standalonepublish/widgets/button_from_svgs.py @@ -0,0 +1,109 @@ +from xml.dom import minidom + +from . import QtGui, QtCore, QtWidgets +from PyQt5 import QtSvg, QtXml + + +class SvgResizable(QtSvg.QSvgWidget): + clicked = QtCore.Signal() + def __init__(self, filepath, width=None, height=None, fill=None): + super().__init__() + self.xmldoc = minidom.parse(filepath) + itemlist = self.xmldoc.getElementsByTagName('svg') + for element in itemlist: + if fill: + element.setAttribute('fill', str(fill)) + if width is not None and height is not None: + self.setMaximumSize(width, height) + self.setMinimumSize(width, height) + xml_string = self.xmldoc.toxml() + svg_bytes = bytearray(xml_string, encoding='utf-8') + + self.load(svg_bytes) + + def change_color(self, color): + element = self.xmldoc.getElementsByTagName('svg')[0] + element.setAttribute('fill', str(color)) + xml_string = self.xmldoc.toxml() + svg_bytes = bytearray(xml_string, encoding='utf-8') + self.load(svg_bytes) + + def mousePressEvent(self, event): + self.clicked.emit() + + +class SvgButton(QtWidgets.QFrame): + clicked = QtCore.Signal() + def __init__( + self, filepath, width=None, height=None, fills=[], + parent=None, checkable=True + ): + super().__init__(parent) + self.checkable = checkable + self.checked = False + + xmldoc = minidom.parse(filepath) + element = xmldoc.getElementsByTagName('svg')[0] + c_actual = '#777777' + if element.hasAttribute('fill'): + c_actual = element.getAttribute('fill') + self.store_fills(fills, c_actual) + + self.installEventFilter(self) + self.svg_widget = SvgResizable(filepath, width, height, self.c_normal) + xmldoc = minidom.parse(filepath) + + layout = QtWidgets.QHBoxLayout(self) + layout.setSpacing(0) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self.svg_widget) + + if width is not None and height is not None: + self.setMaximumSize(width, height) + self.setMinimumSize(width, height) + + def store_fills(self, fills, actual): + if len(fills) == 0: + fills = [actual, actual, actual, actual] + elif len(fills) == 1: + fills = [fills[0], fills[0], fills[0], fills[0]] + elif len(fills) == 2: + fills = [fills[0], fills[1], fills[1], fills[1]] + elif len(fills) == 3: + fills = [fills[0], fills[1], fills[2], fills[2]] + self.c_normal = fills[0] + self.c_hover = fills[1] + self.c_active = fills[2] + self.c_active_hover = fills[3] + + def eventFilter(self, object, event): + if event.type() == QtCore.QEvent.Enter: + self.hoverEnterEvent(event) + return True + elif event.type() == QtCore.QEvent.Leave: + self.hoverLeaveEvent(event) + return True + elif event.type() == QtCore.QEvent.MouseButtonRelease: + self.mousePressEvent(event) + return False + + def change_checked(self, in_bool=False): + if self.checkable: + self.checked = in_bool + + def hoverEnterEvent(self, event=None): + color = self.c_hover + if self.checked: + color = self.c_active_hover + self.svg_widget.change_color(color) + + def hoverLeaveEvent(self, event=None): + color = self.c_normal + if self.checked: + color = self.c_active + self.svg_widget.change_color(color) + + def mousePressEvent(self, event=None): + self.change_checked(not self.checked) + self.hoverEnterEvent() + self.clicked.emit() From d4619716403fc3ebbd69397e64e3051bb6f33d90 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 16 Apr 2019 14:24:09 +0200 Subject: [PATCH 04/65] added widgets for asset selection --- .../standalonepublish/widgets/__init__.py | 10 + .../standalonepublish/widgets/model_asset.py | 158 +++++++++++ .../widgets/model_filter_proxy_exact_match.py | 28 ++ .../model_filter_proxy_recursive_sort.py | 30 +++ .../standalonepublish/widgets/model_node.py | 56 ++++ .../standalonepublish/widgets/model_tree.py | 122 +++++++++ .../widgets/model_tree_view_deselectable.py | 16 ++ .../standalonepublish/widgets/widget_asset.py | 255 ++++++++++++++++++ .../widgets/widget_asset_view.py | 16 ++ 9 files changed, 691 insertions(+) create mode 100644 pype/tools/standalonepublish/widgets/model_asset.py create mode 100644 pype/tools/standalonepublish/widgets/model_filter_proxy_exact_match.py create mode 100644 pype/tools/standalonepublish/widgets/model_filter_proxy_recursive_sort.py create mode 100644 pype/tools/standalonepublish/widgets/model_node.py create mode 100644 pype/tools/standalonepublish/widgets/model_tree.py create mode 100644 pype/tools/standalonepublish/widgets/model_tree_view_deselectable.py create mode 100644 pype/tools/standalonepublish/widgets/widget_asset.py create mode 100644 pype/tools/standalonepublish/widgets/widget_asset_view.py diff --git a/pype/tools/standalonepublish/widgets/__init__.py b/pype/tools/standalonepublish/widgets/__init__.py index f6b3dc2fff..426fa3d33c 100644 --- a/pype/tools/standalonepublish/widgets/__init__.py +++ b/pype/tools/standalonepublish/widgets/__init__.py @@ -9,3 +9,13 @@ PluginRole = QtCore.Qt.UserRole + 5 from ..resources import get_resource from .button_from_svgs import SvgResizable, SvgButton + +from .model_node import Node +from .model_tree import TreeModel +from .model_asset import AssetModel +from .model_filter_proxy_exact_match import ExactMatchesFilterProxyModel +from .model_filter_proxy_recursive_sort import RecursiveSortFilterProxyModel +from .model_tree_view_deselectable import DeselectableTreeView + +from .widget_asset_view import AssetView +from .widget_asset import AssetWidget diff --git a/pype/tools/standalonepublish/widgets/model_asset.py b/pype/tools/standalonepublish/widgets/model_asset.py new file mode 100644 index 0000000000..fdf844342e --- /dev/null +++ b/pype/tools/standalonepublish/widgets/model_asset.py @@ -0,0 +1,158 @@ +import logging +from . import QtCore, QtGui +from . import TreeModel, Node +from . import style, awesome + + +log = logging.getLogger(__name__) + + +def _iter_model_rows(model, + column, + include_root=False): + """Iterate over all row indices in a model""" + indices = [QtCore.QModelIndex()] # start iteration at root + + for index in indices: + + # Add children to the iterations + child_rows = model.rowCount(index) + for child_row in range(child_rows): + child_index = model.index(child_row, column, index) + indices.append(child_index) + + if not include_root and not index.isValid(): + continue + + yield index + + +class AssetModel(TreeModel): + """A model listing assets in the silo in the active project. + + The assets are displayed in a treeview, they are visually parented by + a `visualParent` field in the database containing an `_id` to a parent + asset. + + """ + + COLUMNS = ["label"] + Name = 0 + Deprecated = 2 + ObjectId = 3 + + DocumentRole = QtCore.Qt.UserRole + 2 + ObjectIdRole = QtCore.Qt.UserRole + 3 + + def __init__(self, parent): + super(AssetModel, self).__init__(parent=parent) + self.parent_widget = parent + self.refresh() + + @property + def db(self): + return self.parent_widget.db + + def _add_hierarchy(self, parent=None): + + # Find the assets under the parent + find_data = { + "type": "asset" + } + if parent is None: + find_data['$or'] = [ + {'data.visualParent': {'$exists': False}}, + {'data.visualParent': None} + ] + else: + find_data["data.visualParent"] = parent['_id'] + + assets = self.db.find(find_data).sort('name', 1) + for asset in assets: + # get label from data, otherwise use name + data = asset.get("data", {}) + label = data.get("label", asset['name']) + tags = data.get("tags", []) + + # store for the asset for optimization + deprecated = "deprecated" in tags + + node = Node({ + "_id": asset['_id'], + "name": asset["name"], + "label": label, + "type": asset['type'], + "tags": ", ".join(tags), + "deprecated": deprecated, + "_document": asset + }) + self.add_child(node, parent=parent) + + # Add asset's children recursively + self._add_hierarchy(node) + + def refresh(self): + """Refresh the data for the model.""" + + self.clear() + if ( + self.db.active_project() is None or + self.db.active_project() == '' + ): + return + self.beginResetModel() + self._add_hierarchy(parent=None) + self.endResetModel() + + def flags(self, index): + return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable + + def data(self, index, role): + + if not index.isValid(): + return + + node = index.internalPointer() + if role == QtCore.Qt.DecorationRole: # icon + + column = index.column() + if column == self.Name: + + # Allow a custom icon and custom icon color to be defined + data = node["_document"]["data"] + icon = data.get("icon", None) + color = data.get("color", style.colors.default) + + if icon is None: + # Use default icons if no custom one is specified. + # If it has children show a full folder, otherwise + # show an open folder + has_children = self.rowCount(index) > 0 + icon = "folder" if has_children else "folder-o" + + # Make the color darker when the asset is deprecated + if node.get("deprecated", False): + color = QtGui.QColor(color).darker(250) + + try: + key = "fa.{0}".format(icon) # font-awesome key + icon = awesome.icon(key, color=color) + return icon + except Exception as exception: + # Log an error message instead of erroring out completely + # when the icon couldn't be created (e.g. invalid name) + log.error(exception) + + return + + if role == QtCore.Qt.ForegroundRole: # font color + if "deprecated" in node.get("tags", []): + return QtGui.QColor(style.colors.light).darker(250) + + if role == self.ObjectIdRole: + return node.get("_id", None) + + if role == self.DocumentRole: + return node.get("_document", None) + + return super(AssetModel, self).data(index, role) diff --git a/pype/tools/standalonepublish/widgets/model_filter_proxy_exact_match.py b/pype/tools/standalonepublish/widgets/model_filter_proxy_exact_match.py new file mode 100644 index 0000000000..862e4071db --- /dev/null +++ b/pype/tools/standalonepublish/widgets/model_filter_proxy_exact_match.py @@ -0,0 +1,28 @@ +from . import QtCore + + +class ExactMatchesFilterProxyModel(QtCore.QSortFilterProxyModel): + """Filter model to where key column's value is in the filtered tags""" + + def __init__(self, *args, **kwargs): + super(ExactMatchesFilterProxyModel, self).__init__(*args, **kwargs) + self._filters = set() + + def setFilters(self, filters): + self._filters = set(filters) + + def filterAcceptsRow(self, source_row, source_parent): + + # No filter + if not self._filters: + return True + + else: + model = self.sourceModel() + column = self.filterKeyColumn() + idx = model.index(source_row, column, source_parent) + data = model.data(idx, self.filterRole()) + if data in self._filters: + return True + else: + return False diff --git a/pype/tools/standalonepublish/widgets/model_filter_proxy_recursive_sort.py b/pype/tools/standalonepublish/widgets/model_filter_proxy_recursive_sort.py new file mode 100644 index 0000000000..04ee88229f --- /dev/null +++ b/pype/tools/standalonepublish/widgets/model_filter_proxy_recursive_sort.py @@ -0,0 +1,30 @@ +from . import QtCore + + +class RecursiveSortFilterProxyModel(QtCore.QSortFilterProxyModel): + """Filters to the regex if any of the children matches allow parent""" + def filterAcceptsRow(self, row, parent): + + regex = self.filterRegExp() + if not regex.isEmpty(): + pattern = regex.pattern() + model = self.sourceModel() + source_index = model.index(row, self.filterKeyColumn(), parent) + if source_index.isValid(): + + # Check current index itself + key = model.data(source_index, self.filterRole()) + if re.search(pattern, key, re.IGNORECASE): + return True + + # Check children + rows = model.rowCount(source_index) + for i in range(rows): + if self.filterAcceptsRow(i, source_index): + return True + + # Otherwise filter it + return False + + return super(RecursiveSortFilterProxyModel, + self).filterAcceptsRow(row, parent) diff --git a/pype/tools/standalonepublish/widgets/model_node.py b/pype/tools/standalonepublish/widgets/model_node.py new file mode 100644 index 0000000000..e8326d5b90 --- /dev/null +++ b/pype/tools/standalonepublish/widgets/model_node.py @@ -0,0 +1,56 @@ +import logging + + +log = logging.getLogger(__name__) + + +class Node(dict): + """A node that can be represented in a tree view. + + The node can store data just like a dictionary. + + >>> data = {"name": "John", "score": 10} + >>> node = Node(data) + >>> assert node["name"] == "John" + + """ + + def __init__(self, data=None): + super(Node, self).__init__() + + self._children = list() + self._parent = None + + if data is not None: + assert isinstance(data, dict) + self.update(data) + + def childCount(self): + return len(self._children) + + def child(self, row): + + if row >= len(self._children): + log.warning("Invalid row as child: {0}".format(row)) + return + + return self._children[row] + + def children(self): + return self._children + + def parent(self): + return self._parent + + def row(self): + """ + Returns: + int: Index of this node under parent""" + if self._parent is not None: + siblings = self.parent().children() + return siblings.index(self) + + def add_child(self, child): + """Add a child to this node""" + child._parent = self + self._children.append(child) diff --git a/pype/tools/standalonepublish/widgets/model_tree.py b/pype/tools/standalonepublish/widgets/model_tree.py new file mode 100644 index 0000000000..e4f1aa5eb7 --- /dev/null +++ b/pype/tools/standalonepublish/widgets/model_tree.py @@ -0,0 +1,122 @@ +from . import QtCore +from . import Node + + +class TreeModel(QtCore.QAbstractItemModel): + + COLUMNS = list() + NodeRole = QtCore.Qt.UserRole + 1 + + def __init__(self, parent=None): + super(TreeModel, self).__init__(parent) + self._root_node = Node() + + def rowCount(self, parent): + if parent.isValid(): + node = parent.internalPointer() + else: + node = self._root_node + + return node.childCount() + + def columnCount(self, parent): + return len(self.COLUMNS) + + def data(self, index, role): + + if not index.isValid(): + return None + + if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole: + + node = index.internalPointer() + column = index.column() + + key = self.COLUMNS[column] + return node.get(key, None) + + if role == self.NodeRole: + return index.internalPointer() + + def setData(self, index, value, role=QtCore.Qt.EditRole): + """Change the data on the nodes. + + Returns: + bool: Whether the edit was successful + """ + + if index.isValid(): + if role == QtCore.Qt.EditRole: + + node = index.internalPointer() + column = index.column() + key = self.COLUMNS[column] + node[key] = value + + # passing `list()` for PyQt5 (see PYSIDE-462) + self.dataChanged.emit(index, index, list()) + + # must return true if successful + return True + + return False + + def setColumns(self, keys): + assert isinstance(keys, (list, tuple)) + self.COLUMNS = keys + + def headerData(self, section, orientation, role): + + if role == QtCore.Qt.DisplayRole: + if section < len(self.COLUMNS): + return self.COLUMNS[section] + + super(TreeModel, self).headerData(section, orientation, role) + + def flags(self, index): + return ( + QtCore.Qt.ItemIsEnabled | + QtCore.Qt.ItemIsSelectable + ) + + def parent(self, index): + + node = index.internalPointer() + parent_node = node.parent() + + # If it has no parents we return invalid + if parent_node == self._root_node or not parent_node: + return QtCore.QModelIndex() + + return self.createIndex(parent_node.row(), 0, parent_node) + + def index(self, row, column, parent): + """Return index for row/column under parent""" + + if not parent.isValid(): + parentNode = self._root_node + else: + parentNode = parent.internalPointer() + + childItem = parentNode.child(row) + if childItem: + return self.createIndex(row, column, childItem) + else: + return QtCore.QModelIndex() + + def add_child(self, node, parent=None): + if parent is None: + parent = self._root_node + + parent.add_child(node) + + def column_name(self, column): + """Return column key by index""" + + if column < len(self.COLUMNS): + return self.COLUMNS[column] + + def clear(self): + self.beginResetModel() + self._root_node = Node() + self.endResetModel() diff --git a/pype/tools/standalonepublish/widgets/model_tree_view_deselectable.py b/pype/tools/standalonepublish/widgets/model_tree_view_deselectable.py new file mode 100644 index 0000000000..78bec44d36 --- /dev/null +++ b/pype/tools/standalonepublish/widgets/model_tree_view_deselectable.py @@ -0,0 +1,16 @@ +from . import QtWidgets, QtCore + + +class DeselectableTreeView(QtWidgets.QTreeView): + """A tree view that deselects on clicking on an empty area in the view""" + + def mousePressEvent(self, event): + + index = self.indexAt(event.pos()) + if not index.isValid(): + # clear the selection + self.clearSelection() + # clear the current index + self.setCurrentIndex(QtCore.QModelIndex()) + + QtWidgets.QTreeView.mousePressEvent(self, event) diff --git a/pype/tools/standalonepublish/widgets/widget_asset.py b/pype/tools/standalonepublish/widgets/widget_asset.py new file mode 100644 index 0000000000..665a5913a0 --- /dev/null +++ b/pype/tools/standalonepublish/widgets/widget_asset.py @@ -0,0 +1,255 @@ +import contextlib +from . import QtWidgets, QtCore +from . import RecursiveSortFilterProxyModel, AssetModel, AssetView +from . import awesome, style + +@contextlib.contextmanager +def preserve_expanded_rows(tree_view, + column=0, + role=QtCore.Qt.DisplayRole): + """Preserves expanded row in QTreeView by column's data role. + + This function is created to maintain the expand vs collapse status of + the model items. When refresh is triggered the items which are expanded + will stay expanded and vise versa. + + Arguments: + tree_view (QWidgets.QTreeView): the tree view which is + nested in the application + column (int): the column to retrieve the data from + role (int): the role which dictates what will be returned + + Returns: + None + + """ + + model = tree_view.model() + + expanded = set() + + for index in _iter_model_rows(model, + column=column, + include_root=False): + if tree_view.isExpanded(index): + value = index.data(role) + expanded.add(value) + + try: + yield + finally: + if not expanded: + return + + for index in _iter_model_rows(model, + column=column, + include_root=False): + value = index.data(role) + state = value in expanded + if state: + tree_view.expand(index) + else: + tree_view.collapse(index) + + +@contextlib.contextmanager +def preserve_selection(tree_view, + column=0, + role=QtCore.Qt.DisplayRole, + current_index=True): + """Preserves row selection in QTreeView by column's data role. + + This function is created to maintain the selection status of + the model items. When refresh is triggered the items which are expanded + will stay expanded and vise versa. + + tree_view (QWidgets.QTreeView): the tree view nested in the application + column (int): the column to retrieve the data from + role (int): the role which dictates what will be returned + + Returns: + None + + """ + + model = tree_view.model() + selection_model = tree_view.selectionModel() + flags = selection_model.Select | selection_model.Rows + + if current_index: + current_index_value = tree_view.currentIndex().data(role) + else: + current_index_value = None + + selected_rows = selection_model.selectedRows() + if not selected_rows: + yield + return + + selected = set(row.data(role) for row in selected_rows) + try: + yield + finally: + if not selected: + return + + # Go through all indices, select the ones with similar data + for index in _iter_model_rows(model, + column=column, + include_root=False): + + value = index.data(role) + state = value in selected + if state: + tree_view.scrollTo(index) # Ensure item is visible + selection_model.select(index, flags) + + if current_index_value and value == current_index_value: + tree_view.setCurrentIndex(index) + + +class AssetWidget(QtWidgets.QWidget): + """A Widget to display a tree of assets with filter + + To list the assets of the active project: + >>> # widget = AssetWidget() + >>> # widget.refresh() + >>> # widget.show() + + """ + + assets_refreshed = QtCore.Signal() # on model refresh + selection_changed = QtCore.Signal() # on view selection change + current_changed = QtCore.Signal() # on view current index change + + def __init__(self, parent): + super(AssetWidget, self).__init__(parent=parent) + self.setContentsMargins(0, 0, 0, 0) + + self.parent_widget = parent + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(4) + + # Project + self.combo_projects = QtWidgets.QComboBox() + self._set_projects() + self.combo_projects.currentTextChanged.connect(self.on_project_change) + # Tree View + model = AssetModel(self) + proxy = RecursiveSortFilterProxyModel() + proxy.setSourceModel(model) + proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) + view = AssetView() + view.setModel(proxy) + + # Header + header = QtWidgets.QHBoxLayout() + + icon = awesome.icon("fa.refresh", color=style.colors.light) + refresh = QtWidgets.QPushButton(icon, "") + refresh.setToolTip("Refresh items") + + filter = QtWidgets.QLineEdit() + filter.textChanged.connect(proxy.setFilterFixedString) + filter.setPlaceholderText("Filter assets..") + + header.addWidget(filter) + header.addWidget(refresh) + + # Layout + layout.addWidget(self.combo_projects) + layout.addLayout(header) + layout.addWidget(view) + + # Signals/Slots + selection = view.selectionModel() + selection.selectionChanged.connect(self.selection_changed) + selection.currentChanged.connect(self.current_changed) + refresh.clicked.connect(self.refresh) + + self.refreshButton = refresh + self.model = model + self.proxy = proxy + self.view = view + + @property + def db(self): + return self.parent_widget.db + + def _set_projects(self): + projects = list() + for project in self.db.projects(): + projects.append(project['name']) + + self.combo_projects.clear() + if len(projects) > 0: + self.combo_projects.addItems(projects) + self.db.activate_project(projects[0]) + + def on_project_change(self): + projects = list() + for project in self.db.projects(): + projects.append(project['name']) + project_name = self.combo_projects.currentText() + if project_name in projects: + self.db.activate_project(project_name) + self.refresh() + + def _refresh_model(self): + self.model.refresh() + self.assets_refreshed.emit() + + def refresh(self): + self._refresh_model() + + def get_active_asset(self): + """Return the asset id the current asset.""" + current = self.view.currentIndex() + return current.data(self.model.ObjectIdRole) + + def get_active_index(self): + return self.view.currentIndex() + + def get_selected_assets(self): + """Return the assets' ids that are selected.""" + selection = self.view.selectionModel() + rows = selection.selectedRows() + return [row.data(self.model.ObjectIdRole) for row in rows] + + def select_assets(self, assets, expand=True): + """Select assets by name. + + Args: + assets (list): List of asset names + expand (bool): Whether to also expand to the asset in the view + + Returns: + None + + """ + # TODO: Instead of individual selection optimize for many assets + + assert isinstance(assets, + (tuple, list)), "Assets must be list or tuple" + + # Clear selection + selection_model = self.view.selectionModel() + selection_model.clearSelection() + + # Select + mode = selection_model.Select | selection_model.Rows + for index in _iter_model_rows(self.proxy, + column=0, + include_root=False): + data = index.data(self.model.NodeRole) + name = data['name'] + if name in assets: + selection_model.select(index, mode) + + if expand: + self.view.expand(index) + + # Set the currently active index + self.view.setCurrentIndex(index) diff --git a/pype/tools/standalonepublish/widgets/widget_asset_view.py b/pype/tools/standalonepublish/widgets/widget_asset_view.py new file mode 100644 index 0000000000..27bf374599 --- /dev/null +++ b/pype/tools/standalonepublish/widgets/widget_asset_view.py @@ -0,0 +1,16 @@ +from . import QtCore +from . import DeselectableTreeView + + +class AssetView(DeselectableTreeView): + """Item view. + + This implements a context menu. + + """ + + def __init__(self): + super(AssetView, self).__init__() + self.setIndentation(15) + self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.setHeaderHidden(True) From 09cc2e9e595231dc5122a89c54047a3d53ca178d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 16 Apr 2019 14:24:41 +0200 Subject: [PATCH 05/65] 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) From a09c90aeb3118c9e86d467b508437ad6d2d72c60 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 16 Apr 2019 14:25:19 +0200 Subject: [PATCH 06/65] added widgets for component inserting --- .../standalonepublish/widgets/__init__.py | 7 + .../widgets/widget_component.py | 189 ++++++++++++ .../widgets/widget_component_item.py | 15 + .../widgets/widget_drop_data.py | 41 +++ .../widgets/widget_drop_files.py | 273 ++++++++++++++++++ .../widgets/widget_tree_components.py | 14 + 6 files changed, 539 insertions(+) create mode 100644 pype/tools/standalonepublish/widgets/widget_component.py create mode 100644 pype/tools/standalonepublish/widgets/widget_component_item.py create mode 100644 pype/tools/standalonepublish/widgets/widget_drop_data.py create mode 100644 pype/tools/standalonepublish/widgets/widget_drop_files.py create mode 100644 pype/tools/standalonepublish/widgets/widget_tree_components.py diff --git a/pype/tools/standalonepublish/widgets/__init__.py b/pype/tools/standalonepublish/widgets/__init__.py index 4cf8a238e0..f4f06448a5 100644 --- a/pype/tools/standalonepublish/widgets/__init__.py +++ b/pype/tools/standalonepublish/widgets/__init__.py @@ -21,3 +21,10 @@ from .widget_asset_view import AssetView from .widget_asset import AssetWidget from .widget_family_desc import FamilyDescriptionWidget from .widget_family import FamilyWidget +from .widget_drop_data import DropDataWidget + +from .widget_component import ComponentWidget +from .widget_tree_components import TreeComponents +from .widget_component_item import ComponentItem + +from .widget_drop_files import DropDataFrame diff --git a/pype/tools/standalonepublish/widgets/widget_component.py b/pype/tools/standalonepublish/widgets/widget_component.py new file mode 100644 index 0000000000..f7248e31c1 --- /dev/null +++ b/pype/tools/standalonepublish/widgets/widget_component.py @@ -0,0 +1,189 @@ +from . import QtCore, QtGui, QtWidgets +from . import SvgButton +from . import get_resource + + +class ComponentWidget(QtWidgets.QFrame): + C_NORMAL = '#777777' + C_HOVER = '#ffffff' + C_ACTIVE = '#4BB543' + C_ACTIVE_HOVER = '#4BF543' + signal_remove = QtCore.Signal(object) + + def __init__(self, parent): + super().__init__() + self.resize(290, 70) + self.setMinimumSize(QtCore.QSize(0, 70)) + self.parent_item = parent + # Font + font = QtGui.QFont() + font.setFamily("DejaVu Sans Condensed") + font.setPointSize(9) + font.setBold(True) + font.setWeight(50) + font.setKerning(True) + + # Main widgets + frame = QtWidgets.QFrame(self) + frame.setFrameShape(QtWidgets.QFrame.StyledPanel) + frame.setFrameShadow(QtWidgets.QFrame.Raised) + + layout_main = QtWidgets.QHBoxLayout(frame) + layout_main.setSpacing(2) + layout_main.setContentsMargins(2, 2, 2, 2) + + # Image + Info + frame_image_info = QtWidgets.QFrame(frame) + + # Layout image info + layout = QtWidgets.QVBoxLayout(frame_image_info) + layout.setSpacing(2) + layout.setContentsMargins(2, 2, 2, 2) + + image = QtWidgets.QLabel(frame) + image.setMinimumSize(QtCore.QSize(22, 22)) + image.setMaximumSize(QtCore.QSize(22, 22)) + image.setText("") + image.setScaledContents(True) + pixmap = QtGui.QPixmap(get_resource('image_sequence.png')) + image.setPixmap(pixmap) + + self.info = SvgButton( + get_resource('information.svg'), 22, 22, + [self.C_NORMAL, self.C_HOVER], + frame_image_info, False + ) + + expanding_sizePolicy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding + ) + expanding_sizePolicy.setHorizontalStretch(0) + expanding_sizePolicy.setVerticalStretch(0) + + layout.addWidget(image, alignment=QtCore.Qt.AlignCenter) + layout.addWidget(self.info, alignment=QtCore.Qt.AlignCenter) + + layout_main.addWidget(frame_image_info) + + # Name + representation + self.name = QtWidgets.QLabel(frame) + self.frames = QtWidgets.QLabel(frame) + self.ext = QtWidgets.QLabel(frame) + + self.name.setFont(font) + self.frames.setFont(font) + self.ext.setFont(font) + + self.frames.setStyleSheet('padding-left:3px;') + + expanding_sizePolicy.setHeightForWidth(self.name.sizePolicy().hasHeightForWidth()) + + frame_name_repre = QtWidgets.QFrame(frame) + + self.frames.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) + self.ext.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) + self.name.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) + + layout = QtWidgets.QHBoxLayout(frame_name_repre) + layout.setSpacing(0) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self.name, alignment=QtCore.Qt.AlignLeft) + layout.addWidget(self.frames, alignment=QtCore.Qt.AlignLeft) + layout.addWidget(self.ext, alignment=QtCore.Qt.AlignRight) + + frame_name_repre.setSizePolicy( + QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding + ) + + # Frames + icons + frame_repre_icons = QtWidgets.QFrame(frame) + + label_repre = QtWidgets.QLabel() + label_repre.setText('Representation:') + + self.input_repre = QtWidgets.QLineEdit() + self.input_repre.setMaximumWidth(50) + + frame_icons = QtWidgets.QFrame(frame_repre_icons) + + self.preview = SvgButton( + get_resource('preview.svg'), 64, 18, + [self.C_NORMAL, self.C_HOVER, self.C_ACTIVE, self.C_ACTIVE_HOVER], + frame_icons + ) + + self.thumbnail = SvgButton( + get_resource('thumbnail.svg'), 84, 18, + [self.C_NORMAL, self.C_HOVER, self.C_ACTIVE, self.C_ACTIVE_HOVER], + frame_icons + ) + + layout = QtWidgets.QHBoxLayout(frame_icons) + layout.setSpacing(6) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self.thumbnail) + layout.addWidget(self.preview) + + layout = QtWidgets.QHBoxLayout(frame_repre_icons) + layout.setSpacing(0) + layout.setContentsMargins(0, 0, 0, 0) + + layout.addWidget(label_repre, alignment=QtCore.Qt.AlignLeft) + layout.addWidget(self.input_repre, alignment=QtCore.Qt.AlignLeft) + layout.addWidget(frame_icons, alignment=QtCore.Qt.AlignRight) + + frame_middle = QtWidgets.QFrame(frame) + + layout = QtWidgets.QVBoxLayout(frame_middle) + layout.setSpacing(0) + layout.setContentsMargins(4, 0, 4, 0) + layout.addWidget(frame_name_repre) + layout.addWidget(frame_repre_icons) + + layout.setStretchFactor(frame_name_repre, 1) + layout.setStretchFactor(frame_repre_icons, 1) + + layout_main.addWidget(frame_middle) + + self.remove = SvgButton( + get_resource('trash.svg'), 22, 22, + [self.C_NORMAL, self.C_HOVER], + frame, False + ) + + layout_main.addWidget(self.remove) + + layout = QtWidgets.QVBoxLayout(self) + layout.setSpacing(0) + layout.setContentsMargins(2, 2, 2, 2) + layout.addWidget(frame) + + self.preview.setToolTip('Mark component as Preview') + self.thumbnail.setToolTip('Component will be selected as thumbnail') + + # self.frame.setStyleSheet("border: 1px solid black;") + + def set_context(self, data): + + self.remove.clicked.connect(self._remove) + name = data['name'] + representation = data['representation'] + ext = data['ext'] + file_info = data['file_info'] + thumb = data['thumb'] + prev = data['prev'] + info = data['info'] + + self.name.setText(name) + self.input_repre.setText(representation) + self.ext.setText('( {} )'.format(ext)) + if file_info is None: + self.file_info.setVisible(False) + else: + self.file_info.setText('[{}]'.format(file_info)) + + # self.thumbnail.setVisible(thumb) + # self.preview.setVisible(prev) + + def _remove(self): + self.signal_remove.emit(self.parent_item) diff --git a/pype/tools/standalonepublish/widgets/widget_component_item.py b/pype/tools/standalonepublish/widgets/widget_component_item.py new file mode 100644 index 0000000000..1236a439c0 --- /dev/null +++ b/pype/tools/standalonepublish/widgets/widget_component_item.py @@ -0,0 +1,15 @@ +from . import QtWidgets +from . import ComponentWidget + + +class ComponentItem(QtWidgets.QTreeWidgetItem): + def __init__(self, parent, data): + super().__init__(parent) + self.in_data = data + self._widget = ComponentWidget(self) + self._widget.set_context(data) + + self.treeWidget().setItemWidget(self, 0, self._widget) + + def double_clicked(*args): + pass diff --git a/pype/tools/standalonepublish/widgets/widget_drop_data.py b/pype/tools/standalonepublish/widgets/widget_drop_data.py new file mode 100644 index 0000000000..96294ea99e --- /dev/null +++ b/pype/tools/standalonepublish/widgets/widget_drop_data.py @@ -0,0 +1,41 @@ +import os +import logging +import clique +from . import QtWidgets, QtCore, QtGui + + +class DropDataWidget(QtWidgets.QWidget): + + def __init__(self, parent): + '''Initialise DataDropZone widget.''' + super().__init__(parent) + + layout = QtWidgets.QVBoxLayout(self) + + bottomCenterAlignment = QtCore.Qt.AlignBottom | QtCore.Qt.AlignHCenter + topCenterAlignment = QtCore.Qt.AlignTop | QtCore.Qt.AlignHCenter + + self._label = QtWidgets.QLabel('Drop files here') + layout.addWidget( + self._label, + alignment=bottomCenterAlignment + ) + + self._browseButton = QtWidgets.QPushButton('Browse') + self._browseButton.setToolTip('Browse for file(s).') + layout.addWidget( + self._browseButton, alignment=topCenterAlignment + ) + + def paintEvent(self, event): + super().paintEvent(event) + painter = QtGui.QPainter(self) + pen = QtGui.QPen() + pen.setWidth(1); + pen.setBrush(QtCore.Qt.darkGray); + pen.setStyle(QtCore.Qt.DashLine); + painter.setPen(pen) + painter.drawRect( + 10, 10, + self.rect().width()-15, self.rect().height()-15 + ) diff --git a/pype/tools/standalonepublish/widgets/widget_drop_files.py b/pype/tools/standalonepublish/widgets/widget_drop_files.py new file mode 100644 index 0000000000..0b2241e465 --- /dev/null +++ b/pype/tools/standalonepublish/widgets/widget_drop_files.py @@ -0,0 +1,273 @@ +import os +import clique +from . import QtWidgets, QtCore +from . import ComponentItem, TreeComponents, DropDataWidget + + +class DropDataFrame(QtWidgets.QFrame): + # signal_dropped = QtCore.Signal(object) + + def __init__(self, parent): + super().__init__() + + self.items = [] + + self.setAcceptDrops(True) + layout = QtWidgets.QVBoxLayout(self) + + self.tree_widget = TreeComponents(self) + layout.addWidget(self.tree_widget) + + self.drop_widget = DropDataWidget(self) + layout.addWidget(self.drop_widget) + + self._refresh_view() + + def dragEnterEvent(self, event): + event.setDropAction(QtCore.Qt.CopyAction) + event.accept() + + def dragLeaveEvent(self, event): + event.accept() + + def dropEvent(self, event): + paths = self._processMimeData(event.mimeData()) + if paths: + self._add_components(paths) + event.accept() + + def _processMimeData(self, mimeData): + paths = [] + + if not mimeData.hasUrls(): + print('Dropped invalid file/folder') + return paths + + for path in mimeData.urls(): + local_path = path.toLocalFile() + if os.path.isfile(local_path) or os.path.isdir(local_path): + paths.append(local_path) + else: + print('Invalid input: "{}"'.format(local_path)) + + return paths + + def _add_components(self, paths): + components = self._process_paths(paths) + if not components: + return + for component in components: + self._add_item(component) + + def _add_item(self, data): + # Assign to self so garbage collector wont remove the component + # during initialization + self.new_component = ComponentItem(self.tree_widget, data) + self.new_component._widget.signal_remove.connect(self._remove_item) + self.tree_widget.addTopLevelItem(self.new_component) + self.items.append(self.new_component) + self.new_component = None + + self._refresh_view() + + def _remove_item(self, item): + root = self.tree_widget.invisibleRootItem() + (item.parent() or root).removeChild(item) + self.items.remove(item) + self._refresh_view() + + def _refresh_view(self): + _bool = len(self.items) == 0 + + self.tree_widget.setVisible(not _bool) + self.drop_widget.setVisible(_bool) + + def _process_paths(self, in_paths): + paths = self._get_all_paths(in_paths) + collections, remainders = clique.assemble(paths) + for collection in collections: + self._process_collection(collection) + for remainder in remainders: + self._process_remainder(remainder) + + def _get_all_paths(self, paths): + output_paths = [] + for path in paths: + path = os.path.normpath(path) + if os.path.isfile(path): + output_paths.append(path) + elif os.path.isdir(path): + s_paths = [] + for s_item in os.listdir(path): + s_path = os.path.sep.join([path, s_item]) + s_paths.append(s_path) + output_paths.extend(self._get_all_paths(s_paths)) + else: + print('Invalid path: "{}"'.format(path)) + return output_paths + + def _process_collection(self, collection): + file_base = os.path.basename(collection.head) + folder_path = os.path.dirname(collection.head) + if file_base[-1] in ['.']: + file_base = file_base[:-1] + file_ext = collection.tail + repr_name = file_ext.replace('.', '') + range = self._get_ranges(collection.indexes) + thumb = False + if file_ext in ['.jpeg']: + thumb = True + + prev = False + if file_ext in ['.jpeg']: + prev = True + + files = [] + for file in os.listdir(folder_path): + if file.startswith(file_base) and file.endswith(file_ext): + files.append(os.path.sep.join([folder_path, file])) + info = {} + + data = { + 'files': files, + 'name': file_base, + 'ext': file_ext, + 'file_info': range, + 'representation': repr_name, + 'folder_path': folder_path, + 'icon': 'sequence', + 'thumb': thumb, + 'prev': prev, + 'is_sequence': True, + 'info': info + } + self._process_data(data) + + def _get_ranges(self, indexes): + if len(indexes) == 1: + return str(indexes[0]) + ranges = [] + first = None + last = None + for index in indexes: + if first is None: + first = index + last = index + elif (last+1) == index: + last = index + else: + if first == last: + range = str(first) + else: + range = '{}-{}'.format(first, last) + ranges.append(range) + first = index + last = index + if first == last: + range = str(first) + else: + range = '{}-{}'.format(first, last) + ranges.append(range) + return ', '.join(ranges) + + def _process_remainder(self, remainder): + filename = os.path.basename(remainder) + folder_path = os.path.dirname(remainder) + file_base, file_ext = os.path.splitext(filename) + repr_name = file_ext.replace('.', '') + file_info = None + thumb = False + if file_ext in ['.jpeg']: + thumb = True + + prev = False + if file_ext in ['.jpeg']: + prev = True + + files = [] + files.append(remainder) + + info = {} + + data = { + 'files': files, + 'name': file_base, + 'ext': file_ext, + 'file_info': file_info, + 'representation': repr_name, + 'folder_path': folder_path, + 'icon': 'sequence', + 'thumb': thumb, + 'prev': prev, + 'is_sequence': False, + 'info': info + } + + self._process_data(data) + + def _process_data(self, data): + found = False + for item in self.items: + if data['ext'] != item.in_data['ext']: + continue + if data['folder_path'] != item.in_data['folder_path']: + continue + + new_is_seq = data['is_sequence'] + ex_is_seq = item.in_data['is_sequence'] + + # If both are single files + if not new_is_seq and not ex_is_seq: + if data['name'] != item.in_data['name']: + continue + found = True + break + # If new is sequence and ex is single file + elif new_is_seq and not ex_is_seq: + if data['name'] not in item.in_data['name']: + continue + ex_file = item.in_data['files'][0] + found = True + # If file is one of inserted sequence + if ex_file in data['files']: + self._remove_item(item) + self._add_item(data) + break + # if file is missing in inserted sequence + paths = data['files'] + paths.append(ex_file) + collections, remainders = clique.assemble(paths) + self._process_collection(collections[0]) + break + # If new is single file existing is sequence + elif not new_is_seq and ex_is_seq: + if item.in_data['name'] not in data['name']: + continue + new_file = data['files'][0] + found = True + if new_file in item.in_data['files']: + break + paths = item.in_data['files'] + paths.append(new_file) + collections, remainders = clique.assemble(paths) + self._remove_item(item) + self._process_collection(collections[0]) + + break + # If both are sequence + else: + if data['name'] != item.in_data['name']: + continue + found = True + ex_files = item.in_data['files'] + for file in data['files']: + if file not in ex_files: + ex_files.append(file) + paths = list(set(ex_files)) + collections, remainders = clique.assemble(paths) + self._remove_item(item) + self._process_collection(collections[0]) + break + + if found is False: + self._add_item(data) diff --git a/pype/tools/standalonepublish/widgets/widget_tree_components.py b/pype/tools/standalonepublish/widgets/widget_tree_components.py new file mode 100644 index 0000000000..76e5a9bce0 --- /dev/null +++ b/pype/tools/standalonepublish/widgets/widget_tree_components.py @@ -0,0 +1,14 @@ +from . import QtCore, QtGui, QtWidgets + + +class TreeComponents(QtWidgets.QTreeWidget): + def __init__(self, parent): + super().__init__(parent) + + self.invisibleRootItem().setFlags(QtCore.Qt.ItemIsEnabled) + self.setIndentation(28) + self.headerItem().setText(0, 'Components') + + self.setRootIsDecorated(False) + + self.itemDoubleClicked.connect(lambda i, c: i.double_clicked(c)) From 01b34cebd3eb07946f3b7fddc6c21d739d5260a8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 16 Apr 2019 14:25:38 +0200 Subject: [PATCH 07/65] added basic app --- pype/tools/standalonepublish/app.py | 155 ++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 pype/tools/standalonepublish/app.py diff --git a/pype/tools/standalonepublish/app.py b/pype/tools/standalonepublish/app.py new file mode 100644 index 0000000000..ff548db30f --- /dev/null +++ b/pype/tools/standalonepublish/app.py @@ -0,0 +1,155 @@ +import os +import sys +import json +from subprocess import Popen +from pype import lib as pypelib +from avalon.vendor.Qt import QtWidgets, QtCore +from avalon import api, style, schema +from avalon.tools import lib as parentlib +from .widgets import * +# Move this to pype lib? +from avalon.tools.libraryloader.io_nonsingleton import DbConnector + + +class Window(QtWidgets.QDialog): + _db = DbConnector() + _jobs = {} + WIDTH = 1000 + HEIGHT = 500 + NOT_SELECTED = '< Nothing is selected >' + + def __init__(self, parent=None): + super(Window, self).__init__(parent) + self._db.install() + + self.setWindowTitle("Standalone Publish") + self.setFocusPolicy(QtCore.Qt.StrongFocus) + self.setAttribute(QtCore.Qt.WA_DeleteOnClose) + self.setStyleSheet(style.load_stylesheet()) + + # Validators + self.valid_parent = False + + # statusbar - added under asset_widget + label_message = QtWidgets.QLabel() + label_message.setFixedHeight(20) + + # assets widget + widget_assets_wrap = QtWidgets.QWidget() + widget_assets_wrap.setContentsMargins(0, 0, 0, 0) + widget_assets = AssetWidget(self) + + layout_assets = QtWidgets.QVBoxLayout(widget_assets_wrap) + layout_assets.addWidget(widget_assets) + layout_assets.addWidget(label_message) + + + # family widget + widget_family = FamilyWidget(self) + + # components widget + widget_components = DropDataFrame(self) + + # Body + body = QtWidgets.QSplitter() + body.setContentsMargins(0, 0, 0, 0) + body.setSizePolicy( + QtWidgets.QSizePolicy.Expanding, + QtWidgets.QSizePolicy.Expanding + ) + body.setOrientation(QtCore.Qt.Horizontal) + body.addWidget(widget_assets_wrap) + body.addWidget(widget_family) + body.addWidget(widget_components) + body.setStretchFactor(body.indexOf(widget_assets_wrap), 2) + body.setStretchFactor(body.indexOf(widget_family), 2) + body.setStretchFactor(body.indexOf(widget_components), 3) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(body) + + self.resize(self.WIDTH, self.HEIGHT) + + # signals + widget_assets.selection_changed.connect(self.on_asset_changed) + + self.label_message = label_message + self.widget_assets = widget_assets + self.widget_family = widget_family + + self.echo("Connected to Database") + + # on start + self.on_start() + + @property + def db(self): + return self._db + + def on_start(self): + # Refresh asset input in Family widget + self.on_asset_changed() + + def get_avalon_parent(self, entity): + parent_id = entity['data']['visualParent'] + parents = [] + if parent_id is not None: + parent = self.db.find_one({'_id': parent_id}) + parents.extend(self.get_avalon_parent(parent)) + parents.append(parent['name']) + return parents + + def echo(self, message): + self.label_message.setText(str(message)) + QtCore.QTimer.singleShot(5000, lambda: self.label_message.setText("")) + + def on_asset_changed(self): + """Callback on asset selection changed + + This updates the task view. + + """ + selected = self.widget_assets.get_selected_assets() + if len(selected) == 1: + self.valid_parent = True + asset = self.db.find_one({"_id": selected[0], "type": "asset"}) + self.widget_family.change_asset(asset['name']) + else: + self.valid_parent = False + self.widget_family.change_asset(self.NOT_SELECTED) + self.widget_family.on_data_changed() + + +def show(parent=None, debug=False, context=None): + """Display Loader GUI + + Arguments: + debug (bool, optional): Run loader in debug-mode, + defaults to False + + """ + + try: + module.window.close() + del module.window + except (RuntimeError, AttributeError): + pass + + with parentlib.application(): + window = Window(parent, context) + window.show() + + module.window = window + + +def cli(args): + import argparse + parser = argparse.ArgumentParser() + parser.add_argument("project") + parser.add_argument("asset") + + args = parser.parse_args(args) + # project = args.project + # asset = args.asset + + show() From ba47be80874b5e3fddf457876eac6cde5e77bc52 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 16 Apr 2019 14:28:28 +0200 Subject: [PATCH 08/65] added forgotten Task model in asset widgets --- .../standalonepublish/widgets/__init__.py | 1 + .../widgets/model_tasks_template.py | 65 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 pype/tools/standalonepublish/widgets/model_tasks_template.py diff --git a/pype/tools/standalonepublish/widgets/__init__.py b/pype/tools/standalonepublish/widgets/__init__.py index f4f06448a5..94811ab298 100644 --- a/pype/tools/standalonepublish/widgets/__init__.py +++ b/pype/tools/standalonepublish/widgets/__init__.py @@ -15,6 +15,7 @@ from .model_tree import TreeModel from .model_asset import AssetModel from .model_filter_proxy_exact_match import ExactMatchesFilterProxyModel from .model_filter_proxy_recursive_sort import RecursiveSortFilterProxyModel +from .model_tasks_template import TasksTemplateModel from .model_tree_view_deselectable import DeselectableTreeView from .widget_asset_view import AssetView diff --git a/pype/tools/standalonepublish/widgets/model_tasks_template.py b/pype/tools/standalonepublish/widgets/model_tasks_template.py new file mode 100644 index 0000000000..4af3b9eea7 --- /dev/null +++ b/pype/tools/standalonepublish/widgets/model_tasks_template.py @@ -0,0 +1,65 @@ +from . import QtCore, TreeModel +from . import Node +from . import awesome, style + + +class TasksTemplateModel(TreeModel): + """A model listing the tasks combined for a list of assets""" + + COLUMNS = ["Tasks"] + + def __init__(self): + super(TasksTemplateModel, self).__init__() + self.selectable = False + self._icons = { + "__default__": awesome.icon("fa.folder-o", + color=style.colors.default) + } + + def set_tasks(self, tasks): + """Set assets to track by their database id + + Arguments: + asset_ids (list): List of asset ids. + + """ + + self.clear() + + # let cleared task view if no tasks are available + if len(tasks) == 0: + return + + self.beginResetModel() + + icon = self._icons["__default__"] + for task in tasks: + node = Node({ + "Tasks": task, + "icon": icon + }) + + self.add_child(node) + + self.endResetModel() + + def flags(self, index): + if self.selectable is False: + return QtCore.Qt.ItemIsEnabled + else: + return ( + QtCore.Qt.ItemIsEnabled | + QtCore.Qt.ItemIsSelectable + ) + + def data(self, index, role): + + if not index.isValid(): + return + + # Add icon to the first column + if role == QtCore.Qt.DecorationRole: + if index.column() == 0: + return index.internalPointer()['icon'] + + return super(TasksTemplateModel, self).data(index, role) From b6a1fb8209043f5b510adb0aba4f58eac21f31db Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Apr 2019 09:42:30 +0200 Subject: [PATCH 09/65] added icons to resources --- .../tools/standalonepublish/resources/file.png | Bin 0 -> 803 bytes .../standalonepublish/resources/files.png | Bin 0 -> 484 bytes .../standalonepublish/resources/houdini.png | Bin 0 -> 262950 bytes .../standalonepublish/resources/image_file.png | Bin 0 -> 5118 bytes .../resources/image_files.png | Bin 0 -> 8560 bytes .../resources/image_sequence.png | Bin 5092 -> 0 bytes .../tools/standalonepublish/resources/maya.png | Bin 0 -> 41557 bytes .../tools/standalonepublish/resources/nuke.png | Bin 0 -> 49012 bytes .../standalonepublish/resources/premiere.png | Bin 0 -> 20121 bytes .../standalonepublish/resources/video_file.png | Bin 0 -> 120 bytes 10 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 pype/tools/standalonepublish/resources/file.png create mode 100644 pype/tools/standalonepublish/resources/files.png create mode 100644 pype/tools/standalonepublish/resources/houdini.png create mode 100644 pype/tools/standalonepublish/resources/image_file.png create mode 100644 pype/tools/standalonepublish/resources/image_files.png delete mode 100644 pype/tools/standalonepublish/resources/image_sequence.png create mode 100644 pype/tools/standalonepublish/resources/maya.png create mode 100644 pype/tools/standalonepublish/resources/nuke.png create mode 100644 pype/tools/standalonepublish/resources/premiere.png create mode 100644 pype/tools/standalonepublish/resources/video_file.png diff --git a/pype/tools/standalonepublish/resources/file.png b/pype/tools/standalonepublish/resources/file.png new file mode 100644 index 0000000000000000000000000000000000000000..7a830ad13305a4dbaf73489ed9f373f5382243c0 GIT binary patch literal 803 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD3?#3*wSy!+2l#}z0_p!F3`DHD_7v#tfRZ4; zUDcV|DfRTh`5BL%&h#P@`jH7S&J4gS-O11s&(r( zY}~YY%hnw`_w2oP{l?8(cke%Z@%qh&PhY-$|MC0J-+!HdPj>>%W=!&ScL{i$=d%aM zZt`?-49U3n_SW$rCPx9b2XT?iOi5zKO`ObHT}elJx(@C6f4So3%AoQaFB{m`zI$Er zH~#B85t%B+ec}6V?5Ht0p~bv)*(zD*m8D7l{&Xvf<+cS~pK)JAO!DWHq;K7XCcnQmtP_R_Bv-WZSWV&Q5R@v)N*{l zAdvGoUz@vOL6cYo1B-P;Bd5cLZjl`fEY%i`ybiAzR6j6!$R@EcGCUM?n!q4%?N1X& z!-pH%Cm1;VYnlWaHoOyD!oV|6jXQ~HCBtEHPbPK`Emn~q?-^dN4`*Z*32;Co z8tqwcr2gp8sW<7$JnHvmRiPi7j*H>-jl#Qi_MYY;Gp00i_>zopr0D++3 A5&!@I literal 0 HcmV?d00001 diff --git a/pype/tools/standalonepublish/resources/files.png b/pype/tools/standalonepublish/resources/files.png new file mode 100644 index 0000000000000000000000000000000000000000..f6f89fe14992cde5d9509c2606efe946c1a9e95c GIT binary patch literal 484 zcmVsb@n$`BycGxbTj??esuHn0x-zMy6fcmeDIGr%

n4igvKbDOy^aUL%8G~`^%3w6m;|=edlB&^ zXNJwBvu=~lO3zkHdwf;99pEl7)#UUwJA4JK72!=_BQ?l2@SO9P+(wIP`i&iPew~vk zY&9bG)s>d8F7U7_nZg>Gp`_2i?TXw7U?B%3*0#;XJ*cdj3RcheW zuaepUuJz+O6Xdn}z9nxSxHSl7X*~-(Z3+8FRoA%7|9|XZ^apsG?JOdGsCR(tBZBoJ z;#&&q0!Ia~N5v%1Ne$pJa0NJwh)-!v3LC}@3&3)*-U70)OEE)E9WKcXxkLb{R~fMx aa*4m35Kv^)H)-Dh00008qo>> literal 0 HcmV?d00001 diff --git a/pype/tools/standalonepublish/resources/houdini.png b/pype/tools/standalonepublish/resources/houdini.png new file mode 100644 index 0000000000000000000000000000000000000000..11cfa46dce2afe609a525b9b741f612445d8fe98 GIT binary patch literal 262950 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 l89UyV%&>*Q|1Nzx^yCixAVVXrD*fNYN^>s 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 diff --git a/pype/tools/standalonepublish/resources/maya.png b/pype/tools/standalonepublish/resources/maya.png new file mode 100644 index 0000000000000000000000000000000000000000..e84a6a3742f325769d6cf619045d17107d925f3c GIT binary patch literal 41557 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 literal 0 HcmV?d00001 diff --git a/pype/tools/standalonepublish/resources/nuke.png b/pype/tools/standalonepublish/resources/nuke.png new file mode 100644 index 0000000000000000000000000000000000000000..423445409618d59ff822dc09e1dac2e4d3e241aa GIT binary patch literal 49012 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

n)d3@oZpYFsmbJyGIDGW; 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 R3jP~ZQC3Z+PRcy&{{SdSemwvH literal 0 HcmV?d00001 diff --git a/pype/tools/standalonepublish/resources/premiere.png b/pype/tools/standalonepublish/resources/premiere.png new file mode 100644 index 0000000000000000000000000000000000000000..eb5b3d1ba2adb45f6feb2c35f19bc1bc49a22b93 GIT binary patch literal 20121 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 Date: Wed, 17 Apr 2019 09:44:30 +0200 Subject: [PATCH 10/65] icons are loaded to widget --- .../standalonepublish/widgets/__init__.py | 2 ++ .../widgets/widget_component.py | 28 +++++++++++++------ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/pype/tools/standalonepublish/widgets/__init__.py b/pype/tools/standalonepublish/widgets/__init__.py index 94811ab298..21f703d762 100644 --- a/pype/tools/standalonepublish/widgets/__init__.py +++ b/pype/tools/standalonepublish/widgets/__init__.py @@ -29,3 +29,5 @@ from .widget_tree_components import TreeComponents from .widget_component_item import ComponentItem from .widget_drop_files import DropDataFrame + +from .widget_components import ComponentsWidget diff --git a/pype/tools/standalonepublish/widgets/widget_component.py b/pype/tools/standalonepublish/widgets/widget_component.py index f7248e31c1..5fa5da343c 100644 --- a/pype/tools/standalonepublish/widgets/widget_component.py +++ b/pype/tools/standalonepublish/widgets/widget_component.py @@ -40,13 +40,11 @@ class ComponentWidget(QtWidgets.QFrame): layout.setSpacing(2) layout.setContentsMargins(2, 2, 2, 2) - image = QtWidgets.QLabel(frame) - image.setMinimumSize(QtCore.QSize(22, 22)) - image.setMaximumSize(QtCore.QSize(22, 22)) - image.setText("") - image.setScaledContents(True) - pixmap = QtGui.QPixmap(get_resource('image_sequence.png')) - image.setPixmap(pixmap) + self.icon = QtWidgets.QLabel(frame) + self.icon.setMinimumSize(QtCore.QSize(22, 22)) + self.icon.setMaximumSize(QtCore.QSize(22, 22)) + self.icon.setText("") + self.icon.setScaledContents(True) self.info = SvgButton( get_resource('information.svg'), 22, 22, @@ -60,7 +58,7 @@ class ComponentWidget(QtWidgets.QFrame): expanding_sizePolicy.setHorizontalStretch(0) expanding_sizePolicy.setVerticalStretch(0) - layout.addWidget(image, alignment=QtCore.Qt.AlignCenter) + layout.addWidget(self.icon, alignment=QtCore.Qt.AlignCenter) layout.addWidget(self.info, alignment=QtCore.Qt.AlignCenter) layout_main.addWidget(frame_image_info) @@ -173,6 +171,20 @@ class ComponentWidget(QtWidgets.QFrame): thumb = data['thumb'] prev = data['prev'] info = data['info'] + icon = data['icon'] + + resource = None + if icon is not None: + resource = get_resource('{}.png'.format(icon)) + + if resource is None or not os.path.isfile(resource): + if data['is_sequence']: + resource = get_resource('files.png') + else: + resource = get_resource('file.png') + + pixmap = QtGui.QPixmap(resource) + self.icon.setPixmap(pixmap) self.name.setText(name) self.input_repre.setText(representation) From ffa7f7b5dbf5454abec3f4514d7445adc27e1201 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Apr 2019 09:45:11 +0200 Subject: [PATCH 11/65] added components wiget that holds drop frame and browse and publish buttons --- .../widgets/widget_components.py | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 pype/tools/standalonepublish/widgets/widget_components.py diff --git a/pype/tools/standalonepublish/widgets/widget_components.py b/pype/tools/standalonepublish/widgets/widget_components.py new file mode 100644 index 0000000000..18d4e480d5 --- /dev/null +++ b/pype/tools/standalonepublish/widgets/widget_components.py @@ -0,0 +1,76 @@ +from . import QtWidgets, QtCore, QtGui +from . import DropDataFrame + +class ComponentsWidget(QtWidgets.QWidget): + def __init__(self, parent): + super().__init__() + body = QtWidgets.QWidget() + self.parent_widget = parent + self.drop_frame = DropDataFrame(self) + + buttons = QtWidgets.QWidget() + + layout = QtWidgets.QHBoxLayout(buttons) + + self.btn_browse = QtWidgets.QPushButton('Browse') + self.btn_browse.setToolTip('Browse for file(s).') + + self.btn_publish = QtWidgets.QPushButton('Publish') + self.btn_publish.setToolTip('Publishes data.') + + layout.addWidget(self.btn_browse, alignment=QtCore.Qt.AlignLeft) + layout.addWidget(self.btn_publish, alignment=QtCore.Qt.AlignRight) + + layout = QtWidgets.QVBoxLayout(body) + layout.setSpacing(0) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self.drop_frame) + layout.addWidget(buttons) + + layout = QtWidgets.QVBoxLayout(self) + layout.setSpacing(0) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(body) + + self.btn_browse.clicked.connect(self._browse) + # self.setStyleSheet("border: 1px solid black;") + + def set_valid(self, in_bool): + self.btn_publish.setEnabled(in_bool) + + def set_valid_components(self, in_bool): + self.parent_widget.set_valid_components(in_bool) + + def _browse(self): + options = [ + QtWidgets.QFileDialog.DontResolveSymlinks, + QtWidgets.QFileDialog.DontUseNativeDialog + ] + folders = False + if folders: + # browse folders specifics + caption = "Browse folders to publish image sequences" + file_mode = QtWidgets.QFileDialog.Directory + options.append(QtWidgets.QFileDialog.ShowDirsOnly) + else: + # browse files specifics + caption = "Browse files to publish" + file_mode = QtWidgets.QFileDialog.ExistingFiles + + # create the dialog + file_dialog = QtWidgets.QFileDialog(parent=self, caption=caption) + file_dialog.setLabelText(QtWidgets.QFileDialog.Accept, "Select") + file_dialog.setLabelText(QtWidgets.QFileDialog.Reject, "Cancel") + file_dialog.setFileMode(file_mode) + + # set the appropriate options + for option in options: + file_dialog.setOption(option) + + # browse! + if not file_dialog.exec_(): + return + + # process the browsed files/folders for publishing + paths = file_dialog.selectedFiles() + self.drop_frame._process_paths(paths) From 9a827fbaaa03c9b3eb86bc47354cab11e362e4b9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Apr 2019 09:45:39 +0200 Subject: [PATCH 12/65] assets widget can collect data --- pype/tools/standalonepublish/widgets/widget_asset.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pype/tools/standalonepublish/widgets/widget_asset.py b/pype/tools/standalonepublish/widgets/widget_asset.py index 665a5913a0..82b3700dea 100644 --- a/pype/tools/standalonepublish/widgets/widget_asset.py +++ b/pype/tools/standalonepublish/widgets/widget_asset.py @@ -178,6 +178,13 @@ class AssetWidget(QtWidgets.QWidget): def db(self): return self.parent_widget.db + def collect_data(self): + data = { + 'project': self.combo_projects.currentText(), + 'asset': get_active_asset + } + return data + def _set_projects(self): projects = list() for project in self.db.projects(): From a2776d967ab3272a672e2ab71acd7d2763f4dab5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Apr 2019 09:45:54 +0200 Subject: [PATCH 13/65] family widget can collect data --- pype/tools/standalonepublish/widgets/widget_family.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pype/tools/standalonepublish/widgets/widget_family.py b/pype/tools/standalonepublish/widgets/widget_family.py index a0786b358d..4ef7d9bcf8 100644 --- a/pype/tools/standalonepublish/widgets/widget_family.py +++ b/pype/tools/standalonepublish/widgets/widget_family.py @@ -100,6 +100,15 @@ class FamilyWidget(QtWidgets.QWidget): self.refresh() + def collect_data(self): + plugin = self.list_families.currentItem().data(PluginRole) + family = plugin.family.rsplit(".", 1)[-1] + data = { + 'family': family, + 'subset': self.input_subset.text() + } + return data + @property def db(self): return self.parent_widget.db @@ -109,6 +118,7 @@ class FamilyWidget(QtWidgets.QWidget): def _on_state_changed(self, state): self.state['valid'] = state + self.parent_widget.set_valid_family(state) def _build_menu(self, default_names): """Create optional predefined subset names From cfacb279d7eaf5c236c411dbe29bad5e7d8328b5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Apr 2019 09:48:42 +0200 Subject: [PATCH 14/65] added validations into main window --- pype/tools/standalonepublish/app.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/pype/tools/standalonepublish/app.py b/pype/tools/standalonepublish/app.py index ff548db30f..2edc345cbb 100644 --- a/pype/tools/standalonepublish/app.py +++ b/pype/tools/standalonepublish/app.py @@ -14,7 +14,10 @@ from avalon.tools.libraryloader.io_nonsingleton import DbConnector class Window(QtWidgets.QDialog): _db = DbConnector() _jobs = {} - WIDTH = 1000 + valid_family = False + valid_components = False + initialized = False + WIDTH = 1100 HEIGHT = 500 NOT_SELECTED = '< Nothing is selected >' @@ -76,6 +79,7 @@ class Window(QtWidgets.QDialog): self.label_message = label_message self.widget_assets = widget_assets self.widget_family = widget_family + self.widget_components = widget_components self.echo("Connected to Database") @@ -87,8 +91,10 @@ class Window(QtWidgets.QDialog): return self._db def on_start(self): + self.initialized = True # Refresh asset input in Family widget self.on_asset_changed() + self.validation() def get_avalon_parent(self, entity): parent_id = entity['data']['visualParent'] @@ -119,6 +125,26 @@ class Window(QtWidgets.QDialog): self.widget_family.change_asset(self.NOT_SELECTED) self.widget_family.on_data_changed() + def validation(self): + if not self.initialized: + return + valid = self.valid_family and self.valid_components + self.widget_components.set_valid(valid) + + def set_valid_family(self, valid): + self.valid_family = valid + self.validation() + + def set_valid_components(self, valid): + self.valid_components = valid + self.validation() + + def collect_data(self): + data = {} + data.update(self.widget_assets.collect_data()) + data.update(self.widget_family.collect_data()) + + return data def show(parent=None, debug=False, context=None): """Display Loader GUI From fe3d94c203c7dac17a191ee8fb41cb1b782dc43c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Apr 2019 09:53:20 +0200 Subject: [PATCH 15/65] component item buttons are working now --- .../widgets/widget_component.py | 16 +++- .../widgets/widget_drop_files.py | 75 ++++++++++++------- 2 files changed, 62 insertions(+), 29 deletions(-) diff --git a/pype/tools/standalonepublish/widgets/widget_component.py b/pype/tools/standalonepublish/widgets/widget_component.py index 5fa5da343c..2e65bcf1d8 100644 --- a/pype/tools/standalonepublish/widgets/widget_component.py +++ b/pype/tools/standalonepublish/widgets/widget_component.py @@ -1,3 +1,4 @@ +import os from . import QtCore, QtGui, QtWidgets from . import SvgButton from . import get_resource @@ -9,6 +10,8 @@ class ComponentWidget(QtWidgets.QFrame): C_ACTIVE = '#4BB543' C_ACTIVE_HOVER = '#4BF543' signal_remove = QtCore.Signal(object) + signal_thumbnail = QtCore.Signal(object) + signal_preview = QtCore.Signal(object) def __init__(self, parent): super().__init__() @@ -162,8 +165,9 @@ class ComponentWidget(QtWidgets.QFrame): # self.frame.setStyleSheet("border: 1px solid black;") def set_context(self, data): - self.remove.clicked.connect(self._remove) + self.thumbnail.clicked.connect(self._thumbnail_clicked) + self.preview.clicked.connect(self._preview_clicked) name = data['name'] representation = data['representation'] ext = data['ext'] @@ -194,8 +198,14 @@ class ComponentWidget(QtWidgets.QFrame): else: self.file_info.setText('[{}]'.format(file_info)) - # self.thumbnail.setVisible(thumb) - # self.preview.setVisible(prev) + self.thumbnail.setVisible(thumb) + self.preview.setVisible(prev) def _remove(self): self.signal_remove.emit(self.parent_item) + + def _thumbnail_clicked(self): + self.signal_thumbnail.emit(self.parent_item) + + def _preview_clicked(self): + self.signal_preview.emit(self.parent_item) diff --git a/pype/tools/standalonepublish/widgets/widget_drop_files.py b/pype/tools/standalonepublish/widgets/widget_drop_files.py index 0b2241e465..fbeddcbb41 100644 --- a/pype/tools/standalonepublish/widgets/widget_drop_files.py +++ b/pype/tools/standalonepublish/widgets/widget_drop_files.py @@ -1,20 +1,19 @@ import os import clique +from pypeapp import config from . import QtWidgets, QtCore from . import ComponentItem, TreeComponents, DropDataWidget class DropDataFrame(QtWidgets.QFrame): - # signal_dropped = QtCore.Signal(object) - def __init__(self, parent): super().__init__() - + self.parent_widget = parent self.items = [] + self.presets = config.get_presets()['tools']['standalone_publish'] self.setAcceptDrops(True) layout = QtWidgets.QVBoxLayout(self) - self.tree_widget = TreeComponents(self) layout.addWidget(self.tree_widget) @@ -63,13 +62,43 @@ class DropDataFrame(QtWidgets.QFrame): # Assign to self so garbage collector wont remove the component # during initialization self.new_component = ComponentItem(self.tree_widget, data) - self.new_component._widget.signal_remove.connect(self._remove_item) self.tree_widget.addTopLevelItem(self.new_component) + self.new_component.set_context() + + self.new_component._widget.signal_remove.connect(self._remove_item) + self.new_component._widget.signal_preview.connect(self._set_preview) + self.new_component._widget.signal_thumbnail.connect( + self._set_thumbnail + ) self.items.append(self.new_component) self.new_component = None self._refresh_view() + def _set_thumbnail(self, in_item): + checked_item = None + for item in self.items: + if item.is_thumbnail(): + checked_item = item + break + if checked_item is None or checked_item == in_item: + in_item.change_thumbnail() + else: + checked_item.change_thumbnail(False) + in_item.change_thumbnail() + + def _set_preview(self, in_item): + checked_item = None + for item in self.items: + if item.is_preview(): + checked_item = item + break + if checked_item is None or checked_item == in_item: + in_item.change_preview() + else: + checked_item.change_preview(False) + in_item.change_preview() + def _remove_item(self, item): root = self.tree_widget.invisibleRootItem() (item.parent() or root).removeChild(item) @@ -78,10 +107,11 @@ class DropDataFrame(QtWidgets.QFrame): def _refresh_view(self): _bool = len(self.items) == 0 - self.tree_widget.setVisible(not _bool) self.drop_widget.setVisible(_bool) + self.parent_widget.set_valid_components(not _bool) + def _process_paths(self, in_paths): paths = self._get_all_paths(in_paths) collections, remainders = clique.assemble(paths) @@ -114,13 +144,6 @@ class DropDataFrame(QtWidgets.QFrame): file_ext = collection.tail repr_name = file_ext.replace('.', '') range = self._get_ranges(collection.indexes) - thumb = False - if file_ext in ['.jpeg']: - thumb = True - - prev = False - if file_ext in ['.jpeg']: - prev = True files = [] for file in os.listdir(folder_path): @@ -135,9 +158,6 @@ class DropDataFrame(QtWidgets.QFrame): 'file_info': range, 'representation': repr_name, 'folder_path': folder_path, - 'icon': 'sequence', - 'thumb': thumb, - 'prev': prev, 'is_sequence': True, 'info': info } @@ -176,13 +196,6 @@ class DropDataFrame(QtWidgets.QFrame): file_base, file_ext = os.path.splitext(filename) repr_name = file_ext.replace('.', '') file_info = None - thumb = False - if file_ext in ['.jpeg']: - thumb = True - - prev = False - if file_ext in ['.jpeg']: - prev = True files = [] files.append(remainder) @@ -196,9 +209,6 @@ class DropDataFrame(QtWidgets.QFrame): 'file_info': file_info, 'representation': repr_name, 'folder_path': folder_path, - 'icon': 'sequence', - 'thumb': thumb, - 'prev': prev, 'is_sequence': False, 'info': info } @@ -206,6 +216,19 @@ class DropDataFrame(QtWidgets.QFrame): self._process_data(data) def _process_data(self, data): + ext = data['ext'] + icon = 'default' + for ico, exts in self.presets['extensions'].items(): + if ext in exts: + icon = ico + break + # Add 's' to icon_name if is sequence (image -> images) + if data['is_sequence']: + icon += 's' + data['icon'] = icon + data['thumb'] = ext in self.presets['thumbnailable'] + data['prev'] = ext in self.presets['extensions']['video_file'] + found = False for item in self.items: if data['ext'] != item.in_data['ext']: From b2b4c031c143a938b4aa6c0674827a7ca87d7dc9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Apr 2019 09:54:08 +0200 Subject: [PATCH 16/65] component item can coverup component widget changes --- .../widgets/widget_component_item.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/pype/tools/standalonepublish/widgets/widget_component_item.py b/pype/tools/standalonepublish/widgets/widget_component_item.py index 1236a439c0..dcf66a9c21 100644 --- a/pype/tools/standalonepublish/widgets/widget_component_item.py +++ b/pype/tools/standalonepublish/widgets/widget_component_item.py @@ -5,11 +5,25 @@ from . import ComponentWidget class ComponentItem(QtWidgets.QTreeWidgetItem): def __init__(self, parent, data): super().__init__(parent) + self.parent_widget = parent self.in_data = data - self._widget = ComponentWidget(self) - self._widget.set_context(data) + def set_context(self): + self._widget = ComponentWidget(self) + self._widget.set_context(self.in_data) self.treeWidget().setItemWidget(self, 0, self._widget) + def is_thumbnail(self): + return self._widget.thumbnail.checked + + def change_thumbnail(self, hover=True): + self._widget.thumbnail.change_checked(hover) + + def is_preview(self): + return self._widget.preview.checked + + def change_preview(self, hover=True): + self._widget.preview.change_checked(hover) + def double_clicked(*args): pass From b3c15a16c93a2b39dec1d0a222eebc873fdd5d6e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Apr 2019 09:54:29 +0200 Subject: [PATCH 17/65] removed browse button from drop zone --- .../standalonepublish/widgets/widget_drop_data.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/pype/tools/standalonepublish/widgets/widget_drop_data.py b/pype/tools/standalonepublish/widgets/widget_drop_data.py index 96294ea99e..74e30b4714 100644 --- a/pype/tools/standalonepublish/widgets/widget_drop_data.py +++ b/pype/tools/standalonepublish/widgets/widget_drop_data.py @@ -12,19 +12,12 @@ class DropDataWidget(QtWidgets.QWidget): layout = QtWidgets.QVBoxLayout(self) - bottomCenterAlignment = QtCore.Qt.AlignBottom | QtCore.Qt.AlignHCenter - topCenterAlignment = QtCore.Qt.AlignTop | QtCore.Qt.AlignHCenter + CenterAlignment = QtCore.Qt.AlignVCenter | QtCore.Qt.AlignHCenter self._label = QtWidgets.QLabel('Drop files here') layout.addWidget( self._label, - alignment=bottomCenterAlignment - ) - - self._browseButton = QtWidgets.QPushButton('Browse') - self._browseButton.setToolTip('Browse for file(s).') - layout.addWidget( - self._browseButton, alignment=topCenterAlignment + alignment=CenterAlignment ) def paintEvent(self, event): From 574c9ef66e9ce1de2d3811c26d463f0e78ecdc13 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Apr 2019 09:54:52 +0200 Subject: [PATCH 18/65] svg button works as it should work --- .../standalonepublish/widgets/button_from_svgs.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pype/tools/standalonepublish/widgets/button_from_svgs.py b/pype/tools/standalonepublish/widgets/button_from_svgs.py index aeeeae5c7a..3a35bcd838 100644 --- a/pype/tools/standalonepublish/widgets/button_from_svgs.py +++ b/pype/tools/standalonepublish/widgets/button_from_svgs.py @@ -87,9 +87,13 @@ class SvgButton(QtWidgets.QFrame): self.mousePressEvent(event) return False - def change_checked(self, in_bool=False): + def change_checked(self, hover=True): if self.checkable: - self.checked = in_bool + self.checked = not self.checked + if hover: + self.hoverEnterEvent() + else: + self.hoverLeaveEvent() def hoverEnterEvent(self, event=None): color = self.c_hover @@ -104,6 +108,4 @@ class SvgButton(QtWidgets.QFrame): self.svg_widget.change_color(color) def mousePressEvent(self, event=None): - self.change_checked(not self.checked) - self.hoverEnterEvent() self.clicked.emit() From be1f361b773798e76624900faa37b5da065309f1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Apr 2019 09:56:49 +0200 Subject: [PATCH 19/65] correct component widget is set in main window --- pype/tools/standalonepublish/app.py | 3 +-- .../widgets/widget_component.py | 24 ++++++++++++------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/pype/tools/standalonepublish/app.py b/pype/tools/standalonepublish/app.py index 2edc345cbb..099566c603 100644 --- a/pype/tools/standalonepublish/app.py +++ b/pype/tools/standalonepublish/app.py @@ -46,12 +46,11 @@ class Window(QtWidgets.QDialog): layout_assets.addWidget(widget_assets) layout_assets.addWidget(label_message) - # family widget widget_family = FamilyWidget(self) # components widget - widget_components = DropDataFrame(self) + widget_components = ComponentsWidget(self) # Body body = QtWidgets.QSplitter() diff --git a/pype/tools/standalonepublish/widgets/widget_component.py b/pype/tools/standalonepublish/widgets/widget_component.py index 2e65bcf1d8..c068d696ba 100644 --- a/pype/tools/standalonepublish/widgets/widget_component.py +++ b/pype/tools/standalonepublish/widgets/widget_component.py @@ -68,20 +68,20 @@ class ComponentWidget(QtWidgets.QFrame): # Name + representation self.name = QtWidgets.QLabel(frame) - self.frames = QtWidgets.QLabel(frame) + self.file_info = QtWidgets.QLabel(frame) self.ext = QtWidgets.QLabel(frame) self.name.setFont(font) - self.frames.setFont(font) + self.file_info.setFont(font) self.ext.setFont(font) - self.frames.setStyleSheet('padding-left:3px;') + self.file_info.setStyleSheet('padding-left:3px;') expanding_sizePolicy.setHeightForWidth(self.name.sizePolicy().hasHeightForWidth()) frame_name_repre = QtWidgets.QFrame(frame) - self.frames.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) + self.file_info.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) self.ext.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) self.name.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) @@ -89,22 +89,31 @@ class ComponentWidget(QtWidgets.QFrame): layout.setSpacing(0) layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.name, alignment=QtCore.Qt.AlignLeft) - layout.addWidget(self.frames, alignment=QtCore.Qt.AlignLeft) + layout.addWidget(self.file_info, alignment=QtCore.Qt.AlignLeft) layout.addWidget(self.ext, alignment=QtCore.Qt.AlignRight) frame_name_repre.setSizePolicy( QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding ) - # Frames + icons + # Repre + icons frame_repre_icons = QtWidgets.QFrame(frame) + frame_repre = QtWidgets.QFrame(frame_repre_icons) + label_repre = QtWidgets.QLabel() label_repre.setText('Representation:') self.input_repre = QtWidgets.QLineEdit() self.input_repre.setMaximumWidth(50) + layout = QtWidgets.QHBoxLayout(frame_repre) + layout.setSpacing(0) + layout.setContentsMargins(0, 0, 0, 0) + + layout.addWidget(label_repre, alignment=QtCore.Qt.AlignLeft) + layout.addWidget(self.input_repre, alignment=QtCore.Qt.AlignLeft) + frame_icons = QtWidgets.QFrame(frame_repre_icons) self.preview = SvgButton( @@ -129,8 +138,7 @@ class ComponentWidget(QtWidgets.QFrame): layout.setSpacing(0) layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(label_repre, alignment=QtCore.Qt.AlignLeft) - layout.addWidget(self.input_repre, alignment=QtCore.Qt.AlignLeft) + layout.addWidget(frame_repre, alignment=QtCore.Qt.AlignLeft) layout.addWidget(frame_icons, alignment=QtCore.Qt.AlignRight) frame_middle = QtWidgets.QFrame(frame) From 1d5a74465f5847ffb1930b4342e4ea995be23c80 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Apr 2019 14:19:57 +0200 Subject: [PATCH 20/65] renamed component to component_item --- .../widgets/widget_component.py | 219 ---------------- .../widgets/widget_component_item.py | 237 ++++++++++++++++-- 2 files changed, 220 insertions(+), 236 deletions(-) delete mode 100644 pype/tools/standalonepublish/widgets/widget_component.py diff --git a/pype/tools/standalonepublish/widgets/widget_component.py b/pype/tools/standalonepublish/widgets/widget_component.py deleted file mode 100644 index c068d696ba..0000000000 --- a/pype/tools/standalonepublish/widgets/widget_component.py +++ /dev/null @@ -1,219 +0,0 @@ -import os -from . import QtCore, QtGui, QtWidgets -from . import SvgButton -from . import get_resource - - -class ComponentWidget(QtWidgets.QFrame): - C_NORMAL = '#777777' - C_HOVER = '#ffffff' - C_ACTIVE = '#4BB543' - C_ACTIVE_HOVER = '#4BF543' - signal_remove = QtCore.Signal(object) - signal_thumbnail = QtCore.Signal(object) - signal_preview = QtCore.Signal(object) - - def __init__(self, parent): - super().__init__() - self.resize(290, 70) - self.setMinimumSize(QtCore.QSize(0, 70)) - self.parent_item = parent - # Font - font = QtGui.QFont() - font.setFamily("DejaVu Sans Condensed") - font.setPointSize(9) - font.setBold(True) - font.setWeight(50) - font.setKerning(True) - - # Main widgets - frame = QtWidgets.QFrame(self) - frame.setFrameShape(QtWidgets.QFrame.StyledPanel) - frame.setFrameShadow(QtWidgets.QFrame.Raised) - - layout_main = QtWidgets.QHBoxLayout(frame) - layout_main.setSpacing(2) - layout_main.setContentsMargins(2, 2, 2, 2) - - # Image + Info - frame_image_info = QtWidgets.QFrame(frame) - - # Layout image info - layout = QtWidgets.QVBoxLayout(frame_image_info) - layout.setSpacing(2) - layout.setContentsMargins(2, 2, 2, 2) - - self.icon = QtWidgets.QLabel(frame) - self.icon.setMinimumSize(QtCore.QSize(22, 22)) - self.icon.setMaximumSize(QtCore.QSize(22, 22)) - self.icon.setText("") - self.icon.setScaledContents(True) - - self.info = SvgButton( - get_resource('information.svg'), 22, 22, - [self.C_NORMAL, self.C_HOVER], - frame_image_info, False - ) - - expanding_sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding - ) - expanding_sizePolicy.setHorizontalStretch(0) - expanding_sizePolicy.setVerticalStretch(0) - - layout.addWidget(self.icon, alignment=QtCore.Qt.AlignCenter) - layout.addWidget(self.info, alignment=QtCore.Qt.AlignCenter) - - layout_main.addWidget(frame_image_info) - - # Name + representation - self.name = QtWidgets.QLabel(frame) - self.file_info = QtWidgets.QLabel(frame) - self.ext = QtWidgets.QLabel(frame) - - self.name.setFont(font) - self.file_info.setFont(font) - self.ext.setFont(font) - - self.file_info.setStyleSheet('padding-left:3px;') - - expanding_sizePolicy.setHeightForWidth(self.name.sizePolicy().hasHeightForWidth()) - - frame_name_repre = QtWidgets.QFrame(frame) - - self.file_info.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) - self.ext.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) - self.name.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) - - layout = QtWidgets.QHBoxLayout(frame_name_repre) - layout.setSpacing(0) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(self.name, alignment=QtCore.Qt.AlignLeft) - layout.addWidget(self.file_info, alignment=QtCore.Qt.AlignLeft) - layout.addWidget(self.ext, alignment=QtCore.Qt.AlignRight) - - frame_name_repre.setSizePolicy( - QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding - ) - - # Repre + icons - frame_repre_icons = QtWidgets.QFrame(frame) - - frame_repre = QtWidgets.QFrame(frame_repre_icons) - - label_repre = QtWidgets.QLabel() - label_repre.setText('Representation:') - - self.input_repre = QtWidgets.QLineEdit() - self.input_repre.setMaximumWidth(50) - - layout = QtWidgets.QHBoxLayout(frame_repre) - layout.setSpacing(0) - layout.setContentsMargins(0, 0, 0, 0) - - layout.addWidget(label_repre, alignment=QtCore.Qt.AlignLeft) - layout.addWidget(self.input_repre, alignment=QtCore.Qt.AlignLeft) - - frame_icons = QtWidgets.QFrame(frame_repre_icons) - - self.preview = SvgButton( - get_resource('preview.svg'), 64, 18, - [self.C_NORMAL, self.C_HOVER, self.C_ACTIVE, self.C_ACTIVE_HOVER], - frame_icons - ) - - self.thumbnail = SvgButton( - get_resource('thumbnail.svg'), 84, 18, - [self.C_NORMAL, self.C_HOVER, self.C_ACTIVE, self.C_ACTIVE_HOVER], - frame_icons - ) - - layout = QtWidgets.QHBoxLayout(frame_icons) - layout.setSpacing(6) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(self.thumbnail) - layout.addWidget(self.preview) - - layout = QtWidgets.QHBoxLayout(frame_repre_icons) - layout.setSpacing(0) - layout.setContentsMargins(0, 0, 0, 0) - - layout.addWidget(frame_repre, alignment=QtCore.Qt.AlignLeft) - layout.addWidget(frame_icons, alignment=QtCore.Qt.AlignRight) - - frame_middle = QtWidgets.QFrame(frame) - - layout = QtWidgets.QVBoxLayout(frame_middle) - layout.setSpacing(0) - layout.setContentsMargins(4, 0, 4, 0) - layout.addWidget(frame_name_repre) - layout.addWidget(frame_repre_icons) - - layout.setStretchFactor(frame_name_repre, 1) - layout.setStretchFactor(frame_repre_icons, 1) - - layout_main.addWidget(frame_middle) - - self.remove = SvgButton( - get_resource('trash.svg'), 22, 22, - [self.C_NORMAL, self.C_HOVER], - frame, False - ) - - layout_main.addWidget(self.remove) - - layout = QtWidgets.QVBoxLayout(self) - layout.setSpacing(0) - layout.setContentsMargins(2, 2, 2, 2) - layout.addWidget(frame) - - self.preview.setToolTip('Mark component as Preview') - self.thumbnail.setToolTip('Component will be selected as thumbnail') - - # self.frame.setStyleSheet("border: 1px solid black;") - - def set_context(self, data): - self.remove.clicked.connect(self._remove) - self.thumbnail.clicked.connect(self._thumbnail_clicked) - self.preview.clicked.connect(self._preview_clicked) - name = data['name'] - representation = data['representation'] - ext = data['ext'] - file_info = data['file_info'] - thumb = data['thumb'] - prev = data['prev'] - info = data['info'] - icon = data['icon'] - - resource = None - if icon is not None: - resource = get_resource('{}.png'.format(icon)) - - if resource is None or not os.path.isfile(resource): - if data['is_sequence']: - resource = get_resource('files.png') - else: - resource = get_resource('file.png') - - pixmap = QtGui.QPixmap(resource) - self.icon.setPixmap(pixmap) - - self.name.setText(name) - self.input_repre.setText(representation) - self.ext.setText('( {} )'.format(ext)) - if file_info is None: - self.file_info.setVisible(False) - else: - self.file_info.setText('[{}]'.format(file_info)) - - self.thumbnail.setVisible(thumb) - self.preview.setVisible(prev) - - def _remove(self): - self.signal_remove.emit(self.parent_item) - - def _thumbnail_clicked(self): - self.signal_thumbnail.emit(self.parent_item) - - def _preview_clicked(self): - self.signal_preview.emit(self.parent_item) diff --git a/pype/tools/standalonepublish/widgets/widget_component_item.py b/pype/tools/standalonepublish/widgets/widget_component_item.py index dcf66a9c21..f7221952af 100644 --- a/pype/tools/standalonepublish/widgets/widget_component_item.py +++ b/pype/tools/standalonepublish/widgets/widget_component_item.py @@ -1,29 +1,232 @@ -from . import QtWidgets -from . import ComponentWidget +import os +from . import QtCore, QtGui, QtWidgets +from . import SvgButton +from . import get_resource -class ComponentItem(QtWidgets.QTreeWidgetItem): - def __init__(self, parent, data): - super().__init__(parent) - self.parent_widget = parent +class ComponentItem(QtWidgets.QFrame): + C_NORMAL = '#777777' + C_HOVER = '#ffffff' + C_ACTIVE = '#4BB543' + C_ACTIVE_HOVER = '#4BF543' + signal_remove = QtCore.Signal(object) + signal_thumbnail = QtCore.Signal(object) + signal_preview = QtCore.Signal(object) + + def __init__(self, parent): + super().__init__() + self.resize(290, 70) + self.setMinimumSize(QtCore.QSize(0, 70)) + self.parent_item = parent + # Font + font = QtGui.QFont() + font.setFamily("DejaVu Sans Condensed") + font.setPointSize(9) + font.setBold(True) + font.setWeight(50) + font.setKerning(True) + + # Main widgets + frame = QtWidgets.QFrame(self) + frame.setFrameShape(QtWidgets.QFrame.StyledPanel) + frame.setFrameShadow(QtWidgets.QFrame.Raised) + + layout_main = QtWidgets.QHBoxLayout(frame) + layout_main.setSpacing(2) + layout_main.setContentsMargins(2, 2, 2, 2) + + # Image + Info + frame_image_info = QtWidgets.QFrame(frame) + + # Layout image info + layout = QtWidgets.QVBoxLayout(frame_image_info) + layout.setSpacing(2) + layout.setContentsMargins(2, 2, 2, 2) + + self.icon = QtWidgets.QLabel(frame) + self.icon.setMinimumSize(QtCore.QSize(22, 22)) + self.icon.setMaximumSize(QtCore.QSize(22, 22)) + self.icon.setText("") + self.icon.setScaledContents(True) + + self.info = SvgButton( + get_resource('information.svg'), 22, 22, + [self.C_NORMAL, self.C_HOVER], + frame_image_info, False + ) + + expanding_sizePolicy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding + ) + expanding_sizePolicy.setHorizontalStretch(0) + expanding_sizePolicy.setVerticalStretch(0) + + layout.addWidget(self.icon, alignment=QtCore.Qt.AlignCenter) + layout.addWidget(self.info, alignment=QtCore.Qt.AlignCenter) + + layout_main.addWidget(frame_image_info) + + # Name + representation + self.name = QtWidgets.QLabel(frame) + self.file_info = QtWidgets.QLabel(frame) + self.ext = QtWidgets.QLabel(frame) + + self.name.setFont(font) + self.file_info.setFont(font) + self.ext.setFont(font) + + self.file_info.setStyleSheet('padding-left:3px;') + + expanding_sizePolicy.setHeightForWidth(self.name.sizePolicy().hasHeightForWidth()) + + frame_name_repre = QtWidgets.QFrame(frame) + + self.file_info.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) + self.ext.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) + self.name.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) + + layout = QtWidgets.QHBoxLayout(frame_name_repre) + layout.setSpacing(0) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self.name, alignment=QtCore.Qt.AlignLeft) + layout.addWidget(self.file_info, alignment=QtCore.Qt.AlignLeft) + layout.addWidget(self.ext, alignment=QtCore.Qt.AlignRight) + + frame_name_repre.setSizePolicy( + QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding + ) + + # Repre + icons + frame_repre_icons = QtWidgets.QFrame(frame) + + frame_repre = QtWidgets.QFrame(frame_repre_icons) + + label_repre = QtWidgets.QLabel() + label_repre.setText('Representation:') + + self.input_repre = QtWidgets.QLineEdit() + self.input_repre.setMaximumWidth(50) + + layout = QtWidgets.QHBoxLayout(frame_repre) + layout.setSpacing(0) + layout.setContentsMargins(0, 0, 0, 0) + + layout.addWidget(label_repre, alignment=QtCore.Qt.AlignLeft) + layout.addWidget(self.input_repre, alignment=QtCore.Qt.AlignLeft) + + frame_icons = QtWidgets.QFrame(frame_repre_icons) + + self.preview = SvgButton( + get_resource('preview.svg'), 64, 18, + [self.C_NORMAL, self.C_HOVER, self.C_ACTIVE, self.C_ACTIVE_HOVER], + frame_icons + ) + + self.thumbnail = SvgButton( + get_resource('thumbnail.svg'), 84, 18, + [self.C_NORMAL, self.C_HOVER, self.C_ACTIVE, self.C_ACTIVE_HOVER], + frame_icons + ) + + layout = QtWidgets.QHBoxLayout(frame_icons) + layout.setSpacing(6) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self.thumbnail) + layout.addWidget(self.preview) + + layout = QtWidgets.QHBoxLayout(frame_repre_icons) + layout.setSpacing(0) + layout.setContentsMargins(0, 0, 0, 0) + + layout.addWidget(frame_repre, alignment=QtCore.Qt.AlignLeft) + layout.addWidget(frame_icons, alignment=QtCore.Qt.AlignRight) + + frame_middle = QtWidgets.QFrame(frame) + + layout = QtWidgets.QVBoxLayout(frame_middle) + layout.setSpacing(0) + layout.setContentsMargins(4, 0, 4, 0) + layout.addWidget(frame_name_repre) + layout.addWidget(frame_repre_icons) + + layout.setStretchFactor(frame_name_repre, 1) + layout.setStretchFactor(frame_repre_icons, 1) + + layout_main.addWidget(frame_middle) + + self.remove = SvgButton( + get_resource('trash.svg'), 22, 22, + [self.C_NORMAL, self.C_HOVER], + frame, False + ) + + layout_main.addWidget(self.remove) + + layout = QtWidgets.QVBoxLayout(self) + layout.setSpacing(0) + layout.setContentsMargins(2, 2, 2, 2) + layout.addWidget(frame) + + self.preview.setToolTip('Mark component as Preview') + self.thumbnail.setToolTip('Component will be selected as thumbnail') + + # self.frame.setStyleSheet("border: 1px solid black;") + + def set_context(self, data): self.in_data = data + self.remove.clicked.connect(self._remove) + self.thumbnail.clicked.connect(self._thumbnail_clicked) + self.preview.clicked.connect(self._preview_clicked) + name = data['name'] + representation = data['representation'] + ext = data['ext'] + file_info = data['file_info'] + thumb = data['thumb'] + prev = data['prev'] + info = data['info'] + icon = data['icon'] - def set_context(self): - self._widget = ComponentWidget(self) - self._widget.set_context(self.in_data) - self.treeWidget().setItemWidget(self, 0, self._widget) + resource = None + if icon is not None: + resource = get_resource('{}.png'.format(icon)) + + if resource is None or not os.path.isfile(resource): + if data['is_sequence']: + resource = get_resource('files.png') + else: + resource = get_resource('file.png') + + pixmap = QtGui.QPixmap(resource) + self.icon.setPixmap(pixmap) + + self.name.setText(name) + self.input_repre.setText(representation) + self.ext.setText('( {} )'.format(ext)) + if file_info is None: + self.file_info.setVisible(False) + else: + self.file_info.setText('[{}]'.format(file_info)) + + self.thumbnail.setVisible(thumb) + self.preview.setVisible(prev) + + def _remove(self): + self.signal_remove.emit(self) + + def _thumbnail_clicked(self): + self.signal_thumbnail.emit(self) + + def _preview_clicked(self): + self.signal_preview.emit(self) def is_thumbnail(self): - return self._widget.thumbnail.checked + return self.thumbnail.checked def change_thumbnail(self, hover=True): - self._widget.thumbnail.change_checked(hover) + self.thumbnail.change_checked(hover) def is_preview(self): - return self._widget.preview.checked + return self.preview.checked def change_preview(self, hover=True): - self._widget.preview.change_checked(hover) - - def double_clicked(*args): - pass + self.preview.change_checked(hover) From 1570800b7bcddad5d3fb78da20035692ca4aa26b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Apr 2019 14:20:42 +0200 Subject: [PATCH 21/65] tree components replaced by components list --- .../widgets/widget_components_list.py | 90 +++++++++++++++++++ .../widgets/widget_tree_components.py | 14 --- 2 files changed, 90 insertions(+), 14 deletions(-) create mode 100644 pype/tools/standalonepublish/widgets/widget_components_list.py delete mode 100644 pype/tools/standalonepublish/widgets/widget_tree_components.py diff --git a/pype/tools/standalonepublish/widgets/widget_components_list.py b/pype/tools/standalonepublish/widgets/widget_components_list.py new file mode 100644 index 0000000000..357bd1e671 --- /dev/null +++ b/pype/tools/standalonepublish/widgets/widget_components_list.py @@ -0,0 +1,90 @@ +from . import QtCore, QtGui, QtWidgets + + +class ComponentsList(QtWidgets.QTableWidget): + def __init__(self, parent=None): + super().__init__(parent=parent) + + self._main_column = 0 + + self.setColumnCount(1) + self.setSelectionBehavior( + QtWidgets.QAbstractItemView.SelectRows + ) + self.setSelectionMode( + QtWidgets.QAbstractItemView.ExtendedSelection + ) + self.setVerticalScrollMode( + QtWidgets.QAbstractItemView.ScrollPerPixel + ) + self.verticalHeader().hide() + + try: + self.verticalHeader().setResizeMode( + QtWidgets.QHeaderView.ResizeToContents + ) + except Exception: + self.verticalHeader().setSectionResizeMode( + QtWidgets.QHeaderView.ResizeToContents + ) + + self.horizontalHeader().setStretchLastSection(True) + self.horizontalHeader().hide() + + def count(self): + return self.rowCount() + + def add_widget(self, widget, row=None): + if row is None: + row = self.count() + + self.insertRow(row) + self.setCellWidget(row, self._main_column, widget) + + self.resizeRowToContents(row) + + return row + + def remove_widget(self, row): + self.removeCellWidget(row, self._main_column) + self.removeRow(row) + + def move_widget(self, widget, newRow): + oldRow = self.indexOfWidget(widget) + if oldRow: + self.insertRow(newRow) + # Collect the oldRow after insert to make sure we move the correct + # widget. + oldRow = self.indexOfWidget(widget) + + self.setCellWidget(newRow, self._main_column, widget) + self.resizeRowToContents(oldRow) + + # Remove the old row + self.removeRow(oldRow) + + def clear_widgets(self): + '''Remove all widgets.''' + self.clear() + self.setRowCount(0) + + def widget_index(self, widget): + index = None + for row in range(self.count()): + candidateWidget = self.widget_at(row) + if candidateWidget == widget: + index = row + break + + return index + + def widgets(self): + widgets = [] + for row in range(self.count()): + widget = self.widget_at(row) + widgets.append(widget) + + return widgets + + def widget_at(self, row): + return self.cellWidget(row, self._main_column) diff --git a/pype/tools/standalonepublish/widgets/widget_tree_components.py b/pype/tools/standalonepublish/widgets/widget_tree_components.py deleted file mode 100644 index 76e5a9bce0..0000000000 --- a/pype/tools/standalonepublish/widgets/widget_tree_components.py +++ /dev/null @@ -1,14 +0,0 @@ -from . import QtCore, QtGui, QtWidgets - - -class TreeComponents(QtWidgets.QTreeWidget): - def __init__(self, parent): - super().__init__(parent) - - self.invisibleRootItem().setFlags(QtCore.Qt.ItemIsEnabled) - self.setIndentation(28) - self.headerItem().setText(0, 'Components') - - self.setRootIsDecorated(False) - - self.itemDoubleClicked.connect(lambda i, c: i.double_clicked(c)) From 3e6f02795c22ad664bbcaf888c1fd7e0affab627 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Apr 2019 14:21:11 +0200 Subject: [PATCH 22/65] family widget now use get presets from config --- .../widgets/widget_family.py | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/pype/tools/standalonepublish/widgets/widget_family.py b/pype/tools/standalonepublish/widgets/widget_family.py index 4ef7d9bcf8..26dab9bc19 100644 --- a/pype/tools/standalonepublish/widgets/widget_family.py +++ b/pype/tools/standalonepublish/widgets/widget_family.py @@ -4,13 +4,11 @@ import inspect import json from collections import namedtuple -from . import QtWidgets, QtCore, QtGui +from . import QtWidgets, QtCore 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 +from pypeapp import config class FamilyWidget(QtWidgets.QWidget): @@ -249,14 +247,9 @@ class FamilyWidget(QtWidgets.QWidget): 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) + presets = config.get_presets().get('tools', {}).get( + 'standalone_publish', {} + ) for creator in presets.get('families', {}).values(): creator = namedtuple("Creator", creator.keys())(*creator.values()) @@ -277,9 +270,6 @@ class FamilyWidget(QtWidgets.QWidget): 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): From 0ff2f9efdd3e39a4b53a8fa7451e580b3b314320 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Apr 2019 14:21:32 +0200 Subject: [PATCH 23/65] drop files renamed to drop frame --- ...get_drop_files.py => widget_drop_frame.py} | 63 ++++++++++++------- 1 file changed, 41 insertions(+), 22 deletions(-) rename pype/tools/standalonepublish/widgets/{widget_drop_files.py => widget_drop_frame.py} (84%) diff --git a/pype/tools/standalonepublish/widgets/widget_drop_files.py b/pype/tools/standalonepublish/widgets/widget_drop_frame.py similarity index 84% rename from pype/tools/standalonepublish/widgets/widget_drop_files.py rename to pype/tools/standalonepublish/widgets/widget_drop_frame.py index fbeddcbb41..e13a7c476c 100644 --- a/pype/tools/standalonepublish/widgets/widget_drop_files.py +++ b/pype/tools/standalonepublish/widgets/widget_drop_frame.py @@ -2,7 +2,7 @@ import os import clique from pypeapp import config from . import QtWidgets, QtCore -from . import ComponentItem, TreeComponents, DropDataWidget +from . import DropEmpty, ComponentsList, ComponentItem class DropDataFrame(QtWidgets.QFrame): @@ -14,10 +14,18 @@ class DropDataFrame(QtWidgets.QFrame): self.setAcceptDrops(True) layout = QtWidgets.QVBoxLayout(self) - self.tree_widget = TreeComponents(self) - layout.addWidget(self.tree_widget) + self.components_list = ComponentsList(self) + layout.addWidget(self.components_list) + + self.drop_widget = DropEmpty(self) + + sizePolicy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.drop_widget.sizePolicy().hasHeightForWidth()) + self.drop_widget.setSizePolicy(sizePolicy) - self.drop_widget = DropDataWidget(self) layout.addWidget(self.drop_widget) self._refresh_view() @@ -30,18 +38,29 @@ class DropDataFrame(QtWidgets.QFrame): event.accept() def dropEvent(self, event): - paths = self._processMimeData(event.mimeData()) + self.process_ent_mime(event) + event.accept() + + def process_ent_mime(self, ent): + paths = [] + if ent.mimeData().hasUrls(): + paths = self._processMimeData(ent.mimeData()) + else: + # If path is in clipboard as string + try: + path = ent.text() + if os.path.exists(path): + paths.append(path) + else: + print('Dropped invalid file/folder') + except Exception: + pass if paths: self._add_components(paths) - event.accept() def _processMimeData(self, mimeData): paths = [] - if not mimeData.hasUrls(): - print('Dropped invalid file/folder') - return paths - for path in mimeData.urls(): local_path = path.toLocalFile() if os.path.isfile(local_path) or os.path.isdir(local_path): @@ -61,17 +80,16 @@ class DropDataFrame(QtWidgets.QFrame): def _add_item(self, data): # Assign to self so garbage collector wont remove the component # during initialization - self.new_component = ComponentItem(self.tree_widget, data) - self.tree_widget.addTopLevelItem(self.new_component) - self.new_component.set_context() + new_component = ComponentItem(self.components_list) + new_component.set_context(data) + self.components_list.add_widget(new_component) - self.new_component._widget.signal_remove.connect(self._remove_item) - self.new_component._widget.signal_preview.connect(self._set_preview) - self.new_component._widget.signal_thumbnail.connect( + new_component.signal_remove.connect(self._remove_item) + new_component.signal_preview.connect(self._set_preview) + new_component.signal_thumbnail.connect( self._set_thumbnail ) - self.items.append(self.new_component) - self.new_component = None + self.items.append(new_component) self._refresh_view() @@ -100,14 +118,15 @@ class DropDataFrame(QtWidgets.QFrame): in_item.change_preview() def _remove_item(self, item): - root = self.tree_widget.invisibleRootItem() - (item.parent() or root).removeChild(item) - self.items.remove(item) + index = self.components_list.widget_index(item) + self.components_list.remove_widget(index) + if item in self.items: + self.items.remove(item) self._refresh_view() def _refresh_view(self): _bool = len(self.items) == 0 - self.tree_widget.setVisible(not _bool) + self.components_list.setVisible(not _bool) self.drop_widget.setVisible(_bool) self.parent_widget.set_valid_components(not _bool) From da3214fafe32b45101d5194e64e28123017246ef Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Apr 2019 14:21:43 +0200 Subject: [PATCH 24/65] drop data renamed to drop empty --- .../widgets/widget_drop_data.py | 34 ------------ .../widgets/widget_drop_empty.py | 52 +++++++++++++++++++ 2 files changed, 52 insertions(+), 34 deletions(-) delete mode 100644 pype/tools/standalonepublish/widgets/widget_drop_data.py create mode 100644 pype/tools/standalonepublish/widgets/widget_drop_empty.py diff --git a/pype/tools/standalonepublish/widgets/widget_drop_data.py b/pype/tools/standalonepublish/widgets/widget_drop_data.py deleted file mode 100644 index 74e30b4714..0000000000 --- a/pype/tools/standalonepublish/widgets/widget_drop_data.py +++ /dev/null @@ -1,34 +0,0 @@ -import os -import logging -import clique -from . import QtWidgets, QtCore, QtGui - - -class DropDataWidget(QtWidgets.QWidget): - - def __init__(self, parent): - '''Initialise DataDropZone widget.''' - super().__init__(parent) - - layout = QtWidgets.QVBoxLayout(self) - - CenterAlignment = QtCore.Qt.AlignVCenter | QtCore.Qt.AlignHCenter - - self._label = QtWidgets.QLabel('Drop files here') - layout.addWidget( - self._label, - alignment=CenterAlignment - ) - - def paintEvent(self, event): - super().paintEvent(event) - painter = QtGui.QPainter(self) - pen = QtGui.QPen() - pen.setWidth(1); - pen.setBrush(QtCore.Qt.darkGray); - pen.setStyle(QtCore.Qt.DashLine); - painter.setPen(pen) - painter.drawRect( - 10, 10, - self.rect().width()-15, self.rect().height()-15 - ) diff --git a/pype/tools/standalonepublish/widgets/widget_drop_empty.py b/pype/tools/standalonepublish/widgets/widget_drop_empty.py new file mode 100644 index 0000000000..a68b91da59 --- /dev/null +++ b/pype/tools/standalonepublish/widgets/widget_drop_empty.py @@ -0,0 +1,52 @@ +import os +import logging +import clique +from . import QtWidgets, QtCore, QtGui + + +class DropEmpty(QtWidgets.QWidget): + + def __init__(self, parent): + '''Initialise DataDropZone widget.''' + super().__init__(parent) + + layout = QtWidgets.QVBoxLayout(self) + + BottomCenterAlignment = QtCore.Qt.AlignBottom | QtCore.Qt.AlignHCenter + TopCenterAlignment = QtCore.Qt.AlignTop | QtCore.Qt.AlignHCenter + + font = QtGui.QFont() + font.setFamily("DejaVu Sans Condensed") + font.setPointSize(26) + font.setBold(True) + font.setWeight(50) + font.setKerning(True) + + self._label = QtWidgets.QLabel('Drag & Drop') + self._label.setFont(font) + self._label.setStyleSheet( + 'background-color: rgb(255, 255, 255, 0);' + ) + + font.setPointSize(12) + self._sub_label = QtWidgets.QLabel('(drop files here)') + self._sub_label.setFont(font) + self._sub_label.setStyleSheet( + 'background-color: rgb(255, 255, 255, 0);' + ) + + layout.addWidget(self._label, alignment=BottomCenterAlignment) + layout.addWidget(self._sub_label, alignment=TopCenterAlignment) + + def paintEvent(self, event): + super().paintEvent(event) + painter = QtGui.QPainter(self) + pen = QtGui.QPen() + pen.setWidth(1); + pen.setBrush(QtCore.Qt.darkGray); + pen.setStyle(QtCore.Qt.DashLine); + painter.setPen(pen) + painter.drawRect( + 10, 10, + self.rect().width()-15, self.rect().height()-15 + ) From 8da831c19336d2e1be3bed96b4696102c6ebe214 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Apr 2019 14:23:01 +0200 Subject: [PATCH 25/65] publish and browse btns cant be focused and components can handle mime data --- pype/tools/standalonepublish/widgets/__init__.py | 7 +++---- pype/tools/standalonepublish/widgets/button_from_svgs.py | 2 ++ pype/tools/standalonepublish/widgets/widget_components.py | 6 +++++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/pype/tools/standalonepublish/widgets/__init__.py b/pype/tools/standalonepublish/widgets/__init__.py index 21f703d762..9eb18f4d5d 100644 --- a/pype/tools/standalonepublish/widgets/__init__.py +++ b/pype/tools/standalonepublish/widgets/__init__.py @@ -22,12 +22,11 @@ from .widget_asset_view import AssetView from .widget_asset import AssetWidget from .widget_family_desc import FamilyDescriptionWidget from .widget_family import FamilyWidget -from .widget_drop_data import DropDataWidget -from .widget_component import ComponentWidget -from .widget_tree_components import TreeComponents +from .widget_drop_empty import DropEmpty from .widget_component_item import ComponentItem +from .widget_components_list import ComponentsList -from .widget_drop_files import DropDataFrame +from .widget_drop_frame import DropDataFrame from .widget_components import ComponentsWidget diff --git a/pype/tools/standalonepublish/widgets/button_from_svgs.py b/pype/tools/standalonepublish/widgets/button_from_svgs.py index 3a35bcd838..4255c5f29b 100644 --- a/pype/tools/standalonepublish/widgets/button_from_svgs.py +++ b/pype/tools/standalonepublish/widgets/button_from_svgs.py @@ -6,6 +6,7 @@ from PyQt5 import QtSvg, QtXml class SvgResizable(QtSvg.QSvgWidget): clicked = QtCore.Signal() + def __init__(self, filepath, width=None, height=None, fill=None): super().__init__() self.xmldoc = minidom.parse(filepath) @@ -13,6 +14,7 @@ class SvgResizable(QtSvg.QSvgWidget): for element in itemlist: if fill: element.setAttribute('fill', str(fill)) + # TODO auto scale if only one is set if width is not None and height is not None: self.setMaximumSize(width, height) self.setMinimumSize(width, height) diff --git a/pype/tools/standalonepublish/widgets/widget_components.py b/pype/tools/standalonepublish/widgets/widget_components.py index 18d4e480d5..9d9f2aeb22 100644 --- a/pype/tools/standalonepublish/widgets/widget_components.py +++ b/pype/tools/standalonepublish/widgets/widget_components.py @@ -14,9 +14,11 @@ class ComponentsWidget(QtWidgets.QWidget): self.btn_browse = QtWidgets.QPushButton('Browse') self.btn_browse.setToolTip('Browse for file(s).') + self.btn_browse.setFocusPolicy(QtCore.Qt.NoFocus) self.btn_publish = QtWidgets.QPushButton('Publish') self.btn_publish.setToolTip('Publishes data.') + self.btn_publish.setFocusPolicy(QtCore.Qt.NoFocus) layout.addWidget(self.btn_browse, alignment=QtCore.Qt.AlignLeft) layout.addWidget(self.btn_publish, alignment=QtCore.Qt.AlignRight) @@ -33,7 +35,6 @@ class ComponentsWidget(QtWidgets.QWidget): layout.addWidget(body) self.btn_browse.clicked.connect(self._browse) - # self.setStyleSheet("border: 1px solid black;") def set_valid(self, in_bool): self.btn_publish.setEnabled(in_bool) @@ -41,6 +42,9 @@ class ComponentsWidget(QtWidgets.QWidget): def set_valid_components(self, in_bool): self.parent_widget.set_valid_components(in_bool) + def process_mime_data(self, mime_data): + self.drop_frame.process_ent_mime(mime_data) + def _browse(self): options = [ QtWidgets.QFileDialog.DontResolveSymlinks, From d39f8b79fad6b0442553f2e1d4b548205d58b879 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Apr 2019 14:23:51 +0200 Subject: [PATCH 26/65] body has set better stretch factor and app can handle with clipboard --- pype/tools/standalonepublish/app.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pype/tools/standalonepublish/app.py b/pype/tools/standalonepublish/app.py index 099566c603..f39f553828 100644 --- a/pype/tools/standalonepublish/app.py +++ b/pype/tools/standalonepublish/app.py @@ -64,8 +64,8 @@ class Window(QtWidgets.QDialog): body.addWidget(widget_family) body.addWidget(widget_components) body.setStretchFactor(body.indexOf(widget_assets_wrap), 2) - body.setStretchFactor(body.indexOf(widget_family), 2) - body.setStretchFactor(body.indexOf(widget_components), 3) + body.setStretchFactor(body.indexOf(widget_family), 3) + body.setStretchFactor(body.indexOf(widget_components), 5) layout = QtWidgets.QVBoxLayout(self) layout.addWidget(body) @@ -124,6 +124,12 @@ class Window(QtWidgets.QDialog): self.widget_family.change_asset(self.NOT_SELECTED) self.widget_family.on_data_changed() + def keyPressEvent(self, event): + if event.key() == QtCore.Qt.Key_V and event.modifiers() == QtCore.Qt.ControlModifier: + clip = QtWidgets.QApplication.clipboard() + self.widget_components.process_mime_data(clip) + super().keyPressEvent(event) + def validation(self): if not self.initialized: return From fd394c64f92e534bc515a91790d1880616589b33 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Apr 2019 17:14:53 +0200 Subject: [PATCH 27/65] data are collected from all parts now --- pype/tools/standalonepublish/app.py | 3 ++- .../standalonepublish/widgets/widget_asset.py | 16 ++++++++++++++-- .../widgets/widget_component_item.py | 10 ++++++++++ .../widgets/widget_components.py | 3 +++ .../widgets/widget_drop_frame.py | 6 ++++++ 5 files changed, 35 insertions(+), 3 deletions(-) diff --git a/pype/tools/standalonepublish/app.py b/pype/tools/standalonepublish/app.py index f39f553828..0e8bfaacc1 100644 --- a/pype/tools/standalonepublish/app.py +++ b/pype/tools/standalonepublish/app.py @@ -148,7 +148,8 @@ class Window(QtWidgets.QDialog): data = {} data.update(self.widget_assets.collect_data()) data.update(self.widget_family.collect_data()) - + data.update(self.widget_components.collect_data()) + return data def show(parent=None, debug=False, context=None): diff --git a/pype/tools/standalonepublish/widgets/widget_asset.py b/pype/tools/standalonepublish/widgets/widget_asset.py index 82b3700dea..366fb88dfa 100644 --- a/pype/tools/standalonepublish/widgets/widget_asset.py +++ b/pype/tools/standalonepublish/widgets/widget_asset.py @@ -179,12 +179,24 @@ class AssetWidget(QtWidgets.QWidget): return self.parent_widget.db def collect_data(self): + project = self.db.find_one({'type': 'project'}) + asset = self.db.find_one({'_id': self.get_active_asset()}) data = { - 'project': self.combo_projects.currentText(), - 'asset': get_active_asset + 'project': project, + 'asset': asset, + 'parents': self.get_parents(asset) } return data + def get_parents(self, entity): + output = [] + if entity.get('data', {}).get('visualParent', None) is None: + return output + parent = self.db.find_one({'_id': entity['data']['visualParent']}) + output.append(parent) + output.extend(self.get_parents(parent)) + return output + def _set_projects(self): projects = list() for project in self.db.projects(): diff --git a/pype/tools/standalonepublish/widgets/widget_component_item.py b/pype/tools/standalonepublish/widgets/widget_component_item.py index f7221952af..9551f4cae6 100644 --- a/pype/tools/standalonepublish/widgets/widget_component_item.py +++ b/pype/tools/standalonepublish/widgets/widget_component_item.py @@ -230,3 +230,13 @@ class ComponentItem(QtWidgets.QFrame): def change_preview(self, hover=True): self.preview.change_checked(hover) + + def collect_data(self): + data = { + 'ext': self.in_data['ext'], + 'representation': self.input_repre.text(), + 'files': self.in_data['files'], + 'thumbnail': self.is_thumbnail(), + 'preview': self.is_preview() + } + return data diff --git a/pype/tools/standalonepublish/widgets/widget_components.py b/pype/tools/standalonepublish/widgets/widget_components.py index 9d9f2aeb22..b17f8d5ebc 100644 --- a/pype/tools/standalonepublish/widgets/widget_components.py +++ b/pype/tools/standalonepublish/widgets/widget_components.py @@ -45,6 +45,9 @@ class ComponentsWidget(QtWidgets.QWidget): def process_mime_data(self, mime_data): self.drop_frame.process_ent_mime(mime_data) + def collect_data(self): + return self.drop_frame.collect_data() + def _browse(self): options = [ QtWidgets.QFileDialog.DontResolveSymlinks, diff --git a/pype/tools/standalonepublish/widgets/widget_drop_frame.py b/pype/tools/standalonepublish/widgets/widget_drop_frame.py index e13a7c476c..9b0d49b744 100644 --- a/pype/tools/standalonepublish/widgets/widget_drop_frame.py +++ b/pype/tools/standalonepublish/widgets/widget_drop_frame.py @@ -313,3 +313,9 @@ class DropDataFrame(QtWidgets.QFrame): if found is False: self._add_item(data) + + def collect_data(self): + data = {'components' : []} + for item in self.items: + data['components'].append(item.collect_data()) + return data From f02c917a94651c6869436a5cfa82bd1d30e5c841 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Apr 2019 17:15:22 +0200 Subject: [PATCH 28/65] publish button now prints data to console --- pype/tools/standalonepublish/widgets/widget_components.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pype/tools/standalonepublish/widgets/widget_components.py b/pype/tools/standalonepublish/widgets/widget_components.py index b17f8d5ebc..15222b17d7 100644 --- a/pype/tools/standalonepublish/widgets/widget_components.py +++ b/pype/tools/standalonepublish/widgets/widget_components.py @@ -1,6 +1,7 @@ from . import QtWidgets, QtCore, QtGui from . import DropDataFrame + class ComponentsWidget(QtWidgets.QWidget): def __init__(self, parent): super().__init__() @@ -35,6 +36,7 @@ class ComponentsWidget(QtWidgets.QWidget): layout.addWidget(body) self.btn_browse.clicked.connect(self._browse) + self.btn_publish.clicked.connect(self._publish) def set_valid(self, in_bool): self.btn_publish.setEnabled(in_bool) @@ -81,3 +83,8 @@ class ComponentsWidget(QtWidgets.QWidget): # process the browsed files/folders for publishing paths = file_dialog.selectedFiles() self.drop_frame._process_paths(paths) + + def _publish(self): + data = self.parent_widget.collect_data() + from pprint import pprint + pprint(data) From 61026da1a42e72192aad36a0a449b07975b9ae66 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Apr 2019 17:17:19 +0200 Subject: [PATCH 29/65] info in component changed to actions --- .../standalonepublish/widgets/widget_component_item.py | 7 +++---- pype/tools/standalonepublish/widgets/widget_drop_frame.py | 8 ++++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/pype/tools/standalonepublish/widgets/widget_component_item.py b/pype/tools/standalonepublish/widgets/widget_component_item.py index 9551f4cae6..db35a2c468 100644 --- a/pype/tools/standalonepublish/widgets/widget_component_item.py +++ b/pype/tools/standalonepublish/widgets/widget_component_item.py @@ -49,8 +49,8 @@ class ComponentItem(QtWidgets.QFrame): self.icon.setText("") self.icon.setScaledContents(True) - self.info = SvgButton( - get_resource('information.svg'), 22, 22, + self.action_menu = SvgButton( + get_resource('menu.svg'), 22, 22, [self.C_NORMAL, self.C_HOVER], frame_image_info, False ) @@ -62,7 +62,7 @@ class ComponentItem(QtWidgets.QFrame): expanding_sizePolicy.setVerticalStretch(0) layout.addWidget(self.icon, alignment=QtCore.Qt.AlignCenter) - layout.addWidget(self.info, alignment=QtCore.Qt.AlignCenter) + layout.addWidget(self.action_menu, alignment=QtCore.Qt.AlignCenter) layout_main.addWidget(frame_image_info) @@ -183,7 +183,6 @@ class ComponentItem(QtWidgets.QFrame): file_info = data['file_info'] thumb = data['thumb'] prev = data['prev'] - info = data['info'] icon = data['icon'] resource = None diff --git a/pype/tools/standalonepublish/widgets/widget_drop_frame.py b/pype/tools/standalonepublish/widgets/widget_drop_frame.py index 9b0d49b744..a9c03d8e2e 100644 --- a/pype/tools/standalonepublish/widgets/widget_drop_frame.py +++ b/pype/tools/standalonepublish/widgets/widget_drop_frame.py @@ -168,7 +168,7 @@ class DropDataFrame(QtWidgets.QFrame): for file in os.listdir(folder_path): if file.startswith(file_base) and file.endswith(file_ext): files.append(os.path.sep.join([folder_path, file])) - info = {} + actions = [] data = { 'files': files, @@ -178,7 +178,7 @@ class DropDataFrame(QtWidgets.QFrame): 'representation': repr_name, 'folder_path': folder_path, 'is_sequence': True, - 'info': info + 'actions': actions } self._process_data(data) @@ -219,7 +219,7 @@ class DropDataFrame(QtWidgets.QFrame): files = [] files.append(remainder) - info = {} + actions = [] data = { 'files': files, @@ -229,7 +229,7 @@ class DropDataFrame(QtWidgets.QFrame): 'representation': repr_name, 'folder_path': folder_path, 'is_sequence': False, - 'info': info + 'actions': actions } self._process_data(data) From 74b11b98ee96b3a269282d0d424c7050dfb4c031 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Apr 2019 17:18:44 +0200 Subject: [PATCH 30/65] minor changes in component adding --- .../standalonepublish/resources/menu.svg | 12 +++++++ .../widgets/widget_drop_frame.py | 35 +++++++++++++------ 2 files changed, 37 insertions(+), 10 deletions(-) create mode 100644 pype/tools/standalonepublish/resources/menu.svg diff --git a/pype/tools/standalonepublish/resources/menu.svg b/pype/tools/standalonepublish/resources/menu.svg new file mode 100644 index 0000000000..ac1e728011 --- /dev/null +++ b/pype/tools/standalonepublish/resources/menu.svg @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/pype/tools/standalonepublish/widgets/widget_drop_frame.py b/pype/tools/standalonepublish/widgets/widget_drop_frame.py index a9c03d8e2e..b353b23b41 100644 --- a/pype/tools/standalonepublish/widgets/widget_drop_frame.py +++ b/pype/tools/standalonepublish/widgets/widget_drop_frame.py @@ -67,7 +67,6 @@ class DropDataFrame(QtWidgets.QFrame): paths.append(local_path) else: print('Invalid input: "{}"'.format(local_path)) - return paths def _add_components(self, paths): @@ -158,20 +157,16 @@ class DropDataFrame(QtWidgets.QFrame): def _process_collection(self, collection): file_base = os.path.basename(collection.head) folder_path = os.path.dirname(collection.head) - if file_base[-1] in ['.']: + if file_base[-1] in ['.', '_']: file_base = file_base[:-1] file_ext = collection.tail repr_name = file_ext.replace('.', '') - range = self._get_ranges(collection.indexes) + range = collection.format('{ranges}') - files = [] - for file in os.listdir(folder_path): - if file.startswith(file_base) and file.endswith(file_ext): - files.append(os.path.sep.join([folder_path, file])) actions = [] data = { - 'files': files, + 'files': [file for file in collection], 'name': file_base, 'ext': file_ext, 'file_info': range, @@ -245,7 +240,10 @@ class DropDataFrame(QtWidgets.QFrame): if data['is_sequence']: icon += 's' data['icon'] = icon - data['thumb'] = ext in self.presets['thumbnailable'] + data['thumb'] = ( + ext in self.presets['thumbnailable'] and + data['is_sequence'] is False + ) data['prev'] = ext in self.presets['extensions']['video_file'] found = False @@ -260,9 +258,18 @@ class DropDataFrame(QtWidgets.QFrame): # If both are single files if not new_is_seq and not ex_is_seq: - if data['name'] != item.in_data['name']: + if data['name'] == item.in_data['name']: + found = True + break + paths = data['files'] + paths.extend(item.in_data['files']) + c, r = clique.assemble(paths) + if len(c) == 0: continue + found = True + self._remove_item(item) + self._process_collection(c[0]) break # If new is sequence and ex is single file elif new_is_seq and not ex_is_seq: @@ -279,6 +286,7 @@ class DropDataFrame(QtWidgets.QFrame): paths = data['files'] paths.append(ex_file) collections, remainders = clique.assemble(paths) + self._remove_item(item) self._process_collection(collections[0]) break # If new is single file existing is sequence @@ -288,6 +296,13 @@ class DropDataFrame(QtWidgets.QFrame): new_file = data['files'][0] found = True if new_file in item.in_data['files']: + paths = [] + for path in item.in_data['files']: + if os.path.exists(path): + paths.append(path) + if len(paths) == 1: + self._remove_item(item) + found = False break paths = item.in_data['files'] paths.append(new_file) From 3a0ebe44fb5b56b2366e35dc550eb19d11422ef5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Apr 2019 10:36:13 +0200 Subject: [PATCH 31/65] created shadow widget can be used when app is working --- .../widgets/widget_shadow.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 pype/tools/standalonepublish/widgets/widget_shadow.py diff --git a/pype/tools/standalonepublish/widgets/widget_shadow.py b/pype/tools/standalonepublish/widgets/widget_shadow.py new file mode 100644 index 0000000000..1bb9cee44b --- /dev/null +++ b/pype/tools/standalonepublish/widgets/widget_shadow.py @@ -0,0 +1,40 @@ +from . import QtWidgets, QtCore, QtGui + + +class ShadowWidget(QtWidgets.QWidget): + def __init__(self, parent): + self.parent_widget = parent + super().__init__(parent) + w = self.parent_widget.frameGeometry().width() + h = self.parent_widget.frameGeometry().height() + self.resize(QtCore.QSize(w, h)) + palette = QtGui.QPalette(self.palette()) + palette.setColor(palette.Background, QtCore.Qt.transparent) + self.setPalette(palette) + self.message = '' + + font = QtGui.QFont() + font.setFamily("DejaVu Sans Condensed") + font.setPointSize(40) + font.setBold(True) + font.setWeight(50) + font.setKerning(True) + self.font = font + + def paintEvent(self, event): + painter = QtGui.QPainter() + painter.begin(self) + painter.setFont(self.font) + painter.setRenderHint(QtGui.QPainter.Antialiasing) + painter.fillRect(event.rect(), QtGui.QBrush(QtGui.QColor(0, 0, 0, 127))) + painter.drawText( + QtCore.QRectF( + 0.0, + 0.0, + self.parent_widget.frameGeometry().width(), + self.parent_widget.frameGeometry().height() + ), + QtCore.Qt.AlignCenter|QtCore.Qt.AlignCenter, + self.message + ) + painter.end() From 3781f1cec584c0703671bdaf71f0e864b5a09de0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Apr 2019 10:36:55 +0200 Subject: [PATCH 32/65] implemented shadow widget into main app --- pype/tools/standalonepublish/app.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/pype/tools/standalonepublish/app.py b/pype/tools/standalonepublish/app.py index 0e8bfaacc1..f45c8bd0cd 100644 --- a/pype/tools/standalonepublish/app.py +++ b/pype/tools/standalonepublish/app.py @@ -94,6 +94,18 @@ class Window(QtWidgets.QDialog): # Refresh asset input in Family widget self.on_asset_changed() self.validation() + self.shadow_widget = ShadowWidget(self) + self.shadow_widget.setVisible(False) + + def resizeEvent(self, event=None): + position_x = (self.frameGeometry().width()-self.shadow_widget.frameGeometry().width())/2 + position_y = (self.frameGeometry().height()-self.shadow_widget.frameGeometry().height())/2 + self.shadow_widget.move(position_x, position_y) + w = self.frameGeometry().width() + h = self.frameGeometry().height() + self.shadow_widget.resize(QtCore.QSize(w, h)) + if event: + super().resizeEvent(event) def get_avalon_parent(self, entity): parent_id = entity['data']['visualParent'] @@ -130,6 +142,17 @@ class Window(QtWidgets.QDialog): self.widget_components.process_mime_data(clip) super().keyPressEvent(event) + def working_start(self, msg=None): + if msg is None: + msg = 'Please wait...' + self.shadow_widget.message = msg + self.shadow_widget.setVisible(True) + self.resizeEvent() + QtWidgets.QApplication.processEvents() + + def working_stop(self): + self.shadow_widget.setVisible(False) + def validation(self): if not self.initialized: return From 0019004bc285d9a4c9526162ba7bbdf1c9ca2336 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Apr 2019 10:42:01 +0200 Subject: [PATCH 33/65] added publish that can launch pyblish --- pype/tools/standalonepublish/publish.py | 77 +++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 pype/tools/standalonepublish/publish.py diff --git a/pype/tools/standalonepublish/publish.py b/pype/tools/standalonepublish/publish.py new file mode 100644 index 0000000000..3bc225019f --- /dev/null +++ b/pype/tools/standalonepublish/publish.py @@ -0,0 +1,77 @@ +import os +import sys +import json +import tempfile +import random +import string + +from avalon import io +from avalon import api as avalon + +import pype +from pypeapp import execute + +import pyblish.api + + +pype.install() + + +def set_context(project, asset, app): + os.environ["AVALON_PROJECT"] = project + os.environ["AVALON_ASSET"] = asset + + io.install() + + av_project = io.find_one({'type': 'project'}) + av_asset = io.find_one({ + "type": 'asset', + "name": asset + }) + + parents = av_asset['data']['parents'] + hierarchy = '' + if parents and len(parents) > 0: + hierarchy = os.path.sep.join(parents) + + os.environ["AVALON_HIEARCHY"] = hierarchy + io.Session["AVALON_HIEARCHY"] = hierarchy + + os.environ["AVALON_PROJECTCODE"] = av_project['data'].get('code', '') + io.Session["AVALON_PROJECTCODE"] = av_project['data'].get('code', '') + + io.Session["current_dir"] = os.path.normpath(os.getcwd()) + + os.environ["AVALON_APP"] = app + io.Session["AVALON_APP"] = app + + io.uninstall() + + +def publish(data, gui=True): + io.install() + + # Create hash name folder in temp + chars = "".join( [random.choice(string.ascii_letters) for i in range(15)] ) + staging_dir = tempfile.mkdtemp(chars)#.replace("\\", "/") + + # create also json and fill with data + json_data_path = staging_dir + os.path.basename(staging_dir) + '.json' + with open(json_data_path, 'w') as outfile: + json.dump(data, outfile) + + args = [ + "-pp", os.pathsep.join(pyblish.api.registered_paths()) + ] + + if gui: + args += ["gui"] + + os.environ["PYBLISH_HOSTS"] = "shell" + os.environ["ASAPUBLISH_INPATH"] = json_data_path + + returncode = execute([ + sys.executable, "-u", "-m", "pyblish" + ] + args, env=os.environ) + + io.uninstall() From 0a31e71bf4e5463b77fe48a86a5c101c976b5699 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Apr 2019 10:45:25 +0200 Subject: [PATCH 34/65] added split and merge actions into components --- .../standalonepublish/widgets/__init__.py | 2 + .../widgets/widget_component_item.py | 48 ++++++- .../widgets/widget_components.py | 8 ++ .../widgets/widget_drop_frame.py | 121 ++++++++++-------- 4 files changed, 119 insertions(+), 60 deletions(-) diff --git a/pype/tools/standalonepublish/widgets/__init__.py b/pype/tools/standalonepublish/widgets/__init__.py index 9eb18f4d5d..cd99e15bed 100644 --- a/pype/tools/standalonepublish/widgets/__init__.py +++ b/pype/tools/standalonepublish/widgets/__init__.py @@ -30,3 +30,5 @@ from .widget_components_list import ComponentsList from .widget_drop_frame import DropDataFrame from .widget_components import ComponentsWidget + +from.widget_shadow import ShadowWidget diff --git a/pype/tools/standalonepublish/widgets/widget_component_item.py b/pype/tools/standalonepublish/widgets/widget_component_item.py index db35a2c468..6dec892d91 100644 --- a/pype/tools/standalonepublish/widgets/widget_component_item.py +++ b/pype/tools/standalonepublish/widgets/widget_component_item.py @@ -2,6 +2,7 @@ import os from . import QtCore, QtGui, QtWidgets from . import SvgButton from . import get_resource +from avalon import style class ComponentItem(QtWidgets.QFrame): @@ -13,11 +14,13 @@ class ComponentItem(QtWidgets.QFrame): signal_thumbnail = QtCore.Signal(object) signal_preview = QtCore.Signal(object) - def __init__(self, parent): + def __init__(self, parent, main_parent): super().__init__() + self.actions = [] self.resize(290, 70) self.setMinimumSize(QtCore.QSize(0, 70)) - self.parent_item = parent + self.parent_list = parent + self.parent_widget = main_parent # Font font = QtGui.QFont() font.setFamily("DejaVu Sans Condensed") @@ -49,12 +52,14 @@ class ComponentItem(QtWidgets.QFrame): self.icon.setText("") self.icon.setScaledContents(True) - self.action_menu = SvgButton( + self.btn_action_menu = SvgButton( get_resource('menu.svg'), 22, 22, [self.C_NORMAL, self.C_HOVER], frame_image_info, False ) + self.action_menu = QtWidgets.QMenu() + expanding_sizePolicy = QtWidgets.QSizePolicy( QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding ) @@ -62,7 +67,7 @@ class ComponentItem(QtWidgets.QFrame): expanding_sizePolicy.setVerticalStretch(0) layout.addWidget(self.icon, alignment=QtCore.Qt.AlignCenter) - layout.addWidget(self.action_menu, alignment=QtCore.Qt.AlignCenter) + layout.addWidget(self.btn_action_menu, alignment=QtCore.Qt.AlignCenter) layout_main.addWidget(frame_image_info) @@ -173,6 +178,7 @@ class ComponentItem(QtWidgets.QFrame): # self.frame.setStyleSheet("border: 1px solid black;") def set_context(self, data): + self.btn_action_menu.setVisible(False) self.in_data = data self.remove.clicked.connect(self._remove) self.thumbnail.clicked.connect(self._thumbnail_clicked) @@ -209,6 +215,39 @@ class ComponentItem(QtWidgets.QFrame): self.thumbnail.setVisible(thumb) self.preview.setVisible(prev) + def add_action(self, action_name): + if action_name.lower() == 'split': + for action in self.actions: + if action.text() == 'Split to frames': + return + new_action = QtWidgets.QAction('Split to frames', self) + new_action.triggered.connect(self.split_sequence) + elif action_name.lower() == 'merge': + for action in self.actions: + if action.text() == 'Merge components': + return + new_action = QtWidgets.QAction('Merge components', self) + new_action.triggered.connect(self.merge_sequence) + else: + print('unknown action') + return + self.action_menu.addAction(new_action) + self.actions.append(new_action) + if not self.btn_action_menu.isVisible(): + self.btn_action_menu.setVisible(True) + self.btn_action_menu.clicked.connect(self.show_actions) + self.action_menu.setStyleSheet(style.load_stylesheet()) + + def split_sequence(self): + self.parent_widget.split_items(self) + + def merge_sequence(self): + self.parent_widget.merge_items(self) + + def show_actions(self): + position = QtGui.QCursor().pos() + self.action_menu.popup(position) + def _remove(self): self.signal_remove.emit(self) @@ -233,6 +272,7 @@ class ComponentItem(QtWidgets.QFrame): def collect_data(self): data = { 'ext': self.in_data['ext'], + 'label': self.name.text(), 'representation': self.input_repre.text(), 'files': self.in_data['files'], 'thumbnail': self.is_thumbnail(), diff --git a/pype/tools/standalonepublish/widgets/widget_components.py b/pype/tools/standalonepublish/widgets/widget_components.py index 15222b17d7..61465f2ce5 100644 --- a/pype/tools/standalonepublish/widgets/widget_components.py +++ b/pype/tools/standalonepublish/widgets/widget_components.py @@ -84,6 +84,14 @@ class ComponentsWidget(QtWidgets.QWidget): paths = file_dialog.selectedFiles() self.drop_frame._process_paths(paths) + def working_start(self, msg=None): + if hasattr(self, 'parent_widget'): + self.parent_widget.working_start(msg) + + def working_stop(self): + if hasattr(self, 'parent_widget'): + self.parent_widget.working_stop() + def _publish(self): data = self.parent_widget.collect_data() from pprint import pprint diff --git a/pype/tools/standalonepublish/widgets/widget_drop_frame.py b/pype/tools/standalonepublish/widgets/widget_drop_frame.py index b353b23b41..ec8b07ce34 100644 --- a/pype/tools/standalonepublish/widgets/widget_drop_frame.py +++ b/pype/tools/standalonepublish/widgets/widget_drop_frame.py @@ -56,7 +56,7 @@ class DropDataFrame(QtWidgets.QFrame): except Exception: pass if paths: - self._add_components(paths) + self._process_paths(paths) def _processMimeData(self, mimeData): paths = [] @@ -69,17 +69,10 @@ class DropDataFrame(QtWidgets.QFrame): print('Invalid input: "{}"'.format(local_path)) return paths - def _add_components(self, paths): - components = self._process_paths(paths) - if not components: - return - for component in components: - self._add_item(component) - - def _add_item(self, data): + def _add_item(self, data, actions=[]): # Assign to self so garbage collector wont remove the component # during initialization - new_component = ComponentItem(self.components_list) + new_component = ComponentItem(self.components_list, self) new_component.set_context(data) self.components_list.add_widget(new_component) @@ -88,6 +81,9 @@ class DropDataFrame(QtWidgets.QFrame): new_component.signal_thumbnail.connect( self._set_thumbnail ) + for action in actions: + new_component.add_action(action) + self.items.append(new_component) self._refresh_view() @@ -131,12 +127,14 @@ class DropDataFrame(QtWidgets.QFrame): self.parent_widget.set_valid_components(not _bool) def _process_paths(self, in_paths): + self.parent_widget.working_start() paths = self._get_all_paths(in_paths) collections, remainders = clique.assemble(paths) for collection in collections: self._process_collection(collection) for remainder in remainders: self._process_remainder(remainder) + self.parent_widget.working_stop() def _get_all_paths(self, paths): output_paths = [] @@ -246,6 +244,9 @@ class DropDataFrame(QtWidgets.QFrame): ) data['prev'] = ext in self.presets['extensions']['video_file'] + actions = [] + new_is_seq = data['is_sequence'] + found = False for item in self.items: if data['ext'] != item.in_data['ext']: @@ -253,7 +254,6 @@ class DropDataFrame(QtWidgets.QFrame): if data['folder_path'] != item.in_data['folder_path']: continue - new_is_seq = data['is_sequence'] ex_is_seq = item.in_data['is_sequence'] # If both are single files @@ -266,68 +266,77 @@ class DropDataFrame(QtWidgets.QFrame): c, r = clique.assemble(paths) if len(c) == 0: continue + a_name = 'merge' + item.add_action(a_name) + if a_name not in actions: + actions.append(a_name) - found = True - self._remove_item(item) - self._process_collection(c[0]) - break # If new is sequence and ex is single file elif new_is_seq and not ex_is_seq: if data['name'] not in item.in_data['name']: continue ex_file = item.in_data['files'][0] - found = True - # If file is one of inserted sequence - if ex_file in data['files']: - self._remove_item(item) - self._add_item(data) - break - # if file is missing in inserted sequence - paths = data['files'] - paths.append(ex_file) - collections, remainders = clique.assemble(paths) - self._remove_item(item) - self._process_collection(collections[0]) - break + + a_name = 'merge' + item.add_action(a_name) + if a_name not in actions: + actions.append(a_name) + continue + # If new is single file existing is sequence elif not new_is_seq and ex_is_seq: if item.in_data['name'] not in data['name']: continue - new_file = data['files'][0] - found = True - if new_file in item.in_data['files']: - paths = [] - for path in item.in_data['files']: - if os.path.exists(path): - paths.append(path) - if len(paths) == 1: - self._remove_item(item) - found = False - break - paths = item.in_data['files'] - paths.append(new_file) - collections, remainders = clique.assemble(paths) - self._remove_item(item) - self._process_collection(collections[0]) + a_name = 'merge' + item.add_action(a_name) + if a_name not in actions: + actions.append(a_name) - break # If both are sequence else: if data['name'] != item.in_data['name']: continue - found = True - ex_files = item.in_data['files'] - for file in data['files']: - if file not in ex_files: - ex_files.append(file) - paths = list(set(ex_files)) - collections, remainders = clique.assemble(paths) - self._remove_item(item) - self._process_collection(collections[0]) - break + if data['files'] == item.in_data['files']: + found = True + break + a_name = 'merge' + item.add_action(a_name) + if a_name not in actions: + actions.append(a_name) + + if new_is_seq: + actions.append('split') if found is False: - self._add_item(data) + self._add_item(data, actions) + + def merge_items(self, in_item): + self.parent_widget.working_start() + items = [] + in_paths = in_item.in_data['files'] + paths = in_paths + for item in self.items: + if item.in_data['files'] == in_paths: + items.append(item) + continue + copy_paths = paths.copy() + copy_paths.extend(item.in_data['files']) + collections, remainders = clique.assemble(copy_paths) + if len(collections) == 1 and len(remainders) == 0: + paths.extend(item.in_data['files']) + items.append(item) + for item in items: + self._remove_item(item) + self._process_paths(paths) + self.parent_widget.working_stop() + + def split_items(self, item): + self.parent_widget.working_start() + paths = item.in_data['files'] + self._remove_item(item) + for path in paths: + self._process_remainder(path) + self.parent_widget.working_stop() def collect_data(self): data = {'components' : []} From 91b170f0ac205b3db10da0b25e7f82c3f0dee19f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Apr 2019 10:47:39 +0200 Subject: [PATCH 35/65] added basic file info getter (ffprobe must be in PATH) --- .../widgets/widget_drop_frame.py | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/pype/tools/standalonepublish/widgets/widget_drop_frame.py b/pype/tools/standalonepublish/widgets/widget_drop_frame.py index ec8b07ce34..1c9c9ea359 100644 --- a/pype/tools/standalonepublish/widgets/widget_drop_frame.py +++ b/pype/tools/standalonepublish/widgets/widget_drop_frame.py @@ -1,5 +1,6 @@ import os import clique +import subprocess from pypeapp import config from . import QtWidgets, QtCore from . import DropEmpty, ComponentsList, ComponentItem @@ -218,15 +219,42 @@ class DropDataFrame(QtWidgets.QFrame): 'files': files, 'name': file_base, 'ext': file_ext, - 'file_info': file_info, 'representation': repr_name, 'folder_path': folder_path, 'is_sequence': False, 'actions': actions } + data['file_info'] = self.get_file_info(data) self._process_data(data) + def get_file_info(self, data): + output = None + if data['ext'] == '.mov': + try: + # ffProbe must be in PATH + filepath = data['files'][0] + args = ['ffprobe', '-show_streams', filepath] + p = subprocess.Popen( + args, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=True + ) + datalines=[] + for line in iter(p.stdout.readline, b''): + line = line.decode("utf-8").replace('\r\n', '') + datalines.append(line) + + find_value = 'codec_name' + for line in datalines: + if line.startswith(find_value): + output = line.replace(find_value + '=', '') + break + except Exception as e: + pass + return output + def _process_data(self, data): ext = data['ext'] icon = 'default' From adc063d3ff52702e33b3eeebda280185c95201af Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Apr 2019 10:48:06 +0200 Subject: [PATCH 36/65] publish button opens publish GUI --- .../standalonepublish/widgets/widget_components.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/pype/tools/standalonepublish/widgets/widget_components.py b/pype/tools/standalonepublish/widgets/widget_components.py index 61465f2ce5..5cc66de0b5 100644 --- a/pype/tools/standalonepublish/widgets/widget_components.py +++ b/pype/tools/standalonepublish/widgets/widget_components.py @@ -1,6 +1,8 @@ from . import QtWidgets, QtCore, QtGui from . import DropDataFrame +from .. import publish + class ComponentsWidget(QtWidgets.QWidget): def __init__(self, parent): @@ -93,6 +95,12 @@ class ComponentsWidget(QtWidgets.QWidget): self.parent_widget.working_stop() def _publish(self): - data = self.parent_widget.collect_data() - from pprint import pprint - pprint(data) + self.working_start('Pyblish is running') + try: + data = self.parent_widget.collect_data() + publish.set_context( + data['project'], data['asset'], 'standalonepublish' + ) + publish.publish(data) + finally: + self.working_stop() From 96684f42730442727c178a1e9d997e55b150ebf7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Apr 2019 10:50:34 +0200 Subject: [PATCH 37/65] assets are collecting json serializable data --- pype/tools/standalonepublish/widgets/widget_asset.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pype/tools/standalonepublish/widgets/widget_asset.py b/pype/tools/standalonepublish/widgets/widget_asset.py index 366fb88dfa..45e9757d71 100644 --- a/pype/tools/standalonepublish/widgets/widget_asset.py +++ b/pype/tools/standalonepublish/widgets/widget_asset.py @@ -182,8 +182,8 @@ class AssetWidget(QtWidgets.QWidget): project = self.db.find_one({'type': 'project'}) asset = self.db.find_one({'_id': self.get_active_asset()}) data = { - 'project': project, - 'asset': asset, + 'project': project['name'], + 'asset': asset['name'], 'parents': self.get_parents(asset) } return data @@ -193,7 +193,7 @@ class AssetWidget(QtWidgets.QWidget): if entity.get('data', {}).get('visualParent', None) is None: return output parent = self.db.find_one({'_id': entity['data']['visualParent']}) - output.append(parent) + output.append(parent['name']) output.extend(self.get_parents(parent)) return output From 7128389411e189f99acd14db2b7a906eda30901f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Apr 2019 10:51:33 +0200 Subject: [PATCH 38/65] created first standalone publish collector MUST EDIT --- .../publish/collect_context.py | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 pype/plugins/standalonepublish/publish/collect_context.py diff --git a/pype/plugins/standalonepublish/publish/collect_context.py b/pype/plugins/standalonepublish/publish/collect_context.py new file mode 100644 index 0000000000..d063bcf2dd --- /dev/null +++ b/pype/plugins/standalonepublish/publish/collect_context.py @@ -0,0 +1,73 @@ +import os +import pyblish.api +from avalon import ( + io, + api as avalon +) +import json +import logging + + +log = logging.getLogger("collector") + + +class CollectContextDataSAPublish(pyblish.api.ContextPlugin): + """ + Collecting temp json data sent from a host context + and path for returning json data back to hostself. + + Setting avalon session into correct context + + Args: + context (obj): pyblish context session + + """ + + label = "Collect Context - SA Publish" + order = pyblish.api.CollectorOrder - 0.49 + + def process(self, context): + # get json paths from os and load them + io.install() + json_path = os.environ.get("ASAPUBLISH_INPATH") + with open(json_path, "r") as f: + in_data = json.load(f) + + context.data["stagingDir"] = os.path.dirname(json_path) + project_name = in_data['project'] + asset_name = in_data['asset'] + family = in_data['family'] + subset = in_data['subset'] + + project = io.find_one({'type': 'project'}) + asset = io.find_one({ + 'type': 'asset', + 'name': asset_name + }) + context.data['project'] = project + context.data['asset'] = asset + context.data['family'] = family + context.data['subset'] = subset + + instances = [] + + for component in in_data['components']: + instance = context.create_instance(subset) + # instance.add(node) + + instance.data.update({ + "subset": subset, + "asset": asset_name, + "label": component['label'], + "name": component['representation'], + "subset": subset, + "family": family, + "is_thumbnail": component['thumbnail'], + "is_preview": component['preview'] + }) + + self.log.info("collected instance: {}".format(instance.data)) + instances.append(instance) + + context.data["instances"] = instances + self.log.info(in_data) From 764fd0493f66ad769c39c4215f50b85ab2d8f3c2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Apr 2019 10:55:49 +0200 Subject: [PATCH 39/65] added register of standalone publish plugins path --- pype/tools/standalonepublish/publish.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pype/tools/standalonepublish/publish.py b/pype/tools/standalonepublish/publish.py index 3bc225019f..bb6c7215b4 100644 --- a/pype/tools/standalonepublish/publish.py +++ b/pype/tools/standalonepublish/publish.py @@ -15,6 +15,10 @@ import pyblish.api pype.install() +PUBLISH_PATH = os.path.sep.join( + [pype.PLUGINS_DIR, 'standalonepublish', 'publish'] +) +pyblish.api.register_plugin_path(PUBLISH_PATH) def set_context(project, asset, app): From 009615cf1855ccc992c783f09b1b3c8a4fbcde5f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Apr 2019 10:56:58 +0200 Subject: [PATCH 40/65] removed original resources --- .../resources/original/delete-button.svg | 55 ------------------- .../resources/original/information.svg | 44 --------------- .../resources/original/picture.svg | 48 ---------------- .../resources/original/play_icon.svg | 19 ------- 4 files changed, 166 deletions(-) delete mode 100644 pype/tools/standalonepublish/resources/original/delete-button.svg delete mode 100644 pype/tools/standalonepublish/resources/original/information.svg delete mode 100644 pype/tools/standalonepublish/resources/original/picture.svg delete mode 100644 pype/tools/standalonepublish/resources/original/play_icon.svg diff --git a/pype/tools/standalonepublish/resources/original/delete-button.svg b/pype/tools/standalonepublish/resources/original/delete-button.svg deleted file mode 100644 index 48b09ac787..0000000000 --- a/pype/tools/standalonepublish/resources/original/delete-button.svg +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/pype/tools/standalonepublish/resources/original/information.svg b/pype/tools/standalonepublish/resources/original/information.svg deleted file mode 100644 index c040bab773..0000000000 --- a/pype/tools/standalonepublish/resources/original/information.svg +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/pype/tools/standalonepublish/resources/original/picture.svg b/pype/tools/standalonepublish/resources/original/picture.svg deleted file mode 100644 index 35f912ce80..0000000000 --- a/pype/tools/standalonepublish/resources/original/picture.svg +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/pype/tools/standalonepublish/resources/original/play_icon.svg b/pype/tools/standalonepublish/resources/original/play_icon.svg deleted file mode 100644 index e9bab5a251..0000000000 --- a/pype/tools/standalonepublish/resources/original/play_icon.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - From f6b6f730a8ae2b3abf043d6a26a34ec4719ccb97 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Apr 2019 11:47:23 +0200 Subject: [PATCH 41/65] moved standalone publish from tools into pype --- .../{tools => }/standalonepublish/__init__.py | 1 + pype/{tools => }/standalonepublish/app.py | 18 ++++++------------ pype/{tools => }/standalonepublish/publish.py | 2 ++ .../standalonepublish/resources/__init__.py | 0 .../standalonepublish/resources/edit.svg | 0 .../standalonepublish/resources/file.png | Bin .../standalonepublish/resources/files.png | Bin .../standalonepublish/resources/houdini.png | Bin .../resources/image_file.png | Bin .../resources/image_files.png | Bin .../resources/information.svg | 0 .../standalonepublish/resources/maya.png | Bin .../standalonepublish/resources/menu.svg | 0 .../standalonepublish/resources/nuke.png | Bin .../standalonepublish/resources/premiere.png | Bin .../standalonepublish/resources/preview.svg | 0 .../standalonepublish/resources/thumbnail.svg | 0 .../standalonepublish/resources/trash.svg | 0 .../resources/video_file.png | Bin .../standalonepublish/widgets/__init__.py | 0 .../widgets/button_from_svgs.py | 0 .../standalonepublish/widgets/model_asset.py | 0 .../widgets/model_filter_proxy_exact_match.py | 0 .../model_filter_proxy_recursive_sort.py | 0 .../standalonepublish/widgets/model_node.py | 0 .../widgets/model_tasks_template.py | 0 .../standalonepublish/widgets/model_tree.py | 0 .../widgets/model_tree_view_deselectable.py | 0 .../standalonepublish/widgets/widget_asset.py | 0 .../widgets/widget_asset_view.py | 0 .../widgets/widget_component_item.py | 0 .../widgets/widget_components.py | 0 .../widgets/widget_components_list.py | 0 .../widgets/widget_drop_empty.py | 0 .../widgets/widget_drop_frame.py | 4 ++-- .../widgets/widget_family.py | 4 +--- .../widgets/widget_family_desc.py | 0 .../widgets/widget_shadow.py | 0 pype/tools/standalonepublish/__main__.py | 5 ----- 39 files changed, 12 insertions(+), 22 deletions(-) rename pype/{tools => }/standalonepublish/__init__.py (60%) rename pype/{tools => }/standalonepublish/app.py (95%) rename pype/{tools => }/standalonepublish/publish.py (95%) rename pype/{tools => }/standalonepublish/resources/__init__.py (100%) rename pype/{tools => }/standalonepublish/resources/edit.svg (100%) rename pype/{tools => }/standalonepublish/resources/file.png (100%) rename pype/{tools => }/standalonepublish/resources/files.png (100%) rename pype/{tools => }/standalonepublish/resources/houdini.png (100%) rename pype/{tools => }/standalonepublish/resources/image_file.png (100%) rename pype/{tools => }/standalonepublish/resources/image_files.png (100%) rename pype/{tools => }/standalonepublish/resources/information.svg (100%) rename pype/{tools => }/standalonepublish/resources/maya.png (100%) rename pype/{tools => }/standalonepublish/resources/menu.svg (100%) rename pype/{tools => }/standalonepublish/resources/nuke.png (100%) rename pype/{tools => }/standalonepublish/resources/premiere.png (100%) rename pype/{tools => }/standalonepublish/resources/preview.svg (100%) rename pype/{tools => }/standalonepublish/resources/thumbnail.svg (100%) rename pype/{tools => }/standalonepublish/resources/trash.svg (100%) rename pype/{tools => }/standalonepublish/resources/video_file.png (100%) rename pype/{tools => }/standalonepublish/widgets/__init__.py (100%) rename pype/{tools => }/standalonepublish/widgets/button_from_svgs.py (100%) rename pype/{tools => }/standalonepublish/widgets/model_asset.py (100%) rename pype/{tools => }/standalonepublish/widgets/model_filter_proxy_exact_match.py (100%) rename pype/{tools => }/standalonepublish/widgets/model_filter_proxy_recursive_sort.py (100%) rename pype/{tools => }/standalonepublish/widgets/model_node.py (100%) rename pype/{tools => }/standalonepublish/widgets/model_tasks_template.py (100%) rename pype/{tools => }/standalonepublish/widgets/model_tree.py (100%) rename pype/{tools => }/standalonepublish/widgets/model_tree_view_deselectable.py (100%) rename pype/{tools => }/standalonepublish/widgets/widget_asset.py (100%) rename pype/{tools => }/standalonepublish/widgets/widget_asset_view.py (100%) rename pype/{tools => }/standalonepublish/widgets/widget_component_item.py (100%) rename pype/{tools => }/standalonepublish/widgets/widget_components.py (100%) rename pype/{tools => }/standalonepublish/widgets/widget_components_list.py (100%) rename pype/{tools => }/standalonepublish/widgets/widget_drop_empty.py (100%) rename pype/{tools => }/standalonepublish/widgets/widget_drop_frame.py (98%) rename pype/{tools => }/standalonepublish/widgets/widget_family.py (98%) rename pype/{tools => }/standalonepublish/widgets/widget_family_desc.py (100%) rename pype/{tools => }/standalonepublish/widgets/widget_shadow.py (100%) delete mode 100644 pype/tools/standalonepublish/__main__.py diff --git a/pype/tools/standalonepublish/__init__.py b/pype/standalonepublish/__init__.py similarity index 60% rename from pype/tools/standalonepublish/__init__.py rename to pype/standalonepublish/__init__.py index 29a4e52904..41bc69b9a2 100644 --- a/pype/tools/standalonepublish/__init__.py +++ b/pype/standalonepublish/__init__.py @@ -1,3 +1,4 @@ +from .asapublish_module import ASAPublishModule from .app import ( show, cli diff --git a/pype/tools/standalonepublish/app.py b/pype/standalonepublish/app.py similarity index 95% rename from pype/tools/standalonepublish/app.py rename to pype/standalonepublish/app.py index f45c8bd0cd..1a875505d7 100644 --- a/pype/tools/standalonepublish/app.py +++ b/pype/standalonepublish/app.py @@ -10,6 +10,8 @@ from .widgets import * # Move this to pype lib? from avalon.tools.libraryloader.io_nonsingleton import DbConnector +module = sys.modules[__name__] +module.window = None class Window(QtWidgets.QDialog): _db = DbConnector() @@ -22,7 +24,7 @@ class Window(QtWidgets.QDialog): NOT_SELECTED = '< Nothing is selected >' def __init__(self, parent=None): - super(Window, self).__init__(parent) + super(Window, self).__init__() self._db.install() self.setWindowTitle("Standalone Publish") @@ -172,18 +174,10 @@ class Window(QtWidgets.QDialog): data.update(self.widget_assets.collect_data()) data.update(self.widget_family.collect_data()) data.update(self.widget_components.collect_data()) - + return data -def show(parent=None, debug=False, context=None): - """Display Loader GUI - - Arguments: - debug (bool, optional): Run loader in debug-mode, - defaults to False - - """ - +def show(parent=None, debug=False): try: module.window.close() del module.window @@ -191,7 +185,7 @@ def show(parent=None, debug=False, context=None): pass with parentlib.application(): - window = Window(parent, context) + window = Window(parent) window.show() module.window = window diff --git a/pype/tools/standalonepublish/publish.py b/pype/standalonepublish/publish.py similarity index 95% rename from pype/tools/standalonepublish/publish.py rename to pype/standalonepublish/publish.py index bb6c7215b4..215281bfaf 100644 --- a/pype/tools/standalonepublish/publish.py +++ b/pype/standalonepublish/publish.py @@ -23,7 +23,9 @@ pyblish.api.register_plugin_path(PUBLISH_PATH) def set_context(project, asset, app): os.environ["AVALON_PROJECT"] = project + io.Session["AVALON_PROJECT"] = project os.environ["AVALON_ASSET"] = asset + io.Session["AVALON_ASSET"] = asset io.install() diff --git a/pype/tools/standalonepublish/resources/__init__.py b/pype/standalonepublish/resources/__init__.py similarity index 100% rename from pype/tools/standalonepublish/resources/__init__.py rename to pype/standalonepublish/resources/__init__.py diff --git a/pype/tools/standalonepublish/resources/edit.svg b/pype/standalonepublish/resources/edit.svg similarity index 100% rename from pype/tools/standalonepublish/resources/edit.svg rename to pype/standalonepublish/resources/edit.svg diff --git a/pype/tools/standalonepublish/resources/file.png b/pype/standalonepublish/resources/file.png similarity index 100% rename from pype/tools/standalonepublish/resources/file.png rename to pype/standalonepublish/resources/file.png diff --git a/pype/tools/standalonepublish/resources/files.png b/pype/standalonepublish/resources/files.png similarity index 100% rename from pype/tools/standalonepublish/resources/files.png rename to pype/standalonepublish/resources/files.png diff --git a/pype/tools/standalonepublish/resources/houdini.png b/pype/standalonepublish/resources/houdini.png similarity index 100% rename from pype/tools/standalonepublish/resources/houdini.png rename to pype/standalonepublish/resources/houdini.png diff --git a/pype/tools/standalonepublish/resources/image_file.png b/pype/standalonepublish/resources/image_file.png similarity index 100% rename from pype/tools/standalonepublish/resources/image_file.png rename to pype/standalonepublish/resources/image_file.png diff --git a/pype/tools/standalonepublish/resources/image_files.png b/pype/standalonepublish/resources/image_files.png similarity index 100% rename from pype/tools/standalonepublish/resources/image_files.png rename to pype/standalonepublish/resources/image_files.png diff --git a/pype/tools/standalonepublish/resources/information.svg b/pype/standalonepublish/resources/information.svg similarity index 100% rename from pype/tools/standalonepublish/resources/information.svg rename to pype/standalonepublish/resources/information.svg diff --git a/pype/tools/standalonepublish/resources/maya.png b/pype/standalonepublish/resources/maya.png similarity index 100% rename from pype/tools/standalonepublish/resources/maya.png rename to pype/standalonepublish/resources/maya.png diff --git a/pype/tools/standalonepublish/resources/menu.svg b/pype/standalonepublish/resources/menu.svg similarity index 100% rename from pype/tools/standalonepublish/resources/menu.svg rename to pype/standalonepublish/resources/menu.svg diff --git a/pype/tools/standalonepublish/resources/nuke.png b/pype/standalonepublish/resources/nuke.png similarity index 100% rename from pype/tools/standalonepublish/resources/nuke.png rename to pype/standalonepublish/resources/nuke.png diff --git a/pype/tools/standalonepublish/resources/premiere.png b/pype/standalonepublish/resources/premiere.png similarity index 100% rename from pype/tools/standalonepublish/resources/premiere.png rename to pype/standalonepublish/resources/premiere.png diff --git a/pype/tools/standalonepublish/resources/preview.svg b/pype/standalonepublish/resources/preview.svg similarity index 100% rename from pype/tools/standalonepublish/resources/preview.svg rename to pype/standalonepublish/resources/preview.svg diff --git a/pype/tools/standalonepublish/resources/thumbnail.svg b/pype/standalonepublish/resources/thumbnail.svg similarity index 100% rename from pype/tools/standalonepublish/resources/thumbnail.svg rename to pype/standalonepublish/resources/thumbnail.svg diff --git a/pype/tools/standalonepublish/resources/trash.svg b/pype/standalonepublish/resources/trash.svg similarity index 100% rename from pype/tools/standalonepublish/resources/trash.svg rename to pype/standalonepublish/resources/trash.svg diff --git a/pype/tools/standalonepublish/resources/video_file.png b/pype/standalonepublish/resources/video_file.png similarity index 100% rename from pype/tools/standalonepublish/resources/video_file.png rename to pype/standalonepublish/resources/video_file.png diff --git a/pype/tools/standalonepublish/widgets/__init__.py b/pype/standalonepublish/widgets/__init__.py similarity index 100% rename from pype/tools/standalonepublish/widgets/__init__.py rename to pype/standalonepublish/widgets/__init__.py diff --git a/pype/tools/standalonepublish/widgets/button_from_svgs.py b/pype/standalonepublish/widgets/button_from_svgs.py similarity index 100% rename from pype/tools/standalonepublish/widgets/button_from_svgs.py rename to pype/standalonepublish/widgets/button_from_svgs.py diff --git a/pype/tools/standalonepublish/widgets/model_asset.py b/pype/standalonepublish/widgets/model_asset.py similarity index 100% rename from pype/tools/standalonepublish/widgets/model_asset.py rename to pype/standalonepublish/widgets/model_asset.py diff --git a/pype/tools/standalonepublish/widgets/model_filter_proxy_exact_match.py b/pype/standalonepublish/widgets/model_filter_proxy_exact_match.py similarity index 100% rename from pype/tools/standalonepublish/widgets/model_filter_proxy_exact_match.py rename to pype/standalonepublish/widgets/model_filter_proxy_exact_match.py diff --git a/pype/tools/standalonepublish/widgets/model_filter_proxy_recursive_sort.py b/pype/standalonepublish/widgets/model_filter_proxy_recursive_sort.py similarity index 100% rename from pype/tools/standalonepublish/widgets/model_filter_proxy_recursive_sort.py rename to pype/standalonepublish/widgets/model_filter_proxy_recursive_sort.py diff --git a/pype/tools/standalonepublish/widgets/model_node.py b/pype/standalonepublish/widgets/model_node.py similarity index 100% rename from pype/tools/standalonepublish/widgets/model_node.py rename to pype/standalonepublish/widgets/model_node.py diff --git a/pype/tools/standalonepublish/widgets/model_tasks_template.py b/pype/standalonepublish/widgets/model_tasks_template.py similarity index 100% rename from pype/tools/standalonepublish/widgets/model_tasks_template.py rename to pype/standalonepublish/widgets/model_tasks_template.py diff --git a/pype/tools/standalonepublish/widgets/model_tree.py b/pype/standalonepublish/widgets/model_tree.py similarity index 100% rename from pype/tools/standalonepublish/widgets/model_tree.py rename to pype/standalonepublish/widgets/model_tree.py diff --git a/pype/tools/standalonepublish/widgets/model_tree_view_deselectable.py b/pype/standalonepublish/widgets/model_tree_view_deselectable.py similarity index 100% rename from pype/tools/standalonepublish/widgets/model_tree_view_deselectable.py rename to pype/standalonepublish/widgets/model_tree_view_deselectable.py diff --git a/pype/tools/standalonepublish/widgets/widget_asset.py b/pype/standalonepublish/widgets/widget_asset.py similarity index 100% rename from pype/tools/standalonepublish/widgets/widget_asset.py rename to pype/standalonepublish/widgets/widget_asset.py diff --git a/pype/tools/standalonepublish/widgets/widget_asset_view.py b/pype/standalonepublish/widgets/widget_asset_view.py similarity index 100% rename from pype/tools/standalonepublish/widgets/widget_asset_view.py rename to pype/standalonepublish/widgets/widget_asset_view.py diff --git a/pype/tools/standalonepublish/widgets/widget_component_item.py b/pype/standalonepublish/widgets/widget_component_item.py similarity index 100% rename from pype/tools/standalonepublish/widgets/widget_component_item.py rename to pype/standalonepublish/widgets/widget_component_item.py diff --git a/pype/tools/standalonepublish/widgets/widget_components.py b/pype/standalonepublish/widgets/widget_components.py similarity index 100% rename from pype/tools/standalonepublish/widgets/widget_components.py rename to pype/standalonepublish/widgets/widget_components.py diff --git a/pype/tools/standalonepublish/widgets/widget_components_list.py b/pype/standalonepublish/widgets/widget_components_list.py similarity index 100% rename from pype/tools/standalonepublish/widgets/widget_components_list.py rename to pype/standalonepublish/widgets/widget_components_list.py diff --git a/pype/tools/standalonepublish/widgets/widget_drop_empty.py b/pype/standalonepublish/widgets/widget_drop_empty.py similarity index 100% rename from pype/tools/standalonepublish/widgets/widget_drop_empty.py rename to pype/standalonepublish/widgets/widget_drop_empty.py diff --git a/pype/tools/standalonepublish/widgets/widget_drop_frame.py b/pype/standalonepublish/widgets/widget_drop_frame.py similarity index 98% rename from pype/tools/standalonepublish/widgets/widget_drop_frame.py rename to pype/standalonepublish/widgets/widget_drop_frame.py index 1c9c9ea359..90434e75f4 100644 --- a/pype/tools/standalonepublish/widgets/widget_drop_frame.py +++ b/pype/standalonepublish/widgets/widget_drop_frame.py @@ -11,7 +11,7 @@ class DropDataFrame(QtWidgets.QFrame): super().__init__() self.parent_widget = parent self.items = [] - self.presets = config.get_presets()['tools']['standalone_publish'] + self.presets = config.get_presets()['standalone_publish'] self.setAcceptDrops(True) layout = QtWidgets.QVBoxLayout(self) @@ -267,7 +267,7 @@ class DropDataFrame(QtWidgets.QFrame): icon += 's' data['icon'] = icon data['thumb'] = ( - ext in self.presets['thumbnailable'] and + ext in self.presets['extensions']['thumbnailable'] and data['is_sequence'] is False ) data['prev'] = ext in self.presets['extensions']['video_file'] diff --git a/pype/tools/standalonepublish/widgets/widget_family.py b/pype/standalonepublish/widgets/widget_family.py similarity index 98% rename from pype/tools/standalonepublish/widgets/widget_family.py rename to pype/standalonepublish/widgets/widget_family.py index 26dab9bc19..7259ecdb64 100644 --- a/pype/tools/standalonepublish/widgets/widget_family.py +++ b/pype/standalonepublish/widgets/widget_family.py @@ -247,9 +247,7 @@ class FamilyWidget(QtWidgets.QWidget): def refresh(self): has_families = False - presets = config.get_presets().get('tools', {}).get( - 'standalone_publish', {} - ) + presets = config.get_presets().get('standalone_publish', {}) for creator in presets.get('families', {}).values(): creator = namedtuple("Creator", creator.keys())(*creator.values()) diff --git a/pype/tools/standalonepublish/widgets/widget_family_desc.py b/pype/standalonepublish/widgets/widget_family_desc.py similarity index 100% rename from pype/tools/standalonepublish/widgets/widget_family_desc.py rename to pype/standalonepublish/widgets/widget_family_desc.py diff --git a/pype/tools/standalonepublish/widgets/widget_shadow.py b/pype/standalonepublish/widgets/widget_shadow.py similarity index 100% rename from pype/tools/standalonepublish/widgets/widget_shadow.py rename to pype/standalonepublish/widgets/widget_shadow.py diff --git a/pype/tools/standalonepublish/__main__.py b/pype/tools/standalonepublish/__main__.py deleted file mode 100644 index d77bc585c5..0000000000 --- a/pype/tools/standalonepublish/__main__.py +++ /dev/null @@ -1,5 +0,0 @@ -from . import cli - -if __name__ == '__main__': - import sys - sys.exit(cli(sys.argv[1:])) From 1c777e36c0cb45b566944e6da33aced9f9666616 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Apr 2019 11:47:41 +0200 Subject: [PATCH 42/65] standalone publish is tray module now --- pype/standalonepublish/__init__.py | 3 +++ pype/standalonepublish/__main__.py | 5 +++++ pype/standalonepublish/asapublish_module.py | 15 +++++++++++++++ 3 files changed, 23 insertions(+) create mode 100644 pype/standalonepublish/__main__.py create mode 100644 pype/standalonepublish/asapublish_module.py diff --git a/pype/standalonepublish/__init__.py b/pype/standalonepublish/__init__.py index 41bc69b9a2..cc6f33d47e 100644 --- a/pype/standalonepublish/__init__.py +++ b/pype/standalonepublish/__init__.py @@ -7,3 +7,6 @@ __all__ = [ "show", "cli" ] + +def tray_init(tray_widget, main_widget): + return ASAPublishModule(main_widget, tray_widget) diff --git a/pype/standalonepublish/__main__.py b/pype/standalonepublish/__main__.py new file mode 100644 index 0000000000..d77bc585c5 --- /dev/null +++ b/pype/standalonepublish/__main__.py @@ -0,0 +1,5 @@ +from . import cli + +if __name__ == '__main__': + import sys + sys.exit(cli(sys.argv[1:])) diff --git a/pype/standalonepublish/asapublish_module.py b/pype/standalonepublish/asapublish_module.py new file mode 100644 index 0000000000..d695065601 --- /dev/null +++ b/pype/standalonepublish/asapublish_module.py @@ -0,0 +1,15 @@ +from .app import show +from .widgets import QtWidgets + + +class ASAPublishModule: + def __init__(self, main_parent=None, parent=None): + self.main_parent = main_parent + self.parent_widget = parent + + def tray_menu(self, parent_menu): + self.run_action = QtWidgets.QAction( + "ASAPublish", parent_menu + ) + self.run_action.triggered.connect(show) + parent_menu.addAction(self.run_action) From 8cf349df975c2021c9f8c9c9f1a909ed94d8a92d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 26 Apr 2019 17:19:52 +0200 Subject: [PATCH 43/65] fix from develop branch --- pype/ftrack/actions/action_application_loader.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pype/ftrack/actions/action_application_loader.py b/pype/ftrack/actions/action_application_loader.py index bc126fc691..1b0f48f9be 100644 --- a/pype/ftrack/actions/action_application_loader.py +++ b/pype/ftrack/actions/action_application_loader.py @@ -1,3 +1,4 @@ +import os import toml import time from pype.ftrack import AppAction From 6e1a4a998768e787cf7961c71eb44cb421f66251 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 26 Apr 2019 17:49:55 +0200 Subject: [PATCH 44/65] pyblish is launched through avalon tools now --- pype/standalonepublish/publish.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pype/standalonepublish/publish.py b/pype/standalonepublish/publish.py index 215281bfaf..f16fbdd0d8 100644 --- a/pype/standalonepublish/publish.py +++ b/pype/standalonepublish/publish.py @@ -7,6 +7,7 @@ import string from avalon import io from avalon import api as avalon +from avalon.tools import publish as av_publish import pype from pypeapp import execute @@ -70,14 +71,14 @@ def publish(data, gui=True): "-pp", os.pathsep.join(pyblish.api.registered_paths()) ] - if gui: - args += ["gui"] - os.environ["PYBLISH_HOSTS"] = "shell" os.environ["ASAPUBLISH_INPATH"] = json_data_path - returncode = execute([ - sys.executable, "-u", "-m", "pyblish" - ] + args, env=os.environ) + if gui: + av_publish.show() + else: + returncode = execute([ + sys.executable, "-u", "-m", "pyblish" + ] + args, env=os.environ) io.uninstall() From cdb24724c1830f2f916808cce9e9cb6a908b0fd6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 26 Apr 2019 17:57:45 +0200 Subject: [PATCH 45/65] changed name od module --- pype/standalonepublish/__init__.py | 4 ++-- .../{asapublish_module.py => standalonepublish_module.py} | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename pype/standalonepublish/{asapublish_module.py => standalonepublish_module.py} (85%) diff --git a/pype/standalonepublish/__init__.py b/pype/standalonepublish/__init__.py index cc6f33d47e..c7be80f189 100644 --- a/pype/standalonepublish/__init__.py +++ b/pype/standalonepublish/__init__.py @@ -1,4 +1,4 @@ -from .asapublish_module import ASAPublishModule +from .standalonepublish_module import StandAlonePublishModule from .app import ( show, cli @@ -9,4 +9,4 @@ __all__ = [ ] def tray_init(tray_widget, main_widget): - return ASAPublishModule(main_widget, tray_widget) + return StandAlonePublishModule(main_widget, tray_widget) diff --git a/pype/standalonepublish/asapublish_module.py b/pype/standalonepublish/standalonepublish_module.py similarity index 85% rename from pype/standalonepublish/asapublish_module.py rename to pype/standalonepublish/standalonepublish_module.py index d695065601..8fce2810b7 100644 --- a/pype/standalonepublish/asapublish_module.py +++ b/pype/standalonepublish/standalonepublish_module.py @@ -2,14 +2,14 @@ from .app import show from .widgets import QtWidgets -class ASAPublishModule: +class StandAlonePublishModule: def __init__(self, main_parent=None, parent=None): self.main_parent = main_parent self.parent_widget = parent def tray_menu(self, parent_menu): self.run_action = QtWidgets.QAction( - "ASAPublish", parent_menu + "Publish", parent_menu ) self.run_action.triggered.connect(show) parent_menu.addAction(self.run_action) From c47517a1f060ff627401ec173a08d370b176f84f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 26 Apr 2019 17:58:14 +0200 Subject: [PATCH 46/65] representations have key representations instead of components --- pype/standalonepublish/widgets/widget_drop_frame.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/standalonepublish/widgets/widget_drop_frame.py b/pype/standalonepublish/widgets/widget_drop_frame.py index 90434e75f4..dd9072448b 100644 --- a/pype/standalonepublish/widgets/widget_drop_frame.py +++ b/pype/standalonepublish/widgets/widget_drop_frame.py @@ -367,7 +367,7 @@ class DropDataFrame(QtWidgets.QFrame): self.parent_widget.working_stop() def collect_data(self): - data = {'components' : []} + data = {'representations' : []} for item in self.items: - data['components'].append(item.collect_data()) + data['representations'].append(item.collect_data()) return data From 8553b042d562c709cd087f55772dce7fae4d0b67 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 26 Apr 2019 18:09:21 +0200 Subject: [PATCH 47/65] hide shadow widget only if is visible --- pype/standalonepublish/app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pype/standalonepublish/app.py b/pype/standalonepublish/app.py index 1a875505d7..31a7763ace 100644 --- a/pype/standalonepublish/app.py +++ b/pype/standalonepublish/app.py @@ -153,7 +153,8 @@ class Window(QtWidgets.QDialog): QtWidgets.QApplication.processEvents() def working_stop(self): - self.shadow_widget.setVisible(False) + if self.shadow_widget.isVisible(): + self.shadow_widget.setVisible(False) def validation(self): if not self.initialized: From 362c1248f85f87d379c9985273e624e6f6127030 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Sat, 27 Apr 2019 15:10:08 +0200 Subject: [PATCH 48/65] temporarily fixed crashing on merge/split --- pype/standalonepublish/widgets/widget_drop_frame.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pype/standalonepublish/widgets/widget_drop_frame.py b/pype/standalonepublish/widgets/widget_drop_frame.py index dd9072448b..644f53a732 100644 --- a/pype/standalonepublish/widgets/widget_drop_frame.py +++ b/pype/standalonepublish/widgets/widget_drop_frame.py @@ -11,6 +11,7 @@ class DropDataFrame(QtWidgets.QFrame): super().__init__() self.parent_widget = parent self.items = [] + self.removed = [] self.presets = config.get_presets()['standalone_publish'] self.setAcceptDrops(True) @@ -117,6 +118,7 @@ class DropDataFrame(QtWidgets.QFrame): index = self.components_list.widget_index(item) self.components_list.remove_widget(index) if item in self.items: + self.removed.append(item) self.items.remove(item) self._refresh_view() From 447c63fc331d1eb7b740a5037a972a0ed8d4bca4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 2 May 2019 11:33:46 +0200 Subject: [PATCH 49/65] standalone publisher now handles duplicated representation names --- pype/standalonepublish/app.py | 16 +---- .../widgets/widget_component_item.py | 13 ++++ .../widgets/widget_components.py | 26 ++++++-- .../widgets/widget_drop_frame.py | 60 +++++++++++++++++-- 4 files changed, 93 insertions(+), 22 deletions(-) diff --git a/pype/standalonepublish/app.py b/pype/standalonepublish/app.py index 31a7763ace..ba7dcaa978 100644 --- a/pype/standalonepublish/app.py +++ b/pype/standalonepublish/app.py @@ -92,10 +92,9 @@ class Window(QtWidgets.QDialog): return self._db def on_start(self): - self.initialized = True # Refresh asset input in Family widget self.on_asset_changed() - self.validation() + self.widget_components.validation() self.shadow_widget = ShadowWidget(self) self.shadow_widget.setVisible(False) @@ -156,19 +155,10 @@ class Window(QtWidgets.QDialog): if self.shadow_widget.isVisible(): self.shadow_widget.setVisible(False) - def validation(self): - if not self.initialized: - return - valid = self.valid_family and self.valid_components - self.widget_components.set_valid(valid) - def set_valid_family(self, valid): self.valid_family = valid - self.validation() - - def set_valid_components(self, valid): - self.valid_components = valid - self.validation() + if hasattr(self, 'widget_components'): + self.widget_components.validation() def collect_data(self): data = {} diff --git a/pype/standalonepublish/widgets/widget_component_item.py b/pype/standalonepublish/widgets/widget_component_item.py index 6dec892d91..2e0df9a00c 100644 --- a/pype/standalonepublish/widgets/widget_component_item.py +++ b/pype/standalonepublish/widgets/widget_component_item.py @@ -13,9 +13,11 @@ class ComponentItem(QtWidgets.QFrame): signal_remove = QtCore.Signal(object) signal_thumbnail = QtCore.Signal(object) signal_preview = QtCore.Signal(object) + signal_repre_change = QtCore.Signal(object, object) def __init__(self, parent, main_parent): super().__init__() + self.has_valid_repre = True self.actions = [] self.resize(290, 70) self.setMinimumSize(QtCore.QSize(0, 70)) @@ -183,6 +185,7 @@ class ComponentItem(QtWidgets.QFrame): self.remove.clicked.connect(self._remove) self.thumbnail.clicked.connect(self._thumbnail_clicked) self.preview.clicked.connect(self._preview_clicked) + self.input_repre.textChanged.connect(self._handle_duplicate_repre) name = data['name'] representation = data['representation'] ext = data['ext'] @@ -238,6 +241,13 @@ class ComponentItem(QtWidgets.QFrame): self.btn_action_menu.clicked.connect(self.show_actions) self.action_menu.setStyleSheet(style.load_stylesheet()) + def set_repre_name_valid(self, valid): + self.has_valid_repre = valid + if valid: + self.input_repre.setStyleSheet("") + else: + self.input_repre.setStyleSheet("border: 1px solid red;") + def split_sequence(self): self.parent_widget.split_items(self) @@ -257,6 +267,9 @@ class ComponentItem(QtWidgets.QFrame): def _preview_clicked(self): self.signal_preview.emit(self) + def _handle_duplicate_repre(self, repre_name): + self.signal_repre_change.emit(self, repre_name) + def is_thumbnail(self): return self.thumbnail.checked diff --git a/pype/standalonepublish/widgets/widget_components.py b/pype/standalonepublish/widgets/widget_components.py index 5cc66de0b5..326aefe693 100644 --- a/pype/standalonepublish/widgets/widget_components.py +++ b/pype/standalonepublish/widgets/widget_components.py @@ -7,6 +7,11 @@ from .. import publish class ComponentsWidget(QtWidgets.QWidget): def __init__(self, parent): super().__init__() + self.initialized = False + self.valid_components = False + self.valid_family = False + self.valid_repre_names = False + body = QtWidgets.QWidget() self.parent_widget = parent self.drop_frame = DropDataFrame(self) @@ -39,12 +44,25 @@ class ComponentsWidget(QtWidgets.QWidget): self.btn_browse.clicked.connect(self._browse) self.btn_publish.clicked.connect(self._publish) + self.initialized = True - def set_valid(self, in_bool): - self.btn_publish.setEnabled(in_bool) + def validation(self): + if self.initialized is False: + return + valid = ( + self.parent_widget.valid_family and + self.valid_components and + self.valid_repre_names + ) + self.btn_publish.setEnabled(valid) - def set_valid_components(self, in_bool): - self.parent_widget.set_valid_components(in_bool) + def set_valid_components(self, valid): + self.valid_components = valid + self.validation() + + def set_valid_repre_names(self, valid): + self.valid_repre_names = valid + self.validation() def process_mime_data(self, mime_data): self.drop_frame.process_ent_mime(mime_data) diff --git a/pype/standalonepublish/widgets/widget_drop_frame.py b/pype/standalonepublish/widgets/widget_drop_frame.py index 644f53a732..89c4352717 100644 --- a/pype/standalonepublish/widgets/widget_drop_frame.py +++ b/pype/standalonepublish/widgets/widget_drop_frame.py @@ -1,4 +1,5 @@ import os +import re import clique import subprocess from pypeapp import config @@ -83,6 +84,7 @@ class DropDataFrame(QtWidgets.QFrame): new_component.signal_thumbnail.connect( self._set_thumbnail ) + new_component.signal_repre_change.connect(self.repre_name_changed) for action in actions: new_component.add_action(action) @@ -114,13 +116,19 @@ class DropDataFrame(QtWidgets.QFrame): checked_item.change_preview(False) in_item.change_preview() - def _remove_item(self, item): - index = self.components_list.widget_index(item) + def _remove_item(self, in_item): + index = self.components_list.widget_index(in_item) self.components_list.remove_widget(index) - if item in self.items: - self.removed.append(item) - self.items.remove(item) + if in_item in self.items: + self.removed.append(in_item) + self.items.remove(in_item) self._refresh_view() + if in_item.has_valid_repre: + return + for item in self.items: + if item.has_valid_repre: + continue + self.repre_name_changed(item, item.input_repre.text()) def _refresh_view(self): _bool = len(self.items) == 0 @@ -338,8 +346,50 @@ class DropDataFrame(QtWidgets.QFrame): actions.append('split') if found is False: + new_repre = self.handle_new_repre_name(data['representation']) + data['representation'] = new_repre self._add_item(data, actions) + def handle_new_repre_name(self, repre_name): + renamed = False + for item in self.items: + if repre_name == item.input_repre.text(): + check_regex = '\(\w+\)$' + result = re.findall(check_regex, repre_name) + next_num = 2 + if len(result) == 1: + repre_name = repre_name.replace(result[0], '') + next_num = int(result[0].replace('(', '').replace(')', '')) + next_num += 1 + repre_name = '{}({})'.format(repre_name, next_num) + renamed = True + break + if renamed: + return self.handle_new_repre_name(repre_name) + return repre_name + + def repre_name_changed(self, in_item, repre_name): + is_valid = True + for item in self.items: + if item == in_item: + continue + if item.input_repre.text() == repre_name: + item.set_repre_name_valid(False) + in_item.set_repre_name_valid(False) + is_valid = False + global_valid = is_valid + if is_valid: + in_item.set_repre_name_valid(True) + for item in self.items: + if item.has_valid_repre: + continue + self.repre_name_changed(item, item.input_repre.text()) + for item in self.items: + if not item.has_valid_repre: + global_valid = False + break + self.parent_widget.set_valid_repre_names(global_valid) + def merge_items(self, in_item): self.parent_widget.working_start() items = [] From 060b0e3008f5baeed3c385083ed3ccff7561a24e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 2 May 2019 11:37:08 +0200 Subject: [PATCH 50/65] all video and image files can be set as thumbnail now --- pype/standalonepublish/widgets/widget_drop_frame.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/standalonepublish/widgets/widget_drop_frame.py b/pype/standalonepublish/widgets/widget_drop_frame.py index 89c4352717..5aa9d7ea61 100644 --- a/pype/standalonepublish/widgets/widget_drop_frame.py +++ b/pype/standalonepublish/widgets/widget_drop_frame.py @@ -277,8 +277,8 @@ class DropDataFrame(QtWidgets.QFrame): icon += 's' data['icon'] = icon data['thumb'] = ( - ext in self.presets['extensions']['thumbnailable'] and - data['is_sequence'] is False + ext in self.presets['extensions']['image_file'] or + ext in self.presets['extensions']['video_file'] ) data['prev'] = ext in self.presets['extensions']['video_file'] From aecbd28fe6dfe639ef04ee8bf18ddce298546358 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 2 May 2019 12:13:49 +0200 Subject: [PATCH 51/65] standalone publisher does not allow empty representation name --- .../widgets/widget_drop_frame.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/pype/standalonepublish/widgets/widget_drop_frame.py b/pype/standalonepublish/widgets/widget_drop_frame.py index 5aa9d7ea61..fa8ec66b49 100644 --- a/pype/standalonepublish/widgets/widget_drop_frame.py +++ b/pype/standalonepublish/widgets/widget_drop_frame.py @@ -370,13 +370,17 @@ class DropDataFrame(QtWidgets.QFrame): def repre_name_changed(self, in_item, repre_name): is_valid = True - for item in self.items: - if item == in_item: - continue - if item.input_repre.text() == repre_name: - item.set_repre_name_valid(False) - in_item.set_repre_name_valid(False) - is_valid = False + if repre_name.strip() == '': + in_item.set_repre_name_valid(False) + is_valid = False + else: + for item in self.items: + if item == in_item: + continue + if item.input_repre.text() == repre_name: + item.set_repre_name_valid(False) + in_item.set_repre_name_valid(False) + is_valid = False global_valid = is_valid if is_valid: in_item.set_repre_name_valid(True) From 36f5c91d6a80feb77823ac15183a6cb064f1b4ae Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 2 May 2019 12:15:00 +0200 Subject: [PATCH 52/65] valid repre name is set to True on first representations --- pype/standalonepublish/widgets/widget_drop_frame.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pype/standalonepublish/widgets/widget_drop_frame.py b/pype/standalonepublish/widgets/widget_drop_frame.py index fa8ec66b49..cd5d8fc29e 100644 --- a/pype/standalonepublish/widgets/widget_drop_frame.py +++ b/pype/standalonepublish/widgets/widget_drop_frame.py @@ -89,7 +89,8 @@ class DropDataFrame(QtWidgets.QFrame): new_component.add_action(action) self.items.append(new_component) - + if len(self.items) == 1: + self.parent_widget.set_valid_repre_names(True) self._refresh_view() def _set_thumbnail(self, in_item): From acf1b44b8a98f66b92046f3ea7356a716219f411 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 2 May 2019 15:51:22 +0200 Subject: [PATCH 53/65] basic docstrings --- pype/standalonepublish/app.py | 46 ++++++++++++++++++++++++++++--- pype/standalonepublish/publish.py | 14 ++++++++++ 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/pype/standalonepublish/app.py b/pype/standalonepublish/app.py index ba7dcaa978..8956155230 100644 --- a/pype/standalonepublish/app.py +++ b/pype/standalonepublish/app.py @@ -14,6 +14,11 @@ module = sys.modules[__name__] module.window = None class Window(QtWidgets.QDialog): + """Main window of Standalone publisher. + + :param parent: Main widget that cares about all GUIs + :type parent: QtWidgets.QMainWindow + """ _db = DbConnector() _jobs = {} valid_family = False @@ -24,7 +29,7 @@ class Window(QtWidgets.QDialog): NOT_SELECTED = '< Nothing is selected >' def __init__(self, parent=None): - super(Window, self).__init__() + super(Window, self).__init__(parent=parent) self._db.install() self.setWindowTitle("Standalone Publish") @@ -89,16 +94,23 @@ class Window(QtWidgets.QDialog): @property def db(self): + ''' Returns DB object for MongoDB I/O + ''' return self._db def on_start(self): + ''' Things must be done when initilized. + ''' # Refresh asset input in Family widget self.on_asset_changed() self.widget_components.validation() + # Initializing shadow widget self.shadow_widget = ShadowWidget(self) self.shadow_widget.setVisible(False) def resizeEvent(self, event=None): + ''' Helps resize shadow widget + ''' position_x = (self.frameGeometry().width()-self.shadow_widget.frameGeometry().width())/2 position_y = (self.frameGeometry().height()-self.shadow_widget.frameGeometry().height())/2 self.shadow_widget.move(position_x, position_y) @@ -109,6 +121,8 @@ class Window(QtWidgets.QDialog): super().resizeEvent(event) def get_avalon_parent(self, entity): + ''' Avalon DB entities helper - get all parents (exclude project). + ''' parent_id = entity['data']['visualParent'] parents = [] if parent_id is not None: @@ -118,15 +132,19 @@ class Window(QtWidgets.QDialog): return parents def echo(self, message): + ''' Shows message in label that disappear in 5s + :param message: Message that will be displayed + :type message: str + ''' self.label_message.setText(str(message)) QtCore.QTimer.singleShot(5000, lambda: self.label_message.setText("")) def on_asset_changed(self): - """Callback on asset selection changed + '''Callback on asset selection changed - This updates the task view. + Updates the task view. - """ + ''' selected = self.widget_assets.get_selected_assets() if len(selected) == 1: self.valid_parent = True @@ -138,12 +156,22 @@ class Window(QtWidgets.QDialog): self.widget_family.on_data_changed() def keyPressEvent(self, event): + ''' Handling Ctrl+V KeyPress event + Can handle: + - files/folders in clipboard (tested only on Windows OS) + - copied path of file/folder in clipboard ('c:/path/to/folder') + ''' if event.key() == QtCore.Qt.Key_V and event.modifiers() == QtCore.Qt.ControlModifier: clip = QtWidgets.QApplication.clipboard() self.widget_components.process_mime_data(clip) super().keyPressEvent(event) def working_start(self, msg=None): + ''' Shows shadowed foreground with message + :param msg: Message that will be displayed + (set to `Please wait...` if `None` entered) + :type msg: str + ''' if msg is None: msg = 'Please wait...' self.shadow_widget.message = msg @@ -152,15 +180,25 @@ class Window(QtWidgets.QDialog): QtWidgets.QApplication.processEvents() def working_stop(self): + ''' Hides shadowed foreground + ''' if self.shadow_widget.isVisible(): self.shadow_widget.setVisible(False) def set_valid_family(self, valid): + ''' Sets `valid_family` attribute for validation + + .. note:: + if set to `False` publishing is not possible + ''' self.valid_family = valid + # If widget_components not initialized yet if hasattr(self, 'widget_components'): self.widget_components.validation() def collect_data(self): + ''' Collecting necessary data for pyblish from child widgets + ''' data = {} data.update(self.widget_assets.collect_data()) data.update(self.widget_family.collect_data()) diff --git a/pype/standalonepublish/publish.py b/pype/standalonepublish/publish.py index f16fbdd0d8..0e811aae52 100644 --- a/pype/standalonepublish/publish.py +++ b/pype/standalonepublish/publish.py @@ -15,7 +15,9 @@ from pypeapp import execute import pyblish.api +# Registers Global pyblish plugins pype.install() +# Registers Standalone pyblish plugins PUBLISH_PATH = os.path.sep.join( [pype.PLUGINS_DIR, 'standalonepublish', 'publish'] ) @@ -23,6 +25,12 @@ pyblish.api.register_plugin_path(PUBLISH_PATH) def set_context(project, asset, app): + ''' Sets context for pyblish (must be done before pyblish is launched) + :param project: Name of `Project` where instance should be published + :type project: str + :param asset: Name of `Asset` where instance should be published + :type asset: str + ''' os.environ["AVALON_PROJECT"] = project io.Session["AVALON_PROJECT"] = project os.environ["AVALON_ASSET"] = asset @@ -56,6 +64,12 @@ def set_context(project, asset, app): def publish(data, gui=True): + ''' Launches Pyblish (GUI by default) + :param data: Should include data for pyblish and standalone collector + :type data: dict + :param gui: Pyblish will be launched in GUI mode if set to True + :type gui: bool + ''' io.install() # Create hash name folder in temp From f330a062c789ce7ad0636e8b3d4b7bdf6047c5aa Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 2 May 2019 15:56:34 +0200 Subject: [PATCH 54/65] fixed bug: crashing when publisher is closed before echo message disappered --- pype/standalonepublish/app.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pype/standalonepublish/app.py b/pype/standalonepublish/app.py index 8956155230..956cdb6300 100644 --- a/pype/standalonepublish/app.py +++ b/pype/standalonepublish/app.py @@ -137,7 +137,15 @@ class Window(QtWidgets.QDialog): :type message: str ''' self.label_message.setText(str(message)) - QtCore.QTimer.singleShot(5000, lambda: self.label_message.setText("")) + def clear_text(): + ''' Helps prevent crash if this Window object + is deleted before 5s passed + ''' + try: + self.label_message.set_text("") + except: + pass + QtCore.QTimer.singleShot(5000, lambda: clear_text()) def on_asset_changed(self): '''Callback on asset selection changed From 4d4226b1290f542f1ba17ff60fd021f138083c79 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 2 May 2019 15:57:13 +0200 Subject: [PATCH 55/65] created show method in module class so parent is set --- pype/standalonepublish/standalonepublish_module.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pype/standalonepublish/standalonepublish_module.py b/pype/standalonepublish/standalonepublish_module.py index 8fce2810b7..703f457138 100644 --- a/pype/standalonepublish/standalonepublish_module.py +++ b/pype/standalonepublish/standalonepublish_module.py @@ -11,5 +11,8 @@ class StandAlonePublishModule: self.run_action = QtWidgets.QAction( "Publish", parent_menu ) - self.run_action.triggered.connect(show) + self.run_action.triggered.connect(self.show) parent_menu.addAction(self.run_action) + + def show(self): + show(self.main_parent, False) From b5e0069049fecff284a433859e9ab31b80403bdb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 2 May 2019 15:58:34 +0200 Subject: [PATCH 56/65] renamed ASAPUBLISH_INPATH environment to SAPUBLISH_INPATH --- pype/plugins/standalonepublish/publish/collect_context.py | 2 +- pype/standalonepublish/publish.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/plugins/standalonepublish/publish/collect_context.py b/pype/plugins/standalonepublish/publish/collect_context.py index d063bcf2dd..26411dd132 100644 --- a/pype/plugins/standalonepublish/publish/collect_context.py +++ b/pype/plugins/standalonepublish/publish/collect_context.py @@ -29,7 +29,7 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin): def process(self, context): # get json paths from os and load them io.install() - json_path = os.environ.get("ASAPUBLISH_INPATH") + json_path = os.environ.get("SAPUBLISH_INPATH") with open(json_path, "r") as f: in_data = json.load(f) diff --git a/pype/standalonepublish/publish.py b/pype/standalonepublish/publish.py index 0e811aae52..db41f68eb4 100644 --- a/pype/standalonepublish/publish.py +++ b/pype/standalonepublish/publish.py @@ -86,7 +86,7 @@ def publish(data, gui=True): ] os.environ["PYBLISH_HOSTS"] = "shell" - os.environ["ASAPUBLISH_INPATH"] = json_data_path + os.environ["SAPUBLISH_INPATH"] = json_data_path if gui: av_publish.show() From 27fb79f9f30ea9bd66e54070b5f566efa67517d2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 6 May 2019 17:22:41 +0200 Subject: [PATCH 57/65] fixed representations key in collector --- pype/plugins/standalonepublish/publish/collect_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/standalonepublish/publish/collect_context.py b/pype/plugins/standalonepublish/publish/collect_context.py index 26411dd132..38ceb4dbd1 100644 --- a/pype/plugins/standalonepublish/publish/collect_context.py +++ b/pype/plugins/standalonepublish/publish/collect_context.py @@ -51,7 +51,7 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin): instances = [] - for component in in_data['components']: + for component in in_data['representations']: instance = context.create_instance(subset) # instance.add(node) From a72c517bd275876e22da8272a6362914303bbb6a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 6 May 2019 17:22:55 +0200 Subject: [PATCH 58/65] added output json path to collector --- .../standalonepublish/publish/collect_context.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pype/plugins/standalonepublish/publish/collect_context.py b/pype/plugins/standalonepublish/publish/collect_context.py index 38ceb4dbd1..52b44f61e1 100644 --- a/pype/plugins/standalonepublish/publish/collect_context.py +++ b/pype/plugins/standalonepublish/publish/collect_context.py @@ -29,11 +29,15 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin): def process(self, context): # get json paths from os and load them io.install() - json_path = os.environ.get("SAPUBLISH_INPATH") - with open(json_path, "r") as f: + input_json_path = os.environ.get("SAPUBLISH_INPATH") + output_json_path = os.environ.get("SAPUBLISH_OUTPATH") + + context.data["stagingDir"] = os.path.dirname(input_json_path) + context.data["returnJsonPath"] = output_json_path + + with open(input_json_path, "r") as f: in_data = json.load(f) - context.data["stagingDir"] = os.path.dirname(json_path) project_name = in_data['project'] asset_name = in_data['asset'] family = in_data['family'] From ed2fb8b7cddb782a416f92058c6855832b18fde0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 6 May 2019 18:32:58 +0200 Subject: [PATCH 59/65] fixed removing representations from list --- .../widgets/widget_components_list.py | 1 - .../widgets/widget_drop_frame.py | 39 +++++++++---------- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/pype/standalonepublish/widgets/widget_components_list.py b/pype/standalonepublish/widgets/widget_components_list.py index 357bd1e671..f85e9f0aa6 100644 --- a/pype/standalonepublish/widgets/widget_components_list.py +++ b/pype/standalonepublish/widgets/widget_components_list.py @@ -46,7 +46,6 @@ class ComponentsList(QtWidgets.QTableWidget): return row def remove_widget(self, row): - self.removeCellWidget(row, self._main_column) self.removeRow(row) def move_widget(self, widget, newRow): diff --git a/pype/standalonepublish/widgets/widget_drop_frame.py b/pype/standalonepublish/widgets/widget_drop_frame.py index cd5d8fc29e..0048989707 100644 --- a/pype/standalonepublish/widgets/widget_drop_frame.py +++ b/pype/standalonepublish/widgets/widget_drop_frame.py @@ -11,8 +11,6 @@ class DropDataFrame(QtWidgets.QFrame): def __init__(self, parent): super().__init__() self.parent_widget = parent - self.items = [] - self.removed = [] self.presets = config.get_presets()['standalone_publish'] self.setAcceptDrops(True) @@ -88,14 +86,13 @@ class DropDataFrame(QtWidgets.QFrame): for action in actions: new_component.add_action(action) - self.items.append(new_component) - if len(self.items) == 1: + if len(self.components_list.widgets()) == 1: self.parent_widget.set_valid_repre_names(True) self._refresh_view() def _set_thumbnail(self, in_item): checked_item = None - for item in self.items: + for item in self.components_list.widgets(): if item.is_thumbnail(): checked_item = item break @@ -107,7 +104,7 @@ class DropDataFrame(QtWidgets.QFrame): def _set_preview(self, in_item): checked_item = None - for item in self.items: + for item in self.components_list.widgets(): if item.is_preview(): checked_item = item break @@ -118,21 +115,21 @@ class DropDataFrame(QtWidgets.QFrame): in_item.change_preview() def _remove_item(self, in_item): - index = self.components_list.widget_index(in_item) - self.components_list.remove_widget(index) - if in_item in self.items: - self.removed.append(in_item) - self.items.remove(in_item) + valid_repre = in_item.has_valid_repre is True + + self.components_list.remove_widget( + self.components_list.widget_index(in_item) + ) self._refresh_view() - if in_item.has_valid_repre: + if valid_repre: return - for item in self.items: + for item in self.components_list.widgets(): if item.has_valid_repre: continue self.repre_name_changed(item, item.input_repre.text()) def _refresh_view(self): - _bool = len(self.items) == 0 + _bool = len(self.components_list.widgets()) == 0 self.components_list.setVisible(not _bool) self.drop_widget.setVisible(_bool) @@ -287,7 +284,7 @@ class DropDataFrame(QtWidgets.QFrame): new_is_seq = data['is_sequence'] found = False - for item in self.items: + for item in self.components_list.widgets(): if data['ext'] != item.in_data['ext']: continue if data['folder_path'] != item.in_data['folder_path']: @@ -353,7 +350,7 @@ class DropDataFrame(QtWidgets.QFrame): def handle_new_repre_name(self, repre_name): renamed = False - for item in self.items: + for item in self.components_list.widgets(): if repre_name == item.input_repre.text(): check_regex = '\(\w+\)$' result = re.findall(check_regex, repre_name) @@ -375,7 +372,7 @@ class DropDataFrame(QtWidgets.QFrame): in_item.set_repre_name_valid(False) is_valid = False else: - for item in self.items: + for item in self.components_list.widgets(): if item == in_item: continue if item.input_repre.text() == repre_name: @@ -385,11 +382,11 @@ class DropDataFrame(QtWidgets.QFrame): global_valid = is_valid if is_valid: in_item.set_repre_name_valid(True) - for item in self.items: + for item in self.components_list.widgets(): if item.has_valid_repre: continue self.repre_name_changed(item, item.input_repre.text()) - for item in self.items: + for item in self.components_list.widgets(): if not item.has_valid_repre: global_valid = False break @@ -400,7 +397,7 @@ class DropDataFrame(QtWidgets.QFrame): items = [] in_paths = in_item.in_data['files'] paths = in_paths - for item in self.items: + for item in self.components_list.widgets(): if item.in_data['files'] == in_paths: items.append(item) continue @@ -425,6 +422,6 @@ class DropDataFrame(QtWidgets.QFrame): def collect_data(self): data = {'representations' : []} - for item in self.items: + for item in self.components_list.widgets(): data['representations'].append(item.collect_data()) return data From 2005484fe18f937596c1a3d58b319be9491bad59 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 6 May 2019 18:34:55 +0200 Subject: [PATCH 60/65] widget components is ready for handling publish result --- pype/standalonepublish/widgets/widget_components.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pype/standalonepublish/widgets/widget_components.py b/pype/standalonepublish/widgets/widget_components.py index 326aefe693..1e1fdf88e3 100644 --- a/pype/standalonepublish/widgets/widget_components.py +++ b/pype/standalonepublish/widgets/widget_components.py @@ -119,6 +119,10 @@ class ComponentsWidget(QtWidgets.QWidget): publish.set_context( data['project'], data['asset'], 'standalonepublish' ) - publish.publish(data) + result = publish.publish(data) + # Clear widgets from components list if publishing was successful + if result: + self.drop_frame.components_list.clear_widgets() + self.drop_frame._refresh_view() finally: self.working_stop() From 372ed195f562fe45bfb39bd2176fc4fe82a342b0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 6 May 2019 18:36:03 +0200 Subject: [PATCH 61/65] cli pyblish moved back, avalon_api_pyblish launch kept for sure --- pype/standalonepublish/publish.py | 52 ++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/pype/standalonepublish/publish.py b/pype/standalonepublish/publish.py index db41f68eb4..05c889a2d6 100644 --- a/pype/standalonepublish/publish.py +++ b/pype/standalonepublish/publish.py @@ -64,6 +64,13 @@ def set_context(project, asset, app): def publish(data, gui=True): + # cli pyblish seems like better solution + return cli_publish(data, gui) + # # this uses avalon pyblish launch tool + # avalon_api_publish(data, gui) + + +def avalon_api_publish(data, gui=True): ''' Launches Pyblish (GUI by default) :param data: Should include data for pyblish and standalone collector :type data: dict @@ -74,7 +81,7 @@ def publish(data, gui=True): # Create hash name folder in temp chars = "".join( [random.choice(string.ascii_letters) for i in range(15)] ) - staging_dir = tempfile.mkdtemp(chars)#.replace("\\", "/") + staging_dir = tempfile.mkdtemp(chars) # create also json and fill with data json_data_path = staging_dir + os.path.basename(staging_dir) + '.json' @@ -96,3 +103,46 @@ def publish(data, gui=True): ] + args, env=os.environ) io.uninstall() + + +def cli_publish(data, gui=True): + io.install() + + # Create hash name folder in temp + chars = "".join( [random.choice(string.ascii_letters) for i in range(15)] ) + staging_dir = tempfile.mkdtemp(chars) + + # create json for return data + return_data_path = ( + staging_dir + os.path.basename(staging_dir) + 'return.json' + ) + # create also json and fill with data + json_data_path = staging_dir + os.path.basename(staging_dir) + '.json' + with open(json_data_path, 'w') as outfile: + json.dump(data, outfile) + + args = [ + "-pp", os.pathsep.join(pyblish.api.registered_paths()) + ] + + if gui: + args += ["gui"] + + os.environ["PYBLISH_HOSTS"] = "shell" + os.environ["SAPUBLISH_INPATH"] = json_data_path + os.environ["SAPUBLISH_OUTPATH"] = return_data_path + + returncode = execute([ + sys.executable, "-u", "-m", "pyblish" + ] + args, env=os.environ) + + result = {} + if os.path.exists(json_data_path): + with open(json_data_path, "r") as f: + result = json.load(f) + + io.uninstall() + # TODO: check if was pyblish successful + # if successful return True + print('Check result here') + return False From 1cc6081f754d134535d5e77a393f8b0c3e63c628 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 9 May 2019 17:45:52 +0200 Subject: [PATCH 62/65] add plugin for publishing --- .../publish/collect_context.py | 36 +- .../publish/collect_templates.py | 17 + .../standalonepublish/publish/collect_time.py | 12 + .../standalonepublish/publish/integrate.py | 429 ++++++++++++++++++ pype/standalonepublish/publish.py | 8 +- 5 files changed, 483 insertions(+), 19 deletions(-) create mode 100644 pype/plugins/standalonepublish/publish/collect_templates.py create mode 100644 pype/plugins/standalonepublish/publish/collect_time.py create mode 100644 pype/plugins/standalonepublish/publish/integrate.py diff --git a/pype/plugins/standalonepublish/publish/collect_context.py b/pype/plugins/standalonepublish/publish/collect_context.py index 52b44f61e1..26ebc642fb 100644 --- a/pype/plugins/standalonepublish/publish/collect_context.py +++ b/pype/plugins/standalonepublish/publish/collect_context.py @@ -50,28 +50,28 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin): }) context.data['project'] = project context.data['asset'] = asset - context.data['family'] = family - context.data['subset'] = subset - instances = [] + instance = context.create_instance(subset) + + instance.data.update({ + "subset": family + subset, + "asset": asset_name, + "label": family + subset, + "name": family + subset, + "family": family, + "families": [family, 'ftrack'], + }) + self.log.info("collected instance: {}".format(instance.data)) + + instance.data["files"] = list() + instance.data['destination_list'] = list() for component in in_data['representations']: - instance = context.create_instance(subset) # instance.add(node) - instance.data.update({ - "subset": subset, - "asset": asset_name, - "label": component['label'], - "name": component['representation'], - "subset": subset, - "family": family, - "is_thumbnail": component['thumbnail'], - "is_preview": component['preview'] - }) + instance.data["files"].append(component['files']) + instance.data['destination_list'].append(component['files']) + # "is_thumbnail": component['thumbnail'], + # "is_preview": component['preview'] - self.log.info("collected instance: {}".format(instance.data)) - instances.append(instance) - - context.data["instances"] = instances self.log.info(in_data) diff --git a/pype/plugins/standalonepublish/publish/collect_templates.py b/pype/plugins/standalonepublish/publish/collect_templates.py new file mode 100644 index 0000000000..b59b20892b --- /dev/null +++ b/pype/plugins/standalonepublish/publish/collect_templates.py @@ -0,0 +1,17 @@ + +import pype.api as pype +from pypeapp import Anatomy + +import pyblish.api + + +class CollectTemplates(pyblish.api.ContextPlugin): + """Inject the current working file into context""" + + order = pyblish.api.CollectorOrder + label = "Collect Templates" + + def process(self, context): + # pype.load_data_from_templates() + context.data['anatomy'] = Anatomy() + self.log.info("Anatomy templates collected...") diff --git a/pype/plugins/standalonepublish/publish/collect_time.py b/pype/plugins/standalonepublish/publish/collect_time.py new file mode 100644 index 0000000000..e0adc7dfc3 --- /dev/null +++ b/pype/plugins/standalonepublish/publish/collect_time.py @@ -0,0 +1,12 @@ +import pyblish.api +from avalon import api + + +class CollectTime(pyblish.api.ContextPlugin): + """Store global time at the time of publish""" + + label = "Collect Current Time" + order = pyblish.api.CollectorOrder + + def process(self, context): + context.data["time"] = api.time() diff --git a/pype/plugins/standalonepublish/publish/integrate.py b/pype/plugins/standalonepublish/publish/integrate.py new file mode 100644 index 0000000000..7157a6fb1a --- /dev/null +++ b/pype/plugins/standalonepublish/publish/integrate.py @@ -0,0 +1,429 @@ +import os +import logging +import shutil + +import errno +import pyblish.api +from avalon import api, io +from avalon.vendor import filelink + + +log = logging.getLogger(__name__) + + +class IntegrateAsset(pyblish.api.InstancePlugin): + """Resolve any dependency issies + + This plug-in resolves any paths which, if not updated might break + the published file. + + The order of families is important, when working with lookdev you want to + first publish the texture, update the texture paths in the nodes and then + publish the shading network. Same goes for file dependent assets. + """ + + label = "Integrate Asset" + order = pyblish.api.IntegratorOrder + families = ["animation", + "camera", + "look", + "mayaAscii", + "model", + "pointcache", + "vdbcache", + "setdress", + "assembly", + "layout", + "rig", + "vrayproxy", + "yetiRig", + "yeticache", + "nukescript", + "review", + "workfile", + "scene", + "ass"] + exclude_families = ["clip"] + + def process(self, instance): + if [ef for ef in self.exclude_families + if instance.data["family"] in ef]: + return + + self.register(instance) + + self.log.info("Integrating Asset in to the database ...") + self.integrate(instance) + + def register(self, instance): + # Required environment variables + PROJECT = api.Session["AVALON_PROJECT"] + ASSET = instance.data.get("asset") or api.Session["AVALON_ASSET"] + LOCATION = api.Session["AVALON_LOCATION"] + + context = instance.context + # Atomicity + # + # Guarantee atomic publishes - each asset contains + # an identical set of members. + # __ + # / o + # / \ + # | o | + # \ / + # o __/ + # + assert all(result["success"] for result in context.data["results"]), ( + "Atomicity not held, aborting.") + + # Assemble + # + # | + # v + # ---> <---- + # ^ + # | + # + # stagingdir = instance.data.get("stagingDir") + # assert stagingdir, ("Incomplete instance \"%s\": " + # "Missing reference to staging area." % instance) + + # extra check if stagingDir actually exists and is available + + # self.log.debug("Establishing staging directory @ %s" % stagingdir) + + # Ensure at least one file is set up for transfer in staging dir. + files = instance.data.get("files", []) + assert files, "Instance has no files to transfer" + assert isinstance(files, (list, tuple)), ( + "Instance 'files' must be a list, got: {0}".format(files) + ) + + project = io.find_one({"type": "project"}) + + asset = io.find_one({"type": "asset", + "name": ASSET, + "parent": project["_id"]}) + + assert all([project, asset]), ("Could not find current project or " + "asset '%s'" % ASSET) + + subset = self.get_subset(asset, instance) + + # get next version + latest_version = io.find_one({"type": "version", + "parent": subset["_id"]}, + {"name": True}, + sort=[("name", -1)]) + + next_version = 1 + if latest_version is not None: + next_version += latest_version["name"] + + self.log.info("Verifying version from assumed destination") + + # assumed_data = instance.data["assumedTemplateData"] + # assumed_version = assumed_data["version"] + # if assumed_version != next_version: + # raise AttributeError("Assumed version 'v{0:03d}' does not match" + # "next version in database " + # "('v{1:03d}')".format(assumed_version, + # next_version)) + + self.log.debug("Next version: v{0:03d}".format(next_version)) + + version_data = self.create_version_data(context, instance) + version = self.create_version(subset=subset, + version_number=next_version, + locations=[LOCATION], + data=version_data) + + self.log.debug("Creating version ...") + version_id = io.insert_one(version).inserted_id + + # Write to disk + # _ + # | | + # _| |_ + # ____\ / + # |\ \ / \ + # \ \ v \ + # \ \________. + # \|________| + # + root = api.registered_root() + hierarchy = "" + parents = io.find_one({ + "type": 'asset', + "name": ASSET + })['data']['parents'] + if parents and len(parents) > 0: + # hierarchy = os.path.sep.join(hierarchy) + hierarchy = os.path.join(*parents) + + template_data = {"root": root, + "project": {"name": PROJECT, + "code": project['data']['code']}, + "silo": asset['silo'], + "asset": ASSET, + "family": instance.data['family'], + "subset": subset["name"], + "version": int(version["name"]), + "hierarchy": hierarchy} + + template_publish = project["config"]["template"]["publish"] + anatomy = instance.context.data['anatomy'] + + # Find the representations to transfer amongst the files + # Each should be a single representation (as such, a single extension) + representations = [] + destination_list = [] + if 'transfers' not in instance.data: + instance.data['transfers'] = [] + + for files in instance.data["files"]: + + # Collection + # _______ + # |______|\ + # | |\| + # | || + # | || + # | || + # |_______| + # + + if isinstance(files, list): + collection = files + # Assert that each member has identical suffix + _, ext = os.path.splitext(collection[0]) + assert all(ext == os.path.splitext(name)[1] + for name in collection), ( + "Files had varying suffixes, this is a bug" + ) + + # assert not any(os.path.isabs(name) for name in collection) + + template_data["representation"] = ext[1:] + + for fname in collection: + + src = fname + anatomy_filled = anatomy.format(template_data) + dst = anatomy_filled["publish"]["path"] + + instance.data["transfers"].append([src, dst]) + template = anatomy.templates["publish"]["path"] + + else: + # Single file + # _______ + # | |\ + # | | + # | | + # | | + # |_______| + # + fname = files + assert not os.path.isabs(fname), ( + "Given file name is a full path" + ) + _, ext = os.path.splitext(fname) + + template_data["representation"] = ext[1:] + + # src = os.path.join(stagingdir, fname) + src = fname + anatomy_filled = anatomy.format(template_data) + dst = anatomy_filled["publish"]["path"] + + instance.data["transfers"].append([src, dst]) + template = anatomy.templates["publish"]["path"] + + representation = { + "schema": "pype:representation-2.0", + "type": "representation", + "parent": version_id, + "name": ext[1:], + "data": {'path': dst, 'template': template}, + "dependencies": instance.data.get("dependencies", "").split(), + + # Imprint shortcut to context + # for performance reasons. + "context": { + "root": root, + "project": {"name": PROJECT, + "code": project['data']['code']}, + # 'task': api.Session["AVALON_TASK"], + "silo": asset['silo'], + "asset": ASSET, + "family": instance.data['family'], + "subset": subset["name"], + "version": version["name"], + "hierarchy": hierarchy, + "representation": ext[1:] + } + } + + destination_list.append(dst) + instance.data['destination_list'] = destination_list + representations.append(representation) + + self.log.info("Registering {} items".format(len(representations))) + + io.insert_many(representations) + + def integrate(self, instance): + """Move the files + + Through `instance.data["transfers"]` + + Args: + instance: the instance to integrate + """ + + transfers = instance.data.get("transfers", list()) + + for src, dest in transfers: + self.log.info("Copying file .. {} -> {}".format(src, dest)) + self.copy_file(src, dest) + + # Produce hardlinked copies + # Note: hardlink can only be produced between two files on the same + # server/disk and editing one of the two will edit both files at once. + # As such it is recommended to only make hardlinks between static files + # to ensure publishes remain safe and non-edited. + hardlinks = instance.data.get("hardlinks", list()) + for src, dest in hardlinks: + self.log.info("Hardlinking file .. {} -> {}".format(src, dest)) + self.hardlink_file(src, dest) + + def copy_file(self, src, dst): + """ Copy given source to destination + + Arguments: + src (str): the source file which needs to be copied + dst (str): the destination of the sourc file + Returns: + None + """ + + dirname = os.path.dirname(dst) + try: + os.makedirs(dirname) + except OSError as e: + if e.errno == errno.EEXIST: + pass + else: + self.log.critical("An unexpected error occurred.") + raise + + shutil.copy(src, dst) + + def hardlink_file(self, src, dst): + + dirname = os.path.dirname(dst) + try: + os.makedirs(dirname) + except OSError as e: + if e.errno == errno.EEXIST: + pass + else: + self.log.critical("An unexpected error occurred.") + raise + + filelink.create(src, dst, filelink.HARDLINK) + + def get_subset(self, asset, instance): + + subset = io.find_one({"type": "subset", + "parent": asset["_id"], + "name": instance.data["subset"]}) + + if subset is None: + subset_name = instance.data["subset"] + self.log.info("Subset '%s' not found, creating.." % subset_name) + + _id = io.insert_one({ + "schema": "avalon-core:subset-2.0", + "type": "subset", + "name": subset_name, + "data": {}, + "parent": asset["_id"] + }).inserted_id + + subset = io.find_one({"_id": _id}) + + return subset + + def create_version(self, subset, version_number, locations, data=None): + """ Copy given source to destination + + Args: + subset (dict): the registered subset of the asset + version_number (int): the version number + locations (list): the currently registered locations + + Returns: + dict: collection of data to create a version + """ + # Imprint currently registered location + version_locations = [location for location in locations if + location is not None] + + return {"schema": "avalon-core:version-2.0", + "type": "version", + "parent": subset["_id"], + "name": version_number, + "locations": version_locations, + "data": data} + + def create_version_data(self, context, instance): + """Create the data collection for the version + + Args: + context: the current context + instance: the current instance being published + + Returns: + dict: the required information with instance.data as key + """ + + families = [] + current_families = instance.data.get("families", list()) + instance_family = instance.data.get("family", None) + + if instance_family is not None: + families.append(instance_family) + families += current_families + + self.log.debug("Registered root: {}".format(api.registered_root())) + # # create relative source path for DB + # try: + # source = instance.data['source'] + # except KeyError: + # source = context.data["currentFile"] + # + # relative_path = os.path.relpath(source, api.registered_root()) + # source = os.path.join("{root}", relative_path).replace("\\", "/") + + source = "standalone" + + # self.log.debug("Source: {}".format(source)) + version_data = {"families": families, + "time": context.data["time"], + "author": context.data["user"], + "source": source, + "comment": context.data.get("comment"), + "machine": context.data.get("machine"), + "fps": context.data.get("fps")} + + # Include optional data if present in + optionals = [ + "startFrame", "endFrame", "step", "handles", "sourceHashes" + ] + for key in optionals: + if key in instance.data: + version_data[key] = instance.data[key] + + return version_data diff --git a/pype/standalonepublish/publish.py b/pype/standalonepublish/publish.py index 05c889a2d6..11c2f353d1 100644 --- a/pype/standalonepublish/publish.py +++ b/pype/standalonepublish/publish.py @@ -16,13 +16,19 @@ import pyblish.api # Registers Global pyblish plugins -pype.install() +# pype.install() # Registers Standalone pyblish plugins PUBLISH_PATH = os.path.sep.join( [pype.PLUGINS_DIR, 'standalonepublish', 'publish'] ) pyblish.api.register_plugin_path(PUBLISH_PATH) +# Registers Standalone pyblish plugins +# PUBLISH_PATH = os.path.sep.join( +# [pype.PLUGINS_DIR, 'ftrack', 'publish'] +# ) +# pyblish.api.register_plugin_path(PUBLISH_PATH) + def set_context(project, asset, app): ''' Sets context for pyblish (must be done before pyblish is launched) From 484c8e0815e728e530c43e41ed96c50c4378ca56 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 10 May 2019 12:34:16 +0200 Subject: [PATCH 63/65] replaced brackets in representation name incrementing to underline --- pype/standalonepublish/widgets/widget_drop_frame.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pype/standalonepublish/widgets/widget_drop_frame.py b/pype/standalonepublish/widgets/widget_drop_frame.py index 0048989707..cffe673152 100644 --- a/pype/standalonepublish/widgets/widget_drop_frame.py +++ b/pype/standalonepublish/widgets/widget_drop_frame.py @@ -352,14 +352,14 @@ class DropDataFrame(QtWidgets.QFrame): renamed = False for item in self.components_list.widgets(): if repre_name == item.input_repre.text(): - check_regex = '\(\w+\)$' + check_regex = '_\w+$' result = re.findall(check_regex, repre_name) next_num = 2 if len(result) == 1: repre_name = repre_name.replace(result[0], '') - next_num = int(result[0].replace('(', '').replace(')', '')) + next_num = int(result[0].replace('_', '')) next_num += 1 - repre_name = '{}({})'.format(repre_name, next_num) + repre_name = '{}_{}'.format(repre_name, next_num) renamed = True break if renamed: From ba6ed0c1481d38e0d0d22c066258a022aa713bd9 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Sun, 12 May 2019 22:26:27 +0200 Subject: [PATCH 64/65] add standalone publisher specific plugin temporarily due to certain specifics in the standalone vs in app publishing --- .../publish/collect_context.py | 15 +- .../publish/collect_ftrack_api.py | 40 ++ .../standalonepublish/publish/integrate.py | 73 +-- .../publish/integrate_ftrack_api.py | 315 +++++++++++++ .../publish/integrate_ftrack_instances.py | 101 ++++ .../publish/integrate_rendered_frames.py | 436 ++++++++++++++++++ 6 files changed, 951 insertions(+), 29 deletions(-) create mode 100644 pype/plugins/standalonepublish/publish/collect_ftrack_api.py create mode 100644 pype/plugins/standalonepublish/publish/integrate_ftrack_api.py create mode 100644 pype/plugins/standalonepublish/publish/integrate_ftrack_instances.py create mode 100644 pype/plugins/standalonepublish/publish/integrate_rendered_frames.py diff --git a/pype/plugins/standalonepublish/publish/collect_context.py b/pype/plugins/standalonepublish/publish/collect_context.py index 26ebc642fb..cbe9df1ef6 100644 --- a/pype/plugins/standalonepublish/publish/collect_context.py +++ b/pype/plugins/standalonepublish/publish/collect_context.py @@ -6,6 +6,7 @@ from avalon import ( ) import json import logging +import clique log = logging.getLogger("collector") @@ -65,12 +66,22 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin): instance.data["files"] = list() instance.data['destination_list'] = list() + instance.data['representations'] = list() for component in in_data['representations']: # instance.add(node) + component['destination'] = component['files'] + collections, remainder = clique.assemble(component['files']) + if collections: + self.log.debug(collections) + range = collections[0].format('{range}') + instance.data['startFrame'] = range.split('-')[0] + instance.data['endFrame'] = range.split('-')[1] + + + instance.data["files"].append(component) + instance.data["representations"].append(component) - instance.data["files"].append(component['files']) - instance.data['destination_list'].append(component['files']) # "is_thumbnail": component['thumbnail'], # "is_preview": component['preview'] diff --git a/pype/plugins/standalonepublish/publish/collect_ftrack_api.py b/pype/plugins/standalonepublish/publish/collect_ftrack_api.py new file mode 100644 index 0000000000..6df998350c --- /dev/null +++ b/pype/plugins/standalonepublish/publish/collect_ftrack_api.py @@ -0,0 +1,40 @@ +import os +import pyblish.api + +try: + import ftrack_api_old as ftrack_api +except Exception: + import ftrack_api + + +class CollectFtrackApi(pyblish.api.ContextPlugin): + """ Collects an ftrack session and the current task id. """ + + order = pyblish.api.CollectorOrder + label = "Collect Ftrack Api" + + def process(self, context): + + # Collect session + session = ftrack_api.Session() + context.data["ftrackSession"] = session + + # Collect task + + project = os.environ.get('AVALON_PROJECT', '') + asset = os.environ.get('AVALON_ASSET', '') + task = os.environ.get('AVALON_TASK', None) + + if task: + result = session.query('Task where\ + project.full_name is "{0}" and\ + name is "{1}" and\ + parent.name is "{2}"'.format(project, task, asset)).one() + context.data["ftrackTask"] = result + else: + result = session.query('TypedContext where\ + project.full_name is "{0}" and\ + name is "{1}"'.format(project, asset)).one() + context.data["ftrackEntity"] = result + + self.log.info(result) diff --git a/pype/plugins/standalonepublish/publish/integrate.py b/pype/plugins/standalonepublish/publish/integrate.py index 7157a6fb1a..b6771a52e0 100644 --- a/pype/plugins/standalonepublish/publish/integrate.py +++ b/pype/plugins/standalonepublish/publish/integrate.py @@ -6,6 +6,7 @@ import errno import pyblish.api from avalon import api, io from avalon.vendor import filelink +import clique log = logging.getLogger(__name__) @@ -39,13 +40,14 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "yetiRig", "yeticache", "nukescript", - "review", + # "review", "workfile", "scene", "ass"] exclude_families = ["clip"] def process(self, instance): + if [ef for ef in self.exclude_families if instance.data["family"] in ef]: return @@ -140,7 +142,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): self.log.debug("Creating version ...") version_id = io.insert_one(version).inserted_id - + instance.data['version'] = version['name'] # Write to disk # _ # | | @@ -181,7 +183,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): if 'transfers' not in instance.data: instance.data['transfers'] = [] - for files in instance.data["files"]: + for idx, repre in enumerate(instance.data["representations"]): # Collection # _______ @@ -193,27 +195,43 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # |_______| # - if isinstance(files, list): - collection = files + files = repre['files'] + + if len(files) > 1: + src_collections, remainder = clique.assemble(files) + self.log.debug("dst_collections: {}".format(str(src_collections))) + src_collection = src_collections[0] # Assert that each member has identical suffix - _, ext = os.path.splitext(collection[0]) - assert all(ext == os.path.splitext(name)[1] - for name in collection), ( - "Files had varying suffixes, this is a bug" - ) + src_head = src_collection.format("{head}") + src_tail = ext = src_collection.format("{tail}") - # assert not any(os.path.isabs(name) for name in collection) - - template_data["representation"] = ext[1:] - - for fname in collection: - - src = fname + test_dest_files = list() + for i in [1, 2]: + template_data["representation"] = src_tail[1:] + template_data["frame"] = src_collection.format( + "{padding}") % i anatomy_filled = anatomy.format(template_data) - dst = anatomy_filled["publish"]["path"] + test_dest_files.append(anatomy_filled["publish"]["path"]) + + dst_collections, remainder = clique.assemble(test_dest_files) + dst_collection = dst_collections[0] + dst_head = dst_collection.format("{head}") + dst_tail = dst_collection.format("{tail}") + + instance.data["representations"][idx]['published_path'] = dst_collection.format() + + for i in src_collection.indexes: + src_padding = src_collection.format("{padding}") % i + src_file_name = "{0}{1}{2}".format( + src_head, src_padding, src_tail) + dst_padding = dst_collection.format("{padding}") % i + dst = "{0}{1}{2}".format(dst_head, dst_padding, dst_tail) + + # src = os.path.join(stagingdir, src_file_name) + src = src_file_name + self.log.debug("source: {}".format(src)) instance.data["transfers"].append([src, dst]) - template = anatomy.templates["publish"]["path"] else: # Single file @@ -224,13 +242,13 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # | | # |_______| # - fname = files - assert not os.path.isabs(fname), ( - "Given file name is a full path" - ) - _, ext = os.path.splitext(fname) + fname = files[0] + # assert not os.path.isabs(fname), ( + # "Given file name is a full path" + # ) + # _, ext = os.path.splitext(fname) - template_data["representation"] = ext[1:] + template_data["representation"] = repre['representation'] # src = os.path.join(stagingdir, fname) src = fname @@ -239,12 +257,13 @@ class IntegrateAsset(pyblish.api.InstancePlugin): instance.data["transfers"].append([src, dst]) template = anatomy.templates["publish"]["path"] + instance.data["representations"][idx]['published_path'] = dst representation = { "schema": "pype:representation-2.0", "type": "representation", "parent": version_id, - "name": ext[1:], + "name": repre['representation'], "data": {'path': dst, 'template': template}, "dependencies": instance.data.get("dependencies", "").split(), @@ -261,7 +280,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "subset": subset["name"], "version": version["name"], "hierarchy": hierarchy, - "representation": ext[1:] + # "representation": repre['representation'] } } diff --git a/pype/plugins/standalonepublish/publish/integrate_ftrack_api.py b/pype/plugins/standalonepublish/publish/integrate_ftrack_api.py new file mode 100644 index 0000000000..9eff10ba67 --- /dev/null +++ b/pype/plugins/standalonepublish/publish/integrate_ftrack_api.py @@ -0,0 +1,315 @@ +import os +import sys +import pyblish.api +import clique + + +class IntegrateFtrackApi(pyblish.api.InstancePlugin): + """ Commit components to server. """ + + order = pyblish.api.IntegratorOrder+0.499 + label = "Integrate Ftrack Api" + families = ["ftrack"] + + def query(self, entitytype, data): + """ Generate a query expression from data supplied. + + If a value is not a string, we'll add the id of the entity to the + query. + + Args: + entitytype (str): The type of entity to query. + data (dict): The data to identify the entity. + exclusions (list): All keys to exclude from the query. + + Returns: + str: String query to use with "session.query" + """ + queries = [] + if sys.version_info[0] < 3: + for key, value in data.iteritems(): + if not isinstance(value, (basestring, int)): + self.log.info("value: {}".format(value)) + if "id" in value.keys(): + queries.append( + "{0}.id is \"{1}\"".format(key, value["id"]) + ) + else: + queries.append("{0} is \"{1}\"".format(key, value)) + else: + for key, value in data.items(): + if not isinstance(value, (str, int)): + self.log.info("value: {}".format(value)) + if "id" in value.keys(): + queries.append( + "{0}.id is \"{1}\"".format(key, value["id"]) + ) + else: + queries.append("{0} is \"{1}\"".format(key, value)) + + query = ( + "select id from " + entitytype + " where " + " and ".join(queries) + ) + self.log.debug(query) + return query + + def process(self, instance): + + session = instance.context.data["ftrackSession"] + if instance.context.data.get("ftrackTask"): + task = instance.context.data["ftrackTask"] + name = task['full_name'] + parent = task["parent"] + elif instance.context.data.get("ftrackEntity"): + task = None + name = instance.context.data.get("ftrackEntity")['name'] + parent = instance.context.data.get("ftrackEntity") + + info_msg = "Created new {entity_type} with data: {data}" + info_msg += ", metadata: {metadata}." + + # Iterate over components and publish + for data in instance.data.get("ftrackComponentsList", []): + + # AssetType + # Get existing entity. + assettype_data = {"short": "upload"} + assettype_data.update(data.get("assettype_data", {})) + self.log.debug("data: {}".format(data)) + + assettype_entity = session.query( + self.query("AssetType", assettype_data) + ).first() + + # Create a new entity if none exits. + if not assettype_entity: + assettype_entity = session.create("AssetType", assettype_data) + self.log.debug( + "Created new AssetType with data: ".format(assettype_data) + ) + + # Asset + # Get existing entity. + asset_data = { + "name": name, + "type": assettype_entity, + "parent": parent, + } + asset_data.update(data.get("asset_data", {})) + + asset_entity = session.query( + self.query("Asset", asset_data) + ).first() + + self.log.info("asset entity: {}".format(asset_entity)) + + # Extracting metadata, and adding after entity creation. This is + # due to a ftrack_api bug where you can't add metadata on creation. + asset_metadata = asset_data.pop("metadata", {}) + + # Create a new entity if none exits. + if not asset_entity: + asset_entity = session.create("Asset", asset_data) + self.log.debug( + info_msg.format( + entity_type="Asset", + data=asset_data, + metadata=asset_metadata + ) + ) + + # Adding metadata + existing_asset_metadata = asset_entity["metadata"] + existing_asset_metadata.update(asset_metadata) + asset_entity["metadata"] = existing_asset_metadata + + # AssetVersion + # Get existing entity. + assetversion_data = { + "version": 0, + "asset": asset_entity, + } + if task: + assetversion_data['task'] = task + + assetversion_data.update(data.get("assetversion_data", {})) + + assetversion_entity = session.query( + self.query("AssetVersion", assetversion_data) + ).first() + + # Extracting metadata, and adding after entity creation. This is + # due to a ftrack_api bug where you can't add metadata on creation. + assetversion_metadata = assetversion_data.pop("metadata", {}) + + # Create a new entity if none exits. + if not assetversion_entity: + assetversion_entity = session.create( + "AssetVersion", assetversion_data + ) + self.log.debug( + info_msg.format( + entity_type="AssetVersion", + data=assetversion_data, + metadata=assetversion_metadata + ) + ) + + # Adding metadata + existing_assetversion_metadata = assetversion_entity["metadata"] + existing_assetversion_metadata.update(assetversion_metadata) + assetversion_entity["metadata"] = existing_assetversion_metadata + + # Have to commit the version and asset, because location can't + # determine the final location without. + session.commit() + + # Component + # Get existing entity. + component_data = { + "name": "main", + "version": assetversion_entity + } + component_data.update(data.get("component_data", {})) + + component_entity = session.query( + self.query("Component", component_data) + ).first() + + component_overwrite = data.get("component_overwrite", False) + location = data.get("component_location", session.pick_location()) + + # Overwrite existing component data if requested. + if component_entity and component_overwrite: + + origin_location = session.query( + "Location where name is \"ftrack.origin\"" + ).one() + + # Removing existing members from location + components = list(component_entity.get("members", [])) + components += [component_entity] + for component in components: + for loc in component["component_locations"]: + if location["id"] == loc["location_id"]: + location.remove_component( + component, recursive=False + ) + + # Deleting existing members on component entity + for member in component_entity.get("members", []): + session.delete(member) + del(member) + + session.commit() + + # Reset members in memory + if "members" in component_entity.keys(): + component_entity["members"] = [] + + # Add components to origin location + try: + collection = clique.parse(data["component_path"]) + except ValueError: + # Assume its a single file + # Changing file type + name, ext = os.path.splitext(data["component_path"]) + component_entity["file_type"] = ext + + origin_location.add_component( + component_entity, data["component_path"] + ) + else: + # Changing file type + component_entity["file_type"] = collection.format("{tail}") + + # Create member components for sequence. + for member_path in collection: + + size = 0 + try: + size = os.path.getsize(member_path) + except OSError: + pass + + name = collection.match(member_path).group("index") + + member_data = { + "name": name, + "container": component_entity, + "size": size, + "file_type": os.path.splitext(member_path)[-1] + } + + component = session.create( + "FileComponent", member_data + ) + origin_location.add_component( + component, member_path, recursive=False + ) + component_entity["members"].append(component) + + # Add components to location. + location.add_component( + component_entity, origin_location, recursive=True + ) + + data["component"] = component_entity + msg = "Overwriting Component with path: {0}, data: {1}, " + msg += "location: {2}" + self.log.info( + msg.format( + data["component_path"], + component_data, + location + ) + ) + + # Extracting metadata, and adding after entity creation. This is + # due to a ftrack_api bug where you can't add metadata on creation. + component_metadata = component_data.pop("metadata", {}) + + # Create new component if none exists. + new_component = False + if not component_entity: + component_entity = assetversion_entity.create_component( + data["component_path"], + data=component_data, + location=location + ) + data["component"] = component_entity + msg = "Created new Component with path: {0}, data: {1}" + msg += ", metadata: {2}, location: {3}" + self.log.info( + msg.format( + data["component_path"], + component_data, + component_metadata, + location + ) + ) + new_component = True + + # Adding metadata + existing_component_metadata = component_entity["metadata"] + existing_component_metadata.update(component_metadata) + component_entity["metadata"] = existing_component_metadata + + # if component_data['name'] = 'ftrackreview-mp4-mp4': + # assetversion_entity["thumbnail_id"] + + # Setting assetversion thumbnail + if data.get("thumbnail", False): + assetversion_entity["thumbnail_id"] = component_entity["id"] + + # Inform user about no changes to the database. + if (component_entity and not component_overwrite and + not new_component): + data["component"] = component_entity + self.log.info( + "Found existing component, and no request to overwrite. " + "Nothing has been changed." + ) + else: + # Commit changes. + session.commit() diff --git a/pype/plugins/standalonepublish/publish/integrate_ftrack_instances.py b/pype/plugins/standalonepublish/publish/integrate_ftrack_instances.py new file mode 100644 index 0000000000..8d938bceb0 --- /dev/null +++ b/pype/plugins/standalonepublish/publish/integrate_ftrack_instances.py @@ -0,0 +1,101 @@ +import pyblish.api +import os +import json + + +class IntegrateFtrackInstance(pyblish.api.InstancePlugin): + """Collect ftrack component data + + Add ftrack component list to instance. + + + """ + + order = pyblish.api.IntegratorOrder + 0.48 + label = 'Integrate Ftrack Component' + families = ["ftrack"] + + family_mapping = {'camera': 'cam', + 'look': 'look', + 'mayaAscii': 'scene', + 'model': 'geo', + 'rig': 'rig', + 'setdress': 'setdress', + 'pointcache': 'cache', + 'write': 'img', + 'render': 'render', + 'nukescript': 'comp', + 'review': 'mov'} + + def process(self, instance): + self.log.debug('instance {}'.format(instance)) + + if instance.data.get('version'): + version_number = int(instance.data.get('version')) + + family = instance.data['family'].lower() + + asset_type = '' + asset_type = self.family_mapping[family] + + componentList = [] + ft_session = instance.context.data["ftrackSession"] + + components = instance.data['representations'] + + for comp in components: + self.log.debug('component {}'.format(comp)) + # filename, ext = os.path.splitext(file) + # self.log.debug('dest ext: ' + ext) + + # ext = comp['Context'] + + if comp['thumbnail']: + location = ft_session.query( + 'Location where name is "ftrack.server"').one() + component_data = { + "name": "thumbnail" # Default component name is "main". + } + elif comp['preview']: + if not instance.data.get('startFrameReview'): + instance.data['startFrameReview'] = instance.data['startFrame'] + if not instance.data.get('endFrameReview'): + instance.data['endFrameReview'] = instance.data['endFrame'] + location = ft_session.query( + 'Location where name is "ftrack.server"').one() + component_data = { + # Default component name is "main". + "name": "ftrackreview-mp4", + "metadata": {'ftr_meta': json.dumps({ + 'frameIn': int(instance.data['startFrameReview']), + 'frameOut': int(instance.data['endFrameReview']), + 'frameRate': 25.0})} + } + else: + component_data = { + "name": comp['representation'] # Default component name is "main". + } + location = ft_session.query( + 'Location where name is "ftrack.unmanaged"').one() + + self.log.debug('location {}'.format(location)) + + componentList.append({"assettype_data": { + "short": asset_type, + }, + "asset_data": { + "name": instance.data["subset"], + }, + "assetversion_data": { + "version": version_number, + }, + "component_data": component_data, + "component_path": comp['published_path'], + 'component_location': location, + "component_overwrite": False, + "thumbnail": comp['thumbnail'] + } + ) + + self.log.debug('componentsList: {}'.format(str(componentList))) + instance.data["ftrackComponentsList"] = componentList diff --git a/pype/plugins/standalonepublish/publish/integrate_rendered_frames.py b/pype/plugins/standalonepublish/publish/integrate_rendered_frames.py new file mode 100644 index 0000000000..43653ab0ed --- /dev/null +++ b/pype/plugins/standalonepublish/publish/integrate_rendered_frames.py @@ -0,0 +1,436 @@ +import os +import logging +import shutil +import clique + +import errno +import pyblish.api +from avalon import api, io + + +log = logging.getLogger(__name__) + + +class IntegrateFrames(pyblish.api.InstancePlugin): + """Resolve any dependency issies + + This plug-in resolves any paths which, if not updated might break + the published file. + + The order of families is important, when working with lookdev you want to + first publish the texture, update the texture paths in the nodes and then + publish the shading network. Same goes for file dependent assets. + """ + + label = "Integrate Frames" + order = pyblish.api.IntegratorOrder + families = [ + "imagesequence", + "render", + "write", + "source", + 'review'] + + family_targets = [".frames", ".local", ".review", "review", "imagesequence", "render", "source"] + exclude_families = ["clip"] + + def process(self, instance): + if [ef for ef in self.exclude_families + if instance.data["family"] in ef]: + return + + families = [f for f in instance.data["families"] + for search in self.family_targets + if search in f] + + if not families: + return + + self.register(instance) + + # self.log.info("Integrating Asset in to the database ...") + # self.log.info("instance.data: {}".format(instance.data)) + if instance.data.get('transfer', True): + self.integrate(instance) + + def register(self, instance): + + # Required environment variables + PROJECT = api.Session["AVALON_PROJECT"] + ASSET = instance.data.get("asset") or api.Session["AVALON_ASSET"] + LOCATION = api.Session["AVALON_LOCATION"] + + context = instance.context + # Atomicity + # + # Guarantee atomic publishes - each asset contains + # an identical set of members. + # __ + # / o + # / \ + # | o | + # \ / + # o __/ + # + assert all(result["success"] for result in context.data["results"]), ( + "Atomicity not held, aborting.") + + # Assemble + # + # | + # v + # ---> <---- + # ^ + # | + # + # stagingdir = instance.data.get("stagingDir") + # assert stagingdir, ("Incomplete instance \"%s\": " + # "Missing reference to staging area." % instance) + + # extra check if stagingDir actually exists and is available + + # self.log.debug("Establishing staging directory @ %s" % stagingdir) + + project = io.find_one({"type": "project"}) + + asset = io.find_one({"type": "asset", + "name": ASSET, + "parent": project["_id"]}) + + assert all([project, asset]), ("Could not find current project or " + "asset '%s'" % ASSET) + + subset = self.get_subset(asset, instance) + + # get next version + latest_version = io.find_one({"type": "version", + "parent": subset["_id"]}, + {"name": True}, + sort=[("name", -1)]) + + next_version = 1 + if latest_version is not None: + next_version += latest_version["name"] + + self.log.info("Verifying version from assumed destination") + + # assumed_data = instance.data["assumedTemplateData"] + # assumed_version = assumed_data["version"] + # if assumed_version != next_version: + # raise AttributeError("Assumed version 'v{0:03d}' does not match" + # "next version in database " + # "('v{1:03d}')".format(assumed_version, + # next_version)) + + if instance.data.get('version'): + next_version = int(instance.data.get('version')) + + instance.data['version'] = next_version + + self.log.debug("Next version: v{0:03d}".format(next_version)) + + version_data = self.create_version_data(context, instance) + version = self.create_version(subset=subset, + version_number=next_version, + locations=[LOCATION], + data=version_data) + + self.log.debug("Creating version ...") + version_id = io.insert_one(version).inserted_id + + # Write to disk + # _ + # | | + # _| |_ + # ____\ / + # |\ \ / \ + # \ \ v \ + # \ \________. + # \|________| + # + root = api.registered_root() + hierarchy = "" + parents = io.find_one({"type": 'asset', "name": ASSET})[ + 'data']['parents'] + if parents and len(parents) > 0: + # hierarchy = os.path.sep.join(hierarchy) + hierarchy = os.path.join(*parents) + + template_data = {"root": root, + "project": {"name": PROJECT, + "code": project['data']['code']}, + "silo": asset['silo'], + "task": api.Session["AVALON_TASK"], + "asset": ASSET, + "family": instance.data['family'], + "subset": subset["name"], + "version": int(version["name"]), + "hierarchy": hierarchy} + + # template_publish = project["config"]["template"]["publish"] + anatomy = instance.context.data['anatomy'] + + # Find the representations to transfer amongst the files + # Each should be a single representation (as such, a single extension) + representations = [] + destination_list = [] + + if 'transfers' not in instance.data: + instance.data['transfers'] = [] + + # for repre in instance.data["representations"]: + for idx, repre in enumerate(instance.data["representations"]): + # Collection + # _______ + # |______|\ + # | |\| + # | || + # | || + # | || + # |_______| + # + + files = repre['files'] + + if len(files) > 1: + + src_collections, remainder = clique.assemble(files) + self.log.debug("dst_collections: {}".format(str(src_collections))) + src_collection = src_collections[0] + # Assert that each member has identical suffix + src_head = src_collection.format("{head}") + src_tail = ext = src_collection.format("{tail}") + + test_dest_files = list() + for i in [1, 2]: + template_data["representation"] = repre['representation'] + template_data["frame"] = src_collection.format( + "{padding}") % i + anatomy_filled = anatomy.format(template_data) + test_dest_files.append(anatomy_filled["render"]["path"]) + + dst_collections, remainder = clique.assemble(test_dest_files) + dst_collection = dst_collections[0] + dst_head = dst_collection.format("{head}") + dst_tail = dst_collection.format("{tail}") + + instance.data["representations"][idx]['published_path'] = dst_collection.format() + + for i in src_collection.indexes: + src_padding = src_collection.format("{padding}") % i + src_file_name = "{0}{1}{2}".format( + src_head, src_padding, src_tail) + dst_padding = dst_collection.format("{padding}") % i + dst = "{0}{1}{2}".format(dst_head, dst_padding, dst_tail) + + # src = os.path.join(stagingdir, src_file_name) + src = src_file_name + self.log.debug("source: {}".format(src)) + + instance.data["transfers"].append([src, dst]) + + else: + # Single file + # _______ + # | |\ + # | | + # | | + # | | + # |_______| + # + + template_data.pop("frame", None) + + fname = files[0] + + self.log.info("fname: {}".format(fname)) + + # assert not os.path.isabs(fname), ( + # "Given file name is a full path" + # ) + # _, ext = os.path.splitext(fname) + + template_data["representation"] = repre['representation'] + + # src = os.path.join(stagingdir, fname) + src = src_file_name + + anatomy_filled = anatomy.format(template_data) + dst = anatomy_filled["render"]["path"] + + instance.data["transfers"].append([src, dst]) + instance.data["representations"][idx]['published_path'] = dst + + if repre['ext'] not in ["jpeg", "jpg", "mov", "mp4", "wav"]: + template_data["frame"] = "#" * int(anatomy_filled["render"]["padding"]) + + anatomy_filled = anatomy.format(template_data) + path_to_save = anatomy_filled["render"]["path"] + template = anatomy.templates["render"]["path"] + + self.log.debug("path_to_save: {}".format(path_to_save)) + + representation = { + "schema": "pype:representation-2.0", + "type": "representation", + "parent": version_id, + "name": repre['representation'], + "data": {'path': path_to_save, 'template': template}, + "dependencies": instance.data.get("dependencies", "").split(), + + # Imprint shortcut to context + # for performance reasons. + "context": { + "root": root, + "project": { + "name": PROJECT, + "code": project['data']['code'] + }, + "task": api.Session["AVALON_TASK"], + "silo": asset['silo'], + "asset": ASSET, + "family": instance.data['family'], + "subset": subset["name"], + "version": int(version["name"]), + "hierarchy": hierarchy, + "representation": repre['representation'] + } + } + + destination_list.append(dst) + instance.data['destination_list'] = destination_list + representations.append(representation) + + self.log.info("Registering {} items".format(len(representations))) + io.insert_many(representations) + + def integrate(self, instance): + """Move the files + + Through `instance.data["transfers"]` + + Args: + instance: the instance to integrate + """ + + transfers = instance.data["transfers"] + + for src, dest in transfers: + src = os.path.normpath(src) + dest = os.path.normpath(dest) + if src in dest: + continue + + self.log.info("Copying file .. {} -> {}".format(src, dest)) + self.copy_file(src, dest) + + def copy_file(self, src, dst): + """ Copy given source to destination + + Arguments: + src (str): the source file which needs to be copied + dst (str): the destination of the sourc file + Returns: + None + """ + + dirname = os.path.dirname(dst) + try: + os.makedirs(dirname) + except OSError as e: + if e.errno == errno.EEXIST: + pass + else: + self.log.critical("An unexpected error occurred.") + raise + + shutil.copy(src, dst) + + def get_subset(self, asset, instance): + + subset = io.find_one({"type": "subset", + "parent": asset["_id"], + "name": instance.data["subset"]}) + + if subset is None: + subset_name = instance.data["subset"] + self.log.info("Subset '%s' not found, creating.." % subset_name) + + _id = io.insert_one({ + "schema": "pype:subset-2.0", + "type": "subset", + "name": subset_name, + "data": {}, + "parent": asset["_id"] + }).inserted_id + + subset = io.find_one({"_id": _id}) + + return subset + + def create_version(self, subset, version_number, locations, data=None): + """ Copy given source to destination + + Args: + subset (dict): the registered subset of the asset + version_number (int): the version number + locations (list): the currently registered locations + + Returns: + dict: collection of data to create a version + """ + # Imprint currently registered location + version_locations = [location for location in locations if + location is not None] + + return {"schema": "pype:version-2.0", + "type": "version", + "parent": subset["_id"], + "name": version_number, + "locations": version_locations, + "data": data} + + def create_version_data(self, context, instance): + """Create the data collection for the version + + Args: + context: the current context + instance: the current instance being published + + Returns: + dict: the required information with instance.data as key + """ + + families = [] + current_families = instance.data.get("families", list()) + instance_family = instance.data.get("family", None) + + if instance_family is not None: + families.append(instance_family) + families += current_families + + # try: + # source = instance.data['source'] + # except KeyError: + # source = context.data["currentFile"] + # + # relative_path = os.path.relpath(source, api.registered_root()) + # source = os.path.join("{root}", relative_path).replace("\\", "/") + + source = "standalone" + + version_data = {"families": families, + "time": context.data["time"], + "author": context.data["user"], + "source": source, + "comment": context.data.get("comment")} + + # Include optional data if present in + optionals = ["startFrame", "endFrame", "step", + "handles", "colorspace", "fps", "outputDir"] + + for key in optionals: + if key in instance.data: + version_data[key] = instance.data.get(key, None) + + return version_data From 93e99e41c657be07822e18fef00e024d37cf0d74 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Sun, 12 May 2019 22:27:12 +0200 Subject: [PATCH 65/65] fix hierarchy spelling --- pype/standalonepublish/publish.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pype/standalonepublish/publish.py b/pype/standalonepublish/publish.py index 11c2f353d1..4442f0243c 100644 --- a/pype/standalonepublish/publish.py +++ b/pype/standalonepublish/publish.py @@ -23,7 +23,7 @@ PUBLISH_PATH = os.path.sep.join( ) pyblish.api.register_plugin_path(PUBLISH_PATH) -# Registers Standalone pyblish plugins +# # Registers Standalone pyblish plugins # PUBLISH_PATH = os.path.sep.join( # [pype.PLUGINS_DIR, 'ftrack', 'publish'] # ) @@ -55,8 +55,8 @@ def set_context(project, asset, app): if parents and len(parents) > 0: hierarchy = os.path.sep.join(parents) - os.environ["AVALON_HIEARCHY"] = hierarchy - io.Session["AVALON_HIEARCHY"] = hierarchy + os.environ["AVALON_HIERARCHY"] = hierarchy + io.Session["AVALON_HIERARCHY"] = hierarchy os.environ["AVALON_PROJECTCODE"] = av_project['data'].get('code', '') io.Session["AVALON_PROJECTCODE"] = av_project['data'].get('code', '')