Merged in 2.0/PYPE-285_standalone_publish (pull request #132)
2.0/PYPE-285 standalone publish Approved-by: Milan Kolar <milan@orbi.tools>
88
pype/plugins/standalonepublish/publish/collect_context.py
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import os
|
||||
import pyblish.api
|
||||
from avalon import (
|
||||
io,
|
||||
api as avalon
|
||||
)
|
||||
import json
|
||||
import logging
|
||||
import clique
|
||||
|
||||
|
||||
log = logging.getLogger("collector")
|
||||
|
||||
|
||||
class CollectContextDataSAPublish(pyblish.api.ContextPlugin):
|
||||
"""
|
||||
Collecting temp json data sent from a host context
|
||||
and path for returning json data back to hostself.
|
||||
|
||||
Setting avalon session into correct context
|
||||
|
||||
Args:
|
||||
context (obj): pyblish context session
|
||||
|
||||
"""
|
||||
|
||||
label = "Collect Context - SA Publish"
|
||||
order = pyblish.api.CollectorOrder - 0.49
|
||||
|
||||
def process(self, context):
|
||||
# get json paths from os and load them
|
||||
io.install()
|
||||
input_json_path = os.environ.get("SAPUBLISH_INPATH")
|
||||
output_json_path = os.environ.get("SAPUBLISH_OUTPATH")
|
||||
|
||||
context.data["stagingDir"] = os.path.dirname(input_json_path)
|
||||
context.data["returnJsonPath"] = output_json_path
|
||||
|
||||
with open(input_json_path, "r") as f:
|
||||
in_data = json.load(f)
|
||||
|
||||
project_name = in_data['project']
|
||||
asset_name = in_data['asset']
|
||||
family = in_data['family']
|
||||
subset = in_data['subset']
|
||||
|
||||
project = io.find_one({'type': 'project'})
|
||||
asset = io.find_one({
|
||||
'type': 'asset',
|
||||
'name': asset_name
|
||||
})
|
||||
context.data['project'] = project
|
||||
context.data['asset'] = asset
|
||||
|
||||
instance = context.create_instance(subset)
|
||||
|
||||
instance.data.update({
|
||||
"subset": family + subset,
|
||||
"asset": asset_name,
|
||||
"label": family + subset,
|
||||
"name": family + subset,
|
||||
"family": family,
|
||||
"families": [family, 'ftrack'],
|
||||
})
|
||||
self.log.info("collected instance: {}".format(instance.data))
|
||||
|
||||
instance.data["files"] = list()
|
||||
instance.data['destination_list'] = list()
|
||||
instance.data['representations'] = list()
|
||||
|
||||
for component in in_data['representations']:
|
||||
# instance.add(node)
|
||||
component['destination'] = component['files']
|
||||
collections, remainder = clique.assemble(component['files'])
|
||||
if collections:
|
||||
self.log.debug(collections)
|
||||
range = collections[0].format('{range}')
|
||||
instance.data['startFrame'] = range.split('-')[0]
|
||||
instance.data['endFrame'] = range.split('-')[1]
|
||||
|
||||
|
||||
instance.data["files"].append(component)
|
||||
instance.data["representations"].append(component)
|
||||
|
||||
# "is_thumbnail": component['thumbnail'],
|
||||
# "is_preview": component['preview']
|
||||
|
||||
self.log.info(in_data)
|
||||
40
pype/plugins/standalonepublish/publish/collect_ftrack_api.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import os
|
||||
import pyblish.api
|
||||
|
||||
try:
|
||||
import ftrack_api_old as ftrack_api
|
||||
except Exception:
|
||||
import ftrack_api
|
||||
|
||||
|
||||
class CollectFtrackApi(pyblish.api.ContextPlugin):
|
||||
""" Collects an ftrack session and the current task id. """
|
||||
|
||||
order = pyblish.api.CollectorOrder
|
||||
label = "Collect Ftrack Api"
|
||||
|
||||
def process(self, context):
|
||||
|
||||
# Collect session
|
||||
session = ftrack_api.Session()
|
||||
context.data["ftrackSession"] = session
|
||||
|
||||
# Collect task
|
||||
|
||||
project = os.environ.get('AVALON_PROJECT', '')
|
||||
asset = os.environ.get('AVALON_ASSET', '')
|
||||
task = os.environ.get('AVALON_TASK', None)
|
||||
|
||||
if task:
|
||||
result = session.query('Task where\
|
||||
project.full_name is "{0}" and\
|
||||
name is "{1}" and\
|
||||
parent.name is "{2}"'.format(project, task, asset)).one()
|
||||
context.data["ftrackTask"] = result
|
||||
else:
|
||||
result = session.query('TypedContext where\
|
||||
project.full_name is "{0}" and\
|
||||
name is "{1}"'.format(project, asset)).one()
|
||||
context.data["ftrackEntity"] = result
|
||||
|
||||
self.log.info(result)
|
||||
17
pype/plugins/standalonepublish/publish/collect_templates.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
import pype.api as pype
|
||||
from pypeapp import Anatomy
|
||||
|
||||
import pyblish.api
|
||||
|
||||
|
||||
class CollectTemplates(pyblish.api.ContextPlugin):
|
||||
"""Inject the current working file into context"""
|
||||
|
||||
order = pyblish.api.CollectorOrder
|
||||
label = "Collect Templates"
|
||||
|
||||
def process(self, context):
|
||||
# pype.load_data_from_templates()
|
||||
context.data['anatomy'] = Anatomy()
|
||||
self.log.info("Anatomy templates collected...")
|
||||
12
pype/plugins/standalonepublish/publish/collect_time.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import pyblish.api
|
||||
from avalon import api
|
||||
|
||||
|
||||
class CollectTime(pyblish.api.ContextPlugin):
|
||||
"""Store global time at the time of publish"""
|
||||
|
||||
label = "Collect Current Time"
|
||||
order = pyblish.api.CollectorOrder
|
||||
|
||||
def process(self, context):
|
||||
context.data["time"] = api.time()
|
||||
448
pype/plugins/standalonepublish/publish/integrate.py
Normal file
|
|
@ -0,0 +1,448 @@
|
|||
import os
|
||||
import logging
|
||||
import shutil
|
||||
|
||||
import errno
|
||||
import pyblish.api
|
||||
from avalon import api, io
|
||||
from avalon.vendor import filelink
|
||||
import clique
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IntegrateAsset(pyblish.api.InstancePlugin):
|
||||
"""Resolve any dependency issies
|
||||
|
||||
This plug-in resolves any paths which, if not updated might break
|
||||
the published file.
|
||||
|
||||
The order of families is important, when working with lookdev you want to
|
||||
first publish the texture, update the texture paths in the nodes and then
|
||||
publish the shading network. Same goes for file dependent assets.
|
||||
"""
|
||||
|
||||
label = "Integrate Asset"
|
||||
order = pyblish.api.IntegratorOrder
|
||||
families = ["animation",
|
||||
"camera",
|
||||
"look",
|
||||
"mayaAscii",
|
||||
"model",
|
||||
"pointcache",
|
||||
"vdbcache",
|
||||
"setdress",
|
||||
"assembly",
|
||||
"layout",
|
||||
"rig",
|
||||
"vrayproxy",
|
||||
"yetiRig",
|
||||
"yeticache",
|
||||
"nukescript",
|
||||
# "review",
|
||||
"workfile",
|
||||
"scene",
|
||||
"ass"]
|
||||
exclude_families = ["clip"]
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
if [ef for ef in self.exclude_families
|
||||
if instance.data["family"] in ef]:
|
||||
return
|
||||
|
||||
self.register(instance)
|
||||
|
||||
self.log.info("Integrating Asset in to the database ...")
|
||||
self.integrate(instance)
|
||||
|
||||
def register(self, instance):
|
||||
# Required environment variables
|
||||
PROJECT = api.Session["AVALON_PROJECT"]
|
||||
ASSET = instance.data.get("asset") or api.Session["AVALON_ASSET"]
|
||||
LOCATION = api.Session["AVALON_LOCATION"]
|
||||
|
||||
context = instance.context
|
||||
# Atomicity
|
||||
#
|
||||
# Guarantee atomic publishes - each asset contains
|
||||
# an identical set of members.
|
||||
# __
|
||||
# / o
|
||||
# / \
|
||||
# | o |
|
||||
# \ /
|
||||
# o __/
|
||||
#
|
||||
assert all(result["success"] for result in context.data["results"]), (
|
||||
"Atomicity not held, aborting.")
|
||||
|
||||
# Assemble
|
||||
#
|
||||
# |
|
||||
# v
|
||||
# ---> <----
|
||||
# ^
|
||||
# |
|
||||
#
|
||||
# stagingdir = instance.data.get("stagingDir")
|
||||
# assert stagingdir, ("Incomplete instance \"%s\": "
|
||||
# "Missing reference to staging area." % instance)
|
||||
|
||||
# extra check if stagingDir actually exists and is available
|
||||
|
||||
# self.log.debug("Establishing staging directory @ %s" % stagingdir)
|
||||
|
||||
# Ensure at least one file is set up for transfer in staging dir.
|
||||
files = instance.data.get("files", [])
|
||||
assert files, "Instance has no files to transfer"
|
||||
assert isinstance(files, (list, tuple)), (
|
||||
"Instance 'files' must be a list, got: {0}".format(files)
|
||||
)
|
||||
|
||||
project = io.find_one({"type": "project"})
|
||||
|
||||
asset = io.find_one({"type": "asset",
|
||||
"name": ASSET,
|
||||
"parent": project["_id"]})
|
||||
|
||||
assert all([project, asset]), ("Could not find current project or "
|
||||
"asset '%s'" % ASSET)
|
||||
|
||||
subset = self.get_subset(asset, instance)
|
||||
|
||||
# get next version
|
||||
latest_version = io.find_one({"type": "version",
|
||||
"parent": subset["_id"]},
|
||||
{"name": True},
|
||||
sort=[("name", -1)])
|
||||
|
||||
next_version = 1
|
||||
if latest_version is not None:
|
||||
next_version += latest_version["name"]
|
||||
|
||||
self.log.info("Verifying version from assumed destination")
|
||||
|
||||
# assumed_data = instance.data["assumedTemplateData"]
|
||||
# assumed_version = assumed_data["version"]
|
||||
# if assumed_version != next_version:
|
||||
# raise AttributeError("Assumed version 'v{0:03d}' does not match"
|
||||
# "next version in database "
|
||||
# "('v{1:03d}')".format(assumed_version,
|
||||
# next_version))
|
||||
|
||||
self.log.debug("Next version: v{0:03d}".format(next_version))
|
||||
|
||||
version_data = self.create_version_data(context, instance)
|
||||
version = self.create_version(subset=subset,
|
||||
version_number=next_version,
|
||||
locations=[LOCATION],
|
||||
data=version_data)
|
||||
|
||||
self.log.debug("Creating version ...")
|
||||
version_id = io.insert_one(version).inserted_id
|
||||
instance.data['version'] = version['name']
|
||||
# Write to disk
|
||||
# _
|
||||
# | |
|
||||
# _| |_
|
||||
# ____\ /
|
||||
# |\ \ / \
|
||||
# \ \ v \
|
||||
# \ \________.
|
||||
# \|________|
|
||||
#
|
||||
root = api.registered_root()
|
||||
hierarchy = ""
|
||||
parents = io.find_one({
|
||||
"type": 'asset',
|
||||
"name": ASSET
|
||||
})['data']['parents']
|
||||
if parents and len(parents) > 0:
|
||||
# hierarchy = os.path.sep.join(hierarchy)
|
||||
hierarchy = os.path.join(*parents)
|
||||
|
||||
template_data = {"root": root,
|
||||
"project": {"name": PROJECT,
|
||||
"code": project['data']['code']},
|
||||
"silo": asset['silo'],
|
||||
"asset": ASSET,
|
||||
"family": instance.data['family'],
|
||||
"subset": subset["name"],
|
||||
"version": int(version["name"]),
|
||||
"hierarchy": hierarchy}
|
||||
|
||||
template_publish = project["config"]["template"]["publish"]
|
||||
anatomy = instance.context.data['anatomy']
|
||||
|
||||
# Find the representations to transfer amongst the files
|
||||
# Each should be a single representation (as such, a single extension)
|
||||
representations = []
|
||||
destination_list = []
|
||||
if 'transfers' not in instance.data:
|
||||
instance.data['transfers'] = []
|
||||
|
||||
for idx, repre in enumerate(instance.data["representations"]):
|
||||
|
||||
# Collection
|
||||
# _______
|
||||
# |______|\
|
||||
# | |\|
|
||||
# | ||
|
||||
# | ||
|
||||
# | ||
|
||||
# |_______|
|
||||
#
|
||||
|
||||
files = repre['files']
|
||||
|
||||
if len(files) > 1:
|
||||
src_collections, remainder = clique.assemble(files)
|
||||
self.log.debug("dst_collections: {}".format(str(src_collections)))
|
||||
src_collection = src_collections[0]
|
||||
# Assert that each member has identical suffix
|
||||
src_head = src_collection.format("{head}")
|
||||
src_tail = ext = src_collection.format("{tail}")
|
||||
|
||||
test_dest_files = list()
|
||||
for i in [1, 2]:
|
||||
template_data["representation"] = src_tail[1:]
|
||||
template_data["frame"] = src_collection.format(
|
||||
"{padding}") % i
|
||||
anatomy_filled = anatomy.format(template_data)
|
||||
test_dest_files.append(anatomy_filled["publish"]["path"])
|
||||
|
||||
dst_collections, remainder = clique.assemble(test_dest_files)
|
||||
dst_collection = dst_collections[0]
|
||||
dst_head = dst_collection.format("{head}")
|
||||
dst_tail = dst_collection.format("{tail}")
|
||||
|
||||
instance.data["representations"][idx]['published_path'] = dst_collection.format()
|
||||
|
||||
for i in src_collection.indexes:
|
||||
src_padding = src_collection.format("{padding}") % i
|
||||
src_file_name = "{0}{1}{2}".format(
|
||||
src_head, src_padding, src_tail)
|
||||
dst_padding = dst_collection.format("{padding}") % i
|
||||
dst = "{0}{1}{2}".format(dst_head, dst_padding, dst_tail)
|
||||
|
||||
# src = os.path.join(stagingdir, src_file_name)
|
||||
src = src_file_name
|
||||
self.log.debug("source: {}".format(src))
|
||||
|
||||
instance.data["transfers"].append([src, dst])
|
||||
|
||||
else:
|
||||
# Single file
|
||||
# _______
|
||||
# | |\
|
||||
# | |
|
||||
# | |
|
||||
# | |
|
||||
# |_______|
|
||||
#
|
||||
fname = files[0]
|
||||
# assert not os.path.isabs(fname), (
|
||||
# "Given file name is a full path"
|
||||
# )
|
||||
# _, ext = os.path.splitext(fname)
|
||||
|
||||
template_data["representation"] = repre['representation']
|
||||
|
||||
# src = os.path.join(stagingdir, fname)
|
||||
src = fname
|
||||
anatomy_filled = anatomy.format(template_data)
|
||||
dst = anatomy_filled["publish"]["path"]
|
||||
|
||||
instance.data["transfers"].append([src, dst])
|
||||
template = anatomy.templates["publish"]["path"]
|
||||
instance.data["representations"][idx]['published_path'] = dst
|
||||
|
||||
representation = {
|
||||
"schema": "pype:representation-2.0",
|
||||
"type": "representation",
|
||||
"parent": version_id,
|
||||
"name": repre['representation'],
|
||||
"data": {'path': dst, 'template': template},
|
||||
"dependencies": instance.data.get("dependencies", "").split(),
|
||||
|
||||
# Imprint shortcut to context
|
||||
# for performance reasons.
|
||||
"context": {
|
||||
"root": root,
|
||||
"project": {"name": PROJECT,
|
||||
"code": project['data']['code']},
|
||||
# 'task': api.Session["AVALON_TASK"],
|
||||
"silo": asset['silo'],
|
||||
"asset": ASSET,
|
||||
"family": instance.data['family'],
|
||||
"subset": subset["name"],
|
||||
"version": version["name"],
|
||||
"hierarchy": hierarchy,
|
||||
# "representation": repre['representation']
|
||||
}
|
||||
}
|
||||
|
||||
destination_list.append(dst)
|
||||
instance.data['destination_list'] = destination_list
|
||||
representations.append(representation)
|
||||
|
||||
self.log.info("Registering {} items".format(len(representations)))
|
||||
|
||||
io.insert_many(representations)
|
||||
|
||||
def integrate(self, instance):
|
||||
"""Move the files
|
||||
|
||||
Through `instance.data["transfers"]`
|
||||
|
||||
Args:
|
||||
instance: the instance to integrate
|
||||
"""
|
||||
|
||||
transfers = instance.data.get("transfers", list())
|
||||
|
||||
for src, dest in transfers:
|
||||
self.log.info("Copying file .. {} -> {}".format(src, dest))
|
||||
self.copy_file(src, dest)
|
||||
|
||||
# Produce hardlinked copies
|
||||
# Note: hardlink can only be produced between two files on the same
|
||||
# server/disk and editing one of the two will edit both files at once.
|
||||
# As such it is recommended to only make hardlinks between static files
|
||||
# to ensure publishes remain safe and non-edited.
|
||||
hardlinks = instance.data.get("hardlinks", list())
|
||||
for src, dest in hardlinks:
|
||||
self.log.info("Hardlinking file .. {} -> {}".format(src, dest))
|
||||
self.hardlink_file(src, dest)
|
||||
|
||||
def copy_file(self, src, dst):
|
||||
""" Copy given source to destination
|
||||
|
||||
Arguments:
|
||||
src (str): the source file which needs to be copied
|
||||
dst (str): the destination of the sourc file
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
dirname = os.path.dirname(dst)
|
||||
try:
|
||||
os.makedirs(dirname)
|
||||
except OSError as e:
|
||||
if e.errno == errno.EEXIST:
|
||||
pass
|
||||
else:
|
||||
self.log.critical("An unexpected error occurred.")
|
||||
raise
|
||||
|
||||
shutil.copy(src, dst)
|
||||
|
||||
def hardlink_file(self, src, dst):
|
||||
|
||||
dirname = os.path.dirname(dst)
|
||||
try:
|
||||
os.makedirs(dirname)
|
||||
except OSError as e:
|
||||
if e.errno == errno.EEXIST:
|
||||
pass
|
||||
else:
|
||||
self.log.critical("An unexpected error occurred.")
|
||||
raise
|
||||
|
||||
filelink.create(src, dst, filelink.HARDLINK)
|
||||
|
||||
def get_subset(self, asset, instance):
|
||||
|
||||
subset = io.find_one({"type": "subset",
|
||||
"parent": asset["_id"],
|
||||
"name": instance.data["subset"]})
|
||||
|
||||
if subset is None:
|
||||
subset_name = instance.data["subset"]
|
||||
self.log.info("Subset '%s' not found, creating.." % subset_name)
|
||||
|
||||
_id = io.insert_one({
|
||||
"schema": "avalon-core:subset-2.0",
|
||||
"type": "subset",
|
||||
"name": subset_name,
|
||||
"data": {},
|
||||
"parent": asset["_id"]
|
||||
}).inserted_id
|
||||
|
||||
subset = io.find_one({"_id": _id})
|
||||
|
||||
return subset
|
||||
|
||||
def create_version(self, subset, version_number, locations, data=None):
|
||||
""" Copy given source to destination
|
||||
|
||||
Args:
|
||||
subset (dict): the registered subset of the asset
|
||||
version_number (int): the version number
|
||||
locations (list): the currently registered locations
|
||||
|
||||
Returns:
|
||||
dict: collection of data to create a version
|
||||
"""
|
||||
# Imprint currently registered location
|
||||
version_locations = [location for location in locations if
|
||||
location is not None]
|
||||
|
||||
return {"schema": "avalon-core:version-2.0",
|
||||
"type": "version",
|
||||
"parent": subset["_id"],
|
||||
"name": version_number,
|
||||
"locations": version_locations,
|
||||
"data": data}
|
||||
|
||||
def create_version_data(self, context, instance):
|
||||
"""Create the data collection for the version
|
||||
|
||||
Args:
|
||||
context: the current context
|
||||
instance: the current instance being published
|
||||
|
||||
Returns:
|
||||
dict: the required information with instance.data as key
|
||||
"""
|
||||
|
||||
families = []
|
||||
current_families = instance.data.get("families", list())
|
||||
instance_family = instance.data.get("family", None)
|
||||
|
||||
if instance_family is not None:
|
||||
families.append(instance_family)
|
||||
families += current_families
|
||||
|
||||
self.log.debug("Registered root: {}".format(api.registered_root()))
|
||||
# # create relative source path for DB
|
||||
# try:
|
||||
# source = instance.data['source']
|
||||
# except KeyError:
|
||||
# source = context.data["currentFile"]
|
||||
#
|
||||
# relative_path = os.path.relpath(source, api.registered_root())
|
||||
# source = os.path.join("{root}", relative_path).replace("\\", "/")
|
||||
|
||||
source = "standalone"
|
||||
|
||||
# self.log.debug("Source: {}".format(source))
|
||||
version_data = {"families": families,
|
||||
"time": context.data["time"],
|
||||
"author": context.data["user"],
|
||||
"source": source,
|
||||
"comment": context.data.get("comment"),
|
||||
"machine": context.data.get("machine"),
|
||||
"fps": context.data.get("fps")}
|
||||
|
||||
# Include optional data if present in
|
||||
optionals = [
|
||||
"startFrame", "endFrame", "step", "handles", "sourceHashes"
|
||||
]
|
||||
for key in optionals:
|
||||
if key in instance.data:
|
||||
version_data[key] = instance.data[key]
|
||||
|
||||
return version_data
|
||||
315
pype/plugins/standalonepublish/publish/integrate_ftrack_api.py
Normal file
|
|
@ -0,0 +1,315 @@
|
|||
import os
|
||||
import sys
|
||||
import pyblish.api
|
||||
import clique
|
||||
|
||||
|
||||
class IntegrateFtrackApi(pyblish.api.InstancePlugin):
|
||||
""" Commit components to server. """
|
||||
|
||||
order = pyblish.api.IntegratorOrder+0.499
|
||||
label = "Integrate Ftrack Api"
|
||||
families = ["ftrack"]
|
||||
|
||||
def query(self, entitytype, data):
|
||||
""" Generate a query expression from data supplied.
|
||||
|
||||
If a value is not a string, we'll add the id of the entity to the
|
||||
query.
|
||||
|
||||
Args:
|
||||
entitytype (str): The type of entity to query.
|
||||
data (dict): The data to identify the entity.
|
||||
exclusions (list): All keys to exclude from the query.
|
||||
|
||||
Returns:
|
||||
str: String query to use with "session.query"
|
||||
"""
|
||||
queries = []
|
||||
if sys.version_info[0] < 3:
|
||||
for key, value in data.iteritems():
|
||||
if not isinstance(value, (basestring, int)):
|
||||
self.log.info("value: {}".format(value))
|
||||
if "id" in value.keys():
|
||||
queries.append(
|
||||
"{0}.id is \"{1}\"".format(key, value["id"])
|
||||
)
|
||||
else:
|
||||
queries.append("{0} is \"{1}\"".format(key, value))
|
||||
else:
|
||||
for key, value in data.items():
|
||||
if not isinstance(value, (str, int)):
|
||||
self.log.info("value: {}".format(value))
|
||||
if "id" in value.keys():
|
||||
queries.append(
|
||||
"{0}.id is \"{1}\"".format(key, value["id"])
|
||||
)
|
||||
else:
|
||||
queries.append("{0} is \"{1}\"".format(key, value))
|
||||
|
||||
query = (
|
||||
"select id from " + entitytype + " where " + " and ".join(queries)
|
||||
)
|
||||
self.log.debug(query)
|
||||
return query
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
session = instance.context.data["ftrackSession"]
|
||||
if instance.context.data.get("ftrackTask"):
|
||||
task = instance.context.data["ftrackTask"]
|
||||
name = task['full_name']
|
||||
parent = task["parent"]
|
||||
elif instance.context.data.get("ftrackEntity"):
|
||||
task = None
|
||||
name = instance.context.data.get("ftrackEntity")['name']
|
||||
parent = instance.context.data.get("ftrackEntity")
|
||||
|
||||
info_msg = "Created new {entity_type} with data: {data}"
|
||||
info_msg += ", metadata: {metadata}."
|
||||
|
||||
# Iterate over components and publish
|
||||
for data in instance.data.get("ftrackComponentsList", []):
|
||||
|
||||
# AssetType
|
||||
# Get existing entity.
|
||||
assettype_data = {"short": "upload"}
|
||||
assettype_data.update(data.get("assettype_data", {}))
|
||||
self.log.debug("data: {}".format(data))
|
||||
|
||||
assettype_entity = session.query(
|
||||
self.query("AssetType", assettype_data)
|
||||
).first()
|
||||
|
||||
# Create a new entity if none exits.
|
||||
if not assettype_entity:
|
||||
assettype_entity = session.create("AssetType", assettype_data)
|
||||
self.log.debug(
|
||||
"Created new AssetType with data: ".format(assettype_data)
|
||||
)
|
||||
|
||||
# Asset
|
||||
# Get existing entity.
|
||||
asset_data = {
|
||||
"name": name,
|
||||
"type": assettype_entity,
|
||||
"parent": parent,
|
||||
}
|
||||
asset_data.update(data.get("asset_data", {}))
|
||||
|
||||
asset_entity = session.query(
|
||||
self.query("Asset", asset_data)
|
||||
).first()
|
||||
|
||||
self.log.info("asset entity: {}".format(asset_entity))
|
||||
|
||||
# Extracting metadata, and adding after entity creation. This is
|
||||
# due to a ftrack_api bug where you can't add metadata on creation.
|
||||
asset_metadata = asset_data.pop("metadata", {})
|
||||
|
||||
# Create a new entity if none exits.
|
||||
if not asset_entity:
|
||||
asset_entity = session.create("Asset", asset_data)
|
||||
self.log.debug(
|
||||
info_msg.format(
|
||||
entity_type="Asset",
|
||||
data=asset_data,
|
||||
metadata=asset_metadata
|
||||
)
|
||||
)
|
||||
|
||||
# Adding metadata
|
||||
existing_asset_metadata = asset_entity["metadata"]
|
||||
existing_asset_metadata.update(asset_metadata)
|
||||
asset_entity["metadata"] = existing_asset_metadata
|
||||
|
||||
# AssetVersion
|
||||
# Get existing entity.
|
||||
assetversion_data = {
|
||||
"version": 0,
|
||||
"asset": asset_entity,
|
||||
}
|
||||
if task:
|
||||
assetversion_data['task'] = task
|
||||
|
||||
assetversion_data.update(data.get("assetversion_data", {}))
|
||||
|
||||
assetversion_entity = session.query(
|
||||
self.query("AssetVersion", assetversion_data)
|
||||
).first()
|
||||
|
||||
# Extracting metadata, and adding after entity creation. This is
|
||||
# due to a ftrack_api bug where you can't add metadata on creation.
|
||||
assetversion_metadata = assetversion_data.pop("metadata", {})
|
||||
|
||||
# Create a new entity if none exits.
|
||||
if not assetversion_entity:
|
||||
assetversion_entity = session.create(
|
||||
"AssetVersion", assetversion_data
|
||||
)
|
||||
self.log.debug(
|
||||
info_msg.format(
|
||||
entity_type="AssetVersion",
|
||||
data=assetversion_data,
|
||||
metadata=assetversion_metadata
|
||||
)
|
||||
)
|
||||
|
||||
# Adding metadata
|
||||
existing_assetversion_metadata = assetversion_entity["metadata"]
|
||||
existing_assetversion_metadata.update(assetversion_metadata)
|
||||
assetversion_entity["metadata"] = existing_assetversion_metadata
|
||||
|
||||
# Have to commit the version and asset, because location can't
|
||||
# determine the final location without.
|
||||
session.commit()
|
||||
|
||||
# Component
|
||||
# Get existing entity.
|
||||
component_data = {
|
||||
"name": "main",
|
||||
"version": assetversion_entity
|
||||
}
|
||||
component_data.update(data.get("component_data", {}))
|
||||
|
||||
component_entity = session.query(
|
||||
self.query("Component", component_data)
|
||||
).first()
|
||||
|
||||
component_overwrite = data.get("component_overwrite", False)
|
||||
location = data.get("component_location", session.pick_location())
|
||||
|
||||
# Overwrite existing component data if requested.
|
||||
if component_entity and component_overwrite:
|
||||
|
||||
origin_location = session.query(
|
||||
"Location where name is \"ftrack.origin\""
|
||||
).one()
|
||||
|
||||
# Removing existing members from location
|
||||
components = list(component_entity.get("members", []))
|
||||
components += [component_entity]
|
||||
for component in components:
|
||||
for loc in component["component_locations"]:
|
||||
if location["id"] == loc["location_id"]:
|
||||
location.remove_component(
|
||||
component, recursive=False
|
||||
)
|
||||
|
||||
# Deleting existing members on component entity
|
||||
for member in component_entity.get("members", []):
|
||||
session.delete(member)
|
||||
del(member)
|
||||
|
||||
session.commit()
|
||||
|
||||
# Reset members in memory
|
||||
if "members" in component_entity.keys():
|
||||
component_entity["members"] = []
|
||||
|
||||
# Add components to origin location
|
||||
try:
|
||||
collection = clique.parse(data["component_path"])
|
||||
except ValueError:
|
||||
# Assume its a single file
|
||||
# Changing file type
|
||||
name, ext = os.path.splitext(data["component_path"])
|
||||
component_entity["file_type"] = ext
|
||||
|
||||
origin_location.add_component(
|
||||
component_entity, data["component_path"]
|
||||
)
|
||||
else:
|
||||
# Changing file type
|
||||
component_entity["file_type"] = collection.format("{tail}")
|
||||
|
||||
# Create member components for sequence.
|
||||
for member_path in collection:
|
||||
|
||||
size = 0
|
||||
try:
|
||||
size = os.path.getsize(member_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
name = collection.match(member_path).group("index")
|
||||
|
||||
member_data = {
|
||||
"name": name,
|
||||
"container": component_entity,
|
||||
"size": size,
|
||||
"file_type": os.path.splitext(member_path)[-1]
|
||||
}
|
||||
|
||||
component = session.create(
|
||||
"FileComponent", member_data
|
||||
)
|
||||
origin_location.add_component(
|
||||
component, member_path, recursive=False
|
||||
)
|
||||
component_entity["members"].append(component)
|
||||
|
||||
# Add components to location.
|
||||
location.add_component(
|
||||
component_entity, origin_location, recursive=True
|
||||
)
|
||||
|
||||
data["component"] = component_entity
|
||||
msg = "Overwriting Component with path: {0}, data: {1}, "
|
||||
msg += "location: {2}"
|
||||
self.log.info(
|
||||
msg.format(
|
||||
data["component_path"],
|
||||
component_data,
|
||||
location
|
||||
)
|
||||
)
|
||||
|
||||
# Extracting metadata, and adding after entity creation. This is
|
||||
# due to a ftrack_api bug where you can't add metadata on creation.
|
||||
component_metadata = component_data.pop("metadata", {})
|
||||
|
||||
# Create new component if none exists.
|
||||
new_component = False
|
||||
if not component_entity:
|
||||
component_entity = assetversion_entity.create_component(
|
||||
data["component_path"],
|
||||
data=component_data,
|
||||
location=location
|
||||
)
|
||||
data["component"] = component_entity
|
||||
msg = "Created new Component with path: {0}, data: {1}"
|
||||
msg += ", metadata: {2}, location: {3}"
|
||||
self.log.info(
|
||||
msg.format(
|
||||
data["component_path"],
|
||||
component_data,
|
||||
component_metadata,
|
||||
location
|
||||
)
|
||||
)
|
||||
new_component = True
|
||||
|
||||
# Adding metadata
|
||||
existing_component_metadata = component_entity["metadata"]
|
||||
existing_component_metadata.update(component_metadata)
|
||||
component_entity["metadata"] = existing_component_metadata
|
||||
|
||||
# if component_data['name'] = 'ftrackreview-mp4-mp4':
|
||||
# assetversion_entity["thumbnail_id"]
|
||||
|
||||
# Setting assetversion thumbnail
|
||||
if data.get("thumbnail", False):
|
||||
assetversion_entity["thumbnail_id"] = component_entity["id"]
|
||||
|
||||
# Inform user about no changes to the database.
|
||||
if (component_entity and not component_overwrite and
|
||||
not new_component):
|
||||
data["component"] = component_entity
|
||||
self.log.info(
|
||||
"Found existing component, and no request to overwrite. "
|
||||
"Nothing has been changed."
|
||||
)
|
||||
else:
|
||||
# Commit changes.
|
||||
session.commit()
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
import pyblish.api
|
||||
import os
|
||||
import json
|
||||
|
||||
|
||||
class IntegrateFtrackInstance(pyblish.api.InstancePlugin):
|
||||
"""Collect ftrack component data
|
||||
|
||||
Add ftrack component list to instance.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
order = pyblish.api.IntegratorOrder + 0.48
|
||||
label = 'Integrate Ftrack Component'
|
||||
families = ["ftrack"]
|
||||
|
||||
family_mapping = {'camera': 'cam',
|
||||
'look': 'look',
|
||||
'mayaAscii': 'scene',
|
||||
'model': 'geo',
|
||||
'rig': 'rig',
|
||||
'setdress': 'setdress',
|
||||
'pointcache': 'cache',
|
||||
'write': 'img',
|
||||
'render': 'render',
|
||||
'nukescript': 'comp',
|
||||
'review': 'mov'}
|
||||
|
||||
def process(self, instance):
|
||||
self.log.debug('instance {}'.format(instance))
|
||||
|
||||
if instance.data.get('version'):
|
||||
version_number = int(instance.data.get('version'))
|
||||
|
||||
family = instance.data['family'].lower()
|
||||
|
||||
asset_type = ''
|
||||
asset_type = self.family_mapping[family]
|
||||
|
||||
componentList = []
|
||||
ft_session = instance.context.data["ftrackSession"]
|
||||
|
||||
components = instance.data['representations']
|
||||
|
||||
for comp in components:
|
||||
self.log.debug('component {}'.format(comp))
|
||||
# filename, ext = os.path.splitext(file)
|
||||
# self.log.debug('dest ext: ' + ext)
|
||||
|
||||
# ext = comp['Context']
|
||||
|
||||
if comp['thumbnail']:
|
||||
location = ft_session.query(
|
||||
'Location where name is "ftrack.server"').one()
|
||||
component_data = {
|
||||
"name": "thumbnail" # Default component name is "main".
|
||||
}
|
||||
elif comp['preview']:
|
||||
if not instance.data.get('startFrameReview'):
|
||||
instance.data['startFrameReview'] = instance.data['startFrame']
|
||||
if not instance.data.get('endFrameReview'):
|
||||
instance.data['endFrameReview'] = instance.data['endFrame']
|
||||
location = ft_session.query(
|
||||
'Location where name is "ftrack.server"').one()
|
||||
component_data = {
|
||||
# Default component name is "main".
|
||||
"name": "ftrackreview-mp4",
|
||||
"metadata": {'ftr_meta': json.dumps({
|
||||
'frameIn': int(instance.data['startFrameReview']),
|
||||
'frameOut': int(instance.data['endFrameReview']),
|
||||
'frameRate': 25.0})}
|
||||
}
|
||||
else:
|
||||
component_data = {
|
||||
"name": comp['representation'] # Default component name is "main".
|
||||
}
|
||||
location = ft_session.query(
|
||||
'Location where name is "ftrack.unmanaged"').one()
|
||||
|
||||
self.log.debug('location {}'.format(location))
|
||||
|
||||
componentList.append({"assettype_data": {
|
||||
"short": asset_type,
|
||||
},
|
||||
"asset_data": {
|
||||
"name": instance.data["subset"],
|
||||
},
|
||||
"assetversion_data": {
|
||||
"version": version_number,
|
||||
},
|
||||
"component_data": component_data,
|
||||
"component_path": comp['published_path'],
|
||||
'component_location': location,
|
||||
"component_overwrite": False,
|
||||
"thumbnail": comp['thumbnail']
|
||||
}
|
||||
)
|
||||
|
||||
self.log.debug('componentsList: {}'.format(str(componentList)))
|
||||
instance.data["ftrackComponentsList"] = componentList
|
||||
|
|
@ -0,0 +1,436 @@
|
|||
import os
|
||||
import logging
|
||||
import shutil
|
||||
import clique
|
||||
|
||||
import errno
|
||||
import pyblish.api
|
||||
from avalon import api, io
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IntegrateFrames(pyblish.api.InstancePlugin):
|
||||
"""Resolve any dependency issies
|
||||
|
||||
This plug-in resolves any paths which, if not updated might break
|
||||
the published file.
|
||||
|
||||
The order of families is important, when working with lookdev you want to
|
||||
first publish the texture, update the texture paths in the nodes and then
|
||||
publish the shading network. Same goes for file dependent assets.
|
||||
"""
|
||||
|
||||
label = "Integrate Frames"
|
||||
order = pyblish.api.IntegratorOrder
|
||||
families = [
|
||||
"imagesequence",
|
||||
"render",
|
||||
"write",
|
||||
"source",
|
||||
'review']
|
||||
|
||||
family_targets = [".frames", ".local", ".review", "review", "imagesequence", "render", "source"]
|
||||
exclude_families = ["clip"]
|
||||
|
||||
def process(self, instance):
|
||||
if [ef for ef in self.exclude_families
|
||||
if instance.data["family"] in ef]:
|
||||
return
|
||||
|
||||
families = [f for f in instance.data["families"]
|
||||
for search in self.family_targets
|
||||
if search in f]
|
||||
|
||||
if not families:
|
||||
return
|
||||
|
||||
self.register(instance)
|
||||
|
||||
# self.log.info("Integrating Asset in to the database ...")
|
||||
# self.log.info("instance.data: {}".format(instance.data))
|
||||
if instance.data.get('transfer', True):
|
||||
self.integrate(instance)
|
||||
|
||||
def register(self, instance):
|
||||
|
||||
# Required environment variables
|
||||
PROJECT = api.Session["AVALON_PROJECT"]
|
||||
ASSET = instance.data.get("asset") or api.Session["AVALON_ASSET"]
|
||||
LOCATION = api.Session["AVALON_LOCATION"]
|
||||
|
||||
context = instance.context
|
||||
# Atomicity
|
||||
#
|
||||
# Guarantee atomic publishes - each asset contains
|
||||
# an identical set of members.
|
||||
# __
|
||||
# / o
|
||||
# / \
|
||||
# | o |
|
||||
# \ /
|
||||
# o __/
|
||||
#
|
||||
assert all(result["success"] for result in context.data["results"]), (
|
||||
"Atomicity not held, aborting.")
|
||||
|
||||
# Assemble
|
||||
#
|
||||
# |
|
||||
# v
|
||||
# ---> <----
|
||||
# ^
|
||||
# |
|
||||
#
|
||||
# stagingdir = instance.data.get("stagingDir")
|
||||
# assert stagingdir, ("Incomplete instance \"%s\": "
|
||||
# "Missing reference to staging area." % instance)
|
||||
|
||||
# extra check if stagingDir actually exists and is available
|
||||
|
||||
# self.log.debug("Establishing staging directory @ %s" % stagingdir)
|
||||
|
||||
project = io.find_one({"type": "project"})
|
||||
|
||||
asset = io.find_one({"type": "asset",
|
||||
"name": ASSET,
|
||||
"parent": project["_id"]})
|
||||
|
||||
assert all([project, asset]), ("Could not find current project or "
|
||||
"asset '%s'" % ASSET)
|
||||
|
||||
subset = self.get_subset(asset, instance)
|
||||
|
||||
# get next version
|
||||
latest_version = io.find_one({"type": "version",
|
||||
"parent": subset["_id"]},
|
||||
{"name": True},
|
||||
sort=[("name", -1)])
|
||||
|
||||
next_version = 1
|
||||
if latest_version is not None:
|
||||
next_version += latest_version["name"]
|
||||
|
||||
self.log.info("Verifying version from assumed destination")
|
||||
|
||||
# assumed_data = instance.data["assumedTemplateData"]
|
||||
# assumed_version = assumed_data["version"]
|
||||
# if assumed_version != next_version:
|
||||
# raise AttributeError("Assumed version 'v{0:03d}' does not match"
|
||||
# "next version in database "
|
||||
# "('v{1:03d}')".format(assumed_version,
|
||||
# next_version))
|
||||
|
||||
if instance.data.get('version'):
|
||||
next_version = int(instance.data.get('version'))
|
||||
|
||||
instance.data['version'] = next_version
|
||||
|
||||
self.log.debug("Next version: v{0:03d}".format(next_version))
|
||||
|
||||
version_data = self.create_version_data(context, instance)
|
||||
version = self.create_version(subset=subset,
|
||||
version_number=next_version,
|
||||
locations=[LOCATION],
|
||||
data=version_data)
|
||||
|
||||
self.log.debug("Creating version ...")
|
||||
version_id = io.insert_one(version).inserted_id
|
||||
|
||||
# Write to disk
|
||||
# _
|
||||
# | |
|
||||
# _| |_
|
||||
# ____\ /
|
||||
# |\ \ / \
|
||||
# \ \ v \
|
||||
# \ \________.
|
||||
# \|________|
|
||||
#
|
||||
root = api.registered_root()
|
||||
hierarchy = ""
|
||||
parents = io.find_one({"type": 'asset', "name": ASSET})[
|
||||
'data']['parents']
|
||||
if parents and len(parents) > 0:
|
||||
# hierarchy = os.path.sep.join(hierarchy)
|
||||
hierarchy = os.path.join(*parents)
|
||||
|
||||
template_data = {"root": root,
|
||||
"project": {"name": PROJECT,
|
||||
"code": project['data']['code']},
|
||||
"silo": asset['silo'],
|
||||
"task": api.Session["AVALON_TASK"],
|
||||
"asset": ASSET,
|
||||
"family": instance.data['family'],
|
||||
"subset": subset["name"],
|
||||
"version": int(version["name"]),
|
||||
"hierarchy": hierarchy}
|
||||
|
||||
# template_publish = project["config"]["template"]["publish"]
|
||||
anatomy = instance.context.data['anatomy']
|
||||
|
||||
# Find the representations to transfer amongst the files
|
||||
# Each should be a single representation (as such, a single extension)
|
||||
representations = []
|
||||
destination_list = []
|
||||
|
||||
if 'transfers' not in instance.data:
|
||||
instance.data['transfers'] = []
|
||||
|
||||
# for repre in instance.data["representations"]:
|
||||
for idx, repre in enumerate(instance.data["representations"]):
|
||||
# Collection
|
||||
# _______
|
||||
# |______|\
|
||||
# | |\|
|
||||
# | ||
|
||||
# | ||
|
||||
# | ||
|
||||
# |_______|
|
||||
#
|
||||
|
||||
files = repre['files']
|
||||
|
||||
if len(files) > 1:
|
||||
|
||||
src_collections, remainder = clique.assemble(files)
|
||||
self.log.debug("dst_collections: {}".format(str(src_collections)))
|
||||
src_collection = src_collections[0]
|
||||
# Assert that each member has identical suffix
|
||||
src_head = src_collection.format("{head}")
|
||||
src_tail = ext = src_collection.format("{tail}")
|
||||
|
||||
test_dest_files = list()
|
||||
for i in [1, 2]:
|
||||
template_data["representation"] = repre['representation']
|
||||
template_data["frame"] = src_collection.format(
|
||||
"{padding}") % i
|
||||
anatomy_filled = anatomy.format(template_data)
|
||||
test_dest_files.append(anatomy_filled["render"]["path"])
|
||||
|
||||
dst_collections, remainder = clique.assemble(test_dest_files)
|
||||
dst_collection = dst_collections[0]
|
||||
dst_head = dst_collection.format("{head}")
|
||||
dst_tail = dst_collection.format("{tail}")
|
||||
|
||||
instance.data["representations"][idx]['published_path'] = dst_collection.format()
|
||||
|
||||
for i in src_collection.indexes:
|
||||
src_padding = src_collection.format("{padding}") % i
|
||||
src_file_name = "{0}{1}{2}".format(
|
||||
src_head, src_padding, src_tail)
|
||||
dst_padding = dst_collection.format("{padding}") % i
|
||||
dst = "{0}{1}{2}".format(dst_head, dst_padding, dst_tail)
|
||||
|
||||
# src = os.path.join(stagingdir, src_file_name)
|
||||
src = src_file_name
|
||||
self.log.debug("source: {}".format(src))
|
||||
|
||||
instance.data["transfers"].append([src, dst])
|
||||
|
||||
else:
|
||||
# Single file
|
||||
# _______
|
||||
# | |\
|
||||
# | |
|
||||
# | |
|
||||
# | |
|
||||
# |_______|
|
||||
#
|
||||
|
||||
template_data.pop("frame", None)
|
||||
|
||||
fname = files[0]
|
||||
|
||||
self.log.info("fname: {}".format(fname))
|
||||
|
||||
# assert not os.path.isabs(fname), (
|
||||
# "Given file name is a full path"
|
||||
# )
|
||||
# _, ext = os.path.splitext(fname)
|
||||
|
||||
template_data["representation"] = repre['representation']
|
||||
|
||||
# src = os.path.join(stagingdir, fname)
|
||||
src = src_file_name
|
||||
|
||||
anatomy_filled = anatomy.format(template_data)
|
||||
dst = anatomy_filled["render"]["path"]
|
||||
|
||||
instance.data["transfers"].append([src, dst])
|
||||
instance.data["representations"][idx]['published_path'] = dst
|
||||
|
||||
if repre['ext'] not in ["jpeg", "jpg", "mov", "mp4", "wav"]:
|
||||
template_data["frame"] = "#" * int(anatomy_filled["render"]["padding"])
|
||||
|
||||
anatomy_filled = anatomy.format(template_data)
|
||||
path_to_save = anatomy_filled["render"]["path"]
|
||||
template = anatomy.templates["render"]["path"]
|
||||
|
||||
self.log.debug("path_to_save: {}".format(path_to_save))
|
||||
|
||||
representation = {
|
||||
"schema": "pype:representation-2.0",
|
||||
"type": "representation",
|
||||
"parent": version_id,
|
||||
"name": repre['representation'],
|
||||
"data": {'path': path_to_save, 'template': template},
|
||||
"dependencies": instance.data.get("dependencies", "").split(),
|
||||
|
||||
# Imprint shortcut to context
|
||||
# for performance reasons.
|
||||
"context": {
|
||||
"root": root,
|
||||
"project": {
|
||||
"name": PROJECT,
|
||||
"code": project['data']['code']
|
||||
},
|
||||
"task": api.Session["AVALON_TASK"],
|
||||
"silo": asset['silo'],
|
||||
"asset": ASSET,
|
||||
"family": instance.data['family'],
|
||||
"subset": subset["name"],
|
||||
"version": int(version["name"]),
|
||||
"hierarchy": hierarchy,
|
||||
"representation": repre['representation']
|
||||
}
|
||||
}
|
||||
|
||||
destination_list.append(dst)
|
||||
instance.data['destination_list'] = destination_list
|
||||
representations.append(representation)
|
||||
|
||||
self.log.info("Registering {} items".format(len(representations)))
|
||||
io.insert_many(representations)
|
||||
|
||||
def integrate(self, instance):
|
||||
"""Move the files
|
||||
|
||||
Through `instance.data["transfers"]`
|
||||
|
||||
Args:
|
||||
instance: the instance to integrate
|
||||
"""
|
||||
|
||||
transfers = instance.data["transfers"]
|
||||
|
||||
for src, dest in transfers:
|
||||
src = os.path.normpath(src)
|
||||
dest = os.path.normpath(dest)
|
||||
if src in dest:
|
||||
continue
|
||||
|
||||
self.log.info("Copying file .. {} -> {}".format(src, dest))
|
||||
self.copy_file(src, dest)
|
||||
|
||||
def copy_file(self, src, dst):
|
||||
""" Copy given source to destination
|
||||
|
||||
Arguments:
|
||||
src (str): the source file which needs to be copied
|
||||
dst (str): the destination of the sourc file
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
dirname = os.path.dirname(dst)
|
||||
try:
|
||||
os.makedirs(dirname)
|
||||
except OSError as e:
|
||||
if e.errno == errno.EEXIST:
|
||||
pass
|
||||
else:
|
||||
self.log.critical("An unexpected error occurred.")
|
||||
raise
|
||||
|
||||
shutil.copy(src, dst)
|
||||
|
||||
def get_subset(self, asset, instance):
|
||||
|
||||
subset = io.find_one({"type": "subset",
|
||||
"parent": asset["_id"],
|
||||
"name": instance.data["subset"]})
|
||||
|
||||
if subset is None:
|
||||
subset_name = instance.data["subset"]
|
||||
self.log.info("Subset '%s' not found, creating.." % subset_name)
|
||||
|
||||
_id = io.insert_one({
|
||||
"schema": "pype:subset-2.0",
|
||||
"type": "subset",
|
||||
"name": subset_name,
|
||||
"data": {},
|
||||
"parent": asset["_id"]
|
||||
}).inserted_id
|
||||
|
||||
subset = io.find_one({"_id": _id})
|
||||
|
||||
return subset
|
||||
|
||||
def create_version(self, subset, version_number, locations, data=None):
|
||||
""" Copy given source to destination
|
||||
|
||||
Args:
|
||||
subset (dict): the registered subset of the asset
|
||||
version_number (int): the version number
|
||||
locations (list): the currently registered locations
|
||||
|
||||
Returns:
|
||||
dict: collection of data to create a version
|
||||
"""
|
||||
# Imprint currently registered location
|
||||
version_locations = [location for location in locations if
|
||||
location is not None]
|
||||
|
||||
return {"schema": "pype:version-2.0",
|
||||
"type": "version",
|
||||
"parent": subset["_id"],
|
||||
"name": version_number,
|
||||
"locations": version_locations,
|
||||
"data": data}
|
||||
|
||||
def create_version_data(self, context, instance):
|
||||
"""Create the data collection for the version
|
||||
|
||||
Args:
|
||||
context: the current context
|
||||
instance: the current instance being published
|
||||
|
||||
Returns:
|
||||
dict: the required information with instance.data as key
|
||||
"""
|
||||
|
||||
families = []
|
||||
current_families = instance.data.get("families", list())
|
||||
instance_family = instance.data.get("family", None)
|
||||
|
||||
if instance_family is not None:
|
||||
families.append(instance_family)
|
||||
families += current_families
|
||||
|
||||
# try:
|
||||
# source = instance.data['source']
|
||||
# except KeyError:
|
||||
# source = context.data["currentFile"]
|
||||
#
|
||||
# relative_path = os.path.relpath(source, api.registered_root())
|
||||
# source = os.path.join("{root}", relative_path).replace("\\", "/")
|
||||
|
||||
source = "standalone"
|
||||
|
||||
version_data = {"families": families,
|
||||
"time": context.data["time"],
|
||||
"author": context.data["user"],
|
||||
"source": source,
|
||||
"comment": context.data.get("comment")}
|
||||
|
||||
# Include optional data if present in
|
||||
optionals = ["startFrame", "endFrame", "step",
|
||||
"handles", "colorspace", "fps", "outputDir"]
|
||||
|
||||
for key in optionals:
|
||||
if key in instance.data:
|
||||
version_data[key] = instance.data.get(key, None)
|
||||
|
||||
return version_data
|
||||
12
pype/standalonepublish/__init__.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
from .standalonepublish_module import StandAlonePublishModule
|
||||
from .app import (
|
||||
show,
|
||||
cli
|
||||
)
|
||||
__all__ = [
|
||||
"show",
|
||||
"cli"
|
||||
]
|
||||
|
||||
def tray_init(tray_widget, main_widget):
|
||||
return StandAlonePublishModule(main_widget, tray_widget)
|
||||
5
pype/standalonepublish/__main__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from . import cli
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
sys.exit(cli(sys.argv[1:]))
|
||||
241
pype/standalonepublish/app.py
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
import os
|
||||
import sys
|
||||
import json
|
||||
from subprocess import Popen
|
||||
from pype import lib as pypelib
|
||||
from avalon.vendor.Qt import QtWidgets, QtCore
|
||||
from avalon import api, style, schema
|
||||
from avalon.tools import lib as parentlib
|
||||
from .widgets import *
|
||||
# Move this to pype lib?
|
||||
from avalon.tools.libraryloader.io_nonsingleton import DbConnector
|
||||
|
||||
module = sys.modules[__name__]
|
||||
module.window = None
|
||||
|
||||
class Window(QtWidgets.QDialog):
|
||||
"""Main window of Standalone publisher.
|
||||
|
||||
:param parent: Main widget that cares about all GUIs
|
||||
:type parent: QtWidgets.QMainWindow
|
||||
"""
|
||||
_db = DbConnector()
|
||||
_jobs = {}
|
||||
valid_family = False
|
||||
valid_components = False
|
||||
initialized = False
|
||||
WIDTH = 1100
|
||||
HEIGHT = 500
|
||||
NOT_SELECTED = '< Nothing is selected >'
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(Window, self).__init__(parent=parent)
|
||||
self._db.install()
|
||||
|
||||
self.setWindowTitle("Standalone Publish")
|
||||
self.setFocusPolicy(QtCore.Qt.StrongFocus)
|
||||
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
|
||||
self.setStyleSheet(style.load_stylesheet())
|
||||
|
||||
# Validators
|
||||
self.valid_parent = False
|
||||
|
||||
# statusbar - added under asset_widget
|
||||
label_message = QtWidgets.QLabel()
|
||||
label_message.setFixedHeight(20)
|
||||
|
||||
# assets widget
|
||||
widget_assets_wrap = QtWidgets.QWidget()
|
||||
widget_assets_wrap.setContentsMargins(0, 0, 0, 0)
|
||||
widget_assets = AssetWidget(self)
|
||||
|
||||
layout_assets = QtWidgets.QVBoxLayout(widget_assets_wrap)
|
||||
layout_assets.addWidget(widget_assets)
|
||||
layout_assets.addWidget(label_message)
|
||||
|
||||
# family widget
|
||||
widget_family = FamilyWidget(self)
|
||||
|
||||
# components widget
|
||||
widget_components = ComponentsWidget(self)
|
||||
|
||||
# Body
|
||||
body = QtWidgets.QSplitter()
|
||||
body.setContentsMargins(0, 0, 0, 0)
|
||||
body.setSizePolicy(
|
||||
QtWidgets.QSizePolicy.Expanding,
|
||||
QtWidgets.QSizePolicy.Expanding
|
||||
)
|
||||
body.setOrientation(QtCore.Qt.Horizontal)
|
||||
body.addWidget(widget_assets_wrap)
|
||||
body.addWidget(widget_family)
|
||||
body.addWidget(widget_components)
|
||||
body.setStretchFactor(body.indexOf(widget_assets_wrap), 2)
|
||||
body.setStretchFactor(body.indexOf(widget_family), 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)
|
||||
|
||||
self.label_message = label_message
|
||||
self.widget_assets = widget_assets
|
||||
self.widget_family = widget_family
|
||||
self.widget_components = widget_components
|
||||
|
||||
self.echo("Connected to Database")
|
||||
|
||||
# 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 echo(self, message):
|
||||
''' Shows message in label that disappear in 5s
|
||||
:param message: Message that will be displayed
|
||||
:type message: str
|
||||
'''
|
||||
self.label_message.setText(str(message))
|
||||
def clear_text():
|
||||
''' Helps prevent crash if this Window object
|
||||
is deleted before 5s passed
|
||||
'''
|
||||
try:
|
||||
self.label_message.set_text("")
|
||||
except:
|
||||
pass
|
||||
QtCore.QTimer.singleShot(5000, lambda: clear_text())
|
||||
|
||||
def on_asset_changed(self):
|
||||
'''Callback on asset selection changed
|
||||
|
||||
Updates the task view.
|
||||
|
||||
'''
|
||||
selected = self.widget_assets.get_selected_assets()
|
||||
if len(selected) == 1:
|
||||
self.valid_parent = True
|
||||
asset = self.db.find_one({"_id": selected[0], "type": "asset"})
|
||||
self.widget_family.change_asset(asset['name'])
|
||||
else:
|
||||
self.valid_parent = False
|
||||
self.widget_family.change_asset(self.NOT_SELECTED)
|
||||
self.widget_family.on_data_changed()
|
||||
|
||||
def 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)
|
||||
|
||||
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
|
||||
|
||||
def show(parent=None, debug=False):
|
||||
try:
|
||||
module.window.close()
|
||||
del module.window
|
||||
except (RuntimeError, AttributeError):
|
||||
pass
|
||||
|
||||
with parentlib.application():
|
||||
window = Window(parent)
|
||||
window.show()
|
||||
|
||||
module.window = window
|
||||
|
||||
|
||||
def cli(args):
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("project")
|
||||
parser.add_argument("asset")
|
||||
|
||||
args = parser.parse_args(args)
|
||||
# project = args.project
|
||||
# asset = args.asset
|
||||
|
||||
show()
|
||||
154
pype/standalonepublish/publish.py
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
import os
|
||||
import sys
|
||||
import json
|
||||
import tempfile
|
||||
import random
|
||||
import string
|
||||
|
||||
from avalon import io
|
||||
from avalon import api as avalon
|
||||
from avalon.tools import publish as av_publish
|
||||
|
||||
import pype
|
||||
from pypeapp import execute
|
||||
|
||||
import pyblish.api
|
||||
|
||||
|
||||
# Registers Global pyblish plugins
|
||||
# pype.install()
|
||||
# Registers Standalone pyblish plugins
|
||||
PUBLISH_PATH = os.path.sep.join(
|
||||
[pype.PLUGINS_DIR, 'standalonepublish', 'publish']
|
||||
)
|
||||
pyblish.api.register_plugin_path(PUBLISH_PATH)
|
||||
|
||||
# # Registers Standalone pyblish plugins
|
||||
# PUBLISH_PATH = os.path.sep.join(
|
||||
# [pype.PLUGINS_DIR, 'ftrack', 'publish']
|
||||
# )
|
||||
# pyblish.api.register_plugin_path(PUBLISH_PATH)
|
||||
|
||||
|
||||
def set_context(project, asset, app):
|
||||
''' Sets context for pyblish (must be done before pyblish is launched)
|
||||
: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
|
||||
|
||||
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"] = app
|
||||
io.Session["AVALON_APP"] = app
|
||||
|
||||
io.uninstall()
|
||||
|
||||
|
||||
def publish(data, gui=True):
|
||||
# cli pyblish seems like better solution
|
||||
return cli_publish(data, gui)
|
||||
# # this uses avalon pyblish launch tool
|
||||
# avalon_api_publish(data, gui)
|
||||
|
||||
|
||||
def avalon_api_publish(data, gui=True):
|
||||
''' Launches Pyblish (GUI by default)
|
||||
:param data: Should include data for pyblish and standalone collector
|
||||
:type data: dict
|
||||
:param gui: Pyblish will be launched in GUI mode if set to True
|
||||
:type gui: bool
|
||||
'''
|
||||
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)
|
||||
|
||||
args = [
|
||||
"-pp", os.pathsep.join(pyblish.api.registered_paths())
|
||||
]
|
||||
|
||||
os.environ["PYBLISH_HOSTS"] = "shell"
|
||||
os.environ["SAPUBLISH_INPATH"] = json_data_path
|
||||
|
||||
if gui:
|
||||
av_publish.show()
|
||||
else:
|
||||
returncode = execute([
|
||||
sys.executable, "-u", "-m", "pyblish"
|
||||
] + args, env=os.environ)
|
||||
|
||||
io.uninstall()
|
||||
|
||||
|
||||
def cli_publish(data, gui=True):
|
||||
io.install()
|
||||
|
||||
# Create hash name folder in temp
|
||||
chars = "".join( [random.choice(string.ascii_letters) for i in range(15)] )
|
||||
staging_dir = tempfile.mkdtemp(chars)
|
||||
|
||||
# create json for return data
|
||||
return_data_path = (
|
||||
staging_dir + os.path.basename(staging_dir) + 'return.json'
|
||||
)
|
||||
# create also json and fill with data
|
||||
json_data_path = staging_dir + os.path.basename(staging_dir) + '.json'
|
||||
with open(json_data_path, 'w') as outfile:
|
||||
json.dump(data, outfile)
|
||||
|
||||
args = [
|
||||
"-pp", os.pathsep.join(pyblish.api.registered_paths())
|
||||
]
|
||||
|
||||
if gui:
|
||||
args += ["gui"]
|
||||
|
||||
os.environ["PYBLISH_HOSTS"] = "shell"
|
||||
os.environ["SAPUBLISH_INPATH"] = json_data_path
|
||||
os.environ["SAPUBLISH_OUTPATH"] = return_data_path
|
||||
|
||||
returncode = execute([
|
||||
sys.executable, "-u", "-m", "pyblish"
|
||||
] + args, env=os.environ)
|
||||
|
||||
result = {}
|
||||
if os.path.exists(json_data_path):
|
||||
with open(json_data_path, "r") as f:
|
||||
result = json.load(f)
|
||||
|
||||
io.uninstall()
|
||||
# TODO: check if was pyblish successful
|
||||
# if successful return True
|
||||
print('Check result here')
|
||||
return False
|
||||
14
pype/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/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/standalonepublish/resources/file.png
Normal file
|
After Width: | Height: | Size: 803 B |
BIN
pype/standalonepublish/resources/files.png
Normal file
|
After Width: | Height: | Size: 484 B |
BIN
pype/standalonepublish/resources/houdini.png
Normal file
|
After Width: | Height: | Size: 257 KiB |
BIN
pype/standalonepublish/resources/image_file.png
Normal file
|
After Width: | Height: | Size: 5 KiB |
BIN
pype/standalonepublish/resources/image_files.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
14
pype/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/standalonepublish/resources/maya.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
12
pype/standalonepublish/resources/menu.svg
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<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 56 56" style="enable-background:new 0 0 56 56;" xml:space="preserve">
|
||||
<g>
|
||||
<path d="M28,0C12.561,0,0,12.561,0,28s12.561,28,28,28s28-12.561,28-28S43.439,0,28,0z M28,54C13.663,54,2,42.336,2,28
|
||||
S13.663,2,28,2s26,11.664,26,26S42.337,54,28,54z"/>
|
||||
<path d="M40,16H16c-0.553,0-1,0.448-1,1s0.447,1,1,1h24c0.553,0,1-0.448,1-1S40.553,16,40,16z"/>
|
||||
<path d="M40,27H16c-0.553,0-1,0.448-1,1s0.447,1,1,1h24c0.553,0,1-0.448,1-1S40.553,27,40,27z"/>
|
||||
<path d="M40,38H16c-0.553,0-1,0.448-1,1s0.447,1,1,1h24c0.553,0,1-0.448,1-1S40.553,38,40,38z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 823 B |
BIN
pype/standalonepublish/resources/nuke.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
pype/standalonepublish/resources/premiere.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
19
pype/standalonepublish/resources/preview.svg
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<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 690 200" fill="#777777" style="enable-background:new 0 0 690 200;" xml:space="preserve">
|
||||
<g>
|
||||
<path d="
|
||||
M30,10
|
||||
h630 a20,20 0 0 1 20,20
|
||||
v140 a20,20 0 0 1 -20,20
|
||||
h-630 a20,20 0 0 1 -20,-20
|
||||
v-140 a20,20 0 0 1 20,-20 z
|
||||
|
||||
M17,37
|
||||
v126 a20,20 0 0 0 20,20
|
||||
h616 a20,20 0 0 0 20,-20
|
||||
v-126 a20,20 0 0 0 -20,-20
|
||||
h-616 a20,20 0 0 0 -20,20 z"/>
|
||||
<text style='font-family:Trebuchet MS;font-size:140px;' x="70" y="155" class="small">PREVIEW</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 661 B |
19
pype/standalonepublish/resources/thumbnail.svg
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<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 890 200" fill="#777777" style="enable-background:new 0 0 890 200;" xml:space="preserve">
|
||||
<g>
|
||||
<path d="
|
||||
M30,10
|
||||
h830 a20,20 0 0 1 20,20
|
||||
v140 a20,20 0 0 1 -20,20
|
||||
h-830 a20,20 0 0 1 -20,-20
|
||||
v-140 a20,20 0 0 1 20,-20 z
|
||||
|
||||
M17,37
|
||||
v126 a20,20 0 0 0 20,20
|
||||
h816 a20,20 0 0 0 20,-20
|
||||
v-126 a20,20 0 0 0 -20,-20
|
||||
h-816 a20,20 0 0 0 -20,20 z"/>
|
||||
<text style='font-family:Trebuchet MS;font-size:140px;' x="70" y="155" class="small">THUMBNAIL</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 663 B |
23
pype/standalonepublish/resources/trash.svg
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 16.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"
|
||||
width="482.428px" height="482.429px" viewBox="0 0 482.428 482.429" fill="#ffffff" style="enable-background:new 0 0 482.428 482.429;"
|
||||
xml:space="preserve">
|
||||
<g>
|
||||
<path d="M381.163,57.799h-75.094C302.323,25.316,274.686,0,241.214,0c-33.471,0-61.104,25.315-64.85,57.799h-75.098
|
||||
c-30.39,0-55.111,24.728-55.111,55.117v2.828c0,23.223,14.46,43.1,34.83,51.199v260.369c0,30.39,24.724,55.117,55.112,55.117
|
||||
h210.236c30.389,0,55.111-24.729,55.111-55.117V166.944c20.369-8.1,34.83-27.977,34.83-51.199v-2.828
|
||||
C436.274,82.527,411.551,57.799,381.163,57.799z M241.214,26.139c19.037,0,34.927,13.645,38.443,31.66h-76.879
|
||||
C206.293,39.783,222.184,26.139,241.214,26.139z M375.305,427.312c0,15.978-13,28.979-28.973,28.979H136.096
|
||||
c-15.973,0-28.973-13.002-28.973-28.979V170.861h268.182V427.312z M410.135,115.744c0,15.978-13,28.979-28.973,28.979H101.266
|
||||
c-15.973,0-28.973-13.001-28.973-28.979v-2.828c0-15.978,13-28.979,28.973-28.979h279.897c15.973,0,28.973,13.001,28.973,28.979
|
||||
V115.744z"/>
|
||||
<path d="M171.144,422.863c7.218,0,13.069-5.853,13.069-13.068V262.641c0-7.216-5.852-13.07-13.069-13.07
|
||||
c-7.217,0-13.069,5.854-13.069,13.07v147.154C158.074,417.012,163.926,422.863,171.144,422.863z"/>
|
||||
<path d="M241.214,422.863c7.218,0,13.07-5.853,13.07-13.068V262.641c0-7.216-5.854-13.07-13.07-13.07
|
||||
c-7.217,0-13.069,5.854-13.069,13.07v147.154C228.145,417.012,233.996,422.863,241.214,422.863z"/>
|
||||
<path d="M311.284,422.863c7.217,0,13.068-5.853,13.068-13.068V262.641c0-7.216-5.852-13.07-13.068-13.07
|
||||
c-7.219,0-13.07,5.854-13.07,13.07v147.154C298.213,417.012,304.067,422.863,311.284,422.863z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
BIN
pype/standalonepublish/resources/video_file.png
Normal file
|
After Width: | Height: | Size: 120 B |
18
pype/standalonepublish/standalonepublish_module.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
from .app import show
|
||||
from .widgets import QtWidgets
|
||||
|
||||
|
||||
class StandAlonePublishModule:
|
||||
def __init__(self, main_parent=None, parent=None):
|
||||
self.main_parent = main_parent
|
||||
self.parent_widget = parent
|
||||
|
||||
def tray_menu(self, parent_menu):
|
||||
self.run_action = QtWidgets.QAction(
|
||||
"Publish", parent_menu
|
||||
)
|
||||
self.run_action.triggered.connect(self.show)
|
||||
parent_menu.addAction(self.run_action)
|
||||
|
||||
def show(self):
|
||||
show(self.main_parent, False)
|
||||
34
pype/standalonepublish/widgets/__init__.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
from avalon.vendor.Qt import *
|
||||
from avalon.vendor import qtawesome as awesome
|
||||
from avalon import style
|
||||
|
||||
HelpRole = QtCore.Qt.UserRole + 2
|
||||
FamilyRole = QtCore.Qt.UserRole + 3
|
||||
ExistsRole = QtCore.Qt.UserRole + 4
|
||||
PluginRole = QtCore.Qt.UserRole + 5
|
||||
|
||||
from ..resources import get_resource
|
||||
from .button_from_svgs import SvgResizable, SvgButton
|
||||
|
||||
from .model_node import Node
|
||||
from .model_tree import TreeModel
|
||||
from .model_asset import AssetModel
|
||||
from .model_filter_proxy_exact_match import ExactMatchesFilterProxyModel
|
||||
from .model_filter_proxy_recursive_sort import RecursiveSortFilterProxyModel
|
||||
from .model_tasks_template import TasksTemplateModel
|
||||
from .model_tree_view_deselectable import DeselectableTreeView
|
||||
|
||||
from .widget_asset_view import AssetView
|
||||
from .widget_asset import AssetWidget
|
||||
from .widget_family_desc import FamilyDescriptionWidget
|
||||
from .widget_family import FamilyWidget
|
||||
|
||||
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
|
||||
113
pype/standalonepublish/widgets/button_from_svgs.py
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
from xml.dom import minidom
|
||||
|
||||
from . import QtGui, QtCore, QtWidgets
|
||||
from PyQt5 import QtSvg, QtXml
|
||||
|
||||
|
||||
class SvgResizable(QtSvg.QSvgWidget):
|
||||
clicked = QtCore.Signal()
|
||||
|
||||
def __init__(self, filepath, width=None, height=None, fill=None):
|
||||
super().__init__()
|
||||
self.xmldoc = minidom.parse(filepath)
|
||||
itemlist = self.xmldoc.getElementsByTagName('svg')
|
||||
for element in itemlist:
|
||||
if fill:
|
||||
element.setAttribute('fill', str(fill))
|
||||
# TODO auto scale if only one is set
|
||||
if width is not None and height is not None:
|
||||
self.setMaximumSize(width, height)
|
||||
self.setMinimumSize(width, height)
|
||||
xml_string = self.xmldoc.toxml()
|
||||
svg_bytes = bytearray(xml_string, encoding='utf-8')
|
||||
|
||||
self.load(svg_bytes)
|
||||
|
||||
def change_color(self, color):
|
||||
element = self.xmldoc.getElementsByTagName('svg')[0]
|
||||
element.setAttribute('fill', str(color))
|
||||
xml_string = self.xmldoc.toxml()
|
||||
svg_bytes = bytearray(xml_string, encoding='utf-8')
|
||||
self.load(svg_bytes)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
self.clicked.emit()
|
||||
|
||||
|
||||
class SvgButton(QtWidgets.QFrame):
|
||||
clicked = QtCore.Signal()
|
||||
def __init__(
|
||||
self, filepath, width=None, height=None, fills=[],
|
||||
parent=None, checkable=True
|
||||
):
|
||||
super().__init__(parent)
|
||||
self.checkable = checkable
|
||||
self.checked = False
|
||||
|
||||
xmldoc = minidom.parse(filepath)
|
||||
element = xmldoc.getElementsByTagName('svg')[0]
|
||||
c_actual = '#777777'
|
||||
if element.hasAttribute('fill'):
|
||||
c_actual = element.getAttribute('fill')
|
||||
self.store_fills(fills, c_actual)
|
||||
|
||||
self.installEventFilter(self)
|
||||
self.svg_widget = SvgResizable(filepath, width, height, self.c_normal)
|
||||
xmldoc = minidom.parse(filepath)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.setSpacing(0)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(self.svg_widget)
|
||||
|
||||
if width is not None and height is not None:
|
||||
self.setMaximumSize(width, height)
|
||||
self.setMinimumSize(width, height)
|
||||
|
||||
def store_fills(self, fills, actual):
|
||||
if len(fills) == 0:
|
||||
fills = [actual, actual, actual, actual]
|
||||
elif len(fills) == 1:
|
||||
fills = [fills[0], fills[0], fills[0], fills[0]]
|
||||
elif len(fills) == 2:
|
||||
fills = [fills[0], fills[1], fills[1], fills[1]]
|
||||
elif len(fills) == 3:
|
||||
fills = [fills[0], fills[1], fills[2], fills[2]]
|
||||
self.c_normal = fills[0]
|
||||
self.c_hover = fills[1]
|
||||
self.c_active = fills[2]
|
||||
self.c_active_hover = fills[3]
|
||||
|
||||
def eventFilter(self, object, event):
|
||||
if event.type() == QtCore.QEvent.Enter:
|
||||
self.hoverEnterEvent(event)
|
||||
return True
|
||||
elif event.type() == QtCore.QEvent.Leave:
|
||||
self.hoverLeaveEvent(event)
|
||||
return True
|
||||
elif event.type() == QtCore.QEvent.MouseButtonRelease:
|
||||
self.mousePressEvent(event)
|
||||
return False
|
||||
|
||||
def change_checked(self, hover=True):
|
||||
if self.checkable:
|
||||
self.checked = not self.checked
|
||||
if hover:
|
||||
self.hoverEnterEvent()
|
||||
else:
|
||||
self.hoverLeaveEvent()
|
||||
|
||||
def hoverEnterEvent(self, event=None):
|
||||
color = self.c_hover
|
||||
if self.checked:
|
||||
color = self.c_active_hover
|
||||
self.svg_widget.change_color(color)
|
||||
|
||||
def hoverLeaveEvent(self, event=None):
|
||||
color = self.c_normal
|
||||
if self.checked:
|
||||
color = self.c_active
|
||||
self.svg_widget.change_color(color)
|
||||
|
||||
def mousePressEvent(self, event=None):
|
||||
self.clicked.emit()
|
||||
158
pype/standalonepublish/widgets/model_asset.py
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
import logging
|
||||
from . import QtCore, QtGui
|
||||
from . import TreeModel, Node
|
||||
from . import style, awesome
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _iter_model_rows(model,
|
||||
column,
|
||||
include_root=False):
|
||||
"""Iterate over all row indices in a model"""
|
||||
indices = [QtCore.QModelIndex()] # start iteration at root
|
||||
|
||||
for index in indices:
|
||||
|
||||
# Add children to the iterations
|
||||
child_rows = model.rowCount(index)
|
||||
for child_row in range(child_rows):
|
||||
child_index = model.index(child_row, column, index)
|
||||
indices.append(child_index)
|
||||
|
||||
if not include_root and not index.isValid():
|
||||
continue
|
||||
|
||||
yield index
|
||||
|
||||
|
||||
class AssetModel(TreeModel):
|
||||
"""A model listing assets in the silo in the active project.
|
||||
|
||||
The assets are displayed in a treeview, they are visually parented by
|
||||
a `visualParent` field in the database containing an `_id` to a parent
|
||||
asset.
|
||||
|
||||
"""
|
||||
|
||||
COLUMNS = ["label"]
|
||||
Name = 0
|
||||
Deprecated = 2
|
||||
ObjectId = 3
|
||||
|
||||
DocumentRole = QtCore.Qt.UserRole + 2
|
||||
ObjectIdRole = QtCore.Qt.UserRole + 3
|
||||
|
||||
def __init__(self, parent):
|
||||
super(AssetModel, self).__init__(parent=parent)
|
||||
self.parent_widget = parent
|
||||
self.refresh()
|
||||
|
||||
@property
|
||||
def db(self):
|
||||
return self.parent_widget.db
|
||||
|
||||
def _add_hierarchy(self, parent=None):
|
||||
|
||||
# Find the assets under the parent
|
||||
find_data = {
|
||||
"type": "asset"
|
||||
}
|
||||
if parent is None:
|
||||
find_data['$or'] = [
|
||||
{'data.visualParent': {'$exists': False}},
|
||||
{'data.visualParent': None}
|
||||
]
|
||||
else:
|
||||
find_data["data.visualParent"] = parent['_id']
|
||||
|
||||
assets = self.db.find(find_data).sort('name', 1)
|
||||
for asset in assets:
|
||||
# get label from data, otherwise use name
|
||||
data = asset.get("data", {})
|
||||
label = data.get("label", asset['name'])
|
||||
tags = data.get("tags", [])
|
||||
|
||||
# store for the asset for optimization
|
||||
deprecated = "deprecated" in tags
|
||||
|
||||
node = Node({
|
||||
"_id": asset['_id'],
|
||||
"name": asset["name"],
|
||||
"label": label,
|
||||
"type": asset['type'],
|
||||
"tags": ", ".join(tags),
|
||||
"deprecated": deprecated,
|
||||
"_document": asset
|
||||
})
|
||||
self.add_child(node, parent=parent)
|
||||
|
||||
# Add asset's children recursively
|
||||
self._add_hierarchy(node)
|
||||
|
||||
def refresh(self):
|
||||
"""Refresh the data for the model."""
|
||||
|
||||
self.clear()
|
||||
if (
|
||||
self.db.active_project() is None or
|
||||
self.db.active_project() == ''
|
||||
):
|
||||
return
|
||||
self.beginResetModel()
|
||||
self._add_hierarchy(parent=None)
|
||||
self.endResetModel()
|
||||
|
||||
def flags(self, index):
|
||||
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
|
||||
|
||||
def data(self, index, role):
|
||||
|
||||
if not index.isValid():
|
||||
return
|
||||
|
||||
node = index.internalPointer()
|
||||
if role == QtCore.Qt.DecorationRole: # icon
|
||||
|
||||
column = index.column()
|
||||
if column == self.Name:
|
||||
|
||||
# Allow a custom icon and custom icon color to be defined
|
||||
data = node["_document"]["data"]
|
||||
icon = data.get("icon", None)
|
||||
color = data.get("color", style.colors.default)
|
||||
|
||||
if icon is None:
|
||||
# Use default icons if no custom one is specified.
|
||||
# If it has children show a full folder, otherwise
|
||||
# show an open folder
|
||||
has_children = self.rowCount(index) > 0
|
||||
icon = "folder" if has_children else "folder-o"
|
||||
|
||||
# Make the color darker when the asset is deprecated
|
||||
if node.get("deprecated", False):
|
||||
color = QtGui.QColor(color).darker(250)
|
||||
|
||||
try:
|
||||
key = "fa.{0}".format(icon) # font-awesome key
|
||||
icon = awesome.icon(key, color=color)
|
||||
return icon
|
||||
except Exception as exception:
|
||||
# Log an error message instead of erroring out completely
|
||||
# when the icon couldn't be created (e.g. invalid name)
|
||||
log.error(exception)
|
||||
|
||||
return
|
||||
|
||||
if role == QtCore.Qt.ForegroundRole: # font color
|
||||
if "deprecated" in node.get("tags", []):
|
||||
return QtGui.QColor(style.colors.light).darker(250)
|
||||
|
||||
if role == self.ObjectIdRole:
|
||||
return node.get("_id", None)
|
||||
|
||||
if role == self.DocumentRole:
|
||||
return node.get("_document", None)
|
||||
|
||||
return super(AssetModel, self).data(index, role)
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
from . import QtCore
|
||||
|
||||
|
||||
class ExactMatchesFilterProxyModel(QtCore.QSortFilterProxyModel):
|
||||
"""Filter model to where key column's value is in the filtered tags"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ExactMatchesFilterProxyModel, self).__init__(*args, **kwargs)
|
||||
self._filters = set()
|
||||
|
||||
def setFilters(self, filters):
|
||||
self._filters = set(filters)
|
||||
|
||||
def filterAcceptsRow(self, source_row, source_parent):
|
||||
|
||||
# No filter
|
||||
if not self._filters:
|
||||
return True
|
||||
|
||||
else:
|
||||
model = self.sourceModel()
|
||||
column = self.filterKeyColumn()
|
||||
idx = model.index(source_row, column, source_parent)
|
||||
data = model.data(idx, self.filterRole())
|
||||
if data in self._filters:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
from . import QtCore
|
||||
|
||||
|
||||
class RecursiveSortFilterProxyModel(QtCore.QSortFilterProxyModel):
|
||||
"""Filters to the regex if any of the children matches allow parent"""
|
||||
def filterAcceptsRow(self, row, parent):
|
||||
|
||||
regex = self.filterRegExp()
|
||||
if not regex.isEmpty():
|
||||
pattern = regex.pattern()
|
||||
model = self.sourceModel()
|
||||
source_index = model.index(row, self.filterKeyColumn(), parent)
|
||||
if source_index.isValid():
|
||||
|
||||
# Check current index itself
|
||||
key = model.data(source_index, self.filterRole())
|
||||
if re.search(pattern, key, re.IGNORECASE):
|
||||
return True
|
||||
|
||||
# Check children
|
||||
rows = model.rowCount(source_index)
|
||||
for i in range(rows):
|
||||
if self.filterAcceptsRow(i, source_index):
|
||||
return True
|
||||
|
||||
# Otherwise filter it
|
||||
return False
|
||||
|
||||
return super(RecursiveSortFilterProxyModel,
|
||||
self).filterAcceptsRow(row, parent)
|
||||
56
pype/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)
|
||||
65
pype/standalonepublish/widgets/model_tasks_template.py
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
from . import QtCore, TreeModel
|
||||
from . import Node
|
||||
from . import awesome, style
|
||||
|
||||
|
||||
class TasksTemplateModel(TreeModel):
|
||||
"""A model listing the tasks combined for a list of assets"""
|
||||
|
||||
COLUMNS = ["Tasks"]
|
||||
|
||||
def __init__(self):
|
||||
super(TasksTemplateModel, self).__init__()
|
||||
self.selectable = False
|
||||
self._icons = {
|
||||
"__default__": awesome.icon("fa.folder-o",
|
||||
color=style.colors.default)
|
||||
}
|
||||
|
||||
def set_tasks(self, tasks):
|
||||
"""Set assets to track by their database id
|
||||
|
||||
Arguments:
|
||||
asset_ids (list): List of asset ids.
|
||||
|
||||
"""
|
||||
|
||||
self.clear()
|
||||
|
||||
# let cleared task view if no tasks are available
|
||||
if len(tasks) == 0:
|
||||
return
|
||||
|
||||
self.beginResetModel()
|
||||
|
||||
icon = self._icons["__default__"]
|
||||
for task in tasks:
|
||||
node = Node({
|
||||
"Tasks": task,
|
||||
"icon": icon
|
||||
})
|
||||
|
||||
self.add_child(node)
|
||||
|
||||
self.endResetModel()
|
||||
|
||||
def flags(self, index):
|
||||
if self.selectable is False:
|
||||
return QtCore.Qt.ItemIsEnabled
|
||||
else:
|
||||
return (
|
||||
QtCore.Qt.ItemIsEnabled |
|
||||
QtCore.Qt.ItemIsSelectable
|
||||
)
|
||||
|
||||
def data(self, index, role):
|
||||
|
||||
if not index.isValid():
|
||||
return
|
||||
|
||||
# Add icon to the first column
|
||||
if role == QtCore.Qt.DecorationRole:
|
||||
if index.column() == 0:
|
||||
return index.internalPointer()['icon']
|
||||
|
||||
return super(TasksTemplateModel, self).data(index, role)
|
||||
122
pype/standalonepublish/widgets/model_tree.py
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
from . import QtCore
|
||||
from . import Node
|
||||
|
||||
|
||||
class TreeModel(QtCore.QAbstractItemModel):
|
||||
|
||||
COLUMNS = list()
|
||||
NodeRole = QtCore.Qt.UserRole + 1
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(TreeModel, self).__init__(parent)
|
||||
self._root_node = Node()
|
||||
|
||||
def rowCount(self, parent):
|
||||
if parent.isValid():
|
||||
node = parent.internalPointer()
|
||||
else:
|
||||
node = self._root_node
|
||||
|
||||
return node.childCount()
|
||||
|
||||
def columnCount(self, parent):
|
||||
return len(self.COLUMNS)
|
||||
|
||||
def data(self, index, role):
|
||||
|
||||
if not index.isValid():
|
||||
return None
|
||||
|
||||
if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
|
||||
|
||||
node = index.internalPointer()
|
||||
column = index.column()
|
||||
|
||||
key = self.COLUMNS[column]
|
||||
return node.get(key, None)
|
||||
|
||||
if role == self.NodeRole:
|
||||
return index.internalPointer()
|
||||
|
||||
def setData(self, index, value, role=QtCore.Qt.EditRole):
|
||||
"""Change the data on the nodes.
|
||||
|
||||
Returns:
|
||||
bool: Whether the edit was successful
|
||||
"""
|
||||
|
||||
if index.isValid():
|
||||
if role == QtCore.Qt.EditRole:
|
||||
|
||||
node = index.internalPointer()
|
||||
column = index.column()
|
||||
key = self.COLUMNS[column]
|
||||
node[key] = value
|
||||
|
||||
# passing `list()` for PyQt5 (see PYSIDE-462)
|
||||
self.dataChanged.emit(index, index, list())
|
||||
|
||||
# must return true if successful
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def setColumns(self, keys):
|
||||
assert isinstance(keys, (list, tuple))
|
||||
self.COLUMNS = keys
|
||||
|
||||
def headerData(self, section, orientation, role):
|
||||
|
||||
if role == QtCore.Qt.DisplayRole:
|
||||
if section < len(self.COLUMNS):
|
||||
return self.COLUMNS[section]
|
||||
|
||||
super(TreeModel, self).headerData(section, orientation, role)
|
||||
|
||||
def flags(self, index):
|
||||
return (
|
||||
QtCore.Qt.ItemIsEnabled |
|
||||
QtCore.Qt.ItemIsSelectable
|
||||
)
|
||||
|
||||
def parent(self, index):
|
||||
|
||||
node = index.internalPointer()
|
||||
parent_node = node.parent()
|
||||
|
||||
# If it has no parents we return invalid
|
||||
if parent_node == self._root_node or not parent_node:
|
||||
return QtCore.QModelIndex()
|
||||
|
||||
return self.createIndex(parent_node.row(), 0, parent_node)
|
||||
|
||||
def index(self, row, column, parent):
|
||||
"""Return index for row/column under parent"""
|
||||
|
||||
if not parent.isValid():
|
||||
parentNode = self._root_node
|
||||
else:
|
||||
parentNode = parent.internalPointer()
|
||||
|
||||
childItem = parentNode.child(row)
|
||||
if childItem:
|
||||
return self.createIndex(row, column, childItem)
|
||||
else:
|
||||
return QtCore.QModelIndex()
|
||||
|
||||
def add_child(self, node, parent=None):
|
||||
if parent is None:
|
||||
parent = self._root_node
|
||||
|
||||
parent.add_child(node)
|
||||
|
||||
def column_name(self, column):
|
||||
"""Return column key by index"""
|
||||
|
||||
if column < len(self.COLUMNS):
|
||||
return self.COLUMNS[column]
|
||||
|
||||
def clear(self):
|
||||
self.beginResetModel()
|
||||
self._root_node = Node()
|
||||
self.endResetModel()
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
from . import QtWidgets, QtCore
|
||||
|
||||
|
||||
class DeselectableTreeView(QtWidgets.QTreeView):
|
||||
"""A tree view that deselects on clicking on an empty area in the view"""
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
|
||||
index = self.indexAt(event.pos())
|
||||
if not index.isValid():
|
||||
# clear the selection
|
||||
self.clearSelection()
|
||||
# clear the current index
|
||||
self.setCurrentIndex(QtCore.QModelIndex())
|
||||
|
||||
QtWidgets.QTreeView.mousePressEvent(self, event)
|
||||
274
pype/standalonepublish/widgets/widget_asset.py
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
import contextlib
|
||||
from . import QtWidgets, QtCore
|
||||
from . import RecursiveSortFilterProxyModel, AssetModel, AssetView
|
||||
from . import awesome, style
|
||||
|
||||
@contextlib.contextmanager
|
||||
def preserve_expanded_rows(tree_view,
|
||||
column=0,
|
||||
role=QtCore.Qt.DisplayRole):
|
||||
"""Preserves expanded row in QTreeView by column's data role.
|
||||
|
||||
This function is created to maintain the expand vs collapse status of
|
||||
the model items. When refresh is triggered the items which are expanded
|
||||
will stay expanded and vise versa.
|
||||
|
||||
Arguments:
|
||||
tree_view (QWidgets.QTreeView): the tree view which is
|
||||
nested in the application
|
||||
column (int): the column to retrieve the data from
|
||||
role (int): the role which dictates what will be returned
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
|
||||
model = tree_view.model()
|
||||
|
||||
expanded = set()
|
||||
|
||||
for index in _iter_model_rows(model,
|
||||
column=column,
|
||||
include_root=False):
|
||||
if tree_view.isExpanded(index):
|
||||
value = index.data(role)
|
||||
expanded.add(value)
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
if not expanded:
|
||||
return
|
||||
|
||||
for index in _iter_model_rows(model,
|
||||
column=column,
|
||||
include_root=False):
|
||||
value = index.data(role)
|
||||
state = value in expanded
|
||||
if state:
|
||||
tree_view.expand(index)
|
||||
else:
|
||||
tree_view.collapse(index)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def preserve_selection(tree_view,
|
||||
column=0,
|
||||
role=QtCore.Qt.DisplayRole,
|
||||
current_index=True):
|
||||
"""Preserves row selection in QTreeView by column's data role.
|
||||
|
||||
This function is created to maintain the selection status of
|
||||
the model items. When refresh is triggered the items which are expanded
|
||||
will stay expanded and vise versa.
|
||||
|
||||
tree_view (QWidgets.QTreeView): the tree view nested in the application
|
||||
column (int): the column to retrieve the data from
|
||||
role (int): the role which dictates what will be returned
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
|
||||
model = tree_view.model()
|
||||
selection_model = tree_view.selectionModel()
|
||||
flags = selection_model.Select | selection_model.Rows
|
||||
|
||||
if current_index:
|
||||
current_index_value = tree_view.currentIndex().data(role)
|
||||
else:
|
||||
current_index_value = None
|
||||
|
||||
selected_rows = selection_model.selectedRows()
|
||||
if not selected_rows:
|
||||
yield
|
||||
return
|
||||
|
||||
selected = set(row.data(role) for row in selected_rows)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
if not selected:
|
||||
return
|
||||
|
||||
# Go through all indices, select the ones with similar data
|
||||
for index in _iter_model_rows(model,
|
||||
column=column,
|
||||
include_root=False):
|
||||
|
||||
value = index.data(role)
|
||||
state = value in selected
|
||||
if state:
|
||||
tree_view.scrollTo(index) # Ensure item is visible
|
||||
selection_model.select(index, flags)
|
||||
|
||||
if current_index_value and value == current_index_value:
|
||||
tree_view.setCurrentIndex(index)
|
||||
|
||||
|
||||
class AssetWidget(QtWidgets.QWidget):
|
||||
"""A Widget to display a tree of assets with filter
|
||||
|
||||
To list the assets of the active project:
|
||||
>>> # widget = AssetWidget()
|
||||
>>> # widget.refresh()
|
||||
>>> # widget.show()
|
||||
|
||||
"""
|
||||
|
||||
assets_refreshed = QtCore.Signal() # on model refresh
|
||||
selection_changed = QtCore.Signal() # on view selection change
|
||||
current_changed = QtCore.Signal() # on view current index change
|
||||
|
||||
def __init__(self, parent):
|
||||
super(AssetWidget, self).__init__(parent=parent)
|
||||
self.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.parent_widget = parent
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(4)
|
||||
|
||||
# Project
|
||||
self.combo_projects = QtWidgets.QComboBox()
|
||||
self._set_projects()
|
||||
self.combo_projects.currentTextChanged.connect(self.on_project_change)
|
||||
# Tree View
|
||||
model = AssetModel(self)
|
||||
proxy = RecursiveSortFilterProxyModel()
|
||||
proxy.setSourceModel(model)
|
||||
proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
||||
view = AssetView()
|
||||
view.setModel(proxy)
|
||||
|
||||
# Header
|
||||
header = QtWidgets.QHBoxLayout()
|
||||
|
||||
icon = awesome.icon("fa.refresh", color=style.colors.light)
|
||||
refresh = QtWidgets.QPushButton(icon, "")
|
||||
refresh.setToolTip("Refresh items")
|
||||
|
||||
filter = QtWidgets.QLineEdit()
|
||||
filter.textChanged.connect(proxy.setFilterFixedString)
|
||||
filter.setPlaceholderText("Filter assets..")
|
||||
|
||||
header.addWidget(filter)
|
||||
header.addWidget(refresh)
|
||||
|
||||
# Layout
|
||||
layout.addWidget(self.combo_projects)
|
||||
layout.addLayout(header)
|
||||
layout.addWidget(view)
|
||||
|
||||
# Signals/Slots
|
||||
selection = view.selectionModel()
|
||||
selection.selectionChanged.connect(self.selection_changed)
|
||||
selection.currentChanged.connect(self.current_changed)
|
||||
refresh.clicked.connect(self.refresh)
|
||||
|
||||
self.refreshButton = refresh
|
||||
self.model = model
|
||||
self.proxy = proxy
|
||||
self.view = view
|
||||
|
||||
@property
|
||||
def db(self):
|
||||
return self.parent_widget.db
|
||||
|
||||
def collect_data(self):
|
||||
project = self.db.find_one({'type': 'project'})
|
||||
asset = self.db.find_one({'_id': self.get_active_asset()})
|
||||
data = {
|
||||
'project': project['name'],
|
||||
'asset': asset['name'],
|
||||
'parents': self.get_parents(asset)
|
||||
}
|
||||
return data
|
||||
|
||||
def get_parents(self, entity):
|
||||
output = []
|
||||
if entity.get('data', {}).get('visualParent', None) is None:
|
||||
return output
|
||||
parent = self.db.find_one({'_id': entity['data']['visualParent']})
|
||||
output.append(parent['name'])
|
||||
output.extend(self.get_parents(parent))
|
||||
return output
|
||||
|
||||
def _set_projects(self):
|
||||
projects = list()
|
||||
for project in self.db.projects():
|
||||
projects.append(project['name'])
|
||||
|
||||
self.combo_projects.clear()
|
||||
if len(projects) > 0:
|
||||
self.combo_projects.addItems(projects)
|
||||
self.db.activate_project(projects[0])
|
||||
|
||||
def on_project_change(self):
|
||||
projects = list()
|
||||
for project in self.db.projects():
|
||||
projects.append(project['name'])
|
||||
project_name = self.combo_projects.currentText()
|
||||
if project_name in projects:
|
||||
self.db.activate_project(project_name)
|
||||
self.refresh()
|
||||
|
||||
def _refresh_model(self):
|
||||
self.model.refresh()
|
||||
self.assets_refreshed.emit()
|
||||
|
||||
def refresh(self):
|
||||
self._refresh_model()
|
||||
|
||||
def get_active_asset(self):
|
||||
"""Return the asset id the current asset."""
|
||||
current = self.view.currentIndex()
|
||||
return current.data(self.model.ObjectIdRole)
|
||||
|
||||
def get_active_index(self):
|
||||
return self.view.currentIndex()
|
||||
|
||||
def get_selected_assets(self):
|
||||
"""Return the assets' ids that are selected."""
|
||||
selection = self.view.selectionModel()
|
||||
rows = selection.selectedRows()
|
||||
return [row.data(self.model.ObjectIdRole) for row in rows]
|
||||
|
||||
def select_assets(self, assets, expand=True):
|
||||
"""Select assets by name.
|
||||
|
||||
Args:
|
||||
assets (list): List of asset names
|
||||
expand (bool): Whether to also expand to the asset in the view
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
# TODO: Instead of individual selection optimize for many assets
|
||||
|
||||
assert isinstance(assets,
|
||||
(tuple, list)), "Assets must be list or tuple"
|
||||
|
||||
# Clear selection
|
||||
selection_model = self.view.selectionModel()
|
||||
selection_model.clearSelection()
|
||||
|
||||
# Select
|
||||
mode = selection_model.Select | selection_model.Rows
|
||||
for index in _iter_model_rows(self.proxy,
|
||||
column=0,
|
||||
include_root=False):
|
||||
data = index.data(self.model.NodeRole)
|
||||
name = data['name']
|
||||
if name in assets:
|
||||
selection_model.select(index, mode)
|
||||
|
||||
if expand:
|
||||
self.view.expand(index)
|
||||
|
||||
# Set the currently active index
|
||||
self.view.setCurrentIndex(index)
|
||||
16
pype/standalonepublish/widgets/widget_asset_view.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
from . import QtCore
|
||||
from . import DeselectableTreeView
|
||||
|
||||
|
||||
class AssetView(DeselectableTreeView):
|
||||
"""Item view.
|
||||
|
||||
This implements a context menu.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super(AssetView, self).__init__()
|
||||
self.setIndentation(15)
|
||||
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
self.setHeaderHidden(True)
|
||||
294
pype/standalonepublish/widgets/widget_component_item.py
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
import os
|
||||
from . import QtCore, QtGui, QtWidgets
|
||||
from . import SvgButton
|
||||
from . import get_resource
|
||||
from avalon import style
|
||||
|
||||
|
||||
class ComponentItem(QtWidgets.QFrame):
|
||||
C_NORMAL = '#777777'
|
||||
C_HOVER = '#ffffff'
|
||||
C_ACTIVE = '#4BB543'
|
||||
C_ACTIVE_HOVER = '#4BF543'
|
||||
signal_remove = QtCore.Signal(object)
|
||||
signal_thumbnail = QtCore.Signal(object)
|
||||
signal_preview = QtCore.Signal(object)
|
||||
signal_repre_change = QtCore.Signal(object, object)
|
||||
|
||||
def __init__(self, parent, main_parent):
|
||||
super().__init__()
|
||||
self.has_valid_repre = True
|
||||
self.actions = []
|
||||
self.resize(290, 70)
|
||||
self.setMinimumSize(QtCore.QSize(0, 70))
|
||||
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 = SvgButton(
|
||||
get_resource('menu.svg'), 22, 22,
|
||||
[self.C_NORMAL, self.C_HOVER],
|
||||
frame_image_info, False
|
||||
)
|
||||
|
||||
self.action_menu = QtWidgets.QMenu()
|
||||
|
||||
expanding_sizePolicy = QtWidgets.QSizePolicy(
|
||||
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding
|
||||
)
|
||||
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 = SvgButton(
|
||||
get_resource('preview.svg'), 64, 18,
|
||||
[self.C_NORMAL, self.C_HOVER, self.C_ACTIVE, self.C_ACTIVE_HOVER],
|
||||
frame_icons
|
||||
)
|
||||
|
||||
self.thumbnail = SvgButton(
|
||||
get_resource('thumbnail.svg'), 84, 18,
|
||||
[self.C_NORMAL, self.C_HOVER, self.C_ACTIVE, self.C_ACTIVE_HOVER],
|
||||
frame_icons
|
||||
)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(frame_icons)
|
||||
layout.setSpacing(6)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(self.thumbnail)
|
||||
layout.addWidget(self.preview)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(frame_repre_icons)
|
||||
layout.setSpacing(0)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
layout.addWidget(frame_repre, alignment=QtCore.Qt.AlignLeft)
|
||||
layout.addWidget(frame_icons, alignment=QtCore.Qt.AlignRight)
|
||||
|
||||
frame_middle = QtWidgets.QFrame(frame)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(frame_middle)
|
||||
layout.setSpacing(0)
|
||||
layout.setContentsMargins(4, 0, 4, 0)
|
||||
layout.addWidget(frame_name_repre)
|
||||
layout.addWidget(frame_repre_icons)
|
||||
|
||||
layout.setStretchFactor(frame_name_repre, 1)
|
||||
layout.setStretchFactor(frame_repre_icons, 1)
|
||||
|
||||
layout_main.addWidget(frame_middle)
|
||||
|
||||
self.remove = SvgButton(
|
||||
get_resource('trash.svg'), 22, 22,
|
||||
[self.C_NORMAL, self.C_HOVER],
|
||||
frame, False
|
||||
)
|
||||
|
||||
layout_main.addWidget(self.remove)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.setSpacing(0)
|
||||
layout.setContentsMargins(2, 2, 2, 2)
|
||||
layout.addWidget(frame)
|
||||
|
||||
self.preview.setToolTip('Mark component as Preview')
|
||||
self.thumbnail.setToolTip('Component will be selected as thumbnail')
|
||||
|
||||
# self.frame.setStyleSheet("border: 1px solid black;")
|
||||
|
||||
def set_context(self, data):
|
||||
self.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.checked
|
||||
|
||||
def change_thumbnail(self, hover=True):
|
||||
self.thumbnail.change_checked(hover)
|
||||
|
||||
def is_preview(self):
|
||||
return self.preview.checked
|
||||
|
||||
def change_preview(self, hover=True):
|
||||
self.preview.change_checked(hover)
|
||||
|
||||
def collect_data(self):
|
||||
data = {
|
||||
'ext': self.in_data['ext'],
|
||||
'label': self.name.text(),
|
||||
'representation': self.input_repre.text(),
|
||||
'files': self.in_data['files'],
|
||||
'thumbnail': self.is_thumbnail(),
|
||||
'preview': self.is_preview()
|
||||
}
|
||||
return data
|
||||
128
pype/standalonepublish/widgets/widget_components.py
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
from . import QtWidgets, QtCore, QtGui
|
||||
from . import DropDataFrame
|
||||
|
||||
from .. import publish
|
||||
|
||||
|
||||
class ComponentsWidget(QtWidgets.QWidget):
|
||||
def __init__(self, parent):
|
||||
super().__init__()
|
||||
self.initialized = False
|
||||
self.valid_components = False
|
||||
self.valid_family = False
|
||||
self.valid_repre_names = False
|
||||
|
||||
body = QtWidgets.QWidget()
|
||||
self.parent_widget = parent
|
||||
self.drop_frame = DropDataFrame(self)
|
||||
|
||||
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):
|
||||
self.working_start('Pyblish is running')
|
||||
try:
|
||||
data = self.parent_widget.collect_data()
|
||||
publish.set_context(
|
||||
data['project'], data['asset'], 'standalonepublish'
|
||||
)
|
||||
result = publish.publish(data)
|
||||
# Clear widgets from components list if publishing was successful
|
||||
if result:
|
||||
self.drop_frame.components_list.clear_widgets()
|
||||
self.drop_frame._refresh_view()
|
||||
finally:
|
||||
self.working_stop()
|
||||
89
pype/standalonepublish/widgets/widget_components_list.py
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
from . import QtCore, QtGui, QtWidgets
|
||||
|
||||
|
||||
class ComponentsList(QtWidgets.QTableWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
|
||||
self._main_column = 0
|
||||
|
||||
self.setColumnCount(1)
|
||||
self.setSelectionBehavior(
|
||||
QtWidgets.QAbstractItemView.SelectRows
|
||||
)
|
||||
self.setSelectionMode(
|
||||
QtWidgets.QAbstractItemView.ExtendedSelection
|
||||
)
|
||||
self.setVerticalScrollMode(
|
||||
QtWidgets.QAbstractItemView.ScrollPerPixel
|
||||
)
|
||||
self.verticalHeader().hide()
|
||||
|
||||
try:
|
||||
self.verticalHeader().setResizeMode(
|
||||
QtWidgets.QHeaderView.ResizeToContents
|
||||
)
|
||||
except Exception:
|
||||
self.verticalHeader().setSectionResizeMode(
|
||||
QtWidgets.QHeaderView.ResizeToContents
|
||||
)
|
||||
|
||||
self.horizontalHeader().setStretchLastSection(True)
|
||||
self.horizontalHeader().hide()
|
||||
|
||||
def count(self):
|
||||
return self.rowCount()
|
||||
|
||||
def add_widget(self, widget, row=None):
|
||||
if row is None:
|
||||
row = self.count()
|
||||
|
||||
self.insertRow(row)
|
||||
self.setCellWidget(row, self._main_column, widget)
|
||||
|
||||
self.resizeRowToContents(row)
|
||||
|
||||
return row
|
||||
|
||||
def remove_widget(self, row):
|
||||
self.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)
|
||||
52
pype/standalonepublish/widgets/widget_drop_empty.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import os
|
||||
import logging
|
||||
import clique
|
||||
from . import QtWidgets, QtCore, QtGui
|
||||
|
||||
|
||||
class DropEmpty(QtWidgets.QWidget):
|
||||
|
||||
def __init__(self, parent):
|
||||
'''Initialise DataDropZone widget.'''
|
||||
super().__init__(parent)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
|
||||
BottomCenterAlignment = QtCore.Qt.AlignBottom | QtCore.Qt.AlignHCenter
|
||||
TopCenterAlignment = QtCore.Qt.AlignTop | QtCore.Qt.AlignHCenter
|
||||
|
||||
font = QtGui.QFont()
|
||||
font.setFamily("DejaVu Sans Condensed")
|
||||
font.setPointSize(26)
|
||||
font.setBold(True)
|
||||
font.setWeight(50)
|
||||
font.setKerning(True)
|
||||
|
||||
self._label = QtWidgets.QLabel('Drag & Drop')
|
||||
self._label.setFont(font)
|
||||
self._label.setStyleSheet(
|
||||
'background-color: rgb(255, 255, 255, 0);'
|
||||
)
|
||||
|
||||
font.setPointSize(12)
|
||||
self._sub_label = QtWidgets.QLabel('(drop files here)')
|
||||
self._sub_label.setFont(font)
|
||||
self._sub_label.setStyleSheet(
|
||||
'background-color: rgb(255, 255, 255, 0);'
|
||||
)
|
||||
|
||||
layout.addWidget(self._label, alignment=BottomCenterAlignment)
|
||||
layout.addWidget(self._sub_label, alignment=TopCenterAlignment)
|
||||
|
||||
def paintEvent(self, event):
|
||||
super().paintEvent(event)
|
||||
painter = QtGui.QPainter(self)
|
||||
pen = QtGui.QPen()
|
||||
pen.setWidth(1);
|
||||
pen.setBrush(QtCore.Qt.darkGray);
|
||||
pen.setStyle(QtCore.Qt.DashLine);
|
||||
painter.setPen(pen)
|
||||
painter.drawRect(
|
||||
10, 10,
|
||||
self.rect().width()-15, self.rect().height()-15
|
||||
)
|
||||
427
pype/standalonepublish/widgets/widget_drop_frame.py
Normal file
|
|
@ -0,0 +1,427 @@
|
|||
import os
|
||||
import re
|
||||
import clique
|
||||
import subprocess
|
||||
from pypeapp import config
|
||||
from . import QtWidgets, QtCore
|
||||
from . import DropEmpty, ComponentsList, ComponentItem
|
||||
|
||||
|
||||
class DropDataFrame(QtWidgets.QFrame):
|
||||
def __init__(self, parent):
|
||||
super().__init__()
|
||||
self.parent_widget = parent
|
||||
self.presets = config.get_presets()['standalone_publish']
|
||||
|
||||
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 = 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):
|
||||
checked_item = None
|
||||
for item in self.components_list.widgets():
|
||||
if item.is_thumbnail():
|
||||
checked_item = item
|
||||
break
|
||||
if checked_item is None or checked_item == in_item:
|
||||
in_item.change_thumbnail()
|
||||
else:
|
||||
checked_item.change_thumbnail(False)
|
||||
in_item.change_thumbnail()
|
||||
|
||||
def _set_preview(self, in_item):
|
||||
checked_item = None
|
||||
for item in self.components_list.widgets():
|
||||
if item.is_preview():
|
||||
checked_item = item
|
||||
break
|
||||
if checked_item is None or checked_item == in_item:
|
||||
in_item.change_preview()
|
||||
else:
|
||||
checked_item.change_preview(False)
|
||||
in_item.change_preview()
|
||||
|
||||
def _remove_item(self, 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)
|
||||
collections, remainders = clique.assemble(paths)
|
||||
for collection in collections:
|
||||
self._process_collection(collection)
|
||||
for remainder in remainders:
|
||||
self._process_remainder(remainder)
|
||||
self.parent_widget.working_stop()
|
||||
|
||||
def _get_all_paths(self, paths):
|
||||
output_paths = []
|
||||
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}')
|
||||
|
||||
actions = []
|
||||
|
||||
data = {
|
||||
'files': [file for file in collection],
|
||||
'name': file_base,
|
||||
'ext': file_ext,
|
||||
'file_info': range,
|
||||
'representation': repr_name,
|
||||
'folder_path': folder_path,
|
||||
'is_sequence': True,
|
||||
'actions': actions
|
||||
}
|
||||
self._process_data(data)
|
||||
|
||||
def _get_ranges(self, indexes):
|
||||
if len(indexes) == 1:
|
||||
return str(indexes[0])
|
||||
ranges = []
|
||||
first = None
|
||||
last = None
|
||||
for index in indexes:
|
||||
if first is None:
|
||||
first = index
|
||||
last = index
|
||||
elif (last+1) == index:
|
||||
last = index
|
||||
else:
|
||||
if first == last:
|
||||
range = str(first)
|
||||
else:
|
||||
range = '{}-{}'.format(first, last)
|
||||
ranges.append(range)
|
||||
first = index
|
||||
last = index
|
||||
if first == last:
|
||||
range = str(first)
|
||||
else:
|
||||
range = '{}-{}'.format(first, last)
|
||||
ranges.append(range)
|
||||
return ', '.join(ranges)
|
||||
|
||||
def _process_remainder(self, remainder):
|
||||
filename = os.path.basename(remainder)
|
||||
folder_path = os.path.dirname(remainder)
|
||||
file_base, file_ext = os.path.splitext(filename)
|
||||
repr_name = file_ext.replace('.', '')
|
||||
file_info = None
|
||||
|
||||
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
|
||||
}
|
||||
data['file_info'] = self.get_file_info(data)
|
||||
|
||||
self._process_data(data)
|
||||
|
||||
def get_file_info(self, data):
|
||||
output = None
|
||||
if data['ext'] == '.mov':
|
||||
try:
|
||||
# ffProbe must be in PATH
|
||||
filepath = data['files'][0]
|
||||
args = ['ffprobe', '-show_streams', filepath]
|
||||
p = subprocess.Popen(
|
||||
args,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
shell=True
|
||||
)
|
||||
datalines=[]
|
||||
for line in iter(p.stdout.readline, b''):
|
||||
line = line.decode("utf-8").replace('\r\n', '')
|
||||
datalines.append(line)
|
||||
|
||||
find_value = 'codec_name'
|
||||
for line in datalines:
|
||||
if line.startswith(find_value):
|
||||
output = line.replace(find_value + '=', '')
|
||||
break
|
||||
except Exception as e:
|
||||
pass
|
||||
return output
|
||||
|
||||
def _process_data(self, data):
|
||||
ext = data['ext']
|
||||
icon = 'default'
|
||||
for ico, exts in self.presets['extensions'].items():
|
||||
if ext in exts:
|
||||
icon = ico
|
||||
break
|
||||
# Add 's' to icon_name if is sequence (image -> images)
|
||||
if data['is_sequence']:
|
||||
icon += 's'
|
||||
data['icon'] = icon
|
||||
data['thumb'] = (
|
||||
ext in self.presets['extensions']['image_file'] or
|
||||
ext in self.presets['extensions']['video_file']
|
||||
)
|
||||
data['prev'] = ext in self.presets['extensions']['video_file']
|
||||
|
||||
actions = []
|
||||
new_is_seq = data['is_sequence']
|
||||
|
||||
found = False
|
||||
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 = 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'] == 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
|
||||
288
pype/standalonepublish/widgets/widget_family.py
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
import os
|
||||
import sys
|
||||
import inspect
|
||||
import json
|
||||
from collections import namedtuple
|
||||
|
||||
from . import QtWidgets, QtCore
|
||||
from . import HelpRole, FamilyRole, ExistsRole, PluginRole
|
||||
from . import FamilyDescriptionWidget
|
||||
|
||||
from pypeapp import config
|
||||
|
||||
|
||||
class FamilyWidget(QtWidgets.QWidget):
|
||||
|
||||
stateChanged = QtCore.Signal(bool)
|
||||
data = dict()
|
||||
_jobs = dict()
|
||||
Separator = "---separator---"
|
||||
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
# Store internal states in here
|
||||
self.state = {"valid": False}
|
||||
self.parent_widget = parent
|
||||
|
||||
body = QtWidgets.QWidget()
|
||||
lists = QtWidgets.QWidget()
|
||||
|
||||
container = QtWidgets.QWidget()
|
||||
|
||||
list_families = QtWidgets.QListWidget()
|
||||
input_asset = QtWidgets.QLineEdit()
|
||||
input_asset.setEnabled(False)
|
||||
input_asset.setStyleSheet("color: #BBBBBB;")
|
||||
input_subset = QtWidgets.QLineEdit()
|
||||
input_result = QtWidgets.QLineEdit()
|
||||
input_result.setStyleSheet("color: gray;")
|
||||
input_result.setEnabled(False)
|
||||
|
||||
# region Menu for default subset names
|
||||
btn_subset = QtWidgets.QPushButton()
|
||||
btn_subset.setFixedWidth(18)
|
||||
btn_subset.setFixedHeight(20)
|
||||
menu_subset = QtWidgets.QMenu(btn_subset)
|
||||
btn_subset.setMenu(menu_subset)
|
||||
|
||||
# endregion
|
||||
name_layout = QtWidgets.QHBoxLayout()
|
||||
name_layout.addWidget(input_subset)
|
||||
name_layout.addWidget(btn_subset)
|
||||
name_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(container)
|
||||
|
||||
header = FamilyDescriptionWidget(self)
|
||||
layout.addWidget(header)
|
||||
|
||||
layout.addWidget(QtWidgets.QLabel("Family"))
|
||||
layout.addWidget(list_families)
|
||||
layout.addWidget(QtWidgets.QLabel("Asset"))
|
||||
layout.addWidget(input_asset)
|
||||
layout.addWidget(QtWidgets.QLabel("Subset"))
|
||||
layout.addLayout(name_layout)
|
||||
layout.addWidget(input_result)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
options = QtWidgets.QWidget()
|
||||
|
||||
layout = QtWidgets.QGridLayout(options)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(lists)
|
||||
layout.addWidget(container)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(body)
|
||||
layout.addWidget(lists)
|
||||
layout.addWidget(options, 0, QtCore.Qt.AlignLeft)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.addWidget(body)
|
||||
|
||||
input_subset.textChanged.connect(self.on_data_changed)
|
||||
input_asset.textChanged.connect(self.on_data_changed)
|
||||
list_families.currentItemChanged.connect(self.on_selection_changed)
|
||||
list_families.currentItemChanged.connect(header.set_item)
|
||||
|
||||
self.stateChanged.connect(self._on_state_changed)
|
||||
|
||||
self.input_subset = input_subset
|
||||
self.menu_subset = menu_subset
|
||||
self.btn_subset = btn_subset
|
||||
self.list_families = list_families
|
||||
self.input_asset = input_asset
|
||||
self.input_result = input_result
|
||||
|
||||
self.refresh()
|
||||
|
||||
def collect_data(self):
|
||||
plugin = self.list_families.currentItem().data(PluginRole)
|
||||
family = plugin.family.rsplit(".", 1)[-1]
|
||||
data = {
|
||||
'family': family,
|
||||
'subset': self.input_subset.text()
|
||||
}
|
||||
return data
|
||||
|
||||
@property
|
||||
def db(self):
|
||||
return self.parent_widget.db
|
||||
|
||||
def change_asset(self, name):
|
||||
self.input_asset.setText(name)
|
||||
|
||||
def _on_state_changed(self, state):
|
||||
self.state['valid'] = state
|
||||
self.parent_widget.set_valid_family(state)
|
||||
|
||||
def _build_menu(self, default_names):
|
||||
"""Create optional predefined subset names
|
||||
|
||||
Args:
|
||||
default_names(list): all predefined names
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
# Get and destroy the action group
|
||||
group = self.btn_subset.findChild(QtWidgets.QActionGroup)
|
||||
if group:
|
||||
group.deleteLater()
|
||||
|
||||
state = any(default_names)
|
||||
self.btn_subset.setEnabled(state)
|
||||
if state is False:
|
||||
return
|
||||
|
||||
# Build new action group
|
||||
group = QtWidgets.QActionGroup(self.btn_subset)
|
||||
for name in default_names:
|
||||
if name == self.Separator:
|
||||
self.menu_subset.addSeparator()
|
||||
continue
|
||||
action = group.addAction(name)
|
||||
self.menu_subset.addAction(action)
|
||||
|
||||
group.triggered.connect(self._on_action_clicked)
|
||||
|
||||
def _on_action_clicked(self, action):
|
||||
self.input_subset.setText(action.text())
|
||||
|
||||
def _on_data_changed(self):
|
||||
item = self.list_families.currentItem()
|
||||
subset_name = self.input_subset.text()
|
||||
asset_name = self.input_asset.text()
|
||||
|
||||
# Get the assets from the database which match with the name
|
||||
assets_db = self.db.find(filter={"type": "asset"}, projection={"name": 1})
|
||||
assets = [asset for asset in assets_db if asset_name in asset["name"]]
|
||||
if item is None:
|
||||
return
|
||||
if assets:
|
||||
# Get plugin and family
|
||||
plugin = item.data(PluginRole)
|
||||
if plugin is None:
|
||||
return
|
||||
family = plugin.family.rsplit(".", 1)[-1]
|
||||
|
||||
# Get all subsets of the current asset
|
||||
asset_ids = [asset["_id"] for asset in assets]
|
||||
subsets = self.db.find(filter={"type": "subset",
|
||||
"name": {"$regex": "{}*".format(family),
|
||||
"$options": "i"},
|
||||
"parent": {"$in": asset_ids}}) or []
|
||||
|
||||
# Get all subsets' their subset name, "Default", "High", "Low"
|
||||
existed_subsets = [sub["name"].split(family)[-1]
|
||||
for sub in subsets]
|
||||
|
||||
if plugin.defaults and isinstance(plugin.defaults, list):
|
||||
defaults = plugin.defaults[:] + [self.Separator]
|
||||
lowered = [d.lower() for d in plugin.defaults]
|
||||
for sub in [s for s in existed_subsets
|
||||
if s.lower() not in lowered]:
|
||||
defaults.append(sub)
|
||||
else:
|
||||
defaults = existed_subsets
|
||||
|
||||
self._build_menu(defaults)
|
||||
|
||||
# Update the result
|
||||
if subset_name:
|
||||
subset_name = subset_name[0].upper() + subset_name[1:]
|
||||
self.input_result.setText("{}{}".format(family, subset_name))
|
||||
|
||||
item.setData(ExistsRole, True)
|
||||
self.echo("Ready ..")
|
||||
else:
|
||||
self._build_menu([])
|
||||
item.setData(ExistsRole, False)
|
||||
if asset_name != self.parent_widget.NOT_SELECTED:
|
||||
self.echo("'%s' not found .." % asset_name)
|
||||
|
||||
# Update the valid state
|
||||
valid = (
|
||||
subset_name.strip() != "" and
|
||||
asset_name.strip() != "" and
|
||||
item.data(QtCore.Qt.ItemIsEnabled) and
|
||||
item.data(ExistsRole)
|
||||
)
|
||||
self.stateChanged.emit(valid)
|
||||
|
||||
def on_data_changed(self, *args):
|
||||
|
||||
# Set invalid state until it's reconfirmed to be valid by the
|
||||
# scheduled callback so any form of creation is held back until
|
||||
# valid again
|
||||
self.stateChanged.emit(False)
|
||||
self.schedule(self._on_data_changed, 500, channel="gui")
|
||||
|
||||
def on_selection_changed(self, *args):
|
||||
plugin = self.list_families.currentItem().data(PluginRole)
|
||||
if plugin is None:
|
||||
return
|
||||
|
||||
if plugin.defaults and isinstance(plugin.defaults, list):
|
||||
default = plugin.defaults[0]
|
||||
else:
|
||||
default = "Default"
|
||||
|
||||
self.input_subset.setText(default)
|
||||
|
||||
self.on_data_changed()
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""Custom keyPressEvent.
|
||||
|
||||
Override keyPressEvent to do nothing so that Maya's panels won't
|
||||
take focus when pressing "SHIFT" whilst mouse is over viewport or
|
||||
outliner. This way users don't accidently perform Maya commands
|
||||
whilst trying to name an instance.
|
||||
|
||||
"""
|
||||
|
||||
def refresh(self):
|
||||
has_families = False
|
||||
presets = config.get_presets().get('standalone_publish', {})
|
||||
|
||||
for creator in presets.get('families', {}).values():
|
||||
creator = namedtuple("Creator", creator.keys())(*creator.values())
|
||||
|
||||
label = creator.label or creator.family
|
||||
item = QtWidgets.QListWidgetItem(label)
|
||||
item.setData(QtCore.Qt.ItemIsEnabled, True)
|
||||
item.setData(HelpRole, creator.help or "")
|
||||
item.setData(FamilyRole, creator.family)
|
||||
item.setData(PluginRole, creator)
|
||||
item.setData(ExistsRole, False)
|
||||
self.list_families.addItem(item)
|
||||
|
||||
has_families = True
|
||||
|
||||
if not has_families:
|
||||
item = QtWidgets.QListWidgetItem("No registered families")
|
||||
item.setData(QtCore.Qt.ItemIsEnabled, False)
|
||||
self.list_families.addItem(item)
|
||||
|
||||
self.list_families.setCurrentItem(self.list_families.item(0))
|
||||
|
||||
def echo(self, message):
|
||||
if hasattr(self.parent_widget, 'echo'):
|
||||
self.parent_widget.echo(message)
|
||||
|
||||
def schedule(self, func, time, channel="default"):
|
||||
try:
|
||||
self._jobs[channel].stop()
|
||||
except (AttributeError, KeyError):
|
||||
pass
|
||||
|
||||
timer = QtCore.QTimer()
|
||||
timer.setSingleShot(True)
|
||||
timer.timeout.connect(func)
|
||||
timer.start(time)
|
||||
|
||||
self._jobs[channel] = timer
|
||||
101
pype/standalonepublish/widgets/widget_family_desc.py
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import os
|
||||
import sys
|
||||
import inspect
|
||||
import json
|
||||
|
||||
from . import QtWidgets, QtCore, QtGui
|
||||
from . import HelpRole, FamilyRole, ExistsRole, PluginRole
|
||||
from . import awesome
|
||||
from pype.vendor import six
|
||||
from pype import lib as pypelib
|
||||
|
||||
|
||||
class FamilyDescriptionWidget(QtWidgets.QWidget):
|
||||
"""A family description widget.
|
||||
|
||||
Shows a family icon, family name and a help description.
|
||||
Used in creator header.
|
||||
|
||||
_________________
|
||||
| ____ |
|
||||
| |icon| FAMILY |
|
||||
| |____| help |
|
||||
|_________________|
|
||||
|
||||
"""
|
||||
|
||||
SIZE = 35
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(FamilyDescriptionWidget, self).__init__(parent=parent)
|
||||
|
||||
# Header font
|
||||
font = QtGui.QFont()
|
||||
font.setBold(True)
|
||||
font.setPointSize(14)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
icon = QtWidgets.QLabel()
|
||||
icon.setSizePolicy(QtWidgets.QSizePolicy.Maximum,
|
||||
QtWidgets.QSizePolicy.Maximum)
|
||||
|
||||
# Add 4 pixel padding to avoid icon being cut off
|
||||
icon.setFixedWidth(self.SIZE + 4)
|
||||
icon.setFixedHeight(self.SIZE + 4)
|
||||
icon.setStyleSheet("""
|
||||
QLabel {
|
||||
padding-right: 5px;
|
||||
}
|
||||
""")
|
||||
|
||||
label_layout = QtWidgets.QVBoxLayout()
|
||||
label_layout.setSpacing(0)
|
||||
|
||||
family = QtWidgets.QLabel("family")
|
||||
family.setFont(font)
|
||||
family.setAlignment(QtCore.Qt.AlignBottom | QtCore.Qt.AlignLeft)
|
||||
|
||||
help = QtWidgets.QLabel("help")
|
||||
help.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft)
|
||||
|
||||
label_layout.addWidget(family)
|
||||
label_layout.addWidget(help)
|
||||
|
||||
layout.addWidget(icon)
|
||||
layout.addLayout(label_layout)
|
||||
|
||||
self.help = help
|
||||
self.family = family
|
||||
self.icon = icon
|
||||
|
||||
def set_item(self, item):
|
||||
"""Update elements to display information of a family item.
|
||||
|
||||
Args:
|
||||
family (dict): A family item as registered with name, help and icon
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
if not item:
|
||||
return
|
||||
|
||||
# Support a font-awesome icon
|
||||
plugin = item.data(PluginRole)
|
||||
icon = getattr(plugin, "icon", "info-circle")
|
||||
assert isinstance(icon, six.string_types)
|
||||
icon = awesome.icon("fa.{}".format(icon), color="white")
|
||||
pixmap = icon.pixmap(self.SIZE, self.SIZE)
|
||||
pixmap = pixmap.scaled(self.SIZE, self.SIZE)
|
||||
|
||||
# Parse a clean line from the Creator's docstring
|
||||
docstring = plugin.help or ""
|
||||
|
||||
help = docstring.splitlines()[0] if docstring else ""
|
||||
|
||||
self.icon.setPixmap(pixmap)
|
||||
self.family.setText(item.data(FamilyRole))
|
||||
self.help.setText(help)
|
||||
40
pype/standalonepublish/widgets/widget_shadow.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
from . import QtWidgets, QtCore, QtGui
|
||||
|
||||
|
||||
class ShadowWidget(QtWidgets.QWidget):
|
||||
def __init__(self, parent):
|
||||
self.parent_widget = parent
|
||||
super().__init__(parent)
|
||||
w = self.parent_widget.frameGeometry().width()
|
||||
h = self.parent_widget.frameGeometry().height()
|
||||
self.resize(QtCore.QSize(w, h))
|
||||
palette = QtGui.QPalette(self.palette())
|
||||
palette.setColor(palette.Background, QtCore.Qt.transparent)
|
||||
self.setPalette(palette)
|
||||
self.message = ''
|
||||
|
||||
font = QtGui.QFont()
|
||||
font.setFamily("DejaVu Sans Condensed")
|
||||
font.setPointSize(40)
|
||||
font.setBold(True)
|
||||
font.setWeight(50)
|
||||
font.setKerning(True)
|
||||
self.font = font
|
||||
|
||||
def paintEvent(self, event):
|
||||
painter = QtGui.QPainter()
|
||||
painter.begin(self)
|
||||
painter.setFont(self.font)
|
||||
painter.setRenderHint(QtGui.QPainter.Antialiasing)
|
||||
painter.fillRect(event.rect(), QtGui.QBrush(QtGui.QColor(0, 0, 0, 127)))
|
||||
painter.drawText(
|
||||
QtCore.QRectF(
|
||||
0.0,
|
||||
0.0,
|
||||
self.parent_widget.frameGeometry().width(),
|
||||
self.parent_widget.frameGeometry().height()
|
||||
),
|
||||
QtCore.Qt.AlignCenter|QtCore.Qt.AlignCenter,
|
||||
self.message
|
||||
)
|
||||
painter.end()
|
||||