From 29a02ceae3914e6de7268ebe17906a2c4fdadd89 Mon Sep 17 00:00:00 2001 From: tanyar09 Date: Wed, 8 Oct 2025 15:20:39 -0400 Subject: [PATCH] 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. --- search_gui.py | 303 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 218 insertions(+), 85 deletions(-) diff --git a/search_gui.py b/search_gui.py index 0923216..4257c78 100644 --- a/search_gui.py +++ b/search_gui.py @@ -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("", 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("", on_toggle_enter) + toggle_text.bind("", 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("", show_available_tags_tooltip) + tag_help_icon.bind("", 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("", lambda e: do_search()) # Enter key in tag field triggers search tag_entry.bind("", lambda e: do_search()) - # Enter key in date fields triggers search - date_from_entry.bind("", lambda e: do_search()) - date_to_entry.bind("", 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("", lambda e: do_search()) # Show and center root.update_idletasks()