Source code for hbat.gui.graphviz_preferences_dialog

"""
GraphViz preferences dialog for HBAT.

This module provides a dialog window for configuring GraphViz visualization
settings including engine selection, rendering options, and export preferences.
"""

import logging
import tkinter as tk
from tkinter import messagebox, ttk
from typing import Dict, Optional

from hbat.core.app_config import HBATConfig
from hbat.utilities.graphviz_utils import GraphVizDetector

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

# Background color options
BACKGROUND_COLORS = [
    ("White", "white"),
    ("Light Gray", "lightgray"),
    ("Light Blue", "lightblue"),
    ("Transparent", "transparent"),
]

# Node shape options
NODE_SHAPES = [
    ("Ellipse", "ellipse"),
    ("Box", "box"),
    ("Circle", "circle"),
    ("Diamond", "diamond"),
    ("Polygon", "polygon"),
    ("Record", "record"),
]

# Graph direction options
RANK_DIRECTIONS = [
    ("Top to Bottom", "TB"),
    ("Bottom to Top", "BT"),
    ("Left to Right", "LR"),
    ("Right to Left", "RL"),
]


[docs] class GraphVizPreferencesDialog: """Dialog for configuring GraphViz preferences. Provides a user interface for setting GraphViz engine preferences, rendering options, and export settings. """
[docs] def __init__(self, parent: tk.Widget, config: HBATConfig) -> None: """Initialize GraphViz preferences dialog. :param parent: Parent widget :type parent: tk.Widget :param config: HBAT configuration instance :type config: HBATConfig """ self.parent = parent self.config = config self.dialog: Optional[tk.Toplevel] = None self.result = False # Variables for settings self.vars: Dict[str, tk.Variable] = {} # Check GraphViz availability self.graphviz_available = GraphVizDetector.is_graphviz_available() self.available_engines = GraphVizDetector.get_available_engines() # Create dialog self._create_dialog()
def _create_dialog(self) -> None: """Create the preferences dialog window.""" self.dialog = tk.Toplevel(self.parent) self.dialog.title("GraphViz Preferences") self.dialog.geometry("550x450") 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}") # Create main frame main_frame = ttk.Frame(self.dialog, padding="10") main_frame.pack(fill=tk.BOTH, expand=True) # Create sections self._create_status_section(main_frame) self._create_engine_section(main_frame) self._create_rendering_section(main_frame) self._create_export_section(main_frame) self._create_buttons(main_frame) def _create_status_section(self, parent: ttk.Frame) -> None: """Create GraphViz status section. :param parent: Parent frame :type parent: ttk.Frame """ status_frame = ttk.LabelFrame(parent, text="GraphViz Status", padding="10") status_frame.pack(fill=tk.X, pady=(0, 10)) # GraphViz availability status_text = "Available" if self.graphviz_available else "Not Available" status_color = "green" if self.graphviz_available else "red" status_label = ttk.Label( status_frame, text=f"GraphViz Status: {status_text}", foreground=status_color, ) status_label.pack(anchor=tk.W) if self.graphviz_available: # Show version if available version = GraphVizDetector.get_graphviz_version() if version: ttk.Label(status_frame, text=f"Version: {version}").pack(anchor=tk.W) # Show available engines ttk.Label( status_frame, text=f"Available Engines: {', '.join(self.available_engines)}", ).pack(anchor=tk.W) else: ttk.Label( status_frame, text="GraphViz is not installed. Please install GraphViz to use these features.", foreground="gray", ).pack(anchor=tk.W, pady=(5, 0)) # Enable/Disable GraphViz self.vars["enabled"] = tk.BooleanVar(value=self.config.is_graphviz_enabled()) ttk.Checkbutton( status_frame, text="Enable GraphViz visualization", variable=self.vars["enabled"], state="normal" if self.graphviz_available else "disabled", ).pack(anchor=tk.W, pady=(10, 0)) def _create_engine_section(self, parent: ttk.Frame) -> None: """Create engine selection section. :param parent: Parent frame :type parent: ttk.Frame """ engine_frame = ttk.LabelFrame(parent, text="Layout Engine", padding="10") engine_frame.pack(fill=tk.X, pady=(0, 10)) # Current engine current_engine = self.config.get_graphviz_engine() self.vars["engine"] = tk.StringVar(value=current_engine) # Engine selection - ensure "dot" is first engines_ordered = self.available_engines.copy() if "dot" in engines_ordered: engines_ordered = ["dot"] + [ engine for engine in engines_ordered if engine != "dot" ] for engine in engines_ordered: if engine in ENGINE_DESCRIPTIONS: text = f"{engine} - {ENGINE_DESCRIPTIONS[engine]}" else: text = engine ttk.Radiobutton( engine_frame, text=text, variable=self.vars["engine"], value=engine, state="normal" if self.graphviz_available else "disabled", ).pack(anchor=tk.W, pady=2) def _create_rendering_section(self, parent: ttk.Frame) -> None: """Create rendering options section. :param parent: Parent frame :type parent: ttk.Frame """ render_frame = ttk.LabelFrame(parent, text="Rendering Options", padding="10") render_frame.pack(fill=tk.X, pady=(0, 10)) # Background color bg_frame = ttk.Frame(render_frame) bg_frame.pack(fill=tk.X, pady=(0, 5)) ttk.Label(bg_frame, text="Background Color:").pack(side=tk.LEFT) current_bg = self.config.get_graphviz_preference("background_color", "white") self.vars["background_color"] = tk.StringVar(value=current_bg) bg_menu = ttk.Combobox( bg_frame, textvariable=self.vars["background_color"], values=[name for name, _ in BACKGROUND_COLORS], state="readonly" if self.graphviz_available else "disabled", width=15, ) bg_menu.pack(side=tk.LEFT, padx=(10, 0)) # Node shape shape_frame = ttk.Frame(render_frame) shape_frame.pack(fill=tk.X, pady=(0, 5)) ttk.Label(shape_frame, text="Node Shape:").pack(side=tk.LEFT) current_shape = self.config.get_graphviz_preference("node_shape", "ellipse") self.vars["node_shape"] = tk.StringVar(value=current_shape) shape_menu = ttk.Combobox( shape_frame, textvariable=self.vars["node_shape"], values=[name for name, _ in NODE_SHAPES], state="readonly" if self.graphviz_available else "disabled", width=15, ) shape_menu.pack(side=tk.LEFT, padx=(10, 0)) # Graph direction dir_frame = ttk.Frame(render_frame) dir_frame.pack(fill=tk.X, pady=(0, 5)) ttk.Label(dir_frame, text="Graph Direction:").pack(side=tk.LEFT) current_dir = self.config.get_graphviz_preference("rankdir", "TB") self.vars["rankdir"] = tk.StringVar(value=current_dir) dir_menu = ttk.Combobox( dir_frame, textvariable=self.vars["rankdir"], values=[name for name, _ in RANK_DIRECTIONS], state="readonly" if self.graphviz_available else "disabled", width=15, ) dir_menu.pack(side=tk.LEFT, padx=(10, 0)) def _create_export_section(self, parent: ttk.Frame) -> None: """Create export settings section. :param parent: Parent frame :type parent: ttk.Frame """ export_frame = ttk.LabelFrame(parent, text="Export Settings", padding="10") export_frame.pack(fill=tk.X, pady=(0, 10)) # Export DPI dpi_frame = ttk.Frame(export_frame) dpi_frame.pack(fill=tk.X, pady=(0, 5)) ttk.Label(dpi_frame, text="Export DPI:").pack(side=tk.LEFT) current_dpi = self.config.get_graphviz_export_dpi() self.vars["export_dpi"] = tk.IntVar(value=current_dpi) dpi_spinbox = ttk.Spinbox( dpi_frame, from_=72, to=600, increment=50, textvariable=self.vars["export_dpi"], state="normal" if self.graphviz_available else "disabled", width=10, ) dpi_spinbox.pack(side=tk.LEFT, padx=(10, 0)) ttk.Label(dpi_frame, text="(72-600)").pack(side=tk.LEFT, padx=(5, 0)) # Default format format_frame = ttk.Frame(export_frame) format_frame.pack(fill=tk.X, pady=(0, 5)) ttk.Label(format_frame, text="Default Format:").pack(side=tk.LEFT) current_format = self.config.get_graphviz_render_format() self.vars["render_format"] = tk.StringVar(value=current_format) format_menu = ttk.Combobox( format_frame, textvariable=self.vars["render_format"], values=["png", "svg", "pdf"], state="readonly" if self.graphviz_available else "disabled", width=10, ) format_menu.pack(side=tk.LEFT, padx=(10, 0)) def _create_buttons(self, parent: ttk.Frame) -> None: """Create dialog buttons. :param parent: Parent frame :type parent: ttk.Frame """ button_frame = ttk.Frame(parent) button_frame.pack(fill=tk.X, pady=(10, 0)) # Cancel button ttk.Button(button_frame, text="Cancel", command=self._cancel).pack( side=tk.RIGHT, padx=(5, 0) ) # OK button ttk.Button(button_frame, text="OK", command=self._save_preferences).pack( side=tk.RIGHT ) # Reset button ttk.Button( button_frame, text="Reset to Defaults", command=self._reset_defaults ).pack(side=tk.LEFT) def _save_preferences(self) -> None: """Save preferences and close dialog.""" try: # Save enabled state self.config.enable_graphviz(self.vars["enabled"].get()) # Save engine preference self.config.set_graphviz_engine(self.vars["engine"].get()) # Save rendering options # Convert display names back to values bg_value = "white" for name, value in BACKGROUND_COLORS: if name == self.vars["background_color"].get(): bg_value = value break self.config.set_graphviz_preference("background_color", bg_value) shape_value = "ellipse" for name, value in NODE_SHAPES: if name == self.vars["node_shape"].get(): shape_value = value break self.config.set_graphviz_preference("node_shape", shape_value) dir_value = "TB" for name, value in RANK_DIRECTIONS: if name == self.vars["rankdir"].get(): dir_value = value break self.config.set_graphviz_preference("rankdir", dir_value) # Save export settings dpi = self.vars["export_dpi"].get() if 72 <= dpi <= 600: self.config.set_graphviz_export_dpi(dpi) else: raise ValueError("DPI must be between 72 and 600") self.config.set_graphviz_render_format(self.vars["render_format"].get()) # Save config self.config.save_config(self.config.load_config()) self.result = True self.dialog.destroy() logger.info("GraphViz preferences saved successfully") except Exception as e: logger.error(f"Failed to save preferences: {e}") messagebox.showerror( "Error", f"Failed to save preferences: {str(e)}", parent=self.dialog ) def _cancel(self) -> None: """Cancel and close dialog.""" self.result = False self.dialog.destroy() def _reset_defaults(self) -> None: """Reset all settings to defaults.""" response = messagebox.askyesno( "Reset to Defaults", "Are you sure you want to reset all GraphViz settings to defaults?", parent=self.dialog, ) if response: # Reset all variables to defaults self.vars["enabled"].set(True) self.vars["engine"].set("dot") self.vars["background_color"].set("White") self.vars["node_shape"].set("Ellipse") self.vars["rankdir"].set("Top to Bottom") self.vars["export_dpi"].set(300) self.vars["render_format"].set("png")
[docs] def show(self) -> bool: """Show dialog and wait for result. :returns: True if preferences were saved, False if cancelled :rtype: bool """ self.dialog.wait_window() return self.result
[docs] def show_graphviz_preferences(parent: tk.Widget, config: HBATConfig) -> bool: """Show GraphViz preferences dialog. :param parent: Parent widget :type parent: tk.Widget :param config: HBAT configuration instance :type config: HBATConfig :returns: True if preferences were saved :rtype: bool """ dialog = GraphVizPreferencesDialog(parent, config) return dialog.show()