PySide panels

To create a non-modal panel that can be saved in the project’s layout and docked into the application’s tab-widgets, there is 2 possible way of doing it:

Generally you should define your panels in the initGui.py script (see startup-scripts). You can also define the panel in the Script Editor at run-time of Natron, though this will not persist when Natron is closed.

To make your panel be created upon new project created, register a Python callback in the Preferences–>Python tab in the parameter After project created. This callback will not be called for project being loaded either via an auto-save or via a user action.

#This goes in initGui.py

def createMyPanel():
    #Create panel
    ...

def onProjectCreatedCallback():
    createMyPanel()

Warning

When the initGui.py script is executed, the app variable (or any derivative such as app1 app2 etc…) does not exist since no project is instantiated yet. The purpose of the script is not to instantiate the GUI per-say but to define classes and functions that will be used later on by application instances.

Python panels can be re-created for existing projects using serialization functionalities explained here See the example below (the whole script is available attached below)

# We override the save() function and save the filename
def save(self):
    return self.locationEdit.text()

# We override the restore(data) function and restore the current image
def restore(self,data):

    self.locationEdit.setText(data)
    self.label.setPixmap(QPixmap(data))

The sole requirement to save a panel in the layout is to call the registerPythonPanel(panel,function) function of GuiApp:

app.registerPythonPanel(app.mypanel,"createIconViewer")

See the details of the PyPanel class for more explanation on how to sub-class it.

Also check-out the complete example source code below.

Using user parameters:

Let’s assume we have no use to make our own widgets and want quick parameters fresh and ready, we just have to use the PyPanel class without sub-classing it:

#Callback called when a parameter of the player changes
#The variable paramName is declared by Natron; indicating the name of the parameter which just had its value changed
def myPlayerParamChangedCallback():

    viewer = app.getViewer("Viewer1")
    if viewer == None:
        return
    if paramName == "previous":
        viewer.seek(viewer.getCurrentFrame() - 1)



def createMyPlayer():

    #Create a panel named "My Panel" that will use user parameters
    app.player = NatronGui.PyPanel("fr.inria.myplayer","My Player",True,app)

    #Add a push-button parameter named "Previous"
    app.player.previousFrameButton = app.player.createButtonParam("previous","Previous")

    #Refresh user parameters GUI, necessary after changes to static properties of parameters.
    #See the Param class documentation
    app.player.refreshUserParamsGUI()

    #Set a callback that will be called upon parameter change
    app.player.setParamChangedCallback("myPlayerParamChangedCallback")

Note

For convenience, there is a way to also add custom widgets to python panels that are using user parameters with the addWidget(widget) and insertWidget(index,widget) functions. However the widgets will be appended after any user parameter defined.

Managing panels and panes

Panels in Natron all have an underlying script-name, that is the one you gave as first parameter to the constructor of PyPanel.

You can then move the PyPanel between the application’s panes by calling the function moveTab(scriptName,pane) of GuiApp.

Note

All application’s panes are auto-declared by Natron and can be referenced directly by a variable, such as:

app.pane2

Panels also have a script-name but only viewers and user panels are auto-declared by Natron:

app.pane2.Viewer1
app.pane1.myPySidePanelScriptName

Source code of the example initGui.py

#This Source Code Form is subject to the terms of the Mozilla Public
#License, v. 2.0. If a copy of the MPL was not distributed with this
#file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#Created by Alexandre GAUTHIER-FOICHAT on 01/27/2015.

#PySide is already imported by Natron, but we remove the cumbersome PySide.QtGui and PySide.QtCore prefix
from PySide.QtGui import *
from PySide.QtCore import *

#To import the variable "natron"
from NatronGui import *

#Callback called when a parameter of the player changes
#The variable paramName is declared by Natron; indicating the name of the parameter which just had its value changed
def myPlayerParamChangedCallback(paramName, app, userEdited):

    viewer = app.getViewer("Viewer1")
    if viewer == None:
        return
    if paramName == "previous":
        viewer.seek(viewer.getCurrentFrame() - 1)
    elif paramName == "backward":
        viewer.startBackward()
    elif paramName == "forward":
        viewer.startForward()
    elif paramName == "next":
        viewer.seek(viewer.getCurrentFrame() + 1)
    elif paramName == "stop":
        viewer.pause()

def createMyPlayer():

    app.player = NatronGui.PyPanel("fr.inria.myplayer","My Player",True,app)
    app.player.previousFrameButton = app.player.createButtonParam("previous","Previous")
    app.player.previousFrameButton.setAddNewLine(False)

    app.player.playBackwardButton = app.player.createButtonParam("backward","Rewind")
    app.player.playBackwardButton.setAddNewLine(False)
    
    app.player.stopButton = app.player.createButtonParam("stop","Pause")
    app.player.stopButton.setAddNewLine(False)

    app.player.playForwardButton = app.player.createButtonParam("forward","Play")
    app.player.playForwardButton.setAddNewLine(False)

    app.player.nextFrameButton = app.player.createButtonParam("next","Next")

    app.player.helpLabel = app.player.createStringParam("help","Help")
    app.player.helpLabel.setType(NatronEngine.StringParam.TypeEnum.eStringTypeLabel)
    app.player.helpLabel.set("<br><b>Previous:</b> Seek the previous frame on the timeline</br>"
                        "<br><b>Rewind:</b> Play backward</br>"
                        "<br><b>Pause:</b> Pauses the playback</br>"
                        "<br><b>Play:</b> Play forward</br>"
                        "<br><b>Next:</b> Seek the next frame on the timeline</br>")
                        
    app.player.refreshUserParamsGUI()
    app.player.setParamChangedCallback("myPlayerParamChangedCallback")

    #Add it to the "pane2" tab widget
    app.pane2.appendTab(app.player);
    
    #Register the tab to the application, so it is saved into the layout of the project
    #and can appear in the Panes sub-menu of the "Manage layout" button (in top left-hand corner of each tab widget)
    app.registerPythonPanel(app.player,"createMyPlayer")


#A small panel to load and visualize icons/images
class IconViewer(NatronGui.PyPanel):

    #Register a custom signal
    userFileChanged = QtCore.Signal()

    #Slots should be decorated:
    #http://qt-project.org/wiki/Signals_and_Slots_in_PySide
    
    #This is called upon a user click on the button
    @QtCore.Slot()
    def onButtonClicked(self):
        location = self.currentApp.getFilenameDialog(("jpg","png","bmp","tif"))
        if location:
            self.locationEdit.setText(location)
            
            #Save the file
            self.onUserDataChanged()
        
        self.userFileChanged.emit()
    
    #This is called when the user finish editing of the line edit (when return is pressed or focus out)
    @QtCore.Slot()
    def onLocationEditEditingFinished(self):
        #Save the file
        self.onUserDataChanged()
        self.userFileChanged.emit()
    
    #This is called when our custom userFileChanged signal is emitted
    @QtCore.Slot()
    def onFileChanged(self):
        self.label.setPixmap(QPixmap(self.locationEdit.text()))
    
    
    def __init__(self,scriptName,label,app):
        
        #Init base class, important! otherwise signals/slots won't work.
        NatronGui.PyPanel.__init__(self,scriptName, label, False, app)
        
        #Store the current app as it might no longer be pointing to the app at the time being called
        #when a slot will be invoked later on
        self.currentApp = app
        
        #Set the layout
        self.setLayout( QVBoxLayout())
        
        #Create a widget container for the line edit + button
        fileContainer = QWidget(self)
        fileLayout = QHBoxLayout()
        fileContainer.setLayout(fileLayout)
        
        #Create the line edit, make it expand horizontally
        self.locationEdit = QLineEdit(fileContainer)
        self.locationEdit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
        
        #Create a pushbutton
        self.button = QPushButton(fileContainer)
        #Decorate it with the open-file pixmap built-in into Natron
        buttonPixmap = natron.getIcon(NatronEngine.Natron.PixmapEnum.NATRON_PIXMAP_OPEN_FILE)
        self.button.setIcon(QIcon(buttonPixmap))
        
        #Add widgets to the layout
        fileLayout.addWidget(self.locationEdit)
        fileLayout.addWidget(self.button)
        
        #Use a QLabel to display the images
        self.label = QLabel(self)
        
        #Init the label with the icon of Natron
        natronPixmap = natron.getIcon(NatronEngine.Natron.PixmapEnum.NATRON_PIXMAP_APP_ICON)
        self.label.setPixmap(natronPixmap)
        #Built-in icons of Natron are in the resources
        self.locationEdit.setText(":/Resources/Images/natronIcon256_linux.png")
        
        #Make it expand in both directions so it takes all space
        self.label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)

        #Add widgets to the layout
        self.layout().addWidget(fileContainer)
        self.layout().addWidget(self.label)
        
        #Make signal/slot connections
        self.button.clicked.connect(self.onButtonClicked)
        self.locationEdit.editingFinished.connect(self.onLocationEditEditingFinished)
        self.userFileChanged.connect(self.onFileChanged)

    # We override the save() function and save the filename
    def save(self):
        return self.locationEdit.text()

    # We override the restore(data) function and restore the current image
    def restore(self,data):

        self.locationEdit.setText(data)
        self.label.setPixmap(QPixmap(data))

#To be called to create a new icon viewer panel:
#Note that *app* should be defined. Generally when called from onProjectCreatedCallback
#this is set, but when called from the Script Editor you should set it yourself beforehand:
#app = app1
#See http://natron.readthedocs.org/en/python/natronobjects.html for more info
def createIconViewer():
    
    if hasattr(app,"p"):
        #The icon viewer already exists, it we override the app.p variable, then it will destroy the previous widget
        #and create a new one but we don't really need it
        
        #The warning will be displayed in the Script Editor
        print("Note for us developers: this widget already exists!")
        return
    
    #Create our icon viewer
    app.p = IconViewer("fr.inria.iconViewer","Icon viewer",app)
    
    #Add it to the "pane2" tab widget
    app.pane2.appendTab(app.p);
    
    #Register the tab to the application, so it is saved into the layout of the project
    #and can appear in the Panes sub-menu of the "Manage layout" button (in top left-hand corner of each tab widget)
    app.registerPythonPanel(app.p,"createIconViewer")


#Callback set in the "After project created" parameter in the Preferences-->Python tab of Natron
#This will automatically create our panels when a new project is created
def onProjectCreatedCallback(app):
    #Always create our icon viewer on project creation, you must register this call-back in the
    #"After project created callback" parameter of the Preferences-->Python tab.
    createIconViewer()

    createMyPlayer()

#Add a custom menu entry with a shortcut to create our icon viewer
natron.addMenuCommand("Inria/Scripts/IconViewer","createIconViewer",QtCore.Qt.Key.Key_L,QtCore.Qt.KeyboardModifier.ShiftModifier)