diff --git a/photo_tagger.py b/photo_tagger.py index 4d0ca3c..58b4aae 100644 --- a/photo_tagger.py +++ b/photo_tagger.py @@ -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 diff --git a/search_gui.py b/search_gui.py index aba1ef2..e7f6d5b 100644 --- a/search_gui.py +++ b/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("", 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('', 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("", 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 diff --git a/tag_manager_gui.py b/tag_manager_gui.py index 5855fc9..cb0c2f1 100644 --- a/tag_manager_gui.py +++ b/tag_manager_gui.py @@ -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("", 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("", 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("", 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("", 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("", 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("", 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("", 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("", 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