import itertools
import ui
import os
import errno
import traceback
import hiero.core
import hiero.core.FnExporterBase as FnExporterBase
from hiero.ui.FnElidedLabel import ElidedLabel
from PySide2 import (QtCore, QtWidgets)
from ui import IProcessorUI
from hiero.core.FnCompSourceInfo import CompSourceInfo
from hiero.core.util import filesystem
def isCompItemMissingRenders(compItem):
""" Check if renders for a comp item are missing or out of date. Returns
(missing, out of date) """
missing = False
try:
# Get the render info for the comp
info = CompSourceInfo(compItem)
startFrame = info.firstFrame
endFrame = info.lastFrame
# If the item is a TrackItem, search for missing files in its source range
if isinstance(compItem, hiero.core.TrackItem):
startFrame = int(compItem.sourceIn() + info.firstFrame)
endFrame = int(compItem.sourceOut() + info.firstFrame)
# Iterate over the frame range and check if any files are missing. The + 1 is necessary, because range is exclusive at the end of the interval.
for frame in range(startFrame, endFrame + 1):
framePath = info.writePath % frame
try:
frameModTime = round(filesystem.stat(framePath).st_mtime)
except OSError as e:
# Check if file doesn't exist
if e.errno == errno.ENOENT:
missing = True
break
else:
raise
except:
# Catch all: log, and false will be returned
hiero.core.log.exception("isCompItemMissingRenders unexpected error")
return missing
[docs]class ProcessorUIBase(IProcessorUI):
"""ProcessorUIBase is the base class from which all Processor UI components must derive. Defines the UI structure followed
by the specialised processor UIs. """
[docs] def getTaskItemType(self):
raise NotImplementedError()
def __init__(self, preset, itemTypes):
IProcessorUI.__init__(self)
self._preset = None
self._exportTemplate = None
self._exportStructureViewer = None
self._contentElement = None
self._contentScrollArea = None
self._contentUI = None
self._editMode = IProcessorUI.ReadOnly
self._itemTypes = itemTypes
self._tags = []
self._project = None
self._exportItems = []
self.setPreset(preset)
[docs] def validate ( self, exportItems ):
"""Validate settings in UI. Return False for failure in order to abort export."""
hiero.core.log.info("ProcessorUIBase validate")
exportRoot = self._exportTemplate.exportRootPath()
if exportRoot is None or len(exportRoot)<1:
msgBox = QtWidgets.QMessageBox()
msgBox.setText("The export path root is not set, please set a valid path.")
msgBox.exec_()
return False
elif "{projectroot}" in exportRoot:
project = self.projectFromSelection(exportItems)
from hiero.ui import getProjectRootInteractive
projectRoot = getProjectRootInteractive(project)
if not projectRoot:
return False
# Check for offline track items
if not self.checkOfflineMedia(exportItems):
return False
if not self.checkUnrenderedComps(exportItems):
return False
return True
[docs] def isTranscodeExport(self):
""" Check if there are transcode tasks in this export. """
# To avoid importing the hiero.exporters module here, just check
# for 'Transcode' in the preset class name.
for (exportPath, preset) in self._exportTemplate.flatten():
if "Transcode" in FnExporterBase.classBasename(type(preset)):
return True
return False
[docs] def findCompItems(self, items):
""" Search for comp clips and track items in a list of ItemWrappers. """
for item in items:
if item.clip():
if CompSourceInfo(item.clip()).nkPath:
yield item.clip()
else:
for trackItem in self.toTrackItems([item]):
try:
if CompSourceInfo(trackItem).nkPath:
yield trackItem
except:
pass
[docs] def checkUnrenderedComps(self, exportItems):
""" Check for unrendered comps selected for export and ask the user what to do. """
hiero.core.log.info("ProcessorUIBase checkUnrenderedComps begin")
# Only do this check for transcodes
if not self.isTranscodeExport():
return True
hiero.core.log.info("ProcessorUIBase checkUnrenderedComps is transcode export")
# Scan the items and find comps which haven't been rendered
unrenderedNkSources = set()
for compItem in self.findCompItems(exportItems):
if isinstance(compItem, hiero.core.TrackItem):
source = compItem.source().mediaSource()
elif isinstance(compItem, hiero.core.Clip):
source = compItem.mediaSource()
hiero.core.log.info("ProcessorUIBase checkUnrenderedComps in loop")
unrendered = isCompItemMissingRenders(compItem)
if unrendered:
unrenderedNkSources.add(source)
continueExport = True
renderComps = False
# Show a message box and give the user the option to either render the comps or skip them
if unrenderedNkSources:
messageText = ("Some Comp items have not been rendered or are out of date."
" Do you want to render them now, or skip them?")
messageBox = QtWidgets.QMessageBox( QtWidgets.QMessageBox.Question, "Export", messageText, QtWidgets.QMessageBox.NoButton, hiero.ui.mainWindow() )
cancelButton = messageBox.addButton( "Cancel export", QtWidgets.QMessageBox.RejectRole )
messageBox.setDefaultButton( cancelButton )
renderButton = messageBox.addButton("Render", QtWidgets.QMessageBox.YesRole)
skipButton = messageBox.addButton("Skip", QtWidgets.QMessageBox.YesRole)
messageBox.exec_()
clickedButton = messageBox.clickedButton()
if clickedButton == cancelButton:
continueExport = False
else:
renderComps = (clickedButton == renderButton)
# If the user selected 'Render', render all unrendered comps.
if renderComps:
compsToRender = unrenderedNkSources
self._preset.setCompsToRender(compsToRender)
# If the user selected 'Skip', exclude unrendered comps completely.
else:
self._preset.setCompsToSkip(unrenderedNkSources)
# Otherwise make sure the comps to render list is cleared
else:
self._preset.setCompsToRender([])
return continueExport
[docs] def projectFromSelection(self, items):
# Return the project of the first item in the selection
for item in items:
if item.trackItem():
return item.trackItem().project()
elif item.sequence():
return item.sequence().project()
elif item.clip():
return item.clip().project()
return None
[docs] def toTrackItems(self, items):
# Filter out tracks which have been excluded in the preset. This isn't a great solution
# (we shouldn't really be doing the validation in this class at all), but it works for now.
excludedTracksGUIDs = []
if hasattr(self._preset, "_excludedTrackIDs"):
excludedTracksGUIDs = self._preset._excludedTrackIDs
for item in items:
if item.trackItem():
yield item.trackItem()
elif item.sequence():
for track in itertools.chain(item.sequence().videoTracks(), item.sequence().audioTracks()):
if track.guid() not in excludedTracksGUIDs and track.isEnabled():
for trackItem in track:
yield trackItem
[docs] def populateUI(self, processorUIWidget, taskUIWidget, exportItems):
""" Build the processor UI and add it to widget. """
self._exportItems = exportItems
self._tags = self.findTagsForItems(exportItems)
layout = QtWidgets.QVBoxLayout(processorUIWidget)
layout.setContentsMargins(0, 0, 0, 0)
splitter = QtWidgets.QSplitter(QtCore.Qt.Vertical)
splitter.setChildrenCollapsible(False);
splitter.setHandleWidth(10);
layout.addWidget(splitter)
self._editMode = IProcessorUI.ReadOnly if self._preset.readOnly() else IProcessorUI.Full
# The same enums are declared in 2 classes. They should have the same values but to be sure, map between them
editModeMap = { IProcessorUI.ReadOnly : ui.ExportStructureViewer.ReadOnly,
IProcessorUI.Limited : ui.ExportStructureViewer.Limited,
IProcessorUI.Full : ui.ExportStructureViewer.Full }
structureViewerMode = editModeMap[self._editMode]
###EXPORT STRUCTURE
exportStructureWidget = QtWidgets.QWidget()
splitter.addWidget(exportStructureWidget)
exportStructureLayout = QtWidgets.QVBoxLayout(exportStructureWidget)
exportStructureLayout.setContentsMargins(0, 0, 0, 9)
self._exportStructureViewer = ui.ExportStructureViewer(self._exportTemplate, structureViewerMode)
exportStructureLayout.addWidget(self._exportStructureViewer)
self._project = self.projectFromSelection(exportItems)
if self._project:
self._exportStructureViewer.setProject(self._project)
self._exportStructureViewer.destroyed.connect(self.onExportStructureViewerDestroyed)
self._exportStructureViewer.setItemTypes(self._itemTypes)
self._preset.createResolver().addEntriesToExportStructureViewer(self._exportStructureViewer)
self._exportStructureViewer.structureModified.connect(self.onExportStructureModified)
self._exportStructureViewer.selectionChanged.connect(self.onExportStructureSelectionChanged)
exportStructureLayout.addWidget(self.createVersionWidget())
exportStructureLayout.addWidget(self.createPathPreviewWidget())
splitter.addWidget(self.createProcessorSettingsWidget(exportItems))
taskUILayout = QtWidgets.QVBoxLayout(taskUIWidget)
taskUILayout.setContentsMargins(10, 0, 0, 0)
tabWidget = QtWidgets.QTabWidget()
taskUILayout.addWidget(tabWidget)
self._contentScrollArea = QtWidgets.QScrollArea()
tabWidget.addTab(self._contentScrollArea, "Content")
self._contentScrollArea.setFrameStyle( QtWidgets.QScrollArea.NoFrame )
self._contentScrollArea.setWidgetResizable(True)
[docs] def setPreset(self, preset):
""" Set the export preset. """
self._preset = preset
oldTemplate = self._exportTemplate
self._exportTemplate = hiero.core.ExportStructure2()
self._exportTemplate.restore(self._preset.properties()["exportTemplate"])
if self._preset.properties()["exportRoot"] != "None":
self._exportTemplate.setExportRootPath(self._preset.properties()["exportRoot"])
# Must replace the Export Structure viewer structure object before old template is destroyed
if self._exportStructureViewer is not None:
self._exportStructureViewer.setExportStructure(self._exportTemplate)
# Bug 46032 - Make sure that the task preset shares the same project as its parent.
# Since the taskUI might require information from the project.
for (exportPath, taskPreset) in self._exportTemplate.flatten():
# Preset can be None if the user never set it
if not taskPreset:
continue
taskPreset.setProject(preset.project())
# Setup callbacks on the task preset. This is mainly used by the NukeShotPreset,
# which references other tasks by path and needs to be told when the paths
# change.
taskPreset.initialiseCallbacks(self._exportTemplate)
[docs] def preset(self):
""" Get the export preset. """
return self._preset
[docs] def setTaskContent(self, preset):
""" Get the UI for a task preset and add it in the 'Content' tab. """
# First clear the old task UI. It's important that this doesn't live longer
# than the widgets it created, otherwise it can lead to crashes in PySide2.
self._contentUI = None
taskUIWidget = None
# if selection is valid, grab preset
if preset is not None:
# Get a new TaskUI object from the registry
taskUI = hiero.ui.taskUIRegistry.getNewTaskUIForPreset(preset)
# if UI is valid, set preset and add to 'contentlayout'
if taskUI:
# Set the project on the task UI. It may need it to show project-specific
# information, e.g. colorspace configs.
taskUI.setProject(self._project)
taskUI.setTags(self._tags)
taskUIWidget = QtWidgets.QWidget()
taskUI.setTaskItemType(self.getTaskItemType())
# Initialize and Populate UI
taskUI.initializeAndPopulateUI(taskUIWidget, self._exportTemplate)
self._contentUI = taskUI
if self._editMode == IProcessorUI.ReadOnly:
taskUIWidget.setEnabled(False)
try:
taskUI.propertiesChanged.connect(self.onExportStructureModified,
type=QtCore.Qt.UniqueConnection)
except:
# Signal already connected.
pass
# If there's no task UI, create an empty widget.
if not taskUIWidget:
taskUIWidget = QtWidgets.QWidget()
# Add the task UI widget to the scroll area
self._contentScrollArea.setWidget( taskUIWidget )
[docs] def onExportStructureModified(self):
""" Callback when the export structure is modified by the user. """
self._preset.properties()["exportTemplate"] = self._exportTemplate.flatten()
self._preset.properties()["exportRoot"] = self._exportTemplate.exportRootPath()
if self._exportStructureViewer and self._contentElement is not None:
self._exportStructureViewer.refreshContentField(self._contentElement)
self.updatePathPreview()
[docs] def onExportStructureSelectionChanged(self):
""" Callback when the selection in the export structure viewer changes. """
# Grab current selection
element = self._exportStructureViewer.selection()
if element is not None:
self._contentElement = element
self.setTaskContent(element.preset())
self.updatePathPreview()
[docs] def onExportStructureViewerDestroyed(self):
""" Callback when the export structure viewer is destroyed. Qt will delete it while we still
have a reference, so reset to None when the destroyed() signal is emitted. """
self._exportStructureViewer = None
[docs] def processorSettingsLabel(self):
""" Get the label which is put on the tab for processor-specific settings. To be reimplemented by subclasses. """
raise NotImplementedError()
[docs] def savePreset( self ):
""" Save the export template to the preset. """
self._preset.properties()["exportTemplate"] = self._exportTemplate.flatten()
self._preset.properties()["exportRoot"] = self._exportTemplate.exportRootPath()
[docs] def onVersionIndexChanged(self, value):
""" Callback when the version index changes. """
self._preset.properties()["versionIndex"] = int(value)
self.updatePathPreview()
[docs] def onVersionPaddingChanged(self, padding):
""" Callback when the version padding changes. """
self._preset.properties()["versionPadding"] = int(padding)
self.updatePathPreview()
[docs] def updatePathPreview(self):
""" Update the path preview widget for the currently selected item in the
tree view.
"""
text = ""
try:
selectedElement = self._exportStructureViewer.selection()
if selectedElement:
# Find the first leaf element, which should have a preset associated
# with it that can be used to resolve the path. If a directory element
# has been selected, that will still have a TaskPresetBase associated with
# it for some reason, but that can't be used to create tasks.
def findLeafElementRecursive(element):
children = element.children()
if not children:
return element
else:
return findLeafElementRecursive(children[0])
taskElement = findLeafElementRecursive(selectedElement)
taskPreset = taskElement.preset()
# Create a processor and generate tasks for the current export
processor = hiero.core.taskRegistry.createProcessor(self._preset)
tasks = processor.startProcessing(self._exportItems, preview=True)
# Find the first task which matches the selected preset. If there are
# multiple items being selected, the first in the list is displayed,
# which is enough to give the user an idea of how their path template
# will be expanded
for task in tasks:
if task._preset is taskPreset:
exportRoot = self._exportTemplate.exportRootPath()
path = os.path.join(exportRoot, selectedElement.path())
text = "Preview: %s" % task.resolvePath(path)
break
except:
hiero.core.log.exception("Error generating path preview")
self._pathPreviewWidget.setText(text)
[docs] def skipOffline(self):
return self._preset.skipOffline()
[docs] def refreshContent(self):
""" Refresh the content area of this ProcessorUI """
element = self._exportStructureViewer.selection() if self._exportStructureViewer else None
if element is not None:
currentVerticalScrollPosition = self._contentScrollArea.verticalScrollBar().value()
self._contentElement = element
self.setTaskContent(element.preset())
self._contentScrollArea.verticalScrollBar().setValue(currentVerticalScrollPosition)