From 63fda1fed3a0732947ec55f70dfc8813ec05991d Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 7 Oct 2019 13:18:24 +0530 Subject: [PATCH] Implement the scrollbar inside the web view This allows it to be hidden naturally when displaying the overlay. Also gives nice control when clicking in the gutter to scroll by page --- src/calibre/gui2/viewer/ui.py | 66 +------------------ src/calibre/gui2/viewer/web_view.py | 6 -- src/pyj/read_book/prefs/scrolling.pyj | 9 +-- src/pyj/read_book/scrollbar.pyj | 91 +++++++++++++++++++++++++++ src/pyj/read_book/view.pyj | 22 +++---- src/pyj/session.pyj | 3 +- src/pyj/viewer-main.pyj | 2 - 7 files changed, 109 insertions(+), 90 deletions(-) create mode 100644 src/pyj/read_book/scrollbar.pyj diff --git a/src/calibre/gui2/viewer/ui.py b/src/calibre/gui2/viewer/ui.py index 5df94768fe..54925b0bcb 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, QHBoxLayout, QMimeData, QModelIndex, QPixmap, - QScrollBar, Qt, QUrl, QVBoxLayout, QWidget, pyqtSignal + QApplication, QDockWidget, QEvent, QMimeData, QModelIndex, QPixmap, QScrollBar, + Qt, QUrl, QVBoxLayout, QWidget, pyqtSignal ) from calibre import prints @@ -66,64 +66,6 @@ class ScrollBar(QScrollBar): return QScrollBar.paintEvent(self, ev) -class CentralWidget(QWidget): - - def __init__(self, web_view, parent): - QWidget.__init__(self, parent) - self._ignore_value_changes = False - 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.valueChanged[int].connect(self.value_changed) - l.addWidget(vs) - self.current_book_length = None - web_view.notify_progress_frac.connect(self.update_scrollbar_positions_on_scroll) - 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 __enter__(self): - self._ignore_value_changes = True - - def __exit__(self, *a): - self._ignore_value_changes = False - - 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): - with self: - val = int(self.vertical_scrollbar.maximum() * frac) - self.vertical_scrollbar.setValue(val) - - def value_changed(self, val): - if not self._ignore_value_changes: - frac = val / self.vertical_scrollbar.maximum() - self.web_view.goto_frac(frac) - - def initialize_scrollbars(self, book_length): - with self: - 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_on_scroll(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) @@ -192,8 +134,7 @@ 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.central_widget = CentralWidget(self.web_view, self) - self.setCentralWidget(self.central_widget) + self.setCentralWidget(self.web_view) self.restore_state() if continue_reading: self.continue_reading() @@ -354,7 +295,6 @@ 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 ba718e3b3a..20a91d2fad 100644 --- a/src/calibre/gui2/viewer/web_view.py +++ b/src/calibre/gui2/viewer/web_view.py @@ -232,7 +232,6 @@ 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() @@ -376,8 +375,6 @@ 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): @@ -405,7 +402,6 @@ 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) @@ -507,8 +503,6 @@ 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) diff --git a/src/pyj/read_book/prefs/scrolling.pyj b/src/pyj/read_book/prefs/scrolling.pyj index 932dba09ae..8300d0f42a 100644 --- a/src/pyj/read_book/prefs/scrolling.pyj +++ b/src/pyj/read_book/prefs/scrolling.pyj @@ -10,8 +10,6 @@ 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') @@ -43,10 +41,9 @@ 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:1ex; border-top: solid 1px', '\xa0')) + container.appendChild(cb( + 'book_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/scrollbar.pyj b/src/pyj/read_book/scrollbar.pyj new file mode 100644 index 0000000000..446f18cb18 --- /dev/null +++ b/src/pyj/read_book/scrollbar.pyj @@ -0,0 +1,91 @@ +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2019, Kovid Goyal +from __python__ import bound_methods, hash_literals + +from dom import unique_id +from elementmaker import E +from book_list.globals import get_session_data + + +class BookScrollbar: + + def __init__(self, view): + self.view = view + self.container_id = unique_id('book-scrollbar') + self.sync_to_contents_timer = 0 + self.sync_contents_timer = 0 + + @property + def container(self): + return document.getElementById(self.container_id) + + def create(self): + self.on_bob_mousedown = self.on_bob_mouse_event.bind(None, 'down') + self.on_bob_mousemove = self.on_bob_mouse_event.bind(None, 'move') + self.on_bob_mouseup = self.on_bob_mouse_event.bind(None, 'up') + return E.div( + id=self.container_id, + style='height: 100vh; background-color: #aaa; width: 10px; border-radius: 5px', + onclick=self.bar_clicked, + E.div( + style='position: relative; width: 100%; height: 22px; background-color: #444; border-radius: 5px', + onmousedown=self.on_bob_mousedown, + ), + E.div( + style='position: absolute; z-index: 30000; width: 100vw; height: 100vh; left: 0; top: 0; display: none;' + ) + ) + + def bar_clicked(self, evt): + if evt.button is 0: + c = self.container + b = c.firstChild + bob_top = b.offsetTop + bob_bottom = bob_top + b.offsetHeight + if evt.clientY < bob_top: + self.view.left_margin_clicked(evt) + elif evt.clientY > bob_bottom: + self.view.right_margin_clicked(evt) + + def on_bob_mouse_event(self, which, evt): + c = self.container + bob = c.firstChild + mouse_grab = bob.nextSibling + if which is 'move': + top = evt.pageY - self.down_y + height = c.clientHeight - bob.clientHeight + top = max(0, min(top, height)) + bob.style.top = f'{top}px' + evt.preventDefault(), evt.stopPropagation() + frac = bob.offsetTop / height + if self.sync_contents_timer: + window.clearTimeout(self.sync_contents_timer) + self.sync_contents_timer = window.setTimeout(self.view.goto_frac.bind(None, frac), 2) + elif which is 'down': + if evt.button is not 0: + return + evt.preventDefault(), evt.stopPropagation() + self.down_y = evt.clientY - bob.getBoundingClientRect().top + mouse_grab.style.display = 'block' + mouse_grab.addEventListener('mousemove', self.on_bob_mousemove, {'capture': True, 'passive': False}) + mouse_grab.addEventListener('mouseup', self.on_bob_mouseup, {'capture': True, 'passive': False}) + elif which is 'up': + self.down_y = 0 + mouse_grab.removeEventListener('mousemove', self.on_bob_mousemove, {'capture': True, 'passive': False}) + mouse_grab.removeEventListener('mouseup', self.on_bob_mouseup, {'capture': True, 'passive': False}) + window.setTimeout(def(): self.container.firstChild.nextSibling.style.display = 'none';, 10) + evt.preventDefault(), evt.stopPropagation() + + def apply_visibility(self): + sd = get_session_data() + self.container.style.display = 'block' if sd.get('book_scrollbar') else 'none' + + def set_position(self, frac): + c = self.container + frac = max(0, min(frac, 1)) + c.firstChild.style.top = f'{frac * (c.clientHeight - c.firstChild.clientHeight)}px' + + def sync_to_contents(self, frac): + if self.sync_to_contents_timer: + window.clearTimeout(self.sync_to_contents_timer) + self.sync_to_contents_timer = window.setTimeout(self.set_position.bind(None, frac), 100) diff --git a/src/pyj/read_book/view.pyj b/src/pyj/read_book/view.pyj index eb772e0c08..7d5ffd4a08 100644 --- a/src/pyj/read_book/view.pyj +++ b/src/pyj/read_book/view.pyj @@ -13,6 +13,7 @@ from dom import add_extra_css, build_rule, set_css, svgicon, unique_id from iframe_comm import IframeWrapper from modals import error_dialog, warning_dialog from read_book.content_popup import ContentPopupOverlay +from read_book.scrollbar import BookScrollbar from read_book.globals import ( current_book, runtime, set_current_spine_item, ui_operations ) @@ -150,7 +151,8 @@ class View: self.report_cfi_callbacks = {} self.show_chrome_counter = 0 self.show_loading_callback_timer = None - self.clock_timer_id = 0 + self.timer_ids = {'clock': 0} + self.book_scrollbar = BookScrollbar(self) sd = get_session_data() self.keyboard_shortcut_map = create_shortcut_map(sd.get('keyboard_shortcuts')) left_margin = E.div(svgicon('caret-left'), style='width:{}px;'.format(sd.get('margin_left', 20)), class_='book-side-margin', id='book-left-margin', onclick=self.left_margin_clicked) @@ -169,6 +171,7 @@ class View: margin_elem(sd, 'margin_bottom', 'book-bottom-margin', self.bottom_margin_clicked), ), right_margin, + self.book_scrollbar.create(), E.div(style='position: absolute; top:0; left:0; width: 100%; pointer-events:none; display:none', id='book-search-overlay'), # search overlay E.div(style='position: absolute; top:0; left:0; width: 100%; height: 100%; display:none', id='book-content-popup-overlay'), # content popup overlay E.div(style='position: absolute; top:0; left:0; width: 100%; height: 100%; display:none', id='book-overlay'), # main overlay @@ -574,6 +577,7 @@ class View: # redisplay_book() is called when settings are changed sd = get_session_data() self.keyboard_shortcut_map = create_shortcut_map(sd.get('keyboard_shortcuts')) + self.book_scrollbar.apply_visibility() self.display_book(self.book) def iframe_settings(self, name): @@ -813,12 +817,12 @@ class View: if div: render_template(div, 'margin_top', 'header') if has_clock: - if not self.clock_timer_id: - self.clock_timer_id = window.setInterval(self.update_header_footer, 60000) + if not self.timer_ids.clock: + self.timer_ids.clock = window.setInterval(self.update_header_footer, 60000) else: - if self.clock_timer_id: - window.clearInterval(self.clock_timer_id) - self.clock_timer_id = 0 + if self.timer_ids.clock: + window.clearInterval(self.timer_ids.clock) + self.timer_ids.clock = 0 def on_update_toc_position(self, data): update_visible_toc_nodes(data.visible_anchors) @@ -856,11 +860,7 @@ class View: 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) + self.book_scrollbar.sync_to_contents(self.current_progress_frac) 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 bed329f275..edc4b888af 100644 --- a/src/pyj/session.pyj +++ b/src/pyj/session.pyj @@ -46,9 +46,9 @@ defaults = { 'word_actions': v'[]', 'hide_tooltips': False, 'keyboard_shortcuts': {}, + 'book_scrollbar': False, '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, @@ -73,7 +73,6 @@ 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 e9839bafd3..0dcfe61c64 100644 --- a/src/pyj/viewer-main.pyj +++ b/src/pyj/viewer-main.pyj @@ -312,8 +312,6 @@ 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):