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:
tanyar09 2025-10-07 15:03:20 -04:00
parent 55cd82943a
commit 9ec8b78b05
3 changed files with 511 additions and 12 deletions

View File

@ -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

View File

@ -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

View File

@ -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