punimtag/modify_panel.py
tanyar09 3e88e2cd2c Enhance Dashboard GUI with smart navigation and unified exit behavior
This commit introduces a compact home icon for quick navigation to the welcome screen, improving user experience across all panels. Additionally, all exit buttons now navigate to the home screen instead of closing the application, ensuring a consistent exit behavior. The README has been updated to reflect these enhancements, emphasizing the improved navigation and user experience in the unified dashboard.
2025-10-10 14:47:38 -04:00

741 lines
32 KiB
Python

#!/usr/bin/env python3
"""
Integrated Modify Panel for PunimTag Dashboard
Embeds the full modify identified GUI functionality into the dashboard frame
"""
import os
import tkinter as tk
from tkinter import ttk, messagebox
from PIL import Image, ImageTk
from typing import List, Dict, Tuple, Optional
from config import DEFAULT_FACE_TOLERANCE
from database import DatabaseManager
from face_processing import FaceProcessor
from gui_core import GUICore
class ToolTip:
"""Simple tooltip implementation"""
def __init__(self, widget, text):
self.widget = widget
self.text = text
self.tooltip_window = None
self.widget.bind("<Enter>", self.on_enter)
self.widget.bind("<Leave>", self.on_leave)
def on_enter(self, event=None):
if self.tooltip_window or not self.text:
return
x, y, _, _ = self.widget.bbox("insert") if hasattr(self.widget, 'bbox') else (0, 0, 0, 0)
x += self.widget.winfo_rootx() + 25
y += self.widget.winfo_rooty() + 25
self.tooltip_window = tw = tk.Toplevel(self.widget)
tw.wm_overrideredirect(True)
tw.wm_geometry(f"+{x}+{y}")
label = tk.Label(tw, text=self.text, justify=tk.LEFT,
background="#ffffe0", relief=tk.SOLID, borderwidth=1,
font=("tahoma", "8", "normal"))
label.pack(ipadx=1)
def on_leave(self, event=None):
if self.tooltip_window:
self.tooltip_window.destroy()
self.tooltip_window = None
class ModifyPanel:
"""Integrated modify panel that embeds the full modify identified GUI functionality into the dashboard"""
def __init__(self, parent_frame: ttk.Frame, db_manager: DatabaseManager,
face_processor: FaceProcessor, gui_core: GUICore, on_navigate_home=None, verbose: int = 0):
"""Initialize the modify panel"""
self.parent_frame = parent_frame
self.db = db_manager
self.face_processor = face_processor
self.gui_core = gui_core
self.on_navigate_home = on_navigate_home
self.verbose = verbose
# Panel state
self.is_active = False
self.temp_crops = []
self.right_panel_images = [] # Keep PhotoImage refs alive
self.selected_person_id = None
# Track unmatched faces (temporary changes)
self.unmatched_faces = set() # All face IDs unmatched across people (for global save)
self.unmatched_by_person = {} # person_id -> set(face_id) for per-person undo
self.original_faces_data = [] # store original faces data for potential future use
# People data
self.people_data = [] # list of dicts: {id, name, count, first_name, last_name}
self.people_filtered = None # filtered subset based on last name search
self.current_person_id = None
self.current_person_name = ""
self.resize_job = None
# GUI components
self.components = {}
self.main_frame = None
def create_panel(self) -> ttk.Frame:
"""Create the modify panel with all GUI components"""
self.main_frame = ttk.Frame(self.parent_frame)
# Configure grid weights for full screen responsiveness
self.main_frame.columnconfigure(0, weight=1) # Left panel
self.main_frame.columnconfigure(1, weight=2) # Right panel
self.main_frame.rowconfigure(1, weight=1) # Main panels row - expandable
# Create all GUI components
self._create_gui_components()
# Create main content panels
self._create_main_panels()
return self.main_frame
def _create_gui_components(self):
"""Create all GUI components for the modify interface"""
# Search controls (Last Name) with label under the input (match auto-match style)
self.components['last_name_search_var'] = tk.StringVar()
# Control buttons
self.components['quit_btn'] = None
self.components['save_btn_bottom'] = None
def _create_main_panels(self):
"""Create the main left and right panels"""
# Left panel: People list
self.components['people_frame'] = ttk.LabelFrame(self.main_frame, text="People", padding="10")
self.components['people_frame'].grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(0, 8))
self.components['people_frame'].columnconfigure(0, weight=1)
# Right panel: Faces for selected person
self.components['faces_frame'] = ttk.LabelFrame(self.main_frame, text="Faces", padding="10")
self.components['faces_frame'].grid(row=1, column=1, sticky=(tk.W, tk.E, tk.N, tk.S))
self.components['faces_frame'].columnconfigure(0, weight=1)
self.components['faces_frame'].rowconfigure(0, weight=1)
# Create left panel content
self._create_left_panel_content()
# Create right panel content
self._create_right_panel_content()
# Create control buttons
self._create_control_buttons()
def _create_left_panel_content(self):
"""Create the left panel content for people list"""
people_frame = self.components['people_frame']
# Search controls
search_frame = ttk.Frame(people_frame)
search_frame.grid(row=0, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 6))
# Entry on the left
search_entry = ttk.Entry(search_frame, textvariable=self.components['last_name_search_var'], width=20)
search_entry.grid(row=0, column=0, sticky=tk.W)
# Buttons to the right of the entry
buttons_row = ttk.Frame(search_frame)
buttons_row.grid(row=0, column=1, sticky=tk.W, padx=(6, 0))
search_btn = ttk.Button(buttons_row, text="Search", width=8, command=self.apply_last_name_filter)
search_btn.pack(side=tk.LEFT, padx=(0, 5))
clear_btn = ttk.Button(buttons_row, text="Clear", width=6, command=self.clear_last_name_filter)
clear_btn.pack(side=tk.LEFT)
# Helper label directly under the entry
last_name_label = ttk.Label(search_frame, text="Type Last Name", font=("Arial", 8), foreground="gray")
last_name_label.grid(row=1, column=0, sticky=tk.W, pady=(2, 0))
# People list with scrollbar
people_canvas = tk.Canvas(people_frame, bg='white')
people_scrollbar = ttk.Scrollbar(people_frame, orient="vertical", command=people_canvas.yview)
self.components['people_list_inner'] = ttk.Frame(people_canvas)
people_canvas.create_window((0, 0), window=self.components['people_list_inner'], anchor="nw")
people_canvas.configure(yscrollcommand=people_scrollbar.set)
self.components['people_list_inner'].bind(
"<Configure>",
lambda e: people_canvas.configure(scrollregion=people_canvas.bbox("all"))
)
people_canvas.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
people_scrollbar.grid(row=1, column=1, sticky=(tk.N, tk.S))
people_frame.rowconfigure(1, weight=1)
# Store canvas reference
self.components['people_canvas'] = people_canvas
# Bind Enter key for search
search_entry.bind('<Return>', lambda e: self.apply_last_name_filter())
def _create_right_panel_content(self):
"""Create the right panel content for faces display"""
faces_frame = self.components['faces_frame']
# Style configuration
style = ttk.Style()
canvas_bg_color = style.lookup('TFrame', 'background') or '#d9d9d9'
self.components['faces_canvas'] = tk.Canvas(faces_frame, bg=canvas_bg_color, highlightthickness=0)
faces_scrollbar = ttk.Scrollbar(faces_frame, orient="vertical", command=self.components['faces_canvas'].yview)
self.components['faces_inner'] = ttk.Frame(self.components['faces_canvas'])
self.components['faces_canvas'].create_window((0, 0), window=self.components['faces_inner'], anchor="nw")
self.components['faces_canvas'].configure(yscrollcommand=faces_scrollbar.set)
self.components['faces_inner'].bind(
"<Configure>",
lambda e: self.components['faces_canvas'].configure(scrollregion=self.components['faces_canvas'].bbox("all"))
)
# Bind resize handler for responsive face grid
self.components['faces_canvas'].bind("<Configure>", self.on_faces_canvas_resize)
self.components['faces_canvas'].grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
faces_scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S))
def _create_control_buttons(self):
"""Create control buttons at the bottom"""
# Control buttons
control_frame = ttk.Frame(self.main_frame)
control_frame.grid(row=2, column=0, columnspan=2, pady=(10, 0), sticky=tk.E)
self.components['quit_btn'] = ttk.Button(control_frame, text="❌ Exit Edit Identified", command=self.on_quit)
self.components['quit_btn'].pack(side=tk.RIGHT)
self.components['save_btn_bottom'] = ttk.Button(control_frame, text="💾 Save changes", command=self.on_save_all_changes, state="disabled")
self.components['save_btn_bottom'].pack(side=tk.RIGHT, padx=(0, 10))
self.components['undo_btn'] = ttk.Button(control_frame, text="↶ Undo changes", command=self.undo_changes, state="disabled")
self.components['undo_btn'].pack(side=tk.RIGHT, padx=(0, 10))
def on_faces_canvas_resize(self, event):
"""Handle canvas resize for responsive face grid"""
if self.current_person_id is None:
return
# Debounce re-render on resize
try:
if self.resize_job is not None:
self.main_frame.after_cancel(self.resize_job)
except Exception:
pass
self.resize_job = self.main_frame.after(150, lambda: self.show_person_faces(self.current_person_id, self.current_person_name))
def load_people(self):
"""Load people from database with counts"""
with self.db.get_db_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"""
SELECT p.id, p.first_name, p.last_name, p.middle_name, p.maiden_name, p.date_of_birth, COUNT(f.id) as face_count
FROM people p
JOIN faces f ON f.person_id = p.id
GROUP BY p.id, p.last_name, p.first_name, p.middle_name, p.maiden_name, p.date_of_birth
HAVING face_count > 0
ORDER BY p.last_name, p.first_name COLLATE NOCASE
"""
)
self.people_data = []
for (pid, first_name, last_name, middle_name, maiden_name, date_of_birth, count) in cursor.fetchall():
# Create full name display with all available information
name_parts = []
if first_name:
name_parts.append(first_name)
if middle_name:
name_parts.append(middle_name)
if last_name:
name_parts.append(last_name)
if maiden_name:
name_parts.append(f"({maiden_name})")
full_name = ' '.join(name_parts) if name_parts else "Unknown"
# Create detailed display with date of birth if available
display_name = full_name
if date_of_birth:
display_name += f" - Born: {date_of_birth}"
self.people_data.append({
'id': pid,
'name': display_name,
'full_name': full_name,
'first_name': first_name or "",
'last_name': last_name or "",
'middle_name': middle_name or "",
'maiden_name': maiden_name or "",
'date_of_birth': date_of_birth or "",
'count': count
})
# Re-apply filter (if any) after loading
try:
self.apply_last_name_filter()
except Exception:
pass
def apply_last_name_filter(self):
"""Apply last name filter to people list"""
query = self.components['last_name_search_var'].get().strip().lower()
if query:
self.people_filtered = [p for p in self.people_data if p.get('last_name', '').lower().find(query) != -1]
else:
self.people_filtered = None
self.populate_people_list()
def clear_last_name_filter(self):
"""Clear the last name filter"""
self.components['last_name_search_var'].set("")
self.people_filtered = None
self.populate_people_list()
def populate_people_list(self):
"""Populate the people list with current data"""
# Clear existing widgets
for widget in self.components['people_list_inner'].winfo_children():
widget.destroy()
# Use filtered data if available, otherwise use all data
people_to_show = self.people_filtered if self.people_filtered is not None else self.people_data
for i, person in enumerate(people_to_show):
row_frame = ttk.Frame(self.components['people_list_inner'])
row_frame.pack(fill=tk.X, padx=2, pady=1)
# Edit button (on the left)
edit_btn = ttk.Button(row_frame, text="✏️", width=3,
command=lambda p=person: self.start_edit_person(p))
edit_btn.pack(side=tk.LEFT, padx=(0, 5))
# Add tooltip to edit button
ToolTip(edit_btn, "Update name")
# Label (clickable) - takes remaining space
name_lbl = ttk.Label(row_frame, text=f"{person['name']} ({person['count']})", font=("Arial", 10))
name_lbl.pack(side=tk.LEFT, fill=tk.X, expand=True)
name_lbl.bind("<Button-1>", lambda e, p=person: self.show_person_faces(p['id'], p['name']))
name_lbl.config(cursor="hand2")
# Bold if selected
if (self.selected_person_id is None and i == 0) or (self.selected_person_id == person['id']):
name_lbl.config(font=("Arial", 10, "bold"))
def start_edit_person(self, person_record):
"""Start editing a person's information"""
# Create a new window for editing
edit_window = tk.Toplevel(self.main_frame)
edit_window.title(f"Edit {person_record['name']}")
edit_window.geometry("500x400")
edit_window.transient(self.main_frame)
edit_window.grab_set()
# Center the window
edit_window.update_idletasks()
x = (edit_window.winfo_screenwidth() // 2) - (edit_window.winfo_width() // 2)
y = (edit_window.winfo_screenheight() // 2) - (edit_window.winfo_height() // 2)
edit_window.geometry(f"+{x}+{y}")
# Create form fields
form_frame = ttk.Frame(edit_window, padding="20")
form_frame.pack(fill=tk.BOTH, expand=True)
# First name
ttk.Label(form_frame, text="First name:").grid(row=0, column=0, sticky=tk.W, pady=5)
first_name_var = tk.StringVar(value=person_record.get('first_name', ''))
first_entry = ttk.Entry(form_frame, textvariable=first_name_var, width=30)
first_entry.grid(row=0, column=1, sticky=(tk.W, tk.E), pady=5)
# Last name
ttk.Label(form_frame, text="Last name:").grid(row=1, column=0, sticky=tk.W, pady=5)
last_name_var = tk.StringVar(value=person_record.get('last_name', ''))
last_entry = ttk.Entry(form_frame, textvariable=last_name_var, width=30)
last_entry.grid(row=1, column=1, sticky=(tk.W, tk.E), pady=5)
# Middle name
ttk.Label(form_frame, text="Middle name:").grid(row=2, column=0, sticky=tk.W, pady=5)
middle_name_var = tk.StringVar(value=person_record.get('middle_name', ''))
middle_entry = ttk.Entry(form_frame, textvariable=middle_name_var, width=30)
middle_entry.grid(row=2, column=1, sticky=(tk.W, tk.E), pady=5)
# Maiden name
ttk.Label(form_frame, text="Maiden name:").grid(row=3, column=0, sticky=tk.W, pady=5)
maiden_name_var = tk.StringVar(value=person_record.get('maiden_name', ''))
maiden_entry = ttk.Entry(form_frame, textvariable=maiden_name_var, width=30)
maiden_entry.grid(row=3, column=1, sticky=(tk.W, tk.E), pady=5)
# Date of birth
ttk.Label(form_frame, text="Date of birth:").grid(row=4, column=0, sticky=tk.W, pady=5)
dob_var = tk.StringVar(value=person_record.get('date_of_birth', ''))
dob_entry = ttk.Entry(form_frame, textvariable=dob_var, width=30, state='readonly')
dob_entry.grid(row=4, column=1, sticky=(tk.W, tk.E), pady=5)
# Calendar button for date of birth
def open_dob_calendar():
selected_date = self.gui_core.create_calendar_dialog(edit_window, "Select Date of Birth", dob_var.get())
if selected_date is not None:
dob_var.set(selected_date)
dob_calendar_btn = ttk.Button(form_frame, text="📅", width=3, command=open_dob_calendar)
dob_calendar_btn.grid(row=4, column=2, padx=(5, 0), pady=5)
# Configure grid weights
form_frame.columnconfigure(1, weight=1)
# Buttons
button_frame = ttk.Frame(edit_window)
button_frame.pack(fill=tk.X, padx=20, pady=10)
def save_rename():
"""Save the renamed person"""
try:
with self.db.get_db_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
UPDATE people
SET first_name = ?, last_name = ?, middle_name = ?, maiden_name = ?, date_of_birth = ?
WHERE id = ?
""", (
first_name_var.get().strip(),
last_name_var.get().strip(),
middle_name_var.get().strip(),
maiden_name_var.get().strip(),
dob_var.get().strip(),
person_record['id']
))
conn.commit()
# Refresh the people list
self.load_people()
self.populate_people_list()
# Close the edit window
edit_window.destroy()
messagebox.showinfo("Success", "Person information updated successfully.")
except Exception as e:
messagebox.showerror("Error", f"Failed to update person: {e}")
def cancel_edit():
"""Cancel editing"""
edit_window.destroy()
save_btn = ttk.Button(button_frame, text="Save", command=save_rename)
save_btn.pack(side=tk.LEFT, padx=(0, 10))
cancel_btn = ttk.Button(button_frame, text="Cancel", command=cancel_edit)
cancel_btn.pack(side=tk.LEFT)
# Focus on first name field
first_entry.focus_set()
# Add keyboard shortcuts
def try_save():
if save_btn.cget('state') == 'normal':
save_rename()
first_entry.bind('<Return>', lambda e: try_save())
last_entry.bind('<Return>', lambda e: try_save())
middle_entry.bind('<Return>', lambda e: try_save())
maiden_entry.bind('<Return>', lambda e: try_save())
dob_entry.bind('<Return>', lambda e: try_save())
first_entry.bind('<Escape>', lambda e: cancel_edit())
last_entry.bind('<Escape>', lambda e: cancel_edit())
middle_entry.bind('<Escape>', lambda e: cancel_edit())
maiden_entry.bind('<Escape>', lambda e: cancel_edit())
dob_entry.bind('<Escape>', lambda e: cancel_edit())
# Add validation
def validate_save_button():
first_name = first_name_var.get().strip()
last_name = last_name_var.get().strip()
if first_name and last_name:
save_btn.config(state='normal')
else:
save_btn.config(state='disabled')
# Bind validation to all fields
first_name_var.trace('w', lambda *args: validate_save_button())
last_name_var.trace('w', lambda *args: validate_save_button())
middle_name_var.trace('w', lambda *args: validate_save_button())
maiden_name_var.trace('w', lambda *args: validate_save_button())
dob_var.trace('w', lambda *args: validate_save_button())
# Initial validation
validate_save_button()
def show_person_faces(self, person_id, person_name):
"""Show faces for the selected person"""
self.current_person_id = person_id
self.current_person_name = person_name
self.selected_person_id = person_id
# Clear existing face widgets
for widget in self.components['faces_inner'].winfo_children():
widget.destroy()
# Load faces for this person
with self.db.get_db_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
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 = ?
ORDER BY p.filename
""", (person_id,))
faces = cursor.fetchall()
# Filter out unmatched faces
visible_faces = [face for face in faces if face[0] not in self.unmatched_faces]
if not visible_faces:
if not faces:
no_faces_label = ttk.Label(self.components['faces_inner'],
text="No faces found for this person",
font=("Arial", 12))
else:
no_faces_label = ttk.Label(self.components['faces_inner'],
text="All faces unmatched",
font=("Arial", 12))
no_faces_label.pack(pady=20)
return
# Display faces in a grid
self._display_faces_grid(visible_faces)
# Update people list to show selection
self.populate_people_list()
# Update button states based on unmatched faces
self._update_undo_button_state()
self._update_save_button_state()
def _display_faces_grid(self, faces):
"""Display faces in a responsive grid layout"""
# Calculate grid dimensions based on canvas width
canvas_width = self.components['faces_canvas'].winfo_width()
if canvas_width < 100: # Canvas not yet rendered
canvas_width = 400 # Default width
face_size = 80
padding = 10
faces_per_row = max(1, (canvas_width - padding) // (face_size + padding))
# Clear existing images
self.right_panel_images.clear()
for i, (face_id, photo_id, photo_path, filename, location) in enumerate(faces):
row = i // faces_per_row
col = i % faces_per_row
# Create face frame
face_frame = ttk.Frame(self.components['faces_inner'])
face_frame.grid(row=row, column=col, padx=5, pady=5, sticky=(tk.W, tk.E, tk.N, tk.S))
# Face image
try:
face_crop_path = self.face_processor._extract_face_crop(photo_path, location, face_id)
if face_crop_path and os.path.exists(face_crop_path):
self.temp_crops.append(face_crop_path)
image = Image.open(face_crop_path)
image.thumbnail((face_size, face_size), Image.Resampling.LANCZOS)
photo = ImageTk.PhotoImage(image)
self.right_panel_images.append(photo) # Keep reference
# Create canvas for face image
face_canvas = tk.Canvas(face_frame, width=face_size, height=face_size, highlightthickness=0)
face_canvas.pack()
face_canvas.create_image(face_size//2, face_size//2, image=photo, anchor=tk.CENTER)
# Add photo icon
self.gui_core.create_photo_icon(face_canvas, photo_path, icon_size=15,
face_x=0, face_y=0,
face_width=face_size, face_height=face_size,
canvas_width=face_size, canvas_height=face_size)
# Unmatch button
unmatch_btn = ttk.Button(face_frame, text="Unmatch",
command=lambda fid=face_id: self.unmatch_face(fid))
unmatch_btn.pack(pady=2)
else:
# Placeholder for missing face crop
placeholder_label = ttk.Label(face_frame, text=f"Face {face_id}",
font=("Arial", 8))
placeholder_label.pack()
except Exception as e:
print(f"Error displaying face {face_id}: {e}")
# Placeholder for error
error_label = ttk.Label(face_frame, text=f"Error {face_id}",
font=("Arial", 8), foreground="red")
error_label.pack()
def unmatch_face(self, face_id):
"""Unmatch a face from its person"""
if face_id not in self.unmatched_faces:
self.unmatched_faces.add(face_id)
if self.current_person_id not in self.unmatched_by_person:
self.unmatched_by_person[self.current_person_id] = set()
self.unmatched_by_person[self.current_person_id].add(face_id)
print(f"Face {face_id} marked for unmatching")
# Immediately refresh the display to hide the unmatched face
if self.current_person_id:
self.show_person_faces(self.current_person_id, self.current_person_name)
# Update button states
self._update_undo_button_state()
self._update_save_button_state()
def _update_undo_button_state(self):
"""Update the undo button state based on unmatched faces for current person"""
if 'undo_btn' in self.components:
current_has_unmatched = bool(self.unmatched_by_person.get(self.current_person_id))
if current_has_unmatched:
self.components['undo_btn'].config(state="normal")
else:
self.components['undo_btn'].config(state="disabled")
def _update_save_button_state(self):
"""Update the save button state based on whether there are any unmatched faces to save"""
if 'save_btn_bottom' in self.components:
if self.unmatched_faces:
self.components['save_btn_bottom'].config(state="normal")
else:
self.components['save_btn_bottom'].config(state="disabled")
def undo_changes(self):
"""Undo all unmatched faces for the current person"""
if self.current_person_id and self.current_person_id in self.unmatched_by_person:
# Remove faces for current person from unmatched sets
person_faces = self.unmatched_by_person[self.current_person_id]
self.unmatched_faces -= person_faces
del self.unmatched_by_person[self.current_person_id]
# Refresh the display to show the restored faces
if self.current_person_id:
self.show_person_faces(self.current_person_id, self.current_person_name)
# Update button states
self._update_undo_button_state()
self._update_save_button_state()
messagebox.showinfo("Undo", f"Undid changes for {len(person_faces)} face(s).")
else:
messagebox.showinfo("No Changes", "No changes to undo for this person.")
def on_quit(self):
"""Handle quit button click"""
# Check for unsaved changes
if self.unmatched_faces:
result = self.gui_core.create_large_messagebox(
self.main_frame,
"Unsaved Changes",
f"You have {len(self.unmatched_faces)} unsaved changes.\n\n"
"Do you want to save them before quitting?\n\n"
"• Yes: Save changes and quit\n"
"• No: Quit without saving\n"
"• Cancel: Return to modify",
"askyesnocancel"
)
if result is True: # Yes - Save and quit
self.on_save_all_changes()
elif result is False: # No - Quit without saving
pass
else: # Cancel - Don't quit
return
# Clean up and deactivate
self._cleanup()
self.is_active = False
# Navigate to home if callback is available (dashboard mode)
if self.on_navigate_home:
self.on_navigate_home()
def on_save_all_changes(self):
"""Save all unmatched faces to database"""
if not self.unmatched_faces:
messagebox.showinfo("No Changes", "No changes to save.")
return
try:
with self.db.get_db_connection() as conn:
cursor = conn.cursor()
count = 0
for face_id in self.unmatched_faces:
cursor.execute("UPDATE faces SET person_id = NULL WHERE id = ?", (face_id,))
count += 1
conn.commit()
# Clear the unmatched faces
self.unmatched_faces.clear()
self.unmatched_by_person.clear()
# Refresh the display
if self.current_person_id:
self.show_person_faces(self.current_person_id, self.current_person_name)
# Update button states
self._update_undo_button_state()
self._update_save_button_state()
messagebox.showinfo("Changes Saved", f"Successfully unlinked {count} face(s).")
except Exception as e:
messagebox.showerror("Error", f"Failed to save changes: {e}")
def _cleanup(self):
"""Clean up resources"""
# Clear temporary crops
for crop_path in self.temp_crops:
try:
if os.path.exists(crop_path):
os.remove(crop_path)
except Exception:
pass
self.temp_crops.clear()
# Clear right panel images
self.right_panel_images.clear()
# Clear state
self.unmatched_faces.clear()
self.unmatched_by_person.clear()
self.original_faces_data.clear()
self.people_data.clear()
self.people_filtered = None
self.current_person_id = None
self.current_person_name = ""
self.selected_person_id = None
def activate(self):
"""Activate the panel"""
self.is_active = True
# Initial load
self.load_people()
self.populate_people_list()
# Show first person's faces by default and mark selected
if self.people_data:
self.selected_person_id = self.people_data[0]['id']
self.show_person_faces(self.people_data[0]['id'], self.people_data[0]['name'])
def deactivate(self):
"""Deactivate the panel"""
if self.is_active:
self._cleanup()
self.is_active = False
def update_layout(self):
"""Update panel layout for responsiveness"""
if hasattr(self, 'components') and 'faces_canvas' in self.components:
# Update faces canvas scroll region
canvas = self.components['faces_canvas']
canvas.update_idletasks()
canvas.configure(scrollregion=canvas.bbox("all"))