Hooks – Batch – nodes selections

Batch Nodes Selections:
Save and load (frame) selections of nodes in batch.

A tool to save and load sets of nodes in Batch, and trigger other custom actions. It uses a QT Gui (window) and uses the yaml python module (included) to store selections. It’s still in beta, and the saved selection are general, not per batch group, but it should give a pretty good idea of how it works.
The simplest way to test it is to drop the file in your ‘hooks’ folder. Autodesk default hooks folder: /opt/Autodesk/flame_2019.2.pr94/python/
If you’ve sourced a .bash_profile file containing some path, you can drop it there to.
You can also source any path directly in the shell where you’ll start Flame from.

CUA_batch_selections_standalone.py

Code:



# Stefan Gaillot
# xenjee@gmail.com
# 2019/01/19
# Many thanks (and credits) to Vlad Bakic, Tommy Hooper and Tommy Furukawa.


# ################### SAFETY ###########

from tank.platform.qt import QtGui
from tank.platform.qt import QtCore
import os
import json
import traceback
# in case we need/want to append a path to sys.path:
# import sys


# ######### os.path stuff #########
# turn the relative __file__ value into it's full path:
absolute_path = os.path.realpath(__file__)
# Use the os module to split the filepath using '/' as a seperator to creates a list from which we pick IDs []
root_path = '/'.join(absolute_path.split('/')[0:-1])

# ##### navigate down to the desired folder and append a path with sys.path: #####
# sys.path.append("{root}/modules".format(root=root_path))
# print "{root}/modules".format(root=root_path)

# ########################################################################


# ANSI COLORS for color coding comments:


class sg_colors:
    dark = "\x1b[38;5;232m"
    grey1 = "\x1b[38;5;235m"
    grey2 = "\x1b[38;5;240m"
    grey3 = "\x1b[38;5;247m"
    red1 = "\x1b[38;5;88m"
    red2 = "\x1b[38;5;124m"
    red3 = "\x1b[38;5;196m"
    orange1 = "\x1b[38;5;130m"
    orange2 = "\x1b[38;5;208m"
    yellow1 = "\x1b[38;5;221m"
    yellow2 = "\x1b[38;5;226m"
    yellow3 = "\x1b[38;5;229m"
    green1 = "\x1b[38;5;106m"
    green2 = "\x1b[38;5;70m"
    green3 = "\x1b[38;5;35m"
    green4 = "\x1b[38;5;118m"
    aqua = "\x1b[38;5;44m"
    blue1 = "\x1b[38;5;24m"
    blue2 = "\x1b[38;5;27m"
    blue3 = "\x1b[38;5;75m"
    purple1 = "\x1b[38;5;17m"
    purple2 = "\x1b[38;5;165m"

    endc = "\x1b[0m"


# ########################################################################

print sg_colors.green1 + "--------------- CUA_batch_selections.py -----------------" + sg_colors.endc

# ######################################### APP and UI ################################################ #


# #### READ AND WRITE TO JSON FILE #####
def load_selections(path):
    print "@@@@@@@@@@@@@@@@@ LOAD SELECTION @@@@@@@@@@@@@@@@"
    content = []
    with open(path, 'r') as file_fd:
        for line in file_fd.readlines():
            cnv_line = eval(line.strip())
            print "CNV LINE:", type(cnv_line), cnv_line
            content.extend(cnv_line)
    print "CONTENT:", type(content), content
    file_fd.close()
    return content


def save_selection(path, data):
    with open(path, 'w') as saved_selections:
        saved_selections.write(json.dumps(data))


# #### CREATE SELECTIONS ROW(s) / CONNECT BUTTONS ##### #
class SelectionsRow(QtGui.QWidget):

    _signal = QtCore.Signal()

    def __init__(self, name, data, frame_callback, graphSelected, row_id, parent=None):

        super(SelectionsRow, self).__init__(parent=parent)

        self.row_id = row_id
        self.name = name
        self.data = data
        self.frame_callback = frame_callback
        self.graphSelected = graphSelected

        self._layout = QtGui.QHBoxLayout(self)
        self._layout.setSpacing(0)
        self.setContentsMargins(0, 0, 0, 0)
        self._layout.setContentsMargins(0, 0, 0, 0)

        self.name = QtGui.QLineEdit(self.name)

        self.store_button = QtGui.QPushButton()
        self.store_button.setFlat(True)
        self.store_button.setText('store')
        self.store_button.clicked.connect(self.store)
        self.name.returnPressed.connect(self.store)

        self.frame_button = QtGui.QPushButton()
        self.frame_button.setFlat(True)
        self.frame_button.setText('frame')
        self.frame_button.clicked.connect(self.exec_frame_callback)  # witch just tries: self.frame_callback(self.data)

        self.remove_button = QtGui.QPushButton()
        self.remove_button.setFlat(True)
        self.remove_button.setText('del')
        self.remove_button.clicked.connect(self.remove)

        self._layout.addWidget(self.name)
        self._layout.addWidget(self.store_button)
        self._layout.addWidget(self.frame_button)
        self._layout.addWidget(self.remove_button)

    def remove(self):
        print sg_colors.green1 + "--------------- def remove -----------------" + sg_colors.endc
        self.data = {}
        self.name.setText("")
        self._signal.emit()

    def value(self):
        print sg_colors.green1 + "--------------- def value -----------------" + sg_colors.endc
        return {'name': self.name.text(), 'data': self.data}

    def store(self, *args, **kwargs):
        print sg_colors.green1 + "--------------- def store -----------------" + sg_colors.endc
        try:
            self.data = self.graphSelected()
            if not self.name.text():
                self.name.setText('selection_' + str(self.row_id + 1).zfill(2))
            self._signal.emit()
        except ValueError:
            traceback.print_exc()

    def exec_frame_callback(self):
        print sg_colors.green1 + "--------------- def exec_frame_callback -----------------" + sg_colors.endc
        try:
            self.frame_callback(self.data)
        except ValueError:
            traceback.print_exc()

    # #### SIDE CALLBACKS #####
    @staticmethod
    def frame_selected_nodes(passed_data):
        import flame  # Flame will know
        flame.batch.selected_nodes = (passed_data)
        flame.batch.frame_selected()
        print "PASSED DATA: ", passed_data

# CALLED AT FIRST with 'path' argument: form = SelectionsWidget(path='saved_nodes.yaml')


class SelectionsWidget(QtGui.QWidget):
    print sg_colors.green1 + "--------------- class SelectionsWidget (QtGui.QWidget) -----------------" + sg_colors.endc

    def __init__(self, *args, **kwargs):

        super(SelectionsWidget, self).__init__(parent=kwargs.get('parent'))

        self.path = kwargs.get('path')
        # Amount of slots:
        self.slots = kwargs.get('slots', 10)
        self.graphSelected = kwargs.get('graphSelected')

        self.setAttribute(QtCore.Qt.WA_DeleteOnClose, True)
        self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)

        # --> Initialise Layouts
        self.mainLayout = QtGui.QGridLayout(self)
        self.templateLayout = QtGui.QVBoxLayout()
        self.templateLayout.setContentsMargins(0, 0, 0, 0)
        self.mainLayout.addLayout(self.templateLayout, 0, 0)
        self.setWindowTitle("Selections")
        # self.setGeometry(820, 150, 350, 200)
        self.move(800, 150)
        self._rows = []

        if self.path and os.path.isfile(self.path):
            data = load_selections(self.path)  # load from saved_nodes.py
            print "-" * 80
            print "DATA FROM SELECTION WIDGET", ":", data
            self.create_ui(data)

    # --> CREATE UI: Add Rows to Slots
    # SelectionsRow() class: builds the rows with buttons and lineEdit.
    # The rows will then be added in the create_ui() method of SelectionsWidget() class
    # row_id=i -> i is a row from the above for loop
    def create_ui(self, data):
        print sg_colors.green1 + "--------------- def create_ui -----------------" + sg_colors.endc

        for row in self._rows:
            row.deleteLater()

        # how many rows (slots). Slots are declared in SelectionsWidget in class init
        self.slots = max(len(data), self.slots)

        for i in range(self.slots):
            if i < len(data):
                entry = data[i]
            else:
                entry = {'name': '', 'data': []}
            import flame  # Flame will know
            row = SelectionsRow(entry['name'], entry['data'], SelectionsRow.frame_selected_nodes, self.graphSelected, row_id=i)
            row._signal.connect(self.save)  # _signal = Signal() in SelectionsRow()
            self._rows.append(row)  # _rows is declared in SelectionsWidget() __init__
            self.mainLayout.addWidget(row)
            print sg_colors.endc

    def save(self):
        print sg_colors.green1 + "--------------- def save -----------------" + sg_colors.endc

        config_data = []
        for row in self._rows:
            _value = row.value()
            if _value['data'] and _value['name']:
                config_data.append(_value)
            elif _value['data'] and not _value['name']:
                print sg_colors.grey3 + "Skippin row with data that has no name" + sg_colors.endc

        save_selection(self.path, config_data)


# ######################################### CUA - CustomUIActions ################################################ #

print sg_colors.grey3 + '-' * 80 + sg_colors.endc
print sg_colors.blue2 + "--------------- CUA -----------------" + sg_colors.endc

# Contextuel Menu Entry


# def getCustomUIActions():
def getMainMenuCustomUIActions():

    action1 = {}
    action1["name"] = "Nodes Selections"
    # action1["caption"] = "v01"

    appGroup1 = {}
    appGroup1["name"] = "Nodes Selections"
    appGroup1["actions"] = (action1,)

    return (appGroup1,)

# What happens when you chose (and click) from the contextual menu


def customUIAction(info, userData):

    if info['name'] == 'Nodes Selections':

        import flame  # Flame will know
        import os.path

        # ######### CONFIG FILEPATH ############ #
        configfile_path = "{root}/saved_nodes.json".format(root=root_path)
        print "CONFIGFILE_PATH: ", configfile_path

        # create a dummy list of dict to create and feed a .json config file if there was none.
        dummy_dict = [{'data': ['mux3'], 'name': 'mux3'}]

        if not os.path.isfile(configfile_path):
            print "-" * 30 + "FILE NOT FOUND, CREATING FILE" + "-" * 30

            with open(configfile_path, "w+") as myfilepy:
                myfilepy.write(json.dumps(dummy_dict))
            myfilepy.close()
            with open(configfile_path, "r") as myfilepy2:
                print "-" * 80
                print "DUMMY DICT TO .py CONFIG file: ", ": ", dummy_dict
                print "READ FROM .py CONFIG file: " + myfilepy2.read()
                print "-" * 80

        def get_selection():
            import flame  # Flame will know
            print sg_colors.green1 + "--- def get_selection() ---" + sg_colors.endc
            print sg_colors.red2 + "--- remember: 2018.3 doesn't need '.get_value()', 2019 does ---" + sg_colors.endc
            return ["" + s.name + "" for s in flame.batch.selected_nodes.get_value()]

    #############################################

        form = SelectionsWidget(path=configfile_path, graphSelected=get_selection)
        form.show()
        return form

Leave a Reply