Merge pull request #442 from pypeclub/feature/standalone_publisher_as_tool

Feature/standalone publisher as tool
This commit is contained in:
Milan Kolar 2020-08-17 11:43:12 +02:00 committed by GitHub
commit f4e27a1205
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 243 additions and 369 deletions

View file

@ -0,0 +1,8 @@
from .app import (
show,
cli
)
__all__ = [
"show",
"cli"
]

View file

@ -0,0 +1,24 @@
import os
import sys
import app
import signal
from Qt import QtWidgets
from avalon import style
if __name__ == "__main__":
qt_app = QtWidgets.QApplication([])
# app.setQuitOnLastWindowClosed(False)
qt_app.setStyleSheet(style.load_stylesheet())
def signal_handler(sig, frame):
print("You pressed Ctrl+C. Process ended.")
qt_app.quit()
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
window = app.Window(sys.argv[-1].split(os.pathsep))
window.show()
sys.exit(qt_app.exec_())

View file

@ -0,0 +1,192 @@
from bson.objectid import ObjectId
from Qt import QtWidgets, QtCore
from widgets import AssetWidget, FamilyWidget, ComponentsWidget, ShadowWidget
from avalon.tools.libraryloader.io_nonsingleton import DbConnector
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
valid_components = False
initialized = False
WIDTH = 1100
HEIGHT = 500
def __init__(self, pyblish_paths, parent=None):
super(Window, self).__init__(parent=parent)
self._db.install()
self.pyblish_paths = pyblish_paths
self.setWindowTitle("Standalone Publish")
self.setFocusPolicy(QtCore.Qt.StrongFocus)
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
# Validators
self.valid_parent = False
# assets widget
widget_assets = AssetWidget(dbcon=self._db, parent=self)
# family widget
widget_family = FamilyWidget(dbcon=self._db, parent=self)
# components widget
widget_components = ComponentsWidget(parent=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)
body.addWidget(widget_family)
body.addWidget(widget_components)
body.setStretchFactor(body.indexOf(widget_assets), 2)
body.setStretchFactor(body.indexOf(widget_family), 3)
body.setStretchFactor(body.indexOf(widget_components), 5)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(body)
self.resize(self.WIDTH, self.HEIGHT)
# signals
widget_assets.selection_changed.connect(self.on_asset_changed)
widget_family.stateChanged.connect(self.set_valid_family)
self.widget_assets = widget_assets
self.widget_family = widget_family
self.widget_components = widget_components
# on start
self.on_start()
@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)
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):
''' Avalon DB entities helper - get all parents (exclude project).
'''
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 on_asset_changed(self):
'''Callback on asset selection changed
Updates the task view.
'''
selected = [
asset_id for asset_id in self.widget_assets.get_selected_assets()
if isinstance(asset_id, ObjectId)
]
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(None)
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
self.shadow_widget.setVisible(True)
self.resizeEvent()
QtWidgets.QApplication.processEvents()
def working_stop(self):
''' Hides shadowed foreground
'''
if self.shadow_widget.isVisible():
self.shadow_widget.setVisible(False)
# Refresh version
self.widget_family.on_version_refresh()
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())
data.update(self.widget_components.collect_data())
return data

View file

@ -0,0 +1,35 @@
import os
import sys
import pype
import pyblish.api
def main(env):
from avalon.tools import publish
# Registers pype's Global pyblish plugins
pype.install()
# Register additional paths
addition_paths_str = env.get("PUBLISH_PATHS") or ""
addition_paths = addition_paths_str.split(os.pathsep)
for path in addition_paths:
path = os.path.normpath(path)
if not os.path.exists(path):
continue
pyblish.api.register_plugin_path(path)
# Register project specific plugins
project_name = os.environ["AVALON_PROJECT"]
project_plugins_paths = env.get("PYPE_PROJECT_PLUGINS") or ""
for path in project_plugins_paths.split(os.pathsep):
plugin_path = os.path.join(path, project_name, "plugins")
if os.path.exists(plugin_path):
pyblish.api.register_plugin_path(plugin_path)
return publish.show()
if __name__ == "__main__":
result = main(os.environ)
sys.exit(not bool(result))

View file

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

View file

@ -0,0 +1,9 @@
<?xml version='1.0' encoding='utf-8'?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 129 129" xmlns:xlink="http://www.w3.org/1999/xlink" enable-background="new 0 0 129 129" fill="#ffffff" width="32px" height="32px">
<g>
<g>
<path d="m119.2,114.3h-109.4c-2.3,0-4.1,1.9-4.1,4.1s1.9,4.1 4.1,4.1h109.5c2.3,0 4.1-1.9 4.1-4.1s-1.9-4.1-4.2-4.1z"/>
<path d="m5.7,78l-.1,19.5c0,1.1 0.4,2.2 1.2,3 0.8,0.8 1.8,1.2 2.9,1.2l19.4-.1c1.1,0 2.1-0.4 2.9-1.2l67-67c1.6-1.6 1.6-4.2 0-5.9l-19.2-19.4c-1.6-1.6-4.2-1.6-5.9-1.77636e-15l-13.4,13.5-53.6,53.5c-0.7,0.8-1.2,1.8-1.2,2.9zm71.2-61.1l13.5,13.5-7.6,7.6-13.5-13.5 7.6-7.6zm-62.9,62.9l49.4-49.4 13.5,13.5-49.4,49.3-13.6,.1 .1-13.5z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 730 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 803 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 330 330" fill="#ffffff" style="enable-background:new 0 0 330 330;" xml:space="preserve">
<g>
<path d="M165,0C74.019,0,0,74.02,0,165.001C0,255.982,74.019,330,165,330s165-74.018,165-164.999C330,74.02,255.981,0,165,0z
M165,300c-74.44,0-135-60.56-135-134.999C30,90.562,90.56,30,165,30s135,60.562,135,135.001C300,239.44,239.439,300,165,300z"/>
<path d="M164.998,70c-11.026,0-19.996,8.976-19.996,20.009c0,11.023,8.97,19.991,19.996,19.991
c11.026,0,19.996-8.968,19.996-19.991C184.994,78.976,176.024,70,164.998,70z"/>
<path d="M165,140c-8.284,0-15,6.716-15,15v90c0,8.284,6.716,15,15,15c8.284,0,15-6.716,15-15v-90C180,146.716,173.284,140,165,140z
"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 B

View file

@ -0,0 +1,28 @@
from Qt import QtCore
HelpRole = QtCore.Qt.UserRole + 2
FamilyRole = QtCore.Qt.UserRole + 3
ExistsRole = QtCore.Qt.UserRole + 4
PluginRole = QtCore.Qt.UserRole + 5
PluginKeyRole = QtCore.Qt.UserRole + 6
from .model_node import Node
from .model_tree import TreeModel
from .model_asset import AssetModel, _iter_model_rows
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 import AssetWidget
from .widget_family_desc import FamilyDescriptionWidget
from .widget_family import FamilyWidget
from .widget_drop_empty import DropEmpty
from .widget_component_item import ComponentItem
from .widget_components_list import ComponentsList
from .widget_drop_frame import DropDataFrame
from .widget_components import ComponentsWidget
from .widget_shadow import ShadowWidget

View file

@ -0,0 +1,200 @@
import logging
import collections
from Qt import QtCore, QtGui
from . import TreeModel, Node
from avalon.vendor import qtawesome
from avalon import style
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, dbcon, parent=None):
super(AssetModel, self).__init__(parent=parent)
self.dbcon = dbcon
self.refresh()
def _add_hierarchy(self, assets, parent=None, silos=None):
"""Add the assets that are related to the parent as children items.
This method does *not* query the database. These instead are queried
in a single batch upfront as an optimization to reduce database
queries. Resulting in up to 10x speed increase.
Args:
assets (dict): All assets in the currently active silo stored
by key/value
Returns:
None
"""
if silos:
# WARNING: Silo item "_id" is set to silo value
# mainly because GUI issue with perserve selection and expanded row
# and because of easier hierarchy parenting (in "assets")
for silo in silos:
node = Node({
"_id": silo,
"name": silo,
"label": silo,
"type": "silo"
})
self.add_child(node, parent=parent)
self._add_hierarchy(assets, parent=node)
parent_id = parent["_id"] if parent else None
current_assets = assets.get(parent_id, list())
for asset in current_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 if it has children
if asset["_id"] in assets:
self._add_hierarchy(assets, parent=node)
def refresh(self):
"""Refresh the data for the model."""
self.clear()
if (
self.dbcon.active_project() is None or
self.dbcon.active_project() == ''
):
return
self.beginResetModel()
# Get all assets in current silo sorted by name
db_assets = self.dbcon.find({"type": "asset"}).sort("name", 1)
silos = db_assets.distinct("silo") or None
# if any silo is set to None then it's expected it should not be used
if silos and None in silos:
silos = None
# Group the assets by their visual parent's id
assets_by_parent = collections.defaultdict(list)
for asset in db_assets:
parent_id = (
asset.get("data", {}).get("visualParent") or
asset.get("silo")
)
assets_by_parent[parent_id].append(asset)
# Build the hierarchical tree items recursively
self._add_hierarchy(
assets_by_parent,
parent=None,
silos=silos
)
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.get("_document", {}).get("data", {})
icon = data.get("icon", None)
if icon is None and node.get("type") == "silo":
icon = "database"
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 = qtawesome.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)

View file

@ -0,0 +1,28 @@
from Qt 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

View file

@ -0,0 +1,31 @@
from Qt import QtCore
import re
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)

View file

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

View file

@ -0,0 +1,64 @@
from Qt import QtCore
from . import Node, TreeModel
from avalon.vendor import qtawesome
from avalon import style
class TasksTemplateModel(TreeModel):
"""A model listing the tasks combined for a list of assets"""
COLUMNS = ["Tasks"]
def __init__(self, selectable=True):
super(TasksTemplateModel, self).__init__()
self.selectable = selectable
self.icon = qtawesome.icon(
'fa.calendar-check-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()
for task in tasks:
node = Node({
"Tasks": task,
"icon": self.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)

View file

@ -0,0 +1,122 @@
from Qt import QtCore
from . import Node
class TreeModel(QtCore.QAbstractItemModel):
COLUMNS = list()
ItemRole = 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.ItemRole:
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()

View file

@ -0,0 +1,16 @@
from Qt 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)

View file

@ -0,0 +1,343 @@
import contextlib
from Qt import QtWidgets, QtCore
from . import RecursiveSortFilterProxyModel, AssetModel
from avalon.vendor import qtawesome
from avalon import style
from . import TasksTemplateModel, DeselectableTreeView
from . import _iter_model_rows
@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, dbcon, parent=None):
super(AssetWidget, self).__init__(parent=parent)
self.setContentsMargins(0, 0, 0, 0)
self.dbcon = dbcon
layout = QtWidgets.QVBoxLayout()
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(dbcon=self.dbcon, parent=self)
proxy = RecursiveSortFilterProxyModel()
proxy.setSourceModel(model)
proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
view = DeselectableTreeView()
view.setIndentation(15)
view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
view.setHeaderHidden(True)
view.setModel(proxy)
# Header
header = QtWidgets.QHBoxLayout()
icon = qtawesome.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)
# tasks
task_view = DeselectableTreeView()
task_view.setIndentation(0)
task_view.setHeaderHidden(True)
task_view.setVisible(False)
task_model = TasksTemplateModel()
task_view.setModel(task_model)
main_layout = QtWidgets.QVBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.setSpacing(4)
main_layout.addLayout(layout, 80)
main_layout.addWidget(task_view, 20)
# Signals/Slots
selection = view.selectionModel()
selection.selectionChanged.connect(self.selection_changed)
selection.currentChanged.connect(self.current_changed)
refresh.clicked.connect(self.refresh)
self.selection_changed.connect(self._refresh_tasks)
self.task_view = task_view
self.task_model = task_model
self.refreshButton = refresh
self.model = model
self.proxy = proxy
self.view = view
def collect_data(self):
project = self.dbcon.find_one({'type': 'project'})
asset = self.get_active_asset()
try:
index = self.task_view.selectedIndexes()[0]
task = self.task_model.itemData(index)[0]
except Exception:
task = None
data = {
'project': project['name'],
'asset': asset['name'],
'silo': asset.get("silo"),
'parents': self.get_parents(asset),
'task': task
}
return data
def get_parents(self, entity):
ent_parents = entity.get("data", {}).get("parents")
if ent_parents is not None and isinstance(ent_parents, list):
return ent_parents
output = []
if entity.get('data', {}).get('visualParent', None) is None:
return output
parent = self.dbcon.find_one({'_id': entity['data']['visualParent']})
output.append(parent['name'])
output.extend(self.get_parents(parent))
return output
def _set_projects(self):
projects = list()
for project in self.dbcon.projects():
projects.append(project['name'])
self.combo_projects.clear()
if len(projects) > 0:
self.combo_projects.addItems(projects)
self.dbcon.activate_project(projects[0])
def on_project_change(self):
projects = list()
for project in self.dbcon.projects():
projects.append(project['name'])
project_name = self.combo_projects.currentText()
if project_name in projects:
self.dbcon.activate_project(project_name)
self.refresh()
def _refresh_model(self):
with preserve_expanded_rows(
self.view, column=0, role=self.model.ObjectIdRole
):
with preserve_selection(
self.view, column=0, role=self.model.ObjectIdRole
):
self.model.refresh()
self.assets_refreshed.emit()
def refresh(self):
self._refresh_model()
def _refresh_tasks(self):
tasks = []
selected = self.get_selected_assets()
if len(selected) == 1:
asset = self.dbcon.find_one({
"_id": selected[0], "type": "asset"
})
if asset:
tasks = asset.get('data', {}).get('tasks', [])
self.task_model.set_tasks(tasks)
self.task_view.setVisible(len(tasks)>0)
def get_active_asset(self):
"""Return the asset id the current asset."""
current = self.view.currentIndex()
return current.data(self.model.ItemRole)
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, key="name"):
"""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
if not isinstance(assets, (tuple, list)):
assets = [assets]
assert isinstance(
assets, (tuple, list)
), "Assets must be list or tuple"
# convert to list - tuple cant be modified
assets = list(assets)
# Clear selection
selection_model = self.view.selectionModel()
selection_model.clearSelection()
# Select
mode = selection_model.Select | selection_model.Rows
for index in lib.iter_model_rows(
self.proxy, column=0, include_root=False
):
# stop iteration if there are no assets to process
if not assets:
break
value = index.data(self.model.ItemRole).get(key)
if value not in assets:
continue
# Remove processed asset
assets.pop(assets.index(value))
selection_model.select(index, mode)
if expand:
# Expand parent index
self.view.expand(self.proxy.parent(index))
# Set the currently active index
self.view.setCurrentIndex(index)

View file

@ -0,0 +1,522 @@
import os
from Qt import QtCore, QtGui, QtWidgets
from pype.resources import get_resource
from avalon import style
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)
preview_text = "PREVIEW"
thumbnail_text = "THUMBNAIL"
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))
self.parent_list = parent
self.parent_widget = main_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.btn_action_menu = PngButton(
name="menu", size=QtCore.QSize(22, 22)
)
self.action_menu = QtWidgets.QMenu()
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.btn_action_menu, 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 = LightingButton(self.preview_text)
self.thumbnail = LightingButton(self.thumbnail_text)
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 = PngButton(name="trash", size=QtCore.QSize(22, 22))
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.btn_action_menu.setVisible(False)
self.in_data = data
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']
file_info = data['file_info']
thumb = data['thumb']
prev = data['prev']
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 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 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)
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)
def _thumbnail_clicked(self):
self.signal_thumbnail.emit(self)
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.isChecked()
def change_thumbnail(self, hover=True):
self.thumbnail.setChecked(hover)
def is_preview(self):
return self.preview.isChecked()
def change_preview(self, hover=True):
self.preview.setChecked(hover)
def collect_data(self):
in_files = self.in_data['files']
staging_dir = os.path.dirname(in_files[0])
files = [os.path.basename(file) for file in in_files]
if len(files) == 1:
files = files[0]
data = {
'ext': self.in_data['ext'],
'label': self.name.text(),
'name': self.input_repre.text(),
'stagingDir': staging_dir,
'files': files,
'thumbnail': self.is_thumbnail(),
'preview': self.is_preview()
}
if ("frameStart" in self.in_data and "frameEnd" in self.in_data):
data["frameStart"] = self.in_data["frameStart"]
data["frameEnd"] = self.in_data["frameEnd"]
if 'fps' in self.in_data:
data['fps'] = self.in_data['fps']
return data
class LightingButton(QtWidgets.QPushButton):
lightingbtnstyle = """
QPushButton {
font: %(font_size_pt)spt;
text-align: center;
color: #777777;
background-color: transparent;
border-width: 1px;
border-color: #777777;
border-style: solid;
padding-top: 0px;
padding-bottom: 0px;
padding-left: 3px;
padding-right: 3px;
border-radius: 3px;
}
QPushButton:hover {
border-color: #cccccc;
color: #cccccc;
}
QPushButton:pressed {
border-color: #ffffff;
color: #ffffff;
}
QPushButton:disabled {
border-color: #3A3939;
color: #3A3939;
}
QPushButton:checked {
border-color: #4BB543;
color: #4BB543;
}
QPushButton:checked:hover {
border-color: #4Bd543;
color: #4Bd543;
}
QPushButton:checked:pressed {
border-color: #4BF543;
color: #4BF543;
}
"""
def __init__(self, text, font_size_pt=8, *args, **kwargs):
super(LightingButton, self).__init__(text, *args, **kwargs)
self.setStyleSheet(self.lightingbtnstyle % {
"font_size_pt": font_size_pt
})
self.setCheckable(True)
class PngFactory:
png_names = {
"trash": {
"normal": QtGui.QIcon(get_resource("trash.png")),
"hover": QtGui.QIcon(get_resource("trash_hover.png")),
"pressed": QtGui.QIcon(get_resource("trash_pressed.png")),
"pressed_hover": QtGui.QIcon(
get_resource("trash_pressed_hover.png")
),
"disabled": QtGui.QIcon(get_resource("trash_disabled.png"))
},
"menu": {
"normal": QtGui.QIcon(get_resource("menu.png")),
"hover": QtGui.QIcon(get_resource("menu_hover.png")),
"pressed": QtGui.QIcon(get_resource("menu_pressed.png")),
"pressed_hover": QtGui.QIcon(
get_resource("menu_pressed_hover.png")
),
"disabled": QtGui.QIcon(get_resource("menu_disabled.png"))
}
}
class PngButton(QtWidgets.QPushButton):
png_button_style = """
QPushButton {
border: none;
background-color: transparent;
padding-top: 0px;
padding-bottom: 0px;
padding-left: 0px;
padding-right: 0px;
}
QPushButton:hover {}
QPushButton:pressed {}
QPushButton:disabled {}
QPushButton:checked {}
QPushButton:checked:hover {}
QPushButton:checked:pressed {}
"""
def __init__(
self, name=None, path=None, hover_path=None, pressed_path=None,
hover_pressed_path=None, disabled_path=None,
size=None, *args, **kwargs
):
self._hovered = False
self._pressed = False
super(PngButton, self).__init__(*args, **kwargs)
self.setStyleSheet(self.png_button_style)
png_dict = {}
if name:
png_dict = PngFactory.png_names.get(name) or {}
if not png_dict:
print((
"WARNING: There is not set icon with name \"{}\""
"in PngFactory!"
).format(name))
ico_normal = png_dict.get("normal")
ico_hover = png_dict.get("hover")
ico_pressed = png_dict.get("pressed")
ico_hover_pressed = png_dict.get("pressed_hover")
ico_disabled = png_dict.get("disabled")
if path:
ico_normal = QtGui.QIcon(path)
if hover_path:
ico_hover = QtGui.QIcon(hover_path)
if pressed_path:
ico_pressed = QtGui.QIcon(hover_path)
if hover_pressed_path:
ico_hover_pressed = QtGui.QIcon(hover_pressed_path)
if disabled_path:
ico_disabled = QtGui.QIcon(disabled_path)
self.setIcon(ico_normal)
if size:
self.setIconSize(size)
self.setMaximumSize(size)
self.ico_normal = ico_normal
self.ico_hover = ico_hover
self.ico_pressed = ico_pressed
self.ico_hover_pressed = ico_hover_pressed
self.ico_disabled = ico_disabled
def setDisabled(self, in_bool):
super(PngButton, self).setDisabled(in_bool)
icon = self.ico_normal
if in_bool and self.ico_disabled:
icon = self.ico_disabled
self.setIcon(icon)
def enterEvent(self, event):
self._hovered = True
if not self.isEnabled():
return
icon = self.ico_normal
if self.ico_hover:
icon = self.ico_hover
if self._pressed and self.ico_hover_pressed:
icon = self.ico_hover_pressed
if self.icon() != icon:
self.setIcon(icon)
def mouseMoveEvent(self, event):
super(PngButton, self).mouseMoveEvent(event)
if self._pressed:
mouse_pos = event.pos()
hovering = self.rect().contains(mouse_pos)
if hovering and not self._hovered:
self.enterEvent(event)
elif not hovering and self._hovered:
self.leaveEvent(event)
def leaveEvent(self, event):
self._hovered = False
if not self.isEnabled():
return
icon = self.ico_normal
if self._pressed and self.ico_pressed:
icon = self.ico_pressed
if self.icon() != icon:
self.setIcon(icon)
def mousePressEvent(self, event):
self._pressed = True
if not self.isEnabled():
return
icon = self.ico_hover
if self.ico_pressed:
icon = self.ico_pressed
if self.ico_hover_pressed:
mouse_pos = event.pos()
if self.rect().contains(mouse_pos):
icon = self.ico_hover_pressed
if icon is None:
icon = self.ico_normal
if self.icon() != icon:
self.setIcon(icon)
def mouseReleaseEvent(self, event):
if not self.isEnabled():
return
if self._pressed:
self._pressed = False
mouse_pos = event.pos()
if self.rect().contains(mouse_pos):
self.clicked.emit()
icon = self.ico_normal
if self._hovered and self.ico_hover:
icon = self.ico_hover
if self.icon() != icon:
self.setIcon(icon)

View file

@ -0,0 +1,224 @@
import os
import sys
import json
import tempfile
import random
import string
from Qt import QtWidgets, QtCore
from . import DropDataFrame
from avalon import io
from pype.api import execute, Logger
log = Logger().get_logger("standalonepublisher")
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)
buttons = QtWidgets.QWidget()
layout = QtWidgets.QHBoxLayout(buttons)
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)
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.btn_publish.clicked.connect(self._publish)
self.initialized = True
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, 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)
def collect_data(self):
return self.drop_frame.collect_data()
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)
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):
log.info(self.parent_widget.pyblish_paths)
self.working_start('Pyblish is running')
try:
data = self.parent_widget.collect_data()
set_context(
data['project'],
data['asset'],
data['task']
)
result = cli_publish(data, self.parent_widget.pyblish_paths)
# 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()
def set_context(project, asset, task):
''' 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
io.Session["AVALON_ASSET"] = asset
if not task:
task = ''
os.environ["AVALON_TASK"] = task
io.Session["AVALON_TASK"] = task
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_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', '')
io.Session["current_dir"] = os.path.normpath(os.getcwd())
os.environ["AVALON_APP"] = "standalonepublish"
io.Session["AVALON_APP"] = "standalonepublish"
io.uninstall()
def cli_publish(data, publish_paths, gui=True):
PUBLISH_SCRIPT_PATH = os.path.join(
os.path.dirname(os.path.dirname(__file__)),
"publish.py"
)
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 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)
envcopy = os.environ.copy()
envcopy["PYBLISH_HOSTS"] = "standalonepublisher"
envcopy["SAPUBLISH_INPATH"] = json_data_path
envcopy["PYBLISHGUI"] = "pyblish_pype"
envcopy["PUBLISH_PATHS"] = os.pathsep.join(publish_paths)
if data.get("family", "").lower() == "editorial":
envcopy["PYBLISH_SUSPEND_LOGS"] = "1"
result = execute(
[sys.executable, PUBLISH_SCRIPT_PATH],
env=envcopy
)
result = {}
if os.path.exists(json_data_path):
with open(json_data_path, "r") as f:
result = json.load(f)
log.info(f"Publish result: {result}")
io.uninstall()
return False

View file

@ -0,0 +1,89 @@
from Qt import 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.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)

View file

@ -0,0 +1,51 @@
from Qt 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: transparent;'
)
font.setPointSize(12)
self._sub_label = QtWidgets.QLabel('(drop files here)')
self._sub_label.setFont(font)
self._sub_label.setStyleSheet(
'background-color: transparent;'
)
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
)

View file

@ -0,0 +1,495 @@
import os
import re
import json
import clique
import subprocess
import pype.lib
from Qt import QtWidgets, QtCore
from . import DropEmpty, ComponentsList, ComponentItem
class DropDataFrame(QtWidgets.QFrame):
image_extensions = [
".ani", ".anim", ".apng", ".art", ".bmp", ".bpg", ".bsave", ".cal",
".cin", ".cpc", ".cpt", ".dds", ".dpx", ".ecw", ".exr", ".fits",
".flic", ".flif", ".fpx", ".gif", ".hdri", ".hevc", ".icer",
".icns", ".ico", ".cur", ".ics", ".ilbm", ".jbig", ".jbig2",
".jng", ".jpeg", ".jpeg-ls", ".jpeg", ".2000", ".jpg", ".xr",
".jpeg", ".xt", ".jpeg-hdr", ".kra", ".mng", ".miff", ".nrrd",
".ora", ".pam", ".pbm", ".pgm", ".ppm", ".pnm", ".pcx", ".pgf",
".pictor", ".png", ".psb", ".psp", ".qtvr", ".ras",
".rgbe", ".logluv", ".tiff", ".sgi", ".tga", ".tiff", ".tiff/ep",
".tiff/it", ".ufo", ".ufp", ".wbmp", ".webp", ".xbm", ".xcf",
".xpm", ".xwd"
]
video_extensions = [
".3g2", ".3gp", ".amv", ".asf", ".avi", ".drc", ".f4a", ".f4b",
".f4p", ".f4v", ".flv", ".gif", ".gifv", ".m2v", ".m4p", ".m4v",
".mkv", ".mng", ".mov", ".mp2", ".mp4", ".mpe", ".mpeg", ".mpg",
".mpv", ".mxf", ".nsv", ".ogg", ".ogv", ".qt", ".rm", ".rmvb",
".roq", ".svi", ".vob", ".webm", ".wmv", ".yuv"
]
extensions = {
"nuke": [".nk"],
"maya": [".ma", ".mb"],
"houdini": [".hip"],
"image_file": image_extensions,
"video_file": video_extensions
}
def __init__(self, parent):
super().__init__()
self.parent_widget = parent
self.setAcceptDrops(True)
layout = QtWidgets.QVBoxLayout(self)
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)
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):
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 = os.path.normpath(ent.text())
if os.path.exists(path):
paths.append(path)
else:
print('Dropped invalid file/folder')
except Exception:
pass
if paths:
self._process_paths(paths)
def _processMimeData(self, mimeData):
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_item(self, data, actions=[]):
# Assign to self so garbage collector wont remove the component
# during initialization
new_component = ComponentItem(self.components_list, self)
new_component.set_context(data)
self.components_list.add_widget(new_component)
new_component.signal_remove.connect(self._remove_item)
new_component.signal_preview.connect(self._set_preview)
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)
if len(self.components_list.widgets()) == 1:
self.parent_widget.set_valid_repre_names(True)
self._refresh_view()
def _set_thumbnail(self, in_item):
current_state = in_item.is_thumbnail()
in_item.change_thumbnail(not current_state)
checked_item = None
for item in self.components_list.widgets():
if item.is_thumbnail():
checked_item = item
break
if checked_item is not None and checked_item != in_item:
checked_item.change_thumbnail(False)
in_item.change_thumbnail(current_state)
def _set_preview(self, in_item):
current_state = in_item.is_preview()
in_item.change_preview(not current_state)
checked_item = None
for item in self.components_list.widgets():
if item.is_preview():
checked_item = item
break
if checked_item is not None and checked_item != in_item:
checked_item.change_preview(False)
in_item.change_preview(current_state)
def _remove_item(self, 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 valid_repre:
return
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.components_list.widgets()) == 0
self.components_list.setVisible(not _bool)
self.drop_widget.setVisible(_bool)
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)
collectionable_paths = []
non_collectionable_paths = []
for path in in_paths:
ext = os.path.splitext(path)[1]
if ext in self.image_extensions:
collectionable_paths.append(path)
else:
non_collectionable_paths.append(path)
collections, remainders = clique.assemble(collectionable_paths)
non_collectionable_paths.extend(remainders)
for collection in collections:
self._process_collection(collection)
for remainder in non_collectionable_paths:
self._process_remainder(remainder)
self.parent_widget.working_stop()
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 = collection.format('{ranges}')
# TODO: ranges must not be with missing frames!!!
# - this is goal implementation:
# startFrame, endFrame = range.split('-')
rngs = range.split(',')
startFrame = rngs[0].split('-')[0]
endFrame = rngs[-1].split('-')[-1]
actions = []
data = {
'files': [file for file in collection],
'name': file_base,
'ext': file_ext,
'file_info': range,
"frameStart": startFrame,
"frameEnd": endFrame,
'representation': repr_name,
'folder_path': folder_path,
'is_sequence': True,
'actions': actions
}
self._process_data(data)
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
files = []
files.append(remainder)
actions = []
data = {
'files': files,
'name': file_base,
'ext': file_ext,
'representation': repr_name,
'folder_path': folder_path,
'is_sequence': False,
'actions': actions
}
self._process_data(data)
def load_data_with_probe(self, filepath):
ffprobe_path = pype.lib.get_ffmpeg_tool_path("ffprobe")
args = [
ffprobe_path,
'-v', 'quiet',
'-print_format', 'json',
'-show_format',
'-show_streams', filepath
]
ffprobe_p = subprocess.Popen(
' '.join(args),
stdout=subprocess.PIPE,
shell=True
)
ffprobe_output = ffprobe_p.communicate()[0]
if ffprobe_p.returncode != 0:
raise RuntimeError(
'Failed on ffprobe: check if ffprobe path is set in PATH env'
)
return json.loads(ffprobe_output)['streams'][0]
def get_file_data(self, data):
filepath = data['files'][0]
ext = data['ext'].lower()
output = {}
file_info = None
if 'file_info' in data:
file_info = data['file_info']
if ext in self.image_extensions or ext in self.video_extensions:
probe_data = self.load_data_with_probe(filepath)
if 'fps' not in data:
# default value
fps = 25
fps_string = probe_data.get('r_frame_rate')
if fps_string:
fps = int(fps_string.split('/')[0])
output['fps'] = fps
if "frameStart" not in data or "frameEnd" not in data:
startFrame = endFrame = 1
endFrame_string = probe_data.get('nb_frames')
if endFrame_string:
endFrame = int(endFrame_string)
output["frameStart"] = startFrame
output["frameEnd"] = endFrame
if (ext == '.mov') and (not file_info):
file_info = probe_data.get('codec_name')
output['file_info'] = file_info
return output
def _process_data(self, data):
ext = data['ext']
# load file data info
file_data = self.get_file_data(data)
for key, value in file_data.items():
data[key] = value
icon = 'default'
for ico, exts in self.extensions.items():
if ext in exts:
icon = ico
break
new_is_seq = data['is_sequence']
# Add 's' to icon_name if is sequence (image -> images)
if new_is_seq:
icon += 's'
data['icon'] = icon
data['thumb'] = (
ext in self.image_extensions
or ext in self.video_extensions
)
data['prev'] = (
ext in self.video_extensions
or (new_is_seq and ext in self.image_extensions)
)
actions = []
found = False
if data["ext"] in self.image_extensions:
for item in self.components_list.widgets():
if data['ext'] != item.in_data['ext']:
continue
if data['folder_path'] != item.in_data['folder_path']:
continue
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']:
found = True
break
paths = list(data['files'])
paths.extend(item.in_data['files'])
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)
# 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]
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
a_name = 'merge'
item.add_action(a_name)
if a_name not in actions:
actions.append(a_name)
# If both are sequence
else:
if data['name'] != item.in_data['name']:
continue
if data['files'] == list(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:
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.components_list.widgets():
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('_', ''))
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
if repre_name.strip() == '':
in_item.set_repre_name_valid(False)
is_valid = False
else:
for item in self.components_list.widgets():
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.components_list.widgets():
if item.has_valid_repre:
continue
self.repre_name_changed(item, item.input_repre.text())
for item in self.components_list.widgets():
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 = []
in_paths = in_item.in_data['files']
paths = in_paths
for item in self.components_list.widgets():
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 = {'representations' : []}
for item in self.components_list.widgets():
data['representations'].append(item.collect_data())
return data

View file

@ -0,0 +1,347 @@
from collections import namedtuple
from Qt import QtWidgets, QtCore
from . import HelpRole, FamilyRole, ExistsRole, PluginRole, PluginKeyRole
from . import FamilyDescriptionWidget
from pype.api import config
class FamilyWidget(QtWidgets.QWidget):
stateChanged = QtCore.Signal(bool)
data = dict()
_jobs = dict()
Separator = "---separator---"
NOT_SELECTED = '< Nothing is selected >'
def __init__(self, dbcon, parent=None):
super(FamilyWidget, self).__init__(parent=parent)
# Store internal states in here
self.state = {"valid": False}
self.dbcon = dbcon
self.asset_name = self.NOT_SELECTED
body = QtWidgets.QWidget()
lists = QtWidgets.QWidget()
container = QtWidgets.QWidget()
list_families = QtWidgets.QListWidget()
input_subset = QtWidgets.QLineEdit()
input_result = QtWidgets.QLineEdit()
input_result.setStyleSheet("color: #BBBBBB;")
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)
# version
version_spinbox = QtWidgets.QSpinBox()
version_spinbox.setMinimum(1)
version_spinbox.setMaximum(9999)
version_spinbox.setEnabled(False)
version_spinbox.setStyleSheet("color: #BBBBBB;")
version_checkbox = QtWidgets.QCheckBox("Next Available Version")
version_checkbox.setCheckState(QtCore.Qt.CheckState(2))
version_layout = QtWidgets.QHBoxLayout()
version_layout.addWidget(version_spinbox)
version_layout.addWidget(version_checkbox)
layout = QtWidgets.QVBoxLayout(container)
header = FamilyDescriptionWidget(parent=self)
layout.addWidget(header)
layout.addWidget(QtWidgets.QLabel("Family"))
layout.addWidget(list_families)
layout.addWidget(QtWidgets.QLabel("Subset"))
layout.addLayout(name_layout)
layout.addWidget(input_result)
layout.addWidget(QtWidgets.QLabel("Version"))
layout.addLayout(version_layout)
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)
list_families.currentItemChanged.connect(self.on_selection_changed)
list_families.currentItemChanged.connect(header.set_item)
version_checkbox.stateChanged.connect(self.on_version_refresh)
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_result = input_result
self.version_checkbox = version_checkbox
self.version_spinbox = version_spinbox
self.refresh()
def collect_data(self):
plugin = self.list_families.currentItem().data(PluginRole)
key = self.list_families.currentItem().data(PluginKeyRole)
family = plugin.family.rsplit(".", 1)[-1]
data = {
'family_preset_key': key,
'family': family,
'subset': self.input_result.text(),
'version': self.version_spinbox.value()
}
return data
def change_asset(self, name):
if name is None:
name = self.NOT_SELECTED
self.asset_name = name
self.on_data_changed()
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):
asset_name = self.asset_name
subset_name = self.input_subset.text()
item = self.list_families.currentItem()
if item is None:
return
assets = None
if asset_name != self.NOT_SELECTED:
# Get the assets from the database which match with the name
assets_db = self.dbcon.find(
filter={"type": "asset"},
projection={"name": 1}
)
assets = [
asset for asset in assets_db if asset_name in asset["name"]
]
# Get plugin and family
plugin = item.data(PluginRole)
if plugin is None:
return
family = plugin.family.rsplit(".", 1)[-1]
# Update the result
if subset_name:
subset_name = subset_name[0].upper() + subset_name[1:]
self.input_result.setText("{}{}".format(family, subset_name))
if assets:
# Get all subsets of the current asset
asset_ids = [asset["_id"] for asset in assets]
subsets = self.dbcon.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)
item.setData(ExistsRole, True)
else:
self._build_menu([])
item.setData(ExistsRole, False)
if asset_name != self.NOT_SELECTED:
# TODO add logging into standalone_publish
print("'%s' not found .." % asset_name)
self.on_version_refresh()
# Update the valid state
valid = (
asset_name != self.NOT_SELECTED and
subset_name.strip() != "" and
item.data(QtCore.Qt.ItemIsEnabled) and
item.data(ExistsRole)
)
self.stateChanged.emit(valid)
def on_version_refresh(self):
auto_version = self.version_checkbox.isChecked()
self.version_spinbox.setEnabled(not auto_version)
if not auto_version:
return
asset_name = self.asset_name
subset_name = self.input_result.text()
version = 1
if (
asset_name != self.NOT_SELECTED and
subset_name.strip() != ''
):
asset = self.dbcon.find_one({
'type': 'asset',
'name': asset_name
})
subset = self.dbcon.find_one({
'type': 'subset',
'parent': asset['_id'],
'name': subset_name
})
if subset:
versions = self.dbcon.find({
'type': 'version',
'parent': subset['_id']
})
if versions:
versions = sorted(
[v for v in versions],
key=lambda ver: ver['name']
)
version = int(versions[-1]['name']) + 1
self.version_spinbox.setValue(version)
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
presets = config.get_presets().get('standalone_publish', {})
for key, creator in presets.get('families', {}).items():
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(PluginKeyRole, key)
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)
self.list_families.setCurrentItem(self.list_families.item(0))
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

View file

@ -0,0 +1,95 @@
from Qt import QtWidgets, QtCore, QtGui
from . import FamilyRole, PluginRole
from avalon.vendor import qtawesome
import six
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 = qtawesome.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)

View file

@ -0,0 +1,42 @@
from Qt 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()