diff --git a/pyproject.toml b/pyproject.toml index e70db6d..b3dd238 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/src/cad_documenter/gui.py b/src/cad_documenter/gui.py new file mode 100644 index 0000000..4a6daf3 --- /dev/null +++ b/src/cad_documenter/gui.py @@ -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("<>", 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()