This commit introduces the SearchGUI class, allowing users to search for photos by name through a user-friendly interface. The new functionality includes options to view search results, open photo locations, and display relevant information about matched individuals. The PhotoTagger class is updated to integrate this feature, and the README is revised to include usage instructions for the new search-gui command. Additionally, the search_faces method in SearchStats is enhanced to return detailed results, improving the overall search experience.
253 lines
9.5 KiB
Python
253 lines
9.5 KiB
Python
#!/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("<<ComboboxSelected>>", switch_inputs)
|
|
switch_inputs()
|
|
tree.bind("<Button-1>", on_tree_click)
|
|
tree.bind("<Motion>", on_tree_motion)
|
|
tree.bind("<Leave>", hide_tooltip)
|
|
|
|
# Enter key in name field triggers search
|
|
name_entry.bind("<Return>", lambda e: do_search())
|
|
|
|
# Show and center
|
|
root.update_idletasks()
|
|
self.gui_core.center_window(root, 800, 500)
|
|
root.deiconify()
|
|
root.mainloop()
|
|
return 0
|
|
|
|
|