Enhance Dashboard GUI with smart navigation and unified exit behavior
This commit introduces a compact home icon for quick navigation to the welcome screen, improving user experience across all panels. Additionally, all exit buttons now navigate to the home screen instead of closing the application, ensuring a consistent exit behavior. The README has been updated to reflect these enhancements, emphasizing the improved navigation and user experience in the unified dashboard.
This commit is contained in:
parent
cbc29a9429
commit
3e88e2cd2c
@ -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
|
||||
|
||||
|
||||
@ -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"""
|
||||
|
||||
@ -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
|
||||
|
||||
61
gui_core.py
61
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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"""
|
||||
|
||||
@ -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("<MouseWheel>", on_mousewheel)
|
||||
self.main_frame.bind_all("<ButtonRelease-1>", 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("<Button-1>", close_on_click_outside)
|
||||
self.main_frame.bind("<Button-3>", close_on_click_outside)
|
||||
self.components['content_canvas'].bind("<Button-1>", close_on_click_outside)
|
||||
self.components['content_canvas'].bind("<Button-3>", 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("<MouseWheel>")
|
||||
self.main_frame.unbind_all("<ButtonRelease-1>")
|
||||
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("<Button-3>", 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("<Button-1>", lambda e, col_idx=i: self._start_resize(e, col_idx))
|
||||
separator_frame.bind("<B1-Motion>", lambda e, col_idx=i: self._do_resize(e, col_idx))
|
||||
separator_frame.bind("<ButtonRelease-1>", self._stop_resize)
|
||||
separator_frame.bind("<Enter>", lambda e, f=separator_frame, l=inner_line: (f.configure(bg='orange'), l.configure(bg='darkorange')))
|
||||
separator_frame.bind("<Leave>", lambda e, f=separator_frame, l=inner_line: (f.configure(bg='red'), l.configure(bg='darkred')))
|
||||
|
||||
inner_line.bind("<Button-1>", lambda e, col_idx=i: self._start_resize(e, col_idx))
|
||||
inner_line.bind("<B1-Motion>", lambda e, col_idx=i: self._do_resize(e, col_idx))
|
||||
inner_line.bind("<ButtonRelease-1>", self._stop_resize)
|
||||
|
||||
header_frame.bind("<Button-3>", 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("<Button-3>", lambda e, mode='icons': self._show_column_context_menu(e, mode))
|
||||
|
||||
header_frame.bind("<Button-3>", 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("<Button-3>", lambda e, mode='compact': self._show_column_context_menu(e, mode))
|
||||
|
||||
header_frame.bind("<Button-3>", 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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user