Enhance Search GUI with photo selection and tagging features
This commit updates the SearchGUI class to include a checkbox for selecting photos, allowing users to tag multiple photos at once. New buttons for tagging selected photos and clearing selections have been added, improving the user experience in managing photo tags. Additionally, the GUI now displays tags associated with each photo, enhancing the functionality of the search interface. The PhotoTagger class is updated to accommodate these changes, streamlining the tagging process.
This commit is contained in:
parent
55cd82943a
commit
9ec8b78b05
@ -49,7 +49,7 @@ class PhotoTagger:
|
||||
self.auto_match_gui = AutoMatchGUI(self.db, self.face_processor, verbose)
|
||||
self.modify_identified_gui = ModifyIdentifiedGUI(self.db, self.face_processor, verbose)
|
||||
self.tag_manager_gui = TagManagerGUI(self.db, self.gui_core, self.tag_manager, self.face_processor, verbose)
|
||||
self.search_gui = SearchGUI(self.db, self.search_stats, self.gui_core, verbose)
|
||||
self.search_gui = SearchGUI(self.db, self.search_stats, self.gui_core, self.tag_manager, verbose)
|
||||
self.dashboard_gui = DashboardGUI(self.gui_core, on_scan=self._dashboard_scan, on_process=self._dashboard_process, on_identify=self._dashboard_identify)
|
||||
|
||||
# Legacy compatibility - expose some methods directly
|
||||
|
||||
348
search_gui.py
348
search_gui.py
@ -12,6 +12,7 @@ from typing import List
|
||||
from gui_core import GUICore
|
||||
from search_stats import SearchStats
|
||||
from database import DatabaseManager
|
||||
from tag_management import TagManager
|
||||
|
||||
|
||||
class SearchGUI:
|
||||
@ -30,15 +31,19 @@ class SearchGUI:
|
||||
"Face quality distribution (planned)",
|
||||
]
|
||||
|
||||
def __init__(self, db_manager: DatabaseManager, search_stats: SearchStats, gui_core: GUICore, verbose: int = 0):
|
||||
def __init__(self, db_manager: DatabaseManager, search_stats: SearchStats, gui_core: GUICore, tag_manager: TagManager = None, verbose: int = 0):
|
||||
self.db = db_manager
|
||||
self.search_stats = search_stats
|
||||
self.gui_core = gui_core
|
||||
self.tag_manager = tag_manager or TagManager(db_manager, verbose)
|
||||
self.verbose = verbose
|
||||
|
||||
# Sorting state
|
||||
self.sort_column = None
|
||||
self.sort_reverse = False
|
||||
|
||||
# Selection tracking
|
||||
self.selected_photos = {} # photo_path -> photo_data
|
||||
|
||||
def search_gui(self) -> int:
|
||||
"""Open the Search GUI window."""
|
||||
@ -79,16 +84,18 @@ class SearchGUI:
|
||||
results_frame.pack(fill=tk.BOTH, expand=True)
|
||||
ttk.Label(results_frame, text="Results:", font=("Arial", 10, "bold")).pack(anchor="w")
|
||||
|
||||
columns = ("person", "open_dir", "open_photo", "path")
|
||||
columns = ("person", "open_dir", "open_photo", "path", "select")
|
||||
tree = ttk.Treeview(results_frame, columns=columns, show="headings", selectmode="browse")
|
||||
tree.heading("person", text="Person", command=lambda: sort_treeview("person"))
|
||||
tree.heading("open_dir", text="📁")
|
||||
tree.heading("open_photo", text="📷")
|
||||
tree.heading("open_photo", text="🏷️")
|
||||
tree.heading("path", text="Photo path", command=lambda: sort_treeview("path"))
|
||||
tree.heading("select", text="☑")
|
||||
tree.column("person", width=220, anchor="w")
|
||||
tree.column("open_dir", width=50, anchor="center")
|
||||
tree.column("open_photo", width=50, anchor="center")
|
||||
tree.column("path", width=520, anchor="w")
|
||||
tree.column("select", width=50, anchor="center")
|
||||
tree.pack(fill=tk.BOTH, expand=True, pady=(4, 0))
|
||||
|
||||
# Buttons
|
||||
@ -98,6 +105,10 @@ class SearchGUI:
|
||||
search_btn.pack(side=tk.LEFT)
|
||||
open_btn = ttk.Button(btns, text="Open Photo", command=lambda: open_selected())
|
||||
open_btn.pack(side=tk.LEFT, padx=(6, 0))
|
||||
tag_btn = ttk.Button(btns, text="Tag selected photos", command=lambda: tag_selected_photos())
|
||||
tag_btn.pack(side=tk.LEFT, padx=(6, 0))
|
||||
clear_btn = ttk.Button(btns, text="Clear all selected", command=lambda: clear_all_selected())
|
||||
clear_btn.pack(side=tk.LEFT, padx=(6, 0))
|
||||
close_btn = ttk.Button(btns, text="Close", command=lambda: root.destroy())
|
||||
close_btn.pack(side=tk.RIGHT)
|
||||
|
||||
@ -167,12 +178,14 @@ class SearchGUI:
|
||||
# Reset sorting state for new search
|
||||
self.sort_column = None
|
||||
self.sort_reverse = False
|
||||
# Clear selection tracking
|
||||
self.selected_photos.clear()
|
||||
update_header_display()
|
||||
|
||||
def add_results(rows: List[tuple]):
|
||||
# rows expected: List[(full_name, path)]
|
||||
for full_name, p in rows:
|
||||
tree.insert("", tk.END, values=(full_name, "📁", "📷", p))
|
||||
tree.insert("", tk.END, values=(full_name, "📁", "🏷️", p, "☐"))
|
||||
|
||||
# Sort by person name by default when results are first loaded
|
||||
if rows and self.sort_column is None:
|
||||
@ -233,6 +246,312 @@ class SearchGUI:
|
||||
except Exception:
|
||||
messagebox.showerror("Open Location", "Failed to open the file location.", parent=root)
|
||||
|
||||
def toggle_photo_selection(row_id, vals):
|
||||
"""Toggle checkbox selection for a photo."""
|
||||
if len(vals) < 5:
|
||||
return
|
||||
path = vals[3]
|
||||
current_state = vals[4]
|
||||
if current_state == "☐":
|
||||
# Select photo
|
||||
new_state = "☑"
|
||||
self.selected_photos[path] = {
|
||||
'person': vals[0],
|
||||
'path': path
|
||||
}
|
||||
else:
|
||||
# Deselect photo
|
||||
new_state = "☐"
|
||||
if path in self.selected_photos:
|
||||
del self.selected_photos[path]
|
||||
|
||||
# Update the treeview
|
||||
new_vals = list(vals)
|
||||
new_vals[4] = new_state
|
||||
tree.item(row_id, values=new_vals)
|
||||
|
||||
def tag_selected_photos():
|
||||
"""Open linkage dialog for selected photos."""
|
||||
if not self.selected_photos:
|
||||
messagebox.showinfo("Tag Photos", "Please select photos to tag first.", parent=root)
|
||||
return
|
||||
|
||||
# Get photo IDs for selected photos
|
||||
selected_photo_ids = []
|
||||
with self.db.get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
for path in self.selected_photos.keys():
|
||||
cursor.execute('SELECT id FROM photos WHERE path = ?', (path,))
|
||||
result = cursor.fetchone()
|
||||
if result:
|
||||
selected_photo_ids.append(result[0])
|
||||
|
||||
if not selected_photo_ids:
|
||||
messagebox.showerror("Tag Photos", "Could not find photo IDs for selected photos.", parent=root)
|
||||
return
|
||||
|
||||
# Open the linkage dialog
|
||||
open_linkage_dialog(selected_photo_ids)
|
||||
|
||||
def clear_all_selected():
|
||||
"""Clear all selected photos and update checkboxes."""
|
||||
if not self.selected_photos:
|
||||
return
|
||||
|
||||
# Clear the selection tracking
|
||||
self.selected_photos.clear()
|
||||
|
||||
# Update all checkboxes to unselected state
|
||||
for item in tree.get_children():
|
||||
vals = tree.item(item, "values")
|
||||
if len(vals) >= 5 and vals[4] == "☑":
|
||||
new_vals = list(vals)
|
||||
new_vals[4] = "☐"
|
||||
tree.item(item, values=new_vals)
|
||||
|
||||
def show_photo_tags(photo_path):
|
||||
"""Show tags for a specific photo in a popup."""
|
||||
# Get photo ID
|
||||
photo_id = None
|
||||
with self.db.get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT id FROM photos WHERE path = ?', (photo_path,))
|
||||
result = cursor.fetchone()
|
||||
if result:
|
||||
photo_id = result[0]
|
||||
|
||||
if not photo_id:
|
||||
messagebox.showerror("Error", "Could not find photo ID", parent=root)
|
||||
return
|
||||
|
||||
# Get tags for this photo
|
||||
tag_names = []
|
||||
with self.db.get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
SELECT t.tag_name
|
||||
FROM tags t
|
||||
JOIN phototaglinkage ptl ON t.id = ptl.tag_id
|
||||
WHERE ptl.photo_id = ?
|
||||
ORDER BY t.tag_name
|
||||
''', (photo_id,))
|
||||
tag_names = [row[0] for row in cursor.fetchall()]
|
||||
|
||||
# Create popup
|
||||
popup = tk.Toplevel(root)
|
||||
popup.title("Photo Tags")
|
||||
popup.transient(root)
|
||||
popup.grab_set()
|
||||
popup.geometry("300x200")
|
||||
|
||||
# Center the popup
|
||||
popup.update_idletasks()
|
||||
x = (popup.winfo_screenwidth() // 2) - (popup.winfo_width() // 2)
|
||||
y = (popup.winfo_screenheight() // 2) - (popup.winfo_height() // 2)
|
||||
popup.geometry(f"+{x}+{y}")
|
||||
|
||||
frame = ttk.Frame(popup, padding="10")
|
||||
frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# Photo filename
|
||||
filename = os.path.basename(photo_path)
|
||||
ttk.Label(frame, text=f"Tags for: {filename}", font=("Arial", 10, "bold")).pack(anchor="w", pady=(0, 10))
|
||||
|
||||
if tag_names:
|
||||
# Create scrollable list
|
||||
canvas = tk.Canvas(frame, height=100)
|
||||
scrollbar = ttk.Scrollbar(frame, orient="vertical", command=canvas.yview)
|
||||
scrollable_frame = ttk.Frame(canvas)
|
||||
scrollable_frame.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
|
||||
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
|
||||
canvas.configure(yscrollcommand=scrollbar.set)
|
||||
canvas.pack(side="left", fill="both", expand=True)
|
||||
scrollbar.pack(side="right", fill="y")
|
||||
|
||||
for tag_name in tag_names:
|
||||
ttk.Label(scrollable_frame, text=f"• {tag_name}").pack(anchor="w", pady=1)
|
||||
else:
|
||||
ttk.Label(frame, text="No tags found for this photo", foreground="gray").pack(anchor="w")
|
||||
|
||||
# Close button
|
||||
ttk.Button(frame, text="Close", command=popup.destroy).pack(pady=(10, 0))
|
||||
|
||||
def get_photo_tags_tooltip(photo_path):
|
||||
"""Get tags for a photo to display in tooltip."""
|
||||
# Get photo ID
|
||||
photo_id = None
|
||||
with self.db.get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT id FROM photos WHERE path = ?', (photo_path,))
|
||||
result = cursor.fetchone()
|
||||
if result:
|
||||
photo_id = result[0]
|
||||
|
||||
if not photo_id:
|
||||
return "No photo found"
|
||||
|
||||
# Get tags for this photo
|
||||
tag_names = []
|
||||
with self.db.get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
SELECT t.tag_name
|
||||
FROM tags t
|
||||
JOIN phototaglinkage ptl ON t.id = ptl.tag_id
|
||||
WHERE ptl.photo_id = ?
|
||||
ORDER BY t.tag_name
|
||||
''', (photo_id,))
|
||||
tag_names = [row[0] for row in cursor.fetchall()]
|
||||
|
||||
if tag_names:
|
||||
if len(tag_names) <= 5:
|
||||
return f"Tags: {', '.join(tag_names)}"
|
||||
else:
|
||||
return f"Tags: {', '.join(tag_names[:5])}... (+{len(tag_names)-5} more)"
|
||||
else:
|
||||
return "No tags"
|
||||
|
||||
def open_linkage_dialog(photo_ids):
|
||||
"""Open the linkage dialog for selected photos using tag manager functionality."""
|
||||
popup = tk.Toplevel(root)
|
||||
popup.title("Tag Selected Photos")
|
||||
popup.transient(root)
|
||||
popup.grab_set()
|
||||
popup.geometry("500x400")
|
||||
popup.resizable(True, True)
|
||||
|
||||
top_frame = ttk.Frame(popup, padding="8")
|
||||
top_frame.grid(row=0, column=0, sticky=(tk.W, tk.E))
|
||||
list_frame = ttk.Frame(popup, padding="8")
|
||||
list_frame.grid(row=1, column=0, sticky=(tk.N, tk.S, tk.W, tk.E))
|
||||
bottom_frame = ttk.Frame(popup, padding="8")
|
||||
bottom_frame.grid(row=2, column=0, sticky=(tk.W, tk.E))
|
||||
popup.columnconfigure(0, weight=1)
|
||||
popup.rowconfigure(1, weight=1)
|
||||
|
||||
ttk.Label(top_frame, text=f"Selected Photos: {len(photo_ids)}").grid(row=0, column=0, columnspan=3, sticky=tk.W, pady=(0,6))
|
||||
ttk.Label(top_frame, text="Add tag:").grid(row=1, column=0, padx=(0, 8), sticky=tk.W)
|
||||
|
||||
# Get existing tags using tag manager
|
||||
tag_id_to_name, tag_name_to_id = self.db.load_tag_mappings()
|
||||
existing_tags = sorted(tag_name_to_id.keys())
|
||||
|
||||
tag_var = tk.StringVar()
|
||||
combo = ttk.Combobox(top_frame, textvariable=tag_var, values=existing_tags, width=30)
|
||||
combo.grid(row=1, column=1, padx=(0, 8), sticky=(tk.W, tk.E))
|
||||
combo.focus_set()
|
||||
|
||||
def add_selected_tag():
|
||||
tag_name = tag_var.get().strip()
|
||||
if not tag_name:
|
||||
return
|
||||
|
||||
# Resolve or create tag id
|
||||
if tag_name in tag_name_to_id:
|
||||
tag_id = tag_name_to_id[tag_name]
|
||||
else:
|
||||
# Create new tag in database
|
||||
with self.db.get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('INSERT OR IGNORE INTO tags (tag_name) VALUES (?)', (tag_name,))
|
||||
cursor.execute('SELECT id FROM tags WHERE tag_name = ?', (tag_name,))
|
||||
result = cursor.fetchone()
|
||||
if result:
|
||||
tag_id = result[0]
|
||||
tag_name_to_id[tag_name] = tag_id
|
||||
tag_id_to_name[tag_id] = tag_name
|
||||
if tag_name not in existing_tags:
|
||||
existing_tags.append(tag_name)
|
||||
existing_tags.sort()
|
||||
# Update the combobox values to include the new tag
|
||||
combo['values'] = existing_tags
|
||||
else:
|
||||
messagebox.showerror("Error", f"Failed to create tag '{tag_name}'", parent=popup)
|
||||
return
|
||||
|
||||
# Add tag to all selected photos
|
||||
affected = 0
|
||||
with self.db.get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
for photo_id in photo_ids:
|
||||
# Check if tag already exists for this photo
|
||||
cursor.execute('SELECT linkage_id FROM phototaglinkage WHERE photo_id = ? AND tag_id = ?', (photo_id, tag_id))
|
||||
if not cursor.fetchone():
|
||||
# Add the tag with single linkage type (0)
|
||||
cursor.execute('INSERT INTO phototaglinkage (photo_id, tag_id, linkage_type) VALUES (?, ?, 0)', (photo_id, tag_id))
|
||||
affected += 1
|
||||
|
||||
# Refresh the tag list to show the new tag
|
||||
refresh_tag_list()
|
||||
tag_var.set("")
|
||||
|
||||
ttk.Button(top_frame, text="Add", command=add_selected_tag).grid(row=1, column=2, padx=(8, 0))
|
||||
|
||||
# Allow Enter key to add tag
|
||||
combo.bind('<Return>', lambda e: add_selected_tag())
|
||||
|
||||
# Create scrollable tag list
|
||||
canvas = tk.Canvas(list_frame, height=200)
|
||||
scrollbar = ttk.Scrollbar(list_frame, orient="vertical", command=canvas.yview)
|
||||
scrollable_frame = ttk.Frame(canvas)
|
||||
scrollable_frame.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
|
||||
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
|
||||
canvas.configure(yscrollcommand=scrollbar.set)
|
||||
canvas.pack(side="left", fill="both", expand=True)
|
||||
scrollbar.pack(side="right", fill="y")
|
||||
|
||||
selected_tag_vars = {}
|
||||
|
||||
def refresh_tag_list():
|
||||
for widget in scrollable_frame.winfo_children():
|
||||
widget.destroy()
|
||||
selected_tag_vars.clear()
|
||||
|
||||
# Get all unique tags from selected photos
|
||||
all_tag_ids = set()
|
||||
with self.db.get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
for photo_id in photo_ids:
|
||||
cursor.execute('SELECT tag_id FROM phototaglinkage WHERE photo_id = ?', (photo_id,))
|
||||
for row in cursor.fetchall():
|
||||
all_tag_ids.add(row[0])
|
||||
|
||||
if not all_tag_ids:
|
||||
ttk.Label(scrollable_frame, text="No tags linked to selected photos", foreground="gray").pack(anchor=tk.W, pady=5)
|
||||
return
|
||||
|
||||
for tag_id in sorted(all_tag_ids):
|
||||
tag_name = tag_id_to_name.get(tag_id, f"Unknown {tag_id}")
|
||||
var = tk.BooleanVar()
|
||||
selected_tag_vars[tag_name] = var
|
||||
frame = ttk.Frame(scrollable_frame)
|
||||
frame.pack(fill=tk.X, pady=1)
|
||||
cb = ttk.Checkbutton(frame, variable=var)
|
||||
cb.pack(side=tk.LEFT, padx=(0, 5))
|
||||
ttk.Label(frame, text=tag_name).pack(side=tk.LEFT)
|
||||
|
||||
def remove_selected_tags():
|
||||
tag_ids_to_remove = []
|
||||
for tag_name, var in selected_tag_vars.items():
|
||||
if var.get() and tag_name in tag_name_to_id:
|
||||
tag_ids_to_remove.append(tag_name_to_id[tag_name])
|
||||
|
||||
if not tag_ids_to_remove:
|
||||
return
|
||||
|
||||
# Remove tags from all selected photos
|
||||
with self.db.get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
for photo_id in photo_ids:
|
||||
for tag_id in tag_ids_to_remove:
|
||||
cursor.execute('DELETE FROM phototaglinkage WHERE photo_id = ? AND tag_id = ?', (photo_id, tag_id))
|
||||
|
||||
refresh_tag_list()
|
||||
|
||||
ttk.Button(bottom_frame, text="Remove selected tags", command=remove_selected_tags).pack(side=tk.LEFT, padx=(0, 8))
|
||||
ttk.Button(bottom_frame, text="Close", command=popup.destroy).pack(side=tk.RIGHT)
|
||||
refresh_tag_list()
|
||||
|
||||
# Click handling on icon columns
|
||||
def on_tree_click(event):
|
||||
region = tree.identify("region", event.x, event.y)
|
||||
@ -243,12 +562,14 @@ class SearchGUI:
|
||||
if not row_id or not col_id:
|
||||
return
|
||||
vals = tree.item(row_id, "values")
|
||||
if not vals or len(vals) < 4:
|
||||
if not vals or len(vals) < 5:
|
||||
return
|
||||
path = vals[3]
|
||||
if col_id == "#2":
|
||||
open_dir(path)
|
||||
elif col_id == "#3":
|
||||
elif col_id == "#3": # Tag icon column
|
||||
show_photo_tags(path)
|
||||
elif col_id == "#4": # Photo path column - clickable to open photo
|
||||
try:
|
||||
if os.name == "nt":
|
||||
os.startfile(path) # type: ignore[attr-defined]
|
||||
@ -260,6 +581,8 @@ class SearchGUI:
|
||||
subprocess.run(["xdg-open", path], check=False)
|
||||
except Exception:
|
||||
messagebox.showerror("Open Photo", "Failed to open the selected photo.", parent=root)
|
||||
elif col_id == "#5": # Checkbox column
|
||||
toggle_photo_selection(row_id, vals)
|
||||
|
||||
# Tooltip for icon cells
|
||||
tooltip = None
|
||||
@ -291,10 +614,20 @@ class SearchGUI:
|
||||
tree.config(cursor="")
|
||||
return
|
||||
col_id = tree.identify_column(event.x)
|
||||
row_id = tree.identify_row(event.y)
|
||||
if col_id == "#2":
|
||||
tree.config(cursor="hand2")
|
||||
show_tooltip(tree, event.x_root, event.y_root, "Open file location")
|
||||
elif col_id == "#3":
|
||||
tree.config(cursor="hand2")
|
||||
# Show tags tooltip
|
||||
if row_id:
|
||||
vals = tree.item(row_id, "values")
|
||||
if len(vals) >= 4:
|
||||
path = vals[3]
|
||||
tags_text = get_photo_tags_tooltip(path)
|
||||
show_tooltip(tree, event.x_root, event.y_root, tags_text)
|
||||
elif col_id == "#4":
|
||||
tree.config(cursor="hand2")
|
||||
show_tooltip(tree, event.x_root, event.y_root, "Open photo")
|
||||
else:
|
||||
@ -312,7 +645,8 @@ class SearchGUI:
|
||||
|
||||
# Show and center
|
||||
root.update_idletasks()
|
||||
self.gui_core.center_window(root, 800, 500)
|
||||
# Widened to ensure the checkbox column is visible by default
|
||||
self.gui_core.center_window(root, 900, 520)
|
||||
root.deiconify()
|
||||
root.mainloop()
|
||||
return 0
|
||||
|
||||
@ -23,6 +23,8 @@ class TagManagerGUI:
|
||||
from tkinter import ttk, messagebox, simpledialog
|
||||
from PIL import Image, ImageTk
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
# Create the main window
|
||||
root = tk.Tk()
|
||||
@ -317,6 +319,59 @@ class TagManagerGUI:
|
||||
save_button = ttk.Button(bottom_frame, text="Save Tagging")
|
||||
save_button.pack(side=tk.RIGHT, padx=10, pady=5)
|
||||
|
||||
def open_photo(photo_path: str):
|
||||
if not os.path.exists(photo_path):
|
||||
try:
|
||||
messagebox.showerror("File not found", f"Photo does not exist:\n{photo_path}")
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
try:
|
||||
# Open in an in-app preview window sized reasonably compared to the main GUI
|
||||
img = Image.open(photo_path)
|
||||
screen_w = root.winfo_screenwidth()
|
||||
screen_h = root.winfo_screenheight()
|
||||
max_w = int(min(1000, screen_w * 0.6))
|
||||
max_h = int(min(800, screen_h * 0.6))
|
||||
preview = tk.Toplevel(root)
|
||||
preview.title(os.path.basename(photo_path))
|
||||
preview.transient(root)
|
||||
# Resize image to fit nicely while keeping aspect ratio
|
||||
img_copy = img.copy()
|
||||
img_copy.thumbnail((max_w, max_h), Image.Resampling.LANCZOS)
|
||||
photo_img = ImageTk.PhotoImage(img_copy)
|
||||
photo_images.append(photo_img)
|
||||
pad = 12
|
||||
w, h = photo_img.width(), photo_img.height()
|
||||
# Center the window roughly relative to screen
|
||||
x = int((screen_w - (w + pad)) / 2)
|
||||
y = int((screen_h - (h + pad)) / 2)
|
||||
preview.geometry(f"{w + pad}x{h + pad}+{max(x,0)}+{max(y,0)}")
|
||||
canvas = tk.Canvas(preview, width=w, height=h, highlightthickness=0)
|
||||
canvas.pack(padx=pad//2, pady=pad//2)
|
||||
canvas.create_image(w // 2, h // 2, image=photo_img)
|
||||
try:
|
||||
_ToolTip(canvas, os.path.basename(photo_path))
|
||||
except Exception:
|
||||
pass
|
||||
preview.focus_set()
|
||||
except Exception:
|
||||
# Fallback to system default opener if preview fails for any reason
|
||||
try:
|
||||
if sys.platform.startswith('linux'):
|
||||
subprocess.Popen(['xdg-open', photo_path])
|
||||
elif sys.platform == 'darwin':
|
||||
subprocess.Popen(['open', photo_path])
|
||||
elif os.name == 'nt':
|
||||
os.startfile(photo_path) # type: ignore[attr-defined]
|
||||
else:
|
||||
Image.open(photo_path).show()
|
||||
except Exception as e:
|
||||
try:
|
||||
messagebox.showerror("Error", f"Failed to open photo:\n{e}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def quit_with_warning():
|
||||
has_pending_changes = bool(pending_tag_changes or pending_tag_removals)
|
||||
if has_pending_changes:
|
||||
@ -413,6 +468,7 @@ class TagManagerGUI:
|
||||
root.bind("<Destroy>", lambda e: cleanup_mousewheel())
|
||||
|
||||
photos_data: List[Dict] = []
|
||||
people_names_cache: Dict[int, List[str]] = {} # {photo_id: [list of people names]}
|
||||
|
||||
column_visibility = {
|
||||
'list': {'id': True, 'filename': True, 'path': True, 'processed': True, 'date_taken': True, 'faces': True, 'tags': True},
|
||||
@ -446,8 +502,56 @@ class TagManagerGUI:
|
||||
]
|
||||
}
|
||||
|
||||
def show_people_names_popup(photo_id: int, photo_filename: str):
|
||||
"""Show a popup window with the names of people identified in this photo"""
|
||||
nonlocal people_names_cache
|
||||
people_names = people_names_cache.get(photo_id, [])
|
||||
if not people_names:
|
||||
try:
|
||||
messagebox.showinfo("No People Identified", f"No people have been identified in {photo_filename}")
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
|
||||
popup = tk.Toplevel(root)
|
||||
popup.title(f"People in {photo_filename}")
|
||||
popup.transient(root)
|
||||
popup.geometry("400x300")
|
||||
popup.resizable(True, True)
|
||||
# Don't use grab_set() as it can cause issues
|
||||
# popup.grab_set()
|
||||
|
||||
# Header
|
||||
header_frame = ttk.Frame(popup, padding="10")
|
||||
header_frame.pack(fill=tk.X)
|
||||
ttk.Label(header_frame, text=f"People identified in:", font=("Arial", 12, "bold")).pack(anchor=tk.W)
|
||||
ttk.Label(header_frame, text=photo_filename, font=("Arial", 10)).pack(anchor=tk.W, pady=(2, 0))
|
||||
|
||||
# List of people
|
||||
list_frame = ttk.Frame(popup, padding="10")
|
||||
list_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
canvas = tk.Canvas(list_frame, highlightthickness=0)
|
||||
scrollbar = ttk.Scrollbar(list_frame, orient="vertical", command=canvas.yview)
|
||||
scrollable_frame = ttk.Frame(canvas)
|
||||
scrollable_frame.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
|
||||
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
|
||||
canvas.configure(yscrollcommand=scrollbar.set)
|
||||
canvas.pack(side="left", fill="both", expand=True)
|
||||
scrollbar.pack(side="right", fill="y")
|
||||
|
||||
for i, name in enumerate(people_names):
|
||||
ttk.Label(scrollable_frame, text=f"• {name}", font=("Arial", 11)).pack(anchor=tk.W, pady=2)
|
||||
|
||||
# Close button
|
||||
button_frame = ttk.Frame(popup, padding="10")
|
||||
button_frame.pack(fill=tk.X)
|
||||
ttk.Button(button_frame, text="Close", command=popup.destroy).pack(side=tk.RIGHT)
|
||||
|
||||
popup.focus_set()
|
||||
|
||||
def load_photos():
|
||||
nonlocal photos_data
|
||||
nonlocal photos_data, people_names_cache
|
||||
with self.db.get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
@ -473,6 +577,28 @@ class TagManagerGUI:
|
||||
'face_count': row[6] or 0,
|
||||
'tags': row[7] or ""
|
||||
})
|
||||
|
||||
# Cache people names for each photo
|
||||
people_names_cache.clear()
|
||||
cursor.execute('''
|
||||
SELECT f.photo_id, pe.first_name, pe.last_name
|
||||
FROM faces f
|
||||
JOIN people pe ON pe.id = f.person_id
|
||||
WHERE f.person_id IS NOT NULL
|
||||
ORDER BY pe.last_name, pe.first_name
|
||||
''')
|
||||
cache_rows = cursor.fetchall()
|
||||
for row in cache_rows:
|
||||
photo_id, first_name, last_name = row
|
||||
if photo_id not in people_names_cache:
|
||||
people_names_cache[photo_id] = []
|
||||
# Format as "Last, First" or just "First" if no last name
|
||||
if last_name and first_name:
|
||||
name = f"{last_name}, {first_name}"
|
||||
else:
|
||||
name = first_name or last_name or "Unknown"
|
||||
if name not in people_names_cache[photo_id]:
|
||||
people_names_cache[photo_id].append(name)
|
||||
|
||||
def prepare_folder_grouped_data():
|
||||
from collections import defaultdict
|
||||
@ -1192,7 +1318,20 @@ class TagManagerGUI:
|
||||
create_tag_buttons_frame(row_frame, photo['id'], photo['tags'], existing_tags, use_grid=True, row=0, col=i)
|
||||
continue
|
||||
# Render text wrapped to header width; do not auto-resize columns
|
||||
ttk.Label(row_frame, text=text, wraplength=col['width'], anchor='w', justify='left').grid(row=0, column=i, padx=5, sticky=tk.W)
|
||||
if key == 'filename':
|
||||
lbl = ttk.Label(row_frame, text=text, foreground="blue", cursor="hand2", wraplength=col['width'], anchor='w', justify='left')
|
||||
lbl.grid(row=0, column=i, padx=5, sticky=tk.W)
|
||||
lbl.bind("<Button-1>", lambda e, p=photo['path']: open_photo(p))
|
||||
elif key == 'faces' and photo['face_count'] > 0:
|
||||
lbl = ttk.Label(row_frame, text=text, foreground="blue", cursor="hand2", wraplength=col['width'], anchor='w', justify='left')
|
||||
lbl.grid(row=0, column=i, padx=5, sticky=tk.W)
|
||||
lbl.bind("<Button-1>", lambda e, pid=photo['id'], fname=photo['filename']: show_people_names_popup(pid, fname))
|
||||
try:
|
||||
_ToolTip(lbl, "Click to see people in this photo")
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
ttk.Label(row_frame, text=text, wraplength=col['width'], anchor='w', justify='left').grid(row=0, column=i, padx=5, sticky=tk.W)
|
||||
current_row += 1
|
||||
|
||||
def show_icon_view():
|
||||
@ -1264,7 +1403,20 @@ class TagManagerGUI:
|
||||
col_idx += 1
|
||||
continue
|
||||
# Render text wrapped to header width; do not auto-resize columns
|
||||
ttk.Label(row_frame, text=text, wraplength=col['width'], anchor='w', justify='left').grid(row=0, column=col_idx, padx=5, sticky=tk.W)
|
||||
if key == 'filename':
|
||||
lbl = ttk.Label(row_frame, text=text, foreground="blue", cursor="hand2", wraplength=col['width'], anchor='w', justify='left')
|
||||
lbl.grid(row=0, column=col_idx, padx=5, sticky=tk.W)
|
||||
lbl.bind("<Button-1>", lambda e, p=photo['path']: open_photo(p))
|
||||
elif key == 'faces' and photo['face_count'] > 0:
|
||||
lbl = ttk.Label(row_frame, text=text, foreground="blue", cursor="hand2", wraplength=col['width'], anchor='w', justify='left')
|
||||
lbl.grid(row=0, column=col_idx, padx=5, sticky=tk.W)
|
||||
lbl.bind("<Button-1>", lambda e, pid=photo['id'], fname=photo['filename']: show_people_names_popup(pid, fname))
|
||||
try:
|
||||
_ToolTip(lbl, "Click to see people in this photo")
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
ttk.Label(row_frame, text=text, wraplength=col['width'], anchor='w', justify='left').grid(row=0, column=col_idx, padx=5, sticky=tk.W)
|
||||
col_idx += 1
|
||||
current_row += 1
|
||||
|
||||
@ -1310,7 +1462,20 @@ class TagManagerGUI:
|
||||
col_idx += 1
|
||||
continue
|
||||
# Render text wrapped to header width; do not auto-resize columns
|
||||
ttk.Label(row_frame, text=text, wraplength=col['width'], anchor='w', justify='left').grid(row=0, column=col_idx, padx=5, sticky=tk.W)
|
||||
if key == 'filename':
|
||||
lbl = ttk.Label(row_frame, text=text, foreground="blue", cursor="hand2", wraplength=col['width'], anchor='w', justify='left')
|
||||
lbl.grid(row=0, column=col_idx, padx=5, sticky=tk.W)
|
||||
lbl.bind("<Button-1>", lambda e, p=photo['path']: open_photo(p))
|
||||
elif key == 'faces' and photo['face_count'] > 0:
|
||||
lbl = ttk.Label(row_frame, text=text, foreground="blue", cursor="hand2", wraplength=col['width'], anchor='w', justify='left')
|
||||
lbl.grid(row=0, column=col_idx, padx=5, sticky=tk.W)
|
||||
lbl.bind("<Button-1>", lambda e, pid=photo['id'], fname=photo['filename']: show_people_names_popup(pid, fname))
|
||||
try:
|
||||
_ToolTip(lbl, "Click to see people in this photo")
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
ttk.Label(row_frame, text=text, wraplength=col['width'], anchor='w', justify='left').grid(row=0, column=col_idx, padx=5, sticky=tk.W)
|
||||
col_idx += 1
|
||||
current_row += 1
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user