RV - STUDIO WIDE CUSTOMIZATION
Tweak software, Shotgun software and Autodesk. Lot of things have happened, but RV is still one of my favourite softwares, and it have been for the past 10+ years. When it comes to customization you have a lot of possibilities with Python and RV's Mu language. The Mu API documentation is only available inside RV and unfortionatly we don't have a Python API documentation (yet)... but it is pretty similar to Mu's :) Below are some notes by our pipeline TD; Emil Gunnarsson that hopefully can be usefull and save some time for someone. At the bottom he lists some useful links.
STUDIO WIDE CONFIGURATION
Environment Variables:
At Dupp we use RV_PREFS_CLOBBER_PATH and RV_SUPPORT_PATH and they are pointing to the same network directory.
- RV_PREFS_CLOBBER_PATH
Overrides all user preferenses and only uses studio wide preferences. (RV.ini on Windows) - RV_PREFS_OVERRIDE_PATH
Used to set default preferences, but users can make local overrides. (RV.ini on Windows) - RV_SUPPORT_PATH
Sets RV's script path. This is where you place studio wide stuff. Typical folder structure is shown on the right.
CREATING A SIMPLE PACKAGE
- To import
rv
in python you should set thePYTONPATH=C:\Program Files\Shotgun\RV-2021.0.0\plugins\Python
environment variable on your machine. - Start by creating a python file, for example myMenu.py
- The following code will create a menu and 2 menuitems, and add some functionality to the buttons.
from rv.rvtypes import *
# These two are only available in the RV Runtime Environment
from rv.commands import *
from rv.extra_commands import *
class DuppMenuMode(MinorMode):
def __init__(self):
MinorMode.__init__(self)
# Edit the existing list menu, and add a new dropdown to it, with 2 items
self.init(
"py-duppmenu-mode",
None,
None,
[
# Menu name
# NOTE: If it already exists it will merge with existing, and add submenus / menuitems to the existing one
("-= DUPP =-",
[
# Menuitem name, actionHook (event), key, stateHook
("Print to console", self.printToConsole, None, None),
("Show console", self.showConsole, None, None)
]
)
]
)
# Open the console if it is not already open
if not isConsoleVisible():
showConsole()
def printToConsole(self, event):
print("Printing information to the console: ")
print(getCurrentAttributes())
def showConsole(self, event):
print("Showing console!")
if not isConsoleVisible():
showConsole()
def createMode():
# This function will automatically be called by RV
# to initialize the Class
return DuppMenuMode()
- Next you have to create a PACKAGE file, this being a file named PACKAGE with no extension at all. Below is a basic template for a PACKAGE file. I specified 3.6 as the RV version because it's an older version and should work with most newer ones.
package: DuppMenu
author: Emil Gunnarsson
contact: This email address is being protected from spambots. You need JavaScript enabled to view it.
version: 1.0
rv: 3.6
requires: ''
modes:
- file: myMenu
load: immediate
description: A DuppMenu that allows you to communincate with the Ingrid Python API
- Create the package, to do so you have to select both of the files you've created and zip them, name it something like duppmenu-1.0.rvpkg. It's important to add the version number
-1.0
and the extension.rvpkg
in order for it to show up when you want to add it to RV. - Now you can open RV and go to RV > Preferences > Packages > Add Packages..
Open your .rvpkg file. - Restart RV and you should be able to see your menu.
FUNCTIONS TO KNOW ABOUT
- commands
- commands.getCurrentAttributes() Gives some generic information about the current input, filename & path, timecode, colorcode, etc. It can be helpful to convert this Tuple of Tuples into a Dictonary using something like attributes = dict(commands.getCurrentAttributes())
- commands.readSetting() Reading settings
- commands.writeSetting(string group, string name, SettingsValue value) Writing settings
- commands.isConsoleVisible() Returns a boolean which lets you know if the console is visible or not
- commands.showConsole() Shows the RV Python console
- comands.addSource() Add a source to the timeline
- commands.setSourceMedia() Add an array of sources to the timeline
- commands.relocateSource() Switch the current source with a new one
- commands.sources() Get a list of current sources in the timeline
- commands.bind() Bind a function to an event
- extra_commands
- extra_commands.displayFeedback() Print some feedback in the upper left corner
- rv.qtutils
- rv.qtutils.sessionWindow() Gets the RV MainWindow frame as PySide object, so that you can change the window title, dock widgets, etc.
GETTING THE COLORSPACE FOR THE FIRST SOURCE
def getColorSpace(self):
colorspace = "linear"
# Return Nuke-style colorspaces
# Property types: 1 = float, 2 = int, 8 = string
# int, int, int, float
# Gives a list of 1 element so we use [0] to get the first one (the value)
# We are getting the values for logtype, sRGB2Linear, Rec709ToLinear & Gamma
nLogtype = commands.getIntProperty("sourceGroup000000_tolinPipeline_0.color.logtype")[0]
nRGB2Lin = commands.getIntProperty("sourceGroup000000_tolinPipeline_0.color.sRGB2linear")[0]
nRec2Lin = commands.getIntProperty("sourceGroup000000_tolinPipeline_0.color.Rec709ToLinear")[0]
fGamma22 = round(commands.getFloatProperty("sourceGroup000000_tolinPipeline_0.color.fileGamma")[0], 1)
if fGamma22 == 2.2:
colorspace = "Gamma2.2"
elif nLogtype == 1:
colorspace = "Cineon"
elif nLogtype == 2:
colorspace = "ViperLog"
elif nLogtype == 3 or nLogtype == 4:
colorspace = "AlexaV3LogC"
elif nLogtype == 6 or nLogtype == 7:
colorspace = "REDLog"
elif nLogtype == 0 and nRGB2Lin == 1:
colorspace = "sRGB"
elif nLogtype == 0 and nRec2Lin == 1:
colorspace = "rec709"
return colorspace
SIMPLE MESSAGE BOX
def displayMessageBox(self, title, message, messageType = QMessageBox.Ok):
# Create a simple message dialog
msg = QMessageBox()
msg.setIcon(QMessageBox.Information)
msg.setText(message)
msg.setWindowTitle(title)
msg.setStandardButtons(messageType)
msg.exec_()
# # Additional useful methods
# msg.setInformativeText("This is additional information")
# msg.setDetailedText("The details are as follows:")
# msg.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel)
# QMessageBox.Ok 0x00000400
# QMessageBox.Open 0x00002000
# QMessageBox.Save 0x00000800
# QMessageBox.Cancel 0x00400000
# QMessageBox.Close 0x00200000
# QMessageBox.Yes 0x00004000
# QMessageBox.No 0x00010000
# QMessageBox.Abort 0x00040000
# QMessageBox.Retry 0x00080000
# QMessageBox.Ignore 0x00100000
# # The method should take an index for the buttons
# # EXAMPLE: def buttonClicked(self, index):
# msg.buttonClicked.connect(self.buttonClicked)
# # Get return value from the button clicked
# retval = msg.exec_()
# print("value of pressed message box button: ", retval)
BINDING FUNCTIONS TO EVENTS
# new-source, graph-state-change, after-progressive-loading, media-relocated
commands.bind("default", "global", "media-relocated", self.mediaRelocated, "Doc string")
commands.bind("default", "global", "graph-state-change", self.testEvent, "Doc string")
# Events
def mediaRelocated(self, event):
self.hasBeenRelocated = True
print("Media relocated...")
def testEvent(self, event):
print("another event")
PRINTING NODE INFORMATION
nodeTypes = ['RVSequence', 'RVStack', 'RVSwitch']
for nodeType in nodeTypes:
print("INFO: Checking nodes of type \""+nodeType+"\"")
nodes = rv.commands.nodesOfType(nodeType)
counter = 0
for node in nodes:
props = commands.properties(node)
length = len(nodes)
for prop in props:
propertyType = commands.propertyInfo(prop)['type']
# I know there is some type called "halfproperty" but i havent come across it yet
if propertyType == 1:
print("Float property value ("+ prop +"): ", commands.getFloatProperty(prop))
elif propertyType == 2:
print("Integer property value: ("+ prop +")", commands.getIntProperty(prop))
elif propertyType == 8:
print("String property value: ("+ prop +")", commands.getStringProperty(prop))
else:
print("PROPERTY TYPE: ", propertyType)
if length > 1 and counter != (length - 1):
print("------------------------------------------------------------------------------------------")
counter += 1
print("=============================================")
DOCKED WIDGET IN RV
import sys
sys.path.append("L:\\_DUPP_PIPE\\app_launcher\\DuppAppLauncher\\Code")
from rv.qtutils import *
from rv.rvtypes import *
import duppmenu
# These two are only available during the RV Runtime Environment
from rv.commands import *
from rv.extra_commands import *
from PySide.QtGui import *
from PySide.QtCore import *
from PySide.QtUiTools import QUiLoader
from Ingrid import IngridAPI
class DuppMenuMode(MinorMode):
def __init__(self):
MinorMode.__init__(self)
self.init("py-duppmenu-mode", None, None, [("-= DUPP =-",[("Publish", self.publish, None, None)])])
# Variables
self.comboBoxLayoutCounter = 0
self.currentFolderIds = {'f_id': None, 'f_parent_id': None}
self.isSpecialCase = False
# Ingrid
self.ingrid = IngridAPI()
# GUI
# Load custom Widget to place on top of the Docked Widget
# This is simply a new .ui file which was created as a "Widget" (Not a MainWindow), then simply add a Frame to it and layout it
# Add your desired widgets and find them using .findChild(QObject, "identifier")
loader = QUiLoader()
ui_file = QFile(os.path.join(self.supportPath(duppmenu, "duppmenu"), "CustomDockWidget.ui"))
ui_file.open(QFile.ReadOnly)
self.customDockWidget = loader.load(ui_file)
ui_file.close()
# Target custom widgets
self.comboBox_0 = self.customDockWidget.findChild(QComboBox, "comboBox_0")
self.comboBoxLayout = self.customDockWidget.findChild(QVBoxLayout, "comboBoxLayout")
# Event handlers
self.comboBox_0.currentIndexChanged.connect(lambda: self.onComboBoxIndexChanged(self.comboBox_0))
# Gets the current RV session windows as a PySide QMainWindow.
self.rvWindow = rv.qtutils.sessionWindow()
# Create DockWidget and add the Custom Widget to it
self.dockWidget = QDockWidget("Publish", self.rvWindow)
self.dockWidget.setWidget(self.customDockWidget)
# Dock widget to the RV MainWindow
self.rvWindow.addDockWidget(Qt.RightDockWidgetArea, self.dockWidget)