Add Windows GUI for CAD-Documenter

- New gui.py with CustomTkinter interface
- Project create/open
- Video list management
- Process with export-only option
- Progress feedback
- Entry point: cad-doc-gui

Install GUI deps: uv pip install customtkinter
This commit is contained in:
Mario Lavoie
2026-01-28 02:53:16 +00:00
parent 2c517addc5
commit 5fbd744cca
2 changed files with 316 additions and 0 deletions

View File

@@ -38,12 +38,16 @@ dev = [
"ruff>=0.1.0",
"mypy>=1.0.0",
]
gui = [
"customtkinter>=5.2.0",
]
pdf = [
"pandoc", # For PDF generation fallback
]
[project.scripts]
cad-doc = "cad_documenter.cli:main"
cad-doc-gui = "cad_documenter.gui:main"
[project.urls]
Homepage = "http://100.80.199.40:3000/Antoine/CAD-Documenter"

312
src/cad_documenter/gui.py Normal file
View File

@@ -0,0 +1,312 @@
"""CAD-Documenter GUI - Windows interface for project management."""
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from pathlib import Path
import threading
import json
try:
import customtkinter as ctk
ctk.set_appearance_mode("dark")
ctk.set_default_color_theme("blue")
USE_CTK = True
except ImportError:
USE_CTK = False
ctk = tk # Fallback to regular tkinter
from .project import Project
from .incremental import IncrementalProcessor
from .config import load_config
class CADDocumenterGUI:
"""Main GUI application."""
def __init__(self):
self.root = ctk.CTk() if USE_CTK else tk.Tk()
self.root.title("CAD Documenter")
self.root.geometry("700x600")
self.current_project: Project | None = None
self.config = load_config()
self._create_widgets()
self._update_project_list()
def _create_widgets(self):
"""Create the main UI widgets."""
# Main container
main_frame = ctk.CTkFrame(self.root) if USE_CTK else ttk.Frame(self.root)
main_frame.pack(fill="both", expand=True, padx=10, pady=10)
# Project selection
project_frame = ctk.CTkFrame(main_frame) if USE_CTK else ttk.LabelFrame(main_frame, text="Project")
project_frame.pack(fill="x", pady=(0, 10))
if USE_CTK:
ctk.CTkLabel(project_frame, text="Project:").pack(side="left", padx=5)
else:
ttk.Label(project_frame, text="Project:").pack(side="left", padx=5)
self.project_var = tk.StringVar()
self.project_combo = ctk.CTkComboBox(
project_frame,
variable=self.project_var,
values=[],
command=self._on_project_selected
) if USE_CTK else ttk.Combobox(project_frame, textvariable=self.project_var)
self.project_combo.pack(side="left", fill="x", expand=True, padx=5)
if not USE_CTK:
self.project_combo.bind("<<ComboboxSelected>>", lambda e: self._on_project_selected(None))
btn_new = ctk.CTkButton(project_frame, text="+ New", width=70, command=self._new_project) if USE_CTK else ttk.Button(project_frame, text="+ New", command=self._new_project)
btn_new.pack(side="left", padx=5)
btn_open = ctk.CTkButton(project_frame, text="Open", width=70, command=self._open_project) if USE_CTK else ttk.Button(project_frame, text="Open", command=self._open_project)
btn_open.pack(side="left", padx=5)
# Videos list
videos_frame = ctk.CTkFrame(main_frame) if USE_CTK else ttk.LabelFrame(main_frame, text="Videos")
videos_frame.pack(fill="both", expand=True, pady=(0, 10))
if USE_CTK:
ctk.CTkLabel(videos_frame, text="Videos:").pack(anchor="w", padx=5, pady=5)
# Listbox for videos
list_frame = ctk.CTkFrame(videos_frame) if USE_CTK else ttk.Frame(videos_frame)
list_frame.pack(fill="both", expand=True, padx=5, pady=5)
self.video_listbox = tk.Listbox(list_frame, height=10, selectmode="extended")
self.video_listbox.pack(side="left", fill="both", expand=True)
scrollbar = ttk.Scrollbar(list_frame, orient="vertical", command=self.video_listbox.yview)
scrollbar.pack(side="right", fill="y")
self.video_listbox.config(yscrollcommand=scrollbar.set)
# Video buttons
video_btn_frame = ctk.CTkFrame(videos_frame) if USE_CTK else ttk.Frame(videos_frame)
video_btn_frame.pack(fill="x", padx=5, pady=5)
btn_add = ctk.CTkButton(video_btn_frame, text="Add Videos", command=self._add_videos) if USE_CTK else ttk.Button(video_btn_frame, text="Add Videos", command=self._add_videos)
btn_add.pack(side="left", padx=5)
btn_remove = ctk.CTkButton(video_btn_frame, text="Remove", command=self._remove_video) if USE_CTK else ttk.Button(video_btn_frame, text="Remove", command=self._remove_video)
btn_remove.pack(side="left", padx=5)
# Drop zone hint
if USE_CTK:
ctk.CTkLabel(video_btn_frame, text="(Drag & drop videos here)", text_color="gray").pack(side="right", padx=5)
# Options
options_frame = ctk.CTkFrame(main_frame) if USE_CTK else ttk.LabelFrame(main_frame, text="Options")
options_frame.pack(fill="x", pady=(0, 10))
if USE_CTK:
ctk.CTkLabel(options_frame, text="Options:").pack(anchor="w", padx=5, pady=5)
self.export_only_var = tk.BooleanVar(value=True)
cb_export = ctk.CTkCheckBox(options_frame, text="Export for Clawdbot (no API)", variable=self.export_only_var) if USE_CTK else ttk.Checkbutton(options_frame, text="Export for Clawdbot (no API)", variable=self.export_only_var)
cb_export.pack(anchor="w", padx=20, pady=2)
self.scene_detect_var = tk.BooleanVar(value=True)
cb_scene = ctk.CTkCheckBox(options_frame, text="Use scene detection", variable=self.scene_detect_var) if USE_CTK else ttk.Checkbutton(options_frame, text="Use scene detection", variable=self.scene_detect_var)
cb_scene.pack(anchor="w", padx=20, pady=2)
self.transcribe_var = tk.BooleanVar(value=True)
cb_trans = ctk.CTkCheckBox(options_frame, text="Whisper transcription", variable=self.transcribe_var) if USE_CTK else ttk.Checkbutton(options_frame, text="Whisper transcription", variable=self.transcribe_var)
cb_trans.pack(anchor="w", padx=20, pady=2)
# Process button
self.btn_process = ctk.CTkButton(
main_frame,
text="Process Videos",
height=40,
command=self._process_videos
) if USE_CTK else ttk.Button(main_frame, text="Process Videos", command=self._process_videos)
self.btn_process.pack(fill="x", pady=(0, 10))
# Status
status_frame = ctk.CTkFrame(main_frame) if USE_CTK else ttk.Frame(main_frame)
status_frame.pack(fill="x")
self.status_var = tk.StringVar(value="Ready")
status_label = ctk.CTkLabel(status_frame, textvariable=self.status_var) if USE_CTK else ttk.Label(status_frame, textvariable=self.status_var)
status_label.pack(side="left", padx=5)
self.progress = ctk.CTkProgressBar(status_frame) if USE_CTK else ttk.Progressbar(status_frame, mode="indeterminate")
self.progress.pack(side="right", fill="x", expand=True, padx=5)
if USE_CTK:
self.progress.set(0)
def _update_project_list(self):
"""Update the project dropdown with recent projects."""
# For now, just clear - could load from config/history
pass
def _on_project_selected(self, value):
"""Handle project selection."""
self._refresh_video_list()
def _new_project(self):
"""Create a new project."""
folder = filedialog.askdirectory(title="Select folder for new project")
if not folder:
return
name = Path(folder).name
# Simple dialog for project name
dialog = ctk.CTkInputDialog(
text="Enter project name:",
title="New Project"
) if USE_CTK else None
if USE_CTK:
name = dialog.get_input() or name
try:
self.current_project = Project.create(Path(folder), name, "")
self.project_var.set(f"{name} ({folder})")
self.status_var.set(f"Created project: {name}")
self._refresh_video_list()
except Exception as e:
messagebox.showerror("Error", str(e))
def _open_project(self):
"""Open an existing project."""
folder = filedialog.askdirectory(title="Select project folder")
if not folder:
return
try:
self.current_project = Project.load(Path(folder))
self.project_var.set(f"{self.current_project.manifest.name} ({folder})")
self.status_var.set(f"Loaded project: {self.current_project.manifest.name}")
self._refresh_video_list()
except Exception as e:
messagebox.showerror("Error", f"Failed to load project: {e}")
def _refresh_video_list(self):
"""Refresh the video listbox."""
self.video_listbox.delete(0, tk.END)
if not self.current_project:
return
for video in self.current_project.manifest.videos:
status_icon = {
"pending": "",
"processed": "",
"exported": "📤",
"error": ""
}.get(video.status, "?")
self.video_listbox.insert(tk.END, f"{status_icon} {video.filename}")
def _add_videos(self):
"""Add videos to the project."""
if not self.current_project:
messagebox.showwarning("Warning", "Please create or open a project first")
return
files = filedialog.askopenfilenames(
title="Select videos",
filetypes=[
("Video files", "*.mp4 *.mkv *.avi *.mov *.webm"),
("All files", "*.*")
]
)
for f in files:
try:
self.current_project.add_video(Path(f), copy=True)
except Exception as e:
messagebox.showerror("Error", f"Failed to add {f}: {e}")
self._refresh_video_list()
self.status_var.set(f"Added {len(files)} video(s)")
def _remove_video(self):
"""Remove selected video from project."""
# TODO: Implement video removal
messagebox.showinfo("Info", "Video removal not yet implemented")
def _process_videos(self):
"""Process videos in background thread."""
if not self.current_project:
messagebox.showwarning("Warning", "Please create or open a project first")
return
pending = self.current_project.get_pending_videos()
if not pending:
messagebox.showinfo("Info", "No pending videos to process")
return
# Disable button during processing
self.btn_process.configure(state="disabled")
self.status_var.set("Processing...")
if USE_CTK:
self.progress.set(0)
self.progress.start()
else:
self.progress.start()
def process_thread():
try:
processor = IncrementalProcessor(self.current_project, self.config)
results = processor.process_pending(
export_only=self.export_only_var.get()
)
self.root.after(0, lambda: self._on_process_complete(results))
except Exception as e:
self.root.after(0, lambda: self._on_process_error(str(e)))
thread = threading.Thread(target=process_thread, daemon=True)
thread.start()
def _on_process_complete(self, results):
"""Handle processing completion."""
if USE_CTK:
self.progress.stop()
self.progress.set(1)
else:
self.progress.stop()
self.btn_process.configure(state="normal")
self._refresh_video_list()
msg = f"Processed {results.get('processed', 0)} video(s)"
if self.export_only_var.get():
msg += f"\n\nExports ready! Tell Clawdbot:\n\"Process CAD report for {self.current_project.manifest.name}\""
self.status_var.set("Complete!")
messagebox.showinfo("Success", msg)
def _on_process_error(self, error):
"""Handle processing error."""
if USE_CTK:
self.progress.stop()
self.progress.set(0)
else:
self.progress.stop()
self.btn_process.configure(state="normal")
self.status_var.set("Error")
messagebox.showerror("Error", f"Processing failed: {error}")
def run(self):
"""Run the application."""
self.root.mainloop()
def main():
"""Entry point for GUI."""
app = CADDocumenterGUI()
app.run()
if __name__ == "__main__":
main()