This commit introduces the IdentifyPanel class into the Dashboard GUI, allowing for a fully integrated face identification interface. The Dashboard now requires a database manager and face processor to create the Identify panel, which includes features for face browsing, identification, and management. Additionally, the DatabaseManager has been updated to support case-insensitive person additions, improving data consistency. The PhotoTagger class has also been modified to accommodate these changes, ensuring seamless interaction between components.
442 lines
18 KiB
Python
442 lines
18 KiB
Python
#!/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()
|
|
|
|
# Create main container
|
|
main_container = ttk.Frame(self.root)
|
|
main_container.pack(fill=tk.BOTH, expand=True)
|
|
|
|
# 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")
|
|
|
|
# Center and show window
|
|
self.root.update_idletasks()
|
|
self.gui_core.center_window(self.root, 1200, 800)
|
|
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.pack(fill=tk.X, padx=10, pady=5)
|
|
|
|
# Title
|
|
title_label = ttk.Label(menu_frame, text="PunimTag", font=("Arial", 16, "bold"))
|
|
title_label.pack(side=tk.LEFT, padx=(0, 20))
|
|
|
|
# Menu buttons
|
|
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 text, panel_name, tooltip in menu_buttons:
|
|
btn = ttk.Button(
|
|
menu_frame,
|
|
text=text,
|
|
command=lambda p=panel_name: self.show_panel(p)
|
|
)
|
|
btn.pack(side=tk.LEFT, padx=2)
|
|
|
|
# Add tooltip functionality
|
|
self._add_tooltip(btn, tooltip)
|
|
|
|
# Separator
|
|
separator = ttk.Separator(menu_frame, orient='vertical')
|
|
separator.pack(side=tk.LEFT, padx=10, fill=tk.Y)
|
|
|
|
# Status/Info area (for future use)
|
|
self.status_label = ttk.Label(menu_frame, text="Ready", foreground="#666")
|
|
self.status_label.pack(side=tk.RIGHT)
|
|
|
|
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.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
|
|
|
|
# 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].pack_forget()
|
|
|
|
# Show new panel
|
|
self.panels[panel_name].pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
|
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("<Enter>", show_tooltip)
|
|
widget.bind("<Leave>", hide_tooltip)
|
|
|
|
def _create_home_panel(self) -> ttk.Frame:
|
|
"""Create the home/welcome panel"""
|
|
panel = ttk.Frame(self.content_frame)
|
|
|
|
# Welcome content
|
|
welcome_frame = ttk.Frame(panel)
|
|
welcome_frame.pack(expand=True, fill=tk.BOTH)
|
|
|
|
# Center the content
|
|
center_frame = ttk.Frame(welcome_frame)
|
|
center_frame.place(relx=0.5, rely=0.5, anchor=tk.CENTER)
|
|
|
|
# Title
|
|
title_label = ttk.Label(center_frame, text="Welcome to PunimTag", font=("Arial", 24, "bold"))
|
|
title_label.pack(pady=(0, 20))
|
|
|
|
# Description
|
|
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 = ttk.Label(center_frame, text=desc_text, font=("Arial", 12), justify=tk.LEFT)
|
|
desc_label.pack()
|
|
|
|
return panel
|
|
|
|
def _create_scan_panel(self) -> ttk.Frame:
|
|
"""Create the scan panel (migrated from original dashboard)"""
|
|
panel = ttk.Frame(self.content_frame)
|
|
|
|
# Title
|
|
title_label = ttk.Label(panel, text="📁 Scan Photos", font=("Arial", 18, "bold"))
|
|
title_label.pack(anchor="w", pady=(0, 20))
|
|
|
|
# Scan form
|
|
form_frame = ttk.LabelFrame(panel, text="Scan Configuration", padding="15")
|
|
form_frame.pack(fill=tk.X, pady=(0, 20))
|
|
|
|
# Folder selection
|
|
folder_frame = ttk.Frame(form_frame)
|
|
folder_frame.pack(fill=tk.X, pady=(0, 10))
|
|
|
|
ttk.Label(folder_frame, text="Folder to scan:").pack(anchor="w")
|
|
|
|
folder_input_frame = ttk.Frame(folder_frame)
|
|
folder_input_frame.pack(fill=tk.X, pady=(5, 0))
|
|
|
|
self.folder_var = tk.StringVar()
|
|
folder_entry = ttk.Entry(folder_input_frame, textvariable=self.folder_var)
|
|
folder_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, 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.pack(side=tk.LEFT)
|
|
|
|
# Recursive option
|
|
self.recursive_var = tk.BooleanVar(value=True)
|
|
recursive_check = ttk.Checkbutton(
|
|
form_frame,
|
|
text="Include photos in sub-folders",
|
|
variable=self.recursive_var
|
|
)
|
|
recursive_check.pack(anchor="w", pady=(10, 0))
|
|
|
|
# Action button
|
|
scan_btn = ttk.Button(form_frame, text="🔍 Start Scan", command=self._run_scan)
|
|
scan_btn.pack(anchor="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)
|
|
|
|
# Title
|
|
title_label = ttk.Label(panel, text="🔍 Process Faces", font=("Arial", 18, "bold"))
|
|
title_label.pack(anchor="w", pady=(0, 20))
|
|
|
|
# Process form
|
|
form_frame = ttk.LabelFrame(panel, text="Processing Configuration", padding="15")
|
|
form_frame.pack(fill=tk.X, pady=(0, 20))
|
|
|
|
# Limit option
|
|
limit_frame = ttk.Frame(form_frame)
|
|
limit_frame.pack(fill=tk.X, pady=(0, 10))
|
|
|
|
self.limit_enabled = tk.BooleanVar(value=False)
|
|
limit_check = ttk.Checkbutton(limit_frame, text="Limit processing to", variable=self.limit_enabled)
|
|
limit_check.pack(side=tk.LEFT)
|
|
|
|
self.limit_var = tk.StringVar(value="50")
|
|
limit_entry = ttk.Entry(limit_frame, textvariable=self.limit_var, width=8)
|
|
limit_entry.pack(side=tk.LEFT, padx=(10, 0))
|
|
|
|
ttk.Label(limit_frame, text="photos").pack(side=tk.LEFT, padx=(5, 0))
|
|
|
|
# Action button
|
|
process_btn = ttk.Button(form_frame, text="🚀 Start Processing", command=self._run_process)
|
|
process_btn.pack(anchor="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)
|
|
|
|
# Title
|
|
title_label = ttk.Label(panel, text="👤 Identify Faces", font=("Arial", 18, "bold"))
|
|
title_label.pack(anchor="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.pack(fill=tk.BOTH, expand=True)
|
|
else:
|
|
# Fallback placeholder if dependencies are not available
|
|
placeholder_frame = ttk.LabelFrame(panel, text="Configuration Required", padding="20")
|
|
placeholder_frame.pack(expand=True, fill=tk.BOTH)
|
|
|
|
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 = ttk.Label(placeholder_frame, text=placeholder_text, font=("Arial", 12), justify=tk.LEFT)
|
|
placeholder_label.pack(expand=True)
|
|
|
|
return panel
|
|
|
|
def _create_auto_match_panel(self) -> ttk.Frame:
|
|
"""Create the auto-match panel (placeholder)"""
|
|
panel = ttk.Frame(self.content_frame)
|
|
|
|
title_label = ttk.Label(panel, text="🔗 Auto-Match Faces", font=("Arial", 18, "bold"))
|
|
title_label.pack(anchor="w", pady=(0, 20))
|
|
|
|
placeholder_frame = ttk.LabelFrame(panel, text="Coming Soon", padding="20")
|
|
placeholder_frame.pack(expand=True, fill=tk.BOTH)
|
|
|
|
placeholder_text = "Auto-Match functionality will be integrated here."
|
|
placeholder_label = ttk.Label(placeholder_frame, text=placeholder_text, font=("Arial", 12))
|
|
placeholder_label.pack(expand=True)
|
|
|
|
return panel
|
|
|
|
def _create_search_panel(self) -> ttk.Frame:
|
|
"""Create the search panel (placeholder)"""
|
|
panel = ttk.Frame(self.content_frame)
|
|
|
|
title_label = ttk.Label(panel, text="🔎 Search Photos", font=("Arial", 18, "bold"))
|
|
title_label.pack(anchor="w", pady=(0, 20))
|
|
|
|
placeholder_frame = ttk.LabelFrame(panel, text="Coming Soon", padding="20")
|
|
placeholder_frame.pack(expand=True, fill=tk.BOTH)
|
|
|
|
placeholder_text = "Search functionality will be integrated here."
|
|
placeholder_label = ttk.Label(placeholder_frame, text=placeholder_text, font=("Arial", 12))
|
|
placeholder_label.pack(expand=True)
|
|
|
|
return panel
|
|
|
|
def _create_modify_panel(self) -> ttk.Frame:
|
|
"""Create the modify panel (placeholder)"""
|
|
panel = ttk.Frame(self.content_frame)
|
|
|
|
title_label = ttk.Label(panel, text="✏️ Modify Identified", font=("Arial", 18, "bold"))
|
|
title_label.pack(anchor="w", pady=(0, 20))
|
|
|
|
placeholder_frame = ttk.LabelFrame(panel, text="Coming Soon", padding="20")
|
|
placeholder_frame.pack(expand=True, fill=tk.BOTH)
|
|
|
|
placeholder_text = "Modify functionality will be integrated here."
|
|
placeholder_label = ttk.Label(placeholder_frame, text=placeholder_text, font=("Arial", 12))
|
|
placeholder_label.pack(expand=True)
|
|
|
|
return panel
|
|
|
|
def _create_tags_panel(self) -> ttk.Frame:
|
|
"""Create the tags panel (placeholder)"""
|
|
panel = ttk.Frame(self.content_frame)
|
|
|
|
title_label = ttk.Label(panel, text="🏷️ Tag Manager", font=("Arial", 18, "bold"))
|
|
title_label.pack(anchor="w", pady=(0, 20))
|
|
|
|
placeholder_frame = ttk.LabelFrame(panel, text="Coming Soon", padding="20")
|
|
placeholder_frame.pack(expand=True, fill=tk.BOTH)
|
|
|
|
placeholder_text = "Tag management functionality will be integrated here."
|
|
placeholder_label = ttk.Label(placeholder_frame, text=placeholder_text, font=("Arial", 12))
|
|
placeholder_label.pack(expand=True)
|
|
|
|
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()
|
|
|
|
|