SideFX Houdini customization package
git clone https://git.lucas.co/hou-control.git
hc: tolerate a network editor with no network
An editor handed the event hook an event while its pwd() was None -- seen
live while floating pane tabs were being created and closed -- and the
overlay reached currentNode() through the hook, which raised
AttributeError on every event until the editor had a network again.
Houdini drops an event the hook raises on.
The hook now lets such an event through untouched, before wrapping the
editor. currentNode() and the overlay answer None / draw nothing without a
network, and the nearest-node anchor does the same, so the same state
reached from a command or a reload is harmless too. check.py drives
currentNode() and the overlay with a stub whose pwd() is None, and asserts
the hook's guard precedes the wrapper in source, since nodegraphhooks
cannot be imported without hou.ui.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
python3.13libs/hc/hcnetworkeditor.py | 10 ++++++++-
python3.13libs/nodegraphhooks.py | 7 +++++++
tools/check.py | 39 ++++++++++++++++++++++++++++++++++++
3 files changed, 55 insertions(+), 1 deletion(-)
diff --git a/python3.13libs/hc/hcnetworkeditor.py b/python3.13libs/hc/hcnetworkeditor.py
index ce11003..10e326f 100644
--- a/python3.13libs/hc/hcnetworkeditor.py
+++ b/python3.13libs/hc/hcnetworkeditor.py
@@ -516,6 +516,9 @@ class HCNetworkEditor(HCPathTab):
calling redraw() unconditionally made panning visibly stutter; skip
the work when the resulting picture would be identical.
"""
+ if self.hou_tab.pwd() is None:
+ # Nothing to anchor the cursor to and nothing to draw an arrow at.
+ return
enabled = HCSettings().hcnetcursorEnabled()
rect = self._hcnetcursorDrawRect(self.hcnetcursorRect()) if enabled else None
cursor_path = self._hcnetcursorImageFile() if enabled else None
@@ -538,11 +541,14 @@ class HCNetworkEditor(HCPathTab):
self.updateCurrentNodeOverlay(force=True)
def _nearestNodeToViewportCenter(self):
+ network = self.hou_tab.pwd()
+ if network is None:
+ return None
bounds = self.bounds()
center = bounds.center()
best = None
best_dist = float("inf")
- for node in self.hou_tab.pwd().children():
+ for node in network.children():
npos = node.position()
dx = npos[0] - center[0]
dy = npos[1] - center[1]
@@ -686,6 +692,8 @@ class HCNetworkEditor(HCPathTab):
network on every mouse move would not be free.
"""
network = self.hou_tab.pwd()
+ if network is None:
+ return None
for node in network.selectedChildren():
if node.isCurrent():
return node
diff --git a/python3.13libs/nodegraphhooks.py b/python3.13libs/nodegraphhooks.py
index c94d4d9..a236151 100755
--- a/python3.13libs/nodegraphhooks.py
+++ b/python3.13libs/nodegraphhooks.py
@@ -162,6 +162,13 @@ def _queueSelectionSync(editor, pending_actions):
def createEventHandler(uievent, pending_actions):
editor = getattr(uievent, "editor", None)
+ # An editor can hand out an event while it has no network -- seen live as
+ # pwd() returning None while floating pane tabs were being created and
+ # closed. Everything below reads the network, and a traceback out of this
+ # hook makes Houdini drop the event, so let it have the event untouched.
+ if editor is not None and editor.pwd() is None:
+ return None, False
+
if editor is not None:
hc_editor = HCNetworkEditor(editor)
modifierstate = getattr(uievent, "modifierstate", None)
diff --git a/tools/check.py b/tools/check.py
index 4bd06d4..57d5830 100644
--- a/tools/check.py
+++ b/tools/check.py
@@ -1634,6 +1634,45 @@ def check_current_node():
check("currentNode goes None", nothing_current_is_none)
+ def no_network_is_none():
+ """pwd() can be None -- seen live while floating pane tabs were being
+ created and closed -- and the overlay reached currentNode() through
+ the event hook, which raised on every event until the editor had a
+ network again."""
+ editor = blank(HCNetworkEditor)
+ editor.hou_tab = _StubTab(None)
+ assert editor.currentNode() is None, "answered without a network"
+ return "None without a network"
+
+ check("currentNode without a network", no_network_is_none)
+
+ def overlay_skips_without_network():
+ """The stub has pwd() and nothing else: touching the editor to draw
+ raises AttributeError, so returning quietly is the only way through."""
+ editor = blank(HCNetworkEditor)
+ editor.hou_tab = _StubTab(None)
+ editor.updateCurrentNodeOverlay()
+ editor.updateCurrentNodeOverlay(force=True)
+ return "draws nothing"
+
+ check("overlay without a network", overlay_skips_without_network)
+
+ def hook_skips_without_network():
+ """The event hook lets the event through untouched rather than
+ reading a network the editor has not got. nodegraphhooks cannot be
+ imported without hou.ui (Houdini's nodegraphbase needs it at import),
+ so this reads the source: the guard has to come before the hook
+ wraps the editor."""
+ source = (ROOT / "python3.13libs" / "nodegraphhooks.py").read_text()
+ body = source[source.index("def createEventHandler("):]
+ guard = body.find("editor.pwd() is None")
+ wrap = body.find("HCNetworkEditor(editor)")
+ assert guard != -1, "the hook no longer checks for a missing network"
+ assert wrap != -1 and guard < wrap, "the hook wraps the editor before checking its network"
+ return "guard precedes the wrapper"
+
+ check("hook without a network", hook_skips_without_network)
+
def guards_stay_gone():
"""The `current.parent()` guards only fired in the fallback case, and
threw the answer away when they did. currentNode() now returns a child