diff --git a/README_UNIFIED_DASHBOARD.md b/README_UNIFIED_DASHBOARD.md index 5227098..e3bcc51 100644 --- a/README_UNIFIED_DASHBOARD.md +++ b/README_UNIFIED_DASHBOARD.md @@ -14,6 +14,9 @@ A powerful photo face recognition and tagging system with a modern unified dashb - **🌐 Web-Ready Architecture** - Designed for easy migration to web application - **πŸ“Š Status Updates** - Real-time feedback on current operations - **🎨 Enhanced Typography** - Larger, more readable fonts optimized for full screen +- **🏠 Smart Home Navigation** - Compact home icon for quick return to welcome screen +- **πŸšͺ Unified Exit Behavior** - All exit buttons navigate to home instead of closing +- **βœ… Complete Integration** - All panels fully functional and integrated ## πŸ“‹ System Requirements @@ -51,13 +54,14 @@ python3 setup.py # Installs system deps + Python packages python3 photo_tagger.py dashboard # 3. Use the menu bar to access all features: +# 🏠 Home - Return to welcome screen (βœ… Fully Functional) # πŸ“ Scan - Add photos to your collection (βœ… Fully Functional) # πŸ” Process - Detect faces in photos (βœ… Fully Functional) # πŸ‘€ Identify - Identify people in photos (βœ… Fully Functional) # πŸ”— Auto-Match - Find matching faces automatically (βœ… Fully Functional) # πŸ”Ž Search - Find photos by person name (βœ… Fully Functional) # ✏️ Modify - Edit face identifications (βœ… Fully Functional) -# 🏷️ Tags - Manage photo tags (🚧 Coming Soon) +# 🏷️ Tags - Manage photo tags (βœ… Fully Functional) ``` ## πŸ“¦ Installation @@ -119,6 +123,12 @@ The dashboard automatically opens in full screen mode and provides a fully respo - **Consistent Styling**: All panels use the same enhanced font sizes - **Professional Appearance**: Clean, modern typography throughout +#### **Smart Navigation** +- **🏠 Home Icon**: Compact home button (🏠) in the leftmost position of the menu bar +- **Quick Return**: Click the home icon to instantly return to the welcome screen +- **Exit to Home**: All exit buttons in panels now navigate to home instead of closing +- **Consistent UX**: Unified navigation experience across all panels + ### Dashboard Features #### **🏠 Home Panel** @@ -201,11 +211,15 @@ The auto-match feature works in a **person-centric** way: - **Responsive Layout**: Face grid adapts to window resizing - **Unsaved Changes Protection**: Warns before losing unsaved work -#### **🏷️ Tags Panel** *(Coming Soon)* -- **File Explorer Interface**: Browse photos like a file manager -- **Tag Management**: Add, remove, and organize tags -- **Multiple View Modes**: List, icon, compact, and folder views -- **Column Customization**: Resizable columns and visibility controls +#### **🏷️ Tag Manager Panel** *(Fully Functional)* +- **Photo Explorer**: Browse photos organized by folders with thumbnail previews +- **Multiple View Modes**: List view, icon view, compact view, and folder view +- **Tag Management**: Add, remove, and organize tags with bulk operations +- **People Integration**: View and manage people identified in photos +- **Bulk Tagging**: Link tags to entire folders or multiple photos at once +- **Search & Filter**: Find photos by tags, people, or folder location +- **Responsive Layout**: Adapts to window resizing with proper scrolling +- **Exit to Home**: Exit button navigates to home screen instead of closing ## 🎯 Command Line Interface (Legacy) @@ -253,13 +267,13 @@ python3 photo_tagger.py tag-manager β”‚ Unified Dashboard β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚ β”‚ β”‚ Menu Bar β”‚β”‚ -β”‚ β”‚ [Scan] [Process] [Identify] [Search] [Tags] [Modify] β”‚β”‚ +β”‚ β”‚ [🏠] [Scan] [Process] [Identify] [Search] [Tags] [Modify]β”‚β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚ β”‚ β”‚ Content Area β”‚β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚β”‚ -β”‚ β”‚ β”‚Scan Panel β”‚ β”‚Identify β”‚ β”‚Search Panel β”‚ β”‚β”‚ -β”‚ β”‚ β”‚ β”‚ β”‚Panel β”‚ β”‚ β”‚ β”‚β”‚ +β”‚ β”‚ β”‚Home Panel β”‚ β”‚Identify β”‚ β”‚Search Panel β”‚ β”‚β”‚ +β”‚ β”‚ β”‚(Welcome) β”‚ β”‚Panel β”‚ β”‚ β”‚ β”‚β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ @@ -277,13 +291,13 @@ python3 photo_tagger.py tag-manager β”‚ Web Browser β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚ β”‚ β”‚ Navigation Bar β”‚β”‚ -β”‚ β”‚ [Scan] [Process] [Identify] [Search] [Tags] [Modify] β”‚β”‚ +β”‚ β”‚ [🏠] [Scan] [Process] [Identify] [Search] [Tags] [Modify]β”‚β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚ β”‚ β”‚ Main Content Area β”‚β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚β”‚ -β”‚ β”‚ β”‚Scan Page β”‚ β”‚Identify β”‚ β”‚Search Page β”‚ β”‚β”‚ -β”‚ β”‚ β”‚ β”‚ β”‚Page β”‚ β”‚ β”‚ β”‚β”‚ +β”‚ β”‚ β”‚Home Page β”‚ β”‚Identify β”‚ β”‚Search Page β”‚ β”‚β”‚ +β”‚ β”‚ β”‚(Welcome) β”‚ β”‚Page β”‚ β”‚ β”‚ β”‚β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ @@ -307,6 +321,28 @@ python3 photo_tagger.py tag-manager - **State Management**: Panel switching system mirrors web routing concepts - **API-Ready**: Panel methods can easily become API endpoints +## 🧭 Navigation & User Experience + +### Smart Navigation System +- **🏠 Home Icon**: Compact home button (🏠) positioned at the leftmost side of the menu bar +- **Quick Return**: Single click to return to the welcome screen from any panel +- **Exit to Home**: All exit buttons in panels now navigate to home instead of closing the application +- **Consistent UX**: Unified navigation experience across all panels and features +- **Tooltip Support**: Hover over the home icon to see "Go to the welcome screen" + +### Panel Integration +- **Seamless Switching**: All panels are fully integrated and functional +- **State Preservation**: Panel states are maintained when switching between features +- **Background Processing**: Long operations continue running when switching panels +- **Memory Management**: Proper cleanup and resource management across panels + +### Recent Updates (Latest Version) +- **βœ… Complete Panel Integration**: All 7 panels (Home, Scan, Process, Identify, Auto-Match, Search, Modify, Tags) are fully functional +- **🏠 Home Navigation**: Added compact home icon for instant return to welcome screen +- **πŸšͺ Exit Button Enhancement**: All exit buttons now navigate to home instead of closing +- **🎨 UI Improvements**: Enhanced typography and responsive design for full screen experience +- **πŸ”§ Code Quality**: Improved architecture with proper callback system for navigation + ## πŸ”§ Advanced Features ### Face Recognition Technology @@ -353,13 +389,14 @@ source venv/bin/activate python3 photo_tagger.py dashboard # Then use the menu bar in the dashboard: +# 🏠 Home - Return to welcome screen (βœ… Fully Functional) # πŸ“ Scan - Add photos (βœ… Fully Functional) # πŸ” Process - Detect faces (βœ… Fully Functional) # πŸ‘€ Identify - Identify people (βœ… Fully Functional) # πŸ”— Auto-Match - Find matches (βœ… Fully Functional) # πŸ”Ž Search - Find photos (βœ… Fully Functional) # ✏️ Modify - Edit identifications (βœ… Fully Functional) -# 🏷️ Tags - Manage tags (🚧 Coming Soon) +# 🏷️ Tags - Manage tags (βœ… Fully Functional) # Legacy command line usage python3 photo_tagger.py scan ~/Pictures --recursive @@ -385,12 +422,14 @@ python3 photo_tagger.py stats - [x] Responsive design with dynamic resizing - [x] Enhanced typography for full screen viewing -### Phase 2: GUI Panel Integration (In Progress) +### Phase 2: GUI Panel Integration βœ… - [x] Identify panel integration (fully functional) - [x] Auto-Match panel integration (fully functional) - [x] Search panel integration (fully functional) - [x] Modify panel integration (fully functional) -- [ ] Tags panel integration +- [x] Tag Manager panel integration (fully functional) +- [x] Home icon navigation (compact home button in menu) +- [x] Exit button navigation (all exit buttons navigate to home) ### Phase 3: Web Migration Preparation - [ ] Service layer extraction @@ -413,6 +452,9 @@ python3 photo_tagger.py stats - **Consistent Interface**: All features follow the same design patterns - **Professional Look**: Modern, clean interface design with enhanced typography - **Intuitive Navigation**: Menu bar makes all features easily accessible +- **Smart Home Navigation**: Compact home icon (🏠) for quick return to welcome screen +- **Unified Exit Behavior**: All exit buttons navigate to home instead of closing +- **Complete Feature Set**: All panels fully functional and integrated ### Developer Experience - **Modular Design**: Each panel is independent and maintainable @@ -433,7 +475,8 @@ python3 photo_tagger.py stats **Total project size**: ~4,000+ lines of Python code **Dependencies**: 6 essential packages **Setup time**: ~5 minutes -**Perfect for**: Professional photo management with modern unified interface +**Perfect for**: Professional photo management with modern unified interface +**Status**: All panels fully functional and integrated with smart navigation ## πŸ“ž Support diff --git a/auto_match_panel.py b/auto_match_panel.py index 7bc9b0d..4424914 100644 --- a/auto_match_panel.py +++ b/auto_match_panel.py @@ -20,12 +20,13 @@ class AutoMatchPanel: """Integrated auto-match panel that embeds the full auto-match GUI functionality into the dashboard""" def __init__(self, parent_frame: ttk.Frame, db_manager: DatabaseManager, - face_processor: FaceProcessor, gui_core: GUICore, verbose: int = 0): + face_processor: FaceProcessor, gui_core: GUICore, on_navigate_home=None, verbose: int = 0): """Initialize the auto-match panel""" self.parent_frame = parent_frame self.db = db_manager self.face_processor = face_processor self.gui_core = gui_core + self.on_navigate_home = on_navigate_home self.verbose = verbose # Panel state @@ -781,12 +782,14 @@ class AutoMatchPanel: """Quit the auto-match process""" # Check for unsaved changes before quitting if self._has_unsaved_changes(): - result = messagebox.askyesnocancel( + result = self.gui_core.create_large_messagebox( + self.main_frame, "Unsaved Changes", "You have unsaved changes that will be lost if you quit.\n\n" "Yes: Save current changes and quit\n" "No: Quit without saving\n" - "Cancel: Return to auto-match" + "Cancel: Return to auto-match", + "askyesnocancel" ) if result is None: # Cancel @@ -796,6 +799,10 @@ class AutoMatchPanel: self._save_changes() self._cleanup() + + # Navigate to home if callback is available (dashboard mode) + if self.on_navigate_home: + self.on_navigate_home() def _has_unsaved_changes(self): """Check if there are any unsaved changes""" diff --git a/dashboard_gui.py b/dashboard_gui.py index a0a9e9f..da0e391 100644 --- a/dashboard_gui.py +++ b/dashboard_gui.py @@ -1434,21 +1434,28 @@ class DashboardGUI: # Menu buttons with larger size for full screen menu_buttons = [ + ("🏠", "home", "Go to the welcome screen"), ("πŸ“ 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"), + ("🎯 Auto-Match", "auto_match", "Find and confirm matching faces"), + ("πŸ”Ž Search", "search", "Search photos by people, dates, tags, and more"), ("✏️ Edit Identified", "modify", "View and modify identified faces"), - ("🏷️ Tags", "tags", "Manage photo tags"), + ("🏷️ Tag Photos", "tags", "Manage photo tags"), ] for i, (text, panel_name, tooltip) in enumerate(menu_buttons): + # Make home button smaller than other buttons + if panel_name == "home": + btn_width = 4 # Smaller width for icon-only home button + else: + btn_width = 16 # Standard width for other buttons + btn = ttk.Button( buttons_frame, text=text, command=lambda p=panel_name: self.show_panel(p), - width=16 # Fixed width for consistent layout + width=btn_width ) btn.grid(row=0, column=i, padx=3, sticky=tk.W) @@ -1599,10 +1606,10 @@ class DashboardGUI: "β€’ πŸ“ 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" + "β€’ 🎯 Auto-Match - Find matching faces automatically\n" + "β€’ πŸ”Ž Search - Search photos by people, dates, tags, and more\n" + "β€’ ✏️ Edit Identified - Edit face identifications\n" + "β€’ 🏷️ Tag Photos - Manage photo tags\n\n" "Select a feature from the menu to get started!" ) @@ -1721,7 +1728,7 @@ class DashboardGUI: # 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) + self.identify_panel = IdentifyPanel(panel, self.db_manager, self.face_processor, self.gui_core, on_navigate_home=lambda: self.show_panel("home")) identify_frame = self.identify_panel.create_panel() identify_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) else: @@ -1764,7 +1771,7 @@ class DashboardGUI: # Create the auto-match panel if we have the required dependencies if self.db_manager and self.face_processor: - self.auto_match_panel = AutoMatchPanel(panel, self.db_manager, self.face_processor, self.gui_core) + self.auto_match_panel = AutoMatchPanel(panel, self.db_manager, self.face_processor, self.gui_core, on_navigate_home=lambda: self.show_panel("home")) auto_match_frame = self.auto_match_panel.create_panel() auto_match_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) else: @@ -1853,7 +1860,7 @@ class DashboardGUI: # Create the modify panel if we have the required dependencies if self.db_manager and self.face_processor: - self.modify_panel = ModifyPanel(panel, self.db_manager, self.face_processor, self.gui_core) + self.modify_panel = ModifyPanel(panel, self.db_manager, self.face_processor, self.gui_core, on_navigate_home=lambda: self.show_panel("home")) modify_frame = self.modify_panel.create_panel() modify_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) else: @@ -1894,7 +1901,7 @@ class DashboardGUI: # Create the tag manager panel if we have the required dependencies if self.db_manager and self.tag_manager and self.face_processor: - self.tag_manager_panel = TagManagerPanel(panel, self.db_manager, self.gui_core, self.tag_manager, self.face_processor) + self.tag_manager_panel = TagManagerPanel(panel, self.db_manager, self.gui_core, self.tag_manager, self.face_processor, on_navigate_home=lambda: self.show_panel("home")) tag_manager_frame = self.tag_manager_panel.create_panel() tag_manager_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) else: @@ -1927,7 +1934,7 @@ class DashboardGUI: recursive = bool(self.recursive_var.get()) if not folder: - messagebox.showwarning("Scan", "Please enter a folder path.", parent=self.root) + self.gui_core.create_large_messagebox(self.root, "Scan", "Please enter a folder path.", "warning") return # Validate folder path using path utilities diff --git a/gui_core.py b/gui_core.py index 8089fee..8e16f8e 100644 --- a/gui_core.py +++ b/gui_core.py @@ -405,6 +405,67 @@ class GUICore: result = messagebox.askyesno(title, message, parent=parent) return result + def create_large_messagebox(self, parent, title: str, message: str, msg_type: str = "warning") -> bool: + """Create a larger messagebox dialog that fits text without wrapping""" + import tkinter as tk + from tkinter import ttk, messagebox + + # Calculate appropriate size based on message length + lines = message.count('\n') + 1 + max_line_length = max(len(line) for line in message.split('\n')) + + # Set width to accommodate text (minimum 400, maximum 800) + width = max(400, min(800, max_line_length * 8 + 100)) + # Set height based on number of lines (minimum 200, maximum 500) + height = max(200, min(500, lines * 25 + 150)) + + # Calculate center position first + screen_width = parent.winfo_screenwidth() if parent else tk._default_root.winfo_screenwidth() + screen_height = parent.winfo_screenheight() if parent else tk._default_root.winfo_screenheight() + x = (screen_width - width) // 2 + y = (screen_height - height) // 2 + + # Create a custom dialog for better control over size + dialog = tk.Toplevel(parent) + dialog.title(title) + dialog.transient(parent) + dialog.grab_set() + + # Set geometry with position in one call to prevent jumping + dialog.geometry(f"{width}x{height}+{x}+{y}") + + # Create main frame + main_frame = ttk.Frame(dialog, padding="20") + main_frame.pack(fill=tk.BOTH, expand=True) + + # Add message label + message_label = tk.Label(main_frame, text=message, font=("Arial", 10), + justify=tk.LEFT, wraplength=width-100) + message_label.pack(pady=(0, 20)) + + # Add buttons based on message type + button_frame = ttk.Frame(main_frame) + button_frame.pack() + + def close_dialog(result_value): + """Close dialog and set result""" + dialog._result = result_value + dialog.destroy() + + if msg_type == "warning": + ttk.Button(button_frame, text="OK", command=lambda: close_dialog(True)).pack(side=tk.LEFT, padx=5) + elif msg_type == "askyesno": + ttk.Button(button_frame, text="Yes", command=lambda: close_dialog(True)).pack(side=tk.LEFT, padx=5) + ttk.Button(button_frame, text="No", command=lambda: close_dialog(False)).pack(side=tk.LEFT, padx=5) + elif msg_type == "askyesnocancel": + ttk.Button(button_frame, text="Yes", command=lambda: close_dialog(True)).pack(side=tk.LEFT, padx=5) + ttk.Button(button_frame, text="No", command=lambda: close_dialog(False)).pack(side=tk.LEFT, padx=5) + ttk.Button(button_frame, text="Cancel", command=lambda: close_dialog(None)).pack(side=tk.LEFT, padx=5) + + # Wait for dialog to close + dialog.wait_window() + return getattr(dialog, '_result', None) + def create_input_dialog(self, parent, title: str, prompt: str, default: str = "") -> Optional[str]: """Create an input dialog""" import tkinter as tk diff --git a/identify_panel.py b/identify_panel.py index 2a102e2..a724e30 100644 --- a/identify_panel.py +++ b/identify_panel.py @@ -21,12 +21,13 @@ class IdentifyPanel: """Integrated identify panel that embeds the full identify GUI functionality into the dashboard""" def __init__(self, parent_frame: ttk.Frame, db_manager: DatabaseManager, - face_processor: FaceProcessor, gui_core: GUICore, verbose: int = 0): + face_processor: FaceProcessor, gui_core: GUICore, on_navigate_home=None, verbose: int = 0): """Initialize the identify panel""" self.parent_frame = parent_frame self.db = db_manager self.face_processor = face_processor self.gui_core = gui_core + self.on_navigate_home = on_navigate_home self.verbose = verbose # Panel state @@ -1061,7 +1062,7 @@ class IdentifyPanel: date_of_birth = self.components['date_of_birth_var'].get().strip() if not first_name or not last_name or not date_of_birth: - messagebox.showwarning("Missing Information", "Please fill in first name, last name, and date of birth.") + self.gui_core.create_large_messagebox(self.main_frame, "Missing Information", "Please fill in first name, last name, and date of birth.", "warning") return if not self.current_faces or self.current_face_index >= len(self.current_faces): @@ -1285,13 +1286,15 @@ class IdentifyPanel: pending_identifications = self._get_pending_identifications() if pending_identifications: - result = messagebox.askyesnocancel( + result = self.gui_core.create_large_messagebox( + self.main_frame, "Save Pending Identifications?", f"You have {len(pending_identifications)} pending identifications.\n\n" "Do you want to save them before quitting?\n\n" "β€’ Yes: Save all pending identifications and quit\n" "β€’ No: Quit without saving\n" - "β€’ Cancel: Return to identification" + "β€’ Cancel: Return to identification", + "askyesnocancel" ) if result is True: # Yes - Save and quit @@ -1304,6 +1307,10 @@ class IdentifyPanel: # Clean up self._cleanup() self.is_active = False + + # Navigate to home if callback is available (dashboard mode) + if self.on_navigate_home: + self.on_navigate_home() def _validate_navigation(self): """Validate that navigation is safe (no unsaved changes)""" @@ -1314,13 +1321,15 @@ class IdentifyPanel: # If all three required fields are filled, ask for confirmation if first_name and last_name and date_of_birth: - result = messagebox.askyesnocancel( + result = self.gui_core.create_large_messagebox( + self.main_frame, "Unsaved Changes", "You have unsaved changes in the identification form.\n\n" "Do you want to save them before continuing?\n\n" "β€’ Yes: Save current identification and continue\n" "β€’ No: Discard changes and continue\n" - "β€’ Cancel: Stay on current face" + "β€’ Cancel: Stay on current face", + "askyesnocancel" ) if result is True: # Yes - Save and continue diff --git a/modify_panel.py b/modify_panel.py index ccd0d3d..9be032b 100644 --- a/modify_panel.py +++ b/modify_panel.py @@ -51,12 +51,13 @@ class ModifyPanel: """Integrated modify panel that embeds the full modify identified GUI functionality into the dashboard""" def __init__(self, parent_frame: ttk.Frame, db_manager: DatabaseManager, - face_processor: FaceProcessor, gui_core: GUICore, verbose: int = 0): + face_processor: FaceProcessor, gui_core: GUICore, on_navigate_home=None, verbose: int = 0): """Initialize the modify panel""" self.parent_frame = parent_frame self.db = db_manager self.face_processor = face_processor self.gui_core = gui_core + self.on_navigate_home = on_navigate_home self.verbose = verbose # Panel state @@ -326,7 +327,7 @@ class ModifyPanel: # Create a new window for editing edit_window = tk.Toplevel(self.main_frame) edit_window.title(f"Edit {person_record['name']}") - edit_window.geometry("400x300") + edit_window.geometry("500x400") edit_window.transient(self.main_frame) edit_window.grab_set() @@ -630,13 +631,15 @@ class ModifyPanel: """Handle quit button click""" # Check for unsaved changes if self.unmatched_faces: - result = messagebox.askyesnocancel( + result = self.gui_core.create_large_messagebox( + self.main_frame, "Unsaved Changes", f"You have {len(self.unmatched_faces)} unsaved changes.\n\n" "Do you want to save them before quitting?\n\n" "β€’ Yes: Save changes and quit\n" "β€’ No: Quit without saving\n" - "β€’ Cancel: Return to modify" + "β€’ Cancel: Return to modify", + "askyesnocancel" ) if result is True: # Yes - Save and quit @@ -649,6 +652,10 @@ class ModifyPanel: # Clean up and deactivate self._cleanup() self.is_active = False + + # Navigate to home if callback is available (dashboard mode) + if self.on_navigate_home: + self.on_navigate_home() def on_save_all_changes(self): """Save all unmatched faces to database""" diff --git a/tag_manager_panel.py b/tag_manager_panel.py index 334d08c..db3f251 100644 --- a/tag_manager_panel.py +++ b/tag_manager_panel.py @@ -7,7 +7,12 @@ Embeds the full tag manager GUI functionality into the dashboard frame import os import tkinter as tk from tkinter import ttk, messagebox, simpledialog -from PIL import Image, ImageTk +from PIL import Image +try: + from PIL import ImageTk +except ImportError: + # Fallback for older PIL versions + import ImageTk from typing import List, Dict, Tuple, Optional import sys import subprocess @@ -22,13 +27,14 @@ class TagManagerPanel: """Integrated tag manager panel that embeds the full tag manager GUI functionality into the dashboard""" def __init__(self, parent_frame: ttk.Frame, db_manager: DatabaseManager, - gui_core: GUICore, tag_manager: TagManager, face_processor: FaceProcessor, verbose: int = 0): + gui_core: GUICore, tag_manager: TagManager, face_processor: FaceProcessor, on_navigate_home=None, verbose: int = 0): """Initialize the tag manager panel""" self.parent_frame = parent_frame self.db = db_manager self.gui_core = gui_core self.tag_manager = tag_manager self.face_processor = face_processor + self.on_navigate_home = on_navigate_home self.verbose = verbose # Panel state @@ -60,29 +66,35 @@ class TagManagerPanel: 'compact': {'filename': True, 'faces': True, 'tags': True} } + # Column resizing state + self.resize_start_x = 0 + self.resize_start_widths: List[int] = [] + self.current_visible_cols: List[Dict] = [] + self.is_resizing = False + self.column_config = { 'list': [ - {'key': 'id', 'label': 'ID', 'width': 50, 'weight': 0}, - {'key': 'filename', 'label': 'Filename', 'width': 150, 'weight': 1}, - {'key': 'path', 'label': 'Path', 'width': 200, 'weight': 2}, - {'key': 'processed', 'label': 'Processed', 'width': 80, 'weight': 0}, - {'key': 'date_taken', 'label': 'Date Taken', 'width': 120, 'weight': 0}, - {'key': 'faces', 'label': 'Faces', 'width': 60, 'weight': 0}, - {'key': 'tags', 'label': 'Tags', 'width': 180, 'weight': 1} + {'key': 'id', 'label': 'ID', 'width': 60, 'weight': 0}, + {'key': 'filename', 'label': 'Filename', 'width': 250, 'weight': 1}, + {'key': 'path', 'label': 'Path', 'width': 400, 'weight': 2}, + {'key': 'processed', 'label': 'Processed', 'width': 100, 'weight': 0}, + {'key': 'date_taken', 'label': 'Date Taken', 'width': 140, 'weight': 0}, + {'key': 'faces', 'label': 'Faces', 'width': 80, 'weight': 0}, + {'key': 'tags', 'label': 'Tags', 'width': 300, 'weight': 1} ], 'icons': [ - {'key': 'thumbnail', 'label': 'Thumbnail', 'width': 160, 'weight': 0}, + {'key': 'thumbnail', 'label': 'Thumbnail', 'width': 200, 'weight': 0}, {'key': 'id', 'label': 'ID', 'width': 80, 'weight': 0}, - {'key': 'filename', 'label': 'Filename', 'width': 200, 'weight': 1}, - {'key': 'processed', 'label': 'Processed', 'width': 80, 'weight': 0}, - {'key': 'date_taken', 'label': 'Date Taken', 'width': 120, 'weight': 0}, - {'key': 'faces', 'label': 'Faces', 'width': 60, 'weight': 0}, - {'key': 'tags', 'label': 'Tags', 'width': 180, 'weight': 1} + {'key': 'filename', 'label': 'Filename', 'width': 300, 'weight': 1}, + {'key': 'processed', 'label': 'Processed', 'width': 100, 'weight': 0}, + {'key': 'date_taken', 'label': 'Date Taken', 'width': 140, 'weight': 0}, + {'key': 'faces', 'label': 'Faces', 'width': 80, 'weight': 0}, + {'key': 'tags', 'label': 'Tags', 'width': 300, 'weight': 1} ], 'compact': [ - {'key': 'filename', 'label': 'Filename', 'width': 200, 'weight': 1}, - {'key': 'faces', 'label': 'Faces', 'width': 60, 'weight': 0}, - {'key': 'tags', 'label': 'Tags', 'width': 180, 'weight': 1} + {'key': 'filename', 'label': 'Filename', 'width': 400, 'weight': 1}, + {'key': 'faces', 'label': 'Faces', 'width': 80, 'weight': 0}, + {'key': 'tags', 'label': 'Tags', 'width': 500, 'weight': 1} ] } @@ -183,7 +195,7 @@ class TagManagerPanel: self.components['save_button'].pack(side=tk.RIGHT, padx=10, pady=5) # Quit button (for standalone mode - not used in dashboard) - self.components['quit_button'] = ttk.Button(bottom_frame, text="Quit", + self.components['quit_button'] = ttk.Button(bottom_frame, text="Exit Tag Manager", command=self._quit_with_warning) self.components['quit_button'].pack(side=tk.RIGHT, padx=(0, 10), pady=5) @@ -196,14 +208,136 @@ class TagManagerPanel: # Bind to the main frame and all its children self.main_frame.bind_all("", on_mousewheel) + self.main_frame.bind_all("", self._global_mouse_release) # Store reference for cleanup self._mousewheel_handler = on_mousewheel + def _start_resize(self, event, col_idx): + """Start column resizing""" + self.is_resizing = True + self.resize_start_x = event.x_root + self.resize_start_widths = [col['width'] for col in self.current_visible_cols] + self.main_frame.configure(cursor="sb_h_double_arrow") + + def _do_resize(self, event, col_idx): + """Handle column resizing during drag""" + if not self.is_resizing or not self.resize_start_widths or not self.current_visible_cols: + return + delta_x = event.x_root - self.resize_start_x + if col_idx < len(self.current_visible_cols) and col_idx + 1 < len(self.current_visible_cols): + new_width_left = max(50, self.resize_start_widths[col_idx] + delta_x) + new_width_right = max(50, self.resize_start_widths[col_idx + 1] - delta_x) + self.current_visible_cols[col_idx]['width'] = new_width_left + self.current_visible_cols[col_idx + 1]['width'] = new_width_right + + # Update column config + for i, col in enumerate(self.column_config['list']): + if col['key'] == self.current_visible_cols[col_idx]['key']: + self.column_config['list'][i]['width'] = new_width_left + elif col['key'] == self.current_visible_cols[col_idx + 1]['key']: + self.column_config['list'][i]['width'] = new_width_right + + # Update grid configuration + try: + header_frame_ref = None + row_frames = [] + for widget in self.components['content_inner'].winfo_children(): + if isinstance(widget, ttk.Frame): + if header_frame_ref is None: + header_frame_ref = widget + else: + row_frames.append(widget) + + if header_frame_ref is not None: + header_frame_ref.columnconfigure(col_idx * 2, weight=self.current_visible_cols[col_idx]['weight'], minsize=new_width_left) + header_frame_ref.columnconfigure((col_idx + 1) * 2, weight=self.current_visible_cols[col_idx + 1]['weight'], minsize=new_width_right) + + for rf in row_frames: + rf.columnconfigure(col_idx, weight=self.current_visible_cols[col_idx]['weight'], minsize=new_width_left) + rf.columnconfigure(col_idx + 1, weight=self.current_visible_cols[col_idx + 1]['weight'], minsize=new_width_right) + + self.main_frame.update_idletasks() + except Exception: + pass + + def _stop_resize(self, event): + """Stop column resizing""" + self.is_resizing = False + self.main_frame.configure(cursor="") + + def _global_mouse_release(self, event): + """Handle global mouse release for column resizing""" + if self.is_resizing: + self._stop_resize(event) + + def _show_column_context_menu(self, event, view_mode): + """Show column context menu for show/hide columns""" + popup = tk.Toplevel(self.main_frame) + popup.wm_overrideredirect(True) + popup.wm_geometry(f"+{event.x_root}+{event.y_root}") + popup.configure(bg='white', relief='flat', bd=0) + menu_frame = tk.Frame(popup, bg='white') + menu_frame.pack(padx=2, pady=2) + checkbox_vars: Dict[str, tk.BooleanVar] = {} + protected_columns = {'icons': ['thumbnail'], 'compact': ['filename'], 'list': ['filename']} + + def close_popup(): + try: + popup.destroy() + except Exception: + pass + + def close_on_click_outside(e): + if e.widget != popup: + try: + popup.winfo_exists() + close_popup() + except tk.TclError: + pass + + for col in self.column_config[view_mode]: + key = col['key'] + label = col['label'] + is_visible = self.column_visibility[view_mode][key] + is_protected = key in protected_columns.get(view_mode, []) + item_frame = tk.Frame(menu_frame, bg='white', relief='flat', bd=0) + item_frame.pack(fill=tk.X, pady=1) + var = tk.BooleanVar(value=is_visible) + checkbox_vars[key] = var + + def make_toggle_command(col_key, var_ref): + def toggle_column(): + if col_key in protected_columns.get(view_mode, []): + return + self.column_visibility[view_mode][col_key] = var_ref.get() + self._switch_view_mode(view_mode) + return toggle_column + + if is_protected: + cb = tk.Checkbutton(item_frame, text=label, variable=var, state='disabled', + bg='white', fg='gray', font=("Arial", 9), relief='flat', + bd=0, highlightthickness=0) + cb.pack(side=tk.LEFT, padx=5, pady=2) + tk.Label(item_frame, text="(always visible)", bg='white', fg='gray', + font=("Arial", 8)).pack(side=tk.LEFT, padx=(0, 5)) + else: + cb = tk.Checkbutton(item_frame, text=label, variable=var, + command=make_toggle_command(key, var), bg='white', + font=("Arial", 9), relief='flat', bd=0, highlightthickness=0) + cb.pack(side=tk.LEFT, padx=5, pady=2) + + self.main_frame.bind("", close_on_click_outside) + self.main_frame.bind("", close_on_click_outside) + self.components['content_canvas'].bind("", close_on_click_outside) + self.components['content_canvas'].bind("", close_on_click_outside) + popup.focus_set() + def _unbind_mousewheel_scrolling(self): - """Unbind mousewheel scrolling""" + """Unbind mousewheel scrolling and mouse events""" try: self.main_frame.unbind_all("") + self.main_frame.unbind_all("") except Exception: pass @@ -363,7 +497,7 @@ class TagManagerPanel: dialog.title("Manage Tags") dialog.transient(self.main_frame) dialog.grab_set() - dialog.geometry("500x500") + dialog.geometry("600x600") top_frame = ttk.Frame(dialog, padding="8") top_frame.grid(row=0, column=0, sticky=(tk.W, tk.E)) @@ -471,7 +605,7 @@ class TagManagerPanel: ids_to_delete = [tid for tid, v in selected_tag_vars.items() if v.get()] if not ids_to_delete: return - if not messagebox.askyesno("Confirm Delete", f"Delete {len(ids_to_delete)} selected tag(s)? This will unlink them from photos."): + if not self.gui_core.create_large_messagebox(self.main_frame, "Confirm Delete", f"Delete {len(ids_to_delete)} selected tag(s)? This will unlink them from photos.", "askyesno"): return try: with self.db.get_db_connection() as conn: @@ -563,9 +697,11 @@ class TagManagerPanel: if total_removals > 0: changes_text.append(f"{total_removals} tag removal(s)") changes_summary = " and ".join(changes_text) - result = messagebox.askyesnocancel( + result = self.gui_core.create_large_messagebox( + self.main_frame, "Unsaved Changes", - f"You have unsaved changes: {changes_summary}.\n\nDo you want to save your changes before quitting?\n\nYes = Save and quit\nNo = Quit without saving\nCancel = Stay in dialog" + f"You have unsaved changes: {changes_summary}.\n\nDo you want to save your changes before quitting?\n\nYes = Save and quit\nNo = Quit without saving\nCancel = Stay in dialog", + "askyesnocancel" ) if result is True: self._save_tagging_changes() @@ -574,6 +710,9 @@ class TagManagerPanel: # In dashboard mode, we don't actually quit the window pass # In dashboard mode, we don't actually quit the window + # Navigate to home if callback is available (dashboard mode) + if self.on_navigate_home: + self.on_navigate_home() def _switch_view_mode(self, mode: str): """Switch between different view modes""" @@ -631,7 +770,7 @@ class TagManagerPanel: popup.title("Bulk Link Tags to Folder") popup.transient(self.main_frame) popup.grab_set() - popup.geometry("520x420") + popup.geometry("650x500") top_frame = ttk.Frame(popup, padding="8") top_frame.grid(row=0, column=0, sticky=(tk.W, tk.E)) @@ -888,7 +1027,7 @@ class TagManagerPanel: popup.title("Manage Photo Tags") popup.transient(self.main_frame) popup.grab_set() - popup.geometry("500x400") + popup.geometry("600x500") popup.resizable(True, True) top_frame = ttk.Frame(popup, padding="8") top_frame.grid(row=0, column=0, sticky=(tk.W, tk.E)) @@ -1051,27 +1190,50 @@ class TagManagerPanel: def _show_list_view(self): """Show the list view""" self._clear_content() - visible_cols = [col for col in self.column_config['list'] if self.column_visibility['list'][col['key']]] - col_count = len(visible_cols) + self.current_visible_cols = [col.copy() for col in self.column_config['list'] if self.column_visibility['list'][col['key']]] + col_count = len(self.current_visible_cols) if col_count == 0: ttk.Label(self.components['content_inner'], text="No columns visible. Right-click to show columns.", font=("Arial", 12), foreground="gray").grid(row=0, column=0, pady=20) return # Configure columns - for i, col in enumerate(visible_cols): + for i, col in enumerate(self.current_visible_cols): self.components['content_inner'].columnconfigure(i, weight=col['weight'], minsize=col['width']) - # Create header + # Create header with resizing separators header_frame = ttk.Frame(self.components['content_inner']) header_frame.grid(row=0, column=0, columnspan=col_count, sticky=(tk.W, tk.E), pady=(0, 5)) - for i, col in enumerate(visible_cols): - header_frame.columnconfigure(i, weight=col['weight'], minsize=col['width']) + for i, col in enumerate(self.current_visible_cols): + header_frame.columnconfigure(i * 2, weight=col['weight'], minsize=col['width']) + if i < len(self.current_visible_cols) - 1: + header_frame.columnconfigure(i * 2 + 1, weight=0, minsize=1) - for i, col in enumerate(visible_cols): + for i, col in enumerate(self.current_visible_cols): header_label = ttk.Label(header_frame, text=col['label'], font=("Arial", 10, "bold")) - header_label.grid(row=0, column=i, padx=5, sticky=tk.W) + header_label.grid(row=0, column=i * 2, padx=5, sticky=tk.W) + header_label.bind("", lambda e, mode='list': self._show_column_context_menu(e, mode)) + + # Add resizing separator between columns + if i < len(self.current_visible_cols) - 1: + separator_frame = tk.Frame(header_frame, width=10, bg='red', cursor="sb_h_double_arrow") + separator_frame.grid(row=0, column=i * 2 + 1, sticky='ns', padx=0) + separator_frame.grid_propagate(False) + inner_line = tk.Frame(separator_frame, bg='darkred', width=2) + inner_line.pack(fill=tk.Y, expand=True) + + # Bind resize events + separator_frame.bind("", lambda e, col_idx=i: self._start_resize(e, col_idx)) + separator_frame.bind("", lambda e, col_idx=i: self._do_resize(e, col_idx)) + separator_frame.bind("", self._stop_resize) + separator_frame.bind("", lambda e, f=separator_frame, l=inner_line: (f.configure(bg='orange'), l.configure(bg='darkorange'))) + separator_frame.bind("", lambda e, f=separator_frame, l=inner_line: (f.configure(bg='red'), l.configure(bg='darkred'))) + + inner_line.bind("", lambda e, col_idx=i: self._start_resize(e, col_idx)) + inner_line.bind("", lambda e, col_idx=i: self._do_resize(e, col_idx)) + inner_line.bind("", self._stop_resize) + header_frame.bind("", lambda e, mode='list': self._show_column_context_menu(e, mode)) ttk.Separator(self.components['content_inner'], orient='horizontal').grid(row=1, column=0, columnspan=col_count, sticky=(tk.W, tk.E), pady=(0, 5)) # Create folder grouped data and display @@ -1084,10 +1246,10 @@ class TagManagerPanel: for photo in folder_info['photos']: row_frame = ttk.Frame(self.components['content_inner']) row_frame.grid(row=current_row, column=0, columnspan=col_count, sticky=(tk.W, tk.E), pady=1) - for i, col in enumerate(visible_cols): + for i, col in enumerate(self.current_visible_cols): row_frame.columnconfigure(i, weight=col['weight'], minsize=col['width']) - for i, col in enumerate(visible_cols): + for i, col in enumerate(self.current_visible_cols): key = col['key'] if key == 'id': text = str(photo['id']) @@ -1148,7 +1310,9 @@ class TagManagerPanel: for i, col in enumerate(visible_cols): header_label = ttk.Label(header_frame, text=col['label'], font=("Arial", 10, "bold")) header_label.grid(row=0, column=i, padx=5, sticky=tk.W) + header_label.bind("", lambda e, mode='icons': self._show_column_context_menu(e, mode)) + header_frame.bind("", lambda e, mode='icons': self._show_column_context_menu(e, mode)) ttk.Separator(self.components['content_inner'], orient='horizontal').grid(row=1, column=0, columnspan=col_count, sticky=(tk.W, tk.E), pady=(0, 5)) # Create folder grouped data and display @@ -1247,7 +1411,9 @@ class TagManagerPanel: for i, col in enumerate(visible_cols): header_label = ttk.Label(header_frame, text=col['label'], font=("Arial", 10, "bold")) header_label.grid(row=0, column=i, padx=5, sticky=tk.W) + header_label.bind("", lambda e, mode='compact': self._show_column_context_menu(e, mode)) + header_frame.bind("", lambda e, mode='compact': self._show_column_context_menu(e, mode)) ttk.Separator(self.components['content_inner'], orient='horizontal').grid(row=1, column=0, columnspan=col_count, sticky=(tk.W, tk.E), pady=(0, 5)) # Create folder grouped data and display