Add tagging functionality to PhotoTagger GUI

Introduce a tagging system that allows users to manage tags for photos directly within the interface. Implement a tagging widget for each photo, enabling users to add and remove tags dynamically. Include a save button to persist tag changes to the database, enhancing the overall tagging experience. Update the layout to accommodate the new tagging feature and ensure existing tags are loaded for user convenience.
This commit is contained in:
tanyar09 2025-10-01 15:24:48 -04:00
parent 6bfc44a6c9
commit 7f89c2a825

View File

@ -4281,6 +4281,10 @@ class PhotoTagger:
# Track folder expand/collapse states
folder_states = {} # folder_path -> is_expanded
# Track pending tag changes (photo_id -> list of tag names)
pending_tag_changes = {}
existing_tags = [] # Cache of existing tags from database
# Hide window initially to prevent flash at corner
root.withdraw()
@ -4316,6 +4320,7 @@ class PhotoTagger:
root.rowconfigure(0, weight=1)
main_frame.columnconfigure(0, weight=1)
main_frame.rowconfigure(1, weight=1)
main_frame.rowconfigure(2, weight=0)
# Title and controls frame
header_frame = ttk.Frame(main_frame)
@ -4364,6 +4369,14 @@ class PhotoTagger:
content_canvas.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
content_scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S))
# Bottom frame for save button
bottom_frame = ttk.Frame(main_frame)
bottom_frame.grid(row=2, column=0, sticky=(tk.W, tk.E), pady=(10, 0))
# Save tagging button (function will be defined later)
save_button = ttk.Button(bottom_frame, text="Save Tagging")
save_button.pack(side=tk.RIGHT, padx=10, pady=5)
# Enable mouse scroll anywhere in the dialog
def on_mousewheel(event):
content_canvas.yview_scroll(int(-1*(event.delta/120)), "units")
@ -4486,9 +4499,9 @@ class PhotoTagger:
# Column visibility state
column_visibility = {
'list': {'id': True, 'filename': True, 'path': True, 'processed': True, 'date_taken': True, 'faces': True, 'tags': True},
'icons': {'thumbnail': True, 'id': True, 'filename': True, 'processed': True, 'date_taken': True, 'faces': True, 'tags': True},
'compact': {'filename': True, 'faces': True, 'tags': True}
'list': {'id': True, 'filename': True, 'path': True, 'processed': True, 'date_taken': True, 'faces': True, 'tags': True, 'tagging': True},
'icons': {'thumbnail': True, 'id': True, 'filename': True, 'processed': True, 'date_taken': True, 'faces': True, 'tags': True, 'tagging': True},
'compact': {'filename': True, 'faces': True, 'tags': True, 'tagging': True}
}
# Column order and configuration
@ -4500,7 +4513,8 @@ class PhotoTagger:
{'key': 'processed', 'label': 'Processed', 'width': 80, 'weight': 0},
{'key': 'date_taken', 'label': 'Date Taken', 'width': 120, 'weight': 0},
{'key': 'faces', 'label': 'Faces', 'width': 60, 'weight': 0},
{'key': 'tags', 'label': 'Tags', 'width': 150, 'weight': 1}
{'key': 'tags', 'label': 'Tags', 'width': 150, 'weight': 1},
{'key': 'tagging', 'label': 'Tagging', 'width': 200, 'weight': 0}
],
'icons': [
{'key': 'thumbnail', 'label': 'Thumbnail', 'width': 160, 'weight': 0},
@ -4509,12 +4523,14 @@ class PhotoTagger:
{'key': 'processed', 'label': 'Processed', 'width': 80, 'weight': 0},
{'key': 'date_taken', 'label': 'Date Taken', 'width': 120, 'weight': 0},
{'key': 'faces', 'label': 'Faces', 'width': 60, 'weight': 0},
{'key': 'tags', 'label': 'Tags', 'width': 150, 'weight': 1}
{'key': 'tags', 'label': 'Tags', 'width': 150, 'weight': 1},
{'key': 'tagging', 'label': 'Tagging', 'width': 200, 'weight': 0}
],
'compact': [
{'key': 'filename', 'label': 'Filename', 'width': 200, 'weight': 1},
{'key': 'faces', 'label': 'Faces', 'width': 60, 'weight': 0},
{'key': 'tags', 'label': 'Tags', 'width': 150, 'weight': 1}
{'key': 'tags', 'label': 'Tags', 'width': 150, 'weight': 1},
{'key': 'tagging', 'label': 'Tagging', 'width': 200, 'weight': 0}
]
}
@ -4606,6 +4622,121 @@ class PhotoTagger:
folder_states[folder_path] = not folder_states.get(folder_path, True)
switch_view_mode(view_mode)
def load_existing_tags():
"""Load existing tags from database"""
nonlocal existing_tags
with self.get_db_connection() as conn:
cursor = conn.cursor()
cursor.execute('SELECT tag_name FROM tags ORDER BY tag_name')
existing_tags = [row[0] for row in cursor.fetchall()]
def create_tagging_widget(parent, photo_id, current_tags=""):
"""Create a tagging widget with dropdown and text input"""
import tkinter as tk
from tkinter import ttk
# Create frame for tagging widget
tagging_frame = ttk.Frame(parent)
# Create combobox for tag selection/input
tag_var = tk.StringVar()
tag_combo = ttk.Combobox(tagging_frame, textvariable=tag_var, width=12)
tag_combo['values'] = existing_tags
tag_combo.pack(side=tk.LEFT, padx=2, pady=2)
# Create label to show current pending tags
pending_tags_var = tk.StringVar()
pending_tags_label = ttk.Label(tagging_frame, textvariable=pending_tags_var,
font=("Arial", 8), foreground="blue", width=20)
pending_tags_label.pack(side=tk.LEFT, padx=2, pady=2)
# Initialize pending tags display
if photo_id in pending_tag_changes:
pending_tags_var.set(", ".join(pending_tag_changes[photo_id]))
else:
pending_tags_var.set(current_tags or "")
# Add button to add tag
def add_tag():
tag_name = tag_var.get().strip()
if tag_name:
# Add to pending changes
if photo_id not in pending_tag_changes:
pending_tag_changes[photo_id] = []
# Check if tag already exists (case insensitive)
tag_exists = any(tag.lower() == tag_name.lower() for tag in pending_tag_changes[photo_id])
if not tag_exists:
pending_tag_changes[photo_id].append(tag_name)
# Update display
pending_tags_var.set(", ".join(pending_tag_changes[photo_id]))
tag_var.set("") # Clear the input field
add_button = tk.Button(tagging_frame, text="+", width=2, height=1, command=add_tag)
add_button.pack(side=tk.LEFT, padx=2, pady=2)
# Remove button to remove last tag
def remove_tag():
if photo_id in pending_tag_changes and pending_tag_changes[photo_id]:
pending_tag_changes[photo_id].pop()
if pending_tag_changes[photo_id]:
pending_tags_var.set(", ".join(pending_tag_changes[photo_id]))
else:
pending_tags_var.set("")
del pending_tag_changes[photo_id]
remove_button = tk.Button(tagging_frame, text="-", width=2, height=1, command=remove_tag)
remove_button.pack(side=tk.LEFT, padx=2, pady=2)
return tagging_frame
def save_tagging_changes():
"""Save all pending tag changes to database"""
if not pending_tag_changes:
messagebox.showinfo("Info", "No tag changes to save.")
return
try:
with self.get_db_connection() as conn:
cursor = conn.cursor()
for photo_id, tag_names in pending_tag_changes.items():
for tag_name in tag_names:
# Insert or get tag_id (case insensitive)
cursor.execute(
'INSERT OR IGNORE INTO tags (tag_name) VALUES (?)',
(tag_name,)
)
# Get tag_id (case insensitive lookup)
cursor.execute(
'SELECT id FROM tags WHERE LOWER(tag_name) = LOWER(?)',
(tag_name,)
)
tag_id = cursor.fetchone()[0]
# Insert linkage (ignore if already exists)
cursor.execute(
'INSERT OR IGNORE INTO phototaglinkage (photo_id, tag_id) VALUES (?, ?)',
(photo_id, tag_id)
)
conn.commit()
# Clear pending changes and reload data
pending_tag_changes.clear()
load_existing_tags()
load_photos()
switch_view_mode(view_mode_var.get())
messagebox.showinfo("Success", f"Saved tags for {len(pending_tag_changes)} photos.")
except Exception as e:
messagebox.showerror("Error", f"Failed to save tags: {str(e)}")
# Configure the save button command now that the function is defined
save_button.configure(command=save_tagging_changes)
def clear_content():
for widget in content_inner.winfo_children():
widget.destroy()
@ -4813,6 +4944,11 @@ class PhotoTagger:
text = str(photo['face_count'])
elif key == 'tags':
text = photo['tags'] or "None"
elif key == 'tagging':
# Create tagging widget
tagging_widget = create_tagging_widget(row_frame, photo['id'], photo['tags'] or "")
tagging_widget.grid(row=0, column=i, padx=5, sticky=tk.W)
continue
ttk.Label(row_frame, text=text).grid(row=0, column=i, padx=5, sticky=tk.W)
@ -4917,6 +5053,12 @@ class PhotoTagger:
text = str(photo['face_count'])
elif key == 'tags':
text = photo['tags'] or "None"
elif key == 'tagging':
# Create tagging widget
tagging_widget = create_tagging_widget(row_frame, photo['id'], photo['tags'] or "")
tagging_widget.grid(row=0, column=col_idx, padx=5, sticky=tk.W)
col_idx += 1
continue
ttk.Label(row_frame, text=text).grid(row=0, column=col_idx, padx=5, sticky=tk.W)
@ -4988,6 +5130,12 @@ class PhotoTagger:
text = str(photo['face_count'])
elif key == 'tags':
text = photo['tags'] or "None"
elif key == 'tagging':
# Create tagging widget
tagging_widget = create_tagging_widget(row_frame, photo['id'], photo['tags'] or "")
tagging_widget.grid(row=0, column=col_idx, padx=5, sticky=tk.W)
col_idx += 1
continue
ttk.Label(row_frame, text=text).grid(row=0, column=col_idx, padx=5, sticky=tk.W)
col_idx += 1
@ -5005,6 +5153,7 @@ class PhotoTagger:
# No need for canvas resize handler since icon view is now single column
# Load initial data and show default view
load_existing_tags()
load_photos()
show_list_view()