├── .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 |
60 |
61 |
--------------------------------------------------------------------------------
/OPmenu.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
58 |
--------------------------------------------------------------------------------
/PARMmenu.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
11 |
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 |
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"
--------------------------------------------------------------------------------