├── dev ├── dev-env.toe ├── td-modules │ ├── base_save │ │ ├── base_save.tox │ │ ├── parACTIONS.py │ │ ├── parexec_save.py │ │ ├── execute_check_on_exit.py │ │ ├── parHelperEXT.py │ │ ├── listerFuncs.py │ │ ├── saveUtils.py │ │ ├── saveOp.py │ │ └── saveTox.py │ ├── base_tester │ │ └── base_tester.tox │ └── tools │ │ ├── releaseFiles.json │ │ └── collect_files.py ├── scripts │ ├── parACTIONS.py │ ├── parexec_save.py │ ├── parHelperEXT.py │ ├── saveUtils.py │ ├── saveOp.py │ └── saveEXT.py └── _startDev.cmd ├── assets ├── add-tag.PNG ├── scripts.png ├── ext-set-up.PNG ├── set-path.PNG ├── new-save-dir.PNG ├── new-save-tox.PNG ├── updating-ext.gif ├── comment-colors.png ├── create-new-comp.PNG ├── external-script.gif ├── drag-into-network.gif ├── py-file-with-comp.PNG ├── all-tox-comps-window.png ├── create-project-dir.PNG ├── create-project-toe.PNG ├── external-tox-color.PNG ├── base_save_and_pars_v2.PNG ├── create-td-modules-dir.PNG ├── prompt-to-externalize.PNG ├── work-in-external-editor.gif └── baseline-save_alt-24px.svg ├── release └── base_save.tox ├── sample_project ├── sample_project.toe └── td-modules │ ├── base_my_fx │ └── base_my_fx.tox │ ├── container_output │ ├── container_output.tox │ └── shaders │ │ └── glslmulti1_center.glsl │ └── container_control │ ├── container_control.tox │ └── controllerEXT.py ├── .gitignore ├── .workspaces └── TouchDesigner_project_.code-workspace ├── LICENSE └── README.md /dev/dev-env.toe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raganmd/touchdesigner-save-external/HEAD/dev/dev-env.toe -------------------------------------------------------------------------------- /assets/add-tag.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raganmd/touchdesigner-save-external/HEAD/assets/add-tag.PNG -------------------------------------------------------------------------------- /assets/scripts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raganmd/touchdesigner-save-external/HEAD/assets/scripts.png -------------------------------------------------------------------------------- /assets/ext-set-up.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raganmd/touchdesigner-save-external/HEAD/assets/ext-set-up.PNG -------------------------------------------------------------------------------- /assets/set-path.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raganmd/touchdesigner-save-external/HEAD/assets/set-path.PNG -------------------------------------------------------------------------------- /release/base_save.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raganmd/touchdesigner-save-external/HEAD/release/base_save.tox -------------------------------------------------------------------------------- /assets/new-save-dir.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raganmd/touchdesigner-save-external/HEAD/assets/new-save-dir.PNG -------------------------------------------------------------------------------- /assets/new-save-tox.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raganmd/touchdesigner-save-external/HEAD/assets/new-save-tox.PNG -------------------------------------------------------------------------------- /assets/updating-ext.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raganmd/touchdesigner-save-external/HEAD/assets/updating-ext.gif -------------------------------------------------------------------------------- /assets/comment-colors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raganmd/touchdesigner-save-external/HEAD/assets/comment-colors.png -------------------------------------------------------------------------------- /assets/create-new-comp.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raganmd/touchdesigner-save-external/HEAD/assets/create-new-comp.PNG -------------------------------------------------------------------------------- /assets/external-script.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raganmd/touchdesigner-save-external/HEAD/assets/external-script.gif -------------------------------------------------------------------------------- /assets/drag-into-network.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raganmd/touchdesigner-save-external/HEAD/assets/drag-into-network.gif -------------------------------------------------------------------------------- /assets/py-file-with-comp.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raganmd/touchdesigner-save-external/HEAD/assets/py-file-with-comp.PNG -------------------------------------------------------------------------------- /assets/all-tox-comps-window.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raganmd/touchdesigner-save-external/HEAD/assets/all-tox-comps-window.png -------------------------------------------------------------------------------- /assets/create-project-dir.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raganmd/touchdesigner-save-external/HEAD/assets/create-project-dir.PNG -------------------------------------------------------------------------------- /assets/create-project-toe.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raganmd/touchdesigner-save-external/HEAD/assets/create-project-toe.PNG -------------------------------------------------------------------------------- /assets/external-tox-color.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raganmd/touchdesigner-save-external/HEAD/assets/external-tox-color.PNG -------------------------------------------------------------------------------- /assets/base_save_and_pars_v2.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raganmd/touchdesigner-save-external/HEAD/assets/base_save_and_pars_v2.PNG -------------------------------------------------------------------------------- /assets/create-td-modules-dir.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raganmd/touchdesigner-save-external/HEAD/assets/create-td-modules-dir.PNG -------------------------------------------------------------------------------- /assets/prompt-to-externalize.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raganmd/touchdesigner-save-external/HEAD/assets/prompt-to-externalize.PNG -------------------------------------------------------------------------------- /sample_project/sample_project.toe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raganmd/touchdesigner-save-external/HEAD/sample_project/sample_project.toe -------------------------------------------------------------------------------- /assets/work-in-external-editor.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raganmd/touchdesigner-save-external/HEAD/assets/work-in-external-editor.gif -------------------------------------------------------------------------------- /dev/td-modules/base_save/base_save.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raganmd/touchdesigner-save-external/HEAD/dev/td-modules/base_save/base_save.tox -------------------------------------------------------------------------------- /dev/td-modules/base_tester/base_tester.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raganmd/touchdesigner-save-external/HEAD/dev/td-modules/base_tester/base_tester.tox -------------------------------------------------------------------------------- /sample_project/td-modules/base_my_fx/base_my_fx.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raganmd/touchdesigner-save-external/HEAD/sample_project/td-modules/base_my_fx/base_my_fx.tox -------------------------------------------------------------------------------- /dev/scripts/parACTIONS.py: -------------------------------------------------------------------------------- 1 | def Filecolor(tuplet, par): 2 | parent.save.Set_external_file_colors() 3 | 4 | def Extcolor(tuplet, par): 5 | parent.save.Set_ext_tox_colors() 6 | pass -------------------------------------------------------------------------------- /dev/td-modules/base_save/parACTIONS.py: -------------------------------------------------------------------------------- 1 | def Filecolor(tuplet, par): 2 | parent.save.Set_external_file_colors() 3 | 4 | def Extcolor(tuplet, par): 5 | parent.save.Set_ext_tox_colors() 6 | pass -------------------------------------------------------------------------------- /sample_project/td-modules/container_output/container_output.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raganmd/touchdesigner-save-external/HEAD/sample_project/td-modules/container_output/container_output.tox -------------------------------------------------------------------------------- /sample_project/td-modules/container_control/container_control.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raganmd/touchdesigner-save-external/HEAD/sample_project/td-modules/container_control/container_control.tox -------------------------------------------------------------------------------- /sample_project/td-modules/container_control/controllerEXT.py: -------------------------------------------------------------------------------- 1 | class MyController: 2 | 3 | def __init__(self, my_op): 4 | self.Myop = my_op 5 | 6 | log_msg = 'MyController init from op({})'.format(my_op) 7 | print(log_msg) 8 | 9 | return -------------------------------------------------------------------------------- /assets/baseline-save_alt-24px.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | dev/dep/* 3 | release/dep/* 4 | 5 | # the git-pip python bits 6 | dev/dep/python/get-pip.py 7 | release/dep/python/get-pip.py 8 | get-pip.py 9 | 10 | # the external TD tools 11 | dev/td-modules/tools/releaseVersions/* 12 | 13 | # mac files 14 | *.DS_STORE -------------------------------------------------------------------------------- /sample_project/td-modules/container_output/shaders/glslmulti1_center.glsl: -------------------------------------------------------------------------------- 1 | 2 | // Example Pixel Shader 3 | 4 | // uniform float exampleUniform; 5 | 6 | out vec4 fragColor; 7 | void main() 8 | { 9 | //vec2 newUV = (vUV.st * 2) - 1; 10 | vec2 newUV = vUV.st; 11 | vec4 color = vec4(newUV, vec2(1.0)); 12 | fragColor = TDOutputSwizzle(color); 13 | } 14 | -------------------------------------------------------------------------------- /.workspaces/TouchDesigner_project_.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "name": "TouchDesigner", 5 | "path": "../dev", 6 | "cwd": "../dev", 7 | "python.interpreterPath": "C:/Program Files/Derivative/TouchDesigner.2023.11880/bin/python.exe" 8 | }, 9 | ], 10 | "settings": { 11 | "workbench.colorTheme": "Overflow Contrast (rainglow)", 12 | }, 13 | } -------------------------------------------------------------------------------- /dev/td-modules/tools/releaseFiles.json: -------------------------------------------------------------------------------- 1 | { 2 | "targets" : [ 3 | { 4 | "name" : "base_save", 5 | "url" : "https://github.com/raganmd/touchdesigner-save-external/blob/master/release/base_save.tox?raw=true" 6 | }, 7 | { 8 | "name" : "base_save_for_release", 9 | "url" : "https://github.com/raganmd/touchdesigner-tox-prep-for-release/blob/master/release/base_save_for_release.tox?raw=true" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /dev/scripts/parexec_save.py: -------------------------------------------------------------------------------- 1 | # me - this DAT 2 | # par - the Par object that has changed 3 | # val - the current value 4 | # prev - the previous value 5 | # 6 | # Make sure the corresponding toggle is enabled in the Parameter Execute DAT. 7 | 8 | def onValueChange(par, prev): 9 | # use par.eval() to get current value 10 | parent.save.Par_functions(par) 11 | return 12 | 13 | # Called at end of frame with complete list of individual parameter changes. 14 | # The changes are a list of named tuples, where each tuple is (Par, previous value) 15 | def onValuesChanged(changes): 16 | for c in changes: 17 | # use par.eval() to get current value 18 | par = c.par 19 | prev = c.prev 20 | return 21 | 22 | def onPulse(par): 23 | parent.save.Par_functions(par) 24 | return 25 | 26 | def onExpressionChange(par, val, prev): 27 | return 28 | 29 | def onExportChange(par, val, prev): 30 | return 31 | 32 | def onEnableChange(par, val, prev): 33 | return 34 | 35 | def onModeChange(par, val, prev): 36 | return 37 | -------------------------------------------------------------------------------- /dev/td-modules/base_save/parexec_save.py: -------------------------------------------------------------------------------- 1 | # me - this DAT 2 | # par - the Par object that has changed 3 | # val - the current value 4 | # prev - the previous value 5 | # 6 | # Make sure the corresponding toggle is enabled in the Parameter Execute DAT. 7 | 8 | def onValueChange(par, prev): 9 | # use par.eval() to get current value 10 | parent.save.Par_functions(par) 11 | return 12 | 13 | # Called at end of frame with complete list of individual parameter changes. 14 | # The changes are a list of named tuples, where each tuple is (Par, previous value) 15 | def onValuesChanged(changes): 16 | for c in changes: 17 | # use par.eval() to get current value 18 | par = c.par 19 | prev = c.prev 20 | return 21 | 22 | def onPulse(par): 23 | parent.save.Par_functions(par) 24 | return 25 | 26 | def onExpressionChange(par, val, prev): 27 | return 28 | 29 | def onExportChange(par, val, prev): 30 | return 31 | 32 | def onEnableChange(par, val, prev): 33 | return 34 | 35 | def onModeChange(par, val, prev): 36 | return 37 | -------------------------------------------------------------------------------- /dev/_startDev.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | rem turn off echo 3 | 4 | rem set TouchDesigner build numbers 5 | set TOUCHVERSION=2023.11880 6 | 7 | rem set our project file target 8 | set TOEFILE="dev-env.toe" 9 | 10 | rem set the rest of our paths for executables 11 | set TOUCHDIR=%PROGRAMFILES%\Derivative\TouchDesigner. 12 | set TOUCHEXE=\bin\TouchDesigner.exe 13 | 14 | rem set dev flag to true 15 | set DEV=TRUE 16 | set BLENDMONITORDATA=%~dp0_public 17 | 18 | rem combine our elements so we have a single path to our TouchDesigner.exe 19 | set TOUCHPATH="%TOUCHDIR%%TOUCHVERSION%%TOUCHEXE%" 20 | 21 | 22 | if exist %TOUCHPATH% goto :STARTPROJECT 23 | 24 | echo Touch Version: %TOUCHVERSION% is not installed. 25 | 26 | CHOICE /M "Download" 27 | if %errorlevel% equ 1 goto :DOWNLOAD 28 | if %errorlevel% equ 2 goto :eof 29 | 30 | 31 | :DOWNLOAD 32 | echo Downloading... 33 | rem download version that isn't installed. 34 | start "" https://download.derivative.ca/TouchDesigner.%TOUCHVERSION%.exe 35 | goto :eof 36 | 37 | 38 | :STARTPROJECT 39 | rem start our project file with the target TD installation 40 | start "" %TOUCHPATH% %TOEFILE% 41 | 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Matthew Ragan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /dev/td-modules/base_save/execute_check_on_exit.py: -------------------------------------------------------------------------------- 1 | # me - this DAT 2 | # 3 | # frame - the current frame 4 | # state - True if the timeline is paused 5 | # 6 | # Make sure the corresponding toggle is enabled in the Execute DAT. 7 | 8 | def onStart(): 9 | return 10 | 11 | def onCreate(): 12 | return 13 | 14 | def onExit(): 15 | prompt() 16 | return 17 | 18 | def onFrameStart(frame): 19 | return 20 | 21 | def onFrameEnd(frame): 22 | return 23 | 24 | def onPlayStateChange(state): 25 | return 26 | 27 | def onDeviceChange(): 28 | return 29 | 30 | def onProjectPreSave(): 31 | return 32 | 33 | def onProjectPostSave(): 34 | return 35 | 36 | def findDirtyTox(): 37 | op('table_dirty_ops').clear(keepFirstRow=True) 38 | children = op.Project.findChildren(type=COMP) 39 | for each in children: 40 | if each.dirty: 41 | op('table_dirty_ops').appendRow([each.name, each.path]) 42 | 43 | op('window1').par.winopen.pulse() 44 | 45 | def prompt(): 46 | bodyCopy = '''Before you leave, 47 | do you want to check to see if there are 48 | any unsaved toxes in your network?''' 49 | 50 | msg = ui.messageBox("Check All External Toxes", bodyCopy, buttons=["Yes", "No"]) 51 | if msg: 52 | pass 53 | 54 | else: 55 | parent().Find_dirty_tox() -------------------------------------------------------------------------------- /dev/td-modules/tools/collect_files.py: -------------------------------------------------------------------------------- 1 | import wget 2 | import json 3 | import os 4 | 5 | # vars for execution 6 | release_dir = "releaseVersions" 7 | json_file_ext = "releaseFiles.json" 8 | 9 | # a function to check for and create our release_dir if necessary 10 | def check_for_dir(): 11 | if os.path.isdir(release_dir): 12 | pass 13 | else: 14 | print("Creating releaseVersions directory") 15 | os.mkdir(release_dir) 16 | pass 17 | 18 | # a function to delete old versions 19 | def delete_old_files(del_dir): 20 | # loop through all files in directory and delete them 21 | for each_file in os.listdir(del_dir): 22 | target_file = '{}\{}'.format(del_dir, each_file) 23 | os.remove(target_file) 24 | print('deleting {}'.format(target_file)) 25 | pass 26 | 27 | # a function to download new files as described in external file 28 | def download_files(json_file_with_externals): 29 | # open extenral file and create python dictionary out of json 30 | allRemotes = open(json_file_with_externals, "r") 31 | workingDict = json.load(allRemotes) 32 | allRemotes.close() 33 | 34 | # loop through all entries and download them to the directory specified 35 | for each_remote in workingDict['targets']: 36 | save_name = each_remote['name'] 37 | target_url = each_remote['url'] 38 | 39 | target_path = "{dir}\\{file}.tox".format(dir=release_dir, file=save_name) 40 | wget.download(target_url, target_path) 41 | 42 | print("\ndownloading {save_url} \nsaving to {target_path} \n".format(target_path=target_path, save_url=target_url)) 43 | pass 44 | 45 | # check for directory 46 | print("Checking for releaseVersions directory") 47 | check_for_dir() 48 | 49 | # delete old files 50 | print("Deleting Old Files") 51 | delete_old_files(release_dir) 52 | 53 | print('- ' * 5) 54 | print('\n') 55 | 56 | # download latest release versions - these are all from master branches 57 | print("Downloading New Files") 58 | download_files(json_file_ext) -------------------------------------------------------------------------------- /dev/scripts/parHelperEXT.py: -------------------------------------------------------------------------------- 1 | class par_helper: 2 | '''A generalized parameter helper for simple par callbacks 3 | 4 | The intention of this extension is to provide a slim interface 5 | for easily scritable actions intended to be connected to custom 6 | changes. The model assumes that the developer would like to 7 | call a scriptable action whose function name matches the parameter 8 | name. 9 | ''' 10 | def __init__(self, my_op, actionsMOD, debug=False): 11 | self.My_op = my_op 12 | self.actions = actionsMOD 13 | self.Debug = debug 14 | 15 | def Par_functions(self, par): 16 | '''Attempts to match parameters to appropriatly scoped 17 | functions. 18 | ''' 19 | if par.isPulse: 20 | self.pulse_function(par) 21 | 22 | elif len(par.tuplet) > 1: 23 | self.tuplet_function(par) 24 | 25 | else: 26 | self.general_function(par) 27 | 28 | def pulse_function(self, par): 29 | '''Pulse functions are only one frame long, and are 30 | considered "instant" to touchdesigner. They do not offer 31 | any additional information, they're only an event. 32 | ''' 33 | try: 34 | func = getattr(self.actions, par.name) 35 | func() 36 | except Exception as e: 37 | if self.Debug: 38 | debug(e) 39 | print("This par has no matching function") 40 | 41 | def tuplet_function(self, par): 42 | '''TouchDesigner tuplets have a slight variation 43 | in their parameter pattern. An RGB parameter, for example 44 | will have r g and b appended to the end of the user's 45 | descriptive parameter name. A tuplet_function allows us 46 | to target by Tuplet name, retrieving all pars in the tuplet. 47 | ''' 48 | try: 49 | func = getattr(self.actions, par.tupletName) 50 | func(par.tuplet, par) 51 | except Exception as e: 52 | if self.Debug: 53 | debug(e) 54 | print("This par has no matching function") 55 | 56 | def general_function(self, par): 57 | '''Other TouchDesigner parameters follow a consistent enough 58 | pattern, that we safely map the parameter name and argument 59 | to a set of provided actions. 60 | ''' 61 | try: 62 | func = getattr(self.actions, par.name) 63 | func(par) 64 | except Exception as e: 65 | if self.Debug: 66 | debug(e) 67 | print("This par has no matching function") -------------------------------------------------------------------------------- /dev/td-modules/base_save/parHelperEXT.py: -------------------------------------------------------------------------------- 1 | class par_helper: 2 | '''A generalized parameter helper for simple par callbacks 3 | 4 | The intention of this extension is to provide a slim interface 5 | for easily scritable actions intended to be connected to custom 6 | changes. The model assumes that the developer would like to 7 | call a scriptable action whose function name matches the parameter 8 | name. 9 | ''' 10 | def __init__(self, my_op, actionsMOD, debug=False): 11 | self.My_op = my_op 12 | self.actions = actionsMOD 13 | self.Debug = debug 14 | 15 | def Par_functions(self, par): 16 | '''Attempts to match parameters to appropriatly scoped 17 | functions. 18 | ''' 19 | if par.isPulse: 20 | self.pulse_function(par) 21 | 22 | elif len(par.tuplet) > 1: 23 | self.tuplet_function(par) 24 | 25 | else: 26 | self.general_function(par) 27 | 28 | def pulse_function(self, par): 29 | '''Pulse functions are only one frame long, and are 30 | considered "instant" to touchdesigner. They do not offer 31 | any additional information, they're only an event. 32 | ''' 33 | try: 34 | func = getattr(self.actions, par.name) 35 | func() 36 | except Exception as e: 37 | if self.Debug: 38 | debug(e) 39 | print("This par has no matching function") 40 | 41 | def tuplet_function(self, par): 42 | '''TouchDesigner tuplets have a slight variation 43 | in their parameter pattern. An RGB parameter, for example 44 | will have r g and b appended to the end of the user's 45 | descriptive parameter name. A tuplet_function allows us 46 | to target by Tuplet name, retrieving all pars in the tuplet. 47 | ''' 48 | try: 49 | func = getattr(self.actions, par.tupletName) 50 | func(par.tuplet, par) 51 | except Exception as e: 52 | if self.Debug: 53 | debug(e) 54 | print("This par has no matching function") 55 | 56 | def general_function(self, par): 57 | '''Other TouchDesigner parameters follow a consistent enough 58 | pattern, that we safely map the parameter name and argument 59 | to a set of provided actions. 60 | ''' 61 | try: 62 | func = getattr(self.actions, par.name) 63 | func(par) 64 | except Exception as e: 65 | if self.Debug: 66 | debug(e) 67 | print("This par has no matching function") -------------------------------------------------------------------------------- /dev/td-modules/base_save/listerFuncs.py: -------------------------------------------------------------------------------- 1 | import saveUtils 2 | POP_MENU = parent.save.op('popMenu') 3 | 4 | 5 | def parse_col(info: dict) -> None: 6 | col_name_func_map = { 7 | "save": save_tox, 8 | "view": view_tox, 9 | "show_file": open_folder, 10 | "reload": reinit_tox, 11 | "reload_ext": reinit_extensions 12 | } 13 | 14 | row = info.get('row') 15 | col_name = info.get('colName') 16 | 17 | # skip clicks that are not on a valid row 18 | if row != -1: 19 | # find function by column name 20 | func = col_name_func_map.get(col_name) 21 | 22 | # handle any possible None objects 23 | if func != None: 24 | # call function and pass the info object 25 | func(info) 26 | else: 27 | pass 28 | pass 29 | 30 | else: 31 | pass 32 | 33 | 34 | def parse_right_click(info: dict) -> None: 35 | """ 36 | """ 37 | row = info.get('row') 38 | col_name = info.get('colName') 39 | 40 | if row != -1 and col_name == 'version': 41 | def pop_cb(cb_info: dict): 42 | pop_menu_selection(cb_info) 43 | 44 | POP_MENU.Open(callback=pop_cb, callbackDetails=info) 45 | pass 46 | 47 | else: 48 | pass 49 | 50 | 51 | def pop_menu_selection(info: dict) -> None: 52 | """Runs based on the pop menu selection 53 | """ 54 | current_tox = _get_op_from_tox_path(info.get("details")) 55 | current_version = current_tox.par['Toxversion'].eval() 56 | version_split = current_version.split('.') 57 | split_ints = [int(each) for each in version_split] 58 | index = info.get("index") 59 | 60 | if index == 0: 61 | version_update = f'{split_ints[0]+1}.0.0' 62 | 63 | elif index == 1: 64 | version_update = f'{split_ints[0]}.{split_ints[1]+1}.0' 65 | else: 66 | version_update = '' 67 | pass 68 | 69 | parent.save.Save_over_tox(current_tox, version_update) 70 | 71 | 72 | def _get_op_from_tox_path(info: dict) -> callable: 73 | return op(info.get('rowData').get('toxPath')) 74 | 75 | 76 | def save_tox(info: dict) -> None: 77 | parent.save.Save_over_tox(_get_op_from_tox_path(info)) 78 | 79 | 80 | def view_tox(info: dict) -> None: 81 | """Moves current pane to view the contents of the tox / COMP 82 | """ 83 | saveUtils.Open_network_location(_get_op_from_tox_path(info)) 84 | 85 | 86 | def open_folder(info: dict) -> None: 87 | """Opens containing directory for a tox file 88 | """ 89 | target_op = _get_op_from_tox_path(info) 90 | ui.viewFile(target_op.par.externaltox.eval(), showInFolder=True) 91 | 92 | 93 | def reinit_tox(info: dict) -> None: 94 | """Reloads an entire TOX 95 | """ 96 | target_op = _get_op_from_tox_path(info) 97 | target_op.par.reinitnet.pulse() 98 | 99 | 100 | def reinit_extensions(info: dict) -> None: 101 | """Reinitialize the extension for an COMP 102 | """ 103 | target_op = _get_op_from_tox_path(info) 104 | target_op.par.reinitextensions.pulse() 105 | -------------------------------------------------------------------------------- /dev/scripts/saveUtils.py: -------------------------------------------------------------------------------- 1 | """ 2 | SudoMagic | sudomagic.com 3 | Authors | Matthew Ragan, Ian Shelanskey 4 | Contact | contact@sudomagic.com 5 | """ 6 | 7 | # pure python 8 | import hashlib 9 | from datetime import datetime 10 | 11 | EXCLUDE_OPS = [ 12 | "eval", 13 | "keyboardin", 14 | "opfind", 15 | "folder", 16 | "examine", 17 | "select", 18 | "udpout", 19 | "udpin", 20 | "fifo", 21 | "script", 22 | "null", 23 | "info"] 24 | 25 | DEFAULT_WORKSHEET_COLOR = (0.1, 0.105, .12) 26 | 27 | 28 | def gen_hash_from_op(td_operator: callable) -> callable: 29 | """TODO: complete doc strings 30 | 31 | Args 32 | --------------- 33 | td_operator (`TD_Operator`) 34 | > TouchDesigner operator to generate hash from 35 | 36 | Returns 37 | --------------- 38 | op_hash (`callable`) 39 | > Hash generated from TouchDesigner operator's data 40 | """ 41 | 42 | allPars = [] 43 | 44 | # find all non-external children of an operator 45 | all_children = get_non_external_children(td_operator) 46 | 47 | # generate a dict of elements that may have changed 48 | for each_child in all_children: 49 | child_dict = {} 50 | child_dict['nodeX'] = each_child.nodeX 51 | child_dict['nodeY'] = each_child.nodeY 52 | child_dict['nodeWidth'] = each_child.nodeWidth 53 | child_dict['nodeHeight'] = each_child.nodeHeight 54 | child_dict['color'] = each_child.color 55 | child_dict['path'] = each_child.path 56 | child_dict['pars_dict'] = {} 57 | 58 | for each_par in each_child.pars(): 59 | if each_par.mode != ParMode.EXPRESSION: 60 | if each_par.page == "About": 61 | pass 62 | else: 63 | child_dict['pars_dict'][each_par.name] = each_par.val 64 | allPars.append(child_dict) 65 | 66 | # generate a hash of changed elements 67 | op_hash = allPars 68 | return op_hash 69 | 70 | 71 | def find_all_dats() -> list: 72 | """Returns a list of all external dats 73 | """ 74 | external_dats = [] 75 | exclude_list = EXCLUDE_OPS 76 | 77 | for eachOp in root.findChildren(type=DAT): 78 | if eachOp.type in exclude_list: 79 | pass 80 | 81 | else: 82 | if eachOp.par['file'] != '': 83 | external_dats.append(eachOp) 84 | else: 85 | pass 86 | return external_dats 87 | 88 | 89 | def find_all_comments() -> list: 90 | comment_ops = [] 91 | for eachOp in root.findChildren(type=annotateCOMP): 92 | comment_ops.append(eachOp) 93 | return comment_ops 94 | 95 | 96 | def find_external_ops(): 97 | """Returns a list of all external comps 98 | """ 99 | children = root.findChildren(type=COMP) 100 | external_ops = [ 101 | eachChild for eachChild in children if eachChild.par.externaltox != ''] 102 | return external_ops 103 | 104 | 105 | def get_non_external_children(target_op) -> list: 106 | 107 | # look first at only one layer deep 108 | immediate_children = target_op.findChildren(depth=1) 109 | non_ext_children = [] 110 | 111 | # add ops that are not external to our list 112 | for each_immediate_child in immediate_children: 113 | if each_immediate_child.family == "COMP": 114 | if each_immediate_child.par.externaltox.eval() != '': 115 | pass 116 | else: 117 | non_ext_children.append(each_immediate_child) 118 | else: 119 | non_ext_children.append(each_immediate_child) 120 | 121 | # for each non-external op, find their children 122 | for each_child in non_ext_children: 123 | if each_child.family == "COMP": 124 | grand_children = each_child.findChildren(depth=1) 125 | 126 | non_ext_children.extend(grand_children) 127 | else: 128 | pass 129 | 130 | return non_ext_children 131 | 132 | 133 | def ext_parent(target_op) -> callable: 134 | ext_parent = None 135 | parent_paths = [] 136 | parent_paths_parts = target_op.parent().path.split('/') 137 | last_path = '' 138 | 139 | for each_index in parent_paths_parts[1:]: 140 | last_path = f"{last_path}/{each_index}" 141 | parent_paths.append(last_path) 142 | 143 | parent_paths.reverse() 144 | 145 | for each_path in parent_paths: 146 | if op(each_path).par.externaltox != '': 147 | ext_parent = op(each_path) 148 | break 149 | else: 150 | continue 151 | 152 | return ext_parent 153 | 154 | 155 | def current_save_time() -> str: 156 | today = datetime.now() 157 | time_str = f"{today.year}-{today.month}-{today.day} {today.hour:02d}:{today.minute:02d}:{today.second:02d}" 158 | return time_str 159 | 160 | 161 | def flash_bg(flash_color: tuple, duration: int) -> None: 162 | """ Flashes the background of TouchDesigner 163 | 164 | Used to flash the background of the TD network. 165 | 166 | Notes 167 | --------- 168 | This is a simple tool to flash indicator colors in the 169 | background to help you have some visual confirmation that 170 | you have in fact externalized a file. 171 | 172 | Args 173 | --------- 174 | flash_color (tuple): 175 | > this is the string name to match against the parent's pars() 176 | > for to pull colors to use for changing the background 177 | 178 | duration (int): 179 | > length in frames for how long the color should replace the background 180 | 181 | Returns 182 | --------- 183 | none 184 | """ 185 | 186 | ui.colors['worksheet.bg'] = flash_color 187 | delay_script = "ui.colors['worksheet.bg'] = args[0]" 188 | 189 | # want to change the background color back 190 | run(delay_script, DEFAULT_WORKSHEET_COLOR, delayFrames=duration) 191 | 192 | return 193 | 194 | 195 | def Open_network_location(network_location): 196 | """ Moves network to save location 197 | """ 198 | ui.panes.current.owner = network_location 199 | ui.panes.current.home() 200 | -------------------------------------------------------------------------------- /dev/td-modules/base_save/saveUtils.py: -------------------------------------------------------------------------------- 1 | """ 2 | SudoMagic | sudomagic.com 3 | Authors | Matthew Ragan, Ian Shelanskey 4 | Contact | contact@sudomagic.com 5 | """ 6 | 7 | # pure python 8 | import hashlib 9 | from datetime import datetime 10 | 11 | 12 | DEFAULT_WORKSHEET_COLOR = (0.1, 0.105, .12) 13 | 14 | 15 | def gen_hash_from_op(td_operator: callable) -> callable: 16 | """TODO: complete doc strings 17 | 18 | Args 19 | --------------- 20 | td_operator (`TD_Operator`) 21 | > TouchDesigner operator to generate hash from 22 | 23 | Returns 24 | --------------- 25 | op_hash (`callable`) 26 | > Hash generated from TouchDesigner operator's data 27 | """ 28 | 29 | allPars = [] 30 | 31 | # find all non-external children of an operator 32 | all_children = get_non_external_children(td_operator) 33 | 34 | # generate a dict of elements that may have changed 35 | for each_child in all_children: 36 | child_dict = {} 37 | child_dict['nodeX'] = each_child.nodeX 38 | child_dict['nodeY'] = each_child.nodeY 39 | child_dict['nodeWidth'] = each_child.nodeWidth 40 | child_dict['nodeHeight'] = each_child.nodeHeight 41 | child_dict['color'] = each_child.color 42 | child_dict['path'] = each_child.path 43 | child_dict['pars_dict'] = {} 44 | 45 | for each_par in each_child.pars(): 46 | if each_par.mode != ParMode.EXPRESSION: 47 | if each_par.page == "About": 48 | pass 49 | else: 50 | child_dict['pars_dict'][each_par.name] = each_par.val 51 | allPars.append(child_dict) 52 | 53 | # generate a hash of changed elements 54 | op_hash = allPars 55 | return op_hash 56 | 57 | 58 | def find_all_dats() -> list: 59 | """Returns a list of all external dats 60 | 61 | This will discard ops that do not have a "file" parameter 62 | """ 63 | external_dats = [] 64 | 65 | # locates all DATs in project 66 | all_dats = root.findChildren(type=DAT) 67 | 68 | # reduce all dats to only those with a "file" par 69 | only_file_ops = [each for each in all_dats if each.pars("file")] 70 | 71 | # reduce list to only ops with an external file 72 | for eachOp in only_file_ops: 73 | if eachOp.par['file'] != '': 74 | external_dats.append(eachOp) 75 | else: 76 | pass 77 | return external_dats 78 | 79 | 80 | def find_all_comments() -> list: 81 | comment_ops = [] 82 | for eachOp in root.findChildren(type=annotateCOMP): 83 | comment_ops.append(eachOp) 84 | return comment_ops 85 | 86 | 87 | def find_external_ops(): 88 | """Returns a list of all external comps 89 | """ 90 | children = root.findChildren(type=COMP) 91 | external_ops = [ 92 | eachChild for eachChild in children if eachChild.par.externaltox != ''] 93 | return external_ops 94 | 95 | 96 | def get_non_external_children(target_op) -> list: 97 | 98 | # look first at only one layer deep 99 | immediate_children = target_op.findChildren(depth=1) 100 | non_ext_children = [] 101 | 102 | # add ops that are not external to our list 103 | for each_immediate_child in immediate_children: 104 | if each_immediate_child.family == "COMP": 105 | if each_immediate_child.par.externaltox.eval() != '': 106 | pass 107 | else: 108 | non_ext_children.append(each_immediate_child) 109 | else: 110 | non_ext_children.append(each_immediate_child) 111 | 112 | # for each non-external op, find their children 113 | for each_child in non_ext_children: 114 | if each_child.family == "COMP": 115 | grand_children = each_child.findChildren(depth=1) 116 | 117 | non_ext_children.extend(grand_children) 118 | else: 119 | pass 120 | 121 | return non_ext_children 122 | 123 | 124 | def ext_parent(target_op) -> callable: 125 | ext_parent = None 126 | parent_paths = [] 127 | parent_paths_parts = target_op.parent().path.split('/') 128 | last_path = '' 129 | 130 | for each_index in parent_paths_parts[1:]: 131 | last_path = f"{last_path}/{each_index}" 132 | parent_paths.append(last_path) 133 | 134 | parent_paths.reverse() 135 | 136 | for each_path in parent_paths: 137 | if op(each_path).par.externaltox != '': 138 | ext_parent = op(each_path) 139 | break 140 | else: 141 | continue 142 | 143 | return ext_parent 144 | 145 | 146 | def current_save_time() -> str: 147 | today = datetime.now() 148 | time_str = f"{today.year}-{today.month}-{today.day} {today.hour:02d}:{today.minute:02d}:{today.second:02d}" 149 | return time_str 150 | 151 | 152 | def flash_bg(flash_color: tuple, duration: int) -> None: 153 | """ Flashes the background of TouchDesigner 154 | 155 | Used to flash the background of the TD network. 156 | 157 | Notes 158 | --------- 159 | This is a simple tool to flash indicator colors in the 160 | background to help you have some visual confirmation that 161 | you have in fact externalized a file. 162 | 163 | Args 164 | --------- 165 | flash_color (tuple): 166 | > this is the string name to match against the parent's pars() 167 | > for to pull colors to use for changing the background 168 | 169 | duration (int): 170 | > length in frames for how long the color should replace the background 171 | 172 | Returns 173 | --------- 174 | none 175 | """ 176 | 177 | ui.colors['worksheet.bg'] = flash_color 178 | delay_script = "ui.colors['worksheet.bg'] = args[0]" 179 | 180 | # want to change the background color back 181 | run(delay_script, DEFAULT_WORKSHEET_COLOR, delayFrames=duration) 182 | 183 | return 184 | 185 | 186 | def Open_network_location(network_location): 187 | """ Moves network to save location 188 | """ 189 | ui.panes.current.owner = network_location 190 | ui.panes.current.home() 191 | 192 | 193 | def Reload_tox_extension(comp): 194 | """Pulses the reinitextensions par for any COMP 195 | 196 | The intended use of this function is to reinit the extensions for any 197 | external op 198 | """ 199 | comp.par.reinitextensions.pulse() 200 | 201 | 202 | def Reload_tox(comp): 203 | """Pulses the reinitnet par for any COMP 204 | 205 | The intended use of this function is to reload TOX 206 | """ 207 | comp.par.reinitnet.pulse() 208 | -------------------------------------------------------------------------------- /dev/scripts/saveOp.py: -------------------------------------------------------------------------------- 1 | """ 2 | SudoMagic | sudomagic.com 3 | Authors | Matthew Ragan, Ian Shelanskey 4 | Contact | contact@sudomagic.com 5 | """ 6 | 7 | # td python mods 8 | import saveUtils 9 | 10 | 11 | class SaveOpManager: 12 | """SaveOpManager 13 | """ 14 | 15 | def __init__(self) -> None: 16 | """TODO: complete doc strings 17 | 18 | Args 19 | --------------- 20 | self (`callable`) 21 | > Class instance 22 | 23 | Returns 24 | --------------- 25 | None 26 | """ 27 | 28 | self.External_ops: list = [] 29 | self._build_op_list() 30 | 31 | def add_save_op(self, td_operator: callable) -> None: 32 | """TODO: complete doc strings 33 | 34 | Args 35 | --------------- 36 | self (`callable`) 37 | > Class instance 38 | 39 | Returns 40 | --------------- 41 | None 42 | """ 43 | new_save_op: SaveOp = SaveOp(td_operator) 44 | self.External_ops.append(new_save_op) 45 | pass 46 | 47 | def check_hash_status(self) -> None: 48 | """TODO: complete doc strings 49 | 50 | Args 51 | --------------- 52 | self (`callable`) 53 | > Class instance 54 | 55 | Returns 56 | --------------- 57 | None 58 | """ 59 | pass 60 | 61 | @property 62 | def external_ops(self) -> None: 63 | """Returns a list of all external comps 64 | """ 65 | children = root.findChildren(type=COMP) 66 | external_ops = [ 67 | eachChild for eachChild in children if eachChild.par.externaltox != ''] 68 | return external_ops 69 | 70 | @property 71 | def dirty_ops(self) -> None: 72 | """TODO: complete doc strings 73 | 74 | Args 75 | --------------- 76 | self (`callable`) 77 | > Class instance 78 | 79 | Returns 80 | --------------- 81 | None 82 | """ 83 | 84 | dirty_op_list = [ 85 | each_ext_op for each_ext_op in self.External_ops if each_ext_op.is_dirty] 86 | return dirty_op_list 87 | 88 | def _build_op_list(self) -> None: 89 | """TODO: complete doc strings 90 | 91 | Args 92 | --------------- 93 | self (`callable`) 94 | > Class instance 95 | 96 | Returns 97 | --------------- 98 | None 99 | """ 100 | 101 | externals = self.external_ops 102 | external_list = [] 103 | 104 | for each in externals: 105 | new_save_op: SaveOp = SaveOp(each) 106 | self.External_ops.append(new_save_op) 107 | 108 | def Check_external_ops(self) -> None: 109 | all_externals = self.external_ops 110 | tracked_ids = [each.id for each in self.External_ops] 111 | for each_external in all_externals: 112 | if each_external.id in tracked_ids: 113 | pass 114 | else: 115 | self.add_save_op(each_external) 116 | print(f"adding new tracked external op {each_external}") 117 | 118 | def Dirty_check(self) -> None: 119 | self.Check_external_ops() 120 | for each_index, each_op in enumerate(self.External_ops): 121 | if each_op.td_op.valid: 122 | each_op: SaveOp = each_op 123 | each_op.dirty_check() 124 | else: 125 | self.External_ops.pop(each_index) 126 | 127 | def Ignore_current_dirty_state(self, target_op_id: int) -> None: 128 | target_op: SaveOp = self.get_save_op_by_id(target_op_id) 129 | target_op.op_hash = saveUtils.gen_hash_from_op(target_op.td_op) 130 | target_op.is_dirty = False 131 | 132 | def get_save_op_by_id(self, op_id: int) -> callable: 133 | op_by_id = None 134 | 135 | for each_op in self.External_ops: 136 | each_op: SaveOp = each_op 137 | if each_op.id == op_id: 138 | op_by_id = each_op 139 | break 140 | 141 | return each_op 142 | 143 | def Update_save_op_by_path(self, path: str, value: bool): 144 | """TODO: complete doc strings 145 | 146 | Args 147 | --------------- 148 | self (`callable`) 149 | > Class instance 150 | 151 | Returns 152 | --------------- 153 | None 154 | """ 155 | # for each_save_op_index, each_save_op in enumerate(self.External_ops): 156 | # each_save_op: SaveOp = each_save_op 157 | # if each_save_op.td_op.path == path: 158 | # each_save_op.is_dirty = value 159 | # each_save_op.op_hash = saveUtils.gen_hash_from_op(op(path)) 160 | # break 161 | # else: 162 | # pass 163 | pass 164 | 165 | 166 | class SaveOp: 167 | """SaveOpManager 168 | """ 169 | 170 | def __init__(self, td_operator: callable) -> None: 171 | """Class Object Init 172 | 173 | Notes 174 | --------- 175 | 176 | Args 177 | --------- 178 | self (`callable`) 179 | > Class instance 180 | 181 | td_operator (`callable`) 182 | > A TouchDesigner operator this save_op is built from 183 | 184 | op_hash (`callable`) 185 | > A has constructed from the state of the external op 186 | 187 | Returns 188 | --------- 189 | None 190 | """ 191 | self._ext_op = td_operator 192 | self._is_dirty = tdu.Dependency(False) 193 | self.id = td_operator.id 194 | return 195 | 196 | def dirty_check(self) -> None: 197 | """TODO: complete doc strings 198 | 199 | Args 200 | --------------- 201 | self (`callable`) 202 | > Class instance 203 | 204 | Returns 205 | --------------- 206 | None 207 | """ 208 | 209 | old_hash = self._op_hash 210 | new_hash = saveUtils.gen_hash_from_op(self._ext_op) 211 | if new_hash != old_hash: 212 | self.is_dirty = True 213 | debug(f"{self._ext_op} is dirty") 214 | else: 215 | self.is_dirty = False 216 | 217 | @property 218 | def op_hash(self) -> callable: 219 | return self._op_hash 220 | 221 | @op_hash.setter 222 | def op_hash(self, new_hash: callable) -> None: 223 | self._op_hash = new_hash 224 | 225 | @property 226 | def td_op(self) -> callable: 227 | return self._ext_op 228 | 229 | @property 230 | def is_dirty(self) -> bool: 231 | """is_dirty getter 232 | 233 | Returns tdu.Dependency value 234 | 235 | Args 236 | --------------- 237 | self (`callable`) 238 | > Class instance 239 | 240 | state (`bool`) 241 | > The new state for the saveOp 242 | 243 | Returns 244 | --------------- 245 | is_dirty (`bool`) 246 | > The dirty state as a boolean 247 | """ 248 | return self._is_dirty.val 249 | 250 | @is_dirty.setter 251 | def is_dirty(self, state: bool): 252 | """is_dirty setter 253 | 254 | Updates tdu.Dependency value 255 | 256 | Args 257 | --------------- 258 | self (`callable`) 259 | > Class instance 260 | 261 | state (`bool`) 262 | > The new state for the saveOp 263 | 264 | Returns 265 | --------------- 266 | None 267 | """ 268 | self._is_dirty.val = state 269 | 270 | @property 271 | def last_saved(self) -> callable: 272 | if self._ext_op.par['Lastsaved']: 273 | return self._ext_op.par.Lastsaved 274 | else: 275 | return None 276 | 277 | @property 278 | def version(self) -> callable: 279 | if self._ext_op.par['Toxversion']: 280 | return self._ext_op.par.Toxversion 281 | else: 282 | return None 283 | 284 | @property 285 | def ext_path(self) -> callable: 286 | return self._ext_op.par.Externaltox 287 | 288 | @property 289 | def tags(self) -> callable: 290 | if 'submodule' in self._ext_op.tags: 291 | return 'submodule' 292 | elif 'devTool' in self._ext_op.tags: 293 | return 'DevTool' 294 | else: 295 | return '' 296 | -------------------------------------------------------------------------------- /dev/td-modules/base_save/saveOp.py: -------------------------------------------------------------------------------- 1 | """ 2 | SudoMagic | sudomagic.com 3 | Authors | Matthew Ragan, Ian Shelanskey 4 | Contact | contact@sudomagic.com 5 | """ 6 | 7 | # td python mods 8 | import saveUtils 9 | 10 | 11 | class SaveOpManager: 12 | """SaveOpManager 13 | """ 14 | 15 | def __init__(self, ext_ops_DAT) -> None: 16 | """TODO: complete doc strings 17 | 18 | Args 19 | --------------- 20 | self (`callable`) 21 | > Class instance 22 | 23 | Returns 24 | --------------- 25 | None 26 | """ 27 | self.Ext_ops_DAT = ext_ops_DAT 28 | self.External_ops: list = [] 29 | self._build_op_list() 30 | 31 | def add_save_op(self, td_operator: callable) -> None: 32 | """TODO: complete doc strings 33 | 34 | Args 35 | --------------- 36 | self (`callable`) 37 | > Class instance 38 | 39 | Returns 40 | --------------- 41 | None 42 | """ 43 | new_save_op: SaveOp = SaveOp(td_operator) 44 | self.External_ops.append(new_save_op) 45 | pass 46 | 47 | def check_hash_status(self) -> None: 48 | """TODO: complete doc strings 49 | 50 | Args 51 | --------------- 52 | self (`callable`) 53 | > Class instance 54 | 55 | Returns 56 | --------------- 57 | None 58 | """ 59 | pass 60 | 61 | @property 62 | def external_ops(self) -> None: 63 | """Returns a list of all external comps 64 | """ 65 | children = root.findChildren(type=COMP) 66 | external_ops = [ 67 | eachChild for eachChild in children if eachChild.par.externaltox != ''] 68 | return external_ops 69 | 70 | @property 71 | def dirty_ops(self) -> None: 72 | """TODO: complete doc strings 73 | 74 | Args 75 | --------------- 76 | self (`callable`) 77 | > Class instance 78 | 79 | Returns 80 | --------------- 81 | None 82 | """ 83 | 84 | dirty_op_list = [ 85 | each_ext_op for each_ext_op in self.External_ops if each_ext_op.is_dirty] 86 | return dirty_op_list 87 | 88 | def _build_op_list(self) -> None: 89 | """TODO: complete doc strings 90 | 91 | Args 92 | --------------- 93 | self (`callable`) 94 | > Class instance 95 | 96 | Returns 97 | --------------- 98 | None 99 | """ 100 | 101 | externals = self.external_ops 102 | external_list = [] 103 | 104 | for each in externals: 105 | new_save_op: SaveOp = SaveOp(each) 106 | self.External_ops.append(new_save_op) 107 | 108 | def Check_external_ops(self) -> None: 109 | all_externals = self.external_ops 110 | tracked_ids = [each.id for each in self.External_ops] 111 | for each_external in all_externals: 112 | if each_external.id in tracked_ids: 113 | pass 114 | else: 115 | self.add_save_op(each_external) 116 | print(f"adding new tracked external op {each_external}") 117 | 118 | # force cook DAT with list of all ops 119 | self.Ext_ops_DAT.cook(force=True) 120 | 121 | def Dirty_check(self) -> None: 122 | self.Check_external_ops() 123 | for each_index, each_op in enumerate(self.External_ops): 124 | if each_op.td_op.valid: 125 | each_op: SaveOp = each_op 126 | each_op.dirty_check() 127 | else: 128 | self.External_ops.pop(each_index) 129 | 130 | def Ignore_current_dirty_state(self, target_op_id: int) -> None: 131 | target_op: SaveOp = self.get_save_op_by_id(target_op_id) 132 | target_op.op_hash = saveUtils.gen_hash_from_op(target_op.td_op) 133 | target_op.is_dirty = False 134 | 135 | def get_save_op_by_id(self, op_id: int) -> callable: 136 | op_by_id = None 137 | 138 | for each_op in self.External_ops: 139 | each_op: SaveOp = each_op 140 | if each_op.id == op_id: 141 | op_by_id = each_op 142 | break 143 | 144 | return each_op 145 | 146 | def Update_save_op_by_path(self, path: str, value: bool): 147 | """TODO: complete doc strings 148 | 149 | Args 150 | --------------- 151 | self (`callable`) 152 | > Class instance 153 | 154 | Returns 155 | --------------- 156 | None 157 | """ 158 | # for each_save_op_index, each_save_op in enumerate(self.External_ops): 159 | # each_save_op: SaveOp = each_save_op 160 | # if each_save_op.td_op.path == path: 161 | # each_save_op.is_dirty = value 162 | # each_save_op.op_hash = saveUtils.gen_hash_from_op(op(path)) 163 | # break 164 | # else: 165 | # pass 166 | pass 167 | 168 | 169 | class SaveOp: 170 | """SaveOpManager 171 | """ 172 | 173 | def __init__(self, td_operator: callable) -> None: 174 | """Class Object Init 175 | 176 | Notes 177 | --------- 178 | 179 | Args 180 | --------- 181 | self (`callable`) 182 | > Class instance 183 | 184 | td_operator (`callable`) 185 | > A TouchDesigner operator this save_op is built from 186 | 187 | op_hash (`callable`) 188 | > A has constructed from the state of the external op 189 | 190 | Returns 191 | --------- 192 | None 193 | """ 194 | self._ext_op = td_operator 195 | self._is_dirty = tdu.Dependency(False) 196 | self.id = td_operator.id 197 | return 198 | 199 | def dirty_check(self) -> None: 200 | """TODO: complete doc strings 201 | 202 | Args 203 | --------------- 204 | self (`callable`) 205 | > Class instance 206 | 207 | Returns 208 | --------------- 209 | None 210 | """ 211 | 212 | old_hash = self._op_hash 213 | new_hash = saveUtils.gen_hash_from_op(self._ext_op) 214 | if new_hash != old_hash: 215 | self.is_dirty = True 216 | debug(f"{self._ext_op} is dirty") 217 | else: 218 | self.is_dirty = False 219 | 220 | @property 221 | def op_hash(self) -> callable: 222 | return self._op_hash 223 | 224 | @op_hash.setter 225 | def op_hash(self, new_hash: callable) -> None: 226 | self._op_hash = new_hash 227 | 228 | @property 229 | def td_op(self) -> callable: 230 | return self._ext_op 231 | 232 | @property 233 | def is_dirty(self) -> bool: 234 | """is_dirty getter 235 | 236 | Returns tdu.Dependency value 237 | 238 | Args 239 | --------------- 240 | self (`callable`) 241 | > Class instance 242 | 243 | state (`bool`) 244 | > The new state for the saveOp 245 | 246 | Returns 247 | --------------- 248 | is_dirty (`bool`) 249 | > The dirty state as a boolean 250 | """ 251 | return self._is_dirty.val 252 | 253 | @is_dirty.setter 254 | def is_dirty(self, state: bool): 255 | """is_dirty setter 256 | 257 | Updates tdu.Dependency value 258 | 259 | Args 260 | --------------- 261 | self (`callable`) 262 | > Class instance 263 | 264 | state (`bool`) 265 | > The new state for the saveOp 266 | 267 | Returns 268 | --------------- 269 | None 270 | """ 271 | self._is_dirty.val = state 272 | 273 | @property 274 | def last_saved(self) -> callable: 275 | if self._ext_op.par['Lastsaved']: 276 | return self._ext_op.par.Lastsaved 277 | else: 278 | return None 279 | 280 | @property 281 | def version(self) -> callable: 282 | if self._ext_op.par['Toxversion']: 283 | return self._ext_op.par.Toxversion 284 | else: 285 | return None 286 | 287 | @property 288 | def ext_path(self) -> callable: 289 | return self._ext_op.par.Externaltox 290 | 291 | @property 292 | def tags(self) -> callable: 293 | if 'submodule' in self._ext_op.tags: 294 | return 'submodule' 295 | elif 'devTool' in self._ext_op.tags: 296 | return 'DevTool' 297 | else: 298 | return '' 299 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TouchDesigner Save External 2 | *a simple save external tox and text helper* 3 | [matthew ragan](matthewragan.com) | [SudoMagic](sudomagic.com) 4 | 5 | ## TouchDesigner Version 6 | * 099 2023.11880 7 | 8 | ## TOX Version 9 | 10 | * 1.8.x 11 | 12 | ## OS Support 13 | 14 | * Windows 10 15 | * macOS 16 | 17 | ## Summary 18 | Working with git and TouchDesigner isn't always an easy process, but it's often an essential part of the process of tracking your work and collaborating with others. It also encourages you to begin thinking about how to make your projects and components more modular, portable, and reuseable. Those aren't always easy practices to embrace, but they make a big difference in the amount of time you invest in future projects. It's often hard to plan for the gig in six months when you're worried about the gig on Friday - and we all have those sprints or last minute changes. 19 | 20 | It's also worth remember that no framework will ever be perfect - all of these things change and evolve over time, and that's the very idea behind externalizing pieces of your project's code-base. An assembly of concise individually maintainable tools is often more maintainable than [rube golbergian](https://en.wikipedia.org/wiki/Rube_Goldberg) contraption - and while it's certainly less cool, it does make it easier to make deadlines. 21 | 22 | So, what does all this have to do with saving external tox files? TOX files are the modules of TouchDesigner - they're component operators that can be saved as individual files and dropped into any network. These custom operators are made out of other operators. In 099 they can be set to be private if you have a pro license - keeping prying eyes away from your work (if you're worried about that). 23 | 24 | That makes these components excellent candidates for externalization, but it takes a little extra work to keep them saved and sycned. In a perfect world we would use the same saving mechanism that's employed to save our TOE file to also save any external file, or better yet, to ask us if we want to externalize a file. That, in fact, is the aim of this TOX. 25 | 26 | ## Supported File Types 27 | * `.tox` 28 | 29 | 30 | # Parameters 31 | ![base save and pars](assets/base_save_and_pars_v2.PNG) 32 | 33 | ## Save Settings 34 | 35 | ### Log to Texport 36 | 37 | If you want to track when and where your external files are being saved, or if you're worried that something might be going wrong, you can turn on the `Logtotextport` parameter to see the results of each save operation logged for easy view and tracking. 38 | 39 | ### Include About Page 40 | 41 | This will add a page to your externalized TOX that adds additional support information: 42 | 43 | * TD Version - the version of TouchDesigner that the TOX was saved with 44 | * TD Build - the build of TouchDesigner that the TOX was saved with 45 | * TOX Version - an auto incrementing version number whose patch increment will be automatically updated with each save 46 | * Last Saved - a date stamp with the date and time from the last TOX save 47 | 48 | ## Colors 49 | 50 | ### TouchDesigner 51 | 52 | #### Default Color 53 | 54 | The default color is set as a read-only parameter used to reset the network worksheet background color. This is used in conjunction with the following two parameters to provide visual indicators for when a save or load operation has happened. 55 | 56 | #### BG Color 57 | This is the color that the network background will flash when you externalize a TOX - it's the visual indicator that your tox has been successfully saved. 58 | 59 | #### External Op 60 | This is the color of your operator after it's been externalized. 61 | 62 | ### Python 63 | 64 | #### Default - any external file 65 | 66 | All DATs that have been externalized can be set to this color so you can more quickly identify external files. 67 | 68 | #### Python Extension - any file tagged `EXT` 69 | If an externalized Python file has been tagged with `EXT` this color will be applied to the Text DAT. This is a helpful mechanic for color coding any DATs that you are using as extensions 70 | 71 | #### Python Module - any file tagged `MOD` 72 | 73 | If an externalized Python file has been tagged with `MOD` this color will be applied to the Text DAT. This is a helpful mechanic for color coding any DATs that you are using as modules on demand 74 | 75 | ### Comments 76 | 77 | Comments are a super helpful addition to TouchDesigner networks, but it can be hard to know how to use them. Borrowing from the work done in the [ToDoTree Extension for VS Code](https://marketplace.visualstudio.com/items?itemName=Gruntfuggly.todo-tree) this section of parameters aims to help you color code your comments. Prepend any comment's header with a `#` and one of the identifiers: 78 | 79 | * Bug - `#BUG` 80 | * Hack - `#HACK` 81 | * Fix - `#FIXME` 82 | * Note - `#NOTE` 83 | 84 | Colors associated with these parameters will then be used to set the color of the comment in your network. 85 | 86 | ![base save and pars](assets/comment-colors.png) 87 | 88 | ## Short Cuts 89 | 90 | ### `ctrl+w` 91 | The way you'll use this tox is just as if you were working as you might normally. Only, when you hit `ctrl + w`, if you're inside of a COMP that hasn't been saved externally, you'll be asked if you want to externalize that module. If you select `yes` you'll next be asked where you want to save that module. This module will then create a folder that has the same name as your component, and save the tox inside of that folder (the tox will also have the same name as the component). Better yet, this module will auto-populate the path to the external tox with the location you've selected. When you press `ctrl + w` again it will warn you that you're about to over-write your tox. If you confirm that you want to replace your tox, it will save the updated version right where your previous tox was located. 92 | 93 | ### `ctrl+shift+w` 94 | In addition to saving a single file, you can also see all the components that you've changed by using this shortcut. This will open a menu of all of your external COMPs. 95 | 96 | ## All TOX Components - floating window 97 | 98 | ![create-a-dir](assets/all-tox-comps-window.png) 99 | 100 | It's not always easy to remember which `COMPs` have been externalized, or to get to them quickly. The `ctrl+shift+w` hot key opens a floating window that can help you manage your external TOX files. This floating window provides a quick view of a number of different attributes for your external files. Here you can see the name, version, and last saved date for your `TOX`. There are also four quick action buttons you might use. 101 | 102 | :eye: - jump your current TD network pane to be inside of this component. If you want a fast way to navigate to parts of your TouchDesigner network this can be very helpful. 103 | 104 | :floppy_disk: - Save your TOX. You can use this button to save your TOX instead of using `ctrl+w` from inside the Component 105 | 106 | :open_file_folder: - open your file explorer to see where this TOX is located on your computer. 107 | 108 | :recycle: - reload the TOX in your network. This is equivalent to clicking on the `reload` parameter for your `COMP` 109 | 110 | :snake: - re-init your extension. If your TOX has has an extension, you can use this button to quickly reinitialize it without needing to navigate to your component. 111 | 112 | # Suggested Workflow 113 | 114 | 115 | 116 | Add the `base_save` `TOX` file into your network. You only need to add one. Save your whole project file (your `.toe`). My recommendation is create a folder for your project somewhere on your computer and to save your `.toe` file in this directory. This will make it easier to collect all of your related files close to your project file. I like keeping my `TOX` files in their own directory. I usually create a folder called something like `td-modules` where I'll save my `TOX` files. 117 | 118 | When you're in any COMP in TouchDesigner use the hot-key `ctrl + w` 119 | 120 | ## Externalization Only 121 | 1. Create a directory for your project 122 | ![create-a-dir](assets/create-project-dir.PNG) 123 | 124 | 2. Open TouchDesigner and save your `.TOE` file in your new directory **this is an important step** - saving your project makes sure that the member `project.folder` correct points to your `.TOE` file. 125 | ![create-a-project](https://github.com/raganmd/touchdesigner-save-external/blob/master/assets/create-project-toe.PNG?raw=true) 126 | 127 | 3. Drop the `base_save.tox` from `touchdesigner-save-external\release` into your network - I'd recommend doing this at the root of your project, or in a place in your project specifically designed to hold other tools. I like to create a base called tools where I keep all the things that I use for development, or that any machine might need (meaning when you're thinking on a single `.TOE` file that's configured based on a machine's role) 128 | ![drag-into-network](assets/drag-into-network.gif) 129 | 130 | 4. Create a new component, and navigate inside of this new COMP. 131 | ![create-a-new-comp](assets/create-new-comp.PNG) 132 | 133 | 5. Use `ctrl + w` to save your project as you might usually. 134 | 6. Notice that you're now prompted to save your COMP externally - select `Yes` 135 | ![ctrl-s](assets/prompt-to-externalize.PNG) 136 | 137 | 7. Create a new folder in your project folder called `td-modules` (this is my suggestion, though you can use any name you like). Navigate into this folder and compete the save process. 138 | ![create-a-modules-dir](assets/create-td-modules-dir.PNG) 139 | 140 | 8. Check finder (macOS) or explorer (windows) to see that in `td-moduels` you now have a new directory for your tox, and inside of that directory is your saved tox file. 141 | ![newly-made-dir](assets/new-save-dir.PNG) 142 | ![newly-saved-tox](assets/new-save-tox.PNG) 143 | 144 | 9. Notice that the color of your tox has changed so you know that it's externalized. 145 | 146 | 10. Continue to work and save. Note that when you use `ctrl+w` both your project and your tox are saved. If you happen to create an external `.TOX` inside of a tox that's already externalized, you'll be prompted to save both the `parent()` and the current COMP or just the current COMP. 147 | 148 | ## Using Git 149 | 1. Create a new repo 150 | 2. Clone / Initialize your repo locally 151 | 3. Open TouchDesigner and save your `.TOE` file in your repo 152 | ![create-a-project](assets/create-project-toe.PNG) 153 | 154 | 4. Drop the `base_save.tox` from `touchdesigner-save-external\release` into your network - I'd recommend doing this at the root of your project, or in a place in your project specifically designed to hold other tools. I like to create a base called tools where I keep all the things that I use for development, or that any machine might need (meaning when you're thinking on a single `.TOE` file that's configured based on a machine's role) 155 | ![drag-into-network](assets/drag-into-network.gif) 156 | 157 | 5. Create a new component, and navigate inside of this COMP. 158 | ![create-a-new-comp](assets/create-new-comp.PNG) 159 | 160 | 6. Use `ctrl + w` to save your project as you might usually. 161 | 7. Notice that you're now prompted to save your COMP externally - select `Yes` 162 | ![ctrl-s](assets/prompt-to-externalize.PNG) 163 | 164 | 8. Create a new folder in your project folder called `td-modules` (this is my suggestion, though you can use any name you like). Navigate into this folder and compete the save process. 165 | ![create-a-modules-dir](assets/create-td-modules-dir.PNG) 166 | 167 | 9. Check finder (macOS) or explorer (windows) to see that in `td-moduels` you now have a new directory for your tox, and inside of that directory is your saved tox file. 168 | ![newly-made-dir](assets/new-save-dir.PNG) 169 | ![newly-saved-tox](assets/new-save-tox.PNG?) 170 | 171 | 10. Notice that the color of your tox has changed so you know that it's externalized. 172 | ![external-tox-color](assets/external-tox-color.PNG) 173 | 174 | 11. Continue to work and save. Note that when you use `ctrl+w` both your project and your tox are saved. If you happen to create an external `.TOX` inside of a tox that's already externalized, you'll be prompted to save both the `parent()` and the current COMP or just the current COMP. 175 | 12. Commit and push your work. 176 | 177 | # Additional Considerations and Suggestions 178 | At this point, you might have guess that this kind of approach works best in well structured projects. Some suggestions for organization and approach: 179 | * Think about Order and Structure - while I've structured projects lots of different ways, it's worth finding a file structure that you like and sticking with it. That might be a deeply nested structure (watch out that'll bite you if you get too deep - at least on windows), or it might be something more flat. Regardless, think about a structure and stay with it. 180 | * Make Small Simple Tools - to the best of your ability, try to make sure your modules are independent islands. That's not always possible, but if you can think carefully about creating dependencies, you'll be happier for it. Use custom parameters on your components to keep modules independent from one another. Use select operators, or In's and Out's to build connections. 181 | * Reuse that TOX - while this approach is fancy and fun, especially when working with git, it's also about making your future self happier. Thank carefully about how you might make something re-usable and portable to another project. The more you can think through how to make pieces that can easily move from project to project the more time you can spend on the fun stuff... not on the pieces that are fussy and take lots of time. 182 | 183 | # An Example Project 184 | In the folder called `sample_project` open the `Sample_project.toe` to see how this might work. 185 | 186 | # Credits 187 | ### Inspired by the work of: 188 | [Anton Heestand](http://hexagons.se/) 189 | [Willy Nolan](https://github.com/computersarecool) 190 | I've had the great fortune of working with both of these find developers. I regularly use an externalization tool authored by these two developers, and this TOX is partially inspired by their work. Many thanks for a tool that keeps on working and makes using GIT with TouchDesigner something that's reasonable. 191 | 192 | ### Icons 193 | [Material Design Icons by Google](https://material.io/tools/icons/?icon=save_alt&style=baseline) 194 | -------------------------------------------------------------------------------- /dev/scripts/saveEXT.py: -------------------------------------------------------------------------------- 1 | """ 2 | SudoMagic | sudomagic.com 3 | Authors | Matthew Ragan, Ian Shelanskey 4 | Contact | contact@sudomagic.com 5 | """ 6 | 7 | # pure python 8 | import os 9 | 10 | # td python mods 11 | import saveOp 12 | import saveUtils 13 | 14 | 15 | class ExternalFiles: 16 | """ 17 | The ExternalFiles class is used to handle working with both 18 | externalizing files, as well as ingesting files that were previously 19 | externalized. This helps to minimize the amount of manual work that 20 | might need to otherwise be use for handling external files. 21 | """ 22 | FLASH_DURATION = 14 23 | UNLOCK_TAGS = [ 24 | "unlockOnSave" 25 | ] 26 | 27 | def __init__(self, my_op: callable) -> None: 28 | """Stands in place of an execute dat - ensures all elements start-up 29 | correctly 30 | 31 | Notes 32 | --------- 33 | 34 | Args 35 | --------- 36 | myOp (touchDesignerOperator): 37 | > the operator that is loading the current extension 38 | 39 | Returns 40 | --------- 41 | none 42 | """ 43 | 44 | self.my_op = my_op 45 | self._log_label = "--> SAVE COMP |" 46 | self.Defaultcolor = self.my_op.parGroup.Defaultcolor 47 | self.op_none_color = (0.5450000166893005, 48 | 0.5450000166893005, 49 | 0.5450000166893005) 50 | self.Ext_ops = op('table_all_external_ops') 51 | 52 | self._ops_manager = saveOp.SaveOpManager() 53 | 54 | init_msg = f"Start up and Init at {absTime.frame}" 55 | 56 | self.Colors_map = self._colors_map() 57 | self.Tag_to_color_map = self._tag_to_color_map() 58 | self.Title_text_to_tag_map = self._title_text_to_tag_map() 59 | 60 | self.Logtotextport(init_msg) 61 | 62 | return 63 | 64 | def _colors_map(self) -> dict: 65 | """A map of colors 66 | Structure of this dict is: 67 | Classification (TouchDesigner | Python | Comments) 68 | -> Color type (e.g. python_extension or comment_note) 69 | -> tags and colors for type 70 | 71 | """ 72 | colors_map = { 73 | "TouchDesigner": { 74 | "external_op": { 75 | "color": self.my_op.parGroup.Externalop, 76 | "tags": [] 77 | } 78 | }, 79 | "Python": { 80 | "default": { 81 | "color": self.my_op.parGroup.Pythondefault, 82 | "tags": [] 83 | }, 84 | "python_extension": { 85 | "color": self.my_op.parGroup.Pythonextension, 86 | "tags": ["EXT"] 87 | }, 88 | "python_module": { 89 | "color": self.my_op.parGroup.Pythonmodule, 90 | "tags": ["MOD"] 91 | } 92 | }, 93 | "Comments": { 94 | "bug": { 95 | "color": self.my_op.parGroup.Commentbug, 96 | "tags": ["comment_bug"], 97 | "title_text": "#BUG" 98 | }, 99 | "hack": { 100 | "color": self.my_op.parGroup.Commenthack, 101 | "tags": ["comment_hack"], 102 | "title_text": "#HACK" 103 | }, 104 | "fix": { 105 | "color": self.my_op.parGroup.Commentfix, 106 | "tags": ["comment_fix"], 107 | "title_text": "#FIXME" 108 | }, 109 | "note": { 110 | "color": self.my_op.parGroup.Commentnote, 111 | "tags": ["comment_note"], 112 | "title_text": "#NOTE" 113 | } 114 | } 115 | } 116 | return colors_map 117 | 118 | def _tag_to_color_map(self) -> dict: 119 | """Builds a map of colors by tag 120 | """ 121 | tag_to_color_map = {} 122 | for classification, classificaion_vals in self.Colors_map.items(): 123 | for each_type, type_vals in classificaion_vals.items(): 124 | for each_tag in type_vals.get("tags"): 125 | tag_to_color_map[each_tag] = type_vals.get("color") 126 | return tag_to_color_map 127 | 128 | def _title_text_to_tag_map(self) -> dict: 129 | """Builds a map of colors by title_text 130 | """ 131 | title_text_to_tag_map = {} 132 | comments_dict = self.Colors_map.get("Comments") 133 | for each_type, type_vals in comments_dict.items(): 134 | text_key = type_vals.get("title_text") 135 | title_text_to_tag_map[text_key] = type_vals.get("tags")[0] 136 | return title_text_to_tag_map 137 | 138 | @property 139 | def Ops_manager(self) -> callable: 140 | return self._ops_manager 141 | 142 | @property 143 | def get_current_location(self) -> callable: 144 | return ui.panes.current.owner 145 | 146 | def Prompt_to_save(self) -> None: 147 | """The method used to save an external TOX to file 148 | 149 | Notes 150 | --------- 151 | 152 | Args 153 | --------- 154 | current_loc (str): 155 | > the operator that's related to the currently focused 156 | > pane object. This is required to ensure that we correctly 157 | > grab the appropriate COMP and check to see if needs to be saved 158 | 159 | Returns 160 | --------- 161 | none 162 | """ 163 | current_loc = self.get_current_location 164 | 165 | external_op_color = self.Colors_map.get( 166 | "TouchDesigner").get("external_op").get("color") 167 | msg_box_title = "TOX Save" 168 | msg_box_msg = "Replacing External\n\nYou are about to overwrite an exnteral TOX" 169 | msg_box_buttons = ["Cancel", "Continue"] 170 | 171 | sav_msg_box_title = "Externalize Tox" 172 | sav_msg_box_msg = "This TOX is not yet externalized\n\nWould you like to externalize this TOX?" 173 | sav_msg_box_buttons = ["No", "Yes"] 174 | 175 | save_msg_buttons_parent_too = [ 176 | "No", "This COMP Only", "This COMP and the Parent"] 177 | 178 | # check if location is the root of the project 179 | if current_loc == '/': 180 | # skip if we're at the root of the project 181 | pass 182 | 183 | else: 184 | # if we're not at the root of the project 185 | 186 | # check if external 187 | if current_loc.par.externaltox != '': 188 | confirmation = ui.messageBox( 189 | msg_box_title, msg_box_msg, 190 | buttons=msg_box_buttons) 191 | 192 | if confirmation: 193 | 194 | # save external file 195 | self.Save_over_tox(current_loc) 196 | 197 | else: 198 | # if the user presses "cancel" we pass 199 | pass 200 | 201 | # in this case we are not external, so let's ask if we want to 202 | # externalize the file 203 | else: 204 | 205 | # check if the parent is externalized 206 | if current_loc.parent().par.externaltox != '': 207 | save_ext = ui.messageBox( 208 | sav_msg_box_title, 209 | sav_msg_box_msg, 210 | buttons=save_msg_buttons_parent_too) 211 | 212 | # save this comp only 213 | if save_ext == 1: 214 | self.Save_tox(current_loc) 215 | 216 | # save this comp and the parent 217 | elif save_ext == 2: 218 | self.Save_tox(current_loc) 219 | self.Logtotextport("save this tox") 220 | 221 | # save parent() COMP 222 | self.Save_over_tox(current_loc.parent()) 223 | self.Logtotextport("Save the parent too!") 224 | 225 | # user selected 'No' 226 | else: 227 | pass 228 | 229 | # the parent is not external, so let's ask about externalizing 230 | # the tox 231 | else: 232 | save_ext = ui.messageBox( 233 | sav_msg_box_title, 234 | sav_msg_box_msg, 235 | buttons=sav_msg_box_buttons) 236 | 237 | if save_ext: 238 | self.Save_tox(current_loc) 239 | 240 | else: 241 | # the user selected "No" 242 | pass 243 | 244 | return 245 | 246 | def Save_over_tox(self, current_loc): 247 | # update custom pars 248 | self.update_version_pars(current_loc) 249 | self.update_save_time(current_loc) 250 | 251 | self._save_tox(current_loc) 252 | return 253 | 254 | def Save_tox(self, current_loc): 255 | ext_color = self.Colors_map.get( 256 | "TouchDesigner").get("external_op").get("color") 257 | 258 | # ask user for a save location 259 | save_loc = ui.chooseFolder(title="TOX Location", start=project.folder) 260 | 261 | # construct a relative path and relative loaction for our elements 262 | rel_path = tdu.collapsePath(save_loc) 263 | 264 | # check to see if the location is at the root of the project folder structure 265 | if rel_path == "$TOUCH": 266 | rel_loc = '{new_tox}/{new_tox}.tox'.format( 267 | new_tox=current_loc.name) 268 | 269 | # save path is not in the root of the project 270 | else: 271 | rel_loc = '{new_module}/{new_tox}/{new_tox}.tox'.format( 272 | new_module=rel_path, new_tox=current_loc.name) 273 | 274 | # create path and directory in the OS 275 | new_path = '{selected_path}/{new_module}'.format( 276 | selected_path=save_loc, new_module=current_loc.name) 277 | 278 | try: 279 | os.mkdir(new_path) 280 | valid_external_path = True 281 | except: 282 | self.alert_failed_dir_creation( 283 | new_path=new_path, current_loc=current_loc) 284 | valid_external_path = False 285 | 286 | if valid_external_path: 287 | # format our tox path 288 | tox_path = '{dir_path}/{tox}.tox'.format( 289 | dir_path=new_path, tox=current_loc.name) 290 | 291 | # setup our module correctly 292 | current_loc.par.externaltox = '' if current_loc.par['Sudotool'] else rel_loc 293 | current_loc.par.savebackup = False 294 | 295 | # set color for COMP 296 | current_loc.color = self.op_none_color if current_loc.par['Sudotool'] else ( 297 | ext_color[0], ext_color[1], ext_color[2]) 298 | 299 | # setup about page 300 | self.custom_page_setup(current_loc) 301 | 302 | # private save method 303 | self._save_tox(current_loc) 304 | 305 | # track new SaveOp 306 | self._ops_manager.Check_external_ops() 307 | 308 | else: 309 | pass 310 | 311 | return 312 | 313 | def _save_tox(self, current_loc: str) -> None: 314 | ext_color = self.Colors_map.get( 315 | "TouchDesigner").get("external_op").get("color") 316 | external_path = current_loc.par.externaltox 317 | 318 | # Run pre-save event 319 | self.preToxSave(current_loc) 320 | 321 | # save tox 322 | current_loc.save(external_path) 323 | 324 | # Run post-save event 325 | self.postToxSave(current_loc) 326 | 327 | # set color for COMP 328 | current_loc.color = (ext_color[0], ext_color[1], ext_color[2]) 329 | 330 | # flash color 331 | worksheet_color = self.my_op.parGroup["Bgcolor"] 332 | saveUtils.flash_bg(worksheet_color, ExternalFiles.FLASH_DURATION) 333 | 334 | # set external file colors 335 | self.Set_external_file_colors() 336 | 337 | # set comment colors 338 | self.Set_annotate_colors() 339 | 340 | # update hash_list 341 | self._ops_manager.Update_save_op_by_path( 342 | current_loc, 343 | False) 344 | 345 | # create and print log message 346 | log_msg = "{} saved to {}/{}".format( 347 | current_loc, 348 | project.folder, 349 | external_path) 350 | 351 | self.Logtotextport(log_msg) 352 | 353 | def preToxSave(self, tox): 354 | """ pre tox save event handling """ 355 | 356 | for child in tox.findChildren(tags=['unlockOnSave']): 357 | child.lock = False 358 | return 359 | 360 | def postToxSave(self, tox): 361 | """ post tox save event handling """ 362 | for child in tox.findChildren(tags=['unlockOnSave']): 363 | child.lock = True 364 | return 365 | 366 | def alert_failed_dir_creation(self, **kwargs): 367 | op.TDResources.op('popDialog').Open( 368 | title="OVERWRITE WARNING", 369 | text="""It looks like there is an existing 370 | TOX in this directory. 371 | 372 | Please check your to make sure 373 | this TOX does not already exist. 374 | """, 375 | buttons=["Cancel", "Replace"], 376 | escButton=1, 377 | callback=self.dialogChoice, 378 | details=kwargs 379 | ) 380 | 381 | def dialogChoice(self, info): 382 | button_selection = info.get('button') 383 | 384 | if button_selection == 'Cancel': 385 | pass 386 | 387 | else: 388 | current_loc = info.get('details').get('current_loc') 389 | new_path = info.get('details').get('new_path') 390 | 391 | # format our tox path 392 | tox_path = '{dir_path}/{tox}.tox'.format( 393 | dir_path=new_path, tox=current_loc.name) 394 | 395 | # update custom pars 396 | self.update_version_pars(current_loc) 397 | 398 | # save our tox 399 | current_loc.save(tox_path) 400 | 401 | # flash color 402 | self.Flash_bg("Bgcolor") 403 | 404 | # create and print log message 405 | log_msg = "{} saved to {}/{}".format( 406 | current_loc, 407 | project.folder, 408 | tox_path) 409 | self.Logtotextport(log_msg) 410 | 411 | def update_custom_str_par(self, targetOp, par, value, par_label="Temp"): 412 | if targetOp.par[par] != None: 413 | targetOp.par[par] = value 414 | else: 415 | about_page = targetOp.appendCustomPage("About") 416 | about_page.appendStr(par, label=par_label) 417 | targetOp.par[par] = value 418 | targetOp.par[par].readOnly = True 419 | 420 | def update_version_pars(self, target_op: callable) -> None: 421 | if target_op.par['Toxversion'] == None: 422 | self.update_custom_str_par( 423 | target_op, "Toxversion", "1.0.0", "Tox Version") 424 | else: 425 | new_patch_version = self._patch_update(target_op) 426 | self.update_custom_str_par( 427 | target_op, 'Toxversion', new_patch_version) 428 | self.update_custom_str_par(target_op, "Tdversion", app.version) 429 | self.update_custom_str_par(target_op, "Tdbuild", app.build) 430 | 431 | def _patch_update(self, target_op: callable) -> str: 432 | patch_str = target_op.par.Toxversion.eval() 433 | split_str_default = [1, 0, 0] 434 | if len(patch_str.split('.')) > 1: 435 | try: 436 | split_str = patch_str.split('.') 437 | except: 438 | split_str = split_str_default 439 | else: 440 | split_str = split_str_default 441 | 442 | return f"{split_str[0]}.{split_str[1]}.{int(split_str[2])+1}" 443 | 444 | def update_save_time(self, target_op: callable) -> None: 445 | # add par and set time 446 | if target_op.par['Lastsaved'] == None: 447 | self.update_custom_str_par( 448 | target_op, 449 | "Lastsaved", 450 | saveUtils.current_save_time(), 451 | "Last Saved") 452 | 453 | # update time 454 | else: 455 | self.update_custom_str_par( 456 | target_op, 457 | "Lastsaved", 458 | saveUtils.current_save_time(), 459 | "Last Saved") 460 | 461 | def custom_page_setup(self, target_op): 462 | self.update_custom_str_par( 463 | target_op, 464 | "Tdversion", 465 | app.version, 466 | "TD Version") 467 | self.update_custom_str_par( 468 | target_op, 469 | "Tdbuild", 470 | app.build, 471 | "TD Build") 472 | self.update_custom_str_par( 473 | target_op, 474 | "Toxversion", 475 | "1.0.0", 476 | "Tox Version") 477 | self.update_custom_str_par( 478 | target_op, 479 | "Lastsaved", 480 | saveUtils.current_save_time(), 481 | "Last Saved") 482 | 483 | def Logtotextport(self, logMsg): 484 | ui.status = f"{self._log_label} {logMsg}" 485 | if parent().par.Logtotextport: 486 | print(f"{self._log_label} {logMsg}") 487 | else: 488 | pass 489 | return 490 | 491 | def Set_ext_tox_colors(self): 492 | externalChildren = saveUtils.find_external_ops() 493 | colors = self.Colors_map.get( 494 | "TouchDesigner").get("external_op").get("color") 495 | for eachOp in externalChildren: 496 | eachOp.color = (colors[0], colors[1], colors[2]) 497 | pass 498 | 499 | def Set_external_file_colors(self): 500 | """Sets colors for external files 501 | """ 502 | 503 | external_dats = saveUtils.find_all_dats() 504 | default_color = self.Colors_map.get( 505 | "Python").get("default").get("color") 506 | 507 | for eachDat in external_dats: 508 | dat_color = default_color 509 | for each_tag in eachDat.tags: 510 | if each_tag in self.Tag_to_color_map.keys(): 511 | if self.Tag_to_color_map.get(each_tag) != None: 512 | dat_color = self.Tag_to_color_map.get(each_tag) 513 | eachDat.color = dat_color 514 | pass 515 | 516 | def Set_annotate_colors(self): 517 | network_comments = saveUtils.find_all_comments() 518 | 519 | # update tag on annotates 520 | for each_new_comment in network_comments: 521 | title_text = each_new_comment.par.Titletext.eval() 522 | first_word = title_text.split(" ")[0] 523 | if first_word in self.Title_text_to_tag_map.keys(): 524 | target_tag = self.Title_text_to_tag_map.get(first_word) 525 | each_new_comment.tags.add(target_tag) 526 | 527 | # set color by tag 528 | for eachComment in network_comments: 529 | for each_tag in eachComment.tags: 530 | if each_tag in self.Tag_to_color_map.keys(): 531 | if self.Tag_to_color_map.get(each_tag) != None: 532 | eachComment.color = self.Tag_to_color_map.get(each_tag) 533 | 534 | def Open_network_location(self, network_location: str): 535 | """ Moves network to save location 536 | """ 537 | ui.panes.current.owner = network_location 538 | ui.panes.current.home() 539 | 540 | def Ignore_current_changes(self, op_id: int): 541 | self._ops_manager.Ignore_current_dirty_state(op_id) 542 | 543 | def Open_floating_external_tox_window(self) -> None: 544 | """Opens window for tox files 545 | """ 546 | # run dirty check 547 | # self._ops_manager.Dirty_check() 548 | # force cook op to see current status 549 | self.my_op.op('script1').cook(force=True) 550 | # open window for external tox files 551 | parent.save.op('window1').par.winopen.pulse() 552 | 553 | def Keyboard_input(self, shortcut): 554 | """Keyboard input handler 555 | """ 556 | shortcut_lookup = { 557 | 'ctrl.w': self.Prompt_to_save, 558 | 'ctrl.shift.w': self.Open_floating_external_tox_window 559 | } 560 | 561 | func = shortcut_lookup.get(shortcut) 562 | 563 | try: 564 | func() 565 | 566 | except Exception as e: 567 | self.Logtotextport(e) 568 | -------------------------------------------------------------------------------- /dev/td-modules/base_save/saveTox.py: -------------------------------------------------------------------------------- 1 | """ 2 | SudoMagic | sudomagic.com 3 | Authors | Matthew Ragan, Ian Shelanskey 4 | Contact | contact@sudomagic.com 5 | """ 6 | 7 | # pure python 8 | import os 9 | 10 | # td python mods 11 | import saveOp 12 | import saveUtils 13 | import listerFuncs 14 | 15 | 16 | class ExternalFiles: 17 | """ 18 | The ExternalFiles class is used to handle working with both 19 | externalizing files, as well as ingesting files that were previously 20 | externalized. This helps to minimize the amount of manual work that 21 | might need to otherwise be use for handling external files. 22 | """ 23 | FLASH_DURATION = 14 24 | UNLOCK_TAGS = [ 25 | "unlockOnSave" 26 | ] 27 | 28 | def __init__(self, my_op: callable) -> None: 29 | """Stands in place of an execute dat - ensures all elements start-up 30 | correctly 31 | 32 | Notes 33 | --------- 34 | 35 | Args 36 | --------- 37 | myOp (touchDesignerOperator): 38 | > the operator that is loading the current extension 39 | 40 | Returns 41 | --------- 42 | none 43 | """ 44 | 45 | self.my_op = my_op 46 | self._log_label = "--> SAVE COMP |" 47 | self.Defaultcolor = self.my_op.parGroup.Defaultcolor 48 | self.op_none_color = (0.5450000166893005, 49 | 0.5450000166893005, 50 | 0.5450000166893005) 51 | self.Ext_ops_DAT = my_op.op('script_external_ops') 52 | 53 | self._ops_manager = saveOp.SaveOpManager(self.Ext_ops_DAT) 54 | 55 | init_msg = f"Start up and Init at {absTime.frame}" 56 | 57 | self.Colors_map = self._colors_map() 58 | self.Tag_to_color_map = self._tag_to_color_map() 59 | self.Title_text_to_tag_map = self._title_text_to_tag_map() 60 | 61 | self._lister_COMP = my_op.op( 62 | "widget_tox_finder/lister") 63 | self._search_text_COMP = my_op.op( 64 | "widget_tox_finder/container_search/text_search") 65 | # runs setup 66 | self._setup() 67 | 68 | self.Logtotextport(init_msg) 69 | 70 | return 71 | 72 | pass 73 | 74 | def _setup(self) -> None: 75 | """All set-up procedures for UI""" 76 | # resets lister 77 | self._lister_COMP.par.Refresh.pulse() 78 | 79 | # sets filter col for correct behavior 80 | self._lister_COMP.par.Filtercols = 0 81 | 82 | # set sort col 83 | self._lister_COMP.par.Sortcols = 3 84 | 85 | def _colors_map(self) -> dict: 86 | """A map of colors 87 | Structure of this dict is: 88 | Classification (TouchDesigner | Python | Comments) 89 | -> Color type (e.g. python_extension or comment_note) 90 | -> tags and colors for type 91 | 92 | """ 93 | colors_map = { 94 | "TouchDesigner": { 95 | "external_op": { 96 | "color": self.my_op.parGroup.Externalop, 97 | "tags": [] 98 | } 99 | }, 100 | "Python": { 101 | "default": { 102 | "color": self.my_op.parGroup.Pythondefault, 103 | "tags": [] 104 | }, 105 | "python_extension": { 106 | "color": self.my_op.parGroup.Pythonextension, 107 | "tags": ["EXT"] 108 | }, 109 | "python_module": { 110 | "color": self.my_op.parGroup.Pythonmodule, 111 | "tags": ["MOD"] 112 | } 113 | }, 114 | "Comments": { 115 | "bug": { 116 | "color": self.my_op.parGroup.Commentbug, 117 | "tags": ["comment_bug"], 118 | "title_text": "#BUG" 119 | }, 120 | "hack": { 121 | "color": self.my_op.parGroup.Commenthack, 122 | "tags": ["comment_hack"], 123 | "title_text": "#HACK" 124 | }, 125 | "fix": { 126 | "color": self.my_op.parGroup.Commentfix, 127 | "tags": ["comment_fix"], 128 | "title_text": "#FIXME" 129 | }, 130 | "note": { 131 | "color": self.my_op.parGroup.Commentnote, 132 | "tags": ["comment_note"], 133 | "title_text": "#NOTE" 134 | } 135 | } 136 | } 137 | return colors_map 138 | 139 | def _tag_to_color_map(self) -> dict: 140 | """Builds a map of colors by tag 141 | """ 142 | tag_to_color_map = {} 143 | for classification, classificaion_vals in self.Colors_map.items(): 144 | for each_type, type_vals in classificaion_vals.items(): 145 | for each_tag in type_vals.get("tags"): 146 | tag_to_color_map[each_tag] = type_vals.get("color") 147 | return tag_to_color_map 148 | 149 | def _title_text_to_tag_map(self) -> dict: 150 | """Builds a map of colors by title_text 151 | """ 152 | title_text_to_tag_map = {} 153 | comments_dict = self.Colors_map.get("Comments") 154 | for each_type, type_vals in comments_dict.items(): 155 | text_key = type_vals.get("title_text") 156 | title_text_to_tag_map[text_key] = type_vals.get("tags")[0] 157 | return title_text_to_tag_map 158 | 159 | @property 160 | def Ops_manager(self) -> callable: 161 | return self._ops_manager 162 | 163 | @property 164 | def get_current_location(self) -> callable: 165 | return ui.panes.current.owner 166 | 167 | def Prompt_to_save(self) -> None: 168 | """The method used to save an external TOX to file 169 | 170 | Notes 171 | --------- 172 | 173 | Args 174 | --------- 175 | current_loc (str): 176 | > the operator that's related to the currently focused 177 | > pane object. This is required to ensure that we correctly 178 | > grab the appropriate COMP and check to see if needs to be saved 179 | 180 | Returns 181 | --------- 182 | none 183 | """ 184 | current_loc = self.get_current_location 185 | 186 | external_op_color = self.Colors_map.get( 187 | "TouchDesigner").get("external_op").get("color") 188 | msg_box_title = "TOX Save" 189 | msg_box_msg = "Replacing External\n\nYou are about to overwrite an external TOX" 190 | msg_box_buttons = ["Cancel", "Continue"] 191 | 192 | sav_msg_box_title = "Externalize Tox" 193 | sav_msg_box_msg = "This TOX is not yet externalized\n\nWould you like to externalize this TOX?" 194 | sav_msg_box_buttons = ["No", "Yes"] 195 | 196 | save_msg_buttons_parent_too = [ 197 | "No", "This COMP Only", "This COMP and the Parent"] 198 | 199 | # check if location is the root of the project 200 | if current_loc == '/': 201 | # skip if we're at the root of the project 202 | pass 203 | 204 | else: 205 | # if we're not at the root of the project 206 | 207 | # check if external 208 | if current_loc.par.externaltox != '': 209 | confirmation = ui.messageBox( 210 | msg_box_title, msg_box_msg, 211 | buttons=msg_box_buttons) 212 | 213 | if confirmation: 214 | 215 | # save external file 216 | self.Save_over_tox(current_loc) 217 | 218 | else: 219 | # if the user presses "cancel" we pass 220 | pass 221 | 222 | # in this case we are not external, so let's ask if we want to 223 | # externalize the file 224 | else: 225 | 226 | # check if the parent is externalized 227 | if current_loc.parent().par.externaltox != '': 228 | save_ext = ui.messageBox( 229 | sav_msg_box_title, 230 | sav_msg_box_msg, 231 | buttons=save_msg_buttons_parent_too) 232 | 233 | # save this comp only 234 | if save_ext == 1: 235 | self.Save_tox(current_loc) 236 | 237 | # save this comp and the parent 238 | elif save_ext == 2: 239 | self.Save_tox(current_loc) 240 | self.Logtotextport("save this tox") 241 | 242 | # save parent() COMP 243 | self.Save_over_tox(current_loc.parent()) 244 | self.Logtotextport("Save the parent too!") 245 | 246 | # user selected 'No' or 'X' button 247 | else: 248 | pass 249 | 250 | # the parent is not external, so let's ask about externalizing 251 | # the tox 252 | else: 253 | save_ext = ui.messageBox( 254 | sav_msg_box_title, 255 | sav_msg_box_msg, 256 | buttons=sav_msg_box_buttons) 257 | 258 | if save_ext == 1: 259 | self.Save_tox(current_loc) 260 | 261 | else: 262 | # the user selected "No" or 'X' button 263 | pass 264 | 265 | return 266 | 267 | def Save_over_tox(self, current_loc, specify_version: str = ''): 268 | 269 | # allow user to opt out of about page 270 | if self.my_op.par.Includeaboutpage: 271 | 272 | # update custom pars 273 | self.update_version_pars(current_loc, specify_version) 274 | self.update_save_time(current_loc) 275 | else: 276 | pass 277 | 278 | self._save_tox(current_loc) 279 | return 280 | 281 | def Save_tox(self, current_loc): 282 | ext_color = self.Colors_map.get( 283 | "TouchDesigner").get("external_op").get("color") 284 | 285 | # ask user for a save location 286 | save_loc = ui.chooseFolder(title="TOX Location", start=project.folder) 287 | 288 | # construct a relative path and relative loaction for our elements 289 | rel_path = tdu.collapsePath(save_loc) 290 | 291 | # check to see if the location is at the root of the project folder structure 292 | if rel_path == "$TOUCH": 293 | rel_loc = '{new_tox}/{new_tox}.tox'.format( 294 | new_tox=current_loc.name) 295 | 296 | # save path is not in the root of the project 297 | else: 298 | rel_loc = '{new_module}/{new_tox}/{new_tox}.tox'.format( 299 | new_module=rel_path, new_tox=current_loc.name) 300 | 301 | # create path and directory in the OS 302 | new_path = '{selected_path}/{new_module}'.format( 303 | selected_path=save_loc, new_module=current_loc.name) 304 | 305 | try: 306 | os.mkdir(new_path) 307 | valid_external_path = True 308 | except: 309 | self.alert_failed_dir_creation( 310 | new_path=new_path, current_loc=current_loc) 311 | valid_external_path = False 312 | 313 | if valid_external_path: 314 | # format our tox path 315 | tox_path = '{dir_path}/{tox}.tox'.format( 316 | dir_path=new_path, tox=current_loc.name) 317 | 318 | # setup our module correctly 319 | current_loc.par.externaltox = '' if current_loc.par['Sudotool'] else rel_loc 320 | current_loc.par.savebackup = False 321 | 322 | # set color for COMP 323 | current_loc.color = self.op_none_color if current_loc.par['Sudotool'] else ( 324 | ext_color[0], ext_color[1], ext_color[2]) 325 | 326 | # setup about page - allow user to supress about page at creation 327 | if self.my_op.par.Includeaboutpage: 328 | self.custom_page_setup(current_loc) 329 | else: 330 | pass 331 | 332 | # private save method 333 | self._save_tox(current_loc) 334 | 335 | # track new SaveOp 336 | self._ops_manager.Check_external_ops() 337 | 338 | else: 339 | pass 340 | 341 | return 342 | 343 | def _save_tox(self, current_loc: str) -> None: 344 | ext_color = self.Colors_map.get( 345 | "TouchDesigner").get("external_op").get("color") 346 | external_path = current_loc.par.externaltox 347 | 348 | # Run pre-save event 349 | self.preToxSave(current_loc) 350 | 351 | # save tox 352 | current_loc.save(external_path) 353 | 354 | # Run post-save event 355 | self.postToxSave(current_loc) 356 | 357 | # set color for COMP 358 | current_loc.color = (ext_color[0], ext_color[1], ext_color[2]) 359 | 360 | # flash color 361 | worksheet_color = self.my_op.parGroup["Bgcolor"] 362 | saveUtils.flash_bg(worksheet_color, ExternalFiles.FLASH_DURATION) 363 | 364 | # set external file colors 365 | self.Set_external_file_colors() 366 | 367 | # set comment colors 368 | self.Set_annotate_colors() 369 | 370 | # update hash_list 371 | self._ops_manager.Update_save_op_by_path( 372 | current_loc, 373 | False) 374 | 375 | # create and print log message 376 | log_msg = "{} saved to {}/{}".format( 377 | current_loc, 378 | project.folder, 379 | external_path) 380 | 381 | self.Logtotextport(log_msg) 382 | 383 | def preToxSave(self, tox): 384 | """ pre tox save event handling """ 385 | 386 | for child in tox.findChildren(tags=['unlockOnSave']): 387 | child.lock = False 388 | return 389 | 390 | def postToxSave(self, tox): 391 | """ post tox save event handling """ 392 | for child in tox.findChildren(tags=['unlockOnSave']): 393 | child.lock = True 394 | return 395 | 396 | def alert_failed_dir_creation(self, **kwargs): 397 | op.TDResources.op('popDialog').Open( 398 | title="OVERWRITE WARNING", 399 | text="""It looks like there is an existing 400 | TOX in this directory. 401 | 402 | Please check your to make sure 403 | this TOX does not already exist. 404 | """, 405 | buttons=["Cancel", "Replace"], 406 | escButton=1, 407 | callback=self.dialogChoice, 408 | details=kwargs 409 | ) 410 | 411 | def dialogChoice(self, info): 412 | button_selection = info.get('button') 413 | 414 | if button_selection == 'Cancel': 415 | pass 416 | 417 | else: 418 | current_loc = info.get('details').get('current_loc') 419 | new_path = info.get('details').get('new_path') 420 | 421 | # format our tox path 422 | tox_path = '{dir_path}/{tox}.tox'.format( 423 | dir_path=new_path, tox=current_loc.name) 424 | 425 | # update custom pars 426 | self.update_version_pars(current_loc) 427 | 428 | # save our tox 429 | current_loc.save(tox_path) 430 | 431 | # flash color 432 | self.Flash_bg("Bgcolor") 433 | 434 | # create and print log message 435 | log_msg = "{} saved to {}/{}".format( 436 | current_loc, 437 | project.folder, 438 | tox_path) 439 | self.Logtotextport(log_msg) 440 | 441 | def update_custom_str_par(self, targetOp, par, value, par_label="Temp"): 442 | if targetOp.par[par] != None: 443 | targetOp.par[par] = value 444 | else: 445 | about_page = targetOp.appendCustomPage("About") 446 | about_page.appendStr(par, label=par_label) 447 | targetOp.par[par] = value 448 | targetOp.par[par].readOnly = True 449 | 450 | def update_version_pars(self, target_op: callable, new_version: str = '') -> None: 451 | """Updates semver of TOX 452 | """ 453 | 454 | # of the op does not have a tox version par set to 1.0.0 455 | if target_op.par['Toxversion'] == None: 456 | self.update_custom_str_par( 457 | target_op, "Toxversion", "1.0.0", "Tox Version") 458 | else: 459 | if new_version == '': 460 | # update version based on submitted version 461 | new_patch_version = self._patch_update(target_op) 462 | pass 463 | else: 464 | # only apply a patch update 465 | new_patch_version = new_version 466 | 467 | # updates tox version 468 | self.update_custom_str_par( 469 | target_op, 'Toxversion', new_patch_version) 470 | 471 | # updates TD version 472 | self.update_custom_str_par(target_op, "Tdversion", app.version) 473 | 474 | # updates TD Build 475 | self.update_custom_str_par(target_op, "Tdbuild", app.build) 476 | 477 | def _patch_update(self, target_op: callable) -> str: 478 | """Updates patch semver 479 | """ 480 | patch_str = target_op.par.Toxversion.eval() 481 | split_str_default = [1, 0, 0] 482 | if len(patch_str.split('.')) > 1: 483 | try: 484 | split_str = patch_str.split('.') 485 | except: 486 | split_str = split_str_default 487 | else: 488 | split_str = split_str_default 489 | 490 | return f"{split_str[0]}.{split_str[1]}.{int(split_str[2])+1}" 491 | 492 | def update_save_time(self, target_op: callable) -> None: 493 | # add par and set time 494 | if target_op.par['Lastsaved'] == None: 495 | self.update_custom_str_par( 496 | target_op, 497 | "Lastsaved", 498 | saveUtils.current_save_time(), 499 | "Last Saved") 500 | 501 | # update time 502 | else: 503 | self.update_custom_str_par( 504 | target_op, 505 | "Lastsaved", 506 | saveUtils.current_save_time(), 507 | "Last Saved") 508 | 509 | def custom_page_setup(self, target_op): 510 | self.update_custom_str_par( 511 | target_op, 512 | "Tdversion", 513 | app.version, 514 | "TD Version") 515 | self.update_custom_str_par( 516 | target_op, 517 | "Tdbuild", 518 | app.build, 519 | "TD Build") 520 | self.update_custom_str_par( 521 | target_op, 522 | "Toxversion", 523 | "1.0.0", 524 | "Tox Version") 525 | self.update_custom_str_par( 526 | target_op, 527 | "Lastsaved", 528 | saveUtils.current_save_time(), 529 | "Last Saved") 530 | 531 | def Logtotextport(self, logMsg): 532 | ui.status = f"{self._log_label} {logMsg}" 533 | if parent().par.Logtotextport: 534 | print(f"{self._log_label} {logMsg}") 535 | else: 536 | pass 537 | return 538 | 539 | def Set_ext_tox_colors(self): 540 | externalChildren = saveUtils.find_external_ops() 541 | colors = self.Colors_map.get( 542 | "TouchDesigner").get("external_op").get("color") 543 | for eachOp in externalChildren: 544 | eachOp.color = (colors[0], colors[1], colors[2]) 545 | pass 546 | 547 | def Set_external_file_colors(self): 548 | """Sets colors for external files 549 | """ 550 | 551 | external_dats = saveUtils.find_all_dats() 552 | default_color = self.Colors_map.get( 553 | "Python").get("default").get("color") 554 | 555 | for eachDat in external_dats: 556 | dat_color = default_color 557 | for each_tag in eachDat.tags: 558 | if each_tag in self.Tag_to_color_map.keys(): 559 | if self.Tag_to_color_map.get(each_tag) != None: 560 | dat_color = self.Tag_to_color_map.get(each_tag) 561 | eachDat.color = dat_color 562 | pass 563 | 564 | def Set_annotate_colors(self): 565 | network_comments = saveUtils.find_all_comments() 566 | 567 | # update tag on annotates 568 | for each_new_comment in network_comments: 569 | title_text = each_new_comment.par.Titletext.eval() 570 | first_word = title_text.split(" ")[0] 571 | if first_word in self.Title_text_to_tag_map.keys(): 572 | target_tag = self.Title_text_to_tag_map.get(first_word) 573 | each_new_comment.tags.add(target_tag) 574 | 575 | # set color by tag 576 | for eachComment in network_comments: 577 | for each_tag in eachComment.tags: 578 | if each_tag in self.Tag_to_color_map.keys(): 579 | if self.Tag_to_color_map.get(each_tag) != None: 580 | eachComment.color = self.Tag_to_color_map.get(each_tag) 581 | 582 | def Open_network_location(self, network_location: str): 583 | """ Moves network to save location 584 | """ 585 | ui.panes.current.owner = network_location 586 | ui.panes.current.home() 587 | 588 | def Ignore_current_changes(self, op_id: int): 589 | self._ops_manager.Ignore_current_dirty_state(op_id) 590 | 591 | def Open_floating_external_tox_window(self) -> None: 592 | """Opens window for tox files 593 | """ 594 | # run dirty check 595 | # self._ops_manager.Dirty_check() 596 | # force cook op to see current status 597 | self.my_op.op('script1').cook(force=True) 598 | # open window for external tox files 599 | parent.save.op('window1').par.winopen.pulse() 600 | 601 | def Keyboard_input(self, shortcut): 602 | """Keyboard input handler 603 | """ 604 | shortcut_lookup = { 605 | 'ctrl.w': self.Prompt_to_save, 606 | 'ctrl.shift.w': self.Open_floating_external_tox_window 607 | } 608 | 609 | func = shortcut_lookup.get(shortcut) 610 | 611 | try: 612 | func() 613 | 614 | except Exception as e: 615 | self.Logtotextport(e) 616 | 617 | # NOTE - utils for search 618 | def Edit_search(self, search_val: str) -> None: 619 | """Updates filter string par 620 | """ 621 | self._lister_COMP.par.Filterstring = search_val 622 | 623 | def _clear_search(self) -> None: 624 | """Clears filter string""" 625 | self._search_text_COMP.par.text = '' 626 | 627 | # NOTE - lister utils 628 | def Lister_on_click(self, info: dict) -> None: 629 | """Uses the listerFuncs module to run any onClick functions 630 | """ 631 | listerFuncs.parse_col(info) 632 | 633 | def Lister_on_click_right(self, info: dict) -> None: 634 | """Uses the listerFuncs module to run any onRightClick functions 635 | """ 636 | listerFuncs.parse_right_click(info) 637 | --------------------------------------------------------------------------------