Implement folder filter functionality in Search GUI

This commit enhances the SearchGUI class by introducing a folder filter feature, allowing users to filter search results based on a specified folder path. The GUI now includes an input field for folder location, along with 'Browse' and 'Clear' buttons for easier folder selection and management. Additionally, the search logic has been updated to apply the folder filter across various search types, improving the overall search experience and result accuracy. Tooltip functionality has also been added for better user guidance on available tags and filter options.
This commit is contained in:
tanyar09 2025-10-08 15:20:39 -04:00
parent 6fd5fe3e44
commit 29a02ceae3

View File

@ -68,6 +68,79 @@ class SearchGUI:
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)
# Filters area with expand/collapse functionality
filters_container = ttk.LabelFrame(main, text="", padding="8")
filters_container.pack(fill=tk.X, pady=(10, 6))
# Filters header with toggle text
filters_header = ttk.Frame(filters_container)
filters_header.pack(fill=tk.X)
# Toggle text for expand/collapse
filters_expanded = tk.BooleanVar(value=False) # Start collapsed
def toggle_filters():
if filters_expanded.get():
# Collapse filters
filters_content.pack_forget()
toggle_text.config(text="+")
filters_expanded.set(False)
update_toggle_tooltip()
else:
# Expand filters
filters_content.pack(fill=tk.X, pady=(4, 0))
toggle_text.config(text="-")
filters_expanded.set(True)
update_toggle_tooltip()
def update_toggle_tooltip():
"""Update tooltip text based on current state"""
if filters_expanded.get():
tooltip_text = "Click to collapse filters"
else:
tooltip_text = "Click to expand filters"
toggle_text.tooltip_text = tooltip_text
filters_label = ttk.Label(filters_header, text="Filters", font=("Arial", 10, "bold"))
filters_label.pack(side=tk.LEFT)
toggle_text = ttk.Label(filters_header, text="+", font=("Arial", 10, "bold"), cursor="hand2")
toggle_text.pack(side=tk.LEFT, padx=(6, 0))
toggle_text.bind("<Button-1>", lambda e: toggle_filters())
# Initialize tooltip
toggle_text.tooltip_text = "Click to expand filters"
update_toggle_tooltip()
# Filters content area (start hidden)
filters_content = ttk.Frame(filters_container)
# Folder location filter
folder_filter_frame = ttk.Frame(filters_content)
folder_filter_frame.pack(fill=tk.X, pady=(0, 4))
ttk.Label(folder_filter_frame, text="Folder location:").pack(side=tk.LEFT)
folder_var = tk.StringVar()
folder_entry = ttk.Entry(folder_filter_frame, textvariable=folder_var, width=40)
folder_entry.pack(side=tk.LEFT, padx=(6, 0), fill=tk.X, expand=True)
ttk.Label(folder_filter_frame, text="(optional - filter by folder path)").pack(side=tk.LEFT, padx=(6, 0))
# Browse button for folder selection
def browse_folder():
from tkinter import filedialog
folder_path = filedialog.askdirectory(title="Select folder to filter by")
if folder_path:
folder_var.set(folder_path)
browse_btn = ttk.Button(folder_filter_frame, text="Browse", command=browse_folder)
browse_btn.pack(side=tk.LEFT, padx=(6, 0))
# Clear folder filter button
def clear_folder_filter():
folder_var.set("")
clear_folder_btn = ttk.Button(folder_filter_frame, text="Clear", command=clear_folder_filter)
clear_folder_btn.pack(side=tk.LEFT, padx=(6, 0))
# Inputs area
inputs = ttk.Frame(main)
inputs.pack(fill=tk.X, pady=(10, 6))
@ -85,6 +158,11 @@ class SearchGUI:
tag_var = tk.StringVar()
tag_entry = ttk.Entry(tag_frame, textvariable=tag_var)
tag_entry.pack(side=tk.LEFT, padx=(6, 0), fill=tk.X, expand=True)
# Help icon for available tags (will be defined after tooltip functions)
tag_help_icon = ttk.Label(tag_frame, text="", font=("Arial", 10), cursor="hand2")
tag_help_icon.pack(side=tk.LEFT, padx=(6, 0))
ttk.Label(tag_frame, text="(comma-separated)").pack(side=tk.LEFT, padx=(6, 0))
# Tag search mode
@ -100,7 +178,7 @@ class SearchGUI:
date_frame = ttk.Frame(inputs)
ttk.Label(date_frame, text="From date:").pack(side=tk.LEFT)
date_from_var = tk.StringVar()
date_from_entry = ttk.Entry(date_frame, textvariable=date_from_var, width=12)
date_from_entry = ttk.Entry(date_frame, textvariable=date_from_var, width=12, state="readonly")
date_from_entry.pack(side=tk.LEFT, padx=(6, 0))
# Calendar button for date from
@ -117,7 +195,7 @@ class SearchGUI:
date_to_frame = ttk.Frame(inputs)
ttk.Label(date_to_frame, text="To date:").pack(side=tk.LEFT)
date_to_var = tk.StringVar()
date_to_entry = ttk.Entry(date_to_frame, textvariable=date_to_var, width=12)
date_to_entry = ttk.Entry(date_to_frame, textvariable=date_to_var, width=12, state="readonly")
date_to_entry.pack(side=tk.LEFT, padx=(6, 0))
# Calendar button for date to
@ -264,19 +342,19 @@ class SearchGUI:
name_entry.configure(state="disabled")
tag_entry.configure(state="disabled")
tag_mode_combo.configure(state="disabled")
date_from_entry.configure(state="normal")
date_to_entry.configure(state="normal")
date_from_entry.configure(state="readonly")
date_to_entry.configure(state="readonly")
date_from_btn.configure(state="normal")
date_to_btn.configure(state="normal")
search_btn.configure(state="normal")
# Show person column for date search since photos might have people
tree.column("person", width=180, minwidth=50, anchor="w")
tree.heading("person", text="Person", command=lambda: sort_treeview("person"))
# Hide person column for date search
tree.column("person", width=0, minwidth=0, anchor="w")
tree.heading("person", text="")
# Restore people icon column for date search
tree.column("open_photo", width=50, minwidth=50, anchor="center")
tree.heading("open_photo", text="👤")
# Show all columns for date search
tree["displaycolumns"] = ("select", "person", "tags", "open_dir", "open_photo", "path", "date_taken")
# Show all columns except person for date search
tree["displaycolumns"] = ("select", "tags", "open_dir", "open_photo", "path", "date_taken")
elif choice == self.SEARCH_TYPES[2]: # Search photos by tags
tag_frame.pack(fill=tk.X)
tag_mode_frame.pack(fill=tk.X, pady=(4, 0))
@ -312,14 +390,14 @@ class SearchGUI:
elif choice == self.SEARCH_TYPES[7]: # Photos without tags
# No input needed for this search type
search_btn.configure(state="normal")
# Show person column since photos without tags might still have people
tree.column("person", width=180, minwidth=50, anchor="w")
tree.heading("person", text="Person", command=lambda: sort_treeview("person"))
# Hide person column for photos without tags search
tree.column("person", width=0, minwidth=0, anchor="w")
tree.heading("person", text="")
# Show the people icon column since there might be faces/people
tree.column("open_photo", width=50, minwidth=50, anchor="center")
tree.heading("open_photo", text="👤")
# Show all columns since we want to display person info and tags (which will be empty)
tree["displaycolumns"] = ("select", "person", "tags", "open_dir", "open_photo", "path", "date_taken")
# Show all columns except person for photos without tags search
tree["displaycolumns"] = ("select", "tags", "open_dir", "open_photo", "path", "date_taken")
# Auto-run search for photos without tags
do_search()
else:
@ -332,14 +410,33 @@ class SearchGUI:
date_from_btn.configure(state="disabled")
date_to_btn.configure(state="disabled")
search_btn.configure(state="disabled")
# Show person column for other search types
tree.column("person", width=180, minwidth=50, anchor="w")
tree.heading("person", text="Person", command=lambda: sort_treeview("person"))
# Hide person column for other search types
tree.column("person", width=0, minwidth=0, anchor="w")
tree.heading("person", text="")
# Restore people icon column for other search types
tree.column("open_photo", width=50, minwidth=50, anchor="center")
tree.heading("open_photo", text="👤")
# Restore all columns to display
tree["displaycolumns"] = ("select", "person", "tags", "open_dir", "open_photo", "path", "date_taken")
# Show all columns except person for other search types
tree["displaycolumns"] = ("select", "tags", "open_dir", "open_photo", "path", "date_taken")
def filter_results_by_folder(results, folder_path):
"""Filter search results by folder path if specified."""
if not folder_path or not folder_path.strip():
return results
folder_path = folder_path.strip()
filtered_results = []
for result in results:
if len(result) >= 2:
# Extract photo path from result tuple
photo_path = result[1] if len(result) == 2 else result[0]
# Check if photo path starts with the specified folder path
if photo_path.startswith(folder_path):
filtered_results.append(result)
return filtered_results
def clear_results():
for i in tree.get_children():
@ -358,11 +455,10 @@ class SearchGUI:
for row in rows:
if len(row) == 2:
if search_type_var.get() == self.SEARCH_TYPES[1]: # Date search
# For date search: (path, date_taken) - show all columns
# For date search: (path, date_taken) - hide person column
path, date_taken = row
person_name = get_person_name_for_photo(path)
photo_tags = get_photo_tags_for_display(path)
tree.insert("", tk.END, values=("", person_name, photo_tags, "📁", "👤", path, date_taken))
tree.insert("", tk.END, values=("", "", photo_tags, "📁", "👤", path, date_taken))
elif search_type_var.get() == self.SEARCH_TYPES[2]: # Tag search
# For tag search: (path, tag_info) - hide person column
# Show ALL tags for the photo, not just matching ones
@ -377,12 +473,11 @@ class SearchGUI:
date_taken = get_photo_date_taken(path)
tree.insert("", tk.END, values=("", "", photo_tags, "📁", "", path, date_taken))
elif search_type_var.get() == self.SEARCH_TYPES[7]: # Photos without tags
# For photos without tags: (path, filename) - show all columns but tags will be empty
# For photos without tags: (path, filename) - hide person column
path, filename = row
person_name = get_person_name_for_photo(path)
photo_tags = get_photo_tags_for_display(path) # Will be "No tags"
date_taken = get_photo_date_taken(path)
tree.insert("", tk.END, values=("", person_name, photo_tags, "📁", "👤", path, date_taken))
tree.insert("", tk.END, values=("", "", photo_tags, "📁", "👤", path, date_taken))
else:
# For name search: (full_name, path) - show person column
full_name, p = row
@ -403,8 +498,8 @@ class SearchGUI:
# Sort by path column for photos without faces
self.sort_column = "path"
elif search_type_var.get() == self.SEARCH_TYPES[7]: # Photos without tags
# Sort by person column for photos without tags
self.sort_column = "person"
# Sort by path column for photos without tags (person column is hidden)
self.sort_column = "path"
else:
# Sort by person column for name search
self.sort_column = "person"
@ -431,14 +526,19 @@ class SearchGUI:
def do_search():
clear_results()
choice = search_type_var.get()
folder_filter = folder_var.get().strip()
if choice == self.SEARCH_TYPES[0]: # Search photos by name
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)
# Apply folder filter
rows = filter_results_by_folder(rows, folder_filter)
if not rows:
messagebox.showinfo("Search", f"No photos found for '{query}'.", parent=root)
folder_msg = f" in folder '{folder_filter}'" if folder_filter else ""
messagebox.showinfo("Search", f"No photos found for '{query}'{folder_msg}.", parent=root)
add_results(rows)
elif choice == self.SEARCH_TYPES[1]: # Search photos by date
date_from = date_from_var.get().strip()
@ -468,6 +568,8 @@ class SearchGUI:
rows = self.search_stats.search_photos_by_date(date_from if date_from else None,
date_to if date_to else None)
# Apply folder filter
rows = filter_results_by_folder(rows, folder_filter)
if not rows:
date_range_text = ""
if date_from and date_to:
@ -476,7 +578,8 @@ class SearchGUI:
date_range_text = f" from {date_from}"
elif date_to:
date_range_text = f" up to {date_to}"
messagebox.showinfo("Search", f"No photos found{date_range_text}.", parent=root)
folder_msg = f" in folder '{folder_filter}'" if folder_filter else ""
messagebox.showinfo("Search", f"No photos found{date_range_text}{folder_msg}.", parent=root)
else:
# Convert to the format expected by add_results: (path, date_taken)
formatted_rows = [(path, date_taken) for path, date_taken in rows]
@ -497,14 +600,20 @@ class SearchGUI:
match_all = (tag_mode_var.get() == "ALL")
rows = self.search_stats.search_photos_by_tags(tags, match_all)
# Apply folder filter
rows = filter_results_by_folder(rows, folder_filter)
if not rows:
mode_text = "all" if match_all else "any"
messagebox.showinfo("Search", f"No photos found with {mode_text} of the tags: {', '.join(tags)}", parent=root)
folder_msg = f" in folder '{folder_filter}'" if folder_filter else ""
messagebox.showinfo("Search", f"No photos found with {mode_text} of the tags: {', '.join(tags)}{folder_msg}", parent=root)
add_results(rows)
elif choice == self.SEARCH_TYPES[6]: # Photos without faces
rows = self.search_stats.get_photos_without_faces()
# Apply folder filter
rows = filter_results_by_folder(rows, folder_filter)
if not rows:
messagebox.showinfo("Search", "No photos without faces found.", parent=root)
folder_msg = f" in folder '{folder_filter}'" if folder_filter else ""
messagebox.showinfo("Search", f"No photos without faces found{folder_msg}.", parent=root)
else:
# Convert to the format expected by add_results: (path, tag_info)
# For photos without faces, we don't have person info, so we use empty string
@ -512,8 +621,11 @@ class SearchGUI:
add_results(formatted_rows)
elif choice == self.SEARCH_TYPES[7]: # Photos without tags
rows = self.search_stats.get_photos_without_tags()
# Apply folder filter
rows = filter_results_by_folder(rows, folder_filter)
if not rows:
messagebox.showinfo("Search", "No photos without tags found.", parent=root)
folder_msg = f" in folder '{folder_filter}'" if folder_filter else ""
messagebox.showinfo("Search", f"No photos without tags found{folder_msg}.", parent=root)
else:
# Convert to the format expected by add_results: (path, filename)
# For photos without tags, we have both path and filename
@ -1091,36 +1203,29 @@ class SearchGUI:
return
# Determine column offsets based on search type
is_tag_search = (search_type_var.get() == self.SEARCH_TYPES[2])
is_name_search = (search_type_var.get() == self.SEARCH_TYPES[0])
is_photos_without_faces = (search_type_var.get() == self.SEARCH_TYPES[6])
is_photos_without_tags = (search_type_var.get() == self.SEARCH_TYPES[7])
if is_tag_search:
# Tag search: person column is hidden, select is first
select_col = "#1" # select is now column 1
open_dir_col = "#3" # open_dir is now column 3
face_col = "#4" # open_photo is now column 4
path_col = "#5" # path is now column 5
if is_name_search:
# Name search: all columns visible including person
select_col = "#1" # select is column 1
open_dir_col = "#4" # open_dir is column 4
face_col = "#5" # open_photo is column 5
path_col = "#6" # path is column 6
path_index = 5 # path is at index 5 in values array
elif is_photos_without_faces:
# Photos without faces: person and people icon columns are hidden
select_col = "#1" # select is now column 1
open_dir_col = "#3" # open_dir is now column 3
face_col = "#4" # open_photo is now column 4 (but hidden)
path_col = "#4" # path is now column 4 (since people icon is hidden)
path_index = 5 # path is at index 5 in values array
elif is_photos_without_tags:
# Photos without tags: all columns visible, same as name search
select_col = "#1" # select is now column 1
open_dir_col = "#4" # open_dir is column 4
face_col = "#5" # open_photo is column 5
path_col = "#6" # path is column 6
select_col = "#1" # select is column 1
open_dir_col = "#3" # open_dir is column 3
face_col = "#4" # open_photo is column 4 (but hidden)
path_col = "#4" # path is column 4 (since people icon is hidden)
path_index = 5 # path is at index 5 in values array
else:
# Name search: all columns visible, select is first
select_col = "#1" # select is now column 1
open_dir_col = "#4" # open_dir is column 4
face_col = "#5" # open_photo is column 5
path_col = "#6" # path is column 6
# All other searches: person column is hidden, people icon visible
select_col = "#1" # select is column 1
open_dir_col = "#3" # open_dir is column 3
face_col = "#4" # open_photo is column 4
path_col = "#5" # path is column 5
path_index = 5 # path is at index 5 in values array
path = vals[path_index] # Photo path
@ -1144,7 +1249,7 @@ class SearchGUI:
elif col_id == select_col: # Checkbox column
toggle_photo_selection(row_id, vals)
# Tooltip for icon cells
# Tooltip for icon cells and toggle text
tooltip = None
def show_tooltip(widget, x, y, text: str):
nonlocal tooltip
@ -1166,6 +1271,40 @@ class SearchGUI:
except Exception:
pass
tooltip = None
# Tooltip functionality for toggle text
def on_toggle_enter(event):
if hasattr(toggle_text, 'tooltip_text'):
show_tooltip(toggle_text, event.x_root, event.y_root, toggle_text.tooltip_text)
def on_toggle_leave(event):
hide_tooltip()
# Bind tooltip events to toggle text
toggle_text.bind("<Enter>", on_toggle_enter)
toggle_text.bind("<Leave>", on_toggle_leave)
# Help icon functionality for available tags
def show_available_tags_tooltip(event):
# Get all available tags from database
try:
tag_id_to_name, tag_name_to_id = self.db.load_tag_mappings()
available_tags = sorted(tag_name_to_id.keys())
if available_tags:
# Create tooltip with tags in a column format
tag_list = "\n".join(available_tags)
tooltip_text = f"Available tags:\n{tag_list}"
else:
tooltip_text = "No tags available in database"
show_tooltip(tag_help_icon, event.x_root, event.y_root, tooltip_text)
except Exception:
show_tooltip(tag_help_icon, event.x_root, event.y_root, "Error loading tags")
# Bind tooltip events to help icon
tag_help_icon.bind("<Enter>", show_available_tags_tooltip)
tag_help_icon.bind("<Leave>", hide_tooltip)
def on_tree_motion(event):
region = tree.identify("region", event.x, event.y)
@ -1177,36 +1316,29 @@ class SearchGUI:
row_id = tree.identify_row(event.y)
# Determine column offsets based on search type
is_tag_search = (search_type_var.get() == self.SEARCH_TYPES[2])
is_name_search = (search_type_var.get() == self.SEARCH_TYPES[0])
is_photos_without_faces = (search_type_var.get() == self.SEARCH_TYPES[6])
is_photos_without_tags = (search_type_var.get() == self.SEARCH_TYPES[7])
if is_tag_search:
# Tag search: person column is hidden, select is first
tags_col = "#2" # tags is now column 2
open_dir_col = "#3" # open_dir is now column 3
face_col = "#4" # open_photo is now column 4
path_col = "#5" # path is now column 5
if is_name_search:
# Name search: all columns visible including person
tags_col = "#3" # tags is column 3
open_dir_col = "#4" # open_dir is column 4
face_col = "#5" # open_photo is column 5
path_col = "#6" # path is column 6
path_index = 5 # path is at index 5 in values array
elif is_photos_without_faces:
# Photos without faces: person and people icon columns are hidden
tags_col = "#2" # tags is now column 2
open_dir_col = "#3" # open_dir is now column 3
face_col = "#4" # open_photo is now column 4 (but hidden)
path_col = "#4" # path is now column 4 (since people icon is hidden)
path_index = 5 # path is at index 5 in values array
elif is_photos_without_tags:
# Photos without tags: all columns visible, same as name search
tags_col = "#3" # tags is column 3
open_dir_col = "#4" # open_dir is column 4
face_col = "#5" # open_photo is column 5
path_col = "#6" # path is column 6
tags_col = "#2" # tags is column 2
open_dir_col = "#3" # open_dir is column 3
face_col = "#4" # open_photo is column 4 (but hidden)
path_col = "#4" # path is column 4 (since people icon is hidden)
path_index = 5 # path is at index 5 in values array
else:
# Name search: all columns visible, select is first
tags_col = "#3" # tags is column 3
open_dir_col = "#4" # open_dir is column 4
face_col = "#5" # open_photo is column 5
path_col = "#6" # path is column 6
# All other searches: person column is hidden, people icon visible
tags_col = "#2" # tags is column 2
open_dir_col = "#3" # open_dir is column 3
face_col = "#4" # open_photo is column 4
path_col = "#5" # path is column 5
path_index = 5 # path is at index 5 in values array
if col_id == tags_col: # Tags column
@ -1215,7 +1347,8 @@ class SearchGUI:
if row_id:
vals = tree.item(row_id, "values")
if len(vals) >= 3:
tags_text = vals[2] # Tags are at index 2 (after select and person)
# Tags are at index 2 for all search types (after select, person is hidden in most)
tags_text = vals[2]
show_tooltip(tree, event.x_root, event.y_root, f"Tags: {tags_text}")
elif col_id == open_dir_col: # Open directory column
tree.config(cursor="hand2")
@ -1246,9 +1379,9 @@ class SearchGUI:
name_entry.bind("<Return>", lambda e: do_search())
# Enter key in tag field triggers search
tag_entry.bind("<Return>", lambda e: do_search())
# Enter key in date fields triggers search
date_from_entry.bind("<Return>", lambda e: do_search())
date_to_entry.bind("<Return>", lambda e: do_search())
# Note: Date fields are read-only, so no Enter key binding needed
# Enter key in folder filter field triggers search
folder_entry.bind("<Return>", lambda e: do_search())
# Show and center
root.update_idletasks()