PySide panels — Natron 2.5.0 documentation

# 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:

> - Sub-class PyPanel and create your own GUI using PySide

> - Use the API proposed by PyPanel to add custom user parameters as done for PyModalDialog.

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.

    from qtpy.QtGui import *
    from qtpy.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)