Merge pull request #442 from pypeclub/feature/standalone_publisher_as_tool
Feature/standalone publisher as tool
8
pype/tools/standalonepublish/__init__.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
from .app import (
|
||||
show,
|
||||
cli
|
||||
)
|
||||
__all__ = [
|
||||
"show",
|
||||
"cli"
|
||||
]
|
||||
24
pype/tools/standalonepublish/__main__.py
Normal 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_())
|
||||
192
pype/tools/standalonepublish/app.py
Normal 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
|
||||
35
pype/tools/standalonepublish/publish.py
Normal 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))
|
||||
14
pype/tools/standalonepublish/resources/__init__.py
Normal 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))
|
||||
9
pype/tools/standalonepublish/resources/edit.svg
Normal 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 |
BIN
pype/tools/standalonepublish/resources/file.png
Normal file
|
After Width: | Height: | Size: 803 B |
BIN
pype/tools/standalonepublish/resources/files.png
Normal file
|
After Width: | Height: | Size: 484 B |
BIN
pype/tools/standalonepublish/resources/houdini.png
Normal file
|
After Width: | Height: | Size: 257 KiB |
BIN
pype/tools/standalonepublish/resources/image_file.png
Normal file
|
After Width: | Height: | Size: 5 KiB |
BIN
pype/tools/standalonepublish/resources/image_files.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
14
pype/tools/standalonepublish/resources/information.svg
Normal 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 |
BIN
pype/tools/standalonepublish/resources/maya.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
pype/tools/standalonepublish/resources/menu.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
pype/tools/standalonepublish/resources/menu_disabled.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
pype/tools/standalonepublish/resources/menu_hover.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
pype/tools/standalonepublish/resources/menu_pressed.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
pype/tools/standalonepublish/resources/menu_pressed_hover.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
pype/tools/standalonepublish/resources/nuke.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
pype/tools/standalonepublish/resources/premiere.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
pype/tools/standalonepublish/resources/trash.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
pype/tools/standalonepublish/resources/trash_disabled.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
pype/tools/standalonepublish/resources/trash_hover.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
pype/tools/standalonepublish/resources/trash_pressed.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
pype/tools/standalonepublish/resources/trash_pressed_hover.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
pype/tools/standalonepublish/resources/video_file.png
Normal file
|
After Width: | Height: | Size: 120 B |
28
pype/tools/standalonepublish/widgets/__init__.py
Normal 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
|
||||
200
pype/tools/standalonepublish/widgets/model_asset.py
Normal 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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
56
pype/tools/standalonepublish/widgets/model_node.py
Normal 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)
|
||||
64
pype/tools/standalonepublish/widgets/model_tasks_template.py
Normal 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)
|
||||
122
pype/tools/standalonepublish/widgets/model_tree.py
Normal 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()
|
||||
|
|
@ -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)
|
||||
343
pype/tools/standalonepublish/widgets/widget_asset.py
Normal 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)
|
||||
522
pype/tools/standalonepublish/widgets/widget_component_item.py
Normal 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)
|
||||
224
pype/tools/standalonepublish/widgets/widget_components.py
Normal 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
|
||||
|
|
@ -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)
|
||||
51
pype/tools/standalonepublish/widgets/widget_drop_empty.py
Normal 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
|
||||
)
|
||||
495
pype/tools/standalonepublish/widgets/widget_drop_frame.py
Normal 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
|
||||
347
pype/tools/standalonepublish/widgets/widget_family.py
Normal 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
|
||||
95
pype/tools/standalonepublish/widgets/widget_family_desc.py
Normal 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)
|
||||
42
pype/tools/standalonepublish/widgets/widget_shadow.py
Normal 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()
|
||||