mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 10:44:09 -04:00
Start work on bookmarks panel
This commit is contained in:
parent
34f48e00d2
commit
d97db810c0
@ -193,16 +193,16 @@ def cfi_sort_key(cfi, only_path=True):
|
||||
except Exception:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return ()
|
||||
return (), (0, (0, 0), 0)
|
||||
if not pcfi:
|
||||
import sys
|
||||
print('Failed to parse CFI: %r' % pcfi, file=sys.stderr)
|
||||
return ()
|
||||
return (), (0, (0, 0), 0)
|
||||
steps = get_steps(pcfi)
|
||||
step_nums = tuple(s.get('num', 0) for s in steps)
|
||||
step = steps[-1] if steps else {}
|
||||
offsets = (step.get('temporal_offset', 0), tuple(reversed(step.get('spatial_offset', (0, 0)))), step.get('text_offset', 0), )
|
||||
return (step_nums, offsets)
|
||||
return step_nums, offsets
|
||||
|
||||
|
||||
def decode_cfi(root, cfi):
|
||||
|
@ -48,13 +48,17 @@ def merge_annotations(annots, annots_map):
|
||||
annots_map[annot_type] = list(merge_annots_with_identical_titles(a))
|
||||
|
||||
|
||||
def serialize_annotation(annot):
|
||||
annot['timestamp'] = annot['timestamp'].isoformat()
|
||||
return annot
|
||||
|
||||
|
||||
def serialize_annotations(annots_map):
|
||||
ans = []
|
||||
for atype, annots in iteritems(annots_map):
|
||||
for annot in annots:
|
||||
annot = annot.copy()
|
||||
annot = serialize_annotation(annot.copy())
|
||||
annot['type'] = atype
|
||||
annot['timestamp'] = annot['timestamp'].isoformat()
|
||||
ans.append(annot)
|
||||
return json_dumps(ans)
|
||||
|
||||
|
256
src/calibre/gui2/viewer/bookmarks.py
Normal file
256
src/calibre/gui2/viewer/bookmarks.py
Normal file
@ -0,0 +1,256 @@
|
||||
#!/usr/bin/env python2
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPL v3 Copyright: 2013, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
import json
|
||||
|
||||
from PyQt5.Qt import (
|
||||
QAction, QGridLayout, QIcon, QItemSelectionModel, QLabel, QListWidget,
|
||||
QListWidgetItem, QPushButton, Qt, QWidget, pyqtSignal
|
||||
)
|
||||
|
||||
from calibre.gui2 import choose_files, choose_save_file
|
||||
from calibre.gui2.viewer.annotations import serialize_annotation
|
||||
from calibre.srv.render_book import parse_annotation
|
||||
from calibre.utils.date import EPOCH
|
||||
from calibre.utils.icu import sort_key
|
||||
from polyglot.builtins import range, unicode_type
|
||||
|
||||
|
||||
class BookmarksList(QListWidget):
|
||||
|
||||
changed = pyqtSignal()
|
||||
bookmark_activated = pyqtSignal(object)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QListWidget.__init__(self, parent)
|
||||
self.setDragEnabled(True)
|
||||
self.setDragDropMode(self.InternalMove)
|
||||
self.setDefaultDropAction(Qt.MoveAction)
|
||||
self.setAlternatingRowColors(True)
|
||||
self.setStyleSheet('QListView::item { padding: 0.5ex }')
|
||||
self.viewport().setAcceptDrops(True)
|
||||
self.setDropIndicatorShown(True)
|
||||
self.setContextMenuPolicy(Qt.ActionsContextMenu)
|
||||
self.ac_edit = ac = QAction(QIcon(I('edit_input.png')), _('Edit this bookmark'), self)
|
||||
self.addAction(ac)
|
||||
self.ac_delete = ac = QAction(QIcon(I('trash.png')), _('Remove this bookmark'), self)
|
||||
self.addAction(ac)
|
||||
self.ac_sort = ac = QAction(_('Sort by name'), self)
|
||||
self.addAction(ac)
|
||||
self.ac_sort_pos = ac = QAction(_('Sort by position in book'), self)
|
||||
self.addAction(ac)
|
||||
|
||||
def dropEvent(self, ev):
|
||||
QListWidget.dropEvent(self, ev)
|
||||
if ev.isAccepted():
|
||||
self.changed.emit()
|
||||
|
||||
def keyPressEvent(self, ev):
|
||||
if ev.key() in (Qt.Key_Enter, Qt.Key_Return):
|
||||
i = self.currentItem()
|
||||
if i is not None:
|
||||
self.bookmark_activated.emit(i)
|
||||
ev.accept()
|
||||
return
|
||||
if ev.key() in (Qt.Key_Delete, Qt.Key_Backspace):
|
||||
i = self.currentItem()
|
||||
if i is not None:
|
||||
self.ac_delete.trigger()
|
||||
ev.accept()
|
||||
return
|
||||
return QListWidget.keyPressEvent(self, ev)
|
||||
|
||||
|
||||
class BookmarkManager(QWidget):
|
||||
|
||||
edited = pyqtSignal(object)
|
||||
activated = pyqtSignal(object)
|
||||
create_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.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_sort.triggered.connect(self.sort_by_name)
|
||||
bl.ac_sort_pos.triggered.connect(self.sort_by_pos)
|
||||
bl.ac_delete.triggered.connect(self.delete_bookmark)
|
||||
|
||||
self.la = la = QLabel(_(
|
||||
'Double click to edit and drag-and-drop to re-order 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_delete = b = QPushButton(_('Sort by &name'), self)
|
||||
b.setToolTip(_('Sort bookmarks by name'))
|
||||
b.clicked.connect(self.sort_by_name)
|
||||
l.addWidget(b)
|
||||
|
||||
self.button_delete = b = QPushButton(_('Sort by &position'), self)
|
||||
b.setToolTip(_('Sort bookmarks by position in book'))
|
||||
b.clicked.connect(self.sort_by_pos)
|
||||
l.addWidget(b, l.rowCount() - 1, 1)
|
||||
|
||||
self.button_export = b = QPushButton(QIcon(I('back.png')), _('E&xport'), self)
|
||||
b.clicked.connect(self.export_bookmarks)
|
||||
l.addWidget(b)
|
||||
|
||||
self.button_import = b = QPushButton(QIcon(I('forward.png')), _('&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)
|
||||
|
||||
def set_bookmarks(self, bookmarks=()):
|
||||
self.bookmarks_list.clear()
|
||||
for bm in bookmarks:
|
||||
i = QListWidgetItem(bm['title'])
|
||||
i.setData(Qt.UserRole, self.bm_to_item(bm))
|
||||
i.setFlags(i.flags() | Qt.ItemIsEditable)
|
||||
self.bookmarks_list.addItem(i)
|
||||
if self.bookmarks_list.count() > 0:
|
||||
self.bookmarks_list.setCurrentItem(self.bookmarks_list.item(0), QItemSelectionModel.ClearAndSelect)
|
||||
|
||||
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.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 item_changed(self, item):
|
||||
self.bookmarks_list.blockSignals(True)
|
||||
title = unicode_type(item.data(Qt.DisplayRole))
|
||||
if not title:
|
||||
title = _('Unknown')
|
||||
item.setData(Qt.DisplayRole, title)
|
||||
bm = self.item_to_bm(item)
|
||||
bm['title'] = title
|
||||
item.setData(Qt.UserRole, self.bm_to_item(bm))
|
||||
self.bookmarks_list.blockSignals(False)
|
||||
self.edited.emit(self.get_bookmarks())
|
||||
|
||||
def delete_bookmark(self):
|
||||
row = self.bookmarks_list.currentRow()
|
||||
if row > -1:
|
||||
self.bookmarks_list.takeItem(row)
|
||||
self.edited.emit(self.get_bookmarks())
|
||||
|
||||
def edit_bookmark(self):
|
||||
item = self.bookmarks_list.currentItem()
|
||||
if item is not None:
|
||||
self.bookmarks_list.editItem(item)
|
||||
|
||||
def sort_by_name(self):
|
||||
bm = self.get_bookmarks()
|
||||
bm.sort(key=lambda x:sort_key(x['title']))
|
||||
self.set_bookmarks(bm)
|
||||
self.edited.emit(bm)
|
||||
|
||||
def sort_by_pos(self):
|
||||
from calibre.ebooks.epub.cfi.parse import cfi_sort_key
|
||||
|
||||
def pos_key(b):
|
||||
if b.get('pos_type') == 'epubcfi':
|
||||
return cfi_sort_key(b['pos'], only_path=False)
|
||||
return cfi_sort_key('/99999999')
|
||||
bm = self.get_bookmarks()
|
||||
bm.sort(key=pos_key)
|
||||
self.set_bookmarks(bm)
|
||||
self.edited.emit(bm)
|
||||
|
||||
def bm_to_item(self, bm):
|
||||
return bm.copy()
|
||||
|
||||
def item_to_bm(self, item):
|
||||
return item.data(Qt.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:
|
||||
entries = [serialize_annotation(a) for a in self.get_bookmarks()]
|
||||
data = json.dumps({'type': 'bookmarks', 'entries': entries}, 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'], bm['pos'].lstrip('/'))
|
||||
q = {'pos_type': 'epubcfi', 'pos': epubcfi, 'timestamp': EPOCH, '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']:
|
||||
q = parse_annotation(bm)
|
||||
if q not in bookmarks:
|
||||
bookmarks.append(q)
|
||||
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)
|
Loading…
x
Reference in New Issue
Block a user