punimtag/dashboard_gui.py
tanyar09 e5ec0e4aea Enhance Dashboard GUI with full screen and responsive design features
This commit updates the Dashboard GUI to support automatic full screen mode across platforms, ensuring optimal viewing experiences. It introduces a responsive layout that dynamically adjusts components during window resizing, improving usability. Additionally, typography has been enhanced with larger fonts for better readability. The README has been updated to reflect these new features, emphasizing the unified dashboard's capabilities and user experience improvements.
2025-10-10 11:11:58 -04:00

549 lines
24 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()
# 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('<Configure>', 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("<Enter>", show_tooltip)
widget.bind("<Leave>", 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()