SideFX Houdini customization package
git clone https://git.lucas.co/hou-control.git
python3.13libs/uiready.py (5.3K)
1 import hou
2 import hrpyc
3 import sys
4 import traceback
5
6 from hc import HCSession, HCSettings
7
8 # Start the agent bridge before anything else.
9 #
10 # It used to be the last statement in this file, behind every piece of startup
11 # below, so one exception in hotkey loading or node recolouring or the status
12 # bar aborted the module and took the bridge down with it -- silently, since
13 # nothing here reports. That is exactly backwards: the bridge is how you
14 # inspect a session whose startup went wrong, so it must not depend on that
15 # startup having gone right.
16 #
17 # rpyc's ThreadedServer runs every request on a connection thread, and HOM is
18 # not thread-safe -- a hou call off the main thread segfaults Houdini inside
19 # SWIG. Install the main-thread pump before the server can accept anything;
20 # see hc.hcmainthread and the runner in houdini-agent/bridge.py.
21 from hc import hcmainthread
22
23 hcmainthread.install()
24 try:
25 # use_thread=True by default, so this returns rather than blocking startup.
26 hrpyc.start_server(port=18811)
27 except OSError:
28 pass # already bound, e.g. a second Houdini on the same port
29
30
31 def _step(label, fn, *args):
32 """Run one startup step; report a failure instead of aborting the rest.
33
34 These are independent -- a broken hotkey file should not cost you the
35 status bar -- and a printed traceback beats a session that is half set up
36 for no visible reason.
37 """
38 try:
39 fn(*args)
40 return True
41 except Exception:
42 print(f"[uiready] {label} failed:", file=sys.stderr)
43 traceback.print_exc()
44 return False
45
46
47 hc_session = HCSession()
48
49 # What this session started with, so the settings panel can say which
50 # restart-only settings have since been saved with a different value. After
51 # 123.py, whose startup dialog may have just written desktop_mode.
52 _step("captureStartupSettings", HCSettings().captureStartupValues)
53 _step("reloadHotkeys", hc_session.reloadHotkeys)
54 _step("initializeNetworkEditorsDeferred", hc_session.initializeNetworkEditorsDeferred)
55 _step("updateNodeColors", hc_session.updateNodeColors)
56
57
58 def _startViewRegions():
59 from hc.hcviewregions import HCViewRegions
60 HCViewRegions().start()
61
62
63 # Tells cce-fx which panes want a two-finger swipe as a view drag, so the
64 # others scroll; see hc.hcviewregions. Inert without ccectl.
65 _step("viewRegions", _startViewRegions)
66
67 desktop_mode = HCSettings().desktopMode()
68
69
70 def _applyMainMenuSetting():
71 """Houdini's main menu bar is a persisted preference (showmenu.val), so
72 without this the last session's choice would carry over regardless of
73 the setting."""
74 show = HCSettings().section("startup").get("show_main_menu", False)
75 hc_session.showMainMenu(int(bool(show)))
76
77 if desktop_mode == "detached":
78 def _deferredInit():
79 """Two-phase deferred init for detached mode.
80
81 Phase 1 (tick 1): Desktop setup + initialize existing floating panels.
82 Phase 2 (tick 5): Create any missing floating panels + initialize.
83
84 ALL setup is deferred to avoid posting Qt events during
85 uiready.py module-level execution, which triggers re-entrant
86 event processing that SIGSEGVs in PySide::getWrapperForQObject
87 (Houdini 21.0.700 bug).
88
89 Floating panel initialization (showTabs, showNetworkControls, etc.)
90 is safe at tick 1 because all initialize() methods are pure HOM
91 calls -- no PySide6 event filter installation.
92 """
93 global _layout
94 count = getattr(hou.session, '_hc_deferred_count', 0) + 1
95 hou.session._hc_deferred_count = count
96
97 if count == 1:
98 # Desktop setup + initialize panels already loaded from .desk
99 for d in hou.ui.desktops():
100 if d.name() == "hc_detached":
101 d.setAsCurrent()
102 break
103 hc_session.toggleStowbars()
104 hc_session.hideShelf()
105 _applyMainMenuSetting()
106 from hc import HCStatusBar
107 HCStatusBar().show()
108 from hc.hclayout import HCLayout
109 _layout = HCLayout()
110 _layout.initializeFloatingPanels()
111
112 if count == 5:
113 # Create any panels not already in .desk, initialize immediately
114 from hc.hclayout import launchFloatingLayout
115 launchFloatingLayout()
116 _layout.initializeFloatingPanels()
117 hou.session._hc_deferred_done = True
118 try:
119 hou.ui.removeEventLoopCallback(_safeDeferredInit)
120 except Exception:
121 pass
122
123 def _safeDeferredInit():
124 if getattr(hou.session, '_hc_deferred_done', False):
125 return
126 _deferredInit()
127
128 _layout = None
129 hou.session._hc_deferred_count = 0
130 hou.session._hc_deferred_done = False
131 hou.ui.addEventLoopCallback(_safeDeferredInit)
132 else:
133 # Attached mode runs at module level (has been stable)
134 def _setDesktop():
135 for d in hou.ui.desktops():
136 if d.name() == "hc_attached":
137 d.setAsCurrent()
138 return
139
140 def _showStatusBar():
141 from hc import HCStatusBar
142 HCStatusBar().show()
143
144 _step("set hc_attached desktop", _setDesktop)
145 _step("toggleStowbars", hc_session.toggleStowbars)
146 _step("mainMenu", _applyMainMenuSetting)
147 _step("splitHandles", lambda: hc_session.splitHandles().show())
148 _step("statusBar", _showStatusBar)