# markdownconverter/gui/gui.py import os import sys import logging import re import subprocess import tkinter as tk from datetime import date from tkinter import simpledialog from pathlib import Path import ttkbootstrap as tb from tkinter.scrolledtext import ScrolledText from ttkbootstrap.scrolled import ScrolledFrame from ttkbootstrap.constants import * from tkinter import filedialog, messagebox, StringVar, BooleanVar from ..core.core import ( convert_markdown, convert_docx_to_pdf, scan_template_for_placeholders, ConverterNotFoundError, TemplatePlaceholderError ) from ..utils.config import ( save_configuration, load_configuration, KEY_LAST_MARKDOWN, KEY_LAST_PROFILE, KEY_PROFILES, KEY_TEMPLATE_PATH, KEY_VALUES ) from ..utils.logger import ( setup_basic_logging, add_tkinter_handler, shutdown_logging_system, get_logger ) from .batch_converter import BatchConverterTab # EditorWindow non viene usato in questo file, ma lo lasciamo per coerenza # from .editor import EditorWindow log = get_logger(__name__) # ... (open_with_default_app, open_output_folder, ProfileManagerWindow sono INVARIATE) # ... (ti fornisco comunque il codice completo per sicurezza) def open_with_default_app(filepath: str): log.info(f"Request to open file at path: '{filepath}'") p = Path(filepath) if not p.is_file(): log.warning("Open file requested, but path is invalid or file does not exist.") messagebox.showwarning("Warning", "No output file or folder to open.") return try: log.info(f"Opening '{p}' with default application.") if sys.platform == "win32": os.startfile(p) elif sys.platform == "darwin": subprocess.run(["open", p], check=True) else: subprocess.run(["xdg-open", p], check=True) except Exception as e: log.error(f"Failed to open '{p}': {e}", exc_info=True) messagebox.showerror("Error", f"Could not open the file/folder:\n{str(e)}") def open_output_folder(filepath: str): if not filepath: log.warning("Open folder requested, but no output file has been generated yet.") messagebox.showwarning("Warning", "No output file has been generated yet.") return folder = Path(filepath).parent open_with_default_app(str(folder)) class ProfileManagerWindow(tb.Toplevel): def __init__(self, parent, config_data, app_instance): super().__init__(master=parent, title="Manage Profiles") self.geometry("500x400") self.transient(parent) self.config_data = config_data self.app = app_instance self.selected_profile_name = None self._setup_widgets() self._load_profiles() def _setup_widgets(self): main_frame = tb.Frame(self, padding=10); main_frame.pack(fill=tk.BOTH, expand=True) main_frame.rowconfigure(0, weight=1); main_frame.columnconfigure(0, weight=1) list_frame = tb.Labelframe(main_frame, text="Existing Profiles", padding=5) list_frame.grid(row=0, column=0, sticky="nsew", padx=(0, 10)) list_frame.rowconfigure(0, weight=1); list_frame.columnconfigure(0, weight=1) self.profile_listbox = tk.Listbox(list_frame, exportselection=False) self.profile_listbox.grid(row=0, column=0, sticky="nsew") scrollbar = tb.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.profile_listbox.yview) scrollbar.grid(row=0, column=1, sticky="ns") self.profile_listbox.config(yscrollcommand=scrollbar.set) self.profile_listbox.bind("<>", self._on_profile_select) button_frame = tb.Frame(main_frame); button_frame.grid(row=0, column=1, sticky="ns") tb.Button(button_frame, text="New...", command=self._new_profile, bootstyle=SUCCESS).pack(fill=tk.X, pady=5) self.rename_btn = tb.Button(button_frame, text="Rename...", command=self._rename_profile, state=tk.DISABLED) self.rename_btn.pack(fill=tk.X, pady=5) self.delete_btn = tb.Button(button_frame, text="Delete", command=self._delete_profile, state=tk.DISABLED, bootstyle=DANGER) self.delete_btn.pack(fill=tk.X, pady=5) tb.Button(button_frame, text="Close", command=self.destroy).pack(fill=tk.X, pady=(20, 5)) def _load_profiles(self): self.profile_listbox.delete(0, tk.END) for profile_name in sorted(self.config_data[KEY_PROFILES].keys()): self.profile_listbox.insert(tk.END, profile_name) self._on_profile_select() def _on_profile_select(self, event=None): selection = self.profile_listbox.curselection() if not selection: self.selected_profile_name = None self.rename_btn.config(state=tk.DISABLED); self.delete_btn.config(state=tk.DISABLED) else: self.selected_profile_name = self.profile_listbox.get(selection[0]) self.rename_btn.config(state=tk.NORMAL); self.delete_btn.config(state=tk.NORMAL) def _new_profile(self): profile_name = simpledialog.askstring("New Profile", "Enter a name for the new profile:", parent=self) if not profile_name: return if profile_name in self.config_data[KEY_PROFILES]: messagebox.showerror("Error", f"A profile named '{profile_name}' already exists.", parent=self); return template_path = filedialog.askopenfilename(title=f"Select Template for '{profile_name}'", filetypes=[("Word Documents", "*.docx")], parent=self) if not template_path: return self.config_data[KEY_PROFILES][profile_name] = {KEY_TEMPLATE_PATH: template_path, KEY_VALUES: {}} self.app.update_config(self.config_data) self.app.update_profile_combobox(new_selection=profile_name) self._load_profiles() def _rename_profile(self): if not self.selected_profile_name: return new_name = simpledialog.askstring("Rename Profile", f"Enter a new name for '{self.selected_profile_name}':", parent=self) if not new_name or new_name == self.selected_profile_name: return if new_name in self.config_data[KEY_PROFILES]: messagebox.showerror("Error", f"A profile named '{new_name}' already exists.", parent=self); return self.config_data[KEY_PROFILES][new_name] = self.config_data[KEY_PROFILES].pop(self.selected_profile_name) if self.config_data[KEY_LAST_PROFILE] == self.selected_profile_name: self.config_data[KEY_LAST_PROFILE] = new_name self.app.update_config(self.config_data) self.app.update_profile_combobox(new_selection=new_name) self._load_profiles() def _delete_profile(self): if not self.selected_profile_name: return if not messagebox.askyesno("Confirm Delete", f"Are you sure you want to delete profile '{self.selected_profile_name}'?", parent=self): return del self.config_data[KEY_PROFILES][self.selected_profile_name] if self.config_data[KEY_LAST_PROFILE] == self.selected_profile_name: self.config_data[KEY_LAST_PROFILE] = "" self.app.update_config(self.config_data) self.app.update_profile_combobox() self._load_profiles() class MarkdownConverterApp: def __init__(self, root): self.root = root self.root.title("Markdown Converter") self.root.geometry("900x850") self._setup_logging() self.config = load_configuration() self.dynamic_entry_vars = {} self.loaded_profile_name = "" self.selected_file = StringVar(value=self.config.get(KEY_LAST_MARKDOWN, "")) # --- NEW CODE --- # Add a trace to the StringVar. Whenever its value is written, # the _on_selected_file_change callback will be executed. self.selected_file.trace_add("write", self._on_selected_file_change) self.active_profile_name = StringVar() # Initialized empty self.add_toc_var = BooleanVar(value=True) self.docx_output_path = StringVar() self.pdf_direct_output_path = StringVar() self.pdf_from_docx_output_path = StringVar() self._build_ui() self.update_profile_combobox() # This will set active_profile_name self.root.protocol("WM_DELETE_WINDOW", self._on_closing) def _setup_logging(self): self.log_config = {"default_root_level": logging.DEBUG, "format": "%(asctime)s [%(levelname)-8s] %(name)-20s: %(message)s", "date_format": "%H:%M:%S", "enable_console": True, "enable_file": True, "file_path": "markdown_converter.log", "colors": {logging.DEBUG: "gray", logging.INFO: "black", logging.WARNING: "orange", logging.ERROR: "red", logging.CRITICAL: "purple"}} setup_basic_logging(root_tk_instance_for_processor=self.root, logging_config_dict=self.log_config) def _build_ui(self): # Crea il Notebook (sistema a tab) self.notebook = tb.Notebook(self.root) self.notebook.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) # Tab 1: Conversione Singola (funzionalità originale) self.single_converter_tab = tb.Frame(self.notebook, padding=10) self.notebook.add(self.single_converter_tab, text="Conversione Singola") # Tab 2: Conversione Batch self.batch_converter_tab = BatchConverterTab(self.notebook) self.notebook.add(self.batch_converter_tab, text="Conversione Batch") # Costruisce la UI del tab singolo nel container self._build_single_converter_ui() def _build_single_converter_ui(self): """Costruisce l'interfaccia per la conversione singola.""" container = self.single_converter_tab profile_frame = tb.Labelframe(container, text="Profile & Source File", padding=10) profile_frame.pack(fill=tk.X, pady=(0, 10)); profile_frame.columnconfigure(1, weight=1) tb.Label(profile_frame, text="Active Profile:").grid(row=0, column=0, padx=5, pady=5, sticky="w") self.profile_combobox = tb.Combobox(profile_frame, textvariable=self.active_profile_name, state="readonly") self.profile_combobox.grid(row=0, column=1, padx=5, pady=5, sticky="ew") self.profile_combobox.bind("<>", self._on_profile_change) tb.Button(profile_frame, text="Manage...", command=self.open_profile_manager).grid(row=0, column=2, padx=5, pady=5) tb.Label(profile_frame, text="Markdown File:").grid(row=1, column=0, padx=5, pady=5, sticky="w") tb.Entry(profile_frame, textvariable=self.selected_file).grid(row=1, column=1, padx=5, pady=5, sticky="ew") tb.Button(profile_frame, text="Browse...", command=self.browse_markdown).grid(row=1, column=2, padx=5, pady=5) self.placeholder_frame = ScrolledFrame(container, autohide=True, bootstyle="round") self.placeholder_frame.pack(fill=tk.BOTH, expand=True, pady=10) options_frame = tb.Frame(container); options_frame.pack(fill=tk.X, pady=(0, 10)) tb.Checkbutton(options_frame, text="Add TOC", variable=self.add_toc_var, bootstyle="primary-round-toggle").pack(side=tk.LEFT, padx=(0, 20)) tb.Button(options_frame, text="Open Source Folder", command=lambda: open_output_folder(self.selected_file.get())).pack(side=tk.LEFT, padx=5) action_frame = tb.Labelframe(container, text="Conversion Actions", padding=10) action_frame.pack(fill=tk.X, pady=10); action_frame.columnconfigure(1, weight=1) tb.Button(action_frame, text="MD -> DOCX", command=lambda: self.convert("DOCX"), width=12).grid(row=0, column=0, padx=5, pady=5) tb.Entry(action_frame, textvariable=self.docx_output_path).grid(row=0, column=1, padx=5, pady=5, sticky="ew") tb.Button(action_frame, text="Open", command=self.open_docx_output).grid(row=0, column=2, padx=5, pady=5) tb.Button(action_frame, text="MD -> PDF", command=lambda: self.convert("PDF"), width=12).grid(row=1, column=0, padx=5, pady=5) tb.Entry(action_frame, textvariable=self.pdf_direct_output_path).grid(row=1, column=1, padx=5, pady=5, sticky="ew") tb.Button(action_frame, text="Open", command=self.open_pdf_direct_output).grid(row=1, column=2, padx=5, pady=5) self.docx_to_pdf_btn = tb.Button(action_frame, text="DOCX -> PDF", command=self.convert_from_docx_to_pdf, bootstyle="info", width=12, state=tk.DISABLED) self.docx_to_pdf_btn.grid(row=2, column=0, padx=5, pady=5) tb.Entry(action_frame, textvariable=self.pdf_from_docx_output_path).grid(row=2, column=1, padx=5, pady=5, sticky="ew") tb.Button(action_frame, text="Open", command=self.open_pdf_from_docx_output).grid(row=2, column=2, padx=5, pady=5) log_frame = tb.Labelframe(container, text="Log Viewer", padding=10) log_frame.pack(fill=tk.BOTH, expand=True, pady=(10,0)) log_text_widget = ScrolledText(log_frame, wrap=tk.WORD, state=tk.DISABLED, height=10) log_text_widget.pack(fill=tk.BOTH, expand=True) add_tkinter_handler(gui_log_widget=log_text_widget, root_tk_instance_for_gui_handler=self.root, logging_config_dict=self.log_config) def open_docx_output(self): open_with_default_app(self.docx_output_path.get()) def open_pdf_direct_output(self): open_with_default_app(self.pdf_direct_output_path.get()) def open_pdf_from_docx_output(self): open_with_default_app(self.pdf_from_docx_output_path.get()) def update_profile_combobox(self, new_selection=None): profiles = sorted(self.config[KEY_PROFILES].keys()) self.profile_combobox['values'] = profiles last_profile = self.config.get(KEY_LAST_PROFILE) if new_selection and new_selection in profiles: self.active_profile_name.set(new_selection) elif last_profile in profiles: self.active_profile_name.set(last_profile) else: self.active_profile_name.set("") self._load_profile_data() def open_profile_manager(self): win = ProfileManagerWindow(self.root, self.config, self) self.root.wait_window(win) def _on_profile_change(self, event=None): self._save_current_values() self.config[KEY_LAST_PROFILE] = self.active_profile_name.get() self._load_profile_data() def _load_profile_data(self): for widget in self.placeholder_frame.winfo_children(): widget.destroy() self.dynamic_entry_vars.clear() profile_name = self.active_profile_name.get() self.loaded_profile_name = profile_name # Memorizza il profilo caricato if not profile_name: tb.Label(self.placeholder_frame, text="No profile selected. Create or select a profile.", bootstyle=INFO).pack(pady=10) self._update_output_paths(); return profile_data = self.config[KEY_PROFILES].get(profile_name, {}) template_path = profile_data.get(KEY_TEMPLATE_PATH) if not template_path or not os.path.exists(template_path): tb.Label(self.placeholder_frame, text=f"Template file not found:\n{template_path}", bootstyle=WARNING, wraplength=700).pack(pady=10) return try: dynamic, structural = scan_template_for_placeholders(template_path) if not (dynamic or structural): tb.Label(self.placeholder_frame, text="No placeholders found in template.", bootstyle=INFO).pack(pady=10); return placeholder_grid = tb.Frame(self.placeholder_frame); placeholder_grid.pack(fill=tk.X, padx=5, pady=5); placeholder_grid.columnconfigure(1, weight=1) saved_values = profile_data.get(KEY_VALUES, {}) row = 0 for ph in dynamic: label_text = ph.replace("%%", "") + ":" tb.Label(placeholder_grid, text=label_text).grid(row=row, column=0, padx=5, pady=3, sticky="e") entry_var = StringVar(value=saved_values.get(ph, "")) entry = tb.Entry(placeholder_grid, textvariable=entry_var); entry.grid(row=row, column=1, columnspan=2, padx=5, pady=3, sticky="ew") entry_var.trace_add("write", lambda *_, ph_key=ph: self._on_placeholder_change(ph_key)) self.dynamic_entry_vars[ph] = entry_var row += 1 if structural: tb.Separator(placeholder_grid).grid(row=row, column=0, columnspan=3, pady=10, sticky="ew"); row += 1 for ph in structural: label_text = ph.replace("%%", "") + ":" tb.Label(placeholder_grid, text=label_text).grid(row=row, column=0, padx=5, pady=3, sticky="e") tb.Entry(placeholder_grid, state="readonly", textvariable=StringVar(value="")).grid(row=row, column=1, padx=5, pady=3, sticky="ew") tb.Label(placeholder_grid, text="Managed by application", bootstyle=SECONDARY).grid(row=row, column=2, padx=5, pady=3, sticky="w") row += 1 except (FileNotFoundError, TemplatePlaceholderError) as e: tb.Label(self.placeholder_frame, text=f"Error: {e}", bootstyle=DANGER, wraplength=700).pack(pady=10) self._update_output_paths() def _on_placeholder_change(self, placeholder_key): if placeholder_key in ["%%DOC_PROJECT%%", "%%DOC_NUMBER%%", "%%DOC_REV%%"]: self._update_output_paths() def _gather_current_values(self): return {ph: var.get() for ph, var in self.dynamic_entry_vars.items()} def _save_current_values(self): profile_to_save = self.loaded_profile_name if not profile_to_save or profile_to_save not in self.config[KEY_PROFILES]: return self.config[KEY_PROFILES][profile_to_save][KEY_VALUES] = self._gather_current_values() log.info(f"Updated values in memory for profile '{profile_to_save}'.") def _confirm_overwrite(self, filepath: str) -> bool: if os.path.exists(filepath): return messagebox.askyesno("Confirm Overwrite", f"File exists:\n{filepath}\n\nOverwrite?") return True def browse_markdown(self): path = filedialog.askopenfilename(filetypes=[("Markdown files", "*.md")]) if path: self.selected_file.set(path) def _update_output_paths(self): source_path_str = self.selected_file.get() if not source_path_str: return output_dir = Path(source_path_str).parent values = self._gather_current_values() project = values.get("%%DOC_PROJECT%%", "NoProject"); doc_num = values.get("%%DOC_NUMBER%%", "NoNum"); rev = values.get("%%DOC_REV%%", "NoRev") today = date.today().strftime("%Y%m%d") def sanitize(text: str) -> str: return re.sub(r'[\s/\\:*?"<>|]+', '_', text) docx_fn = f"{sanitize(project)}_SUM_{sanitize(doc_num)}_{sanitize(rev)}_{today}.docx" self.docx_output_path.set(str(output_dir / docx_fn)) pdf_from_docx_fn = f"{sanitize(project)}_SUM_{sanitize(doc_num)}_{sanitize(rev)}_{today}.pdf" self.pdf_from_docx_output_path.set(str(output_dir / pdf_from_docx_fn)) pdf_direct_fn = f"{sanitize(project)}_SUM_{today}.pdf" self.pdf_direct_output_path.set(str(output_dir / pdf_direct_fn)) def convert(self, fmt): self.docx_to_pdf_btn.config(state=tk.DISABLED) input_path = self.selected_file.get() if not input_path: messagebox.showerror("Error", "Please select a Markdown file."); return profile_name = self.active_profile_name.get() if not profile_name: messagebox.showerror("Error", "Please select a profile."); return template_path = self.config[KEY_PROFILES][profile_name].get(KEY_TEMPLATE_PATH) if fmt == "DOCX" and not template_path: messagebox.showwarning("Warning", "The selected profile has no template associated."); return output_path = self.docx_output_path.get() if fmt == "DOCX" else self.pdf_direct_output_path.get() if not output_path: messagebox.showerror("Error", "Please specify a valid output path."); return if not self._confirm_overwrite(output_path): return self._save_current_values() save_configuration(self.config) metadata = self._gather_current_values() try: result_path = convert_markdown(input_path, output_path, fmt, self.add_toc_var.get(), template_path, metadata) messagebox.showinfo("Success", f"File converted successfully:\n{result_path}") if fmt == "DOCX": self.docx_to_pdf_btn.config(state=tk.NORMAL) except Exception as e: log.critical(f"An error occurred during conversion: {e}", exc_info=True) messagebox.showerror("Error", f"An error occurred during conversion:\n{str(e)}") def convert_from_docx_to_pdf(self): input_path = self.docx_output_path.get() output_path = self.pdf_from_docx_output_path.get() if not input_path or not os.path.exists(input_path): messagebox.showerror("Error", "Source DOCX file does not exist. Generate it first."); return if not output_path: messagebox.showerror("Error", "Please specify a valid output path for the PDF."); return if not self._confirm_overwrite(output_path): return try: pdf_output = convert_docx_to_pdf(input_path, output_path) messagebox.showinfo("Success", f"DOCX successfully converted to PDF:\n{pdf_output}") except ConverterNotFoundError as e: messagebox.showerror("Converter Not Found", str(e)) except Exception as e: messagebox.showerror("DOCX to PDF Conversion Error", f"An unexpected error occurred.\nError: {str(e)}") def update_config(self, new_config): self.config = new_config save_configuration(self.config) def _on_closing(self): self._save_current_values() self.config[KEY_LAST_MARKDOWN] = self.selected_file.get() self.config[KEY_LAST_PROFILE] = self.active_profile_name.get() save_configuration(self.config) shutdown_logging_system() self.root.destroy() def _on_selected_file_change(self, *args): """ Callback function that is triggered whenever the self.selected_file StringVar is written to. It ensures the output paths are updated. """ self._update_output_paths() def run_app(): root = tb.Window(themename="sandstone") app = MarkdownConverterApp(root) root.mainloop()