"""
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
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()