Source code for hbat.gui.export_manager

"""
Export manager for HBAT visualizations.

This module provides centralized export functionality for different
visualization renderers, handling file dialogs, format selection,
and export operations.
"""

import logging
import os
import tkinter as tk
from pathlib import Path
from tkinter import filedialog, messagebox, simpledialog, ttk
from typing import Dict, List, Optional, Tuple

from hbat.core.app_config import HBATConfig
from hbat.gui.visualization_renderer import VisualizationRenderer

# Set up logging
logger = logging.getLogger(__name__)

# Export format configurations
EXPORT_FORMATS = {
    "png": {
        "name": "PNG Image",
        "extension": ".png",
        "description": "Portable Network Graphics",
        "filetypes": [("PNG files", "*.png")],
    },
    "svg": {
        "name": "SVG Vector",
        "extension": ".svg",
        "description": "Scalable Vector Graphics",
        "filetypes": [("SVG files", "*.svg")],
    },
    "pdf": {
        "name": "PDF Document",
        "extension": ".pdf",
        "description": "Portable Document Format",
        "filetypes": [("PDF files", "*.pdf")],
    },
    "eps": {
        "name": "EPS Vector",
        "extension": ".eps",
        "description": "Encapsulated PostScript",
        "filetypes": [("EPS files", "*.eps")],
    },
    "dot": {
        "name": "DOT Source",
        "extension": ".dot",
        "description": "GraphViz DOT language source file",
        "filetypes": [("DOT files", "*.dot")],
    },
}


[docs] class ExportManager: """Manages visualization export functionality. Provides a unified interface for exporting visualizations from different renderers with format selection, quality settings, and file management. """
[docs] def __init__(self, renderer: VisualizationRenderer, config: HBATConfig) -> None: """Initialize export manager. :param renderer: Visualization renderer instance :type renderer: VisualizationRenderer :param config: HBAT configuration instance :type config: HBATConfig """ self.renderer = renderer self.config = config self.supported_formats = self._get_supported_formats() self.last_export_directory = self.config.get_preference("last_export_directory")
[docs] def export_visualization( self, filename: Optional[str] = None, format: Optional[str] = None, resolution: Optional[int] = None, ) -> bool: """Export visualization to file. If filename or format are not provided, shows dialog to get them. :param filename: Output filename (optional) :type filename: Optional[str] :param format: Export format (optional) :type format: Optional[str] :param resolution: Export resolution/DPI (optional) :type resolution: Optional[int] :returns: True if export successful :rtype: bool """ try: # Get filename and format if not provided if not filename or not format: export_info = self.show_export_dialog() if not export_info: return False filename, format = export_info # Validate format if format not in self.supported_formats: messagebox.showerror( "Export Error", f"Format '{format}' is not supported by {self.renderer.get_renderer_name()}", ) return False # Set resolution if provided if resolution: if hasattr(self.config, "set_graphviz_export_dpi"): self.config.set_graphviz_export_dpi(resolution) # Perform export success = self.renderer.export(format, filename) if success: # Update last export directory self._update_last_export_directory(filename) # Show success message messagebox.showinfo( "Export Successful", f"Visualization exported to:\n{filename}" ) logger.info(f"Successfully exported visualization to {filename}") else: # Show error message messagebox.showerror( "Export Failed", f"Failed to export visualization to {filename}" ) logger.error(f"Failed to export visualization to {filename}") return success except Exception as e: error_msg = f"Export failed: {str(e)}" messagebox.showerror("Export Error", error_msg) logger.error(error_msg) return False
[docs] def show_export_dialog(self) -> Optional[Tuple[str, str]]: """Show export dialog to get filename and format. :returns: Tuple of (filename, format) or None if cancelled :rtype: Optional[Tuple[str, str]] """ try: # Create export dialog dialog = ExportDialog( self.supported_formats, self.config, self.last_export_directory ) result = dialog.show() if result: filename, format = result return filename, format return None except Exception as e: logger.error(f"Error showing export dialog: {e}") return None
[docs] def get_supported_formats(self) -> List[str]: """Get list of supported export formats. :returns: List of supported format names :rtype: List[str] """ return self.supported_formats
[docs] def get_format_info(self, format: str) -> Optional[Dict[str, any]]: """Get information about an export format. :param format: Format name :type format: str :returns: Format information dictionary :rtype: Optional[Dict[str, any]] """ return EXPORT_FORMATS.get(format)
def _get_supported_formats(self) -> List[str]: """Get formats supported by the current renderer. :returns: List of supported format names :rtype: List[str] """ renderer_formats = self.renderer.get_supported_formats() return [fmt for fmt in renderer_formats if fmt in EXPORT_FORMATS] def _update_last_export_directory(self, filename: str) -> None: """Update the last export directory preference. :param filename: Exported filename :type filename: str """ try: directory = str(Path(filename).parent) self.config.set_preference("last_export_directory", directory) self.last_export_directory = directory except Exception as e: logger.warning(f"Failed to update last export directory: {e}")
[docs] class ExportDialog: """Dialog for selecting export options. Provides a GUI for selecting export format, filename, and quality settings. """
[docs] def __init__( self, supported_formats: List[str], config: HBATConfig, initial_directory: Optional[str] = None, ) -> None: """Initialize export dialog. :param supported_formats: List of supported format names :type supported_formats: List[str] :param config: HBAT configuration instance :type config: HBATConfig :param initial_directory: Initial directory for file dialog :type initial_directory: Optional[str] """ self.supported_formats = supported_formats self.config = config self.initial_directory = initial_directory or os.getcwd() self.result: Optional[Tuple[str, str]] = None # Dialog widgets self.dialog: Optional[tk.Toplevel] = None self.format_var: Optional[tk.StringVar] = None self.resolution_var: Optional[tk.IntVar] = None
[docs] def show(self) -> Optional[Tuple[str, str]]: """Show the export dialog. :returns: Tuple of (filename, format) or None if cancelled :rtype: Optional[Tuple[str, str]] """ # Quick export if only one format supported if len(self.supported_formats) == 1: format = self.supported_formats[0] filename = self._show_file_dialog(format) if filename: return filename, format return None # Show full dialog for multiple formats return self._show_full_dialog()
def _show_full_dialog(self) -> Optional[Tuple[str, str]]: """Show full export options dialog. :returns: Tuple of (filename, format) or None if cancelled :rtype: Optional[Tuple[str, str]] """ # Create dialog window self.dialog = tk.Toplevel() self.dialog.title("Export Visualization") self.dialog.geometry("400x300") self.dialog.resizable(False, False) self.dialog.grab_set() # Make dialog modal # Center the dialog self.dialog.update_idletasks() x = (self.dialog.winfo_screenwidth() - self.dialog.winfo_width()) // 2 y = (self.dialog.winfo_screenheight() - self.dialog.winfo_height()) // 2 self.dialog.geometry(f"+{x}+{y}") self._create_dialog_widgets() # Wait for dialog to close self.dialog.wait_window() return self.result def _create_dialog_widgets(self) -> None: """Create dialog widgets.""" if not self.dialog: return # Main frame main_frame = ttk.Frame(self.dialog, padding="10") main_frame.pack(fill=tk.BOTH, expand=True) # Format selection format_frame = ttk.LabelFrame(main_frame, text="Export Format", padding="10") format_frame.pack(fill=tk.X, pady=(0, 10)) self.format_var = tk.StringVar(value=self.supported_formats[0]) for format in self.supported_formats: format_info = EXPORT_FORMATS.get(format, {}) text = f"{format_info.get('name', format.upper())} ({format})" desc = format_info.get("description", "") if desc: text += f" - {desc}" ttk.Radiobutton( format_frame, text=text, variable=self.format_var, value=format ).pack(anchor=tk.W, pady=2) # Quality settings quality_frame = ttk.LabelFrame( main_frame, text="Quality Settings", padding="10" ) quality_frame.pack(fill=tk.X, pady=(0, 10)) # Resolution setting resolution_frame = ttk.Frame(quality_frame) resolution_frame.pack(fill=tk.X, pady=2) ttk.Label(resolution_frame, text="Resolution (DPI):").pack(side=tk.LEFT) self.resolution_var = tk.IntVar( value=self.config.get_preference("export_dpi", 300) ) resolution_spinbox = ttk.Spinbox( resolution_frame, from_=72, to=600, width=10, textvariable=self.resolution_var, ) resolution_spinbox.pack(side=tk.RIGHT) # Buttons button_frame = ttk.Frame(main_frame) button_frame.pack(fill=tk.X, pady=(10, 0)) ttk.Button(button_frame, text="Cancel", command=self._cancel).pack( side=tk.RIGHT, padx=(5, 0) ) ttk.Button(button_frame, text="Export...", command=self._export).pack( side=tk.RIGHT ) def _export(self) -> None: """Handle export button click.""" if not self.format_var: return format = self.format_var.get() # Update resolution preference if self.resolution_var: resolution = self.resolution_var.get() self.config.set_preference("export_dpi", resolution) # Show file dialog filename = self._show_file_dialog(format) if filename: self.result = (filename, format) if self.dialog: self.dialog.destroy() def _cancel(self) -> None: """Handle cancel button click.""" self.result = None if self.dialog: self.dialog.destroy() def _show_file_dialog(self, format: str) -> Optional[str]: """Show file save dialog for specific format. :param format: Export format :type format: str :returns: Selected filename or None :rtype: Optional[str] """ format_info = EXPORT_FORMATS.get(format, {}) # Prepare file dialog options filetypes = format_info.get( "filetypes", [(f"{format.upper()} files", f"*.{format}")] ) filetypes.append(("All files", "*.*")) # Show save dialog filename = filedialog.asksaveasfilename( title=f"Export as {format_info.get('name', format.upper())}", initialdir=self.initial_directory, filetypes=filetypes, defaultextension=format_info.get("extension", f".{format}"), ) return filename if filename else None
[docs] def show_quick_export_dialog( renderer: VisualizationRenderer, config: HBATConfig ) -> bool: """Show a quick export dialog for immediate export. :param renderer: Visualization renderer :type renderer: VisualizationRenderer :param config: HBAT configuration instance :type config: HBATConfig :returns: True if export successful :rtype: bool """ export_manager = ExportManager(renderer, config) return export_manager.export_visualization()