From 0ccedfdcc74525e71d2eeb0577b55e56e6b41c89 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 6 Oct 2019 22:36:57 +0530 Subject: [PATCH] Viewer: Add an option to display a scrollbar --- src/calibre/gui2/viewer/ui.py | 63 +++++++++++++++++++++++++-- src/calibre/gui2/viewer/web_view.py | 15 ++++++- src/calibre/srv/render_book.py | 2 +- src/pyj/read_book/iframe.pyj | 35 +++++++++------ src/pyj/read_book/prefs/scrolling.pyj | 7 +++ src/pyj/read_book/view.pyj | 52 +++++++++++++++++++--- src/pyj/session.pyj | 2 + src/pyj/viewer-main.pyj | 10 +++++ 8 files changed, 162 insertions(+), 24 deletions(-) diff --git a/src/calibre/gui2/viewer/ui.py b/src/calibre/gui2/viewer/ui.py index dd279d10c0..912771ecbc 100644 --- a/src/calibre/gui2/viewer/ui.py +++ b/src/calibre/gui2/viewer/ui.py @@ -12,8 +12,8 @@ from hashlib import sha256 from threading import Thread from PyQt5.Qt import ( - QApplication, QDockWidget, QEvent, QMimeData, QModelIndex, QPixmap, Qt, QUrl, - QVBoxLayout, QWidget, pyqtSignal + QApplication, QDockWidget, QEvent, QHBoxLayout, QMimeData, QModelIndex, QPixmap, + QScrollBar, Qt, QUrl, QVBoxLayout, QWidget, pyqtSignal ) from calibre import prints @@ -59,6 +59,61 @@ def path_key(path): return sha256(as_bytes(path)).hexdigest() +class ScrollBar(QScrollBar): + + def paintEvent(self, ev): + if self.isEnabled(): + return QScrollBar.paintEvent(self, ev) + + +class CentralWidget(QWidget): + + def __init__(self, web_view, parent): + QWidget.__init__(self, parent) + self.web_view = web_view + self.l = l = QHBoxLayout(self) + l.setContentsMargins(0, 0, 0, 0), l.setSpacing(0) + l.addWidget(web_view) + self.vertical_scrollbar = vs = ScrollBar(Qt.Vertical, self) + vs.sliderMoved[int].connect(self.slider_moved) + l.addWidget(vs) + self.current_book_length = None + web_view.notify_progress_frac.connect(self.update_scrollbar_positions) + web_view.scrollbar_visibility_changed.connect(self.apply_scrollbar_visibility) + web_view.overlay_visibility_changed.connect(self.overlay_visibility_changed) + self.apply_scrollbar_visibility() + + def apply_scrollbar_visibility(self): + visible = get_session_pref('standalone_scrollbar', default=False, group=None) + self.vertical_scrollbar.setVisible(bool(visible)) + + def overlay_visibility_changed(self, visible): + self.vertical_scrollbar.setEnabled(not visible) + + def set_scrollbar_value(self, frac): + val = int(self.vertical_scrollbar.maximum() * frac) + self.vertical_scrollbar.setValue(val) + + def slider_moved(self, val): + frac = val / self.vertical_scrollbar.maximum() + self.web_view.goto_frac(frac) + + def initialize_scrollbars(self, book_length): + self.current_book_length = book_length + maximum = book_length / 10 + bar = self.vertical_scrollbar + bar.setMinimum(0) + bar.setMaximum(maximum) + bar.setSingleStep(10) + bar.setPageStep(100) + + def update_scrollbar_positions(self, progress_frac, file_progress_frac, book_length): + if book_length != self.current_book_length: + self.initialize_scrollbars(book_length) + if not self.vertical_scrollbar.isSliderDown(): + self.set_scrollbar_value(progress_frac) + + class EbookViewer(MainWindow): msg_from_anotherinstance = pyqtSignal(object) @@ -127,7 +182,8 @@ class EbookViewer(MainWindow): self.web_view.selection_changed.connect(self.lookup_widget.selected_text_changed, type=Qt.QueuedConnection) self.web_view.view_image.connect(self.view_image, type=Qt.QueuedConnection) self.web_view.copy_image.connect(self.copy_image, type=Qt.QueuedConnection) - self.setCentralWidget(self.web_view) + self.central_widget = CentralWidget(self.web_view, self) + self.setCentralWidget(self.central_widget) self.restore_state() if continue_reading: self.continue_reading() @@ -288,6 +344,7 @@ class EbookViewer(MainWindow): self.web_view.show_home_page() return set_book_path(data['base'], data['pathtoebook']) + self.central_widget.initialize_scrollbars(set_book_path.parsed_manifest['spine_length']) self.current_book_data = data self.current_book_data['annotations_map'] = defaultdict(list) self.current_book_data['annotations_path_key'] = path_key(data['pathtoebook']) + '.json' diff --git a/src/calibre/gui2/viewer/web_view.py b/src/calibre/gui2/viewer/web_view.py index 630ff4949f..ba718e3b3a 100644 --- a/src/calibre/gui2/viewer/web_view.py +++ b/src/calibre/gui2/viewer/web_view.py @@ -186,7 +186,7 @@ class UrlSchemeHandler(QWebEngineUrlSchemeHandler): def get_session_pref(name, default=None, group='standalone_misc_settings'): sd = vprefs['session_data'] - g = sd.get(group, {}) + g = sd.get(group, {}) if group else sd return g.get(name, default) @@ -232,6 +232,8 @@ class ViewerBridge(Bridge): view_image = from_js(object) copy_image = from_js(object) change_background_image = from_js(object) + notify_progress_frac = from_js(object, object, object) + overlay_visibility_changed = from_js(object) create_view = to_js() show_preparing_message = to_js() @@ -242,6 +244,7 @@ class ViewerBridge(Bridge): get_current_cfi = to_js() show_home_page = to_js() background_image_changed = to_js() + goto_frac = to_js() def apply_font_settings(page_or_view): @@ -373,6 +376,9 @@ class WebView(RestartingWebEngineView): selection_changed = pyqtSignal(object) view_image = pyqtSignal(object) copy_image = pyqtSignal(object) + scrollbar_visibility_changed = pyqtSignal() + notify_progress_frac = pyqtSignal(object, object, object) + overlay_visibility_changed = pyqtSignal(object) def __init__(self, parent=None): self._host_widget = None @@ -399,6 +405,8 @@ class WebView(RestartingWebEngineView): self.bridge.selection_changed.connect(self.selection_changed) self.bridge.view_image.connect(self.view_image) self.bridge.copy_image.connect(self.copy_image) + self.bridge.notify_progress_frac.connect(self.notify_progress_frac) + self.bridge.overlay_visibility_changed.connect(self.overlay_visibility_changed) self.bridge.report_cfi.connect(self.call_callback) self.bridge.change_background_image.connect(self.change_background_image) self.pending_bridge_ready_actions = {} @@ -499,6 +507,8 @@ class WebView(RestartingWebEngineView): vprefs['session_data'] = sd if key in ('standalone_font_settings', 'base_font_size'): apply_font_settings(self._page) + elif key == 'standalone_scrollbar': + self.scrollbar_visibility_changed.emit() def do_callback(self, func_name, callback): cid = next(self.callback_id_counter) @@ -525,3 +535,6 @@ class WebView(RestartingWebEngineView): shutil.copyfileobj(src, dest) background_image.ans = None self.execute_when_ready('background_image_changed', img_id) + + def goto_frac(self, frac): + self.execute_when_ready('goto_frac', frac) diff --git a/src/calibre/srv/render_book.py b/src/calibre/srv/render_book.py index b0dc820880..5428e02726 100644 --- a/src/calibre/srv/render_book.py +++ b/src/calibre/srv/render_book.py @@ -172,7 +172,7 @@ def get_length(root): if elem.tail: num += len(strip_space.sub('', elem.tail)) if tname in 'img svg': - num += 2000 + num += 1000 return num for body in root.iterdescendants(XHTML('body')): diff --git a/src/pyj/read_book/iframe.pyj b/src/pyj/read_book/iframe.pyj index 9894d831ec..5551bebd32 100644 --- a/src/pyj/read_book/iframe.pyj +++ b/src/pyj/read_book/iframe.pyj @@ -85,6 +85,7 @@ class IframeBoss: 'initialize':self.initialize, 'display': self.display, 'scroll_to_anchor': self.on_scroll_to_anchor, + 'scroll_to_frac': self.on_scroll_to_frac, 'next_screen': self.on_next_screen, 'change_font_size': self.change_font_size, 'change_color_scheme': self.change_color_scheme, @@ -105,6 +106,7 @@ class IframeBoss: scroll_viewport.update_window_size(data.width, data.height) window.onerror = self.onerror window.addEventListener('scroll', debounce(self.onscroll, 1000)) + window.addEventListener('scroll', self.no_latency_onscroll) window.addEventListener('resize', debounce(self.onresize, 500)) window.addEventListener('wheel', self.onwheel, {'passive': False}) window.addEventListener('keydown', self.onkeydown, {'passive': False}) @@ -171,6 +173,9 @@ class IframeBoss: root_data, self.mathjax, self.blob_url_map = finalize_resources(self.book, data.name, data.resource_data) self.resource_urls = unserialize_html(root_data, self.content_loaded) + def on_scroll_to_frac(self, data): + self.to_scroll_fraction(data.frac) + def handle_gesture(self, gesture): if gesture.type is 'show-chrome': self.send_message('show_chrome') @@ -280,13 +285,13 @@ class IframeBoss: if si: self.length_before += files[si]?.length or 0 self.onscroll() - self.send_message('content_loaded', progress_frac=self.get_progress_frac()) + self.send_message('content_loaded', progress_frac=self.calculate_progress_frac(), file_progress_frac=progress_frac()) self.last_cfi = None window.setTimeout(self.update_cfi, 0) window.setTimeout(self.update_toc_position, 0) - - def calculate_progress_frac(self, current_name, spine_index): + def calculate_progress_frac(self): + current_name = current_spine_item().name files = self.book.manifest.files file_length = files[current_name]?.length or 0 if self.length_before is None: @@ -295,14 +300,6 @@ class IframeBoss: ans = (self.length_before + (file_length * frac)) / self.book.manifest.spine_length return ans - def get_progress_frac(self): - spine = self.book.manifest.spine - current_name = current_spine_item().name - index = spine.indexOf(current_name) - if index < 0: - return 0 - return self.calculate_progress_frac(current_name, index) - def get_current_cfi(self, data): cfi = at_current() selected_text = window.getSelection().toString() @@ -313,7 +310,7 @@ class IframeBoss: 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), + 'report_cfi', cfi=cfi, progress_frac=self.calculate_progress_frac(), file_progress_frac=progress_frac(), request_id=data.request_id, selected_text=selected_text) return self.send_message( @@ -327,14 +324,18 @@ class IframeBoss: index = spine.indexOf(current_name) if index > -1: cfi = 'epubcfi(/{}{})'.format(2*(index+1), cfi) + pf = self.calculate_progress_frac() + fpf = progress_frac() if cfi is not self.last_cfi: self.last_cfi = cfi selected_text = window.getSelection().toString() self.send_message( 'update_cfi', cfi=cfi, replace_history=self.replace_history_on_next_cfi_update, - progress_frac=self.calculate_progress_frac(current_name, index), - file_progress_frac=progress_frac(), selected_text=selected_text) + progress_frac=pf, file_progress_frac=fpf, selected_text=selected_text) self.replace_history_on_next_cfi_update = True + else: + self.send_message( + 'update_progress_frac', progress_frac=pf, file_progress_frac=fpf) def update_toc_position(self): visible_anchors = update_visible_toc_anchors(self.book.manifest.toc_anchor_map, self.anchor_funcs) @@ -345,6 +346,12 @@ class IframeBoss: self.update_cfi() self.update_toc_position() + def no_latency_onscroll(self): + pf = self.calculate_progress_frac() + fpf = progress_frac() + self.send_message( + 'update_progress_frac', progress_frac=pf, file_progress_frac=fpf) + def onresize(self): self.send_message('request_size') if self.content_ready: diff --git a/src/pyj/read_book/prefs/scrolling.pyj b/src/pyj/read_book/prefs/scrolling.pyj index 62964cacfa..932dba09ae 100644 --- a/src/pyj/read_book/prefs/scrolling.pyj +++ b/src/pyj/read_book/prefs/scrolling.pyj @@ -10,6 +10,8 @@ from dom import unique_id from widgets import create_button from session import defaults +from read_book.globals import runtime + CONTAINER = unique_id('standalone-scrolling-settings') @@ -41,6 +43,11 @@ def create_scrolling_panel(container): container.appendChild(cb( 'paged_margin_clicks_scroll_by_screen', _('Clicking on the margins scrolls by screen fulls instead of pages'))) + if runtime.is_standalone_viewer: + container.appendChild(E.div(style='margin-top:1ex; border-top: solid 1px', '\xa0')) + container.appendChild(cb( + 'standalone_scrollbar', _('Show a scrollbar'))) + container.appendChild(E.div( style='margin-top: 1rem', create_button(_('Restore defaults'), action=restore_defaults) )) diff --git a/src/pyj/read_book/view.pyj b/src/pyj/read_book/view.pyj index 60e09088af..eb772e0c08 100644 --- a/src/pyj/read_book/view.pyj +++ b/src/pyj/read_book/view.pyj @@ -186,6 +186,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, + 'update_progress_frac': self.on_update_progress_frac, 'report_cfi': self.on_report_cfi, 'update_toc_position': self.on_update_toc_position, 'content_loaded': self.on_content_loaded, @@ -288,6 +289,8 @@ class View: def overlay_visibility_changed(self, visible): if self.iframe_wrapper.send_message: self.iframe_wrapper.send_message('set_forward_keypresses', forward=v'!!visible') + if ui_operations.overlay_visibility_changed: + ui_operations.overlay_visibility_changed(visible) def on_handle_shortcut(self, data): if data.name is 'back': @@ -621,6 +624,34 @@ class View: name = self.book.manifest.spine[0 if start else self.book.manifest.spine.length - 1] self.show_name(name, initial_position={'type':'frac', 'frac':0 if start else 1, 'replace_history':False}) + def goto_frac(self, frac): + if not self.book or not self.book.manifest: + return + chapter_start_page = 0 + total_length = self.book.manifest.spine_length + page = total_length * frac + chapter_frac = 0 + chapter_name = None + for name in self.book.manifest.spine: + chapter_length = self.book.manifest.files[name]?.length or 0 + chapter_end_page = chapter_start_page + chapter_length + if chapter_start_page <= page <= chapter_end_page: + num_pages = chapter_end_page - chapter_start_page - 1 + if num_pages > 0: + chapter_frac = (page - chapter_start_page) / num_pages + else: + chapter_frac = 0 + chapter_name = name + break + chapter_start_page = chapter_end_page + if not chapter_name: + chapter_name = self.book.manifest.spine[-1] + chapter_frac = max(0, min(chapter_frac, 1)) + if self.currently_showing.name is chapter_name: + self.iframe_wrapper.send_message('scroll_to_frac', frac=chapter_frac) + else: + self.show_name(chapter_name, initial_position={'type':'frac', 'frac':chapter_frac, 'replace_history':True}) + def on_scroll_to_anchor(self, data): self.show_name(data.name, initial_position={'type':'anchor', 'anchor':data.frag, 'replace_history':False}) @@ -698,8 +729,7 @@ class View: if not self.book.last_read_position: self.book.last_read_position = {} self.book.last_read_position[unkey] = data.cfi - self.current_progress_frac = data.progress_frac - self.current_file_progress_frac = data.file_progress_frac + self.set_progress_frac(data.progress_frac, data.file_progress_frac) self.update_header_footer() if ui_operations.update_last_read_time: ui_operations.update_last_read_time(self.book) @@ -716,6 +746,10 @@ class View: }) v'delete self.report_cfi_callbacks[data.request_id]' + def on_update_progress_frac(self, data): + self.set_progress_frac(data.progress_frac, data.file_progress_frac) + self.update_header_footer() + 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: @@ -741,7 +775,7 @@ class View: has_clock = False if self.book?.manifest: - book_length = self.book.manifest.total_length or 0 + book_length = self.book.manifest.spine_length or 0 name = self.currently_showing.name chapter_length = self.book.manifest.files[name]?.length or 0 else: @@ -815,11 +849,19 @@ class View: def on_content_loaded(self, data): self.processing_spine_item_display = False self.hide_loading() - frac = data.progress_frac or 0 - self.current_progress_frac = frac + self.set_progress_frac(data.progress_frac, data.file_progress_frac) self.update_header_footer() window.scrollTo(0, 0) # ensure window is at 0 on mobile where the navbar causes issues + def set_progress_frac(self, progress_frac, file_progress_frac): + self.current_progress_frac = progress_frac or 0 + self.current_file_progress_frac = file_progress_frac or 0 + if ui_operations.notify_progress_frac: + book_length = 0 + if self.book?.manifest: + book_length = self.book.manifest.spine_length or 0 + ui_operations.notify_progress_frac(self.current_progress_frac, self.current_file_progress_frac, book_length) + def update_font_size(self): self.iframe_wrapper.send_message('change_font_size', base_font_size=get_session_data().get('base_font_size')) diff --git a/src/pyj/session.pyj b/src/pyj/session.pyj index 707dddfa0c..bed329f275 100644 --- a/src/pyj/session.pyj +++ b/src/pyj/session.pyj @@ -48,6 +48,7 @@ defaults = { 'keyboard_shortcuts': {}, 'standalone_font_settings': {}, 'standalone_misc_settings': {}, + 'standalone_scrollbar': False, 'standalone_recently_opened': v'[]', 'paged_wheel_scrolls_by_screen': False, 'paged_margin_clicks_scroll_by_screen': True, @@ -72,6 +73,7 @@ is_local_setting = { 'standalone_font_settings': True, 'standalone_misc_settings': True, 'standalone_recently_opened': True, + 'standalone_scrollbar': False, } diff --git a/src/pyj/viewer-main.pyj b/src/pyj/viewer-main.pyj index faae045997..e9839bafd3 100644 --- a/src/pyj/viewer-main.pyj +++ b/src/pyj/viewer-main.pyj @@ -237,6 +237,12 @@ def get_current_cfi(request_id): view.get_current_cfi(request_id, ui_operations.report_cfi) +@from_python +def goto_frac(frac): + if view: + view.goto_frac(frac) + + @from_python def background_image_changed(img_id): img = document.getElementById(img_id) @@ -306,8 +312,12 @@ if window is window.top: to_python.copy_image(name) ui_operations.change_background_image = def(img_id): to_python.change_background_image(img_id) + ui_operations.notify_progress_frac = def (pf, fpf, book_length): + to_python.notify_progress_frac(pf, fpf, book_length) ui_operations.quit = def(): to_python.quit() + ui_operations.overlay_visibility_changed = def(visible): + to_python.overlay_visibility_changed(visible) document.body.appendChild(E.div(id='view')) window.onerror = onerror