vibefpmgui.window.panels

The panels: docks, their layout and the editor in the middle.

Every panel but the editor is a dock (moved, stacked, floated, closed from View → Panels); RedockOnDoubleClick docks a floating one back.

  1"""The panels: docks, their layout and the editor in the middle.
  2
  3Every panel but the editor is a dock (moved, stacked, floated, closed
  4from View → Panels); `RedockOnDoubleClick` docks a floating one back.
  5"""
  6
  7from __future__ import annotations
  8
  9from qtpy.QtCore import QEvent, QObject, QPointF, Qt, QTimer
 10from qtpy.QtGui import QMouseEvent
 11from qtpy.QtWidgets import (
 12    QApplication,
 13    QDockWidget,
 14    QLabel,
 15    QMainWindow,
 16    QStackedWidget,
 17    QTabWidget,
 18    QWidget,
 19)
 20
 21from ..dock_snap import SnapClosed
 22from ..editor import EditorTab
 23from ..terminal import TerminalPanel
 24
 25
 26class RedockOnDoubleClick(QObject):
 27    """Double-clicking a floating panel's title docks it back, once the
 28    mouse button is up.
 29
 30    On macOS a floating panel has the system's title bar, and pressing on it
 31    starts a window move that macOS carries out; the second press of a
 32    double-click starts one too.
 33    Qt docked the panel at the double-click itself, in the middle of that
 34    move: the panel was left over the main window instead of in its place
 35    (and macOS logged "Window move completed without beginning"). Here the
 36    double-click is taken over, and the panel is docked (as setFloating(False)
 37    does, which works) when the move is done."""
 38
 39    POLL_MS = 30
 40    MAX_POLLS = 100  # (3 s: if the button is never seen up, dock anyway)
 41
 42    def __init__(self, parent: QObject):
 43        super().__init__(parent)
 44        self._dock: QDockWidget | None = None
 45        self._polls = 0
 46        self._timer = QTimer(self)
 47        self._timer.setInterval(self.POLL_MS)
 48        self._timer.timeout.connect(self._check)
 49
 50    def eventFilter(self, dock, event) -> bool:
 51        if not (isinstance(dock, QDockWidget) and dock.isFloating()):
 52            return False
 53        # The title: the window's own title bar (macOS: a floating panel has
 54        # the system's), or Qt's drawn above the panel's content.
 55        on_title = event.type() == QEvent.NonClientAreaMouseButtonDblClick or (
 56            event.type() == QEvent.MouseButtonDblClick and dock.widget() is not None
 57            and event.position().y() < dock.widget().geometry().top())
 58        if on_title and event.button() == Qt.LeftButton:
 59            self._dock, self._polls = dock, 0
 60            self._timer.start()
 61            return True  # (not Qt's own docking, mid-move)
 62        return False
 63
 64    def _check(self):
 65        self._polls += 1
 66        if QApplication.mouseButtons() != Qt.NoButton and self._polls < self.MAX_POLLS:
 67            return  # (still held: macOS may still be moving the window)
 68        self._timer.stop()
 69        dock, self._dock = self._dock, None
 70        if dock is None:
 71            return
 72        # The button-up macOS kept to itself (moving the window): Qt's drag of
 73        # the panel, if it started one, ends. (Left unended, the main window
 74        # kept its layout frozen: panels stayed put when the window was
 75        # resized, and a collapsed one left empty space.)
 76        pos = QPointF(dock.width() / 2, -10)
 77        QApplication.sendEvent(dock, QMouseEvent(QEvent.NonClientAreaMouseButtonRelease, pos, dock.mapToGlobal(pos),
 78                                                 Qt.LeftButton, Qt.NoButton, Qt.NoModifier))
 79        if dock.isFloating():
 80            dock.setFloating(False)
 81        window = dock.parentWidget()
 82        front = [dock]
 83        if isinstance(window, QMainWindow):
 84            # Anything else of the drag's goes too: the layout restored as it is. (Each
 85            # other stack's tab in front is put back after: a restore doesn't keep them.)
 86            stack = window.tabifiedDockWidgets(dock)
 87            front += [d for d in window.findChildren(QDockWidget) if d is not dock and d not in stack
 88                      and d.isVisible() and not d.isFloating() and window.rect().intersects(d.geometry())]
 89            window.restoreState(window.saveState())
 90        for panel in front:
 91            panel.raise_()
 92            QTimer.singleShot(0, panel, panel.raise_)
 93
 94
 95class PanelsMixin:
 96    """The panels and their layout (a part of `vibefpmgui.main_window.MainWindow`)."""
 97
 98    def _build_central(self):
 99        """The editor in the middle (a hint while no file is open)."""
100        self.no_file_hint = QLabel("Open a file from <b>Project files</b>, or with "
101                                   "<b>File → Open File in Editor…</b>")
102        self.no_file_hint.setAlignment(Qt.AlignCenter)
103        self.no_file_hint.setWordWrap(True)  # (narrow: the panels beside it can be wider)
104        self.no_file_hint.setContentsMargins(12, 0, 12, 0)
105        self.no_file_hint.setStyleSheet("color: gray;")
106        self.central = QStackedWidget()
107        self.central.addWidget(self.no_file_hint)
108        self.central.addWidget(self.editor_area)  # (the tabs, and their rows when they wrap)
109        self.setCentralWidget(self.central)
110
111    def _build_docks(self, options: QWidget, files_box: QWidget, graph_tab: QWidget, console_box: QWidget):
112        """Every panel but the editor is a dock: moved to the left, right or
113        bottom, stacked as tabs on another, floated as a window of its own, or
114        closed (View → Panels, filled here)."""
115        # (Not GroupedDragging: a stack of panels dragged out together goes in a
116        # window of Qt's own, and once that's closed its panels come back stuck
117        # over the editor, outside the layout. One panel at a time is solid.
118        # Not AnimatedDocks: on macOS the panel layout could start animating
119        # against itself, each finished animation laying out the panels again
120        # and starting the next, at 100% CPU, the window frozen (seen with a
121        # click in the editor). Panels snap into place instead of sliding; the
122        # style has no animation duration either: see themes.StillStyle.)
123        self.setDockOptions(QMainWindow.AllowNestedDocks | QMainWindow.AllowTabbedDocks)
124        self.setTabPosition(Qt.AllDockWidgetAreas, QTabWidget.North)  # (stacked panels: tabs on top)
125        self.setDocumentMode(True)
126        self.setCorner(Qt.BottomLeftCorner, Qt.LeftDockWidgetArea)  # (the left panels: full height)
127        self.setCorner(Qt.BottomRightCorner, Qt.BottomDockWidgetArea)
128        self.docks: dict[str, QDockWidget] = {}
129        self._dock_of: dict[QWidget, QDockWidget] = {}
130        for name, title, widget in [
131            ("options", "Build options", options), ("files", "Project files", files_box),
132            ("graph", "Dependency graph", graph_tab), ("outputs", "Build outputs", self.outputs_view),
133            ("build", "Build", self.build_monitor), ("tests", "Tests", self.test_monitor),
134            ("run", "Run", self.run_monitor), ("debug", "Debug", self.debug_panel),
135            ("coverage", "Coverage", self.coverage_panel),
136            ("complexity", "Complexity", self.complexity_panel),
137            ("problems", "Problems", self.problems_panel),
138            ("review", "Review", self.review_monitor),
139            ("references", "References", self.references_panel), ("search", "Search", self.search_panel),
140            ("git", "Git", self.git_panel),
141            ("terminal", "Terminal", self.terminal_panel), ("output", "Output", console_box),
142        ]:
143            self._make_dock(name, title, widget)
144        self._docks_ready = False  # (until the window is shown: see _dock_shown)
145        self.docks["outputs"].visibilityChanged.connect(lambda shown: shown and self.refresh_build_outputs())
146        self.docks["terminal"].visibilityChanged.connect(self._terminal_dock_shown)
147        self.docks["problems"].visibilityChanged.connect(self._problems_shown)
148        self.docks["git"].visibilityChanged.connect(self._git_panel_shown)
149        self.docks["complexity"].visibilityChanged.connect(self._complexity_panel_shown)
150        self.docks["graph"].visibilityChanged.connect(self._update_editor_actions)  # (Find: the graph's search)
151        self.default_layout()
152        # Dragging a panel's edge past its smallest size collapses it (see dock_snap).
153        self.snap_closed = SnapClosed(self, list(self.docks.values()))
154        self._update_central()
155        self.view_menu.addSeparator()
156        panels = self.panels_menu = self.view_menu.addMenu("Panels")  # (kept: PySide would let it go)
157        for dock in self.docks.values():
158            panels.addAction(dock.toggleViewAction())  # (ticked while shown)
159        panels.addSeparator()
160        reset = panels.addAction("Reset Layout")
161        reset.setStatusTip("Put every panel back where it starts: build options and project files on the "
162                           "left, the graph and build outputs on the right, the rest at the bottom")
163        reset.triggered.connect(self.reset_layout)
164
165    PANEL_AREAS = Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea | Qt.BottomDockWidgetArea
166
167    LAYOUT_VERSION = 2  # (saved layouts of another version aren't restored; 2: no grouped windows)
168
169    def _make_dock(self, name: str, title: str, widget: QWidget) -> QDockWidget:
170        dock = QDockWidget(title, self)
171        dock.setObjectName(f"panel_{name}")  # (how a saved layout knows it)
172        dock.setWidget(widget)
173        dock.setAllowedAreas(self.PANEL_AREAS)
174        dock.setFeatures(QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetFloatable
175                         | QDockWidget.DockWidgetClosable)
176        self.docks[name] = dock
177        self._dock_of[widget] = dock
178        if not hasattr(self, "_redock"):
179            self._redock = RedockOnDoubleClick(self)
180        dock.installEventFilter(self._redock)  # (see RedockOnDoubleClick)
181        dock.toggleViewAction().toggled.connect(lambda shown, d=dock: shown and self._panel_shown(d))
182        dock.topLevelChanged.connect(lambda floating, d=dock: floating or self._panel_docked(d))
183        return dock
184
185    def _panel_shown(self, dock: QDockWidget):
186        """View → Panels → a panel: in front of its stack, and on a screen.
187        (Not a panel a layout being restored shows: the one in front of each
188        stack would be the last shown; see raise_panels.)"""
189        self._keep_on_screen(dock)
190        if self._docks_ready and not getattr(self, "_restoring_layout", False):
191            QTimer.singleShot(0, dock, dock.raise_)
192
193    def restore_layout(self, layout, front: list[str]) -> bool:
194        """Restore a saved layout (QMainWindow.restoreState), with ``front``
195        the panels in front of their stacks (it doesn't keep those)."""
196        self._restoring_layout = True
197        try:
198            restored = self.restoreState(layout, self.LAYOUT_VERSION)
199        finally:
200            self._restoring_layout = False
201        if restored:
202            self.raise_panels(front)
203        return restored
204
205    def _panel_docked(self, dock: QDockWidget):
206        """A floating panel docked again (double-clicking its title): in front
207        of its stack. (Otherwise, on macOS, Qt leaves it shown but not given a
208        place, outside the window, until the next start.)"""
209        if self._docks_ready:
210            QTimer.singleShot(0, dock, dock.raise_)
211
212    def _keep_on_screen(self, dock: QDockWidget):
213        """A floating panel shown again where no screen is (e.g. on a monitor
214        that's gone): bring it over the window."""
215        if not dock.isFloating():
216            return
217        frame = dock.frameGeometry()
218        if any(screen.availableGeometry().intersects(frame) for screen in QApplication.screens()):
219            return
220        size = dock.size().boundedTo(self.size())
221        dock.resize(size)
222        dock.move(self.geometry().center() - dock.rect().center())
223
224    def default_layout(self):
225        """Build options above Project files on the left; the dependency graph
226        and Build outputs (stacked) on the right; Build, Tests, Run, Problems,
227        Review, References, Search and Terminal (stacked) at the bottom, Output
228        beside them."""
229        d = self.docks
230        for dock in d.values():
231            self.removeDockWidget(dock)
232        self.addDockWidget(Qt.LeftDockWidgetArea, d["options"])
233        self.splitDockWidget(d["options"], d["files"], Qt.Vertical)
234        self.addDockWidget(Qt.RightDockWidgetArea, d["graph"])
235        self.tabifyDockWidget(d["graph"], d["outputs"])
236        self.addDockWidget(Qt.BottomDockWidgetArea, d["build"])
237        # (Output beside Build first: split next to a stacked panel, it would be stacked too.)
238        self.splitDockWidget(d["build"], d["output"], Qt.Horizontal)
239        for name in ("tests", "run", "debug", "coverage", "complexity", "problems", "review", "references", "search",
240                     "git", "terminal"):
241            self.tabifyDockWidget(d["build"], d[name])
242        for dock in d.values():
243            dock.setFloating(False)  # (after it's placed: a floating one stays floating otherwise)
244            dock.show()
245        d["graph"].raise_()
246        d["build"].raise_()
247        self._default_sizes()
248
249    def _default_sizes(self):
250        """The default panels' sizes, for the window's size (applied again once
251        it's shown: before that it isn't its real size)."""
252        d = self.docks
253        self.resizeDocks([d["options"], d["graph"]], [310, max(420, self.width() // 3)], Qt.Horizontal)
254        # Build options as tall as its fields need; Project files gets the rest.
255        options = d["options"].widget().sizeHint().height() + 30
256        self.resizeDocks([d["options"], d["files"]], [options, max(200, self.height() - options)], Qt.Vertical)
257        self.resizeDocks([d["build"]], [max(220, self.height() // 3)], Qt.Vertical)
258        self.resizeDocks([d["build"], d["output"]], [3, 2], Qt.Horizontal)
259
260    def reset_layout(self):
261        self.snap_closed.expand_all()
262        self.default_layout()
263        self.statusBar().showMessage("Panels back in their places", 3000)
264
265    def front_panels(self) -> list[str]:
266        """The panels in front of their stacks (by name), for next time: a
267        restored layout doesn't keep them."""
268        return sorted(name for name, dock in self.docks.items()
269                      if self.tabifiedDockWidgets(dock) and dock.isVisible() and not dock.visibleRegion().isEmpty())
270
271    def raise_panels(self, names: list[str]):
272        """Put these panels in front of their stacks: after the panels a
273        restored layout shows have put themselves in front (see _panel_shown)."""
274        docks = [self.docks[name] for name in names if name in self.docks]
275        QTimer.singleShot(0, self, lambda: [dock.raise_() for dock in docks if not dock.isHidden()])
276
277    def show_panel(self, widget: QWidget):
278        """Show a panel and bring it to the front of its stack."""
279        dock = self._dock_of.get(widget)
280        if dock is not None:
281            dock.show()
282            self.snap_closed.expand(dock)  # (collapsed by dragging its edge: open again)
283            self._keep_on_screen(dock)
284            dock.raise_()
285
286    def _content(self, dock: QDockWidget) -> QWidget | None:
287        """A panel's content (collapsed by dragging its edge, the panel shows a grip instead)."""
288        snap = getattr(self, "snap_closed", None)
289        return snap.content(dock) if snap is not None else dock.widget()
290
291    def panel_in_front(self, widget: QWidget) -> bool:
292        """Whether a panel is shown (and not behind another in its stack)."""
293        dock = self._dock_of.get(widget)
294        return dock is not None and not dock.isHidden() and widget.isVisible()
295
296    def _update_central(self, *_):
297        """The editor's tabs, or a hint while no file is open."""
298        self.central.setCurrentWidget(self.editor_area if self.tabs.count() else self.no_file_hint)
299
300    def _terminal_dock_shown(self, shown: bool):
301        # Choosing the Terminal tab: type there straight away (after the click
302        # is done). Not while the window is being built and laid out.
303        if shown and self._docks_ready:
304            QTimer.singleShot(0, self.terminal_panel, self.terminal_panel.focus)
305
306    def showEvent(self, event):
307        super().showEvent(event)
308        if not self._docks_ready and not getattr(self, "_layout_restored", False):
309            QTimer.singleShot(0, self, self._default_sizes)  # (now the window has its size)
310        QTimer.singleShot(0, self, lambda: setattr(self, "_docks_ready", True))
311
312    def _build_terminal(self):
313        """The Terminal panel."""
314        self.terminal_panel = TerminalPanel()
315        self.terminal_panel.set_font(self._mono_font(editor=True))
316        self.terminal_panel.set_word_wrap(EditorTab.word_wrap)
317
318    def show_terminal(self):
319        """Show the Terminal panel (starting the shell) and type there."""
320        self.show_panel(self.terminal_panel)
321        self.terminal_panel.start()
322        self.terminal_panel.focus()
class RedockOnDoubleClick(PySide6.QtCore.QObject):
27class RedockOnDoubleClick(QObject):
28    """Double-clicking a floating panel's title docks it back, once the
29    mouse button is up.
30
31    On macOS a floating panel has the system's title bar, and pressing on it
32    starts a window move that macOS carries out; the second press of a
33    double-click starts one too.
34    Qt docked the panel at the double-click itself, in the middle of that
35    move: the panel was left over the main window instead of in its place
36    (and macOS logged "Window move completed without beginning"). Here the
37    double-click is taken over, and the panel is docked (as setFloating(False)
38    does, which works) when the move is done."""
39
40    POLL_MS = 30
41    MAX_POLLS = 100  # (3 s: if the button is never seen up, dock anyway)
42
43    def __init__(self, parent: QObject):
44        super().__init__(parent)
45        self._dock: QDockWidget | None = None
46        self._polls = 0
47        self._timer = QTimer(self)
48        self._timer.setInterval(self.POLL_MS)
49        self._timer.timeout.connect(self._check)
50
51    def eventFilter(self, dock, event) -> bool:
52        if not (isinstance(dock, QDockWidget) and dock.isFloating()):
53            return False
54        # The title: the window's own title bar (macOS: a floating panel has
55        # the system's), or Qt's drawn above the panel's content.
56        on_title = event.type() == QEvent.NonClientAreaMouseButtonDblClick or (
57            event.type() == QEvent.MouseButtonDblClick and dock.widget() is not None
58            and event.position().y() < dock.widget().geometry().top())
59        if on_title and event.button() == Qt.LeftButton:
60            self._dock, self._polls = dock, 0
61            self._timer.start()
62            return True  # (not Qt's own docking, mid-move)
63        return False
64
65    def _check(self):
66        self._polls += 1
67        if QApplication.mouseButtons() != Qt.NoButton and self._polls < self.MAX_POLLS:
68            return  # (still held: macOS may still be moving the window)
69        self._timer.stop()
70        dock, self._dock = self._dock, None
71        if dock is None:
72            return
73        # The button-up macOS kept to itself (moving the window): Qt's drag of
74        # the panel, if it started one, ends. (Left unended, the main window
75        # kept its layout frozen: panels stayed put when the window was
76        # resized, and a collapsed one left empty space.)
77        pos = QPointF(dock.width() / 2, -10)
78        QApplication.sendEvent(dock, QMouseEvent(QEvent.NonClientAreaMouseButtonRelease, pos, dock.mapToGlobal(pos),
79                                                 Qt.LeftButton, Qt.NoButton, Qt.NoModifier))
80        if dock.isFloating():
81            dock.setFloating(False)
82        window = dock.parentWidget()
83        front = [dock]
84        if isinstance(window, QMainWindow):
85            # Anything else of the drag's goes too: the layout restored as it is. (Each
86            # other stack's tab in front is put back after: a restore doesn't keep them.)
87            stack = window.tabifiedDockWidgets(dock)
88            front += [d for d in window.findChildren(QDockWidget) if d is not dock and d not in stack
89                      and d.isVisible() and not d.isFloating() and window.rect().intersects(d.geometry())]
90            window.restoreState(window.saveState())
91        for panel in front:
92            panel.raise_()
93            QTimer.singleShot(0, panel, panel.raise_)

Double-clicking a floating panel's title docks it back, once the mouse button is up.

On macOS a floating panel has the system's title bar, and pressing on it starts a window move that macOS carries out; the second press of a double-click starts one too. Qt docked the panel at the double-click itself, in the middle of that move: the panel was left over the main window instead of in its place (and macOS logged "Window move completed without beginning"). Here the double-click is taken over, and the panel is docked (as setFloating(False) does, which works) when the move is done.

RedockOnDoubleClick(parent: PySide6.QtCore.QObject)
43    def __init__(self, parent: QObject):
44        super().__init__(parent)
45        self._dock: QDockWidget | None = None
46        self._polls = 0
47        self._timer = QTimer(self)
48        self._timer.setInterval(self.POLL_MS)
49        self._timer.timeout.connect(self._check)

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

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

POLL_MS = 30
MAX_POLLS = 100
def eventFilter(self, dock, event) -> bool:
51    def eventFilter(self, dock, event) -> bool:
52        if not (isinstance(dock, QDockWidget) and dock.isFloating()):
53            return False
54        # The title: the window's own title bar (macOS: a floating panel has
55        # the system's), or Qt's drawn above the panel's content.
56        on_title = event.type() == QEvent.NonClientAreaMouseButtonDblClick or (
57            event.type() == QEvent.MouseButtonDblClick and dock.widget() is not None
58            and event.position().y() < dock.widget().geometry().top())
59        if on_title and event.button() == Qt.LeftButton:
60            self._dock, self._polls = dock, 0
61            self._timer.start()
62            return True  # (not Qt's own docking, mid-move)
63        return False

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

staticMetaObject = PySide6.QtCore.QMetaObject("RedockOnDoubleClick" inherits "QObject": )
class PanelsMixin:
 96class PanelsMixin:
 97    """The panels and their layout (a part of `vibefpmgui.main_window.MainWindow`)."""
 98
 99    def _build_central(self):
100        """The editor in the middle (a hint while no file is open)."""
101        self.no_file_hint = QLabel("Open a file from <b>Project files</b>, or with "
102                                   "<b>File → Open File in Editor…</b>")
103        self.no_file_hint.setAlignment(Qt.AlignCenter)
104        self.no_file_hint.setWordWrap(True)  # (narrow: the panels beside it can be wider)
105        self.no_file_hint.setContentsMargins(12, 0, 12, 0)
106        self.no_file_hint.setStyleSheet("color: gray;")
107        self.central = QStackedWidget()
108        self.central.addWidget(self.no_file_hint)
109        self.central.addWidget(self.editor_area)  # (the tabs, and their rows when they wrap)
110        self.setCentralWidget(self.central)
111
112    def _build_docks(self, options: QWidget, files_box: QWidget, graph_tab: QWidget, console_box: QWidget):
113        """Every panel but the editor is a dock: moved to the left, right or
114        bottom, stacked as tabs on another, floated as a window of its own, or
115        closed (View → Panels, filled here)."""
116        # (Not GroupedDragging: a stack of panels dragged out together goes in a
117        # window of Qt's own, and once that's closed its panels come back stuck
118        # over the editor, outside the layout. One panel at a time is solid.
119        # Not AnimatedDocks: on macOS the panel layout could start animating
120        # against itself, each finished animation laying out the panels again
121        # and starting the next, at 100% CPU, the window frozen (seen with a
122        # click in the editor). Panels snap into place instead of sliding; the
123        # style has no animation duration either: see themes.StillStyle.)
124        self.setDockOptions(QMainWindow.AllowNestedDocks | QMainWindow.AllowTabbedDocks)
125        self.setTabPosition(Qt.AllDockWidgetAreas, QTabWidget.North)  # (stacked panels: tabs on top)
126        self.setDocumentMode(True)
127        self.setCorner(Qt.BottomLeftCorner, Qt.LeftDockWidgetArea)  # (the left panels: full height)
128        self.setCorner(Qt.BottomRightCorner, Qt.BottomDockWidgetArea)
129        self.docks: dict[str, QDockWidget] = {}
130        self._dock_of: dict[QWidget, QDockWidget] = {}
131        for name, title, widget in [
132            ("options", "Build options", options), ("files", "Project files", files_box),
133            ("graph", "Dependency graph", graph_tab), ("outputs", "Build outputs", self.outputs_view),
134            ("build", "Build", self.build_monitor), ("tests", "Tests", self.test_monitor),
135            ("run", "Run", self.run_monitor), ("debug", "Debug", self.debug_panel),
136            ("coverage", "Coverage", self.coverage_panel),
137            ("complexity", "Complexity", self.complexity_panel),
138            ("problems", "Problems", self.problems_panel),
139            ("review", "Review", self.review_monitor),
140            ("references", "References", self.references_panel), ("search", "Search", self.search_panel),
141            ("git", "Git", self.git_panel),
142            ("terminal", "Terminal", self.terminal_panel), ("output", "Output", console_box),
143        ]:
144            self._make_dock(name, title, widget)
145        self._docks_ready = False  # (until the window is shown: see _dock_shown)
146        self.docks["outputs"].visibilityChanged.connect(lambda shown: shown and self.refresh_build_outputs())
147        self.docks["terminal"].visibilityChanged.connect(self._terminal_dock_shown)
148        self.docks["problems"].visibilityChanged.connect(self._problems_shown)
149        self.docks["git"].visibilityChanged.connect(self._git_panel_shown)
150        self.docks["complexity"].visibilityChanged.connect(self._complexity_panel_shown)
151        self.docks["graph"].visibilityChanged.connect(self._update_editor_actions)  # (Find: the graph's search)
152        self.default_layout()
153        # Dragging a panel's edge past its smallest size collapses it (see dock_snap).
154        self.snap_closed = SnapClosed(self, list(self.docks.values()))
155        self._update_central()
156        self.view_menu.addSeparator()
157        panels = self.panels_menu = self.view_menu.addMenu("Panels")  # (kept: PySide would let it go)
158        for dock in self.docks.values():
159            panels.addAction(dock.toggleViewAction())  # (ticked while shown)
160        panels.addSeparator()
161        reset = panels.addAction("Reset Layout")
162        reset.setStatusTip("Put every panel back where it starts: build options and project files on the "
163                           "left, the graph and build outputs on the right, the rest at the bottom")
164        reset.triggered.connect(self.reset_layout)
165
166    PANEL_AREAS = Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea | Qt.BottomDockWidgetArea
167
168    LAYOUT_VERSION = 2  # (saved layouts of another version aren't restored; 2: no grouped windows)
169
170    def _make_dock(self, name: str, title: str, widget: QWidget) -> QDockWidget:
171        dock = QDockWidget(title, self)
172        dock.setObjectName(f"panel_{name}")  # (how a saved layout knows it)
173        dock.setWidget(widget)
174        dock.setAllowedAreas(self.PANEL_AREAS)
175        dock.setFeatures(QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetFloatable
176                         | QDockWidget.DockWidgetClosable)
177        self.docks[name] = dock
178        self._dock_of[widget] = dock
179        if not hasattr(self, "_redock"):
180            self._redock = RedockOnDoubleClick(self)
181        dock.installEventFilter(self._redock)  # (see RedockOnDoubleClick)
182        dock.toggleViewAction().toggled.connect(lambda shown, d=dock: shown and self._panel_shown(d))
183        dock.topLevelChanged.connect(lambda floating, d=dock: floating or self._panel_docked(d))
184        return dock
185
186    def _panel_shown(self, dock: QDockWidget):
187        """View → Panels → a panel: in front of its stack, and on a screen.
188        (Not a panel a layout being restored shows: the one in front of each
189        stack would be the last shown; see raise_panels.)"""
190        self._keep_on_screen(dock)
191        if self._docks_ready and not getattr(self, "_restoring_layout", False):
192            QTimer.singleShot(0, dock, dock.raise_)
193
194    def restore_layout(self, layout, front: list[str]) -> bool:
195        """Restore a saved layout (QMainWindow.restoreState), with ``front``
196        the panels in front of their stacks (it doesn't keep those)."""
197        self._restoring_layout = True
198        try:
199            restored = self.restoreState(layout, self.LAYOUT_VERSION)
200        finally:
201            self._restoring_layout = False
202        if restored:
203            self.raise_panels(front)
204        return restored
205
206    def _panel_docked(self, dock: QDockWidget):
207        """A floating panel docked again (double-clicking its title): in front
208        of its stack. (Otherwise, on macOS, Qt leaves it shown but not given a
209        place, outside the window, until the next start.)"""
210        if self._docks_ready:
211            QTimer.singleShot(0, dock, dock.raise_)
212
213    def _keep_on_screen(self, dock: QDockWidget):
214        """A floating panel shown again where no screen is (e.g. on a monitor
215        that's gone): bring it over the window."""
216        if not dock.isFloating():
217            return
218        frame = dock.frameGeometry()
219        if any(screen.availableGeometry().intersects(frame) for screen in QApplication.screens()):
220            return
221        size = dock.size().boundedTo(self.size())
222        dock.resize(size)
223        dock.move(self.geometry().center() - dock.rect().center())
224
225    def default_layout(self):
226        """Build options above Project files on the left; the dependency graph
227        and Build outputs (stacked) on the right; Build, Tests, Run, Problems,
228        Review, References, Search and Terminal (stacked) at the bottom, Output
229        beside them."""
230        d = self.docks
231        for dock in d.values():
232            self.removeDockWidget(dock)
233        self.addDockWidget(Qt.LeftDockWidgetArea, d["options"])
234        self.splitDockWidget(d["options"], d["files"], Qt.Vertical)
235        self.addDockWidget(Qt.RightDockWidgetArea, d["graph"])
236        self.tabifyDockWidget(d["graph"], d["outputs"])
237        self.addDockWidget(Qt.BottomDockWidgetArea, d["build"])
238        # (Output beside Build first: split next to a stacked panel, it would be stacked too.)
239        self.splitDockWidget(d["build"], d["output"], Qt.Horizontal)
240        for name in ("tests", "run", "debug", "coverage", "complexity", "problems", "review", "references", "search",
241                     "git", "terminal"):
242            self.tabifyDockWidget(d["build"], d[name])
243        for dock in d.values():
244            dock.setFloating(False)  # (after it's placed: a floating one stays floating otherwise)
245            dock.show()
246        d["graph"].raise_()
247        d["build"].raise_()
248        self._default_sizes()
249
250    def _default_sizes(self):
251        """The default panels' sizes, for the window's size (applied again once
252        it's shown: before that it isn't its real size)."""
253        d = self.docks
254        self.resizeDocks([d["options"], d["graph"]], [310, max(420, self.width() // 3)], Qt.Horizontal)
255        # Build options as tall as its fields need; Project files gets the rest.
256        options = d["options"].widget().sizeHint().height() + 30
257        self.resizeDocks([d["options"], d["files"]], [options, max(200, self.height() - options)], Qt.Vertical)
258        self.resizeDocks([d["build"]], [max(220, self.height() // 3)], Qt.Vertical)
259        self.resizeDocks([d["build"], d["output"]], [3, 2], Qt.Horizontal)
260
261    def reset_layout(self):
262        self.snap_closed.expand_all()
263        self.default_layout()
264        self.statusBar().showMessage("Panels back in their places", 3000)
265
266    def front_panels(self) -> list[str]:
267        """The panels in front of their stacks (by name), for next time: a
268        restored layout doesn't keep them."""
269        return sorted(name for name, dock in self.docks.items()
270                      if self.tabifiedDockWidgets(dock) and dock.isVisible() and not dock.visibleRegion().isEmpty())
271
272    def raise_panels(self, names: list[str]):
273        """Put these panels in front of their stacks: after the panels a
274        restored layout shows have put themselves in front (see _panel_shown)."""
275        docks = [self.docks[name] for name in names if name in self.docks]
276        QTimer.singleShot(0, self, lambda: [dock.raise_() for dock in docks if not dock.isHidden()])
277
278    def show_panel(self, widget: QWidget):
279        """Show a panel and bring it to the front of its stack."""
280        dock = self._dock_of.get(widget)
281        if dock is not None:
282            dock.show()
283            self.snap_closed.expand(dock)  # (collapsed by dragging its edge: open again)
284            self._keep_on_screen(dock)
285            dock.raise_()
286
287    def _content(self, dock: QDockWidget) -> QWidget | None:
288        """A panel's content (collapsed by dragging its edge, the panel shows a grip instead)."""
289        snap = getattr(self, "snap_closed", None)
290        return snap.content(dock) if snap is not None else dock.widget()
291
292    def panel_in_front(self, widget: QWidget) -> bool:
293        """Whether a panel is shown (and not behind another in its stack)."""
294        dock = self._dock_of.get(widget)
295        return dock is not None and not dock.isHidden() and widget.isVisible()
296
297    def _update_central(self, *_):
298        """The editor's tabs, or a hint while no file is open."""
299        self.central.setCurrentWidget(self.editor_area if self.tabs.count() else self.no_file_hint)
300
301    def _terminal_dock_shown(self, shown: bool):
302        # Choosing the Terminal tab: type there straight away (after the click
303        # is done). Not while the window is being built and laid out.
304        if shown and self._docks_ready:
305            QTimer.singleShot(0, self.terminal_panel, self.terminal_panel.focus)
306
307    def showEvent(self, event):
308        super().showEvent(event)
309        if not self._docks_ready and not getattr(self, "_layout_restored", False):
310            QTimer.singleShot(0, self, self._default_sizes)  # (now the window has its size)
311        QTimer.singleShot(0, self, lambda: setattr(self, "_docks_ready", True))
312
313    def _build_terminal(self):
314        """The Terminal panel."""
315        self.terminal_panel = TerminalPanel()
316        self.terminal_panel.set_font(self._mono_font(editor=True))
317        self.terminal_panel.set_word_wrap(EditorTab.word_wrap)
318
319    def show_terminal(self):
320        """Show the Terminal panel (starting the shell) and type there."""
321        self.show_panel(self.terminal_panel)
322        self.terminal_panel.start()
323        self.terminal_panel.focus()

The panels and their layout (a part of vibefpmgui.main_window.MainWindow).

PANEL_AREAS = <DockWidgetArea.LeftDockWidgetArea|RightDockWidgetArea|BottomDockWidgetArea: 11>
LAYOUT_VERSION = 2
def restore_layout(self, layout, front: list[str]) -> bool:
194    def restore_layout(self, layout, front: list[str]) -> bool:
195        """Restore a saved layout (QMainWindow.restoreState), with ``front``
196        the panels in front of their stacks (it doesn't keep those)."""
197        self._restoring_layout = True
198        try:
199            restored = self.restoreState(layout, self.LAYOUT_VERSION)
200        finally:
201            self._restoring_layout = False
202        if restored:
203            self.raise_panels(front)
204        return restored

Restore a saved layout (QMainWindow.restoreState), with front the panels in front of their stacks (it doesn't keep those).

def default_layout(self):
225    def default_layout(self):
226        """Build options above Project files on the left; the dependency graph
227        and Build outputs (stacked) on the right; Build, Tests, Run, Problems,
228        Review, References, Search and Terminal (stacked) at the bottom, Output
229        beside them."""
230        d = self.docks
231        for dock in d.values():
232            self.removeDockWidget(dock)
233        self.addDockWidget(Qt.LeftDockWidgetArea, d["options"])
234        self.splitDockWidget(d["options"], d["files"], Qt.Vertical)
235        self.addDockWidget(Qt.RightDockWidgetArea, d["graph"])
236        self.tabifyDockWidget(d["graph"], d["outputs"])
237        self.addDockWidget(Qt.BottomDockWidgetArea, d["build"])
238        # (Output beside Build first: split next to a stacked panel, it would be stacked too.)
239        self.splitDockWidget(d["build"], d["output"], Qt.Horizontal)
240        for name in ("tests", "run", "debug", "coverage", "complexity", "problems", "review", "references", "search",
241                     "git", "terminal"):
242            self.tabifyDockWidget(d["build"], d[name])
243        for dock in d.values():
244            dock.setFloating(False)  # (after it's placed: a floating one stays floating otherwise)
245            dock.show()
246        d["graph"].raise_()
247        d["build"].raise_()
248        self._default_sizes()

Build options above Project files on the left; the dependency graph and Build outputs (stacked) on the right; Build, Tests, Run, Problems, Review, References, Search and Terminal (stacked) at the bottom, Output beside them.

def reset_layout(self):
261    def reset_layout(self):
262        self.snap_closed.expand_all()
263        self.default_layout()
264        self.statusBar().showMessage("Panels back in their places", 3000)
def front_panels(self) -> list[str]:
266    def front_panels(self) -> list[str]:
267        """The panels in front of their stacks (by name), for next time: a
268        restored layout doesn't keep them."""
269        return sorted(name for name, dock in self.docks.items()
270                      if self.tabifiedDockWidgets(dock) and dock.isVisible() and not dock.visibleRegion().isEmpty())

The panels in front of their stacks (by name), for next time: a restored layout doesn't keep them.

def raise_panels(self, names: list[str]):
272    def raise_panels(self, names: list[str]):
273        """Put these panels in front of their stacks: after the panels a
274        restored layout shows have put themselves in front (see _panel_shown)."""
275        docks = [self.docks[name] for name in names if name in self.docks]
276        QTimer.singleShot(0, self, lambda: [dock.raise_() for dock in docks if not dock.isHidden()])

Put these panels in front of their stacks: after the panels a restored layout shows have put themselves in front (see _panel_shown).

def show_panel(self, widget: PySide6.QtWidgets.QWidget):
278    def show_panel(self, widget: QWidget):
279        """Show a panel and bring it to the front of its stack."""
280        dock = self._dock_of.get(widget)
281        if dock is not None:
282            dock.show()
283            self.snap_closed.expand(dock)  # (collapsed by dragging its edge: open again)
284            self._keep_on_screen(dock)
285            dock.raise_()

Show a panel and bring it to the front of its stack.

def panel_in_front(self, widget: PySide6.QtWidgets.QWidget) -> bool:
292    def panel_in_front(self, widget: QWidget) -> bool:
293        """Whether a panel is shown (and not behind another in its stack)."""
294        dock = self._dock_of.get(widget)
295        return dock is not None and not dock.isHidden() and widget.isVisible()

Whether a panel is shown (and not behind another in its stack).

def showEvent(self, event):
307    def showEvent(self, event):
308        super().showEvent(event)
309        if not self._docks_ready and not getattr(self, "_layout_restored", False):
310            QTimer.singleShot(0, self, self._default_sizes)  # (now the window has its size)
311        QTimer.singleShot(0, self, lambda: setattr(self, "_docks_ready", True))
def show_terminal(self):
319    def show_terminal(self):
320        """Show the Terminal panel (starting the shell) and type there."""
321        self.show_panel(self.terminal_panel)
322        self.terminal_panel.start()
323        self.terminal_panel.focus()

Show the Terminal panel (starting the shell) and type there.