#!/usr/bin/env python3 """ Search GUI implementation for PunimTag """ import os import sys import tkinter as tk from tkinter import ttk, messagebox from typing import List from gui_core import GUICore from search_stats import SearchStats from database import DatabaseManager class SearchGUI: """GUI for searching photos by different criteria.""" SEARCH_TYPES = [ "Search photos by name", "Search photos by date (planned)", "Search photos by tags (planned)", "Search photos by multiple people (planned)", "Most common tags (planned)", "Most photographed people (planned)", "Photos without faces (planned)", "Photos without tags (planned)", "Duplicate faces (planned)", "Face quality distribution (planned)", ] def __init__(self, db_manager: DatabaseManager, search_stats: SearchStats, gui_core: GUICore, verbose: int = 0): self.db = db_manager self.search_stats = search_stats self.gui_core = gui_core self.verbose = verbose def search_gui(self) -> int: """Open the Search GUI window.""" root = tk.Tk() root.title("Search Photos") root.resizable(True, True) # Hide to center and size root.withdraw() main = ttk.Frame(root, padding="10") main.pack(fill=tk.BOTH, expand=True) # Search type selector type_frame = ttk.Frame(main) type_frame.pack(fill=tk.X) ttk.Label(type_frame, text="Search type:", font=("Arial", 10, "bold")).pack(side=tk.LEFT) search_type_var = tk.StringVar(value=self.SEARCH_TYPES[0]) type_combo = ttk.Combobox(type_frame, textvariable=search_type_var, values=self.SEARCH_TYPES, state="readonly") type_combo.pack(side=tk.LEFT, padx=(8, 0), fill=tk.X, expand=True) # Inputs area inputs = ttk.Frame(main) inputs.pack(fill=tk.X, pady=(10, 6)) # Name search input name_frame = ttk.Frame(inputs) ttk.Label(name_frame, text="Person name:").pack(side=tk.LEFT) name_var = tk.StringVar() name_entry = ttk.Entry(name_frame, textvariable=name_var) name_entry.pack(side=tk.LEFT, padx=(6, 0), fill=tk.X, expand=True) # Planned inputs (stubs) planned_label = ttk.Label(inputs, text="This search type is planned and not yet implemented.", foreground="#888") # Results area results_frame = ttk.Frame(main) 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") tree = ttk.Treeview(results_frame, columns=columns, show="headings", selectmode="browse") tree.heading("person", text="Person") tree.heading("open_dir", text="📁") tree.heading("open_photo", text="📷") tree.heading("path", text="Photo path") 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.pack(fill=tk.BOTH, expand=True, pady=(4, 0)) # Buttons btns = ttk.Frame(main) btns.pack(fill=tk.X, pady=(8, 0)) search_btn = ttk.Button(btns, text="Search", command=lambda: do_search()) 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)) close_btn = ttk.Button(btns, text="Close", command=lambda: root.destroy()) close_btn.pack(side=tk.RIGHT) # Behavior def switch_inputs(*_): for w in inputs.winfo_children(): w.pack_forget() choice = search_type_var.get() if choice == self.SEARCH_TYPES[0]: name_frame.pack(fill=tk.X) name_entry.configure(state="normal") search_btn.configure(state="normal") else: planned_label.pack(anchor="w") name_entry.configure(state="disabled") search_btn.configure(state="disabled") def clear_results(): for i in tree.get_children(): tree.delete(i) 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)) def do_search(): clear_results() choice = search_type_var.get() if choice == self.SEARCH_TYPES[0]: query = name_var.get().strip() if not query: messagebox.showinfo("Search", "Please enter a name to search.", parent=root) return rows = self.search_stats.search_faces(query) if not rows: messagebox.showinfo("Search", f"No photos found for '{query}'.", parent=root) add_results(rows) def open_selected(): sel = tree.selection() if not sel: return vals = tree.item(sel[0], "values") path = vals[3] if len(vals) >= 4 else None try: if os.name == "nt": os.startfile(path) # type: ignore[attr-defined] elif sys.platform == "darwin": import subprocess subprocess.run(["open", path], check=False) else: import subprocess subprocess.run(["xdg-open", path], check=False) except Exception: messagebox.showerror("Open Photo", "Failed to open the selected photo.", parent=root) def open_dir(path: str): try: folder = os.path.dirname(path) if os.name == "nt": os.startfile(folder) # type: ignore[attr-defined] elif sys.platform == "darwin": import subprocess subprocess.run(["open", folder], check=False) else: import subprocess subprocess.run(["xdg-open", folder], check=False) except Exception: messagebox.showerror("Open Location", "Failed to open the file location.", parent=root) # Click handling on icon columns def on_tree_click(event): region = tree.identify("region", event.x, event.y) if region != "cell": return row_id = tree.identify_row(event.y) col_id = tree.identify_column(event.x) # '#1', '#2', ... if not row_id or not col_id: return vals = tree.item(row_id, "values") if not vals or len(vals) < 4: return path = vals[3] if col_id == "#2": open_dir(path) elif col_id == "#3": try: if os.name == "nt": os.startfile(path) # type: ignore[attr-defined] elif sys.platform == "darwin": import subprocess subprocess.run(["open", path], check=False) else: import subprocess subprocess.run(["xdg-open", path], check=False) except Exception: messagebox.showerror("Open Photo", "Failed to open the selected photo.", parent=root) # Tooltip for icon cells tooltip = None def show_tooltip(widget, x, y, text: str): nonlocal tooltip hide_tooltip() try: tooltip = tk.Toplevel(widget) tooltip.wm_overrideredirect(True) tooltip.wm_geometry(f"+{x+12}+{y+12}") lbl = tk.Label(tooltip, text=text, background="lightyellow", relief="solid", borderwidth=1, font=("Arial", 9)) lbl.pack() except Exception: tooltip = None def hide_tooltip(*_): nonlocal tooltip if tooltip is not None: try: tooltip.destroy() except Exception: pass tooltip = None def on_tree_motion(event): region = tree.identify("region", event.x, event.y) if region != "cell": hide_tooltip() tree.config(cursor="") return col_id = tree.identify_column(event.x) 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_tooltip(tree, event.x_root, event.y_root, "Open photo") else: tree.config(cursor="") hide_tooltip() type_combo.bind("<>", switch_inputs) switch_inputs() tree.bind("", on_tree_click) tree.bind("", on_tree_motion) tree.bind("", hide_tooltip) # Enter key in name field triggers search name_entry.bind("", lambda e: do_search()) # Show and center root.update_idletasks() self.gui_core.center_window(root, 800, 500) root.deiconify() root.mainloop() return 0