From a2b61da9c79e0f02eaa01603127c174aaa14d553 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 14 May 2020 18:17:29 +0530 Subject: [PATCH] Tag browser: Allow adding/removing tags/authors/etc. to the currently selected book by right clicking on that tag and choosing "Apply to selected books". Fixes #1878308 [[Enhancement] Drag to remove tags](https://bugs.launchpad.net/calibre/+bug/1878308) --- src/calibre/gui2/tag_browser/ui.py | 45 ++++++++++++++++++++++++- src/calibre/gui2/tag_browser/view.py | 49 +++++++++++++++++++++++++--- 2 files changed, 88 insertions(+), 6 deletions(-) diff --git a/src/calibre/gui2/tag_browser/ui.py b/src/calibre/gui2/tag_browser/ui.py index d43ad84e47..20457a1305 100644 --- a/src/calibre/gui2/tag_browser/ui.py +++ b/src/calibre/gui2/tag_browser/ui.py @@ -22,7 +22,7 @@ from calibre.ebooks.metadata import title_sort from calibre.gui2.dialogs.tag_categories import TagCategories from calibre.gui2.dialogs.tag_list_editor import TagListEditor from calibre.gui2.dialogs.edit_authors_dialog import EditAuthorsDialog -from polyglot.builtins import unicode_type +from polyglot.builtins import unicode_type, iteritems class TagBrowserMixin(object): # {{{ @@ -87,6 +87,7 @@ class TagBrowserMixin(object): # {{{ self.tags_view.restriction_error.connect(self.do_restriction_error, type=Qt.QueuedConnection) self.tags_view.tag_item_delete.connect(self.do_tag_item_delete) + self.tags_view.apply_tag_to_selected.connect(self.apply_tag_to_selected) self.populate_tb_manage_menu(db) self.tags_view.model().user_categories_edited.connect(self.user_categories_edited, type=Qt.QueuedConnection) @@ -299,6 +300,48 @@ class TagBrowserMixin(object): # {{{ self.do_tag_item_renamed() self.tags_view.recount() + def apply_tag_to_selected(self, field_name, item_name, remove): + db = self.current_db.new_api + fm = db.field_metadata.get(field_name) + if fm is None: + return + book_ids = self.library_view.get_selected_ids() + if not book_ids: + return error_dialog(self.library_view, _('No books selected'), _( + 'You must select some books to apply {} to').format(item_name), show=True) + existing_values = db.all_field_for(field_name, book_ids) + series_index_field = None + if fm['datatype'] == 'series': + series_index_field = field_name + '_index' + changes = {} + for book_id, existing in iteritems(existing_values): + if isinstance(existing, tuple): + existing = list(existing) + if remove: + try: + existing.remove(item_name) + except ValueError: + continue + changes[book_id] = existing + else: + if item_name not in existing: + changes[book_id] = existing + [item_name] + else: + if remove: + if existing == item_name: + changes[book_id] = None + else: + if existing != item_name: + changes[book_id] = item_name + if changes: + db.set_field(field_name, changes) + if series_index_field is not None: + for book_id in changes: + si = db.get_next_series_num_for(item_name, field=field_name) + db.set_field(series_index_field, {book_id: si}) + self.library_view.model().refresh_ids(set(changes), current_row=self.library_view.currentIndex().row()) + self.tags_view.recount_with_position_based_index() + def do_tag_item_renamed(self): # Clean up library view and search # get information to redo the selection diff --git a/src/calibre/gui2/tag_browser/view.py b/src/calibre/gui2/tag_browser/view.py index b156d01230..66eccb2db1 100644 --- a/src/calibre/gui2/tag_browser/view.py +++ b/src/calibre/gui2/tag_browser/view.py @@ -154,6 +154,7 @@ class TagsView(QTreeView): # {{{ drag_drop_finished = pyqtSignal(object) restriction_error = pyqtSignal() tag_item_delete = pyqtSignal(object, object, object, object) + apply_tag_to_selected = pyqtSignal(object, object, object) def __init__(self, parent=None): QTreeView.__init__(self, parent=None) @@ -177,6 +178,7 @@ class TagsView(QTreeView): # {{{ self.search_icon = QIcon(I('search.png')) self.search_copy_icon = QIcon(I("search_copy_saved.png")) self.user_category_icon = QIcon(I('tb_folder.png')) + self.edit_metadata_icon = QIcon(I('edit_input.png')) self.delete_icon = QIcon(I('list_remove.png')) self.rename_icon = QIcon(I('edit-undo.png')) @@ -510,13 +512,33 @@ class TagsView(QTreeView): # {{{ gprefs['tags_browser_partition_method'] = category elif action == 'defaults': self.hidden_categories.clear() + elif action == 'add_tag': + item = self.model().get_node(index) + if item is not None: + self.apply_to_selected_books(item) + return + elif action == 'remove_tag': + item = self.model().get_node(index) + if item is not None: + self.apply_to_selected_books(item, True) + return self.db.new_api.set_pref('tag_browser_hidden_categories', list(self.hidden_categories)) if reset_filter_categories: self._model.set_categories_filter(None) self._model.rebuild_node_tree() - except: + except Exception: + import traceback + traceback.print_exc() return + def apply_to_selected_books(self, item, remove=False): + if item.type != item.TAG: + return + tag = item.tag + if not tag.category or not tag.original_name: + return + self.apply_tag_to_selected.emit(tag.category, tag.original_name, remove) + def show_context_menu(self, point): def display_name(tag): ans = tag.name @@ -551,6 +573,7 @@ class TagsView(QTreeView): # {{{ # Verify that we are working with a field that we know something about if key not in self.db.field_metadata: return True + fm = self.db.field_metadata[key] # Did the user click on a leaf node? if tag: @@ -589,9 +612,9 @@ class TagsView(QTreeView): # {{{ # is_editable is also overloaded to mean 'can be added # to a User category' - m = self.context_menu.addMenu(self.user_category_icon, - _('Add %s to User category')%display_name(tag)) - nt = self.model().user_category_node_tree + m = QMenu(_('Add %s to User category')%display_name(tag), self.context_menu) + m.setIcon(self.user_category_icon) + added = [False] def add_node_tree(tree_dict, m, path): p = path[:] @@ -602,12 +625,28 @@ class TagsView(QTreeView): # {{{ partial(self.context_menu_handler, 'add_to_category', category='.'.join(p), index=tag_item)) + added[0] = True if len(tree_dict[k]): tm = m.addMenu(self.user_category_icon, _('Children of %s')%n) add_node_tree(tree_dict[k], tm, p) p.pop() - add_node_tree(nt, m, []) + add_node_tree(self.model().user_category_node_tree, m, []) + if added[0]: + self.context_menu.addMenu(m) + + # is_editable also means the tag can be applied/removed + # from selected books + if fm['datatype'] != 'rating': + m = self.context_menu.addMenu(self.edit_metadata_icon, + _('Apply %s to selected books')%display_name(tag)) + m.addAction(QIcon(I('plus.png')), + _('Add %s to selected books') % display_name(tag), + partial(self.context_menu_handler, action='add_tag', index=index)) + m.addAction(QIcon(I('minus.png')), + _('Remove %s from selected books') % display_name(tag), + partial(self.context_menu_handler, action='remove_tag', index=index)) + elif key == 'search' and tag.is_searchable: self.context_menu.addAction(self.rename_icon, _('Rename %s')%display_name(tag),