git.lucas.co / hou-control
git clone https://git.lucas.co/hou-control.git

python3.11libs/hc/hcsplithandles.py (7K)

  1 import hou
  2 from PySide6.QtCore import QEvent, QObject, QPoint, Qt, QTimer
  3 from PySide6.QtWidgets import QWidget
  4 
  5 
  6 HCSPLITHANDLE_THICKNESS = 20
  7 HCSPLITHANDLE_LENGTH = 80
  8 HCSPLITHANDLE_MIN_FRACTION = 0.05
  9 HCSPLITHANDLE_MAX_FRACTION = 0.95
 10 
 11 
 12 class _SplitHandle(QWidget):
 13     REST_STYLE = f"background: #3a3a3a; border-radius: {HCSPLITHANDLE_THICKNESS // 2}px;"
 14     HOVER_STYLE = f"background: #90EE90; border-radius: {HCSPLITHANDLE_THICKNESS // 2}px;"
 15 
 16     def __init__(self, parent, hou_pane, horizontal_boundary, manager):
 17         super().__init__(parent)
 18         self.hou_pane = hou_pane
 19         self.horizontal_boundary = horizontal_boundary
 20         self.manager = manager
 21         self.setObjectName(f"hc_split_handle_{hou_pane.id()}")
 22         self.setCursor(Qt.SplitVCursor if horizontal_boundary else Qt.SplitHCursor)
 23         self.setAttribute(Qt.WA_StyledBackground, True)
 24         self.setStyleSheet(self.REST_STYLE)
 25         self._origin = None
 26         self._start_fraction = None
 27         self._parent_extent = None
 28         self._drag_child = None
 29         self._pending_fraction = None
 30         self._throttle = QTimer(self)
 31         self._throttle.setSingleShot(True)
 32         self._throttle.setInterval(33)
 33         self._throttle.timeout.connect(self._applyPending)
 34 
 35     def event(self, event):
 36         et = event.type()
 37         if et == QEvent.Enter:
 38             self.setStyleSheet(self.HOVER_STYLE)
 39             self.repaint()
 40         elif et == QEvent.Leave:
 41             self.setStyleSheet(self.REST_STYLE)
 42             self.repaint()
 43         return super().event(event)
 44 
 45     def mousePressEvent(self, event):
 46         if event.button() != Qt.LeftButton:
 47             return
 48         self._origin = event.globalPosition().toPoint()
 49         self._drag_child = self.hou_pane.getSplitChild(0)
 50         self._start_fraction = self._drag_child.getSplitFraction()
 51         geom = self.hou_pane.qtScreenGeometry()
 52         self._parent_extent = geom.height() if self.horizontal_boundary else geom.width()
 53         event.accept()
 54 
 55     def mouseMoveEvent(self, event):
 56         if self._origin is None or not self._parent_extent:
 57             return
 58         delta = event.globalPosition().toPoint() - self._origin
 59         pixel_delta = -delta.y() if self.horizontal_boundary else delta.x()
 60         fraction_delta = pixel_delta / self._parent_extent
 61         new_fraction = max(
 62             HCSPLITHANDLE_MIN_FRACTION,
 63             min(HCSPLITHANDLE_MAX_FRACTION, self._start_fraction + fraction_delta),
 64         )
 65         self._pending_fraction = new_fraction
 66         if not self._throttle.isActive():
 67             self._throttle.start()
 68         event.accept()
 69 
 70     def mouseReleaseEvent(self, event):
 71         if self._pending_fraction is not None:
 72             self._applyPending()
 73         self._origin = None
 74         self._start_fraction = None
 75         self._parent_extent = None
 76         self._drag_child = None
 77         self._pending_fraction = None
 78         self._throttle.stop()
 79         self.manager._pin_all()
 80 
 81     def _applyPending(self):
 82         if self._pending_fraction is None or self._drag_child is None:
 83             return
 84         self._drag_child.setSplitFraction(self._pending_fraction)
 85         self._pending_fraction = None
 86         self.manager._pin(self)
 87 
 88 
 89 class _ResizeFilter(QObject):
 90     def __init__(self, manager):
 91         super().__init__(manager.main)
 92         self.manager = manager
 93 
 94     def eventFilter(self, obj, event):
 95         if event.type() == QEvent.Resize:
 96             self.manager._pin_all()
 97         return False
 98 
 99 
100 class HCSplitHandles:
101     def __init__(self):
102         self.main = hou.qt.mainWindow()
103         self.handles = []
104         self._filter = None
105 
106     def show(self):
107         if self.handles:
108             return
109         for split in self._collectSplits():
110             orientation = self._orientation(split)
111             if orientation is None:
112                 continue
113             handle = _SplitHandle(
114                 self.main,
115                 split,
116                 horizontal_boundary=(orientation == "horizontal"),
117                 manager=self,
118             )
119             handle.show()
120             handle.raise_()
121             self.handles.append(handle)
122         self._filter = _ResizeFilter(self)
123         self.main.installEventFilter(self._filter)
124         self._pin_all()
125 
126     def hide(self):
127         for handle in self.handles:
128             handle.setParent(None)
129             handle.deleteLater()
130         self.handles = []
131         if self._filter is not None:
132             self.main.removeEventFilter(self._filter)
133             self._filter = None
134 
135     def refresh(self):
136         self.hide()
137         self.show()
138 
139     def isShowing(self):
140         return bool(self.handles)
141 
142     def toggle(self):
143         if self.isShowing():
144             self.hide()
145         else:
146             self.show()
147 
148     def _collectSplits(self):
149         splits = {}
150         for leaf in hou.ui.curDesktop().panes():
151             parent = leaf.getSplitParent()
152             while parent is not None:
153                 splits[parent.id()] = parent
154                 parent = parent.getSplitParent()
155         return splits.values()
156 
157     def _orientation(self, split):
158         try:
159             c0 = split.getSplitChild(0)
160             c1 = split.getSplitChild(1)
161         except Exception:
162             return None
163         g0 = c0.qtScreenGeometry()
164         g1 = c1.qtScreenGeometry()
165         if g0.width() <= 0 or g0.height() <= 0 or g1.width() <= 0 or g1.height() <= 0:
166             return None
167         if abs(g0.y() - g1.y()) < 5 and g0.width() > 0:
168             return "vertical"
169         if abs(g0.x() - g1.x()) < 5 and g0.height() > 0:
170             return "horizontal"
171         return None
172 
173     def _pin_all(self):
174         for handle in self.handles:
175             self._pin(handle)
176 
177     def _pin(self, handle):
178         split = handle.hou_pane
179         try:
180             c0 = split.getSplitChild(0)
181             c1 = split.getSplitChild(1)
182         except Exception:
183             handle.hide()
184             return
185         g0 = c0.qtScreenGeometry()
186         g1 = c1.qtScreenGeometry()
187         if g0.width() <= 0 or g1.width() <= 0:
188             handle.hide()
189             return
190         split_geom = split.qtScreenGeometry()
191         if handle.horizontal_boundary:
192             y_screen = (g0.y() + g0.height() + g1.y()) // 2
193             x_screen = split_geom.x() + split_geom.width() // 2
194             center = self.main.mapFromGlobal(QPoint(x_screen, y_screen))
195             handle.setGeometry(
196                 center.x() - HCSPLITHANDLE_LENGTH // 2,
197                 center.y() - HCSPLITHANDLE_THICKNESS // 2,
198                 HCSPLITHANDLE_LENGTH,
199                 HCSPLITHANDLE_THICKNESS,
200             )
201         else:
202             x_screen = (g0.x() + g0.width() + g1.x()) // 2
203             y_screen = split_geom.y() + split_geom.height() // 2
204             center = self.main.mapFromGlobal(QPoint(x_screen, y_screen))
205             handle.setGeometry(
206                 center.x() - HCSPLITHANDLE_THICKNESS // 2,
207                 center.y() - HCSPLITHANDLE_LENGTH // 2,
208                 HCSPLITHANDLE_THICKNESS,
209                 HCSPLITHANDLE_LENGTH,
210             )
211         handle.show()
212         handle.raise_()