diff --git a/photo_tagger.py b/photo_tagger.py index 32f8947..f150095 100644 --- a/photo_tagger.py +++ b/photo_tagger.py @@ -4547,8 +4547,16 @@ class PhotoTagger: # Delete tags cursor.execute(f"DELETE FROM tags WHERE id IN ({','.join('?' for _ in ids_to_delete)})", ids_to_delete) conn.commit() + + # Clean up pending tag changes for deleted tags + for photo_id in list(pending_tag_changes.keys()): + pending_tag_changes[photo_id] = [tid for tid in pending_tag_changes[photo_id] if tid not in ids_to_delete] + if not pending_tag_changes[photo_id]: + del pending_tag_changes[photo_id] + refresh_tag_list() load_existing_tags() + load_photos() # Refresh photo data to reflect deleted tags switch_view_mode(view_mode_var.get()) except Exception as e: messagebox.showerror("Error", f"Failed to delete tags: {e}") @@ -5092,6 +5100,106 @@ class PhotoTagger: # Focus the popup popup.focus_set() + # Shared tag linking functions for all view modes + def create_add_tag_handler(photo_id, label_widget, photo_tags, available_tags): + """Create a handler function for adding tags to a photo""" + def handler(): + # Create popup window for tag selection + popup = tk.Toplevel(root) + popup.title("Add Tag") + popup.transient(root) + popup.grab_set() + popup.geometry("300x150") + popup.resizable(False, False) + + # Create main frame + main_frame = ttk.Frame(popup, padding="10") + main_frame.pack(fill=tk.BOTH, expand=True) + + ttk.Label(main_frame, text="Select a tag:").pack(pady=(0, 5)) + + tag_var = tk.StringVar() + combo = ttk.Combobox(main_frame, textvariable=tag_var, values=available_tags, width=30) + combo.pack(pady=(0, 10), fill=tk.X) + combo.focus_set() + + def confirm(): + tag_name = tag_var.get().strip() + if not tag_name: + popup.destroy() + return + + # Get or create tag ID + if tag_name in tag_name_to_id: + tag_id = tag_name_to_id[tag_name] + else: + # Create new tag in database + with self.get_db_connection() as conn: + cursor = conn.cursor() + cursor.execute('INSERT OR IGNORE INTO tags (tag_name) VALUES (?)', (tag_name,)) + cursor.execute('SELECT id FROM tags WHERE tag_name = ?', (tag_name,)) + tag_id = cursor.fetchone()[0] + # Update mappings + tag_name_to_id[tag_name] = tag_id + tag_id_to_name[tag_id] = tag_name + if tag_name not in existing_tags: + existing_tags.append(tag_name) + existing_tags.sort() + + # Check if tag already exists (compare tag IDs) before adding to pending changes + existing_tag_ids = self._get_existing_tag_ids_for_photo(photo_id) + pending_tag_ids = pending_tag_changes.get(photo_id, []) + all_existing_tag_ids = existing_tag_ids + pending_tag_ids + + if tag_id not in all_existing_tag_ids: + # Only add to pending changes if tag is actually new + if photo_id not in pending_tag_changes: + pending_tag_changes[photo_id] = [] + pending_tag_changes[photo_id].append(tag_id) + # Update the display immediately - combine existing and pending, removing duplicates + existing_tags_list = self._parse_tags_string(photo_tags) + pending_tag_names = [tag_id_to_name.get(tid, f"Unknown {tid}") for tid in pending_tag_changes[photo_id]] + all_tags = existing_tags_list + pending_tag_names + unique_tags = self._deduplicate_tags(all_tags) + current_tags = ", ".join(unique_tags) + label_widget.configure(text=current_tags) + update_save_button_text() + + popup.destroy() + + ttk.Button(main_frame, text="Add", command=confirm).pack(pady=(0, 5)) + return handler + + + def create_tag_buttons_frame(parent, photo_id, photo_tags, existing_tags, use_grid=False, row=0, col=0): + """Create a frame with tag display and add button that can be used in any view mode""" + tags_frame = ttk.Frame(parent) + + # Display current tags + existing_tags_list = self._parse_tags_string(photo_tags) + pending_tag_names = [] + if photo_id in pending_tag_changes: + pending_tag_names = [tag_id_to_name.get(tid, f"Unknown {tid}") for tid in pending_tag_changes[photo_id]] + + all_tags = existing_tags_list + pending_tag_names + unique_tags = self._deduplicate_tags(all_tags) + current_display = ", ".join(unique_tags) if unique_tags else "None" + + tags_text = ttk.Label(tags_frame, text=current_display) + tags_text.pack(side=tk.LEFT) + + # Add button + add_btn = tk.Button(tags_frame, text="+", width=2, + command=create_add_tag_handler(photo_id, tags_text, photo_tags, existing_tags)) + add_btn.pack(side=tk.LEFT, padx=(6, 0)) + + # Pack or grid the frame based on the view mode + if use_grid: + tags_frame.grid(row=row, column=col, padx=5, sticky=tk.W) + else: + tags_frame.pack(side=tk.LEFT, padx=5) + + return tags_frame def show_list_view(): clear_content() @@ -5190,93 +5298,8 @@ class PhotoTagger: elif key == 'faces': text = str(photo['face_count']) elif key == 'tags': - # Tags cell with inline '+' to add tags - tags_frame = ttk.Frame(row_frame) - tags_frame.grid(row=0, column=i, padx=5, sticky=tk.W) - - # Show current tags + pending changes - current_display = photo['tags'] or "" - if photo['id'] in pending_tag_changes: - # Convert pending tag IDs to names for display - pending_tag_names = [tag_id_to_name.get(tag_id, f"Unknown {tag_id}") for tag_id in pending_tag_changes[photo['id']]] - existing_tags_list = self._parse_tags_string(current_display) - all_tags = existing_tags_list + pending_tag_names - current_display = ", ".join(all_tags) - if not current_display: - current_display = "None" - tags_text = ttk.Label(tags_frame, text=current_display) - tags_text.pack(side=tk.LEFT) - - def make_add_tag_handler(photo_id, label_widget, photo_tags, available_tags): - def handler(): - # Simple dropdown to choose existing tag - popup = tk.Toplevel(root) - popup.title("Add Tag") - popup.transient(root) - popup.grab_set() - popup.geometry("300x150") - popup.resizable(False, False) - - # Create main frame - main_frame = ttk.Frame(popup, padding="10") - main_frame.pack(fill=tk.BOTH, expand=True) - - ttk.Label(main_frame, text="Select a tag:").pack(pady=(0, 5)) - - tag_var = tk.StringVar() - combo = ttk.Combobox(main_frame, textvariable=tag_var, values=available_tags, width=30) - combo.pack(pady=(0, 10), fill=tk.X) - combo.focus_set() - - def confirm(): - tag_name = tag_var.get().strip() - if not tag_name: - popup.destroy() - return - - # Get or create tag ID - if tag_name in tag_name_to_id: - tag_id = tag_name_to_id[tag_name] - else: - # Create new tag in database - with self.get_db_connection() as conn: - cursor = conn.cursor() - cursor.execute('INSERT OR IGNORE INTO tags (tag_name) VALUES (?)', (tag_name,)) - cursor.execute('SELECT id FROM tags WHERE tag_name = ?', (tag_name,)) - tag_id = cursor.fetchone()[0] - # Update mappings - tag_name_to_id[tag_name] = tag_id - tag_id_to_name[tag_id] = tag_name - if tag_name not in existing_tags: - existing_tags.append(tag_name) - existing_tags.sort() - - # Check if tag already exists (compare tag IDs) before adding to pending changes - existing_tag_ids = self._get_existing_tag_ids_for_photo(photo_id) - pending_tag_ids = pending_tag_changes.get(photo_id, []) - all_existing_tag_ids = existing_tag_ids + pending_tag_ids - - if tag_id not in all_existing_tag_ids: - # Only add to pending changes if tag is actually new - if photo_id not in pending_tag_changes: - pending_tag_changes[photo_id] = [] - pending_tag_changes[photo_id].append(tag_id) - # Update the display immediately - combine existing and pending, removing duplicates - existing_tags_list = self._parse_tags_string(photo_tags) - pending_tag_names = [tag_id_to_name.get(tid, f"Unknown {tid}") for tid in pending_tag_changes[photo_id]] - all_tags = existing_tags_list + pending_tag_names - unique_tags = self._deduplicate_tags(all_tags) - current_tags = ", ".join(unique_tags) - label_widget.configure(text=current_tags) - update_save_button_text() - - popup.destroy() - - ttk.Button(main_frame, text="Add", command=confirm).pack(pady=(0, 5)) - return handler - - add_btn = tk.Button(tags_frame, text="+", width=2, command=make_add_tag_handler(photo['id'], tags_text, photo['tags'], existing_tags)) - add_btn.pack(side=tk.LEFT, padx=(6,0)) + # Use shared tag buttons frame for list view + create_tag_buttons_frame(row_frame, photo['id'], photo['tags'], existing_tags, use_grid=True, row=0, col=i) continue ttk.Label(row_frame, text=text).grid(row=0, column=i, padx=5, sticky=tk.W) @@ -5381,78 +5404,8 @@ class PhotoTagger: elif key == 'faces': text = str(photo['face_count']) elif key == 'tags': - # Tags cell with inline '+' to add tags - tags_frame = ttk.Frame(row_frame) - tags_frame.grid(row=0, column=col_idx, padx=5, sticky=tk.W) - - # Show current tags + pending changes - current_display = photo['tags'] or "None" - if photo['id'] in pending_tag_changes: - # Convert pending tag IDs to names for display - pending_tag_names = [tag_id_to_name.get(tag_id, f"Unknown {tag_id}") for tag_id in pending_tag_changes[photo['id']]] - current_display = ", ".join(pending_tag_names) - tags_text = ttk.Label(tags_frame, text=current_display) - tags_text.pack(side=tk.LEFT) - - def make_add_tag_handler(photo_id, label_widget, photo_tags, available_tags): - def handler(): - popup = tk.Toplevel(root) - popup.title("Add Tag") - popup.transient(root) - popup.grab_set() - ttk.Label(popup, text="Select a tag:").grid(row=0, column=0, padx=8, pady=8, sticky=tk.W) - tag_var = tk.StringVar() - combo = ttk.Combobox(popup, textvariable=tag_var, values=available_tags, width=24) - combo.grid(row=1, column=0, padx=8, pady=(0,8), sticky=(tk.W, tk.E)) - combo.focus_set() - - def confirm(): - tag_name = tag_var.get().strip() - if not tag_name: - popup.destroy() - return - - # Get or create tag ID - if tag_name in tag_name_to_id: - tag_id = tag_name_to_id[tag_name] - else: - # Create new tag in database - with self.get_db_connection() as conn: - cursor = conn.cursor() - cursor.execute('INSERT OR IGNORE INTO tags (tag_name) VALUES (?)', (tag_name,)) - cursor.execute('SELECT id FROM tags WHERE tag_name = ?', (tag_name,)) - tag_id = cursor.fetchone()[0] - # Update mappings - tag_name_to_id[tag_name] = tag_id - tag_id_to_name[tag_id] = tag_name - if tag_name not in existing_tags: - existing_tags.append(tag_name) - existing_tags.sort() - - # Add to pending changes instead of saving immediately - if photo_id not in pending_tag_changes: - pending_tag_changes[photo_id] = [] - - # Check if tag already exists (compare tag IDs) - existing_tag_ids = self._get_existing_tag_ids_for_photo(photo_id) - all_existing_tag_ids = existing_tag_ids + pending_tag_changes[photo_id] - if tag_id not in all_existing_tag_ids: - pending_tag_changes[photo_id].append(tag_id) - # Update the display immediately - combine existing and pending, removing duplicates - existing_tags_list = self._parse_tags_string(photo_tags) - pending_tag_names = [tag_id_to_name.get(tid, f"Unknown {tid}") for tid in pending_tag_changes[photo_id]] - all_tags = existing_tags_list + pending_tag_names - unique_tags = self._deduplicate_tags(all_tags) - current_tags = ", ".join(unique_tags) - label_widget.configure(text=current_tags) - - popup.destroy() - - ttk.Button(popup, text="Add", command=confirm).grid(row=2, column=0, padx=8, pady=(0,8), sticky=tk.E) - return handler - - add_btn = tk.Button(tags_frame, text="+", width=2, command=make_add_tag_handler(photo['id'], tags_text, photo['tags'], existing_tags)) - add_btn.pack(side=tk.LEFT, padx=(6,0)) + # Use shared tag buttons frame for icon view + create_tag_buttons_frame(row_frame, photo['id'], photo['tags'], existing_tags, use_grid=True, row=0, col=col_idx) col_idx += 1 continue @@ -5525,84 +5478,8 @@ class PhotoTagger: elif key == 'faces': text = str(photo['face_count']) elif key == 'tags': - # Tags cell with inline '+' to add tags - tags_frame = ttk.Frame(row_frame) - tags_frame.grid(row=0, column=col_idx, padx=5, sticky=tk.W) - - # Show current tags + pending changes - current_display = photo['tags'] or "" - if photo['id'] in pending_tag_changes: - # Convert pending tag IDs to names for display - pending_tag_names = [tag_id_to_name.get(tag_id, f"Unknown {tag_id}") for tag_id in pending_tag_changes[photo['id']]] - existing_tags_list = self._parse_tags_string(current_display) - all_tags = existing_tags_list + pending_tag_names - current_display = ", ".join(all_tags) - if not current_display: - current_display = "None" - tags_text = ttk.Label(tags_frame, text=current_display) - tags_text.pack(side=tk.LEFT) - - def make_add_tag_handler(photo_id, label_widget, photo_tags, available_tags): - def handler(): - popup = tk.Toplevel(root) - popup.title("Add Tag") - popup.transient(root) - popup.grab_set() - ttk.Label(popup, text="Select a tag:").grid(row=0, column=0, padx=8, pady=8, sticky=tk.W) - tag_var = tk.StringVar() - combo = ttk.Combobox(popup, textvariable=tag_var, values=available_tags, width=24) - combo.grid(row=1, column=0, padx=8, pady=(0,8), sticky=(tk.W, tk.E)) - combo.focus_set() - - def confirm(): - tag_name = tag_var.get().strip() - if not tag_name: - popup.destroy() - return - - # Get or create tag ID - if tag_name in tag_name_to_id: - tag_id = tag_name_to_id[tag_name] - else: - # Create new tag in database - with self.get_db_connection() as conn: - cursor = conn.cursor() - cursor.execute('INSERT OR IGNORE INTO tags (tag_name) VALUES (?)', (tag_name,)) - cursor.execute('SELECT id FROM tags WHERE tag_name = ?', (tag_name,)) - tag_id = cursor.fetchone()[0] - # Update mappings - tag_name_to_id[tag_name] = tag_id - tag_id_to_name[tag_id] = tag_name - if tag_name not in existing_tags: - existing_tags.append(tag_name) - existing_tags.sort() - - # Check if tag already exists (compare tag IDs) before adding to pending changes - existing_tag_ids = self._get_existing_tag_ids_for_photo(photo_id) - pending_tag_ids = pending_tag_changes.get(photo_id, []) - all_existing_tag_ids = existing_tag_ids + pending_tag_ids - - if tag_id not in all_existing_tag_ids: - # Only add to pending changes if tag is actually new - if photo_id not in pending_tag_changes: - pending_tag_changes[photo_id] = [] - pending_tag_changes[photo_id].append(tag_id) - # Update the display immediately - combine existing and pending, removing duplicates - existing_tags_list = self._parse_tags_string(photo_tags) - pending_tag_names = [tag_id_to_name.get(tid, f"Unknown {tid}") for tid in pending_tag_changes[photo_id]] - all_tags = existing_tags_list + pending_tag_names - unique_tags = self._deduplicate_tags(all_tags) - current_tags = ", ".join(unique_tags) - label_widget.configure(text=current_tags) - update_save_button_text() - - popup.destroy() - - ttk.Button(popup, text="Add", command=confirm).grid(row=2, column=0, padx=8, pady=(0,8), sticky=tk.E) - return handler - - add_btn = tk.Button(tags_frame, text="+", width=2, command=make_add_tag_handler(photo['id'], tags_text, photo['tags'], existing_tags)) - add_btn.pack(side=tk.LEFT, padx=(6,0)) + # Use shared tag buttons frame for compact view + create_tag_buttons_frame(row_frame, photo['id'], photo['tags'], existing_tags, use_grid=True, row=0, col=col_idx) col_idx += 1 continue