From eeca114876328d6406e786759d543720e8597c47 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 9 Aug 2019 10:08:02 +0530 Subject: [PATCH] Wire up the bookmarks panel fully --- src/calibre/gui2/viewer/bookmarks.py | 45 ++++++++++++++++++++++------ src/calibre/gui2/viewer/ui.py | 12 ++++++++ src/calibre/gui2/viewer/web_view.py | 23 ++++++++++++++ src/pyj/read_book/iframe.pyj | 15 ++++++++++ src/pyj/read_book/view.pyj | 8 +++++ src/pyj/viewer-main.pyj | 12 ++++++++ 6 files changed, 106 insertions(+), 9 deletions(-) diff --git a/src/calibre/gui2/viewer/bookmarks.py b/src/calibre/gui2/viewer/bookmarks.py index 1815a65b65..cfb4bcf4bd 100644 --- a/src/calibre/gui2/viewer/bookmarks.py +++ b/src/calibre/gui2/viewer/bookmarks.py @@ -7,14 +7,14 @@ from __future__ import absolute_import, division, print_function, unicode_litera import json from PyQt5.Qt import ( - QAction, QGridLayout, QIcon, QItemSelectionModel, QLabel, QListWidget, - QListWidgetItem, QPushButton, Qt, QWidget, pyqtSignal + QAction, QGridLayout, QIcon, QInputDialog, 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.date import EPOCH, utcnow from calibre.utils.icu import sort_key from polyglot.builtins import range, unicode_type @@ -122,7 +122,7 @@ class BookmarkManager(QWidget): def item_activated(self, item): bm = self.item_to_bm(item) - self.activated.emit(bm) + self.activated.emit(bm['pos']) def set_bookmarks(self, bookmarks=()): self.bookmarks_list.clear() @@ -146,12 +146,20 @@ class BookmarkManager(QWidget): for i in range(self.bookmarks_list.count()): yield self.item_to_bm(self.bookmarks_list.item(i)) + def uniqify_bookmark_title(self, base): + all_titles = {bm['title'] for bm in self.get_bookmarks()} + c = 0 + q = base + while q in all_titles: + c += 1 + q = '{} #{}'.format(base, c) + return q + 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) + title = unicode_type(item.data(Qt.DisplayRole)) or _('Unknown') + title = self.uniqify_bookmark_title(title) + item.setData(Qt.DisplayRole, title) bm = self.item_to_bm(item) bm['title'] = title item.setData(Qt.UserRole, self.bm_to_item(bm)) @@ -231,7 +239,7 @@ class BookmarkManager(QWidget): for bm in imported: if bm['title'] == 'calibre_current_page_bookmark': continue - epubcfi = 'epubcfi(/{}/{})'.format(bm['spine'], bm['pos'].lstrip('/')) + epubcfi = 'epubcfi(/{}/{})'.format((bm['spine'] + 1) * 2, bm['pos'].lstrip('/')) q = {'pos_type': 'epubcfi', 'pos': epubcfi, 'timestamp': EPOCH, 'title': bm['title']} if q not in bookmarks: bookmarks.append(q) @@ -254,3 +262,22 @@ class BookmarkManager(QWidget): import_old_bookmarks(imported) else: import_current_bookmarks(imported) + + def create_new_bookmark(self, pos_data): + title, ok = QInputDialog.getText(self, _('Add bookmark'), + _('Enter title for bookmark:')) + title = unicode_type(title).strip() + if not ok or not title: + return + title = self.uniqify_bookmark_title(title) + bm = { + 'title': title, + 'pos_type': 'epubcfi', + 'pos': pos_data['cfi'], + 'timestamp': utcnow() + } + bookmarks = self.get_bookmarks() + bookmarks.append(bm) + self.set_bookmarks(bookmarks) + self.set_current_bookmark(bm) + self.edited.emit(bookmarks) diff --git a/src/calibre/gui2/viewer/ui.py b/src/calibre/gui2/viewer/ui.py index e80e859bff..ea0d58b7e2 100644 --- a/src/calibre/gui2/viewer/ui.py +++ b/src/calibre/gui2/viewer/ui.py @@ -73,6 +73,11 @@ class EbookViewer(MainWindow): self.bookmarks_dock = create_dock(_('Bookmarks'), 'bookmarks-dock', Qt.RightDockWidgetArea) self.bookmarks_widget = w = BookmarkManager(self) + connect_lambda( + w.create_requested, self, + lambda self: self.web_view.get_current_cfi(self.bookmarks_widget.create_new_bookmark)) + self.bookmarks_widget.edited.connect(self.bookmarks_edited) + self.bookmarks_widget.activated.connect(self.bookmark_activated) self.bookmarks_dock.setWidget(w) self.inspector_dock = create_dock(_('Inspector'), 'inspector', Qt.RightDockWidgetArea) @@ -144,6 +149,12 @@ class EbookViewer(MainWindow): def toc_searched(self, index): item = self.toc_model.itemFromIndex(index) self.web_view.goto_toc_node(item.node_id) + + def bookmarks_edited(self, bookmarks): + self.current_book_data['annotations_map']['bookmark'] = bookmarks + + def bookmark_activated(self, cfi): + self.web_view.goto_cfi(cfi) # }}} # Load book {{{ @@ -194,6 +205,7 @@ class EbookViewer(MainWindow): toc = manifest.get('toc') self.toc_model = TOC(toc) self.toc.setModel(self.toc_model) + self.bookmarks_widget.set_bookmarks(self.current_book_data['annotations_map']['bookmark']) def load_book_annotations(self): amap = self.current_book_data['annotations_map'] diff --git a/src/calibre/gui2/viewer/web_view.py b/src/calibre/gui2/viewer/web_view.py index 1860f962e4..2999d5a5a4 100644 --- a/src/calibre/gui2/viewer/web_view.py +++ b/src/calibre/gui2/viewer/web_view.py @@ -6,6 +6,7 @@ from __future__ import absolute_import, division, print_function, unicode_litera import os import sys +from itertools import count from PyQt5.Qt import ( QApplication, QBuffer, QByteArray, QHBoxLayout, QSize, Qt, QTimer, QUrl, QWidget, @@ -177,12 +178,15 @@ class ViewerBridge(Bridge): toggle_bookmarks = from_js() update_current_toc_nodes = from_js(object, object) toggle_full_screen = from_js() + report_cfi = from_js(object, object) create_view = to_js() show_preparing_message = to_js() start_book_load = to_js() goto_toc_node = to_js() + goto_cfi = to_js() full_screen_state_changed = to_js() + get_current_cfi = to_js() class WebPage(QWebEnginePage): @@ -264,6 +268,8 @@ class WebView(RestartingWebEngineView): def __init__(self, parent=None): self._host_widget = None + self.callback_id_counter = count() + self.callback_map = {} self.current_cfi = None RestartingWebEngineView.__init__(self, parent) self.dead_renderer_error_shown = False @@ -278,6 +284,7 @@ class WebView(RestartingWebEngineView): self.bridge.toggle_bookmarks.connect(self.toggle_bookmarks) self.bridge.update_current_toc_nodes.connect(self.update_current_toc_nodes) self.bridge.toggle_full_screen.connect(self.toggle_full_screen) + self.bridge.report_cfi.connect(self.call_callback) self.pending_bridge_ready_actions = {} self.setPage(self._page) self.setAcceptDrops(False) @@ -354,6 +361,9 @@ class WebView(RestartingWebEngineView): def goto_toc_node(self, node_id): self.execute_when_ready('goto_toc_node', node_id) + def goto_cfi(self, cfi): + self.execute_when_ready('goto_cfi', cfi) + def notify_full_screen_state_change(self, in_fullscreen_mode): self.execute_when_ready('full_screen_state_changed', in_fullscreen_mode) @@ -364,3 +374,16 @@ class WebView(RestartingWebEngineView): sd = vprefs['session_data'] sd[key] = val vprefs['session_data'] = sd + + def do_callback(self, func_name, callback): + cid = next(self.callback_id_counter) + self.callback_map[cid] = callback + self.execute_when_ready('get_current_cfi', cid) + + def call_callback(self, request_id, data): + callback = self.callback_map.pop(request_id, None) + if callback is not None: + callback(data) + + def get_current_cfi(self, callback): + self.do_callback('get_current_cfi', callback) diff --git a/src/pyj/read_book/iframe.pyj b/src/pyj/read_book/iframe.pyj index cdd63cdd1f..725aeb3bd3 100644 --- a/src/pyj/read_book/iframe.pyj +++ b/src/pyj/read_book/iframe.pyj @@ -86,6 +86,7 @@ class IframeBoss: 'gesture_from_margin': self.gesture_from_margin, 'find': self.find, 'window_size': self.received_window_size, + 'get_current_cfi': self.get_current_cfi, } self.comm = IframeClient(handlers) self.last_window_ypos = 0 @@ -261,6 +262,20 @@ class IframeBoss: return 0 return self.calculate_progress_frac(current_name, index) + def get_current_cfi(self, data): + cfi = at_current() + if cfi: + spine = self.book.manifest.spine + current_name = current_spine_item().name + index = spine.indexOf(current_name) + if index > -1: + cfi = 'epubcfi(/{}{})'.format(2*(index+1), cfi) + self.send_message( + 'report_cfi', cfi=cfi, progress_frac=self.calculate_progress_frac(current_name, index), + file_progress_frac=progress_frac(), request_id=data.request_id) + return + self.send_message('report_cfi', cfi=None, progress_frac=0, file_progress_frac=0) + def update_cfi(self): cfi = at_current() if cfi: diff --git a/src/pyj/read_book/view.pyj b/src/pyj/read_book/view.pyj index 2223b0afa1..ab86389e4e 100644 --- a/src/pyj/read_book/view.pyj +++ b/src/pyj/read_book/view.pyj @@ -157,6 +157,7 @@ class View: 'goto_doc_boundary': def(data): self.goto_doc_boundary(data.start);, 'scroll_to_anchor': self.on_scroll_to_anchor, 'update_cfi': self.on_update_cfi, + 'report_cfi': self.on_report_cfi, 'update_toc_position': self.on_update_toc_position, 'content_loaded': self.on_content_loaded, 'show_chrome': self.show_chrome, @@ -503,6 +504,13 @@ class View: if toc_node: self.goto_named_destination(toc_node.dest, toc_node.frag) + def get_current_cfi(self, request_id): + self.iframe_wrapper.send_message('get_current_cfi', request_id=request_id) + + def on_report_cfi(self, data): + ui_operations.report_cfi( + data.request_id, {'cfi': data.cfi, 'progress_frac': data.progress_frac, 'file_progress_frac': data.file_progress_frac}) + def on_update_cfi(self, data): overlay_shown = not self.processing_spine_item_display and self.overlay.is_visible if overlay_shown or self.search_overlay.is_visible or self.content_popup_overlay.is_visible: diff --git a/src/pyj/viewer-main.pyj b/src/pyj/viewer-main.pyj index b321ed206e..9e6cec443e 100644 --- a/src/pyj/viewer-main.pyj +++ b/src/pyj/viewer-main.pyj @@ -199,11 +199,21 @@ def goto_toc_node(node_id): view.goto_toc_node(node_id) +@from_python +def goto_cfi(cfi): + view.goto_bookpos(cfi) + + @from_python def full_screen_state_changed(viewer_in_full_screen): runtime.viewer_in_full_screen = viewer_in_full_screen +@from_python +def get_current_cfi(request_id): + view.get_current_cfi(request_id) + + def onerror(msg, script_url, line_number, column_number, error_object): if not error_object: # cross domain error @@ -243,6 +253,8 @@ if window is window.top: to_python.update_current_toc_nodes(current_node_id, top_level_node_id) ui_operations.toggle_full_screen = def(): to_python.toggle_full_screen() + ui_operations.report_cfi = def(request_id, data): + to_python.report_cfi(request_id, data) document.body.appendChild(E.div(id='view')) window.onerror = onerror create_modal_container()