vibefpmgui.dock_snap

Dragging a panel's edge past its smallest size collapses it, as in VS Code.

Qt stops a dock separator at the panels' minimum sizes, so SnapClosed does separator drags itself (they reach the main window, not a child widget). Once the mouse is more than half a panel's minimum size past where it would stop, the panel collapses to a thin Grip; dragging the grip (or the separator beside it) back out opens it, and so does double-clicking it. At the edge of a side of the window the whole side collapses; between two panels, just the squeezed one, with the tabs stacked with it.

A command showing a collapsed panel (a test run showing Tests…), View → Panels and Reset Layout open it too.

  1"""Dragging a panel's edge past its smallest size collapses it, as in VS Code.
  2
  3Qt stops a dock separator at the panels' minimum sizes, so `SnapClosed`
  4does separator drags itself (they reach the main window, not a child
  5widget). Once the mouse is more than half a panel's minimum size past where
  6it would stop, the panel collapses to a thin `Grip`; dragging the grip (or
  7the separator beside it) back out opens it, and so does double-clicking it.
  8At the edge of a side of the window the whole side collapses; between two
  9panels, just the squeezed one, with the tabs stacked with it.
 10
 11A command showing a collapsed panel (a test run showing Tests…), View →
 12Panels and Reset Layout open it too.
 13"""
 14
 15from __future__ import annotations
 16
 17from dataclasses import dataclass
 18
 19from qtpy.QtCore import QEvent, QObject, QPoint, QRect, QSize, Qt, QTimer
 20from qtpy.QtWidgets import QDockWidget, QMainWindow, QStyle, QTabBar, QWidget
 21
 22SLACK = 3  # (pixels around a separator still counted as on it)
 23SEPARATOR_NAME = "qt_qmainwindow_extended_splitter"  # (Qt's widget over a thin separator)
 24GRIP = 7  # a collapsed panel's thickness
 25REOPEN = 12  # (pixels past where it collapses that a collapsed panel opens again: not at every wobble)
 26
 27
 28class _NoTitle(QWidget):
 29    """A collapsed panel's title bar: nothing."""
 30
 31    def sizeHint(self):
 32        return QSize(0, 0)
 33
 34    def minimumSizeHint(self):
 35        return QSize(0, 0)
 36
 37
 38class Grip(QWidget):
 39    """A collapsed panel's content: a thin strip with the resize cursor (the
 40    style draws the dots on the separator beside it). Not its title bar:
 41    with no content, Qt keeps a panel only as big as its title, in a corner."""
 42
 43    def __init__(self, snap: SnapClosed, side: Side):
 44        super().__init__()
 45        self.snap, self.side = snap, side
 46        self.setCursor(Qt.SplitHCursor if side.horizontal else Qt.SplitVCursor)
 47        self.setToolTip("Drag to open (or double-click)")
 48
 49    def sizeHint(self):
 50        return QSize(GRIP, GRIP)
 51
 52    def minimumSizeHint(self):
 53        return QSize(GRIP, GRIP)
 54
 55    def mousePressEvent(self, event):
 56        if event.button() == Qt.LeftButton:
 57            # The window takes the rest of the drag: the grip goes once the panel opens.
 58            window = self.snap.window
 59            self.snap.start_drag([self.side], self.mapTo(window, event.position().toPoint()))
 60            window.grabMouse(self.cursor())
 61        event.accept()  # (not the panel's: it would float it)
 62
 63    def mouseDoubleClickEvent(self, event):
 64        self.snap.expand(self.side.docks[0])
 65
 66
 67@dataclass(eq=False)
 68class Side:
 69    """The panels on one side of a separator: what collapses together."""
 70    docks: list[QDockWidget]       # the ones in front along the separator
 71    group: list[QDockWidget]       # and every one stacked with them
 72    size: int                      # their extent across the separator (at the press, or before collapsing)
 73    minimum: int
 74    sign: int                      # +1: the mouse moving along the axis makes them bigger
 75    horizontal: bool               # (the drag's axis: a vertical separator moves sideways)
 76    press_size: int = 0            # (during a drag: the size at the press)
 77    cross: list[int] | None = None  # the docks' sizes along the separator, before collapsing
 78
 79    def size_at(self, delta: int) -> int:
 80        return self.press_size + self.sign * delta
 81
 82
 83def along(rect, horizontal: bool) -> tuple[int, int, int, int]:
 84    """A rectangle's start and end along a drag, then across it."""
 85    return ((rect.left(), rect.right(), rect.top(), rect.bottom()) if horizontal
 86            else (rect.top(), rect.bottom(), rect.left(), rect.right()))
 87
 88
 89def touching(rect, pos: QPoint, before: bool, horizontal: bool, gap: int) -> bool:
 90    """Whether ``rect`` ends just before ``pos`` (``before``) or starts
 91    just after it, along the drag, and reaches it across."""
 92    start, end, low, high = along(rect, horizontal)
 93    p, q = (pos.x(), pos.y()) if horizontal else (pos.y(), pos.x())
 94    distance = p - end if before else start - p
 95    return 0 <= distance <= gap and low - gap <= q <= high + gap
 96
 97
 98class SnapClosed(QObject):
 99    def __init__(self, window: QMainWindow, docks: list[QDockWidget]):
100        super().__init__(window)
101        self.window = window
102        self.docks = docks
103        self.collapsed: list[Side] = []
104        self.contents: dict[QDockWidget, QWidget] = {}  # a collapsed panel's content (its widget: the grip)
105        self._press: QPoint | None = None
106        self._sides: list[Side] = []
107        for dock in docks:
108            dock.toggleViewAction().toggled.connect(lambda on, d=dock: on and self.expand(d))
109        window.installEventFilter(self)
110
111    # ------------------------------------------------------------ finding
112    def _gap(self) -> int:
113        return self.window.style().pixelMetric(QStyle.PM_DockWidgetSeparatorExtent, None, self.window) + SLACK
114
115    def _front(self) -> list[QDockWidget]:
116        """The docked panels on view (behind a tab, or collapsed, one is
117        "visible" but moved out of the window)."""
118        rect = self.window.rect()
119        return [d for d in self.docks if d.isVisible() and not d.isFloating() and rect.intersects(d.geometry())]
120
121    def _tab_bars(self) -> list[QTabBar]:
122        return [bar for bar in self.window.findChildren(QTabBar) if bar.parent() is self.window and bar.isVisible()]
123
124    def _rect(self, widget: QWidget, tab_bars: list) -> QRect:
125        """Where a panel is, with its stack's tab bar (which sits between it
126        and the separator when the tabs are on that side)."""
127        rect = widget.geometry()
128        if isinstance(widget, QDockWidget):
129            for bar in tab_bars:
130                other = bar.geometry()
131                # Its own stack's: starting where it starts and within it, along
132                # its edge (a bar across: tabs above or below it), or down its
133                # side (a bar down: tabs beside it). (Not another stack's that
134                # only touches it: with the macOS style's thin separators, the
135                # bottom panels' tab bar touched the right side's, which then
136                # reached across the window; and a stack's bar, beside a panel
137                # and level with its top, was taken to be its tabs down its side.)
138                across = other.width() >= other.height()
139                lined_up = (across and abs(other.left() - rect.left()) <= 4 and other.right() <= rect.right() + 4) \
140                    or (not across and abs(other.top() - rect.top()) <= 4 and other.bottom() <= rect.bottom() + 4)
141                if lined_up and other.adjusted(-2, -2, 2, 2).intersects(rect):
142                    rect = rect.united(other)
143        return rect
144
145    def _on_separator(self, pos: QPoint) -> bool:
146        """No widget there but the window, or one of the invisible widgets Qt
147        puts over thin separators to catch the mouse (the macOS style's are
148        1 pixel)."""
149        widget = self.window.childAt(pos)
150        return widget is None or widget.objectName() == SEPARATOR_NAME
151
152    def _sides_at(self, pos: QPoint) -> list[Side]:
153        """The panels on each side of the separator at ``pos`` that could collapse."""
154        gap, tab_bars = self._gap(), self._tab_bars()
155        # A collapsed side: its separator is next to its grips. (Not "next to
156        # what's beyond it": the right side's panels are beyond the left side,
157        # and their edges are at the right side's separator too.)
158        for side in self.collapsed:
159            if any(d.isVisible() and touching(d.geometry(), pos, side.sign > 0, side.horizontal, gap)
160                   for d in side.docks):
161                return [side]
162        front = self._front()
163        rects = {dock: self._rect(dock, tab_bars) for dock in front}
164        central = self.window.centralWidget()
165        area = self.window.dockWidgetArea
166        for horizontal in (True, False):
167            before = [d for d in front if touching(rects[d], pos, True, horizontal, gap)]
168            after = [d for d in front if touching(rects[d], pos, False, horizontal, gap)]
169            if not before and not after:
170                continue
171            for side, check in ((before, True), (after, False)):
172                if central is not None and touching(central.geometry(), pos, check, horizontal, gap):
173                    side.append(central)
174            sides = []
175            for docks, others, sign in ((before, after, 1), (after, before, -1)):
176                if not docks or central in docks:
177                    continue  # (the editor's side can't collapse)
178                areas = {area(d) for d in docks}
179                if len(areas) != 1:
180                    continue
181                (here,) = areas
182                if central in others or any(area(d) != here for d in others) \
183                        or self._outside(pos, [rects[d] for d in front if area(d) == here], horizontal):
184                    # The edge of a side of the window: all of that side
185                    # collapses, moved the way it opens (the left one sideways).
186                    if (here in (Qt.LeftDockWidgetArea, Qt.RightDockWidgetArea)) != horizontal:
187                        continue
188                    docks = [d for d in front if area(d) == here]
189                group = []
190                for dock in docks:
191                    for member in [dock, *self.window.tabifiedDockWidgets(dock)]:
192                        if member not in group:
193                            group.append(member)
194                spans = [along(rects[d], horizontal) for d in docks]
195                size = max(span[1] for span in spans) - min(span[0] for span in spans) + 1
196                # (All its tabs': Qt keeps a stack as big as its biggest tab needs.)
197                minimum = max((d.minimumSizeHint().width() if horizontal else d.minimumSizeHint().height())
198                              for d in group)
199                sides.append(Side(docks, group, size, max(minimum, 1), sign, horizontal))
200            if sides:
201                return sides
202        return []
203
204    @staticmethod
205    def _outside(pos: QPoint, rects: list[QRect], horizontal: bool) -> bool:
206        """Whether ``pos`` is beyond all of a side's panels, along the drag: the
207        side's own edge, whatever's beyond it. (Not found beyond: a row past
208        where the editor ends, which the macOS style's widget over a thin
209        separator reaches; only the panel there was taken, and one of two
210        side by side collapsed, leaving the side as tall, blank.)"""
211        spans = [along(rect, horizontal) for rect in rects]
212        p = pos.x() if horizontal else pos.y()
213        return bool(spans) and (p < min(span[0] for span in spans) or p > max(span[1] for span in spans))
214
215    # ------------------------------------------------------------ collapsing
216    def _collapse(self, side: Side):
217        """The panels in front: no content or title, so no minimum size; the
218        ones behind them in their stacks: hidden (with one panel left, Qt
219        takes the stack's tab bar away, which would keep a strip open).
220        Panels among them already collapsed (on their own: one of two side by
221        side, then both) are opened first: their content put back, to be
222        saved again (saved as it was, a panel's would be its grip, and it would
223        come back blank); they open with the others."""
224        for other in [s for s in self.collapsed if set(s.group) & set(side.group)]:
225            self._open(other)
226        side.cross = [d.height() if side.horizontal else d.width() for d in side.docks]
227        for dock in side.group:
228            if dock in side.docks:
229                content = dock.widget()
230                self.contents[dock] = content
231                dock.setTitleBarWidget(_NoTitle())
232                dock.setWidget(Grip(self, side))
233                if content is not None:
234                    content.hide()  # (still the panel's child, put back by _open)
235            else:
236                dock.hide()
237        self.collapsed.append(side)
238
239    def _open(self, side: Side):
240        self.collapsed.remove(side)  # (first: showing them below mustn't open it again)
241        for dock in side.docks:
242            grip = dock.widget()
243            content = self.contents.pop(dock, None)
244            dock.setTitleBarWidget(None)
245            if content is not None:
246                dock.setWidget(content)
247                content.show()
248            if isinstance(grip, Grip):
249                grip.hide()
250                grip.deleteLater()
251        for dock in side.group:
252            dock.show()  # (back in their places in the stacks)
253        for dock in side.docks:  # (the tabs that were in front; again once Qt has put the others back)
254            dock.raise_()
255            QTimer.singleShot(0, dock, dock.raise_)
256        if side.cross and len(side.docks) > 1:  # (the panels beside each other: as they were)
257            self.window.resizeDocks(side.docks, side.cross, Qt.Vertical if side.horizontal else Qt.Horizontal)
258
259    def _resize(self, side: Side, size: int):
260        orientation = Qt.Horizontal if side.horizontal else Qt.Vertical
261        self.window.resizeDocks(side.docks, [max(size, 1)] * len(side.docks), orientation)
262
263    def content(self, dock: QDockWidget) -> QWidget | None:
264        """A panel's content, collapsed or not."""
265        return self.contents.get(dock) or dock.widget()
266
267    def is_collapsed(self, dock: QDockWidget) -> bool:
268        return any(dock in side.group for side in self.collapsed)
269
270    def expand(self, dock: QDockWidget):
271        """Open a collapsed panel (and the others collapsed with it), at the
272        size it had."""
273        side = next((s for s in self.collapsed if dock in s.group), None)
274        if side is None:
275            return
276        self._open(side)
277        QTimer.singleShot(0, dock, dock.raise_)  # (in front: after _open's raising of the tabs that were)
278        QTimer.singleShot(0, self.window, lambda: self._resize(side, side.size))
279
280    def expand_all(self):
281        """Open every collapsed panel, at its size (Reset Layout; quitting,
282        so the saved layout has them open)."""
283        for side in list(self.collapsed):
284            self._open(side)
285            self._resize(side, side.size)
286
287    # ------------------------------------------------------------ the drag
288    # On a separator next to panels that can collapse, the drag is ours (Qt's
289    # own stops at the minimum sizes, and doesn't follow a panel just opened).
290    def eventFilter(self, obj, event):
291        kind = event.type()
292        if kind == QEvent.MouseButtonPress and event.button() == Qt.LeftButton:
293            pos = event.position().toPoint()
294            self._press = None
295            if self._on_separator(pos):
296                sides = self._sides_at(pos)
297                if sides:
298                    self.start_drag(sides, pos)
299                    return True
300        elif kind == QEvent.MouseMove and self._press is not None:
301            if not event.buttons() & Qt.LeftButton:
302                self.end_drag()
303                return False
304            self._drag_to(event.position().toPoint())
305            return True
306        elif kind == QEvent.MouseButtonRelease and self._press is not None:
307            self.end_drag()
308            return True
309        return False
310
311    def start_drag(self, sides: list[Side], pos: QPoint):
312        """A drag of a separator (or a collapsed panel's grip) from ``pos``
313        (in the window)."""
314        self._sides, self._press = sides, pos
315        for side in sides:
316            side.press_size = GRIP if side in self.collapsed else side.size
317
318    def end_drag(self):
319        self._press, self._sides = None, []
320        if QWidget.mouseGrabber() is self.window:
321            self.window.releaseMouse()
322
323    def _drag_to(self, pos: QPoint):
324        moved = pos - self._press
325        sizes = []
326        for side in self._sides:
327            size = side.size_at(moved.x() if side.horizontal else moved.y())
328            collapsed = side in self.collapsed
329            # (Opening again a little further than it collapsed: the mouse wobbling
330            # where it collapses would swap it back and forth at every move.)
331            if not collapsed and size < side.minimum / 2:
332                side.size = side.press_size  # (to open at, later)
333                self._collapse(side)
334            elif collapsed and size >= side.minimum / 2 + REOPEN:
335                self._open(side)
336            sizes.append((side, GRIP if side in self.collapsed else max(size, side.minimum)))
337        # Both sides of the separator sized (one alone, and Qt shares the change
338        # between both: the separator moved half as far as the mouse), the
339        # collapsed one first: the other takes what's left.
340        sizes.sort(key=lambda pair: pair[0] not in self.collapsed)
341        docks, lengths = [], []
342        for side, size in sizes:
343            docks += side.docks
344            lengths += [max(size, 1)] * len(side.docks)
345        self.window.resizeDocks(docks, lengths, Qt.Horizontal if sizes[0][0].horizontal else Qt.Vertical)
SLACK = 3
SEPARATOR_NAME = 'qt_qmainwindow_extended_splitter'
GRIP = 7
REOPEN = 12
class Grip(PySide6.QtWidgets.QWidget):
39class Grip(QWidget):
40    """A collapsed panel's content: a thin strip with the resize cursor (the
41    style draws the dots on the separator beside it). Not its title bar:
42    with no content, Qt keeps a panel only as big as its title, in a corner."""
43
44    def __init__(self, snap: SnapClosed, side: Side):
45        super().__init__()
46        self.snap, self.side = snap, side
47        self.setCursor(Qt.SplitHCursor if side.horizontal else Qt.SplitVCursor)
48        self.setToolTip("Drag to open (or double-click)")
49
50    def sizeHint(self):
51        return QSize(GRIP, GRIP)
52
53    def minimumSizeHint(self):
54        return QSize(GRIP, GRIP)
55
56    def mousePressEvent(self, event):
57        if event.button() == Qt.LeftButton:
58            # The window takes the rest of the drag: the grip goes once the panel opens.
59            window = self.snap.window
60            self.snap.start_drag([self.side], self.mapTo(window, event.position().toPoint()))
61            window.grabMouse(self.cursor())
62        event.accept()  # (not the panel's: it would float it)
63
64    def mouseDoubleClickEvent(self, event):
65        self.snap.expand(self.side.docks[0])

A collapsed panel's content: a thin strip with the resize cursor (the style draws the dots on the separator beside it). Not its title bar: with no content, Qt keeps a panel only as big as its title, in a corner.

Grip( snap: SnapClosed, side: Side)
44    def __init__(self, snap: SnapClosed, side: Side):
45        super().__init__()
46        self.snap, self.side = snap, side
47        self.setCursor(Qt.SplitHCursor if side.horizontal else Qt.SplitVCursor)
48        self.setToolTip("Drag to open (or double-click)")

__init__(self, /, parent: PySide6.QtWidgets.QWidget | None = None, f: PySide6.QtCore.Qt.WindowType = Default(Qt.WindowFlags), *, modal: bool | None = None, windowModality: PySide6.QtCore.Qt.WindowModality | None = None, enabled: bool | None = None, geometry: PySide6.QtCore.QRect | None = None, frameGeometry: PySide6.QtCore.QRect | None = None, normalGeometry: PySide6.QtCore.QRect | None = None, x: int | None = None, y: int | None = None, pos: PySide6.QtCore.QPoint | None = None, frameSize: PySide6.QtCore.QSize | None = None, size: PySide6.QtCore.QSize | None = None, width: int | None = None, height: int | None = None, rect: PySide6.QtCore.QRect | None = None, childrenRect: PySide6.QtCore.QRect | None = None, childrenRegion: PySide6.QtGui.QRegion | None = None, sizePolicy: PySide6.QtWidgets.QSizePolicy | None = None, minimumSize: PySide6.QtCore.QSize | None = None, maximumSize: PySide6.QtCore.QSize | None = None, minimumWidth: int | None = None, minimumHeight: int | None = None, maximumWidth: int | None = None, maximumHeight: int | None = None, sizeIncrement: PySide6.QtCore.QSize | None = None, baseSize: PySide6.QtCore.QSize | None = None, palette: PySide6.QtGui.QPalette | None = None, font: PySide6.QtGui.QFont | None = None, cursor: PySide6.QtGui.QCursor | None = None, mouseTracking: bool | None = None, tabletTracking: bool | None = None, isActiveWindow: bool | None = None, focusPolicy: PySide6.QtCore.Qt.FocusPolicy | None = None, focus: bool | None = None, contextMenuPolicy: PySide6.QtCore.Qt.ContextMenuPolicy | None = None, updatesEnabled: bool | None = None, visible: bool | None = None, minimized: bool | None = None, maximized: bool | None = None, fullScreen: bool | None = None, sizeHint: PySide6.QtCore.QSize | None = None, minimumSizeHint: PySide6.QtCore.QSize | None = None, acceptDrops: bool | None = None, windowTitle: str | None = None, windowIcon: PySide6.QtGui.QIcon | None = None, windowIconText: str | None = None, windowOpacity: float | None = None, windowModified: bool | None = None, toolTip: str | None = None, toolTipDuration: int | None = None, statusTip: str | None = None, whatsThis: str | None = None, accessibleName: str | None = None, accessibleDescription: str | None = None, accessibleIdentifier: str | None = None, layoutDirection: PySide6.QtCore.Qt.LayoutDirection | None = None, autoFillBackground: bool | None = None, styleSheet: str | None = None, locale: PySide6.QtCore.QLocale | None = None, windowFilePath: str | None = None, inputMethodHints: PySide6.QtCore.Qt.InputMethodHint | None = None) -> None

Initialize self. See help(type(self)) for accurate signature.

def sizeHint(self):
50    def sizeHint(self):
51        return QSize(GRIP, GRIP)

sizeHint(self, /) -> PySide6.QtCore.QSize

def minimumSizeHint(self):
53    def minimumSizeHint(self):
54        return QSize(GRIP, GRIP)

minimumSizeHint(self, /) -> PySide6.QtCore.QSize

def mousePressEvent(self, event):
56    def mousePressEvent(self, event):
57        if event.button() == Qt.LeftButton:
58            # The window takes the rest of the drag: the grip goes once the panel opens.
59            window = self.snap.window
60            self.snap.start_drag([self.side], self.mapTo(window, event.position().toPoint()))
61            window.grabMouse(self.cursor())
62        event.accept()  # (not the panel's: it would float it)

mousePressEvent(self, event: PySide6.QtGui.QMouseEvent, /) -> None

def mouseDoubleClickEvent(self, event):
64    def mouseDoubleClickEvent(self, event):
65        self.snap.expand(self.side.docks[0])

mouseDoubleClickEvent(self, event: PySide6.QtGui.QMouseEvent, /) -> None

staticMetaObject = PySide6.QtCore.QMetaObject("Grip" inherits "QWidget": )
@dataclass(eq=False)
class Side:
68@dataclass(eq=False)
69class Side:
70    """The panels on one side of a separator: what collapses together."""
71    docks: list[QDockWidget]       # the ones in front along the separator
72    group: list[QDockWidget]       # and every one stacked with them
73    size: int                      # their extent across the separator (at the press, or before collapsing)
74    minimum: int
75    sign: int                      # +1: the mouse moving along the axis makes them bigger
76    horizontal: bool               # (the drag's axis: a vertical separator moves sideways)
77    press_size: int = 0            # (during a drag: the size at the press)
78    cross: list[int] | None = None  # the docks' sizes along the separator, before collapsing
79
80    def size_at(self, delta: int) -> int:
81        return self.press_size + self.sign * delta

The panels on one side of a separator: what collapses together.

Side( docks: list[PySide6.QtWidgets.QDockWidget], group: list[PySide6.QtWidgets.QDockWidget], size: int, minimum: int, sign: int, horizontal: bool, press_size: int = 0, cross: list[int] | None = None)
docks: list[PySide6.QtWidgets.QDockWidget]
group: list[PySide6.QtWidgets.QDockWidget]
size: int
minimum: int
sign: int
horizontal: bool
press_size: int = 0
cross: list[int] | None = None
def size_at(self, delta: int) -> int:
80    def size_at(self, delta: int) -> int:
81        return self.press_size + self.sign * delta
def along(rect, horizontal: bool) -> tuple[int, int, int, int]:
84def along(rect, horizontal: bool) -> tuple[int, int, int, int]:
85    """A rectangle's start and end along a drag, then across it."""
86    return ((rect.left(), rect.right(), rect.top(), rect.bottom()) if horizontal
87            else (rect.top(), rect.bottom(), rect.left(), rect.right()))

A rectangle's start and end along a drag, then across it.

def touching( rect, pos: PySide6.QtCore.QPoint, before: bool, horizontal: bool, gap: int) -> bool:
90def touching(rect, pos: QPoint, before: bool, horizontal: bool, gap: int) -> bool:
91    """Whether ``rect`` ends just before ``pos`` (``before``) or starts
92    just after it, along the drag, and reaches it across."""
93    start, end, low, high = along(rect, horizontal)
94    p, q = (pos.x(), pos.y()) if horizontal else (pos.y(), pos.x())
95    distance = p - end if before else start - p
96    return 0 <= distance <= gap and low - gap <= q <= high + gap

Whether rect ends just before pos (before) or starts just after it, along the drag, and reaches it across.

class SnapClosed(PySide6.QtCore.QObject):
 99class SnapClosed(QObject):
100    def __init__(self, window: QMainWindow, docks: list[QDockWidget]):
101        super().__init__(window)
102        self.window = window
103        self.docks = docks
104        self.collapsed: list[Side] = []
105        self.contents: dict[QDockWidget, QWidget] = {}  # a collapsed panel's content (its widget: the grip)
106        self._press: QPoint | None = None
107        self._sides: list[Side] = []
108        for dock in docks:
109            dock.toggleViewAction().toggled.connect(lambda on, d=dock: on and self.expand(d))
110        window.installEventFilter(self)
111
112    # ------------------------------------------------------------ finding
113    def _gap(self) -> int:
114        return self.window.style().pixelMetric(QStyle.PM_DockWidgetSeparatorExtent, None, self.window) + SLACK
115
116    def _front(self) -> list[QDockWidget]:
117        """The docked panels on view (behind a tab, or collapsed, one is
118        "visible" but moved out of the window)."""
119        rect = self.window.rect()
120        return [d for d in self.docks if d.isVisible() and not d.isFloating() and rect.intersects(d.geometry())]
121
122    def _tab_bars(self) -> list[QTabBar]:
123        return [bar for bar in self.window.findChildren(QTabBar) if bar.parent() is self.window and bar.isVisible()]
124
125    def _rect(self, widget: QWidget, tab_bars: list) -> QRect:
126        """Where a panel is, with its stack's tab bar (which sits between it
127        and the separator when the tabs are on that side)."""
128        rect = widget.geometry()
129        if isinstance(widget, QDockWidget):
130            for bar in tab_bars:
131                other = bar.geometry()
132                # Its own stack's: starting where it starts and within it, along
133                # its edge (a bar across: tabs above or below it), or down its
134                # side (a bar down: tabs beside it). (Not another stack's that
135                # only touches it: with the macOS style's thin separators, the
136                # bottom panels' tab bar touched the right side's, which then
137                # reached across the window; and a stack's bar, beside a panel
138                # and level with its top, was taken to be its tabs down its side.)
139                across = other.width() >= other.height()
140                lined_up = (across and abs(other.left() - rect.left()) <= 4 and other.right() <= rect.right() + 4) \
141                    or (not across and abs(other.top() - rect.top()) <= 4 and other.bottom() <= rect.bottom() + 4)
142                if lined_up and other.adjusted(-2, -2, 2, 2).intersects(rect):
143                    rect = rect.united(other)
144        return rect
145
146    def _on_separator(self, pos: QPoint) -> bool:
147        """No widget there but the window, or one of the invisible widgets Qt
148        puts over thin separators to catch the mouse (the macOS style's are
149        1 pixel)."""
150        widget = self.window.childAt(pos)
151        return widget is None or widget.objectName() == SEPARATOR_NAME
152
153    def _sides_at(self, pos: QPoint) -> list[Side]:
154        """The panels on each side of the separator at ``pos`` that could collapse."""
155        gap, tab_bars = self._gap(), self._tab_bars()
156        # A collapsed side: its separator is next to its grips. (Not "next to
157        # what's beyond it": the right side's panels are beyond the left side,
158        # and their edges are at the right side's separator too.)
159        for side in self.collapsed:
160            if any(d.isVisible() and touching(d.geometry(), pos, side.sign > 0, side.horizontal, gap)
161                   for d in side.docks):
162                return [side]
163        front = self._front()
164        rects = {dock: self._rect(dock, tab_bars) for dock in front}
165        central = self.window.centralWidget()
166        area = self.window.dockWidgetArea
167        for horizontal in (True, False):
168            before = [d for d in front if touching(rects[d], pos, True, horizontal, gap)]
169            after = [d for d in front if touching(rects[d], pos, False, horizontal, gap)]
170            if not before and not after:
171                continue
172            for side, check in ((before, True), (after, False)):
173                if central is not None and touching(central.geometry(), pos, check, horizontal, gap):
174                    side.append(central)
175            sides = []
176            for docks, others, sign in ((before, after, 1), (after, before, -1)):
177                if not docks or central in docks:
178                    continue  # (the editor's side can't collapse)
179                areas = {area(d) for d in docks}
180                if len(areas) != 1:
181                    continue
182                (here,) = areas
183                if central in others or any(area(d) != here for d in others) \
184                        or self._outside(pos, [rects[d] for d in front if area(d) == here], horizontal):
185                    # The edge of a side of the window: all of that side
186                    # collapses, moved the way it opens (the left one sideways).
187                    if (here in (Qt.LeftDockWidgetArea, Qt.RightDockWidgetArea)) != horizontal:
188                        continue
189                    docks = [d for d in front if area(d) == here]
190                group = []
191                for dock in docks:
192                    for member in [dock, *self.window.tabifiedDockWidgets(dock)]:
193                        if member not in group:
194                            group.append(member)
195                spans = [along(rects[d], horizontal) for d in docks]
196                size = max(span[1] for span in spans) - min(span[0] for span in spans) + 1
197                # (All its tabs': Qt keeps a stack as big as its biggest tab needs.)
198                minimum = max((d.minimumSizeHint().width() if horizontal else d.minimumSizeHint().height())
199                              for d in group)
200                sides.append(Side(docks, group, size, max(minimum, 1), sign, horizontal))
201            if sides:
202                return sides
203        return []
204
205    @staticmethod
206    def _outside(pos: QPoint, rects: list[QRect], horizontal: bool) -> bool:
207        """Whether ``pos`` is beyond all of a side's panels, along the drag: the
208        side's own edge, whatever's beyond it. (Not found beyond: a row past
209        where the editor ends, which the macOS style's widget over a thin
210        separator reaches; only the panel there was taken, and one of two
211        side by side collapsed, leaving the side as tall, blank.)"""
212        spans = [along(rect, horizontal) for rect in rects]
213        p = pos.x() if horizontal else pos.y()
214        return bool(spans) and (p < min(span[0] for span in spans) or p > max(span[1] for span in spans))
215
216    # ------------------------------------------------------------ collapsing
217    def _collapse(self, side: Side):
218        """The panels in front: no content or title, so no minimum size; the
219        ones behind them in their stacks: hidden (with one panel left, Qt
220        takes the stack's tab bar away, which would keep a strip open).
221        Panels among them already collapsed (on their own: one of two side by
222        side, then both) are opened first: their content put back, to be
223        saved again (saved as it was, a panel's would be its grip, and it would
224        come back blank); they open with the others."""
225        for other in [s for s in self.collapsed if set(s.group) & set(side.group)]:
226            self._open(other)
227        side.cross = [d.height() if side.horizontal else d.width() for d in side.docks]
228        for dock in side.group:
229            if dock in side.docks:
230                content = dock.widget()
231                self.contents[dock] = content
232                dock.setTitleBarWidget(_NoTitle())
233                dock.setWidget(Grip(self, side))
234                if content is not None:
235                    content.hide()  # (still the panel's child, put back by _open)
236            else:
237                dock.hide()
238        self.collapsed.append(side)
239
240    def _open(self, side: Side):
241        self.collapsed.remove(side)  # (first: showing them below mustn't open it again)
242        for dock in side.docks:
243            grip = dock.widget()
244            content = self.contents.pop(dock, None)
245            dock.setTitleBarWidget(None)
246            if content is not None:
247                dock.setWidget(content)
248                content.show()
249            if isinstance(grip, Grip):
250                grip.hide()
251                grip.deleteLater()
252        for dock in side.group:
253            dock.show()  # (back in their places in the stacks)
254        for dock in side.docks:  # (the tabs that were in front; again once Qt has put the others back)
255            dock.raise_()
256            QTimer.singleShot(0, dock, dock.raise_)
257        if side.cross and len(side.docks) > 1:  # (the panels beside each other: as they were)
258            self.window.resizeDocks(side.docks, side.cross, Qt.Vertical if side.horizontal else Qt.Horizontal)
259
260    def _resize(self, side: Side, size: int):
261        orientation = Qt.Horizontal if side.horizontal else Qt.Vertical
262        self.window.resizeDocks(side.docks, [max(size, 1)] * len(side.docks), orientation)
263
264    def content(self, dock: QDockWidget) -> QWidget | None:
265        """A panel's content, collapsed or not."""
266        return self.contents.get(dock) or dock.widget()
267
268    def is_collapsed(self, dock: QDockWidget) -> bool:
269        return any(dock in side.group for side in self.collapsed)
270
271    def expand(self, dock: QDockWidget):
272        """Open a collapsed panel (and the others collapsed with it), at the
273        size it had."""
274        side = next((s for s in self.collapsed if dock in s.group), None)
275        if side is None:
276            return
277        self._open(side)
278        QTimer.singleShot(0, dock, dock.raise_)  # (in front: after _open's raising of the tabs that were)
279        QTimer.singleShot(0, self.window, lambda: self._resize(side, side.size))
280
281    def expand_all(self):
282        """Open every collapsed panel, at its size (Reset Layout; quitting,
283        so the saved layout has them open)."""
284        for side in list(self.collapsed):
285            self._open(side)
286            self._resize(side, side.size)
287
288    # ------------------------------------------------------------ the drag
289    # On a separator next to panels that can collapse, the drag is ours (Qt's
290    # own stops at the minimum sizes, and doesn't follow a panel just opened).
291    def eventFilter(self, obj, event):
292        kind = event.type()
293        if kind == QEvent.MouseButtonPress and event.button() == Qt.LeftButton:
294            pos = event.position().toPoint()
295            self._press = None
296            if self._on_separator(pos):
297                sides = self._sides_at(pos)
298                if sides:
299                    self.start_drag(sides, pos)
300                    return True
301        elif kind == QEvent.MouseMove and self._press is not None:
302            if not event.buttons() & Qt.LeftButton:
303                self.end_drag()
304                return False
305            self._drag_to(event.position().toPoint())
306            return True
307        elif kind == QEvent.MouseButtonRelease and self._press is not None:
308            self.end_drag()
309            return True
310        return False
311
312    def start_drag(self, sides: list[Side], pos: QPoint):
313        """A drag of a separator (or a collapsed panel's grip) from ``pos``
314        (in the window)."""
315        self._sides, self._press = sides, pos
316        for side in sides:
317            side.press_size = GRIP if side in self.collapsed else side.size
318
319    def end_drag(self):
320        self._press, self._sides = None, []
321        if QWidget.mouseGrabber() is self.window:
322            self.window.releaseMouse()
323
324    def _drag_to(self, pos: QPoint):
325        moved = pos - self._press
326        sizes = []
327        for side in self._sides:
328            size = side.size_at(moved.x() if side.horizontal else moved.y())
329            collapsed = side in self.collapsed
330            # (Opening again a little further than it collapsed: the mouse wobbling
331            # where it collapses would swap it back and forth at every move.)
332            if not collapsed and size < side.minimum / 2:
333                side.size = side.press_size  # (to open at, later)
334                self._collapse(side)
335            elif collapsed and size >= side.minimum / 2 + REOPEN:
336                self._open(side)
337            sizes.append((side, GRIP if side in self.collapsed else max(size, side.minimum)))
338        # Both sides of the separator sized (one alone, and Qt shares the change
339        # between both: the separator moved half as far as the mouse), the
340        # collapsed one first: the other takes what's left.
341        sizes.sort(key=lambda pair: pair[0] not in self.collapsed)
342        docks, lengths = [], []
343        for side, size in sizes:
344            docks += side.docks
345            lengths += [max(size, 1)] * len(side.docks)
346        self.window.resizeDocks(docks, lengths, Qt.Horizontal if sizes[0][0].horizontal else Qt.Vertical)

QObject(self, /, parent: PySide6.QtCore.QObject | None = None, *, objectName: str | None = None) -> None

SnapClosed( window: PySide6.QtWidgets.QMainWindow, docks: list[PySide6.QtWidgets.QDockWidget])
100    def __init__(self, window: QMainWindow, docks: list[QDockWidget]):
101        super().__init__(window)
102        self.window = window
103        self.docks = docks
104        self.collapsed: list[Side] = []
105        self.contents: dict[QDockWidget, QWidget] = {}  # a collapsed panel's content (its widget: the grip)
106        self._press: QPoint | None = None
107        self._sides: list[Side] = []
108        for dock in docks:
109            dock.toggleViewAction().toggled.connect(lambda on, d=dock: on and self.expand(d))
110        window.installEventFilter(self)

__init__(self, /, parent: PySide6.QtCore.QObject | None = None, *, objectName: str | None = None) -> None

Initialize self. See help(type(self)) for accurate signature.

window
docks
collapsed: list[Side]
contents: dict[PySide6.QtWidgets.QDockWidget, PySide6.QtWidgets.QWidget]
def content( self, dock: PySide6.QtWidgets.QDockWidget) -> PySide6.QtWidgets.QWidget | None:
264    def content(self, dock: QDockWidget) -> QWidget | None:
265        """A panel's content, collapsed or not."""
266        return self.contents.get(dock) or dock.widget()

A panel's content, collapsed or not.

def is_collapsed(self, dock: PySide6.QtWidgets.QDockWidget) -> bool:
268    def is_collapsed(self, dock: QDockWidget) -> bool:
269        return any(dock in side.group for side in self.collapsed)
def expand(self, dock: PySide6.QtWidgets.QDockWidget):
271    def expand(self, dock: QDockWidget):
272        """Open a collapsed panel (and the others collapsed with it), at the
273        size it had."""
274        side = next((s for s in self.collapsed if dock in s.group), None)
275        if side is None:
276            return
277        self._open(side)
278        QTimer.singleShot(0, dock, dock.raise_)  # (in front: after _open's raising of the tabs that were)
279        QTimer.singleShot(0, self.window, lambda: self._resize(side, side.size))

Open a collapsed panel (and the others collapsed with it), at the size it had.

def expand_all(self):
281    def expand_all(self):
282        """Open every collapsed panel, at its size (Reset Layout; quitting,
283        so the saved layout has them open)."""
284        for side in list(self.collapsed):
285            self._open(side)
286            self._resize(side, side.size)

Open every collapsed panel, at its size (Reset Layout; quitting, so the saved layout has them open).

def eventFilter(self, obj, event):
291    def eventFilter(self, obj, event):
292        kind = event.type()
293        if kind == QEvent.MouseButtonPress and event.button() == Qt.LeftButton:
294            pos = event.position().toPoint()
295            self._press = None
296            if self._on_separator(pos):
297                sides = self._sides_at(pos)
298                if sides:
299                    self.start_drag(sides, pos)
300                    return True
301        elif kind == QEvent.MouseMove and self._press is not None:
302            if not event.buttons() & Qt.LeftButton:
303                self.end_drag()
304                return False
305            self._drag_to(event.position().toPoint())
306            return True
307        elif kind == QEvent.MouseButtonRelease and self._press is not None:
308            self.end_drag()
309            return True
310        return False

eventFilter(self, watched: PySide6.QtCore.QObject, event: PySide6.QtCore.QEvent, /) -> bool

def start_drag( self, sides: list[Side], pos: PySide6.QtCore.QPoint):
312    def start_drag(self, sides: list[Side], pos: QPoint):
313        """A drag of a separator (or a collapsed panel's grip) from ``pos``
314        (in the window)."""
315        self._sides, self._press = sides, pos
316        for side in sides:
317            side.press_size = GRIP if side in self.collapsed else side.size

A drag of a separator (or a collapsed panel's grip) from pos (in the window).

def end_drag(self):
319    def end_drag(self):
320        self._press, self._sides = None, []
321        if QWidget.mouseGrabber() is self.window:
322            self.window.releaseMouse()
staticMetaObject = PySide6.QtCore.QMetaObject("SnapClosed" inherits "QObject": )