mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 18:54:09 -04:00
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
This commit is contained in:
parent
bb5b7b0253
commit
63fda1fed3
@ -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'
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
91
src/pyj/read_book/scrollbar.pyj
Normal file
91
src/pyj/read_book/scrollbar.pyj
Normal file
@ -0,0 +1,91 @@
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPL v3 Copyright: 2019, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
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)
|
@ -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'))
|
||||
|
@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
@ -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):
|
||||
|
Loading…
x
Reference in New Issue
Block a user