Skip to content

Commit

Permalink
Cuesubmit jobs from config file with dynamic widgets (#1425)
Browse files Browse the repository at this point in the history
* feat: add a `Browse` button, not visible by default

* feat: add 2 functions `getFolder` and `getFile` (with optional file filter)

* feat: add wrappers to `getFile` and `getFolder` to set the returned result on the calling widget

* feat: add functions to display the browse button and attach wrappers that feeds the lineEdit widget with browse result.

* feat: Make `BaseMayaSettings` and `InMayaSettings` browsable with file filter

* feat: Make `BaseNukeSettings` and `InNukeSettings` browsable with file filter

* feat: Make `BaseBlenderSettings` browsable with file filter

* feat!: Group all job specific widgets in a groupBox with title

* feat: moved job specific widget group below job setup widgets

* feat: add styling for QGroupBox

* feat: add `greyOut()` function to `CueLabelLineEdit`

* feat: add styling for disabled `CueLabelLineEdit`

* fix: make maya command use start and end frames instead of fixed single frame.

* feat!: add `silent` argument to `build*Cmd()` functions so we can feed them to the UI before they are fully valid.

* fix!: remove redundant functions and use the new `silent` argument

* feat: add constants `FRAME_START` and `FRAME_END`

* fix!: FRAME_TOKEN must not be configurable as it is not forwarded to the java code replacing it with the proper value.

* feat: add commandFeedback widget in `CueSubmitWidget` and its `updateFeedbackCommand()` function

* fix: revert 2 deleted lines

* fix: update blender command when changing output format

* fix: update layout var

* clean: move line up with sibling

* fix: change expected maya command test result

* feat: add Constants `MAYA_FILE_FILTERS`, `NUKE_FILE_FILTERS`, `BLENDER_FILE_FILTERS`

* fix: use Constants `MAYA_FILE_FILTERS`, `NUKE_FILE_FILTERS`, `BLENDER_FILE_FILTERS`

* Added missing CueLabelLineEdit.setter and fixed some linting issues

---------

Co-authored-by: Kern Attila Germain <[email protected]>
  • Loading branch information
lithorus and KernAttila authored Jul 24, 2024
1 parent e0ffd63 commit e17163a
Show file tree
Hide file tree
Showing 7 changed files with 190 additions and 55 deletions.
10 changes: 10 additions & 0 deletions cuesubmit/cuesubmit/Constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@
'#JOB#': 'Name of the Job',
'#FRAME#': 'Name of the Frame'
}

MAYA_FILE_FILTERS = [
'Maya Ascii file (*.ma)',
'Maya Binary file (*.mb)',
'Maya file (*.ma *.mb)'
]
NUKE_FILE_FILTERS = ['Nuke script file (*.nk)']
BLENDER_FILE_FILTERS = ['Blender file (*.blend)']


BLENDER_FORMATS = ['', 'AVIJPEG', 'AVIRAW', 'BMP', 'CINEON', 'DPX', 'EXR', 'HDR', 'IRIS', 'IRIZ',
'JP2', 'JPEG', 'MPEG', 'MULTILAYER', 'PNG', 'RAWTGA', 'TGA', 'TIFF']
BLENDER_OUTPUT_OPTIONS_URL = \
Expand Down
69 changes: 28 additions & 41 deletions cuesubmit/cuesubmit/Submission.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,25 +31,27 @@
from cuesubmit import JobTypes


def buildMayaCmd(layerData):
def buildMayaCmd(layerData, silent=False):
"""From a layer, builds a Maya Render command."""
camera = layerData.cmd.get('camera')
mayaFile = layerData.cmd.get('mayaFile')
if not mayaFile:
if not mayaFile and not silent:
raise ValueError('No Maya File provided. Cannot submit job.')
renderCommand = '{renderCmd} -r file -s {frameToken} -e {frameToken}'.format(
renderCmd=Constants.MAYA_RENDER_CMD, frameToken=Constants.FRAME_TOKEN)
renderCommand = '{renderCmd} -r file -s {frameStart} -e {frameEnd}'.format(
renderCmd=Constants.MAYA_RENDER_CMD,
frameStart=Constants.FRAME_START_TOKEN,
frameEnd=Constants.FRAME_END_TOKEN)
if camera:
renderCommand += ' -cam {}'.format(camera)
renderCommand += ' {}'.format(mayaFile)
return renderCommand


def buildNukeCmd(layerData):
def buildNukeCmd(layerData, silent=False):
"""From a layer, builds a Nuke Render command."""
writeNodes = layerData.cmd.get('writeNodes')
nukeFile = layerData.cmd.get('nukeFile')
if not nukeFile:
if not nukeFile and not silent:
raise ValueError('No Nuke file provided. Cannot submit job.')
renderCommand = '{renderCmd} -F {frameToken} '.format(
renderCmd=Constants.NUKE_RENDER_CMD, frameToken=Constants.FRAME_TOKEN)
Expand All @@ -59,13 +61,13 @@ def buildNukeCmd(layerData):
return renderCommand


def buildBlenderCmd(layerData):
def buildBlenderCmd(layerData, silent=False):
"""From a layer, builds a Blender render command."""
blenderFile = layerData.cmd.get('blenderFile')
outputPath = layerData.cmd.get('outputPath')
outputFormat = layerData.cmd.get('outputFormat')
frameRange = layerData.layerRange
if not blenderFile:
if not blenderFile and not silent:
raise ValueError('No Blender file provided. Cannot submit job.')

renderCommand = '{renderCmd} -b -noaudio {blenderFile}'.format(
Expand Down Expand Up @@ -110,46 +112,31 @@ def buildLayer(layerData, command, lastLayer=None):
layer.depend_on(lastLayer)
return layer


def buildMayaLayer(layerData, lastLayer):
"""Builds a PyOutline layer running a Maya command."""
mayaCmd = buildMayaCmd(layerData)
return buildLayer(layerData, mayaCmd, lastLayer)


def buildNukeLayer(layerData, lastLayer):
"""Builds a PyOutline layer running a Nuke command."""
nukeCmd = buildNukeCmd(layerData)
return buildLayer(layerData, nukeCmd, lastLayer)


def buildBlenderLayer(layerData, lastLayer):
"""Builds a PyOutline layer running a Blender command."""
blenderCmd = buildBlenderCmd(layerData)
return buildLayer(layerData, blenderCmd, lastLayer)


def buildShellLayer(layerData, lastLayer):
"""Builds a PyOutline layer running a shell command."""
return buildLayer(layerData, layerData.cmd['commandTextBox'], lastLayer)

def buildLayerCommand(layerData, silent=False):
"""Builds the command to be sent per jobType"""
if layerData.layerType == JobTypes.JobTypes.MAYA:
command = buildMayaCmd(layerData, silent)
elif layerData.layerType == JobTypes.JobTypes.SHELL:
command = layerData.cmd.get('commandTextBox') if silent else layerData.cmd['commandTextBox']
elif layerData.layerType == JobTypes.JobTypes.NUKE:
command = buildNukeCmd(layerData, silent)
elif layerData.layerType == JobTypes.JobTypes.BLENDER:
command = buildBlenderCmd(layerData, silent)
else:
if silent:
command = 'Error: unrecognized layer type {}'.format(layerData.layerType)
else:
raise ValueError('unrecognized layer type {}'.format(layerData.layerType))
return command

def submitJob(jobData):
"""Submits the job using the PyOutline API."""
ol = outline.Outline(
jobData['name'], shot=jobData['shot'], show=jobData['show'], user=jobData['username'])
lastLayer = None
for layerData in jobData['layers']:
if layerData.layerType == JobTypes.JobTypes.MAYA:
layer = buildMayaLayer(layerData, lastLayer)
elif layerData.layerType == JobTypes.JobTypes.SHELL:
layer = buildShellLayer(layerData, lastLayer)
elif layerData.layerType == JobTypes.JobTypes.NUKE:
layer = buildNukeLayer(layerData, lastLayer)
elif layerData.layerType == JobTypes.JobTypes.BLENDER:
layer = buildBlenderLayer(layerData, lastLayer)
else:
raise ValueError('unrecognized layer type %s' % layerData.layerType)
command = buildLayerCommand(layerData)
layer = buildLayer(layerData, command, lastLayer)
ol.add_layer(layer)
lastLayer = layer

Expand Down
51 changes: 41 additions & 10 deletions cuesubmit/cuesubmit/ui/SettingsWidgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ class BaseSettingsWidget(QtWidgets.QWidget):
def __init__(self, parent=None):
super(BaseSettingsWidget, self).__init__(parent)
self.mainLayout = QtWidgets.QVBoxLayout()
self.groupBox = QtWidgets.QGroupBox('options')
self.groupLayout = QtWidgets.QVBoxLayout()
self.groupBox.setLayout(self.groupLayout)
self.groupBox.setStyleSheet(Widgets.Style.GROUP_BOX)
self.mainLayout.addWidget(self.groupBox)
self.setLayout(self.mainLayout)
self.mainLayout.setContentsMargins(0, 0, 0, 0)

Expand All @@ -53,17 +58,25 @@ class InMayaSettings(BaseSettingsWidget):
# pylint: disable=keyword-arg-before-vararg,unused-argument
def __init__(self, cameras=None, filename=None, parent=None, *args, **kwargs):
super(InMayaSettings, self).__init__(parent=parent)
self.groupBox.setTitle('Maya options')
self.mayaFileInput = Widgets.CueLabelLineEdit('Maya File:', filename)
self.fileFilters = Constants.MAYA_FILE_FILTERS
self.cameraSelector = Widgets.CueSelectPulldown('Render Cameras', options=cameras)
self.selectorLayout = QtWidgets.QHBoxLayout()
self.setupUi()
self.setupConnections()

def setupUi(self):
"""Creates the Maya-specific widget layout."""
self.mainLayout.addWidget(self.mayaFileInput)
self.groupLayout.addWidget(self.mayaFileInput)
self.selectorLayout.addWidget(self.cameraSelector)
self.selectorLayout.addSpacerItem(Widgets.CueSpacerItem(Widgets.SpacerTypes.HORIZONTAL))
self.mainLayout.addLayout(self.selectorLayout)
self.groupLayout.addLayout(self.selectorLayout)

def setupConnections(self):
"""Sets up widget signals."""
self.mayaFileInput.lineEdit.textChanged.connect(self.dataChanged.emit) # pylint: disable=no-member
self.mayaFileInput.setFileBrowsable(fileFilter=self.fileFilters)

def setCommandData(self, commandData):
self.mayaFileInput.setText(commandData.get('mayaFile', ''))
Expand All @@ -82,17 +95,20 @@ class BaseMayaSettings(BaseSettingsWidget):
# pylint: disable=keyword-arg-before-vararg,unused-argument
def __init__(self, parent=None, *args, **kwargs):
super(BaseMayaSettings, self).__init__(parent=parent)
self.groupBox.setTitle('Maya options')
self.mayaFileInput = Widgets.CueLabelLineEdit('Maya File:')
self.fileFilters = Constants.MAYA_FILE_FILTERS
self.setupUi()
self.setupConnections()

def setupUi(self):
"""Creates the widget layout with a single input for the path to the Maya scene."""
self.mainLayout.addWidget(self.mayaFileInput)
self.groupLayout.addWidget(self.mayaFileInput)

def setupConnections(self):
"""Sets up widget signals."""
self.mayaFileInput.lineEdit.textChanged.connect(self.dataChanged.emit) # pylint: disable=no-member
self.mayaFileInput.setFileBrowsable(fileFilter=self.fileFilters)

def setCommandData(self, commandData):
self.mayaFileInput.setText(commandData.get('mayaFile', ''))
Expand All @@ -109,18 +125,26 @@ class InNukeSettings(BaseSettingsWidget):
# pylint: disable=keyword-arg-before-vararg,unused-argument
def __init__(self, writeNodes=None, filename=None, parent=None, *args, **kwargs):
super(InNukeSettings, self).__init__(parent=parent)
self.groupBox.setTitle('Nuke options')
self.fileInput = Widgets.CueLabelLineEdit('Nuke File:', filename)
self.fileFilters = Constants.NUKE_FILE_FILTERS
self.writeNodeSelector = Widgets.CueSelectPulldown('Write Nodes:', emptyText='[All]',
options=writeNodes)
self.selectorLayout = QtWidgets.QHBoxLayout()
self.setupUi()
self.setupConnections()

def setupUi(self):
"""Creates the Nuke-specific widget layout."""
self.mainLayout.addWidget(self.fileInput)
self.groupLayout.addWidget(self.fileInput)
self.selectorLayout.addWidget(self.writeNodeSelector)
self.selectorLayout.addSpacerItem(Widgets.CueSpacerItem(Widgets.SpacerTypes.HORIZONTAL))
self.mainLayout.addLayout(self.selectorLayout)
self.groupLayout.addLayout(self.selectorLayout)

def setupConnections(self):
"""Sets up widget signals."""
self.fileInput.lineEdit.textChanged.connect(self.dataChanged.emit) # pylint: disable=no-member
self.fileInput.setFileBrowsable(fileFilter=self.fileFilters)

def setCommandData(self, commandData):
self.fileInput.setText(commandData.get('nukeFile', ''))
Expand All @@ -139,17 +163,20 @@ class BaseNukeSettings(BaseSettingsWidget):
# pylint: disable=keyword-arg-before-vararg,unused-argument
def __init__(self, parent=None, *args, **kwargs):
super(BaseNukeSettings, self).__init__(parent=parent)
self.groupBox.setTitle('Nuke options')
self.fileInput = Widgets.CueLabelLineEdit('Nuke File:')
self.fileFilters = Constants.NUKE_FILE_FILTERS
self.setupUi()
self.setupConnections()

def setupUi(self):
"""Creates the widget layout with a single input for the path to the Nuke script."""
self.mainLayout.addWidget(self.fileInput)
self.groupLayout.addWidget(self.fileInput)

def setupConnections(self):
"""Sets up widget signals."""
self.fileInput.lineEdit.textChanged.connect(self.dataChanged.emit) # pylint: disable=no-member
self.fileInput.setFileBrowsable(fileFilter=self.fileFilters)

def setCommandData(self, commandData):
self.fileInput.setText(commandData.get('nukeFile', ''))
Expand All @@ -166,15 +193,15 @@ class ShellSettings(BaseSettingsWidget):
# pylint: disable=keyword-arg-before-vararg,unused-argument
def __init__(self, parent=None, *args, **kwargs):
super(ShellSettings, self).__init__(parent=parent)

self.groupBox.setTitle('Shell options')
self.commandTextBox = Command.CueCommandWidget()

self.setupUi()
self.setupConnections()

def setupUi(self):
"""Creates the widget layout with a single input for the shell command."""
self.mainLayout.addWidget(self.commandTextBox)
self.groupLayout.addWidget(self.commandTextBox)

def setupConnections(self):
"""Sets up widget signals."""
Expand All @@ -193,6 +220,8 @@ class BaseBlenderSettings(BaseSettingsWidget):
# pylint: disable=keyword-arg-before-vararg,unused-argument
def __init__(self, parent=None, *args, **kwargs):
super(BaseBlenderSettings, self).__init__(parent=parent)
self.groupBox.setTitle('Blender options')
self.fileFilters = Constants.BLENDER_FILE_FILTERS
self.fileInput = Widgets.CueLabelLineEdit('Blender File:')
self.outputPath = Widgets.CueLabelLineEdit(
'Output Path (Optional):',
Expand All @@ -207,8 +236,8 @@ def __init__(self, parent=None, *args, **kwargs):

def setupUi(self):
"""Creates the Blender-specific widget layout."""
self.mainLayout.addWidget(self.fileInput)
self.mainLayout.addLayout(self.outputLayout)
self.groupLayout.addWidget(self.fileInput)
self.groupLayout.addLayout(self.outputLayout)
self.outputLayout.addWidget(self.outputPath)
self.outputLayout.addWidget(self.outputSelector)

Expand All @@ -217,6 +246,8 @@ def setupConnections(self):
# pylint: disable=no-member
self.fileInput.lineEdit.textChanged.connect(self.dataChanged.emit)
self.outputPath.lineEdit.textChanged.connect(self.dataChanged.emit)
self.outputSelector.optionsMenu.triggered.connect(self.dataChanged.emit)
self.fileInput.setFileBrowsable(fileFilter=self.fileFilters)
# pylint: enable=no-member

def setCommandData(self, commandData):
Expand Down
17 changes: 17 additions & 0 deletions cuesubmit/cuesubmit/ui/Style.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,23 @@
}
"""

DISABLED_LINE_EDIT = """
QLineEdit {
color: rgb(110, 110, 110);
border: 0px solid;
background-color: rgb(30, 35, 40);
border-radius: 4px;
}
"""

GROUP_BOX = """
QGroupBox {
border: 3px solid rgb(30, 40, 50);
border-radius: 6px;
font-size: 8pt;
}
"""

SEPARATOR_LINE = 'border: 1px solid rgb(20, 30, 40)'

TEXT = 'background-color: rgb(40, 50, 60); color: rgb(250, 250, 250); font-weight: regular;'
Expand Down
16 changes: 14 additions & 2 deletions cuesubmit/cuesubmit/ui/Submit.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,8 @@ def __init__(
self.facilitySelector.setChecked(selected_facility)

self.settingsWidget = self.jobTypes.build(self.primaryWidgetType, *args, **kwargs)
self.commandFeedback = Widgets.CueLabelLineEdit( labelText='Final command:' )
self.commandFeedback.greyOut()
self.jobTreeWidget = Job.CueJobWidget()
self.submitButtons = CueSubmitButtons()
self.setupUi()
Expand Down Expand Up @@ -264,8 +266,6 @@ def setupUi(self):
self.scrollingLayout.addSpacerItem(Widgets.CueSpacerItem(Widgets.SpacerTypes.VERTICAL))
self.scrollingLayout.addWidget(Widgets.CueLabelLine('Layer Info'))
self.layerInfoLayout.addWidget(self.layerNameInput)
self.settingsLayout.addWidget(self.settingsWidget)
self.layerInfoLayout.addLayout(self.settingsLayout)
self.layerInfoLayout.addSpacerItem(Widgets.CueSpacerItem(Widgets.SpacerTypes.VERTICAL))
self.layerInfoLayout.addWidget(self.frameBox)

Expand All @@ -280,8 +280,12 @@ def setupUi(self):
self.coresLayout.addWidget(self.dependSelector)
self.coresLayout.addSpacerItem(Widgets.CueSpacerItem(Widgets.SpacerTypes.HORIZONTAL))
self.layerInfoLayout.addLayout(self.coresLayout)
self.layerInfoLayout.addWidget(self.commandFeedback)
self.scrollingLayout.addLayout(self.layerInfoLayout)

self.settingsLayout.addWidget(self.settingsWidget)
self.layerInfoLayout.addLayout(self.settingsLayout)

self.scrollingLayout.addSpacerItem(Widgets.CueSpacerItem(Widgets.SpacerTypes.VERTICAL))
self.scrollingLayout.addWidget(Widgets.CueLabelLine('Submission Details'))

Expand Down Expand Up @@ -329,6 +333,7 @@ def jobLayerSelectionChanged(self, layerObject):
self.dependSelector.clearChecked()
self.dependSelector.setChecked([layerObject.dependType])
self.settingsWidget.setCommandData(layerObject.cmd)
self.updateFeedbackCommand(layerObject)
self.skipDataChangedEvent = False

def jobDataChanged(self):
Expand All @@ -351,6 +356,13 @@ def jobDataChanged(self):
dependsOn=None
)
self.jobTreeWidget.updateJobData(self.jobNameInput.text())
self.updateFeedbackCommand(self.jobTreeWidget.currentLayerData)

def updateFeedbackCommand(self, layerData):
""" Builds the final command for this layer and displays it in the feedback widget """
command = Submission.buildLayerCommand(layerData=layerData,
silent=True)
self.commandFeedback.setText(text=command)

def jobTypeChanged(self):
"""Action when the job type is changed."""
Expand Down
Loading

0 comments on commit e17163a

Please sign in to comment.