%PDF- %PDF-
Direktori : /usr/lib/calibre/calibre/gui2/viewer/ |
Current File : //usr/lib/calibre/calibre/gui2/viewer/bookmarks.py |
#!/usr/bin/env python3 # License: GPL v3 Copyright: 2013, Kovid Goyal <kovid at kovidgoyal.net> import json from operator import itemgetter from qt.core import ( QAction, QComboBox, QGridLayout, QHBoxLayout, QIcon, QInputDialog, QItemSelectionModel, QLabel, QListWidget, QListWidgetItem, QPushButton, Qt, QWidget, pyqtSignal ) from calibre.gui2 import choose_files, choose_save_file from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.viewer.shortcuts import get_shortcut_for from calibre.gui2.viewer.web_view import vprefs from calibre.utils.date import EPOCH, utcnow from calibre.utils.icu import primary_sort_key class BookmarksList(QListWidget): changed = pyqtSignal() bookmark_activated = pyqtSignal(object) def __init__(self, parent=None): QListWidget.__init__(self, parent) self.setAlternatingRowColors(True) self.setStyleSheet('QListView::item { padding: 0.5ex }') self.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu) self.ac_edit = ac = QAction(QIcon(I('edit_input.png')), _('Rename this bookmark'), self) self.addAction(ac) self.ac_delete = ac = QAction(QIcon(I('trash.png')), _('Remove this bookmark'), self) self.addAction(ac) @property def current_non_removed_item(self): ans = self.currentItem() if ans is not None: bm = ans.data(Qt.ItemDataRole.UserRole) if not bm.get('removed'): return ans def keyPressEvent(self, ev): if ev.key() in (Qt.Key.Key_Enter, Qt.Key.Key_Return): i = self.current_non_removed_item if i is not None: self.bookmark_activated.emit(i) ev.accept() return if ev.key() in (Qt.Key.Key_Delete, Qt.Key.Key_Backspace): i = self.current_non_removed_item if i is not None: self.ac_delete.trigger() ev.accept() return return QListWidget.keyPressEvent(self, ev) def activate_related_bookmark(self, delta=1): if not self.count(): return items = [self.item(r) for r in range(self.count())] row = self.currentRow() current_item = items[row] items = [i for i in items if not i.isHidden()] count = len(items) if not count: return row = items.index(current_item) nrow = (row + delta + count) % count self.setCurrentItem(items[nrow]) self.bookmark_activated.emit(self.currentItem()) def next_bookmark(self): self.activate_related_bookmark() def previous_bookmark(self): self.activate_related_bookmark(-1) class BookmarkManager(QWidget): edited = pyqtSignal(object) activated = pyqtSignal(object) create_requested = pyqtSignal() toggle_requested = pyqtSignal() def __init__(self, parent): QWidget.__init__(self, parent) self.l = l = QGridLayout(self) l.setContentsMargins(0, 0, 0, 0) self.setLayout(l) self.toc = parent.toc self.bookmarks_list = bl = BookmarksList(self) bl.itemChanged.connect(self.item_changed) l.addWidget(bl, 0, 0, 1, -1) bl.itemClicked.connect(self.item_activated) bl.bookmark_activated.connect(self.item_activated) bl.changed.connect(lambda : self.edited.emit(self.get_bookmarks())) bl.ac_edit.triggered.connect(self.edit_bookmark) bl.ac_delete.triggered.connect(self.delete_bookmark) self.la = la = QLabel(_( 'Double click to edit the bookmarks')) la.setWordWrap(True) l.addWidget(la, l.rowCount(), 0, 1, -1) self.button_new = b = QPushButton(QIcon(I('bookmarks.png')), _('&New'), self) b.clicked.connect(self.create_requested) b.setToolTip(_('Create a new bookmark at the current location')) l.addWidget(b) self.button_delete = b = QPushButton(QIcon(I('trash.png')), _('&Remove'), self) b.setToolTip(_('Remove the currently selected bookmark')) b.clicked.connect(self.delete_bookmark) l.addWidget(b, l.rowCount() - 1, 1) self.button_prev = b = QPushButton(QIcon(I('back.png')), _('Pre&vious'), self) b.clicked.connect(self.bookmarks_list.previous_bookmark) l.addWidget(b) self.button_next = b = QPushButton(QIcon(I('forward.png')), _('Nex&t'), self) b.clicked.connect(self.bookmarks_list.next_bookmark) l.addWidget(b, l.rowCount() - 1, 1) la = QLabel(_('&Sort by:')) self.sort_by = sb = QComboBox(self) la.setBuddy(sb) sb.addItem(_('Title'), 'title') sb.addItem(_('Position in book'), 'pos') sb.addItem(_('Date'), 'timestamp') sb.setToolTip(_('Change how the bookmarks are sorted')) i = sb.findData(vprefs['bookmarks_sort']) if i > -1: sb.setCurrentIndex(i) h = QHBoxLayout() h.addWidget(la), h.addWidget(sb, 10) l.addLayout(h, l.rowCount(), 0, 1, 2) sb.currentIndexChanged.connect(self.sort_by_changed) self.button_export = b = QPushButton(_('E&xport'), self) b.clicked.connect(self.export_bookmarks) l.addWidget(b, l.rowCount(), 0) self.button_import = b = QPushButton(_('&Import'), self) b.clicked.connect(self.import_bookmarks) l.addWidget(b, l.rowCount() - 1, 1) def item_activated(self, item): bm = self.item_to_bm(item) self.activated.emit(bm['pos']) @property def current_sort_by(self): return self.sort_by.currentData() def sort_by_changed(self): vprefs['bookmarks_sort'] = self.current_sort_by self.set_bookmarks(self.get_bookmarks()) def set_bookmarks(self, bookmarks=()): csb = self.current_sort_by if csb in ('name', 'title'): sk = lambda x: primary_sort_key(x['title']) elif csb == 'timestamp': sk = itemgetter('timestamp') else: from calibre.ebooks.epub.cfi.parse import cfi_sort_key defval = cfi_sort_key('/99999999') def pos_key(b): if b.get('pos_type') == 'epubcfi': return cfi_sort_key(b['pos'], only_path=False) return defval sk = pos_key bookmarks = sorted(bookmarks, key=sk) current_bookmark_id = self.current_bookmark_id self.bookmarks_list.clear() for bm in bookmarks: i = QListWidgetItem(bm['title']) i.setData(Qt.ItemDataRole.ToolTipRole, bm['title']) i.setData(Qt.ItemDataRole.UserRole, self.bm_to_item(bm)) i.setFlags(i.flags() | Qt.ItemFlag.ItemIsEditable) self.bookmarks_list.addItem(i) if bm.get('removed'): i.setHidden(True) for i in range(self.bookmarks_list.count()): item = self.bookmarks_list.item(i) if not item.isHidden(): self.bookmarks_list.setCurrentItem(item, QItemSelectionModel.SelectionFlag.ClearAndSelect) break if current_bookmark_id is not None: self.current_bookmark_id = current_bookmark_id @property def current_bookmark_id(self): item = self.bookmarks_list.currentItem() if item is not None: return item.data(Qt.ItemDataRole.DisplayRole) @current_bookmark_id.setter def current_bookmark_id(self, val): for i, q in enumerate(self): if q['title'] == val: item = self.bookmarks_list.item(i) self.bookmarks_list.setCurrentItem(item, QItemSelectionModel.SelectionFlag.ClearAndSelect) self.bookmarks_list.scrollToItem(item) def set_current_bookmark(self, bm): for i, q in enumerate(self): if bm == q: l = self.bookmarks_list item = l.item(i) l.setCurrentItem(item, QItemSelectionModel.SelectionFlag.ClearAndSelect) l.scrollToItem(item) def __iter__(self): for i in range(self.bookmarks_list.count()): yield self.item_to_bm(self.bookmarks_list.item(i)) def uniqify_bookmark_title(self, base): remove = [] for i in range(self.bookmarks_list.count()): item = self.bookmarks_list.item(i) bm = item.data(Qt.ItemDataRole.UserRole) if bm.get('removed') and bm['title'] == base: remove.append(i) for i in reversed(remove): self.bookmarks_list.takeItem(i) all_titles = {bm['title'] for bm in self.get_bookmarks()} c = 0 q = base while q in all_titles: c += 1 q = f'{base} #{c}' return q def item_changed(self, item): self.bookmarks_list.blockSignals(True) title = str(item.data(Qt.ItemDataRole.DisplayRole)) or _('Unknown') title = self.uniqify_bookmark_title(title) item.setData(Qt.ItemDataRole.DisplayRole, title) item.setData(Qt.ItemDataRole.ToolTipRole, title) bm = item.data(Qt.ItemDataRole.UserRole) bm['title'] = title bm['timestamp'] = utcnow().isoformat() item.setData(Qt.ItemDataRole.UserRole, bm) self.bookmarks_list.blockSignals(False) self.edited.emit(self.get_bookmarks()) def delete_bookmark(self): item = self.bookmarks_list.current_non_removed_item if item is not None: bm = item.data(Qt.ItemDataRole.UserRole) if confirm( _('Are you sure you want to delete the bookmark: {0}?').format(bm['title']), 'delete-bookmark-from-viewer', parent=self, config_set=vprefs ): bm['removed'] = True bm['timestamp'] = utcnow().isoformat() self.bookmarks_list.blockSignals(True) item.setData(Qt.ItemDataRole.UserRole, bm) self.bookmarks_list.blockSignals(False) item.setHidden(True) self.edited.emit(self.get_bookmarks()) def edit_bookmark(self): item = self.bookmarks_list.current_non_removed_item if item is not None: self.bookmarks_list.editItem(item) def bm_to_item(self, bm): return bm.copy() def item_to_bm(self, item): return item.data(Qt.ItemDataRole.UserRole).copy() def get_bookmarks(self): return list(self) def export_bookmarks(self): filename = choose_save_file( self, 'export-viewer-bookmarks', _('Export bookmarks'), filters=[(_('Saved bookmarks'), ['calibre-bookmarks'])], all_files=False, initial_filename='bookmarks.calibre-bookmarks') if filename: bm = [x for x in self.get_bookmarks() if not x.get('removed')] data = json.dumps({'type': 'bookmarks', 'entries': bm}, indent=True) if not isinstance(data, bytes): data = data.encode('utf-8') with lopen(filename, 'wb') as fileobj: fileobj.write(data) def import_bookmarks(self): files = choose_files(self, 'export-viewer-bookmarks', _('Import bookmarks'), filters=[(_('Saved bookmarks'), ['calibre-bookmarks'])], all_files=False, select_only_single_file=True) if not files: return filename = files[0] imported = None with lopen(filename, 'rb') as fileobj: imported = json.load(fileobj) def import_old_bookmarks(imported): try: for bm in imported: if 'title' not in bm: return except Exception: return bookmarks = self.get_bookmarks() for bm in imported: if bm['title'] == 'calibre_current_page_bookmark': continue epubcfi = 'epubcfi(/{}/{})'.format((bm['spine'] + 1) * 2, bm['pos'].lstrip('/')) q = {'pos_type': 'epubcfi', 'pos': epubcfi, 'timestamp': EPOCH.isoformat(), 'title': bm['title']} if q not in bookmarks: bookmarks.append(q) self.set_bookmarks(bookmarks) self.edited.emit(self.get_bookmarks()) def import_current_bookmarks(imported): if imported.get('type') != 'bookmarks': return bookmarks = self.get_bookmarks() for bm in imported['entries']: if bm not in bookmarks: bookmarks.append(bm) self.set_bookmarks(bookmarks) self.edited.emit(self.get_bookmarks()) if imported is not None: if isinstance(imported, list): import_old_bookmarks(imported) else: import_current_bookmarks(imported) def create_new_bookmark(self, pos_data): base_default_title = self.toc.model().title_for_current_node or _('Bookmark') all_titles = {bm['title'] for bm in self.get_bookmarks()} c = 0 while True: c += 1 default_title = f'{base_default_title} #{c}' if default_title not in all_titles: break title, ok = QInputDialog.getText(self, _('Add bookmark'), _('Enter title for bookmark:'), text=pos_data.get('selected_text') or default_title) title = str(title).strip() if not ok or not title: return title = self.uniqify_bookmark_title(title) cfi = (pos_data.get('selection_bounds') or {}).get('start') or pos_data['cfi'] bm = { 'title': title, 'pos_type': 'epubcfi', 'pos': cfi, 'timestamp': utcnow().isoformat(), } bookmarks = self.get_bookmarks() bookmarks.append(bm) self.set_bookmarks(bookmarks) self.set_current_bookmark(bm) self.edited.emit(bookmarks) def keyPressEvent(self, ev): sc = get_shortcut_for(self, ev) if ev.key() == Qt.Key.Key_Escape or sc == 'toggle_bookmarks': self.toggle_requested.emit() return if sc == 'new_bookmark': self.create_requested.emit() return return QWidget.keyPressEvent(self, ev)