punimtag/dashboard_gui.py
tanyar09 34c7998ce9 Revamp Dashboard GUI with unified interface and menu navigation
This commit transforms the Dashboard GUI into a unified interface designed for web migration, featuring a single window with a menu bar for easy access to all functionalities. Key enhancements include the addition of a content area for seamless panel switching, improved panel management, and real-time status updates. The README has also been updated to reflect these changes, providing a comprehensive overview of the new dashboard features and system requirements.
2025-10-09 14:19:05 -04:00

433 lines
17 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
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, on_scan=None, on_process=None, on_identify=None):
self.gui_core = gui_core
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 (placeholder for now)"""
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))
# Placeholder content
placeholder_frame = ttk.LabelFrame(panel, text="Coming Soon", padding="20")
placeholder_frame.pack(expand=True, fill=tk.BOTH)
placeholder_text = (
"The Identify panel will be integrated here.\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()