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()