import pymel.core as pm import os import json import inspect from PySide2 import QtWidgets, QtCore import maya.OpenMayaUI as omui from shiboken2 import wrapInstance from maya.app.general.mayaMixin import MayaQWidgetDockableMixin import onionSkinRenderer.onionSkinRendererCore as onionCore import onionSkinRenderer.onionSkinRendererWidget as onionWidget import onionSkinRenderer.onionSkinRendererFrameWidget as onionFrame import onionSkinRenderer.onionSkinRendererObjectWidget as onionObject import onionSkinRenderer.onionSkinRendererPreferences as onionPrefs ''' 2017 and 2018 Version using pyside2 ''' kDebugAll = False # wrapper to get mayas main window def getMayaMainWindow(): mayaPtr = omui.MQtUtil.mainWindow() return wrapInstance(long(mayaPtr), QtWidgets.QWidget) onionUI = None def openOnionSkinRenderer(develop = False, dockable = False): if develop: reload(onionFrame) reload(onionWidget) reload(onionCore) reload(onionObject) reload(onionPrefs) #if __name__ == "__main__": try: onionUI.close() except: pass onionUI = OnionSkinRendererWindow() onionUI.show(dockable = dockable) ''' ONION SKIN RENDERER MAIN UI This class creates connections between UI and CORE ''' class OnionSkinRendererWindow(MayaQWidgetDockableMixin, QtWidgets.QMainWindow, onionWidget.Ui_onionSkinRenderer): # def __init__(self, parent = getMayaMainWindow()): super(OnionSkinRendererWindow, self).__init__(parent) # the dockable feature creates this control that needs to be deleted manually # otherwise it throws an error that this name already exists self.deleteControl('onionSkinRendererWorkspaceControl') # This registers the override in maya # I previously had it as plugin, but this made it impossible to get # the viewRenderOverrideInstance (sth to do with python namespaces i guess) # so i just call init myself. # It feels a bit hacky, but it works anyway onionCore.initializeOverride() # member variables self.mOnionObjectSet = set() self.mAbsoluteOnionSet = set() self.mPrefs = {} self.mRelativeFrameAmount = 8 self.mToolPath = os.path.dirname(os.path.abspath(inspect.stack()[0][1])) self.mActiveEditor = None # create the ui from the compiled qt designer file self.setupUi(self) self.setAttribute(QtCore.Qt.WA_DeleteOnClose) self.createConnections() # load settings from the settings file self.loadSettings() # def closeEvent(self, event): # when the UI is closed, deactivate the override if kDebugAll: print 'close event start' self.saveSettings() onionCore.uninitializeOverride() if kDebugAll: print 'close event end' # special event for the dockable feature def dockCloseEventTriggered(self, event): if kDebugAll: print 'dock close event start' try: self.saveSettings() except Exception as e: print e onionCore.uninitializeOverride() if kDebugAll: print 'dock close event end' # code from https://gist.github.com/liorbenhorin/217bfb7e54c6f75b9b1b2b3d73a1a43a def deleteControl(self, control): if kDebugAll: print 'delete Control' if pm.workspaceControl(control, q=True, exists=True): pm.workspaceControl(control, e=True, close=True) pm.deleteUI(control, control=True) # def createConnections(self): self.onionObjects_add_btn.clicked.connect(self.addSelectedObjects) self.onionObjects_remove_btn.clicked.connect(self.removeSelectedObjects) self.onionObjects_clear_btn.clicked.connect(self.clearOnionObjects) self.toggleRenderer_btn.clicked.connect(self.toggleRenderer) self.globalOpacity_slider.sliderMoved.connect(self.setGlobalOpacity) self.onionType_cBox.currentTextChanged.connect(self.setOnionType) self.drawBehind_chkBx.stateChanged.connect(self.setDrawBehind) self.tint_type_cBox.currentTextChanged.connect(self.setTintType) self.relative_futureTint_btn.clicked.connect(self.pickColor) self.relative_pastTint_btn.clicked.connect(self.pickColor) self.relative_tint_strength_slider.sliderMoved.connect(self.setTintStrength) self.relative_keyframes_chkbx.clicked.connect(self.toggleRelativeKeyframeDisplay) self.relative_step_spinBox.valueChanged.connect(self.setRelativeStep) self.absolute_tint_btn.clicked.connect(self.pickColor) self.absolute_addCrnt_btn.clicked.connect(self.addAbsoluteFrame) self.absolute_add_btn.clicked.connect(self.addAbsoluteFrameFromSpinbox) self.absolute_clear_btn.clicked.connect(self.clearAbsoluteFrames) self.settings_clearBuffer.triggered.connect(self.clearBuffer) self.settings_autoClearBuffer.triggered.connect(self.setAutoClearBuffer) self.settings_preferences.triggered.connect(self.changePrefs) self.settings_saveSettings.triggered.connect(self.saveSettings) self.onionObjects_grp.clicked.connect(self.toggleGroupBox) self.onionSkinFrames_grp.clicked.connect(self.toggleGroupBox) self.onionSkinSettings_grp.clicked.connect(self.toggleGroupBox) # ------------------ # UI REFRESH # def refreshObjectList(self): self.onionObjects_list.clear() for obj in self.mOnionObjectSet: listWidget = OnionListObject() listWidget.object_label.setText(obj.nodeName()) listWidget.object_remove_btn.clicked.connect(lambda b_obj = obj: self.removeOnionObject(b_obj)) listItem = QtWidgets.QListWidgetItem() listItem.setSizeHint(listWidget.sizeHint()) self.onionObjects_list.addItem(listItem) self.onionObjects_list.setItemWidget(listItem, listWidget) # def refreshRelativeFrame(self): activeFrames = [] # clear the frame of all widgets first for child in self.relative_frame.findChildren(OnionListFrame): if child.frame_visibility_btn.isChecked(): activeFrames.append(int(child.frame_number.text())) child.setParent(None) # fill the relative frames list for index in range(self.mRelativeFrameAmount + 1): if not index-self.mRelativeFrameAmount/2 == 0: listWidget = OnionListFrame() frame = index-self.mRelativeFrameAmount/2 listWidget.frame_number.setText(str(frame)) listWidget.frame_opacity_slider.setValue(75/abs(index-self.mRelativeFrameAmount/2)) listWidget.frame_visibility_btn.toggled.connect(self.toggleRelativeFrame) if frame in activeFrames: listWidget.frame_visibility_btn.setChecked(True) activeFrames.remove(frame) listWidget.frame_opacity_slider.sliderMoved.connect(self.setRelativeOpacity) self.relative_frame_layout.addWidget(listWidget) # remove all remaining frames from onion skin renderer # since their visibility is no longer accesible from ui for frame in activeFrames: onionCore.viewRenderOverrideInstance.removeRelativeOnion(frame) # def refreshAbsoluteList(self): # remove any entries that don't exist anymore framesInList = [] for i in reversed(xrange(self.absolute_list.count())): frame = self.absolute_list.item(i).data(QtCore.Qt.UserRole) framesInList.append(frame) if frame not in self.mAbsoluteOnionSet: self.absolute_list.takeItem(i) # add any missing entry for frame in self.mAbsoluteOnionSet: if frame not in framesInList: listWidget = OnionListFrame() listWidget.frame_number.setText(str(int(frame))) listWidget.frame_opacity_slider.setValue(onionCore.viewRenderOverrideInstance.getAbsoluteOpacity(int(frame))) listWidget.addRemoveButton() listWidget.frame_visibility_btn.setChecked(onionCore.viewRenderOverrideInstance.absoluteOnionExists(int(frame))) listWidget.frame_remove_btn.clicked.connect(lambda b_frame = frame: self.removeAbsoluteFrame(b_frame)) listWidget.frame_visibility_btn.toggled.connect(self.toggleAbsoluteFrame) listWidget.frame_opacity_slider.sliderMoved.connect(self.setAbsoluteOpacity) listItem = QtWidgets.QListWidgetItem() listItem.setData(QtCore.Qt.UserRole, int(frame)) listItem.setSizeHint(listWidget.sizeHint()) # insert item at correct position correctRow = 0 for i in xrange(self.absolute_list.count()): if frame < self.absolute_list.item(i).data(QtCore.Qt.UserRole): break correctRow = i+1 self.absolute_list.insertItem(correctRow, listItem) self.absolute_list.setItemWidget(listItem, listWidget) # --------------------------- # CONNECTIONS # def addSelectedObjects(self): onionCore.viewRenderOverrideInstance.addSelectedOnion() for obj in pm.selected(): self.mOnionObjectSet.add(obj) self.refreshObjectList() # def removeSelectedObjects(self): onionCore.viewRenderOverrideInstance.removeSelectedOnion() for obj in pm.selected(): if obj in self.mOnionObjectSet: self.mOnionObjectSet.remove(obj) self.refreshObjectList() # def removeOnionObject(self, obj): try: onionCore.viewRenderOverrideInstance.removeOnionObject(obj.fullPath()) except: onionCore.viewRenderOverrideInstance.removeOnionObject(obj.nodeName()) self.mOnionObjectSet.remove(obj) self.refreshObjectList() # def clearOnionObjects(self): onionCore.viewRenderOverrideInstance.clearOnionObjects() self.mOnionObjectSet.clear() self.refreshObjectList() # def toggleRelativeFrame(self): sender = self.sender() frame = sender.parent().findChild(QtWidgets.QLabel, 'frame_number').text() sliderValue = sender.parent().findChild(QtWidgets.QSlider, 'frame_opacity_slider').value() if sender.isChecked(): onionCore.viewRenderOverrideInstance.addRelativeOnion(frame, sliderValue) else: onionCore.viewRenderOverrideInstance.removeRelativeOnion(frame) # def toggleRelativeKeyframeDisplay(self): sender = self.sender() onionCore.viewRenderOverrideInstance.setRelativeKeyDisplay(self.sender().isChecked()) self.saveSettings() # def addAbsoluteFrame(self, **kwargs): frame = kwargs.setdefault('frame', pm.animation.getCurrentTime()) if int(frame) not in self.mAbsoluteOnionSet: onionCore.viewRenderOverrideInstance.addAbsoluteOnion(frame, 50) self.mAbsoluteOnionSet.add(frame) self.refreshAbsoluteList() # def addAbsoluteFrameFromSpinbox(self): frame = self.sender().parent().findChild(QtWidgets.QSpinBox, 'absolute_add_spinBox').value() self.addAbsoluteFrame(frame = frame) # def toggleAbsoluteFrame(self): sender = self.sender() frame = sender.parent().findChild(QtWidgets.QLabel, 'frame_number').text() sliderValue = sender.parent().findChild(QtWidgets.QSlider, 'frame_opacity_slider').value() if sender.isChecked(): onionCore.viewRenderOverrideInstance.addAbsoluteOnion(frame, sliderValue) else: onionCore.viewRenderOverrideInstance.removeAbsoluteOnion(frame) # def removeAbsoluteFrame(self, frame): onionCore.viewRenderOverrideInstance.removeAbsoluteOnion(frame) self.mAbsoluteOnionSet.remove(frame) self.refreshAbsoluteList() # def clearAbsoluteFrames(self): onionCore.viewRenderOverrideInstance.clearAbsoluteOnions() self.mAbsoluteOnionSet.clear() self.refreshAbsoluteList() # def clearBuffer(self): onionCore.viewRenderOverrideInstance.rotOnions() # def pickColor(self): color = QtWidgets.QColorDialog.getColor() if color.isValid(): self.setOnionColor(self.sender(), color.getRgb()) self.saveSettings() # def setRelativeOpacity(self): opacity = self.sender().value() frame = self.sender().parent().findChild(QtWidgets.QLabel, 'frame_number').text() onionCore.viewRenderOverrideInstance.setRelativeOpacity(frame, opacity) # def setAbsoluteOpacity(self): opacity = self.sender().value() frame = self.sender().parent().findChild(QtWidgets.QLabel, 'frame_number').text() onionCore.viewRenderOverrideInstance.setAbsoluteOpacity(int(frame), opacity) # def setTintStrength(self): onionCore.viewRenderOverrideInstance.setTintStrength( self.sender().value() ) # def setAutoClearBuffer(self): value = self.sender().isChecked() onionCore.viewRenderOverrideInstance.setAutoClearBuffer(value) # def changePrefs(self): prefUi = OnionPreferences(self) if prefUi.exec_(): values = prefUi.getValues() onionCore.viewRenderOverrideInstance.setMaxBuffer(values['maxBuffer']) onionCore.viewRenderOverrideInstance.setOutlineWidth(values['outlineWidth']) onionCore.viewRenderOverrideInstance.setTintSeed(values['tintSeed']) self.mRelativeFrameAmount = values['relativeKeyCount']*2 self.refreshRelativeFrame() self.saveSettings() # def setRelativeStep(self): onionCore.viewRenderOverrideInstance.setRelativeStep(self.sender().value()) self.saveSettings() # togle active or saved editor between onion Skin Renderer and vp2 def toggleRenderer(self): modelPanelList = [] modelEditorList = pm.lsUI(editors=True) # find all model panels for myModelPanel in modelEditorList: if myModelPanel.find('modelPanel') != -1: modelPanelList.append(myModelPanel) onionPanel = None # if any of those is already set to onion skin renderer for modelPanel in modelPanelList: if pm.uitypes.ModelEditor(modelPanel).getRendererOverrideName() == 'onionSkinRenderer': onionPanel = pm.uitypes.ModelEditor(modelPanel) break # if there is a panel with the onion skin renderer # deactivate it and save the panel if onionPanel: try: # Always better to try in the case of active panel operations # as the active panel might not be a viewport. onionPanel.setRendererOverrideName('') self.mActiveEditor = onionPanel except Exception as e: # Handle exception print e else: # if there is a saved editor panel activate the renderer on it if self.mActiveEditor: self.mActiveEditor.setRendererOverrideName('onionSkinRenderer') # else toggle the active one else: for modelPanel in modelPanelList: if pm.uitypes.ModelEditor(modelPanel).getActiveView(): try: if pm.uitypes.ModelEditor(modelPanel).getRendererOverrideName() == '': pm.uitypes.ModelEditor(modelPanel).setRendererOverrideName('onionSkinRenderer') else: pm.uitypes.ModelEditor(modelPanel).setRendererOverrideName('') except Exception as e: # Handle exception print e # def setGlobalOpacity(self): onionCore.viewRenderOverrideInstance.setGlobalOpacity(self.sender().value()) # def setOnionType(self): onionCore.viewRenderOverrideInstance.setOnionType(self.onionType_cBox.currentIndex()) # def setDrawBehind(self): onionCore.viewRenderOverrideInstance.setDrawBehind(self.drawBehind_chkBx.isChecked()) # def toggleGroupBox(self): h = self.sender().maximumHeight() if h > 100000: self.sender().setMaximumHeight(14) else: self.sender().setMaximumHeight(200000) # def setTintType(self): tintType = self.tint_type_cBox.currentIndex() if tintType == 0: self.constant_col_widget.setMaximumHeight(16777215) self.constant_col_widget.setEnabled(True) else: self.constant_col_widget.setMaximumHeight(0) self.constant_col_widget.setEnabled(False) onionCore.viewRenderOverrideInstance.setTintType(tintType) # UTILITY # def setOnionColor(self, btn, rgba): btn.setStyleSheet('background-color: rgb(%s,%s,%s);'%(rgba[0], rgba[1], rgba[2])) onionCore.viewRenderOverrideInstance.setTint(rgba, btn.objectName()) # def loadSettings(self): with open(os.path.join(self.mToolPath,'settings.txt')) as json_file: self.mPrefs = json.load(json_file) self.settings_autoClearBuffer.setChecked(self.mPrefs.setdefault('autoClearBuffer',True)) onionCore.viewRenderOverrideInstance.setAutoClearBuffer(self.mPrefs.setdefault('autoClearBuffer',True)) self.relative_keyframes_chkbx.setChecked(self.mPrefs.setdefault('displayKeyframes',True)) onionCore.viewRenderOverrideInstance.setRelativeKeyDisplay(self.mPrefs.setdefault('displayKeyframes',True)) self.setOnionColor(self.relative_futureTint_btn, self.mPrefs.setdefault('rFutureTint',[0,0,125])) self.setOnionColor(self.relative_pastTint_btn, self.mPrefs.setdefault('rPastTint',[0,125,0])) self.setOnionColor(self.absolute_tint_btn, self.mPrefs.setdefault('aTint', [125,0,0])) onionCore.viewRenderOverrideInstance.setTintSeed(self.mPrefs.setdefault('tintSeed', 0)) self.tint_type_cBox.setCurrentIndex(self.mPrefs.setdefault('tintType',0)) self.onionType_cBox.setCurrentIndex(self.mPrefs.setdefault('onionType',1)) self.drawBehind_chkBx.setChecked(self.mPrefs.setdefault('drawBehind', True)) self.mRelativeFrameAmount = self.mPrefs.setdefault('relativeFrameAmount',4) self.refreshRelativeFrame() activeRelativeFrames = self.mPrefs.setdefault('activeRelativeFrames',[]) for child in self.relative_frame.findChildren(OnionListFrame): if int(child.frame_number.text()) in activeRelativeFrames: child.frame_visibility_btn.setChecked(True) self.relative_step_spinBox.setValue(self.mPrefs.setdefault('relativeStep', 1)) onionCore.viewRenderOverrideInstance.setMaxBuffer(self.mPrefs.setdefault('maxBufferSize', 200)) onionCore.viewRenderOverrideInstance.setOutlineWidth(self.mPrefs.setdefault('outlineWidth',3)) # save values into a json file def saveSettings(self): if kDebugAll: print 'start save' data = {} data['autoClearBuffer'] = self.settings_autoClearBuffer.isChecked() data['displayKeyframes'] = self.relative_keyframes_chkbx.isChecked() data['rFutureTint'] = self.extractRGBFromStylesheet(self.relative_futureTint_btn.styleSheet()) data['rPastTint'] = self.extractRGBFromStylesheet(self.relative_pastTint_btn.styleSheet()) data['aTint'] = self.extractRGBFromStylesheet(self.absolute_tint_btn.styleSheet()) data['tintSeed'] = onionCore.viewRenderOverrideInstance.getTintSeed() data['tintType'] = self.tint_type_cBox.currentIndex() data['relativeFrameAmount'] = self.mRelativeFrameAmount data['relativeStep'] = self.relative_step_spinBox.value() data['maxBufferSize'] = onionCore.viewRenderOverrideInstance.getMaxBuffer() data['outlineWidth'] = onionCore.viewRenderOverrideInstance.getOutlineWidth() data['onionType'] = self.onionType_cBox.currentIndex() data['drawBehind'] = self.drawBehind_chkBx.isChecked() data['activeRelativeFrames'] = self.getActiveRelativeFrameIndices() with open(os.path.join(self.mToolPath,'settings.txt'), 'w') as outfile: json.dump(data, outfile) if kDebugAll: print 'end save' # def extractRGBFromStylesheet(self, s): return map(int,(s[s.find("(")+1:s.find(")")]).split(',')) def getActiveRelativeFrameIndices(self): activeFrames = [] # clear the frame of all widgets first for child in self.relative_frame.findChildren(OnionListFrame): if child.frame_visibility_btn.isChecked(): activeFrames.append(int(child.frame_number.text())) return activeFrames ''' FRAME WIDGET the widget for displaying a frame in a list. includes visibility, opacity slider and on demand a remove button ''' class OnionListFrame(QtWidgets.QWidget, onionFrame.Ui_onionSkinFrame_layout): def __init__(self, parent = getMayaMainWindow()): super(OnionListFrame, self).__init__(parent) self.setupUi(self) def addRemoveButton(self): self.frame_remove_btn = QtWidgets.QPushButton('rm') sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.frame_remove_btn.sizePolicy().hasHeightForWidth()) self.frame_remove_btn.setSizePolicy(sizePolicy) self.frame_remove_btn.setMinimumSize(QtCore.QSize(16, 16)) self.frame_remove_btn.setMaximumSize(QtCore.QSize(16, 16)) self.frame_widget_layout.addWidget(self.frame_remove_btn) ''' OBJECT WIDGET the widget for displaying an object in a list ''' class OnionListObject(QtWidgets.QWidget, onionObject.Ui_onionSkinObject_layout): def __init__(self, parent = getMayaMainWindow()): super(OnionListObject, self).__init__(parent) self.setupUi(self) ''' Settings Dialog in this window the user can set some preferences ''' class OnionPreferences(QtWidgets.QDialog, onionPrefs.Ui_onionSkinRendererPreferences): def __init__(self, parent): super(OnionPreferences, self).__init__(parent) self.setupUi(self) self.relativeKeyCount_spinBox.setValue(parent.mRelativeFrameAmount/2) self.maxBuffer_spinBox.setValue(onionCore.viewRenderOverrideInstance.getMaxBuffer()) self.outlineWidth_spinBox.setValue(onionCore.viewRenderOverrideInstance.getOutlineWidth()) self.tintSeed_spinBox.setValue(onionCore.viewRenderOverrideInstance.getTintSeed()) def getValues(self): values = {} values['maxBuffer'] = self.maxBuffer_spinBox.value() values['relativeKeyCount'] = self.relativeKeyCount_spinBox.value() values['outlineWidth'] = self.outlineWidth_spinBox.value() values['tintSeed'] = self.tintSeed_spinBox.value() return values