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