diff --git a/photo_tagger.py b/photo_tagger.py index aff1d6f..63d3560 100644 --- a/photo_tagger.py +++ b/photo_tagger.py @@ -642,7 +642,9 @@ class PhotoTagger: root.rowconfigure(0, weight=1) main_frame.columnconfigure(0, weight=1) # Left panel main_frame.columnconfigure(1, weight=1) # Right panel for similar faces - main_frame.rowconfigure(2, weight=1) # Main content row + # Configure row weights to minimize spacing around Unique checkbox + main_frame.rowconfigure(2, weight=0) # Unique checkbox row - no expansion + main_frame.rowconfigure(3, weight=1) # Main panels row - expandable # Photo info info_label = ttk.Label(main_frame, text="", font=("Arial", 10, "bold")) @@ -844,19 +846,140 @@ class PhotoTagger: # Initial calendar display update_calendar() + # Unique faces only checkbox variable (must be defined before widgets that use it) + unique_faces_var = tk.BooleanVar() + + # Define update_similar_faces function first - reusing auto-match display logic + def update_similar_faces(): + """Update the similar faces panel when compare is enabled - reuses auto-match display logic""" + nonlocal similar_faces_data, similar_face_vars, similar_face_images, similar_face_crops, face_selection_states + + # Note: Selection states are now saved automatically via callbacks (auto-match style) + + # Clear existing similar faces + for widget in similar_scrollable_frame.winfo_children(): + widget.destroy() + similar_face_vars.clear() + similar_face_images.clear() + + # Clean up existing face crops + for crop_path in similar_face_crops: + try: + if os.path.exists(crop_path): + os.remove(crop_path) + except: + pass + similar_face_crops.clear() + + if compare_var.get(): + # Use the same filtering and sorting logic as auto-match (no unique faces filter for similar faces) + unidentified_similar_faces = self._get_filtered_similar_faces(face_id, tolerance, include_same_photo=False, face_status=face_status) + + if unidentified_similar_faces: + # Get current face_id for selection state management + current_face_id = original_faces[i][0] # Get current face_id + + # Reuse auto-match display logic for similar faces + self._display_similar_faces_in_panel(similar_scrollable_frame, unidentified_similar_faces, + similar_face_vars, similar_face_images, similar_face_crops, + current_face_id, face_selection_states, identify_data_cache) + + # Note: Selection states are now restored automatically during checkbox creation (auto-match style) + else: + # No similar unidentified faces found + no_faces_label = ttk.Label(similar_scrollable_frame, text="No similar unidentified faces found", + foreground="gray", font=("Arial", 10)) + no_faces_label.pack(pady=20) + else: + # Compare disabled - clear the panel + clear_label = ttk.Label(similar_scrollable_frame, text="Enable 'Compare with similar faces' to see matches", + foreground="gray", font=("Arial", 10)) + clear_label.pack(pady=20) + + # Update button states based on compare checkbox and list contents + update_select_clear_buttons_state() + + # Unique faces change handler (must be defined before checkbox that uses it) + def on_unique_faces_change(): + """Handle unique faces checkbox change""" + nonlocal original_faces, i + + if unique_faces_var.get(): + # Show progress message + print("🔄 Applying unique faces filter...") + root.update() # Update UI to show the message + + # Apply unique faces filtering to the main face list + try: + original_faces = self._filter_unique_faces_from_list(original_faces) + print(f"✅ Filter applied: {len(original_faces)} unique faces remaining") + except Exception as e: + print(f"⚠️ Error applying filter: {e}") + # Revert checkbox state + unique_faces_var.set(False) + return + else: + # Reload the original unfiltered face list + print("🔄 Reloading all faces...") + root.update() # Update UI to show the message + + with self.get_db_connection() as conn: + cursor = conn.cursor() + query = ''' + SELECT f.id, f.photo_id, p.path, p.filename, f.location + FROM faces f + JOIN photos p ON f.photo_id = p.id + WHERE f.person_id IS NULL + ''' + params = [] + + # Add date taken filtering if specified + if date_from: + query += ' AND p.date_taken >= ?' + params.append(date_from) + + if date_to: + query += ' AND p.date_taken <= ?' + params.append(date_to) + + # Add date processed filtering if specified + if date_processed_from: + query += ' AND DATE(p.date_added) >= ?' + params.append(date_processed_from) + + if date_processed_to: + query += ' AND DATE(p.date_added) <= ?' + params.append(date_processed_to) + + query += ' ORDER BY f.id' + cursor.execute(query, params) + original_faces = list(cursor.fetchall()) + + print(f"✅ Reloaded: {len(original_faces)} faces") + + # Reset to first face and update display + i = 0 + update_similar_faces() + + # Compare checkbox variable and handler (must be defined before widgets that use it) + compare_var = tk.BooleanVar() + + def on_compare_change(): + """Handle compare checkbox change""" + update_similar_faces() + update_select_clear_buttons_state() + # Date filter controls - date_filter_frame = ttk.LabelFrame(main_frame, text="Date Filters", padding="5") - date_filter_frame.grid(row=1, column=0, columnspan=2, pady=(0, 10), sticky=(tk.W, tk.E)) - date_filter_frame.columnconfigure(1, weight=1) - date_filter_frame.columnconfigure(4, weight=1) - date_filter_frame.columnconfigure(7, weight=1) - date_filter_frame.columnconfigure(10, weight=1) + date_filter_frame = ttk.LabelFrame(main_frame, text="Filter", padding="5") + date_filter_frame.grid(row=1, column=0, pady=(0, 0), sticky=tk.W) + date_filter_frame.columnconfigure(1, weight=0) + date_filter_frame.columnconfigure(4, weight=0) # Date from - ttk.Label(date_filter_frame, text="From:").grid(row=0, column=0, sticky=tk.W, padx=(0, 5)) + ttk.Label(date_filter_frame, text="Taken date: from").grid(row=0, column=0, sticky=tk.W, padx=(0, 5)) date_from_var = tk.StringVar(value=date_from or "") - date_from_entry = ttk.Entry(date_filter_frame, textvariable=date_from_var, width=12, state='readonly') - date_from_entry.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(0, 5)) + date_from_entry = ttk.Entry(date_filter_frame, textvariable=date_from_var, width=10, state='readonly') + date_from_entry.grid(row=0, column=1, sticky=tk.W, padx=(0, 5)) # Calendar button for date from def open_calendar_from(): @@ -866,10 +989,10 @@ class PhotoTagger: calendar_from_btn.grid(row=0, column=2, padx=(0, 10)) # Date to - ttk.Label(date_filter_frame, text="To:").grid(row=0, column=3, sticky=tk.W, padx=(0, 5)) + ttk.Label(date_filter_frame, text="to").grid(row=0, column=3, sticky=tk.W, padx=(0, 5)) date_to_var = tk.StringVar(value=date_to or "") - date_to_entry = ttk.Entry(date_filter_frame, textvariable=date_to_var, width=12, state='readonly') - date_to_entry.grid(row=0, column=4, sticky=(tk.W, tk.E), padx=(0, 5)) + date_to_entry = ttk.Entry(date_filter_frame, textvariable=date_to_var, width=10, state='readonly') + date_to_entry.grid(row=0, column=4, sticky=tk.W, padx=(0, 5)) # Calendar button for date to def open_calendar_to(): @@ -953,14 +1076,15 @@ class PhotoTagger: print(f"👤 Found {len(unidentified)} unidentified faces with date filters") print("💡 Navigate to refresh the display with filtered faces") + # Apply filter button (inside filter frame) apply_filter_btn = ttk.Button(date_filter_frame, text="Apply Filter", command=apply_date_filter) apply_filter_btn.grid(row=0, column=6, padx=(10, 0)) # Date processed filter (second row) - ttk.Label(date_filter_frame, text="Processed From:").grid(row=1, column=0, sticky=tk.W, padx=(0, 5), pady=(10, 0)) + ttk.Label(date_filter_frame, text="Processed date: from").grid(row=1, column=0, sticky=tk.W, padx=(0, 5), pady=(10, 0)) date_processed_from_var = tk.StringVar() - date_processed_from_entry = ttk.Entry(date_filter_frame, textvariable=date_processed_from_var, width=12, state='readonly') - date_processed_from_entry.grid(row=1, column=1, sticky=(tk.W, tk.E), padx=(0, 5), pady=(10, 0)) + date_processed_from_entry = ttk.Entry(date_filter_frame, textvariable=date_processed_from_var, width=10, state='readonly') + date_processed_from_entry.grid(row=1, column=1, sticky=tk.W, padx=(0, 5), pady=(10, 0)) # Calendar button for date processed from def open_calendar_processed_from(): @@ -970,10 +1094,10 @@ class PhotoTagger: calendar_processed_from_btn.grid(row=1, column=2, padx=(0, 10), pady=(10, 0)) # Date processed to - ttk.Label(date_filter_frame, text="Processed To:").grid(row=1, column=3, sticky=tk.W, padx=(0, 5), pady=(10, 0)) + ttk.Label(date_filter_frame, text="to").grid(row=1, column=3, sticky=tk.W, padx=(0, 5), pady=(10, 0)) date_processed_to_var = tk.StringVar() - date_processed_to_entry = ttk.Entry(date_filter_frame, textvariable=date_processed_to_var, width=12, state='readonly') - date_processed_to_entry.grid(row=1, column=4, sticky=(tk.W, tk.E), padx=(0, 5), pady=(10, 0)) + date_processed_to_entry = ttk.Entry(date_filter_frame, textvariable=date_processed_to_var, width=10, state='readonly') + date_processed_to_entry.grid(row=1, column=4, sticky=tk.W, padx=(0, 5), pady=(10, 0)) # Calendar button for date processed to def open_calendar_processed_to(): @@ -982,14 +1106,24 @@ class PhotoTagger: calendar_processed_to_btn = ttk.Button(date_filter_frame, text="📅", width=3, command=open_calendar_processed_to) calendar_processed_to_btn.grid(row=1, column=5, padx=(0, 10), pady=(10, 0)) + # Unique checkbox under the filter frame + unique_faces_checkbox = ttk.Checkbutton(main_frame, text="Unique faces only", + variable=unique_faces_var, command=on_unique_faces_change) + unique_faces_checkbox.grid(row=2, column=0, sticky=tk.W, padx=(0, 5), pady=0) + + # Compare checkbox on the same row as Unique + compare_checkbox = ttk.Checkbutton(main_frame, text="Compare similar faces", variable=compare_var, + command=on_compare_change) + compare_checkbox.grid(row=2, column=1, sticky=tk.W, padx=(5, 10), pady=0) + # Left panel for main face left_panel = ttk.Frame(main_frame) - left_panel.grid(row=2, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(0, 5)) + left_panel.grid(row=3, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(0, 5), pady=(0, 0)) left_panel.columnconfigure(0, weight=1) # Right panel for similar faces right_panel = ttk.LabelFrame(main_frame, text="Similar Faces", padding="5") - right_panel.grid(row=2, column=1, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(5, 0)) + right_panel.grid(row=3, column=1, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(5, 0)) right_panel.columnconfigure(0, weight=1) right_panel.rowconfigure(0, weight=1) # Make right panel expandable vertically @@ -1282,136 +1416,14 @@ class PhotoTagger: # Initialize calendar update_calendar() - # Unique faces only checkbox variable (defined before update_similar_faces function) - unique_faces_var = tk.BooleanVar() + # (moved) unique_faces_var is defined earlier before date filter widgets - # Define update_similar_faces function first - reusing auto-match display logic - def update_similar_faces(): - """Update the similar faces panel when compare is enabled - reuses auto-match display logic""" - nonlocal similar_faces_data, similar_face_vars, similar_face_images, similar_face_crops, face_selection_states - - # Note: Selection states are now saved automatically via callbacks (auto-match style) - - # Clear existing similar faces - for widget in similar_scrollable_frame.winfo_children(): - widget.destroy() - similar_face_vars.clear() - similar_face_images.clear() - - # Clean up existing face crops - for crop_path in similar_face_crops: - try: - if os.path.exists(crop_path): - os.remove(crop_path) - except: - pass - similar_face_crops.clear() - - if compare_var.get(): - # Use the same filtering and sorting logic as auto-match (no unique faces filter for similar faces) - unidentified_similar_faces = self._get_filtered_similar_faces(face_id, tolerance, include_same_photo=False, face_status=face_status) - - if unidentified_similar_faces: - # Get current face_id for selection state management - current_face_id = original_faces[i][0] # Get current face_id - - # Reuse auto-match display logic for similar faces - self._display_similar_faces_in_panel(similar_scrollable_frame, unidentified_similar_faces, - similar_face_vars, similar_face_images, similar_face_crops, - current_face_id, face_selection_states, identify_data_cache) - - # Note: Selection states are now restored automatically during checkbox creation (auto-match style) - else: - # No similar unidentified faces found - no_faces_label = ttk.Label(similar_scrollable_frame, text="No similar unidentified faces found", - foreground="gray", font=("Arial", 10)) - no_faces_label.pack(pady=20) - else: - # Compare disabled - clear the panel - clear_label = ttk.Label(similar_scrollable_frame, text="Enable 'Compare with similar faces' to see matches", - foreground="gray", font=("Arial", 10)) - clear_label.pack(pady=20) - - # Update button states based on compare checkbox and list contents - update_select_clear_buttons_state() + # (moved) update_similar_faces function is defined earlier before on_unique_faces_change - # Compare checkbox - compare_var = tk.BooleanVar() + # (moved) Compare checkbox is now inside date_filter_frame to the right of dates - def on_compare_change(): - """Handle compare checkbox change""" - update_similar_faces() - update_select_clear_buttons_state() + # (moved) on_unique_faces_change function is defined earlier before date filter widgets - compare_checkbox = ttk.Checkbutton(input_frame, text="Compare with similar faces", variable=compare_var, - command=on_compare_change) - compare_checkbox.grid(row=3, column=0, columnspan=4, sticky=tk.W, pady=(5, 0)) - - # Unique faces only checkbox widget - def on_unique_faces_change(): - """Handle unique faces checkbox change""" - nonlocal original_faces, i - - if unique_faces_var.get(): - # Show progress message - print("🔄 Applying unique faces filter...") - root.update() # Update UI to show the message - - # Apply unique faces filtering to the main face list - try: - original_faces = self._filter_unique_faces_from_list(original_faces) - print(f"✅ Filter applied: {len(original_faces)} unique faces remaining") - except Exception as e: - print(f"⚠️ Error applying filter: {e}") - # Revert checkbox state - unique_faces_var.set(False) - return - else: - # Reload the original unfiltered face list - print("🔄 Reloading all faces...") - root.update() # Update UI to show the message - - with self.get_db_connection() as conn: - cursor = conn.cursor() - query = ''' - SELECT f.id, f.photo_id, p.path, p.filename, f.location - FROM faces f - JOIN photos p ON f.photo_id = p.id - WHERE f.person_id IS NULL - ''' - params = [] - - # Add date taken filtering if specified - if date_from: - query += ' AND p.date_taken >= ?' - params.append(date_from) - - if date_to: - query += ' AND p.date_taken <= ?' - params.append(date_to) - - # Add date processed filtering if specified - if date_processed_from: - query += ' AND DATE(p.date_added) >= ?' - params.append(date_processed_from) - - if date_processed_to: - query += ' AND DATE(p.date_added) <= ?' - params.append(date_processed_to) - - query += ' ORDER BY f.id' - cursor.execute(query, params) - original_faces = list(cursor.fetchall()) - - print(f"✅ Reloaded: {len(original_faces)} faces") - - # Reset to first face and update display - i = 0 - update_similar_faces() - - unique_faces_checkbox = ttk.Checkbutton(date_filter_frame, text="Unique faces only (hide duplicates with high/medium confidence)", - variable=unique_faces_var, command=on_unique_faces_change) - unique_faces_checkbox.grid(row=2, column=0, columnspan=6, sticky=tk.W, pady=(10, 0)) # Add callback to save person name when it changes def on_name_change(*args): @@ -1757,9 +1769,9 @@ class PhotoTagger: first_name_entry.bind('', on_enter) last_name_entry.bind('', on_enter) - # Bottom control panel + # Bottom control panel (move to bottom below panels) control_frame = ttk.Frame(main_frame) - control_frame.grid(row=3, column=0, columnspan=2, pady=(10, 0), sticky=tk.E) + control_frame.grid(row=4, column=0, columnspan=3, pady=(10, 0), sticky=(tk.E, tk.S)) # Create button references for state management back_btn = ttk.Button(control_frame, text="⬅️ Back", command=on_back)