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):