SideFX Houdini customization package
git clone https://git.lucas.co/hou-control.git
python3.13libs/hc/hclayout.py (6.3K)
1 import hou
2 from .hcsession import HCSession, floatWindow
3
4
5 DEFAULT_FLOATING_PANE_TYPES = (
6 hou.paneTabType.Parm,
7 hou.paneTabType.NetworkEditor,
8 )
9
10
11 class HCLayout:
12 """Wraps the current Houdini desktop / pane / floating-panel state."""
13
14
15 """ Desktop """
16
17
18 def desktop(self):
19 return hou.ui.curDesktop()
20
21 def desktopName(self):
22 return self.desktop().name()
23
24 def mode(self):
25 return "detached" if self.desktopName() == "hc_detached" else "attached"
26
27 def setDesktop(self, name):
28 for d in hou.ui.desktops():
29 if d.name() == name:
30 d.setAsCurrent()
31 return True
32 return False
33
34
35 """ Panes """
36
37
38 def panes(self):
39 return self.desktop().panes()
40
41 def paneCount(self):
42 return len(self.panes())
43
44 def paneTabs(self):
45 tabs = []
46 for pane in self.panes():
47 tabs.extend(pane.tabs())
48 return tabs
49
50 def paneTabTypes(self):
51 return [tab.type() for tab in self.paneTabs()]
52
53
54 """ Floating """
55
56
57 def floatingPanels(self):
58 return [p for p in hou.ui.floatingPanels() if p.qtParentWindow() is not None]
59
60 def floatingPaneTabs(self):
61 tabs = []
62 for fp in self.floatingPanels():
63 tabs.extend(fp.paneTabs())
64 return tabs
65
66 def floatingPaneTabTypes(self):
67 return [tab.type() for tab in self.floatingPaneTabs()]
68
69 def _initializePanelTabs(self, panel):
70 from .hcpane import HCPane
71 initialized_panes = set()
72 for hou_tab in panel.paneTabs():
73 pane = hou_tab.pane()
74 if pane is None:
75 continue
76 hc_pane = HCPane(pane)
77 if pane.id() not in initialized_panes:
78 try:
79 hc_pane.initializeObject(pane)
80 except Exception as e:
81 print(f"[HCLayout] initializeObject() failed for pane: {e!r}")
82 initialized_panes.add(pane.id())
83 else:
84 try:
85 hc_pane.initializeObject(hou_tab)
86 except Exception as e:
87 print(f"[HCLayout] initializeObject() failed for {hou_tab.type().name()}: {e!r}")
88
89 def _ensureSceneViewerDetailsTabInPanes(self, panes):
90 """Add a DetailsView tab to the first pane that has a SceneViewer but no DetailsView.
91
92 NOTE: Temporarily disabled -- pane.createTab(DetailsView) on a SceneViewer
93 pane causes SIGSEGV in Houdini 21.0.671 GL layer during startup. This
94 is a Houdini bug, not fixable from Python.
95 """
96 pass
97
98 def launchFloating(self, pane_types=None):
99 """Open one floating panel per pane type, skipping any already present.
100
101 Each floating panel is a separate OS window the WM can tile. The main
102 Houdini window still exists alongside them -- that is the whole point
103 of detached mode, so these panels are deliberately left as ordinary
104 toplevels for the WM to lay out.
105
106 An entry may instead be a `(pane_type, (width, height))` pair, which
107 opts that one panel out of tiling: it is created at that size and its
108 toplevel is marked as a dialog (see hcsession.floatWindow), which is
109 what stops a tiling WM from resizing it to the main window's tile.
110 DEFAULT_FLOATING_PANE_TYPES uses bare types, so the default layout
111 still tiles.
112
113 NOTE: Tab initialization (_initializePanelTabs) is intentionally NOT
114 called here -- the PySide6/GL context isn't ready during startup and
115 calling initialize() (which installs Qt event filters) causes SIGSEGV.
116 The caller is responsible for deferring initialization separately.
117 """
118 import sys
119 if pane_types is None:
120 pane_types = DEFAULT_FLOATING_PANE_TYPES
121 existing = set(self.floatingPaneTabTypes())
122 created = []
123 for entry in pane_types:
124 pane_type, size = self._paneTypeAndSize(entry)
125 if pane_type in existing:
126 continue
127 kwargs = {"immediate": True}
128 if size is not None:
129 kwargs["size"] = size
130 panel = self.desktop().createFloatingPanel(pane_type, **kwargs)
131 panel.attachToDesktop(True)
132 if size is not None:
133 self._floatPanel(panel, size)
134 created.append(panel)
135 return created
136
137 @staticmethod
138 def _paneTypeAndSize(entry):
139 """Split a launchFloating() entry into (pane_type, size or None)."""
140 if isinstance(entry, tuple):
141 pane_type, size = entry
142 return pane_type, tuple(size)
143 return entry, None
144
145 def _floatPanel(self, panel, size):
146 """Mark a freshly created panel's toplevel as a floating dialog.
147
148 Only ever called for an entry that asked for a size. Failures are
149 non-fatal: this runs during startup, and a panel that ends up tiled is
150 better than a traceback that stops the rest of the layout.
151 """
152 import sys
153 try:
154 tabs = panel.paneTabs()
155 window = tabs[0].qtParentWindow() if tabs else None
156 if window is None:
157 return
158 floatWindow(window, size=size)
159 window.show()
160 window.raise_()
161 except Exception as e:
162 print(f"[HCLayout] _floatPanel failed: {e!r}", file=sys.stderr, flush=True)
163
164 def initializeFloatingPanels(self):
165 """Initialize (HC-wrap) all current floating panel tabs.
166
167 Safe to call deferred after the GL event loop has settled.
168 """
169 import sys
170 for panel in self.floatingPanels():
171 try:
172 self._initializePanelTabs(panel)
173 except Exception as e:
174 print(f"[HCLayout] initializeFloatingPanels failed: {e!r}", file=sys.stderr, flush=True)
175
176
177 """ Snapshot """
178
179
180 def info(self):
181 return {
182 "desktop": self.desktopName(),
183 "mode": self.mode(),
184 "pane_count": self.paneCount(),
185 "pane_tab_types": [t.name() for t in self.paneTabTypes()],
186 "floating_pane_count": len(self.floatingPanels()),
187 "floating_pane_tab_types": [t.name() for t in self.floatingPaneTabTypes()],
188 }
189
190
191 def launchFloatingLayout(pane_types=None):
192 return HCLayout().launchFloating(pane_types)
193