"""Capture et restauration de l'etat de l'espace de travail (onglets, splits, tailles) : le pont entre ConnectionController (etat d'ecran reel) et WorkspaceStore (persistance).""" from __future__ import annotations from PySide6.QtCore import QObject, Signal from PySide6.QtWidgets import QWidget from app.app_settings import get_setting, set_setting from app.controllers.connection_controller import ConnectionController from app.local_terminal import DEFAULT_SHELL from app.session_manager import SessionStore from app.ui.split_pane import LAYOUT_SINGLE, SplitContainer from app.ui.tabs import LocalTerminalTabWidget, SessionTabWidget, SFTPTabWidget from app.workspace import ( ITEM_LOCAL_SHELL, ITEM_SESSION_SFTP, ITEM_SESSION_SSH, Workspace, WorkspaceItem, WorkspaceStore, WorkspaceTab, ) SETTING_RESTORE_ON_STARTUP = "restore_last_workspace" SETTING_LAST_WORKSPACE_ID = "last_workspace_id" class WorkspaceManager(QObject): workspaces_changed = Signal() def __init__( self, store: WorkspaceStore, session_store: SessionStore, connection_controller: ConnectionController, parent_widget: QWidget | None = None, ): super().__init__(parent_widget) self.store = store self.session_store = session_store self.cc = connection_controller self._parent_widget = parent_widget # ---------- capture (Enregistrer) ---------- def _item_for_widget(self, widget) -> WorkspaceItem | None: """None = contenu non restaurable (ex: onglet SFTP dans un panneau, type inconnu) : l'appelant doit alors sauter cet onglet/panneau.""" if isinstance(widget, SessionTabWidget): return WorkspaceItem(kind=ITEM_SESSION_SSH, session_id=widget.session.id) if isinstance(widget, SFTPTabWidget): return WorkspaceItem(kind=ITEM_SESSION_SFTP, session_id=widget.session.id) if isinstance(widget, LocalTerminalTabWidget): return WorkspaceItem(kind=ITEM_LOCAL_SHELL, shell=widget.shell) return None def capture(self, name: str = "") -> Workspace: """Construit un Workspace a partir de l'etat actuel des onglets. Un onglet dont un panneau n'est pas restaurable (aucun cas actuellement, prevu pour un futur type de panneau) est simplement omis plutot que de faire echouer toute la capture.""" tabs = self.cc.tabs workspace_tabs: list[WorkspaceTab] = [] for i in range(tabs.count()): widget = tabs.widget(i) title = tabs.tabText(i) if isinstance(widget, SplitContainer): items = [self._item_for_widget(p.content) for p in widget.panes] if not items or any(it is None for it in items): continue workspace_tabs.append(WorkspaceTab( title=title, layout=widget.layout_kind, items=items, splitter_sizes=widget.get_layout_sizes(), )) else: item = self._item_for_widget(widget) if item is None: continue workspace_tabs.append(WorkspaceTab(title=title, layout=LAYOUT_SINGLE, items=[item])) return Workspace(name=name, tabs=workspace_tabs) def save_as_new(self, name: str) -> Workspace: workspace = self.capture(name) self.store.add(workspace) self.workspaces_changed.emit() return workspace def save_over(self, workspace_id: str) -> Workspace | None: """Ecrase un workspace existant avec l'etat actuel (garde son nom).""" existing = self.store.get(workspace_id) if existing is None: return None workspace = self.capture(existing.name) workspace.id = existing.id self.store.update(workspace) self.workspaces_changed.emit() return workspace def rename(self, workspace_id: str, new_name: str) -> None: workspace = self.store.get(workspace_id) if workspace is None: return workspace.name = new_name self.store.update(workspace) self.workspaces_changed.emit() def delete(self, workspace_id: str) -> None: self.store.delete(workspace_id) if get_setting(SETTING_LAST_WORKSPACE_ID) == workspace_id: set_setting(SETTING_LAST_WORKSPACE_ID, "") self.workspaces_changed.emit() # ---------- restauration (Ouvrir) ---------- def _widget_for_item(self, item: WorkspaceItem): """None = reference introuvable (session supprimee depuis) : l'appelant doit sauter cet element plutot qu'echouer toute la restauration.""" if item.kind == ITEM_LOCAL_SHELL: return self.cc.new_local_terminal(item.shell or DEFAULT_SHELL) session = self.session_store.get(item.session_id) if session is None: return None if item.kind == ITEM_SESSION_SSH: widget = self.cc.build_session_tab(session) elif item.kind == ITEM_SESSION_SFTP: widget = self.cc.build_sftp_tab(session) else: return None if widget is not None: self.cc.broadcast.register(widget) return widget def restore(self, workspace: Workspace) -> int: """Ouvre chaque onglet du workspace. Renvoie le nombre d'onglets/ panneaux ignores (reference introuvable), pour que l'appelant puisse en informer l'utilisateur si besoin.""" skipped = 0 for wtab in workspace.tabs: skipped += self._restore_tab(wtab) set_setting(SETTING_LAST_WORKSPACE_ID, workspace.id) return skipped def _restore_tab(self, wtab: WorkspaceTab) -> int: if not wtab.items: return 0 if wtab.layout == LAYOUT_SINGLE or len(wtab.items) == 1: widget = self._widget_for_item(wtab.items[0]) if widget is None: return 1 index = self.cc.tabs.addTab(widget, wtab.title or self.cc.default_title_for(widget)) self.cc.tabs.setCurrentIndex(index) return 0 first = self._widget_for_item(wtab.items[0]) if first is None: return len(wtab.items) index = self.cc.tabs.addTab(first, wtab.title or "⊞ Split") self.cc.split_tab(index, wtab.layout) container = self.cc.tabs.widget(index) skipped = 0 # strict=True est volontairement evite : un workspace legerement perime # (enregistre par une version anterieure, ou modifie a la main) peut avoir # une liste d'items plus courte que le nombre de panneaux - on restaure # alors ce qui correspond et on laisse les panneaux excedentaires a leur # terminal local par defaut, plutot que d'abandonner toute la restauration. for pane, item in zip(container.panes[1:], wtab.items[1:]): # noqa: B905 widget = self._widget_for_item(item) if widget is None: skipped += 1 continue container.replace_pane_content(pane, widget) if wtab.splitter_sizes: container.set_layout_sizes(wtab.splitter_sizes) return skipped # ---------- restauration automatique au demarrage ---------- def try_restore_last_on_startup(self) -> bool: if not get_setting(SETTING_RESTORE_ON_STARTUP, False): return False workspace_id = get_setting(SETTING_LAST_WORKSPACE_ID, "") workspace = self.store.get(workspace_id) if workspace_id else None if workspace is None or not workspace.tabs: return False self.restore(workspace) return True