"""Affichage de plusieurs terminaux dans un meme onglet (split), via des QSplitter imbriques. Un SplitContainer prend la place d'un onglet normal dans le QTabWidget de ConnectionController ; chaque panneau (PaneSlot) heberge un widget d'onglet existant (SessionTabWidget, LocalTerminalTabWidget, ...) sans le modifier. """ from __future__ import annotations from typing import Callable from PySide6.QtCore import Qt, Signal from PySide6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QSplitter, QToolButton, QFrame, ) LAYOUT_SINGLE = "1" LAYOUT_2V = "2v" # 2 panneaux cote a cote (ligne de separation verticale) LAYOUT_2H = "2h" # 2 panneaux empiles (ligne de separation horizontale) LAYOUT_4 = "4" # grille 2x2 LAYOUT_LABELS = { LAYOUT_2V: "2 panneaux verticaux (cote a cote)", LAYOUT_2H: "2 panneaux horizontaux (empiles)", LAYOUT_4: "4 panneaux", } _PANE_COUNTS = {LAYOUT_SINGLE: 1, LAYOUT_2V: 2, LAYOUT_2H: 2, LAYOUT_4: 4} class PaneSlot(QWidget): """Un emplacement dans la grille : une fine barre (menu + fermeture) au-dessus du contenu, qui reste un widget d'onglet inchange.""" close_requested = Signal(object) # emet elle-meme menu_requested = Signal(object) # emet elle-meme ; le conteneur construit le menu def __init__(self, content: QWidget, parent=None): super().__init__(parent) layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) bar = QHBoxLayout() bar.setContentsMargins(4, 2, 4, 2) self.menu_btn = QToolButton() self.menu_btn.setText("☰") self.menu_btn.setToolTip("Changer le contenu de ce panneau") self.menu_btn.clicked.connect(lambda: self.menu_requested.emit(self)) bar.addWidget(self.menu_btn) bar.addStretch() close_btn = QToolButton() close_btn.setText("✕") close_btn.setToolTip("Fermer ce panneau") close_btn.clicked.connect(lambda: self.close_requested.emit(self)) bar.addWidget(close_btn) bar_frame = QFrame() bar_frame.setObjectName("tabBar") bar_frame.setLayout(bar) layout.addWidget(bar_frame) self.content = content layout.addWidget(content, 1) def close_connection(self) -> None: if hasattr(self.content, "close_connection"): self.content.close_connection() def set_content(self, new_content: QWidget) -> None: self.layout().replaceWidget(self.content, new_content) self.content.setParent(None) self.content = new_content new_content.show() class SplitContainer(QWidget): """Grille de terminaux (1/2/4 panneaux). Redescend automatiquement a un seul panneau quand il n'en reste plus qu'un (ferme les autres).""" all_panes_closed = Signal(object) # (widget restant) -> le proprietaire deplie l'onglet pane_menu_requested = Signal(object, object) # (pane, container) -> le proprietaire affiche un menu def __init__(self, first_widget: QWidget, layout_kind: str, make_filler: Callable[[], QWidget], parent=None): super().__init__(parent) self.panes: list[PaneSlot] = [] self._root_layout = QVBoxLayout(self) self._root_layout.setContentsMargins(0, 0, 0, 0) self._root_layout.setSpacing(0) contents = [first_widget] + [make_filler() for _ in range(_PANE_COUNTS[layout_kind] - 1)] for content in contents: self._add_pane(content) self._root_layout.addWidget(self._arrange(layout_kind, self.panes)) def _make_pane(self, content: QWidget) -> PaneSlot: pane = PaneSlot(content) pane.close_requested.connect(self._on_pane_close_requested) pane.menu_requested.connect(lambda p: self.pane_menu_requested.emit(p, self)) return pane def _add_pane(self, content: QWidget) -> PaneSlot: pane = self._make_pane(content) self.panes.append(pane) return pane @staticmethod def _arrange(layout_kind: str, panes: list[PaneSlot]) -> QWidget: if layout_kind == LAYOUT_SINGLE: return panes[0] if layout_kind == LAYOUT_2V: splitter = QSplitter(Qt.Horizontal) elif layout_kind == LAYOUT_2H: splitter = QSplitter(Qt.Vertical) elif layout_kind == LAYOUT_4: top = QSplitter(Qt.Horizontal) top.addWidget(panes[0]) top.addWidget(panes[1]) bottom = QSplitter(Qt.Horizontal) bottom.addWidget(panes[2]) bottom.addWidget(panes[3]) outer = QSplitter(Qt.Vertical) outer.addWidget(top) outer.addWidget(bottom) return outer else: raise ValueError(f"layout de split inconnu : {layout_kind}") for pane in panes: splitter.addWidget(pane) return splitter def replace_pane_content(self, pane: PaneSlot, new_content: QWidget) -> None: """Ferme l'ancien contenu du panneau et le remplace (ex : terminal local par defaut remplace par un onglet deplace ici).""" pane.close_connection() pane.set_content(new_content) def _on_pane_close_requested(self, pane: PaneSlot) -> None: pane.close_connection() self.panes.remove(pane) # Le retirer de son QSplitter suffit : celui-ci redistribue l'espace # aux panneaux restants tout seul, sans reconstruire toute la grille. pane.setParent(None) pane.deleteLater() if len(self.panes) <= 1: remaining = self.panes[0].content if self.panes else None self.all_panes_closed.emit(remaining) def close_all(self) -> None: for pane in self.panes: pane.close_connection()