git.lucas.co / hou-control
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