diff --git a/cuesubmit/cuesubmit/Constants.py b/cuesubmit/cuesubmit/Constants.py index 3aeae767d..89bbbea51 100644 --- a/cuesubmit/cuesubmit/Constants.py +++ b/cuesubmit/cuesubmit/Constants.py @@ -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 = \ diff --git a/cuesubmit/cuesubmit/Submission.py b/cuesubmit/cuesubmit/Submission.py index f1083b23d..dc5e64d0f 100644 --- a/cuesubmit/cuesubmit/Submission.py +++ b/cuesubmit/cuesubmit/Submission.py @@ -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) @@ -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( @@ -110,29 +112,22 @@ 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.""" @@ -140,16 +135,8 @@ def submitJob(jobData): 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 diff --git a/cuesubmit/cuesubmit/ui/SettingsWidgets.py b/cuesubmit/cuesubmit/ui/SettingsWidgets.py index 612e67193..d3062c594 100644 --- a/cuesubmit/cuesubmit/ui/SettingsWidgets.py +++ b/cuesubmit/cuesubmit/ui/SettingsWidgets.py @@ -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) @@ -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', '')) @@ -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', '')) @@ -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', '')) @@ -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', '')) @@ -166,7 +193,7 @@ 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() @@ -174,7 +201,7 @@ def __init__(self, parent=None, *args, **kwargs): 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.""" @@ -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):', @@ -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) @@ -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): diff --git a/cuesubmit/cuesubmit/ui/Style.py b/cuesubmit/cuesubmit/ui/Style.py index 0679e6aa4..e3d527562 100644 --- a/cuesubmit/cuesubmit/ui/Style.py +++ b/cuesubmit/cuesubmit/ui/Style.py @@ -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;' diff --git a/cuesubmit/cuesubmit/ui/Submit.py b/cuesubmit/cuesubmit/ui/Submit.py index 5f0b42f5b..cd2cbc39f 100644 --- a/cuesubmit/cuesubmit/ui/Submit.py +++ b/cuesubmit/cuesubmit/ui/Submit.py @@ -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() @@ -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) @@ -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')) @@ -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): @@ -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.""" diff --git a/cuesubmit/cuesubmit/ui/Widgets.py b/cuesubmit/cuesubmit/ui/Widgets.py index 0be3ca359..fb16400a9 100644 --- a/cuesubmit/cuesubmit/ui/Widgets.py +++ b/cuesubmit/cuesubmit/ui/Widgets.py @@ -21,6 +21,8 @@ from __future__ import absolute_import from builtins import object +from functools import partial + from qtpy import QtCore, QtGui, QtWidgets from cuesubmit import Constants @@ -53,8 +55,10 @@ def __init__(self, labelText=None, defaultText='', tooltip=None, validators=None self.label.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) self.lineEdit = CueLineEdit(defaultText, completerStrings=completers) self.lineEdit.setToolTip(tooltip) + self.browseButton = QtWidgets.QPushButton(text='Browse') self.horizontalLine = CueHLine() self.validators = validators or [] + self.setter = self.setText self.setupUi() self.setupConnections() self.setAutoFillBackground(True) @@ -64,6 +68,7 @@ def setupUi(self): self.setLayout(self.mainLayout) self.mainLayout.addWidget(self.label, 0, 0, 1, 1) self.mainLayout.addWidget(self.lineEdit, 1, 0, 1, 4) + self.browseButton.setVisible(False) self.mainLayout.addWidget(self.horizontalLine, 2, 0, 1, 4) self.label.setStyleSheet(Style.LABEL_TEXT) @@ -74,6 +79,34 @@ def setupConnections(self): self.lineEdit.focusChange.connect(self.textFocusChange) # pylint: enable=no-member + def setFileBrowsable(self, fileFilter=None): + """ Displays the Browse button and hook it to a fileBrowser with optional file filters + + :param fileFilter: single or multiple file filters (ex: 'Maya Ascii File (*.ma)') + :type fileFilter: str or list + """ + self._showBrowseButton() + if isinstance(fileFilter, (list, tuple)): + fileFilter = ';;'.join(fileFilter) + # pylint: disable=no-member + self.browseButton.clicked.connect(partial(_setBrowseFileText, + widget_setter=self.setter, + fileFilter=fileFilter)) + + def setFolderBrowsable(self): + """ Displays the Browse button and hook it to a folderBrowser """ + self._showBrowseButton() + # pylint: disable=no-member + self.browseButton.clicked.connect(partial(_setBrowseFolderText, + widget_setter=self.setter)) + + def _showBrowseButton(self): + """ Re-layout lineEdit and browse button and display it """ + self.mainLayout.removeWidget(self.lineEdit) + self.mainLayout.addWidget(self.lineEdit, 1, 0, 1, 3) + self.mainLayout.addWidget(self.browseButton, 1, 3, 1, 1) + self.browseButton.setVisible(True) + def setText(self, text): """Set the text to the given value. @type text: str @@ -104,6 +137,11 @@ def text(self): """ return self.lineEdit.text() + def greyOut(self): + """Make widget grey and read-only""" + self.lineEdit.setReadOnly(True) + self.lineEdit.setStyleSheet(Style.DISABLED_LINE_EDIT) + class CueLineEdit(QtWidgets.QLineEdit): """Wrapper around QLineEdit that allows for changing text with up/down arrow keys.""" @@ -357,6 +395,7 @@ def setValue(self, value): """ self.slider.setValue(value*self.float_mult) + class CueLabelToggle(QtWidgets.QWidget): """Container widget that holds a label and a toggle.""" @@ -529,6 +568,45 @@ def separatorLine(): line.setStyleSheet(Style.SEPARATOR_LINE) return line +def getFile(fileFilter=None): + """ Opens a file browser and returns the result + :param fileFilter: optional filters (ex: "Maya Ascii File (*.ma);; + Maya Binary File (*.mb);;Maya Files (*.ma *.mb)") + :type fileFilter: str + :returns: Name of the file + :rtype: str + """ + filename, _ = QtWidgets.QFileDialog.getOpenFileName(caption='Select file', + dir='.', filter=fileFilter) + return filename + +def getFolder(): + """ Opens a folder browser and returns the result + :returns: Name of the folder + :rtype: str + """ + folder = QtWidgets.QFileDialog.getExistingDirectory(caption='Select folder', + dir='.', filter='') + return folder + +def _setBrowseFileText(widget_setter, fileFilter): + """ wrapper function to open a fileBrowser and set its result back in the widget + :param widget_setter: widget's function to set its text + :type widget_setter: function + :param fileFilter: optional filters (ex: "Maya Ascii File (*.ma);; + Maya Binary File (*.mb);;Maya Files (*.ma *.mb)") + :type fileFilter: str + """ + result = getFile(fileFilter) + widget_setter(result) + +def _setBrowseFolderText(widget_setter): + """ wrapper function to open a folderBrowser and set its result back in the widget + :param widget_setter: widget's function to set its text + :type widget_setter: function + """ + result = getFolder() + widget_setter(result) class CueMessageBox(QtWidgets.QMessageBox): """A QMessageBox with message and OK button.""" diff --git a/cuesubmit/tests/Submission_tests.py b/cuesubmit/tests/Submission_tests.py index e2219070d..38e10bcab 100644 --- a/cuesubmit/tests/Submission_tests.py +++ b/cuesubmit/tests/Submission_tests.py @@ -99,7 +99,7 @@ def testSubmitMayaJob(self, launchMock): self.assertEqual(MAYA_LAYER_DATA['name'], layer.get_name()) self.assertEqual( [ - 'Render', '-r', 'file', '-s', '#IFRAME#', '-e', '#IFRAME#', '-cam', + 'Render', '-r', 'file', '-s', '#FRAME_START#', '-e', '#FRAME_END#', '-cam', MAYA_LAYER_DATA['cmd']['camera'], MAYA_LAYER_DATA['cmd']['mayaFile'] ], layer.get_arg('command') @@ -198,7 +198,7 @@ def testSubmitMayaAndShellJob(self, launchMock): self.assertEqual(MAYA_LAYER_DATA['name'], mayaLayer.get_name()) self.assertEqual( [ - 'Render', '-r', 'file', '-s', '#IFRAME#', '-e', '#IFRAME#', '-cam', + 'Render', '-r', 'file', '-s', '#FRAME_START#', '-e', '#FRAME_END#', '-cam', MAYA_LAYER_DATA['cmd']['camera'], MAYA_LAYER_DATA['cmd']['mayaFile'] ], mayaLayer.get_arg('command')