IC Python API:Plugin Window

From Reallusion Wiki!
Jump to: navigation, search

Main article: RL Python Samples.

If you are interested in creating plugins and tools for the iClone environment, then you'll eventually want to add the access to the scripts to iClone upon every start-up. This way you can execute your scripts from the Plugins menu instead of reading it from file each and every time.

Window Creation Workflow

Ic python api plugin window 01.png

The standard start-up boilerplate for a plugin is the following:

  1. iClone automatically calls the run_script command.
  2. The plugin should initialize the process of adding itself into the plugin menu.
  3. The plugin menu action should call to the function for showing the window.
  4. The show window function should decide which action to take depending on the situation:
  • If the window instance does not exist, then create a new window.
  • If the window is hidden then show it.
  • If the window is visible then show a message notifying the user of its existence.

Necessary Modules and Global Variables

Besides the rudimentary Reallusion Python module, we'll also need Pyside2 to build the user interface. And we'll need a global variable to house a reference to our window elements.

import RLPy
from PySide2 import *
from PySide2.QtCore import Qt
from PySide2.shiboken2 import wrapInstance

ui = {}

Dialog Event Callback

Next, we'll need a dialog event callback for the dialog window that will be created. This custom class inherits from RLPy.RDialogCallback. It is crucial to clear the global variable when the dialog window is closed; in order to compare to it when the same window initialization process is called. This will prevent the creation of duplicate windows.

class DialogEventCallback(RLPy.RDialogCallback):
    def __init__(self):
        RLPy.RDialogCallback.__init__(self)

    def OnDialogClose(self):
        global ui

        ui.clear()  # Clear global variables
        return True

Notice how OnDialogClose returns True in order to notify iClone that the window is to be disposed of.

Create the Window

Ic python api plugin window 02.png

Let's create the actual plugin window interface. We'll also add a hide button to make it convenient to call the hide command on this window.

def create_window():
    global ui
    ui["window"] = RLPy.RUi.CreateRDialog()
    ui["window"].SetWindowTitle("Plugin Window")

    # Create Pyside layout for RDialog
    qt_dialog = wrapInstance(int(ui["window"].GetWindow()), QtWidgets.QDialog)
    qt_dialog.setFixedWidth(400)
    qt_dialog.setFixedHeight(280)

    # Create a label for previous status
    ui["pre-state"] = QtWidgets.QLabel("This window was just created.")
    ui["pre-state"].setAlignment(Qt.AlignCenter)
    ui["pre-state"].setStyleSheet("color:#ff7c7c; font-weight:bold")
    qt_dialog.layout().addWidget(ui["pre-state"])

    # Create a description label
    label = QtWidgets.QLabel(
        '''<p>This is a dialog window created by the {0}Plugin Window{1} Python script.</p>            
<p>Window check works by registering the window to a global variable where it can be compared.
This only works if the script is executed from the {0}Plugins{1} menu 
because global variable don't share the same scope when loaded via the {0}Script{1} menu.</p>
<p>If the variable does not exist, then a new window is created.
If the variable does exist and the window is hidden then the it will be shown again.
If the variable does exist and the window is visible, 
then a message will be issued to notify the user of its existence.</p>'''.format(
            '''<span style="color:#a2ec13; font-weight:bold">''', '''</span>'''))
    label.setWordWrap(True)
    label.setTextFormat(Qt.RichText)
    qt_dialog.layout().addWidget(label)

    qt_dialog.layout().addStretch()

    hide_button = QtWidgets.QPushButton("Hide Window")
    hide_button.clicked.connect(lambda: ui["window"].Hide())
    hide_button.setFixedHeight(28)
    qt_dialog.layout().addWidget(hide_button)

    ui["window"].Show()

Showing the Window

Ic python api plugin window 03.png

Here comes the crucial part of deciding which action to take depending on the state of the plugin window.

def show_window():
    global ui

    if "window" in ui:
        if ui["window"].IsVisible():
            RLPy.RUi.ShowMessageBox(
                "Plugin Window",
                "The current Plugin Window session is still running.  You must first close the window to start another session.",
                RLPy.EMsgButton_Ok
            )
            ui["pre-state"].setText("This window is still visible.")
        else:
            ui["window"].Show()
            ui["pre-state"].setText("This window was hidden.")
    else:
        create_window()
        ui["dialog_events"] = DialogEventCallback()
        ui["window"].RegisterEventCallback(ui["dialog_events"])

Initializing the Plugin

Global variable only share the same scope if they are initialized from the Plugin menu. Therefore we must add the initialization command to the Plugin menu.

def initialize_plugin():
    # Create Pyside interface with iClone main window
    ic_dlg = wrapInstance(int(RLPy.RUi.GetMainWindow()), QtWidgets.QMainWindow)

    plugin_menu = ic_dlg.menuBar().findChild(QtWidgets.QMenu, "potm_fundamentals")  # Check if the menu item exists
    if plugin_menu is None:

        # Create Pyside layout for QMenu named "POTM Fundamentals" and attach it to the Plugins menu
        plugin_menu = wrapInstance(int(RLPy.RUi.AddMenu("POTM Fundamentals", RLPy.EMenu_Plugins)), QtWidgets.QMenu)
        plugin_menu.setObjectName("potm_fundamentals")  # Setting an object name for the menu is equivalent to giving it an ID

    # Check if the menu action already exists
    menu_actions = plugin_menu.actions()
    for i in range(len(menu_actions)):
        if menu_actions[i].text() == "Plugin Window":
            plugin_menu.removeAction(menu_actions[i])  # Remove duplicate actions

    # Set up the menu action
    menu_action = plugin_menu.addAction("Plugin Window")
    menu_action.triggered.connect(show_window)

Notice that there are two crucial checks that must be performed:

  1. Checking if the plugin menu already exists.
  2. Checking if the action already exists under the plugin sub-menu.

This is to prevent the existence of duplicate plugin menus and actions.

Running the Script

Ic python api plugin window 04.png

You can run this script via Script > Load and it will be accessible via Plugins > POTM Fundamentals > Plugin Window command; with the help of the following code snippet:

def run_script():
    initialize_plugin()

    RLPy.RUi.ShowMessageBox(
        "Plugin Window",
        "You can run the Plugin Window script from the menu bar under <b>Plugins > POTM Fundamentals</b>.",
        RLPy.EMsgButton_Ok
    )

Everything Put Together

You can copy and paste the following code into a PY file and load it into iClone via Script > Load Python.

import RLPy
from PySide2 import *
from PySide2.QtCore import Qt
from PySide2.shiboken2 import wrapInstance

ui = {}


class DialogEventCallback(RLPy.RDialogCallback):
    def __init__(self):
        RLPy.RDialogCallback.__init__(self)

    def OnDialogClose(self):
        global ui

        ui.clear()  # Clear global variables
        return True


def create_window():
    global ui
    ui["window"] = RLPy.RUi.CreateRDialog()
    ui["window"].SetWindowTitle("Plugin Window")

    # Create Pyside layout for RDialog
    qt_dialog = wrapInstance(int(ui["window"].GetWindow()), QtWidgets.QDialog)
    qt_dialog.setFixedWidth(400)
    qt_dialog.setFixedHeight(280)

    # Create a label for previous status
    ui["pre-state"] = QtWidgets.QLabel("This window was just created.")
    ui["pre-state"].setAlignment(Qt.AlignCenter)
    ui["pre-state"].setStyleSheet("color:#ff7c7c; font-weight:bold")
    qt_dialog.layout().addWidget(ui["pre-state"])

    # Create a description label
    label = QtWidgets.QLabel(
        '''<p>This is a dialog window created by the {0}Plugin Window{1} Python script.</p>            
<p>Window check works by registering the window to a global variable where it can be compared.
This only works if the script is executed from the {0}Plugins{1} menu 
because global variable don't share the same scope when loaded via the {0}Script{1} menu.</p>
<p>If the variable does not exist, then a new window is created.
If the variable does exist and the window is hidden then the it will be shown again.
If the variable does exist and the window is visible, 
then a message will be issued to notify the user of its existence.</p>'''.format(
            '''<span style="color:#a2ec13; font-weight:bold">''', '''</span>'''))
    label.setWordWrap(True)
    label.setTextFormat(Qt.RichText)
    qt_dialog.layout().addWidget(label)

    qt_dialog.layout().addStretch()

    hide_button = QtWidgets.QPushButton("Hide Window")
    hide_button.clicked.connect(lambda: ui["window"].Hide())
    hide_button.setFixedHeight(28)
    qt_dialog.layout().addWidget(hide_button)

    ui["window"].Show()


def show_window():
    global ui

    if "window" in ui:
        if ui["window"].IsVisible():
            RLPy.RUi.ShowMessageBox(
                "Plugin Window",
                "The current Plugin Window session is still running.  You must first close the window to start another session.",
                RLPy.EMsgButton_Ok
            )
            ui["pre-state"].setText("This window is still visible.")
        else:
            ui["window"].Show()
            ui["pre-state"].setText("This window was hidden.")
    else:
        create_window()
        ui["dialog_events"] = DialogEventCallback()
        ui["window"].RegisterEventCallback(ui["dialog_events"])


def initialize_plugin():
    # Create Pyside interface with iClone main window
    ic_dlg = wrapInstance(int(RLPy.RUi.GetMainWindow()), QtWidgets.QMainWindow)

    plugin_menu = ic_dlg.menuBar().findChild(QtWidgets.QMenu, "potm_fundamentals")  # Check if the menu item exists
    if plugin_menu is None:

        # Create Pyside layout for QMenu named "POTM Fundamentals" and attach it to the Plugins menu
        plugin_menu = wrapInstance(int(RLPy.RUi.AddMenu("POTM Fundamentals", RLPy.EMenu_Plugins)), QtWidgets.QMenu)
        plugin_menu.setObjectName("potm_fundamentals")  # Setting an object name for the menu is equivalent to giving it an ID

    # Check if the menu action already exists
    menu_actions = plugin_menu.actions()
    for i in range(len(menu_actions)):
        if menu_actions[i].text() == "Plugin Window":
            plugin_menu.removeAction(menu_actions[i])  # Remove duplicate actions

    # Set up the menu action
    menu_action = plugin_menu.addAction("Plugin Window")
    menu_action.triggered.connect(show_window)


def run_script():
    initialize_plugin()

    RLPy.RUi.ShowMessageBox(
        "Plugin Window",
        "You can run the Plugin Window script from the menu bar under <b>Plugins > POTM Fundamentals</b>.",
        RLPy.EMsgButton_Ok
    )

APIs Used

You can research the following references for the APIs deployed in this code.