%PDF- %PDF-
| Direktori : /lib/calibre/calibre/gui2/viewer/ |
| Current File : //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)