mirror of
git://github.com/kovidgoyal/calibre.git
synced 2025-12-26 15:36:23 +01: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
7 changed files with 109 additions and 90 deletions
|
|
@ -12,8 +12,8 @@
|
|||
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 @@ def paintEvent(self, ev):
|
|||
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 @@ def create_dock(title, name, area, areas=Qt.LeftDockWidgetArea | Qt.RightDockWid
|
|||
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 @@ def load_finished(self, ok, data):
|
|||
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 @@ def __init__(self, parent=None):
|
|||
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 @@ def set_session_data(self, key, val):
|
|||
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 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 cb(name, text):
|
|||
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 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 @@ def __init__(self, container):
|
|||
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 @@ def __init__(self, container):
|
|||
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 @@ def redisplay_book(self):
|
|||
# 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 @@ def render_template(div, sz_attr, name):
|
|||
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 @@ def on_content_loaded(self, data):
|
|||
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 @@
|
|||
'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 @@
|
|||
'standalone_font_settings': True,
|
||||
'standalone_misc_settings': True,
|
||||
'standalone_recently_opened': True,
|
||||
'standalone_scrollbar': False,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -312,8 +312,6 @@ def onerror(msg, script_url, line_number, column_number, error_object):
|
|||
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…
Reference in a new issue