diff --git a/photo_tagger.py b/photo_tagger.py index ab475ad..bbc27c1 100644 --- a/photo_tagger.py +++ b/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()