From a93e6b663ff714625c3ce203d9593fb076906e9d Mon Sep 17 00:00:00 2001 From: Charles Haley Date: Wed, 13 Apr 2022 21:23:56 +0100 Subject: [PATCH] Enhancement #1968810: provide an interface ot mark books with arbitrary text. --- resources/images/marked-text.png | Bin 0 -> 4915 bytes src/calibre/db/view.py | 5 +- src/calibre/gui2/actions/mark_books.py | 110 ++++++++++++++++++++++++- src/calibre/gui2/library/models.py | 8 +- src/calibre/gui2/library/views.py | 2 + 5 files changed, 120 insertions(+), 5 deletions(-) create mode 100644 resources/images/marked-text.png diff --git a/resources/images/marked-text.png b/resources/images/marked-text.png new file mode 100644 index 0000000000000000000000000000000000000000..cf25d4528d0f645ca3be51f85fd496183c7683a0 GIT binary patch literal 4915 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 0: + layout.addWidget(QLabel(_('Enter a value:')), button_rows+1, 0, 1, 2) + label = QLabel('&' + str(button_rows+1)) + else: + label = QLabel('') + label.setBuddy(textbox) + layout.addWidget(label, button_rows+2, 0, 1, 1) + layout.addWidget(textbox, button_rows+2, 1) + textbox.setFocus() + button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | + QDialogButtonBox.StandardButton.Cancel) + button_box.accepted.connect(self.accept) + button_box.rejected.connect(self.reject) + layout.addWidget(button_box, button_rows+3, 0, 1, 2) + + def text(self): + return self.text_box.text() + + def button_pushed(self, checked, text=''): + self.text_box.setText(text) + self.text_box.save_history() + self.accept() + + def accept(self): + if not self.text_box.text(): + d = error_dialog(self.gui, _('Value cannot be empty'), _('You must provide a value')) + d.exec_() + else: + super().accept() class MarkBooksAction(InterfaceAction): @@ -49,12 +118,18 @@ class MarkBooksAction(InterfaceAction): self.toggle_ids(book_ids) def genesis(self): + self.search_icon = QIcon.ic('search.png') self.qaction.triggered.connect(self.toggle_selected) self.menu = m = self.qaction.menu() m.aboutToShow.connect(self.about_to_show_menu) ma = partial(self.create_menu_action, m) + self.show_marked_action = a = ma('mark_with_text', _('Mark books with text label'), icon='marked-text.png') + a.triggered.connect(self.mark_with_text) self.show_marked_action = a = ma('show-marked', _('Show marked books'), icon='search.png', shortcut='Shift+Ctrl+M') a.triggered.connect(self.show_marked) + self.show_marked_with_text = QMenu(_('Show marked books with text label')) + self.show_marked_with_text.setIcon(self.search_icon) + m.addMenu(self.show_marked_with_text) self.clear_marked_action = a = ma('clear-all-marked', _('Clear all marked books'), icon='clear_left.png') a.triggered.connect(self.clear_all_marked) m.addSeparator() @@ -86,9 +161,22 @@ class MarkBooksAction(InterfaceAction): def about_to_show_menu(self): db = self.gui.current_db - num = len(frozenset(db.data.marked_ids).intersection(db.new_api.all_book_ids())) + marked_ids = db.data.marked_ids + num = len(frozenset(marked_ids).intersection(db.new_api.all_book_ids())) text = _('Show marked book') if num == 1 else (_('Show marked books') + (' (%d)' % num)) self.show_marked_action.setText(text) + counts = dict() + for v in marked_ids.values(): + counts[v] = counts.get(v, 0) + 1 + labels = sorted(counts.keys(), key=sort_key) + self.show_marked_with_text.clear() + if len(labels): + self.show_marked_with_text.setEnabled(True) + for t in labels: + ac = self.show_marked_with_text.addAction(self.search_icon, f'{t} ({counts[t]})') + ac.triggered.connect(partial(self.show_marked_text, txt=t)) + else: + self.show_marked_with_text.setEnabled(False) def location_selected(self, loc): enabled = loc == 'library' @@ -116,6 +204,9 @@ class MarkBooksAction(InterfaceAction): def show_marked(self): self.gui.search.set_search_string('marked:true') + def show_marked_text(self, txt=None): + self.gui.search.set_search_string(f'marked:"={txt}"') + def clear_all_marked(self): self.gui.current_db.data.set_marked_ids(()) if str(self.gui.search.text()).startswith('marked:'): @@ -139,3 +230,18 @@ class MarkBooksAction(InterfaceAction): else: mids.pop(book_id, None) db.data.set_marked_ids(mids) + + def mark_with_text(self): + book_ids = self._get_selected_ids() + if not book_ids: + return + dialog = MarkWithTextDialog(self.gui) + if dialog.exec_() != QDialog.DialogCode.Accepted: + return + txt = dialog.text() + txt = txt if txt else 'true' + db = self.gui.current_db + mids = db.data.marked_ids.copy() + for book_id in book_ids: + mids[book_id] = txt + db.data.set_marked_ids(mids) diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index 22e512ea47..325f11926c 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -232,6 +232,7 @@ class BooksModel(QAbstractTableModel): # {{{ # remember that the cover grid view needs a larger version of the icon, # anyway) self.marked_icon = QIcon(I('marked.png')) + self.marked_text_icon = QIcon(I('marked-text.png')) self.bool_blank_icon_as_icon = QIcon(self.bool_blank_icon) self.row_decoration = None self.device_connected = False @@ -1072,7 +1073,12 @@ class BooksModel(QAbstractTableModel): # {{{ return (section+1) if role == Qt.ItemDataRole.DecorationRole: try: - return self.marked_icon if self.db.data.get_marked(self.db.data.index_to_id(section)) else self.row_decoration + m = self.db.data.get_marked(self.db.data.index_to_id(section)) + if m: + i = self.marked_icon if m == 'true' else self.marked_text_icon + else: + i = self.row_decoration + return i except (ValueError, IndexError): pass return None diff --git a/src/calibre/gui2/library/views.py b/src/calibre/gui2/library/views.py index e2a67929cf..5253c61acd 100644 --- a/src/calibre/gui2/library/views.py +++ b/src/calibre/gui2/library/views.py @@ -946,6 +946,8 @@ class BooksView(QTableView): # {{{ # This is needed otherwise Qt does not always update the # viewport correctly. See https://bugs.launchpad.net/bugs/1404697 self.row_header.viewport().update() + # refresh the rows because there might be a composite that uses marked_books() + self.model().refresh_rows(changed) else: # Marked items have either appeared or all been removed self.model().set_row_decoration(current_marked)