SXXXXXXX_PyMsc/pymsc/gui/docking/workspace_manager.py

264 lines
9.7 KiB
Python

# -*- 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}")