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:
parent
6bfc44a6c9
commit
7f89c2a825
161
photo_tagger.py
161
photo_tagger.py
@ -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()
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user