Viewer: Add an option to display a scrollbar

This commit is contained in:
Kovid Goyal 2019-10-06 22:36:57 +05:30
parent 42ae6dea61
commit 0ccedfdcc7
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
8 changed files with 162 additions and 24 deletions

View File

@ -12,8 +12,8 @@ from hashlib import sha256
from threading import Thread from threading import Thread
from PyQt5.Qt import ( from PyQt5.Qt import (
QApplication, QDockWidget, QEvent, QMimeData, QModelIndex, QPixmap, Qt, QUrl, QApplication, QDockWidget, QEvent, QHBoxLayout, QMimeData, QModelIndex, QPixmap,
QVBoxLayout, QWidget, pyqtSignal QScrollBar, Qt, QUrl, QVBoxLayout, QWidget, pyqtSignal
) )
from calibre import prints from calibre import prints
@ -59,6 +59,61 @@ def path_key(path):
return sha256(as_bytes(path)).hexdigest() 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): class EbookViewer(MainWindow):
msg_from_anotherinstance = pyqtSignal(object) 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.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.view_image.connect(self.view_image, type=Qt.QueuedConnection)
self.web_view.copy_image.connect(self.copy_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() self.restore_state()
if continue_reading: if continue_reading:
self.continue_reading() self.continue_reading()
@ -288,6 +344,7 @@ class EbookViewer(MainWindow):
self.web_view.show_home_page() self.web_view.show_home_page()
return return
set_book_path(data['base'], data['pathtoebook']) 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 = data
self.current_book_data['annotations_map'] = defaultdict(list) self.current_book_data['annotations_map'] = defaultdict(list)
self.current_book_data['annotations_path_key'] = path_key(data['pathtoebook']) + '.json' self.current_book_data['annotations_path_key'] = path_key(data['pathtoebook']) + '.json'

View File

@ -186,7 +186,7 @@ class UrlSchemeHandler(QWebEngineUrlSchemeHandler):
def get_session_pref(name, default=None, group='standalone_misc_settings'): def get_session_pref(name, default=None, group='standalone_misc_settings'):
sd = vprefs['session_data'] sd = vprefs['session_data']
g = sd.get(group, {}) g = sd.get(group, {}) if group else sd
return g.get(name, default) return g.get(name, default)
@ -232,6 +232,8 @@ class ViewerBridge(Bridge):
view_image = from_js(object) view_image = from_js(object)
copy_image = from_js(object) copy_image = from_js(object)
change_background_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() create_view = to_js()
show_preparing_message = to_js() show_preparing_message = to_js()
@ -242,6 +244,7 @@ class ViewerBridge(Bridge):
get_current_cfi = to_js() get_current_cfi = to_js()
show_home_page = to_js() show_home_page = to_js()
background_image_changed = to_js() background_image_changed = to_js()
goto_frac = to_js()
def apply_font_settings(page_or_view): def apply_font_settings(page_or_view):
@ -373,6 +376,9 @@ class WebView(RestartingWebEngineView):
selection_changed = pyqtSignal(object) selection_changed = pyqtSignal(object)
view_image = pyqtSignal(object) view_image = pyqtSignal(object)
copy_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): def __init__(self, parent=None):
self._host_widget = None self._host_widget = None
@ -399,6 +405,8 @@ class WebView(RestartingWebEngineView):
self.bridge.selection_changed.connect(self.selection_changed) self.bridge.selection_changed.connect(self.selection_changed)
self.bridge.view_image.connect(self.view_image) self.bridge.view_image.connect(self.view_image)
self.bridge.copy_image.connect(self.copy_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.report_cfi.connect(self.call_callback)
self.bridge.change_background_image.connect(self.change_background_image) self.bridge.change_background_image.connect(self.change_background_image)
self.pending_bridge_ready_actions = {} self.pending_bridge_ready_actions = {}
@ -499,6 +507,8 @@ class WebView(RestartingWebEngineView):
vprefs['session_data'] = sd vprefs['session_data'] = sd
if key in ('standalone_font_settings', 'base_font_size'): if key in ('standalone_font_settings', 'base_font_size'):
apply_font_settings(self._page) apply_font_settings(self._page)
elif key == 'standalone_scrollbar':
self.scrollbar_visibility_changed.emit()
def do_callback(self, func_name, callback): def do_callback(self, func_name, callback):
cid = next(self.callback_id_counter) cid = next(self.callback_id_counter)
@ -525,3 +535,6 @@ class WebView(RestartingWebEngineView):
shutil.copyfileobj(src, dest) shutil.copyfileobj(src, dest)
background_image.ans = None background_image.ans = None
self.execute_when_ready('background_image_changed', img_id) self.execute_when_ready('background_image_changed', img_id)
def goto_frac(self, frac):
self.execute_when_ready('goto_frac', frac)

View File

@ -172,7 +172,7 @@ def get_length(root):
if elem.tail: if elem.tail:
num += len(strip_space.sub('', elem.tail)) num += len(strip_space.sub('', elem.tail))
if tname in 'img svg': if tname in 'img svg':
num += 2000 num += 1000
return num return num
for body in root.iterdescendants(XHTML('body')): for body in root.iterdescendants(XHTML('body')):

View File

@ -85,6 +85,7 @@ class IframeBoss:
'initialize':self.initialize, 'initialize':self.initialize,
'display': self.display, 'display': self.display,
'scroll_to_anchor': self.on_scroll_to_anchor, 'scroll_to_anchor': self.on_scroll_to_anchor,
'scroll_to_frac': self.on_scroll_to_frac,
'next_screen': self.on_next_screen, 'next_screen': self.on_next_screen,
'change_font_size': self.change_font_size, 'change_font_size': self.change_font_size,
'change_color_scheme': self.change_color_scheme, 'change_color_scheme': self.change_color_scheme,
@ -105,6 +106,7 @@ class IframeBoss:
scroll_viewport.update_window_size(data.width, data.height) scroll_viewport.update_window_size(data.width, data.height)
window.onerror = self.onerror window.onerror = self.onerror
window.addEventListener('scroll', debounce(self.onscroll, 1000)) window.addEventListener('scroll', debounce(self.onscroll, 1000))
window.addEventListener('scroll', self.no_latency_onscroll)
window.addEventListener('resize', debounce(self.onresize, 500)) window.addEventListener('resize', debounce(self.onresize, 500))
window.addEventListener('wheel', self.onwheel, {'passive': False}) window.addEventListener('wheel', self.onwheel, {'passive': False})
window.addEventListener('keydown', self.onkeydown, {'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) 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) 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): def handle_gesture(self, gesture):
if gesture.type is 'show-chrome': if gesture.type is 'show-chrome':
self.send_message('show_chrome') self.send_message('show_chrome')
@ -280,13 +285,13 @@ class IframeBoss:
if si: if si:
self.length_before += files[si]?.length or 0 self.length_before += files[si]?.length or 0
self.onscroll() 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 self.last_cfi = None
window.setTimeout(self.update_cfi, 0) window.setTimeout(self.update_cfi, 0)
window.setTimeout(self.update_toc_position, 0) window.setTimeout(self.update_toc_position, 0)
def calculate_progress_frac(self):
def calculate_progress_frac(self, current_name, spine_index): current_name = current_spine_item().name
files = self.book.manifest.files files = self.book.manifest.files
file_length = files[current_name]?.length or 0 file_length = files[current_name]?.length or 0
if self.length_before is None: if self.length_before is None:
@ -295,14 +300,6 @@ class IframeBoss:
ans = (self.length_before + (file_length * frac)) / self.book.manifest.spine_length ans = (self.length_before + (file_length * frac)) / self.book.manifest.spine_length
return ans 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): def get_current_cfi(self, data):
cfi = at_current() cfi = at_current()
selected_text = window.getSelection().toString() selected_text = window.getSelection().toString()
@ -313,7 +310,7 @@ class IframeBoss:
if index > -1: if index > -1:
cfi = 'epubcfi(/{}{})'.format(2*(index+1), cfi) cfi = 'epubcfi(/{}{})'.format(2*(index+1), cfi)
self.send_message( 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) file_progress_frac=progress_frac(), request_id=data.request_id, selected_text=selected_text)
return return
self.send_message( self.send_message(
@ -327,14 +324,18 @@ class IframeBoss:
index = spine.indexOf(current_name) index = spine.indexOf(current_name)
if index > -1: if index > -1:
cfi = 'epubcfi(/{}{})'.format(2*(index+1), cfi) cfi = 'epubcfi(/{}{})'.format(2*(index+1), cfi)
pf = self.calculate_progress_frac()
fpf = progress_frac()
if cfi is not self.last_cfi: if cfi is not self.last_cfi:
self.last_cfi = cfi self.last_cfi = cfi
selected_text = window.getSelection().toString() selected_text = window.getSelection().toString()
self.send_message( self.send_message(
'update_cfi', cfi=cfi, replace_history=self.replace_history_on_next_cfi_update, 'update_cfi', cfi=cfi, replace_history=self.replace_history_on_next_cfi_update,
progress_frac=self.calculate_progress_frac(current_name, index), progress_frac=pf, file_progress_frac=fpf, selected_text=selected_text)
file_progress_frac=progress_frac(), selected_text=selected_text)
self.replace_history_on_next_cfi_update = True 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): def update_toc_position(self):
visible_anchors = update_visible_toc_anchors(self.book.manifest.toc_anchor_map, self.anchor_funcs) 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_cfi()
self.update_toc_position() 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): def onresize(self):
self.send_message('request_size') self.send_message('request_size')
if self.content_ready: if self.content_ready:

View File

@ -10,6 +10,8 @@ from dom import unique_id
from widgets import create_button from widgets import create_button
from session import defaults from session import defaults
from read_book.globals import runtime
CONTAINER = unique_id('standalone-scrolling-settings') CONTAINER = unique_id('standalone-scrolling-settings')
@ -41,6 +43,11 @@ def create_scrolling_panel(container):
container.appendChild(cb( container.appendChild(cb(
'paged_margin_clicks_scroll_by_screen', _('Clicking on the margins scrolls by screen fulls instead of pages'))) '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( container.appendChild(E.div(
style='margin-top: 1rem', create_button(_('Restore defaults'), action=restore_defaults) style='margin-top: 1rem', create_button(_('Restore defaults'), action=restore_defaults)
)) ))

View File

@ -186,6 +186,7 @@ class View:
'goto_doc_boundary': def(data): self.goto_doc_boundary(data.start);, 'goto_doc_boundary': def(data): self.goto_doc_boundary(data.start);,
'scroll_to_anchor': self.on_scroll_to_anchor, 'scroll_to_anchor': self.on_scroll_to_anchor,
'update_cfi': self.on_update_cfi, 'update_cfi': self.on_update_cfi,
'update_progress_frac': self.on_update_progress_frac,
'report_cfi': self.on_report_cfi, 'report_cfi': self.on_report_cfi,
'update_toc_position': self.on_update_toc_position, 'update_toc_position': self.on_update_toc_position,
'content_loaded': self.on_content_loaded, 'content_loaded': self.on_content_loaded,
@ -288,6 +289,8 @@ class View:
def overlay_visibility_changed(self, visible): def overlay_visibility_changed(self, visible):
if self.iframe_wrapper.send_message: if self.iframe_wrapper.send_message:
self.iframe_wrapper.send_message('set_forward_keypresses', forward=v'!!visible') 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): def on_handle_shortcut(self, data):
if data.name is 'back': 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] 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}) 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): def on_scroll_to_anchor(self, data):
self.show_name(data.name, initial_position={'type':'anchor', 'anchor':data.frag, 'replace_history':False}) 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: if not self.book.last_read_position:
self.book.last_read_position = {} self.book.last_read_position = {}
self.book.last_read_position[unkey] = data.cfi self.book.last_read_position[unkey] = data.cfi
self.current_progress_frac = data.progress_frac self.set_progress_frac(data.progress_frac, data.file_progress_frac)
self.current_file_progress_frac = data.file_progress_frac
self.update_header_footer() self.update_header_footer()
if ui_operations.update_last_read_time: if ui_operations.update_last_read_time:
ui_operations.update_last_read_time(self.book) ui_operations.update_last_read_time(self.book)
@ -716,6 +746,10 @@ class View:
}) })
v'delete self.report_cfi_callbacks[data.request_id]' 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): def on_update_cfi(self, data):
overlay_shown = not self.processing_spine_item_display and self.overlay.is_visible 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: 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 has_clock = False
if self.book?.manifest: 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 name = self.currently_showing.name
chapter_length = self.book.manifest.files[name]?.length or 0 chapter_length = self.book.manifest.files[name]?.length or 0
else: else:
@ -815,11 +849,19 @@ class View:
def on_content_loaded(self, data): def on_content_loaded(self, data):
self.processing_spine_item_display = False self.processing_spine_item_display = False
self.hide_loading() self.hide_loading()
frac = data.progress_frac or 0 self.set_progress_frac(data.progress_frac, data.file_progress_frac)
self.current_progress_frac = frac
self.update_header_footer() self.update_header_footer()
window.scrollTo(0, 0) # ensure window is at 0 on mobile where the navbar causes issues 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): def update_font_size(self):
self.iframe_wrapper.send_message('change_font_size', base_font_size=get_session_data().get('base_font_size')) self.iframe_wrapper.send_message('change_font_size', base_font_size=get_session_data().get('base_font_size'))

View File

@ -48,6 +48,7 @@ defaults = {
'keyboard_shortcuts': {}, 'keyboard_shortcuts': {},
'standalone_font_settings': {}, 'standalone_font_settings': {},
'standalone_misc_settings': {}, 'standalone_misc_settings': {},
'standalone_scrollbar': False,
'standalone_recently_opened': v'[]', 'standalone_recently_opened': v'[]',
'paged_wheel_scrolls_by_screen': False, 'paged_wheel_scrolls_by_screen': False,
'paged_margin_clicks_scroll_by_screen': True, 'paged_margin_clicks_scroll_by_screen': True,
@ -72,6 +73,7 @@ is_local_setting = {
'standalone_font_settings': True, 'standalone_font_settings': True,
'standalone_misc_settings': True, 'standalone_misc_settings': True,
'standalone_recently_opened': True, 'standalone_recently_opened': True,
'standalone_scrollbar': False,
} }

View File

@ -237,6 +237,12 @@ def get_current_cfi(request_id):
view.get_current_cfi(request_id, ui_operations.report_cfi) view.get_current_cfi(request_id, ui_operations.report_cfi)
@from_python
def goto_frac(frac):
if view:
view.goto_frac(frac)
@from_python @from_python
def background_image_changed(img_id): def background_image_changed(img_id):
img = document.getElementById(img_id) img = document.getElementById(img_id)
@ -306,8 +312,12 @@ if window is window.top:
to_python.copy_image(name) to_python.copy_image(name)
ui_operations.change_background_image = def(img_id): ui_operations.change_background_image = def(img_id):
to_python.change_background_image(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(): ui_operations.quit = def():
to_python.quit() to_python.quit()
ui_operations.overlay_visibility_changed = def(visible):
to_python.overlay_visibility_changed(visible)
document.body.appendChild(E.div(id='view')) document.body.appendChild(E.div(id='view'))
window.onerror = onerror window.onerror = onerror