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:
@@ -38,12 +38,16 @@ dev = [
|
|||||||
"ruff>=0.1.0",
|
"ruff>=0.1.0",
|
||||||
"mypy>=1.0.0",
|
"mypy>=1.0.0",
|
||||||
]
|
]
|
||||||
|
gui = [
|
||||||
|
"customtkinter>=5.2.0",
|
||||||
|
]
|
||||||
pdf = [
|
pdf = [
|
||||||
"pandoc", # For PDF generation fallback
|
"pandoc", # For PDF generation fallback
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
cad-doc = "cad_documenter.cli:main"
|
cad-doc = "cad_documenter.cli:main"
|
||||||
|
cad-doc-gui = "cad_documenter.gui:main"
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
Homepage = "http://100.80.199.40:3000/Antoine/CAD-Documenter"
|
Homepage = "http://100.80.199.40:3000/Antoine/CAD-Documenter"
|
||||||
|
|||||||
312
src/cad_documenter/gui.py
Normal file
312
src/cad_documenter/gui.py
Normal 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()
|
||||||
Reference in New Issue
Block a user