# -*- coding: utf-8 -*- """ WorkspaceManager: Manages layout and lifecycle of dockable panels. Uses tk.PanedWindow for resizable splits. """ import tkinter as tk from tkinter import ttk import json import os from typing import Dict, Optional from .dock_frame import DockFrame class WorkspaceManager: """ Manages the layout and lifecycle of dock frames. Features: - Dynamic layout with resizable panes (tk.PanedWindow) - Dock registration and retrieval - Save/load workspace layouts to JSON - Multiple dock positions (left, right, bottom, etc.) """ def __init__(self, root: tk.Tk, layouts_dir: str = "layouts"): """ Args: root: Root Tk window layouts_dir: Directory to store layout configurations """ self.root = root self.layouts_dir = layouts_dir self.docks: Dict[str, DockFrame] = {} # Create layouts directory if needed if not os.path.exists(self.layouts_dir): os.makedirs(self.layouts_dir) self._create_layout() def _create_layout(self): """Create the main paned window layout structure.""" # Main container self.main_container = ttk.Frame(self.root) self.main_container.pack(fill=tk.BOTH, expand=True) # Main vertical split (top | bottom logger) self.main_pane = tk.PanedWindow( self.main_container, orient=tk.VERTICAL, sashrelief=tk.RAISED, sashwidth=4, bg='#cccccc' ) self.main_pane.pack(fill=tk.BOTH, expand=True) # Top horizontal split (MCS | right panels) self.top_pane = tk.PanedWindow( self.main_pane, orient=tk.HORIZONTAL, sashrelief=tk.RAISED, sashwidth=4, bg='#cccccc' ) self.main_pane.add(self.top_pane, minsize=400) # Left panel container (MCS) self.left_container = ttk.Frame(self.top_pane) self.top_pane.add(self.left_container, minsize=400, width=650) # Right side vertical split (nav | irst) self.right_pane = tk.PanedWindow( self.top_pane, orient=tk.VERTICAL, sashrelief=tk.RAISED, sashwidth=4, bg='#cccccc' ) self.top_pane.add(self.right_pane, minsize=300) # Right top container (Navigation) self.right_top_container = ttk.Frame(self.right_pane) self.right_pane.add(self.right_top_container, minsize=150) # Right bottom container (IRST) self.right_bottom_container = ttk.Frame(self.right_pane) self.right_pane.add(self.right_bottom_container, minsize=150) # Bottom logger container (full width) self.bottom_container = ttk.Frame(self.main_pane) self.main_pane.add(self.bottom_container, minsize=150, height=200) def add_dock(self, dock_id: str, dock: DockFrame, position: str = 'left') -> DockFrame: """ Register and place a dock in the workspace. Args: dock_id: Unique identifier for the dock dock: DockFrame instance position: Where to place the dock ('left', 'right_top', 'right_bottom', 'bottom') Returns: The dock instance """ self.docks[dock_id] = dock if position == 'left': dock.pack(in_=self.left_container, side=tk.TOP, fill=tk.BOTH, expand=True) elif position == 'right_top': dock.pack(in_=self.right_top_container, side=tk.TOP, fill=tk.BOTH, expand=True) elif position == 'right_bottom': dock.pack(in_=self.right_bottom_container, side=tk.TOP, fill=tk.BOTH, expand=True) elif position == 'bottom': dock.pack(in_=self.bottom_container, side=tk.TOP, fill=tk.BOTH, expand=True) else: raise ValueError(f"Unknown position: {position}") # Call populate_content if not done yet dock.populate_content() return dock def get_dock(self, dock_id: str) -> Optional[DockFrame]: """Retrieve a dock by its ID.""" return self.docks.get(dock_id) def remove_dock(self, dock_id: str): """Remove a dock from the workspace.""" dock = self.docks.get(dock_id) if dock: dock.close() del self.docks[dock_id] def toggle_dock_visibility(self, dock_id: str): """Toggle the visibility of a dock.""" dock = self.docks.get(dock_id) if dock: if dock.is_visible: dock.close() else: dock.show() def save_layout(self, layout_name: str = "default"): """ Save the current workspace layout to a JSON file. Args: layout_name: Name of the layout configuration """ layout_file = os.path.join(self.layouts_dir, f"{layout_name}.json") # Get current window geometry window_geometry = self.root.geometry() # Get current sash positions top_sash_pos = None right_sash_pos = None main_sash_pos = None try: # Main vertical split (top | bottom) if hasattr(self, 'main_pane') and len(self.main_pane.panes()) > 0: main_sash_pos = self.main_pane.sash_coord(0) # Top horizontal split (MCS | right) if hasattr(self, 'top_pane') and len(self.top_pane.panes()) > 0: top_sash_pos = self.top_pane.sash_coord(0) # Right vertical split (nav | irst) if hasattr(self, 'right_pane') and len(self.right_pane.panes()) > 0: right_sash_pos = self.right_pane.sash_coord(0) except tk.TclError: pass # Ignore errors if panes not ready layout_config = { 'window_geometry': window_geometry, 'main_sash_position': main_sash_pos, 'top_sash_position': top_sash_pos, 'right_sash_position': right_sash_pos, 'docks': { dock_id: { 'visible': dock.is_visible, 'minimized': dock.is_minimized } for dock_id, dock in self.docks.items() } } try: with open(layout_file, 'w') as f: json.dump(layout_config, f, indent=2) print(f"Layout saved to {layout_file}") except Exception as e: print(f"Failed to save layout: {e}") def load_layout(self, layout_name: str = "default"): """ Load a workspace layout from a JSON file. Args: layout_name: Name of the layout configuration to load """ layout_file = os.path.join(self.layouts_dir, f"{layout_name}.json") if not os.path.exists(layout_file): print(f"Layout file not found: {layout_file} - using default layout") return try: with open(layout_file) as f: layout_config = json.load(f) # Restore window geometry immediately if layout_config.get('window_geometry'): try: self.root.geometry(layout_config['window_geometry']) print(f"Window geometry restored: {layout_config['window_geometry']}") except Exception as e: print(f"Error restoring window geometry: {e}") # Restore sash positions after a delay to ensure widgets are rendered def restore_sashes(): try: # Restore main vertical split (top | bottom) if layout_config.get('main_sash_position'): x, y = layout_config['main_sash_position'] if hasattr(self, 'main_pane') and len(self.main_pane.panes()) > 0: self.main_pane.sash_place(0, x, y) print(f"Main sash restored: ({x}, {y})") # Restore top horizontal split (MCS | right) if layout_config.get('top_sash_position'): x, y = layout_config['top_sash_position'] if hasattr(self, 'top_pane') and len(self.top_pane.panes()) > 0: self.top_pane.sash_place(0, x, y) print(f"Top sash restored: ({x}, {y})") # Restore right vertical split (nav | irst) if layout_config.get('right_sash_position'): x, y = layout_config['right_sash_position'] if hasattr(self, 'right_pane') and len(self.right_pane.panes()) > 0: self.right_pane.sash_place(0, x, y) print(f"Right sash restored: ({x}, {y})") print(f"✓ Layout fully loaded from {layout_file}") except Exception as e: print(f"Error restoring sash positions: {e}") # Schedule sash restoration after widgets are fully rendered self.root.after(300, restore_sashes) # Restore dock states for dock_id, state in layout_config.get('docks', {}).items(): dock = self.docks.get(dock_id) if dock: if not state['visible']: dock.close() elif state['minimized']: dock.minimize() except Exception as e: print(f"Failed to load layout: {e}")