SXXXXXXX_MarkdownConverter/markdownconverter/gui/gui.py
2025-06-18 08:09:24 +02:00

310 lines
14 KiB
Python

# markdownconverter/gui/gui.py
import os
import sys
import logging
import subprocess
import tkinter as tk
from datetime import date
import ttkbootstrap as tb
from tkinter.scrolledtext import ScrolledText
from ttkbootstrap.constants import *
from tkinter import filedialog, messagebox, StringVar, BooleanVar
from ..core.core import convert_markdown, convert_docx_to_pdf, ConverterNotFoundError
from ..utils.config import save_configuration, load_configuration
from ..utils.logger import (
setup_basic_logging,
add_tkinter_handler,
shutdown_logging_system,
get_logger
)
from .editor import EditorWindow
log = get_logger(__name__)
def open_with_default_app(filepath):
if not filepath or not os.path.exists(filepath):
log.warning("Open file/folder requested, but path is invalid or not provided.")
messagebox.showwarning("Warning", "No output file or folder to open.")
return
try:
log.info(f"Opening '{filepath}' with default application.")
if sys.platform == "win32":
os.startfile(filepath)
elif sys.platform == "darwin":
subprocess.run(["open", filepath], check=True)
else:
subprocess.run(["xdg-open", filepath], check=True)
except Exception as e:
log.error(f"Failed to open '{filepath}': {e}", exc_info=True)
messagebox.showerror("Error", f"Could not open the file/folder:\n{str(e)}")
def open_output_folder(filepath):
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 = os.path.dirname(filepath)
open_with_default_app(folder)
def run_app():
app = tb.Window(themename="sandstone")
app.title("Markdown Converter")
app.geometry("900x850")
app.resizable(True, True)
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=app, logging_config_dict=log_config)
config = load_configuration()
metadata_config = config.get("metadata", {})
# --- Variabili di stato della GUI ---
selected_file = StringVar(value=config.get("last_markdown_file", ""))
selected_template = StringVar(value=config.get("last_template_file", ""))
add_toc_var = BooleanVar(value=config.get("add_toc", True))
doc_security_var = StringVar(value=metadata_config.get("DOC_SECURITY", ""))
doc_number_var = StringVar(value=metadata_config.get("DOC_NUMBER", ""))
doc_rev_var = StringVar(value=metadata_config.get("DOC_REV", ""))
doc_project_var = StringVar(value=metadata_config.get("DOC_PROJECT", ""))
customer_var = StringVar(value=metadata_config.get("DOC_CUSTOMER", ""))
docx_output_path = StringVar()
pdf_direct_output_path = StringVar()
pdf_from_docx_output_path = StringVar()
# --- Funzioni di supporto ---
def _confirm_overwrite(filepath: str) -> bool:
"""Checks if a file exists and asks the user for overwrite confirmation."""
if os.path.exists(filepath):
return messagebox.askyesno(
title="Confirm Overwrite",
message=f"The file already exists:\n\n{filepath}\n\nDo you want to overwrite it?"
)
return True # File doesn't exist, proceed.
def _update_output_paths(*args):
source_path = selected_file.get()
if not source_path:
return
output_dir = os.path.dirname(source_path)
project = doc_project_var.get() or "NoProject"
doc_num = doc_number_var.get() or "NoNum"
rev = doc_rev_var.get() or "NoRev"
today = date.today().strftime("%Y%m%d")
docx_filename = f"{project}_SUM_{doc_num}_{rev}_{today}.docx"
docx_output_path.set(os.path.join(output_dir, docx_filename).replace("\\", "/"))
pdf_from_docx_filename = f"{project}_SUM_{doc_num}_{rev}_{today}.pdf"
pdf_from_docx_output_path.set(os.path.join(output_dir, pdf_from_docx_filename).replace("\\", "/"))
pdf_direct_filename = f"{project}_SUM_{today}.pdf"
pdf_direct_output_path.set(os.path.join(output_dir, pdf_direct_filename).replace("\\", "/"))
for var in [selected_file, doc_project_var, doc_number_var, doc_rev_var]:
var.trace_add("write", _update_output_paths)
# --- Funzioni associate agli eventi della GUI ---
def open_editor():
file_path = selected_file.get()
if not file_path or not os.path.exists(file_path):
messagebox.showerror("Error", "Please select a valid Markdown file first.")
return
EditorWindow(parent=app, file_path=file_path)
def browse_markdown():
path = filedialog.askopenfilename(filetypes=[("Markdown files", "*.md"), ("All files", "*.*")])
if path:
selected_file.set(path)
def browse_template():
path = filedialog.askopenfilename(
title="Select a Template Document",
filetypes=[("Word Documents", "*.docx"), ("All files", "*.*")]
)
if path:
selected_template.set(path)
edit_template_btn.config(state=tk.NORMAL)
docx_to_pdf_btn = None
def convert_from_docx_to_pdf():
input_path = docx_output_path.get()
output_path = pdf_from_docx_output_path.get()
if not input_path or not os.path.exists(input_path):
messagebox.showerror("Error", "The 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 _confirm_overwrite(output_path):
return # User cancelled overwrite
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 convert(fmt):
nonlocal docx_to_pdf_btn
if docx_to_pdf_btn:
docx_to_pdf_btn.config(state=tk.DISABLED)
input_path = selected_file.get()
if not input_path:
messagebox.showerror("Error", "Please select a Markdown file.")
return
template = selected_template.get()
if fmt == "DOCX" and not template:
messagebox.showwarning("Warning", "A DOCX template is required.")
return
output_path_for_conversion = ""
if fmt == "DOCX":
output_path_for_conversion = docx_output_path.get()
elif fmt == "PDF":
output_path_for_conversion = pdf_direct_output_path.get()
if not output_path_for_conversion:
messagebox.showerror("Error", "Please specify a valid output path.")
return
if not _confirm_overwrite(output_path_for_conversion):
return # User cancelled overwrite
metadata_to_pass = {
'DOC_SECURITY': doc_security_var.get(),
'DOC_NUMBER': doc_number_var.get(),
'DOC_REV': doc_rev_var.get(),
'DOC_PROJECT': doc_project_var.get(),
'DOC_CUSTOMER': customer_var.get(),
}
try:
output = convert_markdown(
input_file=input_path,
output_path=output_path_for_conversion, # Pass the final path to the core
output_format=fmt,
add_toc=add_toc_var.get(),
template_path=template,
metadata=metadata_to_pass,
)
messagebox.showinfo("Success", f"File converted successfully:\n{output}")
save_configuration(
last_markdown=input_path, last_template=template,
add_toc=add_toc_var.get(), metadata=metadata_to_pass
)
if fmt == "DOCX" and docx_to_pdf_btn:
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)}")
# --- Costruzione della GUI ---
container = tb.Frame(app, padding=10)
container.pack(fill=tk.BOTH, expand=True)
input_frame = tb.Labelframe(container, text="Input Files", padding=10)
input_frame.pack(fill=tk.X, side=tk.TOP, pady=(0, 10))
input_frame.columnconfigure(1, weight=1)
tb.Label(input_frame, text="Markdown File:").grid(row=0, column=0, padx=(0, 10), pady=5, sticky="w")
tb.Entry(input_frame, textvariable=selected_file).grid(row=0, column=1, padx=5, pady=5, sticky="ew")
file_button_frame = tb.Frame(input_frame)
file_button_frame.grid(row=0, column=2, sticky="w")
tb.Button(file_button_frame, text="Browse...", command=browse_markdown, bootstyle=PRIMARY).pack(side=tk.LEFT, padx=(0, 5))
tb.Button(file_button_frame, text="Edit...", command=open_editor, bootstyle=INFO).pack(side=tk.LEFT)
tb.Label(input_frame, text="Template File:").grid(row=1, column=0, padx=(0, 10), pady=5, sticky="w")
tb.Entry(input_frame, textvariable=selected_template).grid(row=1, column=1, padx=5, pady=5, sticky="ew")
template_button_frame = tb.Frame(input_frame)
template_button_frame.grid(row=1, column=2, sticky="w")
tb.Button(template_button_frame, text="Browse...", command=browse_template, bootstyle=SECONDARY).pack(side=tk.LEFT, padx=(0, 5))
edit_template_btn = tb.Button(template_button_frame, text="Edit...", command=lambda: open_with_default_app(selected_template.get()), bootstyle="info", state=tk.DISABLED)
edit_template_btn.pack(side=tk.LEFT)
metadata_frame = tb.Labelframe(container, text="Template Placeholders", padding=10)
metadata_frame.pack(fill=tk.X, side=tk.TOP, pady=(0, 10))
metadata_frame.columnconfigure(1, weight=1)
metadata_frame.columnconfigure(3, weight=1)
tb.Label(metadata_frame, text="Project Name:").grid(row=0, column=0, padx=5, pady=3, sticky="w")
tb.Entry(metadata_frame, textvariable=doc_project_var).grid(row=0, column=1, padx=5, pady=3, sticky="ew")
tb.Label(metadata_frame, text="Customer:").grid(row=0, column=2, padx=(10, 5), pady=3, sticky="w")
tb.Entry(metadata_frame, textvariable=customer_var).grid(row=0, column=3, padx=5, pady=3, sticky="ew")
tb.Label(metadata_frame, text="Document Number:").grid(row=1, column=0, padx=5, pady=3, sticky="w")
tb.Entry(metadata_frame, textvariable=doc_number_var).grid(row=1, column=1, padx=5, pady=3, sticky="ew")
tb.Label(metadata_frame, text="Revision:").grid(row=1, column=2, padx=(10, 5), pady=3, sticky="w")
tb.Entry(metadata_frame, textvariable=doc_rev_var).grid(row=1, column=3, padx=5, pady=3, sticky="ew")
tb.Label(metadata_frame, text="Security Class:").grid(row=2, column=0, padx=5, pady=3, sticky="w")
tb.Entry(metadata_frame, textvariable=doc_security_var).grid(row=2, column=1, padx=5, pady=3, sticky="ew")
options_frame = tb.Frame(container)
options_frame.pack(fill=tk.X, pady=(0, 10))
tb.Checkbutton(options_frame, text="Add Table of Contents", variable=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(selected_file.get()), bootstyle=WARNING).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: convert("DOCX"), bootstyle=SUCCESS, width=12).grid(row=0, column=0, padx=5, pady=5)
tb.Entry(action_frame, textvariable=docx_output_path).grid(row=0, column=1, padx=5, pady=5, sticky="ew")
tb.Button(action_frame, text="Open", command=lambda: open_with_default_app(docx_output_path.get()), bootstyle="secondary").grid(row=0, column=2, padx=5, pady=5)
tb.Button(action_frame, text="MD -> PDF", command=lambda: convert("PDF"), bootstyle=SUCCESS, width=12).grid(row=1, column=0, padx=5, pady=5)
tb.Entry(action_frame, textvariable=pdf_direct_output_path).grid(row=1, column=1, padx=5, pady=5, sticky="ew")
tb.Button(action_frame, text="Open", command=lambda: open_with_default_app(pdf_direct_output_path.get()), bootstyle="secondary").grid(row=1, column=2, padx=5, pady=5)
docx_to_pdf_btn = tb.Button(action_frame, text="DOCX -> PDF", command=convert_from_docx_to_pdf, bootstyle="info", width=12, state=tk.DISABLED)
docx_to_pdf_btn.grid(row=2, column=0, padx=5, pady=5)
tb.Entry(action_frame, textvariable=pdf_from_docx_output_path).grid(row=2, column=1, padx=5, pady=5, sticky="ew")
tb.Button(action_frame, text="Open", command=lambda: open_with_default_app(pdf_from_docx_output_path.get()), bootstyle="secondary").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)
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=app, logging_config_dict=log_config)
_update_output_paths()
if selected_template.get():
edit_template_btn.config(state=tk.NORMAL)
def on_closing():
shutdown_logging_system()
app.destroy()
app.protocol("WM_DELETE_WINDOW", on_closing)
log.info("Application GUI initialized successfully.")
app.mainloop()