# MIT License
#
# Copyright (c) 2017 Guillaume Jobst, www.cgtoolbox.com
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
import hou
import os
import sys
import subprocess
import hdefereval
import tempfile
import hashlib
try:
from PySide2 import QtCore
from PySide2 import QtWidgets
Slot = QtCore.Slot(str)
except ImportError:
try:
from PySide import QtCore
from PySide import QtGui as QtWidgets
Slot = QtCore.Slot(str)
except ImportError:
from PyQt import QtCore
from PyQt import QtGui as QtWidgets
Slot = QtCore.pyqtSlot(str)
TEMP_FOLDER = os.environ.get("EXTERNAL_EDITOR_TEMP_PATH",
tempfile.gettempdir())
def is_valid_parm(parm):
template = parm.parmTemplate()
if template.dataType() in [hou.parmData.Float,
hou.parmData.Int,
hou.parmData.String]:
return True
return False
def is_python_node(node):
node_def = node.type().definition()
if not node_def:
return False
if node_def.sections().get("PythonCook") is not None:
return True
return False
def clean_exp(parm):
try:
exp = parm.expression()
if exp == "":
exp = None
except hou.OperationFailed:
exp = None
if exp is not None:
parm.deleteAllKeyframes()
def get_config_file():
try:
return hou.findFile("ExternalEditor.cfg")
except hou.OperationFailed:
ver = hou.applicationVersion()
if sys.platform in ["darwin", "os2", "os2emx"]:
verStr = "{}.{}".format(ver[0], ver[1])
else:
verStr = "houdini{}.{}".format(ver[0], ver[1])
cfg_root = hou.expandString("$HOME") + os.sep + verStr
if not os.path.exists(cfg_root):
os.makedirs(cfg_root)
cfg = cfg_root + os.sep + "ExternalEditor.cfg"
return cfg
def set_external_editor():
r = QtWidgets.QFileDialog.getOpenFileName(hou.ui.mainQtWindow(),
"Select an external editor program")
if r[0]:
cfg = get_config_file()
with open(cfg, 'w') as f:
f.write(r[0])
root, file = os.path.split(r[0])
QtWidgets.QMessageBox.information(hou.ui.mainQtWindow(),
"Editor set",
"External editor set to: " + file)
return r[0]
return None
def get_external_editor():
editor = os.environ.get("EDITOR")
if not editor or not os.path.exists(editor):
cfg = get_config_file()
if os.path.exists(cfg):
with open(cfg, 'r') as f:
editor = f.read().strip()
else:
editor = ""
if os.path.exists(editor):
return editor
else:
r = QtWidgets.QMessageBox.information(hou.ui.mainQtWindow(),
"Editor not set",
"No external editor set, pick one ?",
"Yes", "Cancel")
if r == 1:
return
return set_external_editor()
return None
@QtCore.Slot(str)
def filechanged(file_name):
""" Signal emitted by the watcher to update the parameter contents.
TODO: set expression when not a string parm.
"""
parms_bindings = getattr(hou.session, "PARMS_BINDINGS", None)
if not parms_bindings:
return
parm = None
node = None
try:
binding = parms_bindings.get(file_name)
if isinstance(binding, hou.Parm):
parm = binding
else:
node = binding
if node is not None:
try:
with open(file_name, 'r') as f:
data = f.read()
node.type().definition().sections()["PythonCook"].setContents(data)
except hou.OperationFailed:
remove_file_from_watcher(file_name)
return
if parm is not None:
# check if the parm exists, if not, remove the file from watcher
try:
parm.parmTemplate()
except hou.ObjectWasDeleted:
remove_file_from_watcher(file_name)
del parms_bindings[file_name]
return
with open(file_name, 'r') as f:
data = f.read()
template = parm.parmTemplate()
if template.dataType() == hou.parmData.String:
parm.set(data)
return
if template.dataType() == hou.parmData.Float:
try:
data = float(data)
clean_exp(parm)
parm.set(data)
return
except ValueError:
parm.setExpression(data)
return
if template.dataType() == hou.parmData.Int:
try:
data = int(data)
clean_exp(parm)
parm.set(data)
return
except ValueError:
parm.setExpression(data)
return
except Exception as e:
print("Watcher error: " + str(e))
def get_file_ext(parm, type_="parm"):
""" Get the file name's extention according to parameter's temaplate.
"""
if type_ == "python_node":
return ".py"
template = parm.parmTemplate()
editorlang = template.tags().get("editorlang", "").lower()
if editorlang == "vex":
return ".vfl"
elif editorlang == "python":
return ".py"
else:
try:
if parm.expressionLanguage() == hou.exprLanguage.Python:
return ".py"
else:
return ".txt"
except hou.OperationFailed:
return ".txt"
def get_file_name(data, type_="parm"):
""" Construct an unique file name from a parameter with right extension.
"""
if type_ == "parm":
node = data.node()
sid = str(node.sessionId())
file_name = sid + '_' + node.name() + '_' + data.name() + get_file_ext(data)
file_path = TEMP_FOLDER + os.sep + file_name
elif type_ == "python_node":
sid = hashlib.sha1(data.path()).hexdigest()
file_name = sid + '_' + data.name() + get_file_ext(data, type_="python_node")
file_path = TEMP_FOLDER + os.sep + file_name
return file_path
def get_file_watcher():
return getattr(hou.session, "FILE_WATCHER", None)
def get_parm_bindings