#!/usr/bin/env python3 """ Unified Dashboard GUI for PunimTag features Designed with web migration in mind - single window with menu bar and content area """ import os import threading import tkinter as tk from tkinter import ttk, messagebox from typing import Dict, Optional, Callable from gui_core import GUICore from identify_panel import IdentifyPanel class DashboardGUI: """Unified Dashboard with menu bar and content area for all features. Designed to be web-migration friendly with clear separation between navigation (menu bar) and content (panels). """ def __init__(self, gui_core: GUICore, db_manager=None, face_processor=None, on_scan=None, on_process=None, on_identify=None): self.gui_core = gui_core self.db_manager = db_manager self.face_processor = face_processor self.on_scan = on_scan self.on_process = on_process self.on_identify = on_identify # Panel management for future web migration self.panels: Dict[str, ttk.Frame] = {} self.current_panel: Optional[str] = None self.root: Optional[tk.Tk] = None def open(self) -> int: """Open the unified dashboard with menu bar and content area""" self.root = tk.Tk() self.root.title("PunimTag - Unified Dashboard") self.root.resizable(True, True) self.root.withdraw() # Make window full screen - use geometry instead of state for better compatibility try: # Try Windows-style maximized state first self.root.state('zoomed') except tk.TclError: try: # Try Linux-style maximized attribute self.root.attributes('-zoomed', True) except tk.TclError: # Fallback: set geometry to screen size screen_width = self.root.winfo_screenwidth() screen_height = self.root.winfo_screenheight() self.root.geometry(f"{screen_width}x{screen_height}+0+0") # Get screen dimensions for dynamic sizing screen_width = self.root.winfo_screenwidth() screen_height = self.root.winfo_screenheight() # Set minimum window size self.root.minsize(800, 600) # Create main container with proper grid configuration main_container = ttk.Frame(self.root) main_container.pack(fill=tk.BOTH, expand=True) # Configure main container grid weights for responsiveness main_container.columnconfigure(0, weight=1) main_container.rowconfigure(0, weight=0) # Menu bar - fixed height main_container.rowconfigure(1, weight=1) # Content area - expandable # Add window resize handler for dynamic responsiveness self.root.bind('', self._on_window_resize) # Create menu bar self._create_menu_bar(main_container) # Create content area self._create_content_area(main_container) # Initialize panels self._initialize_panels() # Show default panel self.show_panel("home") # Show window self.root.deiconify() self.root.mainloop() return 0 def _create_menu_bar(self, parent: ttk.Frame): """Create the top menu bar with all functionality buttons""" menu_frame = ttk.Frame(parent) menu_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), padx=15, pady=10) menu_frame.columnconfigure(0, weight=0) # Title - fixed width menu_frame.columnconfigure(1, weight=1) # Buttons - expandable menu_frame.columnconfigure(2, weight=0) # Status - fixed width # Title with larger font for full screen title_label = tk.Label(menu_frame, text="PunimTag", font=("Arial", 20, "bold")) title_label.grid(row=0, column=0, padx=(0, 30), sticky=tk.W) # Create buttons frame for better organization buttons_frame = ttk.Frame(menu_frame) buttons_frame.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=20) # Menu buttons with larger size for full screen menu_buttons = [ ("📁 Scan", "scan", "Scan folders and add photos"), ("🔍 Process", "process", "Detect faces in photos"), ("👤 Identify", "identify", "Identify faces in photos"), ("🔗 Auto-Match", "auto_match", "Find and confirm matching faces"), ("🔎 Search", "search", "Search photos by person name"), ("✏️ Modify", "modify", "View and modify identified faces"), ("🏷️ Tags", "tags", "Manage photo tags"), ] for i, (text, panel_name, tooltip) in enumerate(menu_buttons): btn = ttk.Button( buttons_frame, text=text, command=lambda p=panel_name: self.show_panel(p), width=12 # Fixed width for consistent layout ) btn.grid(row=0, column=i, padx=3, sticky=tk.W) # Add tooltip functionality self._add_tooltip(btn, tooltip) # Status/Info area with better styling status_frame = ttk.Frame(menu_frame) status_frame.grid(row=0, column=2, sticky=tk.E, padx=(20, 0)) self.status_label = tk.Label(status_frame, text="Ready", foreground="#666", font=("Arial", 10)) self.status_label.pack(side=tk.RIGHT) # Add a subtle separator line below the menu separator = ttk.Separator(parent, orient='horizontal') separator.grid(row=1, column=0, sticky=(tk.W, tk.E), padx=15, pady=(0, 5)) def _create_content_area(self, parent: ttk.Frame): """Create the main content area where panels will be displayed""" self.content_frame = ttk.Frame(parent) self.content_frame.grid(row=2, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=15, pady=(0, 15)) # Configure content frame to expand self.content_frame.columnconfigure(0, weight=1) self.content_frame.rowconfigure(0, weight=1) # Add a subtle border self.content_frame.configure(relief='sunken', borderwidth=1) def _initialize_panels(self): """Initialize all panels (currently placeholders)""" # Home panel (default) self.panels["home"] = self._create_home_panel() # Functional panels (placeholders for now) self.panels["scan"] = self._create_scan_panel() self.panels["process"] = self._create_process_panel() self.panels["identify"] = self._create_identify_panel() self.panels["auto_match"] = self._create_auto_match_panel() self.panels["search"] = self._create_search_panel() self.panels["modify"] = self._create_modify_panel() self.panels["tags"] = self._create_tags_panel() def show_panel(self, panel_name: str): """Show the specified panel in the content area""" if panel_name not in self.panels: messagebox.showerror("Error", f"Panel '{panel_name}' not found", parent=self.root) return # Hide current panel if self.current_panel: self.panels[self.current_panel].grid_remove() # Show new panel self.panels[panel_name].grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=15, pady=15) self.current_panel = panel_name # Update status self.status_label.config(text=f"Viewing: {panel_name.replace('_', ' ').title()}") def _add_tooltip(self, widget, text): """Add a simple tooltip to a widget""" def show_tooltip(event): tooltip = tk.Toplevel() tooltip.wm_overrideredirect(True) tooltip.wm_geometry(f"+{event.x_root+10}+{event.y_root+10}") label = ttk.Label(tooltip, text=text, background="#ffffe0", relief="solid", borderwidth=1) label.pack() widget.tooltip = tooltip def hide_tooltip(event): if hasattr(widget, 'tooltip'): widget.tooltip.destroy() del widget.tooltip widget.bind("", show_tooltip) widget.bind("", hide_tooltip) def _on_window_resize(self, event): """Handle window resize events for dynamic responsiveness""" # Only handle resize events for the main window, not child widgets if event.widget == self.root: # Update status with current window size width = self.root.winfo_width() height = self.root.winfo_height() self.status_label.config(text=f"Ready - {width}x{height}") # Force update of all panels to ensure proper resizing if hasattr(self, 'identify_panel') and self.identify_panel: # Update identify panel layout if it's active if self.current_panel == "identify": self.identify_panel.update_layout() def _create_home_panel(self) -> ttk.Frame: """Create the home/welcome panel""" panel = ttk.Frame(self.content_frame) # Configure panel grid for responsiveness panel.columnconfigure(0, weight=1) panel.rowconfigure(0, weight=1) # Welcome content welcome_frame = ttk.Frame(panel) welcome_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=20, pady=20) welcome_frame.columnconfigure(0, weight=1) welcome_frame.rowconfigure(0, weight=1) # Center the content center_frame = ttk.Frame(welcome_frame) center_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) center_frame.columnconfigure(0, weight=1) # Title with larger font for full screen title_label = tk.Label(center_frame, text="Welcome to PunimTag", font=("Arial", 32, "bold")) title_label.grid(row=0, column=0, pady=(0, 30)) # Description with larger font desc_text = ( "PunimTag is a powerful photo face recognition and tagging system.\n\n" "Use the menu above to access different features:\n\n" "• 📁 Scan - Add photos to your collection\n" "• 🔍 Process - Detect faces in photos\n" "• 👤 Identify - Identify people in photos\n" "• 🔗 Auto-Match - Find matching faces automatically\n" "• 🔎 Search - Find photos by person name\n" "• ✏️ Modify - Edit face identifications\n" "• 🏷️ Tags - Manage photo tags\n\n" "Select a feature from the menu to get started!" ) desc_label = tk.Label(center_frame, text=desc_text, font=("Arial", 14), justify=tk.LEFT) desc_label.grid(row=1, column=0, pady=(0, 20)) return panel def _create_scan_panel(self) -> ttk.Frame: """Create the scan panel (migrated from original dashboard)""" panel = ttk.Frame(self.content_frame) # Configure panel grid for responsiveness panel.columnconfigure(0, weight=1) panel.rowconfigure(1, weight=1) # Title with larger font for full screen title_label = tk.Label(panel, text="📁 Scan Photos", font=("Arial", 24, "bold")) title_label.grid(row=0, column=0, sticky=tk.W, pady=(0, 20)) # Scan form form_frame = ttk.LabelFrame(panel, text="Scan Configuration", padding="20") form_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 20)) form_frame.columnconfigure(0, weight=1) # Folder selection folder_frame = ttk.Frame(form_frame) folder_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=(0, 15)) folder_frame.columnconfigure(0, weight=1) tk.Label(folder_frame, text="Folder to scan:", font=("Arial", 12)).grid(row=0, column=0, sticky=tk.W) folder_input_frame = ttk.Frame(folder_frame) folder_input_frame.grid(row=1, column=0, sticky=(tk.W, tk.E), pady=(5, 0)) folder_input_frame.columnconfigure(0, weight=1) self.folder_var = tk.StringVar() folder_entry = tk.Entry(folder_input_frame, textvariable=self.folder_var, font=("Arial", 11)) folder_entry.grid(row=0, column=0, sticky=(tk.W, tk.E), padx=(0, 10)) def browse_folder(): from tkinter import filedialog folder_path = filedialog.askdirectory(title="Select folder to scan for photos") if folder_path: self.folder_var.set(folder_path) browse_btn = ttk.Button(folder_input_frame, text="Browse", command=browse_folder) browse_btn.grid(row=0, column=1) # Recursive option self.recursive_var = tk.BooleanVar(value=True) recursive_check = tk.Checkbutton( form_frame, text="Include photos in sub-folders", variable=self.recursive_var, font=("Arial", 11) ) recursive_check.grid(row=1, column=0, sticky=tk.W, pady=(15, 0)) # Action button scan_btn = ttk.Button(form_frame, text="🔍 Start Scan", command=self._run_scan) scan_btn.grid(row=2, column=0, sticky=tk.W, pady=(20, 0)) return panel def _create_process_panel(self) -> ttk.Frame: """Create the process panel (migrated from original dashboard)""" panel = ttk.Frame(self.content_frame) # Configure panel grid for responsiveness panel.columnconfigure(0, weight=1) panel.rowconfigure(1, weight=1) # Title with larger font for full screen title_label = tk.Label(panel, text="🔍 Process Faces", font=("Arial", 24, "bold")) title_label.grid(row=0, column=0, sticky=tk.W, pady=(0, 20)) # Process form form_frame = ttk.LabelFrame(panel, text="Processing Configuration", padding="20") form_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 20)) form_frame.columnconfigure(0, weight=1) # Limit option limit_frame = ttk.Frame(form_frame) limit_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=(0, 15)) self.limit_enabled = tk.BooleanVar(value=False) limit_check = tk.Checkbutton(limit_frame, text="Limit processing to", variable=self.limit_enabled, font=("Arial", 11)) limit_check.grid(row=0, column=0, sticky=tk.W) self.limit_var = tk.StringVar(value="50") limit_entry = tk.Entry(limit_frame, textvariable=self.limit_var, width=8, font=("Arial", 11)) limit_entry.grid(row=0, column=1, padx=(10, 5)) tk.Label(limit_frame, text="photos", font=("Arial", 11)).grid(row=0, column=2, sticky=tk.W) # Action button process_btn = ttk.Button(form_frame, text="🚀 Start Processing", command=self._run_process) process_btn.grid(row=1, column=0, sticky=tk.W, pady=(20, 0)) return panel def _create_identify_panel(self) -> ttk.Frame: """Create the identify panel with full functionality""" panel = ttk.Frame(self.content_frame) # Configure panel grid for responsiveness panel.columnconfigure(0, weight=1) panel.rowconfigure(1, weight=1) # Title with larger font for full screen title_label = tk.Label(panel, text="👤 Identify Faces", font=("Arial", 24, "bold")) title_label.grid(row=0, column=0, sticky=tk.W, pady=(0, 20)) # Create the identify panel if we have the required dependencies if self.db_manager and self.face_processor: self.identify_panel = IdentifyPanel(panel, self.db_manager, self.face_processor, self.gui_core) identify_frame = self.identify_panel.create_panel() identify_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) else: # Fallback placeholder if dependencies are not available placeholder_frame = ttk.LabelFrame(panel, text="Configuration Required", padding="20") placeholder_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) placeholder_frame.columnconfigure(0, weight=1) placeholder_frame.rowconfigure(0, weight=1) placeholder_text = ( "Identify panel requires database and face processor to be configured.\n\n" "This will contain the full face identification interface\n" "currently available in the separate Identify window.\n\n" "Features will include:\n" "• Face browsing and identification\n" "• Similar face matching\n" "• Person management\n" "• Batch processing options" ) placeholder_label = tk.Label(placeholder_frame, text=placeholder_text, font=("Arial", 14), justify=tk.LEFT) placeholder_label.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) return panel def _create_auto_match_panel(self) -> ttk.Frame: """Create the auto-match panel (placeholder)""" panel = ttk.Frame(self.content_frame) # Configure panel grid for responsiveness panel.columnconfigure(0, weight=1) panel.rowconfigure(1, weight=1) title_label = tk.Label(panel, text="🔗 Auto-Match Faces", font=("Arial", 24, "bold")) title_label.grid(row=0, column=0, sticky=tk.W, pady=(0, 20)) placeholder_frame = ttk.LabelFrame(panel, text="Coming Soon", padding="20") placeholder_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) placeholder_frame.columnconfigure(0, weight=1) placeholder_frame.rowconfigure(0, weight=1) placeholder_text = "Auto-Match functionality will be integrated here." placeholder_label = tk.Label(placeholder_frame, text=placeholder_text, font=("Arial", 14)) placeholder_label.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) return panel def _create_search_panel(self) -> ttk.Frame: """Create the search panel (placeholder)""" panel = ttk.Frame(self.content_frame) # Configure panel grid for responsiveness panel.columnconfigure(0, weight=1) panel.rowconfigure(1, weight=1) title_label = tk.Label(panel, text="🔎 Search Photos", font=("Arial", 24, "bold")) title_label.grid(row=0, column=0, sticky=tk.W, pady=(0, 20)) placeholder_frame = ttk.LabelFrame(panel, text="Coming Soon", padding="20") placeholder_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) placeholder_frame.columnconfigure(0, weight=1) placeholder_frame.rowconfigure(0, weight=1) placeholder_text = "Search functionality will be integrated here." placeholder_label = tk.Label(placeholder_frame, text=placeholder_text, font=("Arial", 14)) placeholder_label.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) return panel def _create_modify_panel(self) -> ttk.Frame: """Create the modify panel (placeholder)""" panel = ttk.Frame(self.content_frame) # Configure panel grid for responsiveness panel.columnconfigure(0, weight=1) panel.rowconfigure(1, weight=1) title_label = tk.Label(panel, text="✏️ Modify Identified", font=("Arial", 24, "bold")) title_label.grid(row=0, column=0, sticky=tk.W, pady=(0, 20)) placeholder_frame = ttk.LabelFrame(panel, text="Coming Soon", padding="20") placeholder_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) placeholder_frame.columnconfigure(0, weight=1) placeholder_frame.rowconfigure(0, weight=1) placeholder_text = "Modify functionality will be integrated here." placeholder_label = tk.Label(placeholder_frame, text=placeholder_text, font=("Arial", 14)) placeholder_label.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) return panel def _create_tags_panel(self) -> ttk.Frame: """Create the tags panel (placeholder)""" panel = ttk.Frame(self.content_frame) # Configure panel grid for responsiveness panel.columnconfigure(0, weight=1) panel.rowconfigure(1, weight=1) title_label = tk.Label(panel, text="🏷️ Tag Manager", font=("Arial", 24, "bold")) title_label.grid(row=0, column=0, sticky=tk.W, pady=(0, 20)) placeholder_frame = ttk.LabelFrame(panel, text="Coming Soon", padding="20") placeholder_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) placeholder_frame.columnconfigure(0, weight=1) placeholder_frame.rowconfigure(0, weight=1) placeholder_text = "Tag management functionality will be integrated here." placeholder_label = tk.Label(placeholder_frame, text=placeholder_text, font=("Arial", 14)) placeholder_label.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) return panel def _run_scan(self): """Run the scan operation (migrated from original dashboard)""" folder = self.folder_var.get().strip() recursive = bool(self.recursive_var.get()) if not folder: messagebox.showwarning("Scan", "Please enter a folder path.", parent=self.root) return # Validate folder path using path utilities from path_utils import validate_path_exists, normalize_path try: folder = normalize_path(folder) if not validate_path_exists(folder): messagebox.showerror("Scan", f"Folder does not exist or is not accessible: {folder}", parent=self.root) return except ValueError as e: messagebox.showerror("Scan", f"Invalid folder path: {e}", parent=self.root) return if not callable(self.on_scan): messagebox.showinfo("Scan", "Scan functionality is not wired yet.", parent=self.root) return def worker(): try: self.status_label.config(text="Scanning...") result = self.on_scan(folder, recursive) messagebox.showinfo("Scan", f"Scan completed. Result: {result}", parent=self.root) self.status_label.config(text="Ready") except Exception as e: messagebox.showerror("Scan", f"Error during scan: {e}", parent=self.root) self.status_label.config(text="Ready") threading.Thread(target=worker, daemon=True).start() def _run_process(self): """Run the process operation (migrated from original dashboard)""" if not callable(self.on_process): messagebox.showinfo("Process", "Process functionality is not wired yet.", parent=self.root) return limit_value = None if self.limit_enabled.get(): try: limit_value = int(self.limit_var.get().strip()) if limit_value <= 0: raise ValueError except Exception: messagebox.showerror("Process", "Please enter a valid positive integer for limit.", parent=self.root) return def worker(): try: self.status_label.config(text="Processing...") result = self.on_process(limit_value) messagebox.showinfo("Process", f"Processing completed. Result: {result}", parent=self.root) self.status_label.config(text="Ready") except Exception as e: messagebox.showerror("Process", f"Error during processing: {e}", parent=self.root) self.status_label.config(text="Ready") threading.Thread(target=worker, daemon=True).start()