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()
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.
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.
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
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).
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).
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.
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.
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).
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.
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).