feature/OP-7692_Folder_Batch_publishing_tool

This commit is contained in:
Braden Jennings 2024-03-07 13:35:32 +13:00
parent 7bf7780dd6
commit abd609fb3c
8 changed files with 1199 additions and 0 deletions

View file

@ -0,0 +1,97 @@
import click
# from openpype.lib import get_openpype_execute_args
# from openpype.lib.execute import run_detached_process
from openpype.modules import OpenPypeModule, ITrayAction, IHostAddon
class BatchPublishAddon(OpenPypeModule, IHostAddon, ITrayAction):
label = "Batch Publisher"
name = "batchpublisher"
host_name = "batchpublisher"
def initialize(self, modules_settings):
self.enabled = True
# UI which must not be created at this time
self._dialog = None
def tray_init(self):
return
def on_action_trigger(self):
self.show_dialog()
# self.run_batchpublisher()
def connect_with_modules(self, enabled_modules):
"""Collect publish paths from other modules."""
return
# def run_batchpublisher(self):
# name = "traypublisher"
# args = get_openpype_execute_args(
# "module", name, "launch"
# # "module", self.name, "launch"
# )
# print("ARGS" , args)
# run_detached_process(args)
def cli(self, click_group):
click_group.add_command(cli_main)
def _create_dialog(self):
# # Don't recreate dialog if already exists
# if self._dialog is not None:
# return
from importlib import reload
import openpype.hosts.batchpublisher.controller
import openpype.hosts.batchpublisher.ui.batch_publisher_model
import openpype.hosts.batchpublisher.ui.batch_publisher_delegate
import openpype.hosts.batchpublisher.ui.batch_publisher_view
import openpype.hosts.batchpublisher.ui.window
# TODO: These lines are only for testing current branch
reload(openpype.hosts.batchpublisher.controller)
reload(openpype.hosts.batchpublisher.ui.batch_publisher_model)
reload(
openpype.hosts.batchpublisher.ui.batch_publisher_delegate)
reload(openpype.hosts.batchpublisher.ui.batch_publisher_view)
reload(openpype.hosts.batchpublisher.ui.window)
import openpype.hosts.batchpublisher.deadline
import openpype.hosts.batchpublisher.publish
import openpype.hosts.batchpublisher.utils
reload(openpype.hosts.batchpublisher.deadline)
reload(openpype.hosts.batchpublisher.publish)
reload(openpype.hosts.batchpublisher.utils)
# from openpype.hosts.batchpublisher.ui.window \
# import BatchPublisherWindow
self._dialog = openpype.hosts.batchpublisher.ui.window. \
BatchPublisherWindow()
def show_dialog(self):
"""Show dialog with connected modules.
This can be called from anywhere but can also crash in headless mode.
There is no way to prevent addon to do invalid operations if he's
not handling them.
"""
# Make sure dialog is created
self._create_dialog()
# Show dialog
self._dialog.show()
@click.group(BatchPublishAddon.name, help="BatchPublisher related commands.")
def cli_main():
pass
@cli_main.command()
def launch():
"""Launch BatchPublisher tool UI."""
print("LAUNCHING BATCH PUBLISHER")
from openpype.hosts.batchpublisher.ui import window
window.main()

View file

@ -0,0 +1,379 @@
import collections
import glob
import os
import re
# from openpype.settings import get_project_settings
from openpype.client.entities import (
get_projects,
get_assets,
)
from openpype.hosts.batchpublisher import publish
# List that contains dictionary including glob statement to check for match.
# If filepath matches then it becomes product type.
# TODO: add to OpenPype settings so other studios can change
GLOB_SEARCH_TO_PRODUCT_INFO_MAP = [
{
"glob": "*/fbx/*.fbx",
"is_sequence": False,
"product_type": "model",
"representation_name": "fbx"
},
{
"glob": "*/ma/*.ma",
"is_sequence": False,
"product_type": "model",
"representation_name": "maya"
},
]
# Dictionary that maps the extension name to the representation name.
# We only use this as fallback if
# GLOB_SEARCH_TO_PRODUCT_INFO_MAP doesn't match any glob statement.
# TODO: add to OpenPype settings so other studios can change
EXT_TO_REP_NAME_MAP = {
".nk": "nuke",
".ma": "maya",
".mb": "maya",
".hip": "houdini",
".sfx": "silhouette",
".mocha": "mocha",
".psd": "photoshop"
}
# Dictionary that maps the product type (family) to file extensions
# We only use this as fallback if
# GLOB_SEARCH_TO_PRODUCT_INFO_MAP doesn't match any glob statement.
# TODO: add to OpenPype settings so other studios can change
PRODUCT_TYPE_TO_EXT_MAP = {
"render": {".exr", ".dpx", ".tif", ".tiff", ".jpg", ".jpeg"},
"pointcache": {".abc"},
"camera": {".abc", ".fbx"},
"reference": {".mov", ".mp4", ".mxf", ".avi", ".wmv"},
"workfile": {".nk", ".ma", ".mb", ".hip", ".sfx", ".mocha", ".psd"},
"distortion": {".nk", ".exr"},
"color_grade": {".ccc", ".cc"},
}
class ProductItem(object):
def __init__(
self,
filepath,
product_type,
representation_name,
product_name=None,
version=None,
comment=None,
enabled=True,
folder_path=None,
task_name=None,
frame_start=None,
frame_end=None):
self.enabled = enabled
self.filepath = filepath
self.product_type = product_type
self.product_name = product_name
self.representation_name = representation_name
self.version = version
self.folder_path = folder_path
self.comment = comment
self.task_name = task_name
self.frame_start = frame_start
self.frame_end = frame_end
self.derive_product_name()
def derive_product_name(self):
filename = os.path.basename(self.filepath)
filename_no_ext, extension = os.path.splitext(filename)
# Exclude possible frame in product name
product_name = filename_no_ext.split(".")[0]
# Add the product type as prefix to product name
if product_name.startswith("_"):
product_name = self.product_type + product_name
else:
product_name = self.product_type + "_" + product_name
# Try to extract version number from filename
self.version = None
results = re.findall("_v[0-9]*", self.filepath)
if results:
self.version = int(results[0].replace("_v", ""))
# Remove version from product name
self.product_name = re.sub("_v[0-9]*", "", product_name)
return self.product_name
@property
def defined(self):
return all([
self.filepath,
self.folder_path,
self.task_name,
self.product_type,
self.product_name,
self.representation_name])
class HierarchyItem:
def __init__(self, folder_name, folder_path, folder_id, parent_id):
self.folder_name = folder_name
self.folder_path = folder_path
self.folder_id = folder_id
self.parent_id = parent_id
class BatchPublisherController(object):
def __init__(self):
self._selected_project_name = None
self._project_names = None
self._asset_docs_by_project = {}
self._asset_docs_by_path = {}
def get_project_names(self):
if self._project_names is None:
projects = get_projects(fields={"name"})
project_names = []
for project in projects:
project_names.append(project["name"])
self._project_names = project_names
return self._project_names
def get_selected_project_name(self):
return self._selected_project_name
def set_selected_project_name(self, project_name):
self._selected_project_name = project_name
def _get_asset_docs(self):
"""
Returns:
dict[str, dict]: Dictionary of asset documents by path.
"""
project_name = self._selected_project_name
if not project_name:
return {}
asset_docs = self._asset_docs_by_project.get(project_name)
if asset_docs is None:
asset_docs = list(get_assets(
project_name,
fields={
"name",
# "data.visualParent",
"data.parents",
"data.tasks",
}
))
asset_docs_by_path = self._prepare_assets_by_path(asset_docs)
self._asset_docs_by_project[project_name] = asset_docs_by_path
return self._asset_docs_by_project[project_name]
def get_hierarchy_items(self):
"""
Returns:
list[HierarchyItem]: List of hierarchy items.
"""
asset_docs = self._get_asset_docs()
if not asset_docs:
return []
output = []
for folder_path, asset_doc in asset_docs.items():
folder_name = asset_doc["name"]
folder_id = asset_doc["_id"]
parent_id = asset_doc["data"]["visualParent"]
hierarchy_item = HierarchyItem(
folder_name, folder_path, folder_id, parent_id)
output.append(hierarchy_item)
return output
def get_task_names(self, folder_path):
asset_docs_by_path = self._get_asset_docs()
if not asset_docs_by_path:
return []
asset_doc = asset_docs_by_path.get(folder_path)
if not asset_doc:
return []
return list(asset_doc["data"]["tasks"].keys())
def _prepare_assets_by_path(self, asset_docs):
output = {}
for asset_doc in asset_docs:
parents = list(asset_doc["data"]["parents"])
parents.append(asset_doc["name"])
folder_path = "/" + "/".join(parents)
output[folder_path] = asset_doc
return output
def get_product_items(self, directory):
"""
Returns:
list[ProductItem]: List of ingest files for the given directory
"""
product_items = collections.OrderedDict()
if not directory or not os.path.exists(directory):
return product_items
# project_name = self._selected_project_name
# project_settings = get_project_settings(project_name)
# file_mappings = project_settings["batchpublisher"].get(
# "file_mappings", [])
file_mappings = GLOB_SEARCH_TO_PRODUCT_INFO_MAP
for file_mapping in file_mappings:
product_type = file_mapping["product_type"]
glob_full_path = directory + "/" + file_mapping["glob"]
representation_name = file_mapping.get("representation_name")
files = glob.glob(glob_full_path, recursive=False)
for filepath in files:
filename = os.path.basename(filepath)
frame_start = None
frame_end = None
if filename.count(".") >= 2:
# Lets add the star in place of the frame number
filepath_parts = filepath.split(".")
filepath_parts[-2] = "#" * len(filepath_parts[-2])
# Replace the file path with the version with star in it
_filepath = ".".join(filepath_parts)
frames = self._get_frames_for_filepath(_filepath)
if frames:
filepath = _filepath
frame_start = frames[0]
frame_end = frames[-1]
# Do not add ingest file path, if it's already been added
if filepath in product_items:
continue
_filename_no_ext, extension = os.path.splitext(filename)
# Create representation name from extension
representation_name = representation_name or \
EXT_TO_REP_NAME_MAP.get(extension)
if not representation_name:
representation_name = extension.lstrip(".")
product_item = ProductItem(
filepath,
product_type,
representation_name,
frame_start=frame_start,
frame_end=frame_end)
product_items[filepath] = product_item
# Walk the entire directory structure again
# and look for product items to add.
# This time we are looking for product types
# by PRODUCT_TYPE_TO_EXT_MAP
for root, _dirs, filenames in os.walk(directory, topdown=False):
for filename in filenames:
filepath = os.path.join(root, filename)
filename = os.path.basename(filepath)
# Get frame infomration (if any)
frame_start = None
frame_end = None
if filename.count(".") >= 2:
# Lets add the star in place of the frame number
filepath_parts = filepath.split(".")
filepath_parts[-2] = "*"
# Replace the file path with the version with star in it
_filepath = ".".join(filepath_parts)
frames = self._get_frames_for_filepath(_filepath)
if frames:
filepath = _filepath
frame_start = frames[0]
frame_end = frames[-1]
# Do not add ingest file path, if it's already been added
if filepath in product_items:
continue
_filename_no_ext, extension = os.path.splitext(filename)
product_type = None
for _product_type, extensions in \
PRODUCT_TYPE_TO_EXT_MAP.items():
if extension in extensions:
product_type = _product_type
break
if not product_type:
continue
# Create representation name from extension
representation_name = EXT_TO_REP_NAME_MAP.get(extension)
if not representation_name:
representation_name = extension.lstrip(".")
product_item = ProductItem(
filepath,
product_type,
representation_name,
frame_start=frame_start,
frame_end=frame_end)
product_items[filepath] = product_item
return list(product_items.values())
def publish_product_items(self, product_items):
"""
Args:
product_items (list[ProductItem]): List of ingest files to publish.
"""
for product_item in product_items:
if product_item.enabled and product_item.defined:
self._publish_product_item(product_item)
def _publish_product_item(self, product_item):
msg = f"""
Publishing (ingesting): {product_item.filepath}
As Folder (Asset): {product_item.folder_path}
Task: {product_item.task_name}
Product Type (Family): {product_item.product_type}
Product Name (Subset): {product_item.product_name}
Representation: {product_item.representation_name}
Version: {product_item.version}
Comment: {product_item.comment}
Frame start: {product_item.frame_start}
Frame end: {product_item.frame_end}
Project: {self._selected_project_name}
"""
print(msg)
publish_data = dict()
publish_data["version"] = product_item.version
publish_data["comment"] = product_item.comment
expected_representations = dict()
expected_representations[product_item.representation_name] = \
product_item.filepath
publish.publish_version_pyblish(
self._selected_project_name,
product_item.folder_path,
product_item.task_name,
product_item.product_type,
product_item.product_name,
expected_representations,
publish_data,
frame_start=product_item.frame_start,
frame_end=product_item.frame_end)
# publish.publish_version(
# self._selected_project_name,
# product_item.folder_path,
# product_item.task_name,
# product_item.product_type,
# product_item.product_name,
# expected_representations,
# publish_data)
# publish.publish_version(
# project_name,
# asset_name,
# task_name,
# family_name,
# subset_name,
# expected_representations,
# publish_data,
def _get_frames_for_filepath(self, filepath):
# Collect all the frames found within the paths of glob search string
frames = list()
for _filepath in glob.glob(filepath):
filepath_parts = _filepath.split(".")
try:
frame = int(filepath_parts[-2])
except Exception:
continue
frames.append(frame)
return sorted(frames)

View file

@ -0,0 +1,189 @@
import collections
from qtpy import QtWidgets, QtCore, QtGui
from .batch_publisher_model import BatchPublisherModel
FOLDER_PATH_ROLE = QtCore.Qt.UserRole + 1
class BatchPublisherTableDelegate(QtWidgets.QStyledItemDelegate):
def __init__(self, controller, parent=None):
super(BatchPublisherTableDelegate, self).__init__(parent)
self._controller = controller
def createEditor(self, parent, option, index):
model = index.model()
ingest_file = model.get_product_items()[index.row()]
if index.column() == BatchPublisherModel.COLUMN_OF_FOLDER:
editor = None
view = parent.parent()
# NOTE: Project name has been disabled to change from this dialog
accepted, _project_name, folder_path, task_name = \
self._on_choose_context(ingest_file.folder_path)
if accepted and folder_path:
for _index in view.selectedIndexes():
model.setData(
model.index(_index.row(), model.COLUMN_OF_FOLDER),
folder_path,
QtCore.Qt.EditRole)
if task_name:
model.setData(
model.index(_index.row(), model.COLUMN_OF_TASK),
task_name,
QtCore.Qt.EditRole)
# # clear the folder
# model.setData(index, None, QtCore.Qt.EditRole)
# # clear the task
# model.setData(
# model.index(index.row(), BatchPublisherModel.COLUMN_OF_TASK),
# None,
# QtCore.Qt.EditRole)
# treeview = QtWidgets.QTreeView()
# treeview.setEditTriggers(QtWidgets.QTreeView.NoEditTriggers)
# treeview.setSelectionBehavior(QtWidgets.QTreeView.SelectRows)
# treeview.setSelectionMode(QtWidgets.QTreeView.SingleSelection)
# treeview.setItemsExpandable(True)
# treeview.header().setVisible(False)
# treeview.setMinimumHeight(250)
# editor = ComboBox(parent)
# editor.setInsertPolicy(QtWidgets.QComboBox.NoInsert)
# editor.setView(treeview)
# model = QtGui.QStandardItemModel()
# editor.setModel(model)
# self._fill_model_with_hierarchy(model)
# editor.view().expandAll()
# # editor.showPopup()
# # editor = QtWidgets.QLineEdit(parent)
# # completer = QtWidgets.QCompleter(self._folder_names, self)
# # completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
# # editor.setCompleter(completer)
return editor
elif index.column() == BatchPublisherModel.COLUMN_OF_TASK:
task_names = self._controller.get_task_names(
ingest_file.folder_path)
# editor = QtWidgets.QLineEdit(parent)
# completer = QtWidgets.QCompleter(
# task_names,
# self)
# completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
# editor.setCompleter(completer)
editor = QtWidgets.QComboBox(parent)
editor.addItems(task_names)
return editor
elif index.column() == BatchPublisherModel.COLUMN_OF_PRODUCT_TYPE:
from openpype.plugins.publish import integrate
product_types = sorted(integrate.IntegrateAsset.families)
editor = QtWidgets.QComboBox(parent)
editor.addItems(product_types)
return editor
# return QtWidgets.QStyledItemDelegate.createEditor(
# self,
# parent,
# option,
# index)
def setEditorData(self, editor, index):
# if index.column() == BatchPublisherModel.COLUMN_OF_FOLDER:
# editor.blockSignals(True)
# # value = index.data(QtCore.Qt.DisplayRole)
# # editor.setText(value)
# # Lets return the QComboxBox back to unselected state
# editor.setRootModelIndex(QtCore.QModelIndex())
# editor.setCurrentIndex(-1)
# editor.blockSignals(False)
if index.column() == BatchPublisherModel.COLUMN_OF_TASK:
editor.blockSignals(True)
value = index.data(QtCore.Qt.DisplayRole)
row = editor.findText(value)
editor.setCurrentIndex(row)
editor.blockSignals(False)
elif index.column() == BatchPublisherModel.COLUMN_OF_PRODUCT_TYPE:
editor.blockSignals(True)
value = index.data(QtCore.Qt.DisplayRole)
row = editor.findText(value)
editor.setCurrentIndex(row)
editor.blockSignals(False)
def setModelData(self, editor, model, index):
model = index.model()
# if index.column() == BatchPublisherModel.COLUMN_OF_FOLDER:
# # value = editor.text()
# value = editor.model().data(
# editor.view().currentIndex(),
# FOLDER_PATH_ROLE)
# model.setData(index, value, QtCore.Qt.EditRole)
if index.column() == BatchPublisherModel.COLUMN_OF_TASK:
value = editor.currentText()
model.setData(index, value, QtCore.Qt.EditRole)
elif index.column() == BatchPublisherModel.COLUMN_OF_PRODUCT_TYPE:
value = editor.currentText()
model.setData(index, value, QtCore.Qt.EditRole)
def _fill_model_with_hierarchy(self, model):
hierarchy_items = self._controller.get_hierarchy_items()
hierarchy_items_by_parent_id = collections.defaultdict(list)
for hierarchy_item in hierarchy_items:
hierarchy_items_by_parent_id[hierarchy_item.parent_id].append(
hierarchy_item
)
root_item = model.invisibleRootItem()
hierarchy_queue = collections.deque()
hierarchy_queue.append((root_item, None))
while hierarchy_queue:
(parent_item, parent_id) = hierarchy_queue.popleft()
new_rows = []
for hierarchy_item in hierarchy_items_by_parent_id[parent_id]:
new_row = QtGui.QStandardItem(hierarchy_item.folder_name)
new_row.setData(hierarchy_item.folder_path, FOLDER_PATH_ROLE)
new_row.setData(
hierarchy_item.folder_path, QtCore.Qt.ToolTipRole)
# new_row.setFlags(
# QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
new_rows.append(new_row)
hierarchy_queue.append((new_row, hierarchy_item.folder_id))
if new_rows:
parent_item.appendRows(new_rows)
def _on_choose_context(self, folder_path):
from openpype.tools.context_dialog import ContextDialog
project_name = self._controller.get_selected_project_name()
dialog = ContextDialog()
dialog._project_combobox.hide()
dialog.set_context(
project_name=project_name)
accepted = dialog.exec_()
if accepted:
context = dialog.get_context()
project = context["project"]
asset = context["asset"]
# AYON version of dialog stores the folder path
folder_path = context.get("folder_path")
if folder_path:
# Folder path returned by ContextDialog is missing slash
folder_path = "/" + folder_path
folder_path = folder_path or asset
task_name = context["task"]
return accepted, project, folder_path, task_name
return accepted, None, None, None
class ComboBox(QtWidgets.QComboBox):
def keyPressEvent(self, event):
# This is to prevent pressing "a" button with folder cell
# selected and the "assets" is selected in QComboBox.
# A default behaviour coming from QComboBox, when key is pressed
# it selects first matching item in QComboBox root model index.
# We don't want to select the "assets", since its not a full path
# of folder.
if event.type() == QtCore.QEvent.KeyPress:
return

View file

@ -0,0 +1,213 @@
from qtpy import QtCore, QtGui
from openpype.plugins.publish import integrate
class BatchPublisherModel(QtCore.QAbstractTableModel):
HEADER_LABELS = [
str(),
"Filepath",
"Folder",
"Task",
"Product Type",
"Product Name",
"Representation",
"Version",
"Comment"]
COLUMN_OF_ENABLED = 0
COLUMN_OF_FILEPATH = 1
COLUMN_OF_FOLDER = 2
COLUMN_OF_TASK = 3
COLUMN_OF_PRODUCT_TYPE = 4
COLUMN_OF_PRODUCT_NAME = 5
COLUMN_OF_REPRESENTATION = 6
COLUMN_OF_VERSION = 7
COLUMN_OF_COMMENT = 8
def __init__(self, controller):
super(BatchPublisherModel, self).__init__()
self._controller = controller
self._product_items = []
def set_current_directory(self, directory):
self._populate_from_directory(directory)
def get_product_items(self):
return list(self._product_items)
def rowCount(self, parent=None):
if parent is None:
parent = QtCore.QModelIndex()
return len(self._product_items)
def columnCount(self, parent=None):
if parent is None:
parent = QtCore.QModelIndex()
return len(BatchPublisherModel.HEADER_LABELS)
def headerData(self, section, orientation, role=None):
if role is None:
role = QtCore.Qt.DisplayRole
if role != QtCore.Qt.DisplayRole:
return None
if orientation == QtCore.Qt.Horizontal:
return BatchPublisherModel.HEADER_LABELS[section]
def setData(self, index, value, role=None):
column = index.column()
row = index.row()
product_item = self._product_items[row]
if role == QtCore.Qt.EditRole:
if column == BatchPublisherModel.COLUMN_OF_FILEPATH:
product_item.filepath = value
elif column == BatchPublisherModel.COLUMN_OF_FOLDER:
# Check folder path is valid in available docs.
# Folder path might also be reset to None.
asset_docs_by_path = self._controller._get_asset_docs()
if value is None or value in asset_docs_by_path:
# Update folder path
product_item.folder_path = value
# Update task name
product_item.task_name = None
task_names = self._controller.get_task_names(value)
if not product_item.task_name and task_names:
product_item.task_name = task_names[0]
elif column == BatchPublisherModel.COLUMN_OF_TASK:
# Check task is valid in availble task names.
# Task name might also be reset to None.
if value is None or value in self._controller.get_task_names(
product_item.folder_path):
product_item.task_name = value
elif column == BatchPublisherModel.COLUMN_OF_PRODUCT_TYPE:
# Check family is valid in available families
# Product type might also be reset to None.
if value is None or value in integrate.IntegrateAsset.families:
product_item.product_type = value
# Update the product name based on product type
product_item.derive_product_name()
roles = [QtCore.Qt.DisplayRole]
self.dataChanged.emit(
self.index(
row, BatchPublisherModel.COLUMN_OF_ENABLED),
self.index(
row, BatchPublisherModel.COLUMN_OF_COMMENT),
roles)
elif column == BatchPublisherModel.COLUMN_OF_PRODUCT_NAME:
product_item.product_name = value
elif column == BatchPublisherModel.COLUMN_OF_REPRESENTATION:
product_item.representation_name = value
elif column == BatchPublisherModel.COLUMN_OF_VERSION:
try:
product_item.version = int(value)
except Exception:
product_item.version = None
elif column == BatchPublisherModel.COLUMN_OF_COMMENT:
product_item.comment = value
return True
elif role == QtCore.Qt.CheckStateRole:
if column == BatchPublisherModel.COLUMN_OF_ENABLED:
enabled = True if value == QtCore.Qt.Checked else False
product_item.enabled = enabled
roles = [QtCore.Qt.ForegroundRole]
self.dataChanged.emit(
self.index(row, column),
self.index(row, BatchPublisherModel.COLUMN_OF_VERSION),
roles)
return True
def data(self, index, role=QtCore.Qt.DisplayRole):
column = index.column()
row = index.row()
product_item = self._product_items[row]
if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
if column == BatchPublisherModel.COLUMN_OF_FILEPATH:
return product_item.filepath
elif column == BatchPublisherModel.COLUMN_OF_FOLDER:
return product_item.folder_path
elif column == BatchPublisherModel.COLUMN_OF_TASK:
return product_item.task_name
elif column == BatchPublisherModel.COLUMN_OF_PRODUCT_TYPE:
return product_item.product_type
elif column == BatchPublisherModel.COLUMN_OF_PRODUCT_NAME:
return product_item.product_name
elif column == BatchPublisherModel.COLUMN_OF_REPRESENTATION:
return product_item.representation_name
elif column == BatchPublisherModel.COLUMN_OF_VERSION:
return str(product_item.version or "")
elif column == BatchPublisherModel.COLUMN_OF_COMMENT:
return product_item.comment
elif role == QtCore.Qt.ForegroundRole:
if product_item.defined and product_item.enabled:
return QtGui.QColor(240, 240, 240)
else:
return QtGui.QColor(120, 120, 120)
# elif role == QtCore.Qt.BackgroundRole:
# return QtGui.QColor(QtCore.Qt.white)
# elif role == QtCore.Qt.TextAlignmentRole:
# return QtCore.Qt.AlignRight
elif role == QtCore.Qt.ToolTipRole:
project_name = self._controller.get_selected_project_name()
task_names = self._controller.get_task_names(
product_item.folder_path)
tooltip = f"""
Enabled: <b>{product_item.enabled}</b>
<br>Filepath: <b>{product_item.filepath}</b>
<br>Folder (Asset): <b>{product_item.folder_path}</b>
<br>Task: <b>{product_item.task_name}</b>
<br>Product Type (Family): <b>{product_item.product_type}</b>
<br>Product Name (Subset): <b>{product_item.product_name}</b>
<br>Representation: <b>{product_item.representation_name}</b>
<br>Version: <b>{product_item.version}</b>
<br>Comment: <b>{product_item.comment}</b>
<br>Frame start: <b>{product_item.frame_start}</b>
<br>Frame end: <b>{product_item.frame_end}</b>
<br>Defined: <b>{product_item.defined}</b>
<br>Task Names: <b>{task_names}</b>
<br>Project: <b>{project_name}</b>
"""
return tooltip
elif role == QtCore.Qt.CheckStateRole:
if column == BatchPublisherModel.COLUMN_OF_ENABLED:
return QtCore.Qt.Checked if product_item.enabled \
else QtCore.Qt.Unchecked
elif role == QtCore.Qt.FontRole:
# if column in [
# BatchPublisherModel.COLUMN_OF_FILEPATH,
# BatchPublisherModel.COLUMN_OF_PRODUCT_TYPE,
# BatchPublisherModel.COLUMN_OF_PRODUCT_NAME]:
font = QtGui.QFont()
font.setPointSize(9)
return font
# return None
def flags(self, index):
flags = QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
if index.column() == BatchPublisherModel.COLUMN_OF_ENABLED:
flags |= QtCore.Qt.ItemIsUserCheckable
elif index.column() > BatchPublisherModel.COLUMN_OF_FILEPATH:
flags |= QtCore.Qt.ItemIsEditable
return flags
def _populate_from_directory(self, directory):
print("model set_current_directory", directory)
self.beginResetModel()
self._product_items = self._controller.get_product_items(
directory
)
self.endResetModel()
def _change_project(self, project_name):
"""Clear the existing picked folder names, since project changed"""
for row in range(self.rowCount()):
product_item = self._product_items[row]
product_item.folder_path = None
product_item.task_name = None
roles = [QtCore.Qt.DisplayRole]
self.dataChanged.emit(
self.index(row, self.COLUMN_OF_ENABLED),
self.index(row, self.COLUMN_OF_COMMENT),
roles)

View file

@ -0,0 +1,140 @@
import functools
from qtpy import QtWidgets, QtCore, QtGui
from .batch_publisher_model import BatchPublisherModel
class BatchPublisherTableView(QtWidgets.QTableView):
def __init__(self, controller, parent=None):
super(BatchPublisherTableView, self).__init__(parent)
model = BatchPublisherModel(controller)
self.setModel(model)
# self.setEditTriggers(self.NoEditTriggers)
self.setSelectionMode(self.ExtendedSelection)
# self.setSelectionBehavior(self.SelectRows)
self.setColumnWidth(model.COLUMN_OF_ENABLED, 22)
self.setColumnWidth(model.COLUMN_OF_FILEPATH, 700)
self.setColumnWidth(model.COLUMN_OF_FOLDER, 200)
self.setColumnWidth(model.COLUMN_OF_TASK, 90)
self.setColumnWidth(model.COLUMN_OF_PRODUCT_TYPE, 140)
self.setColumnWidth(model.COLUMN_OF_PRODUCT_NAME, 275)
self.setColumnWidth(model.COLUMN_OF_REPRESENTATION, 120)
self.setColumnWidth(model.COLUMN_OF_VERSION, 70)
self.setColumnWidth(model.COLUMN_OF_COMMENT, 120)
self.setTextElideMode(QtCore.Qt.ElideNone)
self.setWordWrap(False)
header = self.horizontalHeader()
header.setSectionResizeMode(
BatchPublisherModel.COLUMN_OF_FILEPATH,
header.Stretch)
self.verticalHeader().hide()
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self._open_menu)
self._model = model
self._controller = controller
def set_current_directory(self, directory):
print("view set_current_directory", directory)
self._model.set_current_directory(directory)
def get_product_items(self):
return self._model.get_product_items()
def commitData(self, editor):
super(BatchPublisherTableView, self).commitData(editor)
current_index = self.currentIndex()
model = self.currentIndex().model()
# Apply edit role to every other row of selection
value = model.data(current_index, QtCore.Qt.EditRole)
for qmodelindex in self.selectedIndexes():
# row = qmodelindex.row()
# product_item = model.product_items[row]
model.setData(qmodelindex, value, role=QtCore.Qt.EditRole)
# When changing folder we need to propagate
# the chosen task value to every other row
if current_index.column() == BatchPublisherModel.COLUMN_OF_FOLDER:
qmodelindex_task = model.index(
current_index.row(),
BatchPublisherModel.COLUMN_OF_TASK)
value_task = model.data(
qmodelindex_task,
QtCore.Qt.DisplayRole)
for qmodelindex in self.selectedIndexes():
qmodelindex_task = model.index(
qmodelindex.row(),
BatchPublisherModel.COLUMN_OF_TASK)
model.setData(
qmodelindex_task,
value_task,
QtCore.Qt.EditRole)
def _open_menu(self, pos):
index = self.indexAt(pos)
if not index.isValid():
return
product_items = self._model.get_product_items()
product_item = product_items[index.row()]
enabled = not product_item.enabled
menu = QtWidgets.QMenu()
action_copy = QtWidgets.QAction()
action_copy.setText("Copy selected text")
action_copy.triggered.connect(
functools.partial(self.__copy_selected_text, pos))
menu.addAction(action_copy)
action_paste = QtWidgets.QAction()
action_paste.setText("Paste text into selected cells")
action_paste.triggered.connect(
functools.partial(self.__paste_selected_text, pos))
menu.addAction(action_paste)
action_toggle_enabled = QtWidgets.QAction()
action_toggle_enabled.setText("Toggle enabled")
action_toggle_enabled.triggered.connect(
functools.partial(self.__toggle_selected_enabled, enabled))
menu.addAction(action_toggle_enabled)
menu.exec_(self.viewport().mapToGlobal(pos))
def __toggle_selected_enabled(self, enabled):
product_items = self._model.get_product_items()
for _index in self.selectedIndexes():
product_item = product_items[_index.row()]
product_item.enabled = enabled
roles = [QtCore.Qt.DisplayRole]
self._model.dataChanged.emit(
self._model.index(_index.row(), self._model.COLUMN_OF_ENABLED),
self._model.index(_index.row(), self._model.COLUMN_OF_COMMENT),
roles)
def __copy_selected_text(self, pos):
index = self.indexAt(pos)
if not index.isValid():
return
value = self._model.data(index)
clipboard = QtWidgets.QApplication.clipboard()
clipboard.setText(value, QtGui.QClipboard.Clipboard)
def __paste_selected_text(self, pos):
index = self.indexAt(pos)
if not index.isValid():
return
value = QtWidgets.QApplication.clipboard().text()
column = index.column()
for index in self.selectedIndexes():
if column == self._model.COLUMN_OF_FILEPATH:
continue
self._model.setData(index, value, QtCore.Qt.EditRole)

View file

@ -0,0 +1,181 @@
from openpype import style
from qtpy import QtWidgets
from openpype.hosts.batchpublisher import controller
from .batch_publisher_model import BatchPublisherModel
from .batch_publisher_delegate import BatchPublisherTableDelegate
from .batch_publisher_view import BatchPublisherTableView
class BatchPublisherWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(BatchPublisherWindow, self).__init__(parent)
self.setWindowTitle("AYON Batch Publisher")
self.resize(1750, 900)
main_widget = QtWidgets.QWidget(self)
self.setCentralWidget(main_widget)
# --- Top inputs (project, directory) ---
top_inputs_widget = QtWidgets.QWidget(self)
self._project_combobox = QtWidgets.QComboBox(top_inputs_widget)
self._project_combobox.setSizePolicy(
QtWidgets.QSizePolicy.Expanding,
QtWidgets.QSizePolicy.Fixed)
dir_inputs_widget = QtWidgets.QWidget(top_inputs_widget)
dir_input = QtWidgets.QLineEdit(dir_inputs_widget)
dir_browse_btn = QtWidgets.QPushButton("Browse", dir_inputs_widget)
dir_inputs_layout = QtWidgets.QHBoxLayout(dir_inputs_widget)
dir_inputs_layout.setContentsMargins(0, 0, 0, 0)
dir_inputs_layout.addWidget(dir_input, 1)
dir_inputs_layout.addWidget(dir_browse_btn, 0)
top_inputs_layout = QtWidgets.QFormLayout(top_inputs_widget)
top_inputs_layout.setContentsMargins(0, 0, 0, 0)
top_inputs_layout.addRow("Choose project", self._project_combobox)
# pushbutton_change_project = QtWidgets.QPushButton("Change project")
# top_inputs_layout.addRow(pushbutton_change_project)
top_inputs_layout.addRow("Directory to ingest", dir_inputs_widget)
self._controller = controller.BatchPublisherController()
# --- Main view ---
table_view = BatchPublisherTableView(self._controller, main_widget)
# --- Footer ---
footer_widget = QtWidgets.QWidget(main_widget)
publish_btn = QtWidgets.QPushButton("Publish", footer_widget)
footer_layout = QtWidgets.QHBoxLayout(footer_widget)
footer_layout.setContentsMargins(0, 0, 0, 0)
footer_layout.addStretch(1)
footer_layout.addWidget(publish_btn, 0)
# --- Main layout ---
main_layout = QtWidgets.QVBoxLayout(main_widget)
main_layout.setContentsMargins(12, 12, 12, 12)
main_layout.setSpacing(12)
main_layout.addWidget(top_inputs_widget, 0)
main_layout.addWidget(table_view, 1)
main_layout.addWidget(footer_widget, 0)
self.setStyleSheet(style.load_stylesheet())
self._project_combobox.currentIndexChanged.connect(
self._on_project_changed)
# pushbutton_change_project.clicked.connect(self._on_project_changed)
dir_browse_btn.clicked.connect(self._on_browse_button_clicked)
publish_btn.clicked.connect(self._on_publish_button_clicked)
editors_delegate = BatchPublisherTableDelegate(self._controller)
table_view.setItemDelegateForColumn(
BatchPublisherModel.COLUMN_OF_FOLDER,
editors_delegate)
table_view.setItemDelegateForColumn(
BatchPublisherModel.COLUMN_OF_TASK,
editors_delegate)
table_view.setItemDelegateForColumn(
BatchPublisherModel.COLUMN_OF_PRODUCT_TYPE,
editors_delegate)
dir_input.textChanged.connect(self._on_dir_change)
# self._project_combobox = project_combobox
self._dir_input = dir_input
self._table_view = table_view
self._editors_delegate = editors_delegate
self._pushbutton_publish = publish_btn
self._first_show = True
def showEvent(self, event):
super(BatchPublisherWindow, self).showEvent(event)
if self._first_show:
self._first_show = False
self._on_first_show()
def _on_first_show(self):
project_names = sorted(self._controller.get_project_names())
for project_name in project_names:
self._project_combobox.addItem(project_name)
def _on_project_changed(self):
project_name = str(self._project_combobox.currentText())
self._controller.set_selected_project_name(project_name)
self._table_view._model._change_project(project_name)
def _on_browse_button_clicked(self):
directory = self._dir_input.text()
directory = QtWidgets.QFileDialog.getExistingDirectory(
self,
dir=directory)
if not directory:
return
# Lets insure text changes even if the directory picked
# is the same as before
self._dir_input.blockSignals(True)
self._dir_input.setText(directory)
self._dir_input.blockSignals(False)
self._dir_input.textChanged.emit(directory)
def _on_dir_change(self, directory):
print("_on_dir_changed")
self._table_view.set_current_directory(directory)
def _on_publish_button_clicked(self):
product_items = self._table_view.get_product_items()
publish_count = 0
enabled_count = 0
defined_count = 0
for product_item in product_items:
if product_item.enabled and product_item.defined:
publish_count += 1
if product_item.enabled:
enabled_count += 1
if product_item.defined:
defined_count += 1
if publish_count == 0:
msg = "You must provide asset, task, family, "
msg += "subset etc and they must be enabled"
QtWidgets.QMessageBox.warning(
None,
"No enabled and defined ingest items!",
msg)
return
elif publish_count > 0:
msg = "Are you sure you want to publish "
msg += "{} products".format(publish_count)
result = QtWidgets.QMessageBox.question(
None,
"Okay to publish?",
msg,
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
if result == QtWidgets.QMessageBox.No:
print("User cancelled publishing")
return
elif enabled_count == 0:
QtWidgets.QMessageBox.warning(
None,
"Nothing enabled for publish!",
"There is no items enabled for publish")
return
elif defined_count == 0:
QtWidgets.QMessageBox.warning(
None,
"No defined ingest items!",
"You must provide asset, task, family, subset etc")
return
self._controller.publish_product_items(product_items)
def main():
batch_publisher = BatchPublisherWindow()
batch_publisher.show()