feat: ajouter le split terminal (plusieurs panneaux par onglet)
Layouts : 1 (normal), 2 verticaux (cote a cote), 2 horizontaux (empiles), 4 panneaux (grille 2x2), via des QSplitter imbriques (app/ui/split_pane.py). Chaque panneau reutilise tel quel un widget d'onglet existant (SessionTabWidget, LocalTerminalTabWidget) : aucune modification de ces classes, seule une fine barre (menu ☰ + fermeture) les enveloppe. - Menu contextuel d'un onglet (SSH ou terminal local, pas SFTP, pas d'imbrication de splits) : "Diviser" -> choix du layout. Le contenu actuel occupe le premier panneau, les autres recoivent un terminal local par defaut. - Menu ☰ d'un panneau : changer son contenu (autre terminal local), ou "Deplacer un onglet ici" pour y faire migrer un onglet de premier niveau existant sans fermer sa connexion. - Fermer un panneau le retire de son QSplitter (qui redistribue l'espace tout seul) ; s'il n'en reste qu'un, l'onglet redevient un onglet normal (redescente automatique, pas de chrome de split residuel). - Fermer l'onglet divise (bouton x) ferme toutes les connexions des panneaux. - BroadcastManager (Multi-Send) atteint desormais aussi les panneaux a l'interieur d'un split (MainWindow._iter_tabs les deplie). Bug intercepte par les tests avant integration : reparenter le widget d'origine dans son panneau retire automatiquement son onglet du QTabWidget (un widget ne peut avoir qu'un seul parent), ce qui decale les onglets suivants ; un appel explicite a removeTab() ensuite visait donc le mauvais onglet. Corrige en laissant Qt gerer ce retrait implicitement plutot que de le dupliquer. tests/test_split_terminal.py : SSHConnection.start()/LocalTerminalConnection.start() neutralisees (teste l'agencement des panneaux et le cycle de vie des onglets, pas une connexion reelle). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ from app.openssh.config import OpenSSHHost, session_from_openssh_host
|
||||
from app.bastion import BastionTarget, resolve_bastion_target, BastionResolutionError
|
||||
from app.controllers.broadcast_manager import BroadcastManager
|
||||
from app.ui.tabs import BaseTerminalTab, SessionTabWidget, SFTPTabWidget, LocalTerminalTabWidget
|
||||
from app.ui.split_pane import SplitContainer, PaneSlot, LAYOUT_LABELS
|
||||
|
||||
|
||||
class ConnectionController(QObject):
|
||||
@@ -108,13 +109,19 @@ class ConnectionController(QObject):
|
||||
self.open_session(session_from_openssh_host(host))
|
||||
|
||||
def open_local_terminal(self, shell: str = DEFAULT_SHELL) -> None:
|
||||
tab = LocalTerminalTabWidget(shell)
|
||||
tab.sftp_requested.connect(self.quick_sftp)
|
||||
self.broadcast.register(tab)
|
||||
tab = self._new_local_terminal(shell)
|
||||
label = SHELL_LABELS.get(shell, shell)
|
||||
index = self.tabs.addTab(tab, f"💻 {label}")
|
||||
self.tabs.setCurrentIndex(index)
|
||||
|
||||
def _new_local_terminal(self, shell: str = DEFAULT_SHELL) -> LocalTerminalTabWidget:
|
||||
"""Cree un terminal local cable (multi-send, SFTP detecte), sans l'ajouter
|
||||
au QTabWidget : reutilise pour un onglet normal comme pour un panneau."""
|
||||
tab = LocalTerminalTabWidget(shell)
|
||||
tab.sftp_requested.connect(self.quick_sftp)
|
||||
self.broadcast.register(tab)
|
||||
return tab
|
||||
|
||||
def quick_sftp(self, target: dict | None = None) -> None:
|
||||
"""Ouvre un SFTP en saisissant directement les infos de connexion, sans favori.
|
||||
|
||||
@@ -183,9 +190,20 @@ class ConnectionController(QObject):
|
||||
index = bar.tabAt(pos)
|
||||
if index < 0:
|
||||
return
|
||||
widget = self.tabs.widget(index)
|
||||
menu = QMenu(self._parent_widget)
|
||||
act_rename = menu.addAction("Renommer... (double-clic)")
|
||||
menu.addSeparator()
|
||||
|
||||
# Diviser : seulement pour un terminal simple (SSH ou local), pas pour
|
||||
# un SFTP ni pour un split existant (pas d'imbrication de splits).
|
||||
split_actions = {}
|
||||
if isinstance(widget, (SessionTabWidget, LocalTerminalTabWidget)):
|
||||
split_menu = menu.addMenu("Diviser")
|
||||
for kind, label in LAYOUT_LABELS.items():
|
||||
split_actions[split_menu.addAction(label)] = kind
|
||||
menu.addSeparator()
|
||||
|
||||
act_close = menu.addAction("Fermer cet onglet")
|
||||
act_close_others = menu.addAction("Fermer les autres onglets")
|
||||
act_close_others.setEnabled(self.tabs.count() > 1)
|
||||
@@ -201,10 +219,14 @@ class ConnectionController(QObject):
|
||||
for other in range(self.tabs.count() - 1, -1, -1):
|
||||
if self.tabs.widget(other) is not cible:
|
||||
self._on_tab_close(other)
|
||||
elif action in split_actions:
|
||||
self.split_tab(index, split_actions[action])
|
||||
|
||||
def _on_tab_close(self, index: int) -> None:
|
||||
widget = self.tabs.widget(index)
|
||||
if isinstance(widget, (BaseTerminalTab, SFTPTabWidget)):
|
||||
if isinstance(widget, SplitContainer):
|
||||
widget.close_all()
|
||||
elif isinstance(widget, (BaseTerminalTab, SFTPTabWidget)):
|
||||
widget.close_connection()
|
||||
self.tabs.removeTab(index)
|
||||
|
||||
@@ -213,5 +235,92 @@ class ConnectionController(QObject):
|
||||
connexions (SSH, locales, SFTP) avant que les widgets ne soient detruits."""
|
||||
for i in range(self.tabs.count()):
|
||||
widget = self.tabs.widget(i)
|
||||
if isinstance(widget, (BaseTerminalTab, SFTPTabWidget)):
|
||||
if isinstance(widget, SplitContainer):
|
||||
widget.close_all()
|
||||
elif isinstance(widget, (BaseTerminalTab, SFTPTabWidget)):
|
||||
widget.close_connection()
|
||||
|
||||
# ---------- split terminal ----------
|
||||
def split_tab(self, index: int, layout_kind: str) -> None:
|
||||
"""Transforme l'onglet en une grille de terminaux : le contenu actuel
|
||||
occupe le premier panneau, les autres recoivent un terminal local par
|
||||
defaut (remplacable ensuite via le menu ☰ de chaque panneau)."""
|
||||
widget = self.tabs.widget(index)
|
||||
if not isinstance(widget, (SessionTabWidget, LocalTerminalTabWidget)):
|
||||
return
|
||||
title = self.tabs.tabText(index)
|
||||
# SplitContainer reparente `widget` dans un de ses panneaux ; Qt retire
|
||||
# alors tout seul l'onglet correspondant (un widget ne peut avoir qu'un
|
||||
# parent), en decalant les onglets suivants - inutile donc d'appeler
|
||||
# removeTab(index) explicitement, ce qui viserait desormais un autre onglet.
|
||||
container = SplitContainer(widget, layout_kind, self._new_local_terminal)
|
||||
container.all_panes_closed.connect(
|
||||
lambda remaining, c=container: self._on_split_collapsed(c, remaining)
|
||||
)
|
||||
container.pane_menu_requested.connect(self._show_pane_menu)
|
||||
self.tabs.insertTab(index, container, f"⊞ {title}")
|
||||
self.tabs.setCurrentIndex(index)
|
||||
|
||||
def _on_split_collapsed(self, container: SplitContainer, remaining: QWidget | None) -> None:
|
||||
"""Il ne reste plus qu'un panneau : on redeplie vers un onglet normal."""
|
||||
index = self.tabs.indexOf(container)
|
||||
if index < 0:
|
||||
return
|
||||
self.tabs.removeTab(index)
|
||||
if remaining is None:
|
||||
return
|
||||
title = self._default_title_for(remaining)
|
||||
self.tabs.insertTab(index, remaining, title)
|
||||
self.tabs.setCurrentIndex(index)
|
||||
|
||||
@staticmethod
|
||||
def _default_title_for(widget: QWidget) -> str:
|
||||
if isinstance(widget, SessionTabWidget):
|
||||
return f"🖥 {widget.session.name}"
|
||||
if isinstance(widget, SFTPTabWidget):
|
||||
return f"📁 {widget.session.name}"
|
||||
if isinstance(widget, LocalTerminalTabWidget):
|
||||
return f"💻 {SHELL_LABELS.get(widget.shell, widget.shell)}"
|
||||
return "Onglet"
|
||||
|
||||
def _movable_tabs(self) -> list[tuple[int, str, QWidget]]:
|
||||
"""Onglets de premier niveau deplacables vers un panneau (ni SFTP, ni
|
||||
un split - on ne gere pas les deplacements panneau-a-panneau)."""
|
||||
result = []
|
||||
for i in range(self.tabs.count()):
|
||||
widget = self.tabs.widget(i)
|
||||
if isinstance(widget, (SessionTabWidget, LocalTerminalTabWidget)):
|
||||
result.append((i, self.tabs.tabText(i), widget))
|
||||
return result
|
||||
|
||||
def _show_pane_menu(self, pane: PaneSlot, container: SplitContainer) -> None:
|
||||
menu = QMenu(self._parent_widget)
|
||||
term_menu = menu.addMenu("Terminal local")
|
||||
shell_actions = {}
|
||||
for shell_id, label in SHELL_LABELS.items():
|
||||
shell_actions[term_menu.addAction(label)] = shell_id
|
||||
|
||||
movable = self._movable_tabs()
|
||||
move_actions = {}
|
||||
if movable:
|
||||
menu.addSeparator()
|
||||
move_menu = menu.addMenu("Deplacer un onglet ici")
|
||||
for index, title, widget in movable:
|
||||
move_actions[move_menu.addAction(title)] = (index, widget)
|
||||
else:
|
||||
hint = menu.addAction("(aucun autre onglet a deplacer ici)")
|
||||
hint.setEnabled(False)
|
||||
|
||||
action = menu.exec(pane.menu_btn.mapToGlobal(pane.menu_btn.rect().bottomLeft()))
|
||||
if action in shell_actions:
|
||||
container.replace_pane_content(pane, self._new_local_terminal(shell_actions[action]))
|
||||
elif action in move_actions:
|
||||
index, moved_widget = move_actions[action]
|
||||
self.move_tab_into_pane(index, container, pane)
|
||||
|
||||
def move_tab_into_pane(self, tab_index: int, container: SplitContainer, pane: PaneSlot) -> None:
|
||||
"""Deplace l'onglet de premier niveau `tab_index` dans `pane` : sa
|
||||
connexion continue de vivre, seul son emplacement visuel change."""
|
||||
widget = self.tabs.widget(tab_index)
|
||||
self.tabs.removeTab(tab_index) # ne ferme pas la connexion : elle continue dans le panneau
|
||||
container.replace_pane_content(pane, widget)
|
||||
|
||||
@@ -22,6 +22,7 @@ from app.theme import THEME_LABELS, apply_theme, current_theme, save_theme
|
||||
from app.controllers.broadcast_manager import BroadcastManager
|
||||
from app.controllers.session_controller import SessionController
|
||||
from app.controllers.connection_controller import ConnectionController
|
||||
from app.ui.split_pane import SplitContainer
|
||||
|
||||
# re-exporte pour compatibilite (utilise par tests/test_detect.py et du code existant)
|
||||
from app.services.ssh_target import detect_ssh_target # noqa: F401
|
||||
@@ -59,8 +60,17 @@ class MainWindow(QMainWindow):
|
||||
return self.connection_controller.tabs
|
||||
|
||||
def _iter_tabs(self):
|
||||
"""Widgets de terminal joignables par le Multi-Send : les onglets de
|
||||
premier niveau, et les panneaux a l'interieur des onglets divises."""
|
||||
tabs = self.connection_controller.tabs
|
||||
return [tabs.widget(i) for i in range(tabs.count())]
|
||||
result = []
|
||||
for i in range(tabs.count()):
|
||||
widget = tabs.widget(i)
|
||||
if isinstance(widget, SplitContainer):
|
||||
result.extend(pane.content for pane in widget.panes)
|
||||
else:
|
||||
result.append(widget)
|
||||
return result
|
||||
|
||||
# ---------- cablage entre controleurs ----------
|
||||
def _wire_controllers(self) -> None:
|
||||
|
||||
150
app/ui/split_pane.py
Normal file
150
app/ui/split_pane.py
Normal file
@@ -0,0 +1,150 @@
|
||||
"""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()
|
||||
124
tests/test_split_terminal.py
Normal file
124
tests/test_split_terminal.py
Normal file
@@ -0,0 +1,124 @@
|
||||
"""Split terminal : diviser un onglet en 2/4 panneaux, deplacer un onglet dans
|
||||
un panneau, fermer un panneau (redescente automatique a un onglet simple),
|
||||
fermeture globale d'un onglet divise.
|
||||
|
||||
SSHConnection.start() et LocalTerminalConnection.start() sont neutralisees :
|
||||
ce module teste l'agencement des panneaux et le cycle de vie des onglets, pas
|
||||
une vraie connexion (deja couverte par test_keepalive.py / test_bastion.py)."""
|
||||
import sys, pathlib
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||
|
||||
import itertools
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from PySide6.QtWidgets import QApplication, QWidget
|
||||
|
||||
from app.session_manager import SessionStore
|
||||
from app.key_manager import KeyManager
|
||||
from app.ssh_connection import SSHConnection
|
||||
from app.local_terminal import LocalTerminalConnection
|
||||
from app.controllers.broadcast_manager import BroadcastManager
|
||||
from app.controllers.connection_controller import ConnectionController
|
||||
from app.ui.tabs import LocalTerminalTabWidget
|
||||
from app.ui.split_pane import SplitContainer, LAYOUT_2V, LAYOUT_4
|
||||
|
||||
app = QApplication.instance() or QApplication([])
|
||||
|
||||
tmp = Path(tempfile.mkdtemp())
|
||||
_counter = itertools.count()
|
||||
|
||||
no_ssh_thread = patch.object(SSHConnection, "start", lambda self: None)
|
||||
no_local_thread = patch.object(LocalTerminalConnection, "start", lambda self: None)
|
||||
no_ssh_thread.start()
|
||||
no_local_thread.start()
|
||||
|
||||
|
||||
def make_controller() -> ConnectionController:
|
||||
n = next(_counter)
|
||||
session_store = SessionStore(path=tmp / f"sessions_{n}.json")
|
||||
key_manager = KeyManager(registry_path=tmp / f"keys_{n}.json")
|
||||
host_widget = QWidget()
|
||||
broadcast = BroadcastManager(lambda: [host_widget])
|
||||
return ConnectionController(session_store, key_manager, broadcast, parent_widget=host_widget)
|
||||
|
||||
|
||||
print("=== 1. Diviser un onglet en 2 : le contenu original occupe le 1er panneau ===")
|
||||
cc = make_controller()
|
||||
cc.open_local_terminal()
|
||||
original_widget = cc.tabs.widget(0)
|
||||
cc.split_tab(0, LAYOUT_2V)
|
||||
container = cc.tabs.widget(0)
|
||||
print(" type de l'onglet apres division:", type(container).__name__)
|
||||
assert isinstance(container, SplitContainer)
|
||||
assert len(container.panes) == 2
|
||||
assert container.panes[0].content is original_widget
|
||||
assert cc.tabs.count() == 1, "la division ne doit pas creer de nouvel onglet de premier niveau"
|
||||
print(" OK")
|
||||
|
||||
print()
|
||||
print("=== 2. Diviser en 4 panneaux ===")
|
||||
cc2 = make_controller()
|
||||
cc2.open_local_terminal()
|
||||
cc2.split_tab(0, LAYOUT_4)
|
||||
container2 = cc2.tabs.widget(0)
|
||||
assert len(container2.panes) == 4
|
||||
print(" 4 panneaux crees : OK")
|
||||
|
||||
print()
|
||||
print("=== 3. Deplacer un onglet existant dans un panneau (sans fermer sa connexion) ===")
|
||||
cc3 = make_controller()
|
||||
cc3.open_local_terminal() # onglet 0 -> deviendra le split
|
||||
cc3.open_local_terminal() # onglet 1 -> sera deplace dans un panneau
|
||||
moved_widget = cc3.tabs.widget(1)
|
||||
assert cc3.tabs.count() == 2
|
||||
cc3.split_tab(0, LAYOUT_2V)
|
||||
container3 = cc3.tabs.widget(0)
|
||||
assert cc3.tabs.count() == 2 # l'onglet 1 (independant) existe toujours
|
||||
target_pane = container3.panes[1]
|
||||
filler_before_move = target_pane.content
|
||||
cc3.move_tab_into_pane(1, container3, target_pane)
|
||||
print(" onglets de premier niveau restants:", cc3.tabs.count())
|
||||
assert cc3.tabs.count() == 1, "l'onglet deplace ne doit plus etre un onglet independant"
|
||||
assert target_pane.content is moved_widget
|
||||
assert filler_before_move is not moved_widget
|
||||
print(" OK")
|
||||
|
||||
print()
|
||||
print("=== 4. Fermer un panneau (sur 2) -> redescente automatique a un onglet simple ===")
|
||||
cc4 = make_controller()
|
||||
cc4.open_local_terminal()
|
||||
original4 = cc4.tabs.widget(0)
|
||||
cc4.split_tab(0, LAYOUT_2V)
|
||||
container4 = cc4.tabs.widget(0)
|
||||
other_pane = container4.panes[1]
|
||||
other_pane.close_requested.emit(other_pane)
|
||||
print(" type de l'onglet apres fermeture d'un panneau:", type(cc4.tabs.widget(0)).__name__)
|
||||
assert cc4.tabs.count() == 1
|
||||
assert cc4.tabs.widget(0) is original4, "le panneau restant doit redevenir l'onglet normal"
|
||||
assert cc4.tabs.tabText(0).startswith("💻"), cc4.tabs.tabText(0)
|
||||
print(" OK")
|
||||
|
||||
print()
|
||||
print("=== 5. Fermer un onglet divise (bouton x) ferme toutes les connexions ===")
|
||||
cc5 = make_controller()
|
||||
cc5.open_local_terminal()
|
||||
cc5.split_tab(0, LAYOUT_2V)
|
||||
container5 = cc5.tabs.widget(0)
|
||||
panes_before = list(container5.panes)
|
||||
closed_flags = []
|
||||
for pane in panes_before:
|
||||
original_close = pane.content.close_connection
|
||||
pane.content.close_connection = lambda o=original_close: (o(), closed_flags.append(True))
|
||||
cc5._on_tab_close(0)
|
||||
print(" connexions fermees:", len(closed_flags), "/", len(panes_before))
|
||||
assert len(closed_flags) == len(panes_before)
|
||||
assert cc5.tabs.count() == 0
|
||||
print(" OK")
|
||||
|
||||
no_ssh_thread.stop()
|
||||
no_local_thread.stop()
|
||||
|
||||
print()
|
||||
print("SPLIT TERMINAL : TOUS LES TESTS OK")
|
||||
Reference in New Issue
Block a user