├── .gitattributes ├── .gitignore ├── HoudiniExprEditor.pyproj ├── MainMenuCommon.xml ├── OPmenu.xml ├── PARMmenu.xml ├── README.md ├── ShelfToolMenu.xml ├── build_infos.txt ├── doc ├── doc_code_applied.png ├── doc_pick_editor.png ├── doc_remove_file_watcher.png ├── doc_right_clik_parm.png ├── houdini_expr_a.png ├── houdini_expr_b.png ├── houdini_expr_c.png ├── houdini_expr_d.png ├── houdini_expr_e.png └── houdini_expr_f.png └── scripts └── python └── HoudiniExprEditor ├── ParmWatcher.py └── __init__.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # vscode 7 | .vscode 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | env/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *,cover 49 | .hypothesis/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | 59 | # Flask instance folder 60 | instance/ 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # IPython Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # dotenv 81 | .env 82 | 83 | # virtualenv 84 | venv/ 85 | ENV/ 86 | 87 | # Spyder project settings 88 | .spyderproject 89 | 90 | # Rope project settings 91 | .ropeproject 92 | 93 | # ========================= 94 | # Operating System Files 95 | # ========================= 96 | 97 | # OSX 98 | # ========================= 99 | 100 | .DS_Store 101 | .AppleDouble 102 | .LSOverride 103 | 104 | # Thumbnails 105 | ._* 106 | 107 | # Files that might appear in the root of a volume 108 | .DocumentRevisions-V100 109 | .fseventsd 110 | .Spotlight-V100 111 | .TemporaryItems 112 | .Trashes 113 | .VolumeIcon.icns 114 | 115 | # Directories potentially created on remote AFP share 116 | .AppleDB 117 | .AppleDesktop 118 | Network Trash Folder 119 | Temporary Items 120 | .apdisk 121 | 122 | # Windows 123 | # ========================= 124 | 125 | # Windows image file caches 126 | Thumbs.db 127 | ehthumbs.db 128 | 129 | # Folder config file 130 | Desktop.ini 131 | 132 | # Recycle Bin used on file shares 133 | $RECYCLE.BIN/ 134 | 135 | # Windows Installer files 136 | *.cab 137 | *.msi 138 | *.msm 139 | *.msp 140 | 141 | # Windows shortcuts 142 | *.lnk 143 | -------------------------------------------------------------------------------- /HoudiniExprEditor.pyproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Debug 5 | 2.0 6 | {e1f0542d-9594-432d-8b78-19d500fa3786} 7 | 8 | 9 | 10 | 11 | . 12 | . 13 | {888888a0-9f3d-457c-b088-3a5042f75d52} 14 | Standard Python launcher 15 | HoudiniExprEditor 16 | 17 | 18 | 19 | 20 | 10.0 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | Code 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /MainMenuCommon.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | preferences_submenu 18 | h.prefs_misc 19 | 20 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | windows_menu 36 | h.python_source_editor 37 | 38 | 39 | 45 | 46 | 47 | 48 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /OPmenu.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 16 | 17 | 18 | 19 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 0 41 | except Exception as e: 42 | return False]]> 43 | 44 | 45 | 46 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /PARMmenu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | expression_menu 18 | edit_expression 19 | 20 | 21 | 27 | 28 | 29 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | expression_menu 45 | open_external_editor 46 | 47 | 48 | 54 | 55 | 56 | 57 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HoudiniExprEditor 2 | Houdini External Editor with file watchers system 3 | 4 | More infos: 5 | http://cgtoolbox.com/houdini-expression-editor/ 6 | -------------------------------------------------------------------------------- /ShelfToolMenu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | edit_tool 70 | 71 | 72 | 78 | 79 | 80 | 81 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 106 | 107 | 108 | 109 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /build_infos.txt: -------------------------------------------------------------------------------- 1 | $VERSION:scripts/python/HoudiniExprEditor/__init__.py 2 | $NAME:HoudiniExprEditor 3 | $DOC_LINK:cgtoolbox.com/houdini-expression-editor/ 4 | scripts:scripts 5 | /:PARMmenu.xml 6 | /:MainMenuCommon.xml 7 | /:OPmenu.xml 8 | /:ShelfToolMenu.xml -------------------------------------------------------------------------------- /doc/doc_code_applied.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgtoolbox/HoudiniExprEditor/7f87c5490625a7f600a00c7af55b9f6bd8ced3b6/doc/doc_code_applied.png -------------------------------------------------------------------------------- /doc/doc_pick_editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgtoolbox/HoudiniExprEditor/7f87c5490625a7f600a00c7af55b9f6bd8ced3b6/doc/doc_pick_editor.png -------------------------------------------------------------------------------- /doc/doc_remove_file_watcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgtoolbox/HoudiniExprEditor/7f87c5490625a7f600a00c7af55b9f6bd8ced3b6/doc/doc_remove_file_watcher.png -------------------------------------------------------------------------------- /doc/doc_right_clik_parm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgtoolbox/HoudiniExprEditor/7f87c5490625a7f600a00c7af55b9f6bd8ced3b6/doc/doc_right_clik_parm.png -------------------------------------------------------------------------------- /doc/houdini_expr_a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgtoolbox/HoudiniExprEditor/7f87c5490625a7f600a00c7af55b9f6bd8ced3b6/doc/houdini_expr_a.png -------------------------------------------------------------------------------- /doc/houdini_expr_b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgtoolbox/HoudiniExprEditor/7f87c5490625a7f600a00c7af55b9f6bd8ced3b6/doc/houdini_expr_b.png -------------------------------------------------------------------------------- /doc/houdini_expr_c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgtoolbox/HoudiniExprEditor/7f87c5490625a7f600a00c7af55b9f6bd8ced3b6/doc/houdini_expr_c.png -------------------------------------------------------------------------------- /doc/houdini_expr_d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgtoolbox/HoudiniExprEditor/7f87c5490625a7f600a00c7af55b9f6bd8ced3b6/doc/houdini_expr_d.png -------------------------------------------------------------------------------- /doc/houdini_expr_e.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgtoolbox/HoudiniExprEditor/7f87c5490625a7f600a00c7af55b9f6bd8ced3b6/doc/houdini_expr_e.png -------------------------------------------------------------------------------- /doc/houdini_expr_f.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgtoolbox/HoudiniExprEditor/7f87c5490625a7f600a00c7af55b9f6bd8ced3b6/doc/houdini_expr_f.png -------------------------------------------------------------------------------- /scripts/python/HoudiniExprEditor/ParmWatcher.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # MIT License 4 | # 5 | # Copyright (c) 2017-2020 Guillaume Jobst, www.cgtoolbox.com 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | # 25 | 26 | import hou 27 | import os 28 | import sys 29 | import time 30 | import subprocess 31 | import hdefereval 32 | import tempfile 33 | import hashlib 34 | 35 | try: 36 | from PySide2 import QtCore 37 | from PySide2 import QtWidgets 38 | Slot = QtCore.Slot(str) 39 | except ImportError: 40 | 41 | try: 42 | from PySide import QtCore 43 | from PySide import QtGui as QtWidgets 44 | Slot = QtCore.Slot(str) 45 | except ImportError: 46 | from PyQt import QtCore 47 | from PyQt import QtGui as QtWidgets 48 | Slot = QtCore.pyqtSlot(str) 49 | 50 | TEMP_FOLDER = os.environ.get("EXTERNAL_EDITOR_TEMP_PATH", 51 | tempfile.gettempdir()) 52 | 53 | def is_valid_parm(parm): 54 | 55 | template = parm.parmTemplate() 56 | if template.dataType() in [hou.parmData.Float, 57 | hou.parmData.Int, 58 | hou.parmData.String]: 59 | return True 60 | 61 | return False 62 | 63 | def is_python_node(node): 64 | 65 | node_def = node.type().definition() 66 | if not node_def: 67 | return False 68 | 69 | if node_def.sections().get("PythonCook") is not None: 70 | return True 71 | return False 72 | 73 | def clean_exp(parm): 74 | 75 | try: 76 | exp = parm.expression() 77 | if exp == "": 78 | exp = None 79 | except hou.OperationFailed: 80 | exp = None 81 | 82 | if exp is not None: 83 | parm.deleteAllKeyframes() 84 | 85 | def get_extra_file_scripts(node): 86 | 87 | node_def = node.type().definition() 88 | 89 | if node_def is None: 90 | return [] 91 | 92 | extra_file_options = node_def.extraFileOptions() 93 | pymodules = [m.split('/')[0] for m in extra_file_options.keys() \ 94 | if "IsPython" in m \ 95 | and extra_file_options[m]] 96 | 97 | return pymodules 98 | 99 | def get_config_file(): 100 | 101 | try: 102 | return hou.findFile("ExternalEditor.cfg") 103 | except hou.OperationFailed: 104 | return os.path.join(hou.expandString("$HOUDINI_USER_PREF_DIR"), "ExternalEditor.cfg") 105 | 106 | def set_external_editor(): 107 | 108 | r = QtWidgets.QFileDialog.getOpenFileName(hou.ui.mainQtWindow(), 109 | "Select an external editor program") 110 | if r[0]: 111 | 112 | cfg = get_config_file() 113 | 114 | with open(cfg, 'w') as f: 115 | f.write(r[0]) 116 | 117 | root, file = os.path.split(r[0]) 118 | 119 | QtWidgets.QMessageBox.information(hou.ui.mainQtWindow(), 120 | "Editor set", 121 | "External editor set to: " + file) 122 | 123 | return r[0] 124 | 125 | return None 126 | 127 | def get_external_editor(): 128 | 129 | editor = os.environ.get("EDITOR") 130 | if not editor or not os.path.exists(editor): 131 | 132 | cfg = get_config_file() 133 | if os.path.exists(cfg): 134 | with open(cfg, 'r') as f: 135 | editor = f.read().strip() 136 | 137 | else: 138 | editor = "" 139 | 140 | if os.path.exists(editor): 141 | return editor 142 | 143 | else: 144 | 145 | r = QtWidgets.QMessageBox.information(hou.ui.mainQtWindow(), 146 | "Editor not set", 147 | "No external editor set, pick one ?", 148 | QtWidgets.QMessageBox.Yes, 149 | QtWidgets.QMessageBox.Cancel) 150 | if r == QtWidgets.QMessageBox.Cancel: 151 | return 152 | 153 | return set_external_editor() 154 | 155 | return None 156 | 157 | def _read_file_data(file_name): 158 | # Some external editor ( like VSCode ) empty the file before saving it 159 | # this will trigger the file watcher and will read empty data. We try 160 | # to read it again after half a second to be sure the data is really empty or not. 161 | # For VSCode: https://github.com/microsoft/vscode/pull/62296 162 | 163 | with open(file_name, 'r') as f: 164 | data = f.read() 165 | 166 | if data == '': 167 | time.sleep(0.5) 168 | with open(file_name, 'r') as f: 169 | data = f.read() 170 | 171 | return data 172 | 173 | @QtCore.Slot(str) 174 | def filechanged(file_name): 175 | """ Signal emitted by the watcher to update the parameter contents. 176 | TODO: set expression when not a string parm. 177 | """ 178 | parms_bindings = getattr(hou.session, "PARMS_BINDINGS", None) 179 | if not parms_bindings: 180 | return 181 | 182 | parm = None 183 | node = None 184 | tool = None 185 | 186 | try: 187 | binding = parms_bindings.get(file_name) 188 | if isinstance(binding, hou.Parm): 189 | parm = binding 190 | elif isinstance(binding, hou.Tool): 191 | tool = binding 192 | else: 193 | node = binding 194 | 195 | try: 196 | if binding == "__temp__python_source_editor": 197 | 198 | data = _read_file_data(file_name) 199 | try: 200 | hou.setSessionModuleSource(data) 201 | except hou.OperationFailed: 202 | print("Watcher error: Invalid source code.") 203 | return 204 | except hou.ObjectWasDeleted: 205 | remove_file_from_watcher(file_name) 206 | del parms_bindings[file_name] 207 | return 208 | 209 | if tool is not None: 210 | data = _read_file_data(file_name) 211 | try: 212 | tool.setScript(data) 213 | except hou.ObjectWasDeleted: 214 | remove_file_from_watcher(file_name) 215 | del parms_bindings[file_name] 216 | return 217 | return 218 | 219 | if node is not None: 220 | try: 221 | data = _read_file_data(file_name) 222 | 223 | section = "PythonCook" 224 | if "_extraSection_" in file_name: 225 | section = file_name.split("_extraSection_")[-1].split('.')[0] 226 | 227 | # Block file watcher during module section update to prevent infinite loops in certain cases 228 | watcher = get_file_watcher() 229 | watcher.blockSignals(True) 230 | node.type().definition().sections()[section].setContents(data) 231 | watcher.blockSignals(False) 232 | 233 | except hou.OperationFailed as e: 234 | print("HoudiniExprEditor: Can't update module content {}, watcher will be removed.".format(e)) 235 | remove_file_from_watcher(file_name) 236 | del parms_bindings[file_name] 237 | return 238 | 239 | if parm is not None: 240 | 241 | # check if the parm exists, if not, remove the file from watcher 242 | try: 243 | parm.parmTemplate() 244 | except hou.ObjectWasDeleted: 245 | remove_file_from_watcher(file_name) 246 | del parms_bindings[file_name] 247 | return 248 | 249 | data = _read_file_data(file_name) 250 | 251 | template = parm.parmTemplate() 252 | if template.dataType() == hou.parmData.String: 253 | parm.set(data) 254 | return 255 | 256 | if template.dataType() == hou.parmData.Float: 257 | 258 | try: 259 | data = float(data) 260 | 261 | clean_exp(parm) 262 | 263 | parm.set(data) 264 | return 265 | 266 | except ValueError: 267 | parm.setExpression(data) 268 | return 269 | 270 | if template.dataType() == hou.parmData.Int: 271 | 272 | try: 273 | data = int(data) 274 | 275 | clean_exp(parm) 276 | 277 | parm.set(data) 278 | return 279 | 280 | except ValueError: 281 | parm.setExpression(data) 282 | return 283 | 284 | except Exception as e: 285 | print("Watcher error: " + str(e)) 286 | 287 | def get_file_ext(parm, type_="parm"): 288 | """ Get the file name's extention according to parameter's temaplate. 289 | """ 290 | 291 | if type_ == "python_node": 292 | return ".py" 293 | 294 | template = parm.parmTemplate() 295 | editorlang = template.tags().get("editorlang", "").lower() 296 | 297 | if editorlang == "vex": 298 | return ".vfl" 299 | 300 | elif editorlang == "python": 301 | return ".py" 302 | 303 | elif editorlang == "opencl": 304 | return ".cl" 305 | 306 | else: 307 | 308 | try: 309 | if parm.expressionLanguage() == hou.exprLanguage.Python: 310 | return ".py" 311 | else: 312 | return ".txt" 313 | except hou.OperationFailed: 314 | return ".txt" 315 | 316 | def get_file_name(data, type_="parm"): 317 | """ Construct an unique file name from a parameter with right extension. 318 | """ 319 | 320 | if type_ == "parm": 321 | node = data.node() 322 | sid = str(node.sessionId()) 323 | file_name = sid + '_' + node.name() + '_' + data.name() + get_file_ext(data) 324 | file_path = TEMP_FOLDER + os.sep + file_name 325 | 326 | elif type_ == "python_node" or "extra_section|" in type_: 327 | sid = hashlib.sha1(data.path().encode("utf-8")).hexdigest() 328 | 329 | name = data.name() 330 | if "extra_section|" in type_: 331 | name += "_extraSection_" + type_.split('|')[-1] 332 | 333 | file_name = sid + '_' + name + get_file_ext(data, type_="python_node") 334 | file_path = TEMP_FOLDER + os.sep + file_name 335 | 336 | elif type_.startswith("__shelf_tool|"): 337 | 338 | language = type_.split('|')[-1] 339 | if language == "python": 340 | file_name = "__shelf_tool_" + data.name() + ".py" 341 | else: 342 | file_name = "__shelf_tool_" + data.name() + ".txt" 343 | file_path = TEMP_FOLDER + os.sep + file_name 344 | 345 | elif type_ == "__temp__python_source_editor": 346 | 347 | file_name = "__python_source_editor.py" 348 | file_path = TEMP_FOLDER + os.sep + file_name 349 | 350 | return file_path 351 | 352 | def get_file_watcher(): 353 | 354 | return getattr(hou.session, "FILE_WATCHER", None) 355 | 356 | def get_parm_bindings(): 357 | 358 | return getattr(hou.session, "PARMS_BINDINGS", None) 359 | 360 | def clean_files(): 361 | 362 | try: 363 | bindings = get_parm_bindings() 364 | watcher = get_file_watcher() 365 | keys_to_delete = [] 366 | 367 | if bindings is not None and watcher is not None: 368 | for k, v in bindings.items(): 369 | 370 | if isinstance(v, str) and v == "__temp__python_source_editor": 371 | # never clean python source editor as it can't be deleted. 372 | continue 373 | elif not os.path.exists(k): 374 | remove_file_from_watcher(k) 375 | keys_to_delete.append(k) 376 | elif isinstance(v, hou.Tool): 377 | try: 378 | v.filePath() 379 | except hou.ObjectWasDeleted: 380 | remove_file_from_watcher(k) 381 | keys_to_delete.append(k) 382 | else: 383 | try: 384 | v.path() 385 | except hou.ObjectWasDeleted: 386 | remove_file_from_watcher(k) 387 | keys_to_delete.append(k) 388 | 389 | if not k in watcher.files(): 390 | keys_to_delete.append(k) 391 | 392 | for k in keys_to_delete: 393 | del bindings[k] 394 | 395 | except Exception as e: 396 | print("HoudiniExprEditor: Can't clean files: " + str(e)) 397 | 398 | def _node_deleted(node, **kwargs): 399 | 400 | try: 401 | file_name = get_file_name(node, type_="python_node") 402 | bindings = get_parm_bindings() 403 | if bindings: 404 | if file_name in bindings.keys(): 405 | del bindings[file_name] 406 | remove_file_from_watcher(file_name) 407 | except Exception as e: 408 | print("Error un callback: onDelete: " + str(e)) 409 | 410 | def add_watcher_to_section(selection): 411 | 412 | sel_def = selection.type().definition() 413 | if not sel_def: return 414 | 415 | sections = get_extra_file_scripts(selection) 416 | r = hou.ui.selectFromList(sections, exclusive=True, 417 | title="Pick a section:") 418 | if not r: return 419 | 420 | section = sections[r[0]] 421 | add_watcher(selection, type_="extra_section|" + section) 422 | 423 | def add_watcher(selection, type_="parm"): 424 | """ Create a file with the current parameter contents and 425 | create a file watcher, if not already created and found in hou.Session, 426 | add the file to the list of watched files. 427 | 428 | Link the file created to a parameter where the tool has been executed from 429 | and when the file changed, edit the parameter contents with text contents. 430 | """ 431 | 432 | file_path = get_file_name(selection, type_=type_) 433 | 434 | if type_ == "parm": 435 | # fetch parm content, either raw value or expression if any 436 | try: 437 | data = selection.expression() 438 | except hou.OperationFailed: 439 | if os.environ.get("EXTERNAL_EDITOR_EVAL_EXPRESSION") == '1': 440 | data = str(selection.eval()) 441 | else: 442 | data = str(selection.rawValue()) 443 | elif type_ == "python_node": 444 | data = selection.type().definition().sections()["PythonCook"].contents() 445 | 446 | elif "extra_section|" in type_: 447 | 448 | sec_name = type_.split('|')[-1] 449 | sec = selection.type().definition().sections().get(sec_name) 450 | if not sec: 451 | print("Error: No section {} found.".format(sec)) 452 | data = sec.contents() 453 | 454 | elif type_ == "__temp__python_source_editor": 455 | 456 | data = hou.sessionModuleSource() 457 | 458 | elif type_.startswith("__shelf_tool|"): 459 | 460 | data = selection.script() 461 | 462 | # use uft-8 only when nacessary 463 | with open(file_path, 'w') as f: 464 | try: 465 | f.write(data) 466 | except UnicodeEncodeError: 467 | f.write(data.encode("UTF-8")) 468 | 469 | vsc = get_external_editor() 470 | if not vsc: 471 | hou.ui.setStatusMessage("No external editor set", 472 | severity=hou.severityType.Error) 473 | return 474 | 475 | p = QtCore.QProcess(parent=hou.ui.mainQtWindow()) 476 | p.start(vsc, [file_path]) 477 | 478 | watcher = get_file_watcher() 479 | 480 | if not watcher: 481 | 482 | watcher = QtCore.QFileSystemWatcher([file_path], 483 | parent=hou.ui.mainQtWindow()) 484 | watcher.fileChanged.connect(filechanged) 485 | hou.session.FILE_WATCHER = watcher 486 | 487 | else: 488 | if not file_path in watcher.files(): 489 | 490 | watcher.addPath(file_path) 491 | 492 | parms_bindings = get_parm_bindings() 493 | if not parms_bindings: 494 | hou.session.PARMS_BINDINGS = {} 495 | parms_bindings = hou.session.PARMS_BINDINGS 496 | 497 | if not file_path in parms_bindings.keys(): 498 | 499 | parms_bindings[file_path] = selection 500 | 501 | # add "on removed" callback to remove file from watcher 502 | # when node is deleted 503 | if type_ == "python_node" or "extra_section|" in type_: 504 | 505 | selection.addEventCallback((hou.nodeEventType.BeingDeleted,), 506 | _node_deleted) 507 | 508 | clean_files() 509 | 510 | def parm_has_watcher(parm): 511 | """ Check if a parameter has a watcher attached to it 512 | Used to display or hide "Remove Watcher" menu. 513 | """ 514 | file_name = get_file_name(parm) 515 | watcher = get_file_watcher() 516 | if not watcher: 517 | return False 518 | 519 | parms_bindings = get_parm_bindings() 520 | if not parms_bindings: 521 | return False 522 | 523 | if file_name in parms_bindings.keys(): 524 | return True 525 | 526 | return False 527 | 528 | def tool_has_watcher(tool, type_=""): 529 | """ Check if a shelf tool has a watcher attached to it 530 | Used to display or hide "Remove Watcher" menu. 531 | """ 532 | file_name = get_file_name(tool, type_=type_) 533 | watcher = get_file_watcher() 534 | if not watcher: 535 | return False 536 | 537 | parms_bindings = get_parm_bindings() 538 | if not parms_bindings: 539 | return False 540 | 541 | if file_name in parms_bindings.keys(): 542 | return True 543 | 544 | return False 545 | 546 | def remove_file_from_watcher(file_name): 547 | 548 | watcher = get_file_watcher() 549 | if file_name in watcher.files(): 550 | watcher.removePath(file_name) 551 | return True 552 | 553 | return False 554 | 555 | def remove_file_watched(parm, type_="parm"): 556 | """ Check if a given parameter's watched file exist and remove it 557 | from watcher list, do not remove the file itself. 558 | """ 559 | 560 | file_name = get_file_name(parm, type_=type_) 561 | r = remove_file_from_watcher(file_name) 562 | if r: 563 | clean_files() 564 | QtWidgets.QMessageBox.information(hou.ui.mainQtWindow(), 565 | "Watcher Removed", 566 | "Watcher removed on file: " + file_name) 567 | -------------------------------------------------------------------------------- /scripts/python/HoudiniExprEditor/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | __version__ = "1.4.8" --------------------------------------------------------------------------------