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)