diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index f4357b049..73382b1da 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -8,6 +8,7 @@ """A Qt driver for TagStudio.""" import ctypes +import copy import logging import math import os @@ -64,6 +65,7 @@ TS_FOLDER_NAME, VERSION_BRANCH, VERSION, + TEXT_FIELDS, TAG_FAVORITE, TAG_ARCHIVED, ) @@ -299,6 +301,9 @@ def start(self) -> None: icon.addFile(str(icon_path)) app.setWindowIcon(icon) + self.copied_fields: list[dict] = [] + self.is_buffer_merged: bool = False + menu_bar = QMenuBar(self.main_window) self.main_window.setMenuBar(menu_bar) menu_bar.setNativeMenuBar(True) @@ -392,6 +397,36 @@ def start(self) -> None: edit_menu.addSeparator() + # NOTE: Name is set in update_clipboard_actions() + self.copy_entry_fields_action = QAction(menu_bar) + self.copy_entry_fields_action.triggered.connect( + lambda: self.copy_entry_fields_callback() + ) + self.copy_entry_fields_action.setShortcut( + QtCore.QKeyCombination( + QtCore.Qt.KeyboardModifier(QtCore.Qt.KeyboardModifier.ControlModifier), + QtCore.Qt.Key.Key_C, + ) + ) + self.copy_entry_fields_action.setToolTip("Ctrl+C") + edit_menu.addAction(self.copy_entry_fields_action) + + # NOTE: Name is set in update_clipboard_actions() + self.paste_entry_fields_action = QAction(menu_bar) + self.paste_entry_fields_action.triggered.connect( + self.paste_entry_fields_callback + ) + self.paste_entry_fields_action.setShortcut( + QtCore.QKeyCombination( + QtCore.Qt.KeyboardModifier(QtCore.Qt.KeyboardModifier.ControlModifier), + QtCore.Qt.Key.Key_V, + ) + ) + self.paste_entry_fields_action.setToolTip("Ctrl+V") + edit_menu.addAction(self.paste_entry_fields_action) + + edit_menu.addSeparator() + select_all_action = QAction("Select All", menu_bar) select_all_action.triggered.connect(self.select_all_action_callback) select_all_action.setShortcut( @@ -505,6 +540,8 @@ def start(self) -> None: help_menu.addAction(self.repo_action) self.set_macro_menu_viability() + self.update_clipboard_actions() + menu_bar.addMenu(file_menu) menu_bar.addMenu(edit_menu) menu_bar.addMenu(tools_menu) @@ -700,6 +737,10 @@ def close_library(self): self.cur_frame_idx = -1 self.cur_query = "" self.selected.clear() + self.copied_fields.clear() + self.is_buffer_merged = False + self.update_clipboard_actions() + self.set_macro_menu_viability() self.preview_panel.update_widgets() self.filter_items() self.main_window.toggle_landing_page(True) @@ -950,6 +991,114 @@ def run_macro(self, name: str, entry_id: int): mode="replace", ) + def copy_entry_fields_callback(self): + """Copies fields from selected Entries into to buffer.""" + merged_fields: list[dict] = [] + merged_count: int = 0 + for item_type, item_id in self.selected: + if item_type == ItemType.ENTRY: + entry = self.lib.get_entry(item_id) + + if len(entry.fields) > 0: + merged_count += 1 + + for field in entry.fields: + field_id: int = self.lib.get_field_attr(field, "id") + content = self.lib.get_field_attr(field, "content") + + if self.lib.get_field_obj(int(field_id))["type"] == "tag_box": + existing_fields: list[int] = self.lib.get_field_index_in_entry( + entry, field_id + ) + if existing_fields and merged_fields: + for i in content: + field_index = copy.deepcopy(existing_fields[0]) + if i not in merged_fields[field_index][field_id]: + merged_fields[field_index][field_id].append( + copy.deepcopy(i) + ) + else: + merged_fields.append(copy.deepcopy({field_id: content})) + + if self.lib.get_field_obj(int(field_id))["type"] in TEXT_FIELDS: + if {field_id: content} not in merged_fields: + merged_fields.append(copy.deepcopy({field_id: content})) + + # Only set merged state to True if multiple Entries with actual field data were copied. + if merged_count > 1: + self.is_buffer_merged = True + else: + self.is_buffer_merged = False + + self.copied_fields = merged_fields + self.update_clipboard_actions() + + def paste_entry_fields_callback(self): + """Pastes buffered fields into currently selected Entries.""" + # Code ported from ts_cli.py + if self.copied_fields: + for item_type, item_id in self.selected: + if item_type == ItemType.ENTRY: + entry = self.lib.get_entry(item_id) + + for field in self.copied_fields: + field_id: int = self.lib.get_field_attr(field, "id") + content = self.lib.get_field_attr(field, "content") + + if self.lib.get_field_obj(int(field_id))["type"] == "tag_box": + existing_fields: list[int] = ( + self.lib.get_field_index_in_entry(entry, field_id) + ) + if existing_fields: + self.lib.update_entry_field( + item_id, existing_fields[0], content, "append" + ) + else: + self.lib.add_field_to_entry(item_id, field_id) + self.lib.update_entry_field( + item_id, -1, content, "append" + ) + + if self.lib.get_field_obj(int(field_id))["type"] in TEXT_FIELDS: + if not self.lib.does_field_content_exist( + item_id, field_id, content + ): + self.lib.add_field_to_entry(item_id, field_id) + self.lib.update_entry_field( + item_id, -1, content, "replace" + ) + + self.preview_panel.update_widgets() + self.update_badges() + self.update_clipboard_actions() + + def update_clipboard_actions(self): + """Updates the text and enabled state of the field copy & paste actions.""" + # Buffer State Dependant + if self.copied_fields: + self.paste_entry_fields_action.setDisabled(False) + else: + self.paste_entry_fields_action.setDisabled(True) + self.paste_entry_fields_action.setText("&Paste Fields") + + # Selection Count Dependant + if len(self.selected) <= 0: + self.copy_entry_fields_action.setDisabled(True) + self.paste_entry_fields_action.setDisabled(True) + self.copy_entry_fields_action.setText("&Copy Fields") + if len(self.selected) == 1: + self.copy_entry_fields_action.setDisabled(False) + self.copy_entry_fields_action.setText("&Copy Fields") + elif len(self.selected) > 1: + self.copy_entry_fields_action.setDisabled(False) + self.copy_entry_fields_action.setText("&Copy Combined Fields") + + # Merged State Dependant + if self.is_buffer_merged: + self.paste_entry_fields_action.setText("&Paste Combined Fields") + else: + self.paste_entry_fields_action.setText("&Paste Fields") + def mouse_navigation(self, event: QMouseEvent): # print(event.button()) if event.button() == Qt.MouseButton.ForwardButton: @@ -1197,6 +1346,7 @@ def select_item(self, type: ItemType, id: int, append: bool, bridge: bool): self.preview_panel.set_tags_updated_slot(it.update_badges) self.set_macro_menu_viability() + self.update_clipboard_actions() self.preview_panel.update_widgets() def set_macro_menu_viability(self):