mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Viewer: Implement auto-scroll in flow mode. Can be toggled by pressing Ctrl+Spacebar. Its speed can be controlled in Viewer preferences under scrolling behavior.
Viewer: Allow configuring the speed with which the viewer scolls wheh pressing the down key in flow mode (Viewer preferences->Scrolling behavior) Merge branch 'scrolling' of https://github.com/mgziminsky/calibre
This commit is contained in:
commit
fc5f471682
@ -6,10 +6,26 @@ from select import word_at_point
|
|||||||
|
|
||||||
from dom import set_css
|
from dom import set_css
|
||||||
from read_book.globals import current_spine_item, get_boss
|
from read_book.globals import current_spine_item, get_boss
|
||||||
|
from read_book.settings import opts
|
||||||
from read_book.viewport import scroll_viewport
|
from read_book.viewport import scroll_viewport
|
||||||
from utils import document_height, viewport_to_document
|
from utils import document_height, viewport_to_document
|
||||||
|
|
||||||
|
|
||||||
|
def line_height():
|
||||||
|
if not line_height.ans:
|
||||||
|
ds = window.getComputedStyle(document.body)
|
||||||
|
try:
|
||||||
|
# will fail if line-height = "normal"
|
||||||
|
lh = float(ds.lineHeight)
|
||||||
|
except:
|
||||||
|
try:
|
||||||
|
lh = 1.2 * float(ds.fontSize)
|
||||||
|
except:
|
||||||
|
lh = 15
|
||||||
|
line_height.ans = max(5, lh)
|
||||||
|
return line_height.ans
|
||||||
|
|
||||||
|
|
||||||
def flow_to_scroll_fraction(frac, on_initial_load):
|
def flow_to_scroll_fraction(frac, on_initial_load):
|
||||||
scroll_viewport.scroll_to(0, document_height() * frac)
|
scroll_viewport.scroll_to(0, document_height() * frac)
|
||||||
|
|
||||||
@ -41,15 +57,15 @@ def add_small_scroll(amt):
|
|||||||
|
|
||||||
|
|
||||||
def report_human_scroll(amt):
|
def report_human_scroll(amt):
|
||||||
|
h = scroll_viewport.height()
|
||||||
|
is_large_scroll = (abs(amt) / h) >= 0.5
|
||||||
if amt > 0:
|
if amt > 0:
|
||||||
h = scroll_viewport.height()
|
|
||||||
is_large_scroll = (amt / h) >= 0.5
|
|
||||||
if is_large_scroll:
|
if is_large_scroll:
|
||||||
clear_small_scrolls()
|
clear_small_scrolls()
|
||||||
get_boss().report_human_scroll(amt / document_height())
|
get_boss().report_human_scroll(amt / document_height())
|
||||||
else:
|
else:
|
||||||
add_small_scroll(amt)
|
add_small_scroll(amt)
|
||||||
else:
|
elif amt is 0 or is_large_scroll:
|
||||||
clear_small_scrolls()
|
clear_small_scrolls()
|
||||||
|
|
||||||
|
|
||||||
@ -59,8 +75,11 @@ last_change_spine_item_request = {}
|
|||||||
def _check_for_scroll_end(func, obj, args, report):
|
def _check_for_scroll_end(func, obj, args, report):
|
||||||
before = window.pageYOffset
|
before = window.pageYOffset
|
||||||
func.apply(obj, args)
|
func.apply(obj, args)
|
||||||
|
|
||||||
|
now = window.performance.now()
|
||||||
|
scroll_animator.sync(now)
|
||||||
|
|
||||||
if window.pageYOffset is before:
|
if window.pageYOffset is before:
|
||||||
now = Date.now()
|
|
||||||
csi = current_spine_item()
|
csi = current_spine_item()
|
||||||
if last_change_spine_item_request.name is csi.name and now - last_change_spine_item_request.at < 2000:
|
if last_change_spine_item_request.name is csi.name and now - last_change_spine_item_request.at < 2000:
|
||||||
return False
|
return False
|
||||||
@ -95,7 +114,7 @@ def flow_onwheel(evt):
|
|||||||
if evt.deltaMode is WheelEvent.DOM_DELTA_PIXEL:
|
if evt.deltaMode is WheelEvent.DOM_DELTA_PIXEL:
|
||||||
dy = evt.deltaY
|
dy = evt.deltaY
|
||||||
elif evt.deltaMode is WheelEvent.DOM_DELTA_LINE:
|
elif evt.deltaMode is WheelEvent.DOM_DELTA_LINE:
|
||||||
dy = 15 * evt.deltaY
|
dy = line_height() * evt.deltaY
|
||||||
if evt.deltaMode is WheelEvent.DOM_DELTA_PAGE:
|
if evt.deltaMode is WheelEvent.DOM_DELTA_PAGE:
|
||||||
dy = (scroll_viewport.height() - 30) * evt.deltaY
|
dy = (scroll_viewport.height() - 30) * evt.deltaY
|
||||||
if evt.deltaX:
|
if evt.deltaX:
|
||||||
@ -110,24 +129,9 @@ def flow_onwheel(evt):
|
|||||||
elif Math.abs(dy) >= 1:
|
elif Math.abs(dy) >= 1:
|
||||||
scroll_by(dy)
|
scroll_by(dy)
|
||||||
|
|
||||||
smooth_y_data = {'last_event_at':0, 'up': False, 'timer':None, 'source':'wheel', 'pixels_per_ms': 0.2, 'scroll_interval':10, 'stop_scrolling_after':100}
|
|
||||||
|
|
||||||
def do_y_scroll():
|
|
||||||
dy = (-1 if smooth_y_data.up else 1) * smooth_y_data.pixels_per_ms * smooth_y_data.scroll_interval
|
|
||||||
if Math.abs(dy) >= 1 and scroll_by(dy):
|
|
||||||
if Date.now() - smooth_y_data.last_event_at < smooth_y_data.stop_scrolling_after:
|
|
||||||
smooth_y_data.timer = setTimeout(do_y_scroll, smooth_y_data.scroll_interval)
|
|
||||||
|
|
||||||
def smooth_y_scroll(up):
|
|
||||||
clearTimeout(smooth_y_data.timer)
|
|
||||||
smooth_y_data.last_event_at = Date.now()
|
|
||||||
smooth_y_data.up = up
|
|
||||||
do_y_scroll()
|
|
||||||
|
|
||||||
|
|
||||||
@check_for_scroll_end
|
@check_for_scroll_end
|
||||||
def goto_boundary(y):
|
def goto_boundary(dir):
|
||||||
scroll_viewport.scroll_to(window.pageXOffset, 0)
|
scroll_viewport.scroll_to(window.pageXOffset, 0 if dir is DIRECTION.Up else document_height())
|
||||||
get_boss().report_human_scroll()
|
get_boss().report_human_scroll()
|
||||||
|
|
||||||
|
|
||||||
@ -139,16 +143,16 @@ def scroll_by_page(direction):
|
|||||||
|
|
||||||
def handle_shortcut(sc_name, evt):
|
def handle_shortcut(sc_name, evt):
|
||||||
if sc_name is 'down':
|
if sc_name is 'down':
|
||||||
smooth_y_scroll(False)
|
scroll_animator.start(DIRECTION.Down, False)
|
||||||
return True
|
return True
|
||||||
if sc_name is 'up':
|
if sc_name is 'up':
|
||||||
smooth_y_scroll(True)
|
scroll_animator.start(DIRECTION.Up, False)
|
||||||
return True
|
return True
|
||||||
if sc_name is 'start_of_file':
|
if sc_name is 'start_of_file':
|
||||||
goto_boundary(-1)
|
goto_boundary(DIRECTION.Up)
|
||||||
return True
|
return True
|
||||||
if sc_name is 'end_of_file':
|
if sc_name is 'end_of_file':
|
||||||
goto_boundary(1)
|
goto_boundary(DIRECTION.Down)
|
||||||
return True
|
return True
|
||||||
if sc_name is 'left':
|
if sc_name is 'left':
|
||||||
window.scrollBy(-15, 0)
|
window.scrollBy(-15, 0)
|
||||||
@ -168,12 +172,154 @@ def handle_shortcut(sc_name, evt):
|
|||||||
if sc_name is 'pagedown':
|
if sc_name is 'pagedown':
|
||||||
scroll_by_page(1)
|
scroll_by_page(1)
|
||||||
return True
|
return True
|
||||||
|
if sc_name is 'toggle_autoscroll':
|
||||||
|
if scroll_animator.auto and scroll_animator.is_running():
|
||||||
|
cancel_scroll()
|
||||||
|
else:
|
||||||
|
scroll_animator.start(DIRECTION.Down, True)
|
||||||
|
return True
|
||||||
|
|
||||||
|
if sc_name.startsWith('scrollspeed_'):
|
||||||
|
scroll_animator.sync()
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def layout(is_single_page):
|
def layout(is_single_page):
|
||||||
|
line_height.ans = None
|
||||||
|
scroll_animator.wait = False
|
||||||
|
scroll_animator.sync()
|
||||||
set_css(document.body, margin='0', border_width='0', padding='0')
|
set_css(document.body, margin='0', border_width='0', padding='0')
|
||||||
|
|
||||||
|
# Pause auto-scroll while minimized
|
||||||
|
document.addEventListener("visibilitychange", def():
|
||||||
|
if (document.visibilityState is 'visible'):
|
||||||
|
scroll_animator.sync()
|
||||||
|
else:
|
||||||
|
scroll_animator.pause()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def cancel_scroll():
|
||||||
|
scroll_animator.stop()
|
||||||
|
|
||||||
|
|
||||||
|
def is_scroll_end(pos):
|
||||||
|
return not (0 <= pos <= document_height() - window.innerHeight)
|
||||||
|
|
||||||
|
DIRECTION = {'Up': -1, 'Down': 1}
|
||||||
|
|
||||||
|
|
||||||
|
class ScrollAnimator:
|
||||||
|
DURATION = 100 # milliseconds
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.animation_id = None
|
||||||
|
self.auto = False
|
||||||
|
|
||||||
|
def is_running(self):
|
||||||
|
return self.animation_id is not None
|
||||||
|
|
||||||
|
def start(self, direction, auto):
|
||||||
|
if self.wait:
|
||||||
|
return
|
||||||
|
|
||||||
|
now = window.performance.now()
|
||||||
|
self.end_time = now + self.DURATION
|
||||||
|
clearTimeout(self.auto_timer)
|
||||||
|
|
||||||
|
if not self.is_running() or direction is not self.direction or auto is not self.auto:
|
||||||
|
if self.auto and not auto:
|
||||||
|
self.pause()
|
||||||
|
self.stop()
|
||||||
|
self.auto = auto
|
||||||
|
self.direction = direction
|
||||||
|
self.start_time = now
|
||||||
|
self.start_offset = window.pageYOffset
|
||||||
|
self.csi_idx = current_spine_item().index
|
||||||
|
self.animation_id = window.requestAnimationFrame(self.auto_scroll if auto else self.smooth_scroll)
|
||||||
|
|
||||||
|
def smooth_scroll(self, ts):
|
||||||
|
duration = self.end_time - self.start_time
|
||||||
|
progress = max(0, min(1, (ts - self.start_time) / duration)) # max/min to account for jitter
|
||||||
|
scroll_target = self.start_offset
|
||||||
|
scroll_target += Math.trunc(self.direction * progress * duration * line_height() * opts.lines_per_sec_smooth) / 1000
|
||||||
|
|
||||||
|
window.scrollTo(0, scroll_target)
|
||||||
|
amt = window.pageYOffset - self.start_offset
|
||||||
|
|
||||||
|
if is_scroll_end(scroll_target) and (not opts.scroll_stop_boundaries or (abs(amt) < 3 and duration is self.DURATION)):
|
||||||
|
# "Turn the page" if stop at boundaries option is false or
|
||||||
|
# this is a new scroll action and we were already at the end
|
||||||
|
self.animation_id = None
|
||||||
|
self.wait = True
|
||||||
|
report_human_scroll(amt)
|
||||||
|
get_boss().send_message('next_spine_item', previous=self.direction is DIRECTION.Up)
|
||||||
|
elif progress < 1:
|
||||||
|
self.animation_id = window.requestAnimationFrame(self.smooth_scroll)
|
||||||
|
elif self.paused:
|
||||||
|
self.resume()
|
||||||
|
else:
|
||||||
|
self.animation_id = None
|
||||||
|
report_human_scroll(amt)
|
||||||
|
|
||||||
|
def auto_scroll(self, ts):
|
||||||
|
elapsed = max(0, ts - self.start_time) # max to account for jitter
|
||||||
|
scroll_target = self.start_offset
|
||||||
|
scroll_target += Math.trunc(self.direction * elapsed * line_height() * opts.lines_per_sec_auto) / 1000
|
||||||
|
|
||||||
|
window.scrollTo(0, scroll_target)
|
||||||
|
scroll_finished = is_scroll_end(scroll_target)
|
||||||
|
|
||||||
|
# report every second
|
||||||
|
if elapsed >= 1000:
|
||||||
|
self.sync(ts)
|
||||||
|
|
||||||
|
if scroll_finished:
|
||||||
|
self.pause()
|
||||||
|
if opts.scroll_auto_boundary_delay >= 0:
|
||||||
|
self.auto_timer = setTimeout(def():
|
||||||
|
get_boss().send_message('next_spine_item', previous=self.direction is DIRECTION.Up)
|
||||||
|
, opts.scroll_auto_boundary_delay * 1000)
|
||||||
|
else:
|
||||||
|
self.animation_id = window.requestAnimationFrame(self.auto_scroll)
|
||||||
|
|
||||||
|
def report(self):
|
||||||
|
amt = window.pageYOffset - self.start_offset
|
||||||
|
if abs(amt) > 0 and self.csi_idx is current_spine_item().index:
|
||||||
|
report_human_scroll(amt)
|
||||||
|
|
||||||
|
def sync(self, ts):
|
||||||
|
if self.auto:
|
||||||
|
self.report()
|
||||||
|
self.csi_idx = current_spine_item().index
|
||||||
|
self.start_time = ts or window.performance.now()
|
||||||
|
self.start_offset = window.pageYOffset
|
||||||
|
else:
|
||||||
|
self.resume()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.auto = False
|
||||||
|
if self.animation_id is not None:
|
||||||
|
window.cancelAnimationFrame(self.animation_id)
|
||||||
|
self.animation_id = None
|
||||||
|
self.report()
|
||||||
|
|
||||||
|
def pause(self):
|
||||||
|
if self.auto:
|
||||||
|
self.paused = self.direction
|
||||||
|
self.stop()
|
||||||
|
else:
|
||||||
|
self.paused = False
|
||||||
|
|
||||||
|
# Resume auto-scroll
|
||||||
|
def resume(self):
|
||||||
|
if self.paused:
|
||||||
|
self.start(self.paused, True)
|
||||||
|
self.paused = False
|
||||||
|
|
||||||
|
scroll_animator = ScrollAnimator()
|
||||||
|
|
||||||
|
|
||||||
class FlickAnimator:
|
class FlickAnimator:
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ from read_book.extract import get_elements
|
|||||||
from read_book.flow_mode import (
|
from read_book.flow_mode import (
|
||||||
anchor_funcs as flow_anchor_funcs, flow_onwheel, flow_to_scroll_fraction,
|
anchor_funcs as flow_anchor_funcs, flow_onwheel, flow_to_scroll_fraction,
|
||||||
handle_gesture as flow_handle_gesture, handle_shortcut as flow_handle_shortcut,
|
handle_gesture as flow_handle_gesture, handle_shortcut as flow_handle_shortcut,
|
||||||
layout as flow_layout, scroll_by_page as flow_scroll_by_page
|
layout as flow_layout, scroll_by_page as flow_scroll_by_page, cancel_scroll as flow_cancel_auto_scroll
|
||||||
)
|
)
|
||||||
from read_book.footnotes import is_footnote_link
|
from read_book.footnotes import is_footnote_link
|
||||||
from read_book.globals import (
|
from read_book.globals import (
|
||||||
@ -91,27 +91,33 @@ class IframeBoss:
|
|||||||
self.forward_keypresses = False
|
self.forward_keypresses = False
|
||||||
set_boss(self)
|
set_boss(self)
|
||||||
handlers = {
|
handlers = {
|
||||||
'initialize':self.initialize,
|
|
||||||
'display': self.display,
|
|
||||||
'scroll_to_anchor': self.on_scroll_to_anchor,
|
|
||||||
'scroll_to_ref': self.on_scroll_to_ref,
|
|
||||||
'scroll_to_frac': self.on_scroll_to_frac,
|
|
||||||
'next_screen': self.on_next_screen,
|
|
||||||
'change_font_size': self.change_font_size,
|
|
||||||
'change_color_scheme': self.change_color_scheme,
|
'change_color_scheme': self.change_color_scheme,
|
||||||
'gesture_from_margin': self.gesture_from_margin,
|
'change_font_size': self.change_font_size,
|
||||||
'wheel_from_margin': self.wheel_from_margin,
|
'change_scroll_speed': self.change_scroll_speed,
|
||||||
|
'display': self.display,
|
||||||
'find': self.find,
|
'find': self.find,
|
||||||
'window_size': self.received_window_size,
|
'gesture_from_margin': self.gesture_from_margin,
|
||||||
'get_current_cfi': self.get_current_cfi,
|
'get_current_cfi': self.get_current_cfi,
|
||||||
|
'initialize':self.initialize,
|
||||||
|
'modify_selection': self.modify_selection,
|
||||||
|
'next_screen': self.on_next_screen,
|
||||||
|
'scroll_to_anchor': self.on_scroll_to_anchor,
|
||||||
|
'scroll_to_frac': self.on_scroll_to_frac,
|
||||||
|
'scroll_to_ref': self.on_scroll_to_ref,
|
||||||
'set_forward_keypresses': self.set_forward_keypresses,
|
'set_forward_keypresses': self.set_forward_keypresses,
|
||||||
'set_reference_mode': self.set_reference_mode,
|
'set_reference_mode': self.set_reference_mode,
|
||||||
'modify_selection': self.modify_selection,
|
'wheel_from_margin': self.wheel_from_margin,
|
||||||
|
'window_size': self.received_window_size,
|
||||||
|
'overlay_shown': self.on_overlay_shown,
|
||||||
}
|
}
|
||||||
self.comm = IframeClient(handlers)
|
self.comm = IframeClient(handlers)
|
||||||
self.last_window_ypos = 0
|
self.last_window_ypos = 0
|
||||||
self.length_before = None
|
self.length_before = None
|
||||||
|
|
||||||
|
def on_overlay_shown(self):
|
||||||
|
if current_layout_mode() is 'flow':
|
||||||
|
flow_cancel_auto_scroll()
|
||||||
|
|
||||||
def modify_selection(self, data):
|
def modify_selection(self, data):
|
||||||
sel = window.getSelection()
|
sel = window.getSelection()
|
||||||
sel.modify('extend', data.direction, data.granularity)
|
sel.modify('extend', data.direction, data.granularity)
|
||||||
@ -243,6 +249,10 @@ class IframeBoss:
|
|||||||
opts.base_font_size = data.base_font_size
|
opts.base_font_size = data.base_font_size
|
||||||
apply_font_size()
|
apply_font_size()
|
||||||
|
|
||||||
|
def change_scroll_speed(self, data):
|
||||||
|
if data.lines_per_sec_auto?:
|
||||||
|
opts.lines_per_sec_auto = data.lines_per_sec_auto
|
||||||
|
|
||||||
def change_stylesheet(self, data):
|
def change_stylesheet(self, data):
|
||||||
opts.user_stylesheet = data.sheet or ''
|
opts.user_stylesheet = data.sheet or ''
|
||||||
apply_stylesheet()
|
apply_stylesheet()
|
||||||
|
@ -12,17 +12,39 @@ from session import defaults
|
|||||||
|
|
||||||
CONTAINER = unique_id('standalone-scrolling-settings')
|
CONTAINER = unique_id('standalone-scrolling-settings')
|
||||||
|
|
||||||
|
# Scroll speeds in lines/sec
|
||||||
|
MIN_SCROLL_SPEED_AUTO = 0.25
|
||||||
|
MAX_SCROLL_SPEED_AUTO = 5
|
||||||
|
|
||||||
|
MIN_SCROLL_AUTO_DELAY = -1
|
||||||
|
MAX_SCROLL_AUTO_DELAY = 10
|
||||||
|
|
||||||
|
MIN_SCROLL_SPEED_SMOOTH = 10
|
||||||
|
MAX_SCROLL_SPEED_SMOOTH = 50
|
||||||
|
|
||||||
def restore_defaults():
|
def restore_defaults():
|
||||||
container = get_container()
|
container = get_container()
|
||||||
for control in container.querySelectorAll('input[name]'):
|
for control in container.querySelectorAll('input[name]'):
|
||||||
control.checked = defaults[control.getAttribute('name')]
|
val = defaults[control.getAttribute('name')]
|
||||||
|
if control.type is 'checkbox':
|
||||||
|
control.checked = val
|
||||||
|
else:
|
||||||
|
control.valueAsNumber = val
|
||||||
|
|
||||||
|
|
||||||
def get_container():
|
def get_container():
|
||||||
return document.getElementById(CONTAINER)
|
return document.getElementById(CONTAINER)
|
||||||
|
|
||||||
|
|
||||||
|
def change_scroll_speed(amt):
|
||||||
|
sd = get_session_data()
|
||||||
|
lps = sd.get('lines_per_sec_auto')
|
||||||
|
nlps = max(MIN_SCROLL_SPEED_AUTO, min(lps + amt, MAX_SCROLL_SPEED_AUTO))
|
||||||
|
if nlps != lps:
|
||||||
|
sd.set('lines_per_sec_auto', nlps)
|
||||||
|
return nlps
|
||||||
|
|
||||||
|
|
||||||
def create_scrolling_panel(container, apply_func, cancel_func):
|
def create_scrolling_panel(container, apply_func, cancel_func):
|
||||||
container.appendChild(E.div(id=CONTAINER, style='margin: 1rem'))
|
container.appendChild(E.div(id=CONTAINER, style='margin: 1rem'))
|
||||||
container = container.lastChild
|
container = container.lastChild
|
||||||
@ -34,16 +56,54 @@ def create_scrolling_panel(container, apply_func, cancel_func):
|
|||||||
ans.checked = True
|
ans.checked = True
|
||||||
return E.div(style='margin-top:1ex', E.label(ans, '\xa0' + text))
|
return E.div(style='margin-top:1ex', E.label(ans, '\xa0' + text))
|
||||||
|
|
||||||
container.appendChild(E.div(style='margin-top:1ex', _(
|
def spinner(name, text, **kwargs):
|
||||||
'Control how mouse based scrolling works in paged mode')))
|
ans = E.input(type='number', name=name, id=name)
|
||||||
|
for key, val in Object.entries(kwargs):
|
||||||
|
ans[key] = val
|
||||||
|
ans.valueAsNumber = sd.get(name, defaults[name])
|
||||||
|
return E.label("for"=name, text), ans
|
||||||
|
|
||||||
|
container.appendChild(E.div(style='margin-top:1ex', _('Control how mouse based scrolling works in paged mode')))
|
||||||
container.appendChild(cb(
|
container.appendChild(cb(
|
||||||
'paged_wheel_scrolls_by_screen', _('Mouse wheel scrolls by screen fulls instead of pages')))
|
'paged_wheel_scrolls_by_screen', _('Mouse wheel scrolls by screen fulls instead of pages')))
|
||||||
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')))
|
||||||
|
|
||||||
container.appendChild(E.div(style='margin-top:1ex; border-top: solid 1px', '\xa0'))
|
container.appendChild(E.hr())
|
||||||
|
container.appendChild(E.div(style='margin-top:1ex', _('Control how smooth scrolling works in flow mode')))
|
||||||
container.appendChild(cb(
|
container.appendChild(cb(
|
||||||
'book_scrollbar', _('Show a scrollbar')))
|
'scroll_stop_boundaries',
|
||||||
|
_('Stop at internal file boundaries when smooth scrolling by holding down the scroll key')
|
||||||
|
))
|
||||||
|
container.appendChild(
|
||||||
|
E.div(style='display:grid;margin-top:1ex;align-items:center;grid-template-columns:25em min-content;grid-gap:1ex',
|
||||||
|
*spinner(
|
||||||
|
'lines_per_sec_smooth',
|
||||||
|
_('Smooth scrolling speed in lines/sec'),
|
||||||
|
step=5,
|
||||||
|
min=MIN_SCROLL_SPEED_SMOOTH,
|
||||||
|
max=MAX_SCROLL_SPEED_SMOOTH
|
||||||
|
),
|
||||||
|
*spinner(
|
||||||
|
'lines_per_sec_auto',
|
||||||
|
_('Auto scrolling speed in lines/sec'),
|
||||||
|
step=MIN_SCROLL_SPEED_AUTO,
|
||||||
|
min=MIN_SCROLL_SPEED_AUTO,
|
||||||
|
max=MAX_SCROLL_SPEED_AUTO
|
||||||
|
),
|
||||||
|
*spinner(
|
||||||
|
'scroll_auto_boundary_delay',
|
||||||
|
_('Seconds to pause before auto-scrolling past internal file boundaries'),
|
||||||
|
title=_('Use negative values to not auto-scroll past internal file boundaries'),
|
||||||
|
step=0.25,
|
||||||
|
min=MIN_SCROLL_AUTO_DELAY,
|
||||||
|
max=MAX_SCROLL_AUTO_DELAY
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
container.appendChild(E.hr())
|
||||||
|
container.appendChild(cb('book_scrollbar', _('Show a scrollbar')))
|
||||||
|
|
||||||
container.appendChild(create_button_box(restore_defaults, apply_func, cancel_func))
|
container.appendChild(create_button_box(restore_defaults, apply_func, cancel_func))
|
||||||
|
|
||||||
@ -57,8 +117,8 @@ def commit_scrolling(onchange):
|
|||||||
changed = False
|
changed = False
|
||||||
for control in container.querySelectorAll('input[name]'):
|
for control in container.querySelectorAll('input[name]'):
|
||||||
name = control.getAttribute('name')
|
name = control.getAttribute('name')
|
||||||
val = control.checked
|
val = control.checked if control.type is 'checkbox' else control.valueAsNumber
|
||||||
if val is not sd.get(name):
|
if val is not sd.get(name) and control.validity.valid:
|
||||||
sd.set(name, val)
|
sd.set(name, val)
|
||||||
changed = True
|
changed = True
|
||||||
if changed:
|
if changed:
|
||||||
|
@ -4,23 +4,28 @@ from __python__ import hash_literals
|
|||||||
|
|
||||||
from elementmaker import E
|
from elementmaker import E
|
||||||
from read_book.globals import runtime
|
from read_book.globals import runtime
|
||||||
|
from session import defaults
|
||||||
|
|
||||||
opts = {}
|
opts = {}
|
||||||
|
|
||||||
def update_settings(settings):
|
def update_settings(settings):
|
||||||
settings = settings or {}
|
settings = Object.assign({}, defaults, settings)
|
||||||
opts.columns_per_screen = settings.columns_per_screen or {'portrait':0, 'landscape':0}
|
opts.base_font_size = max(8, min(settings.base_font_size, 64))
|
||||||
opts.margin_left = max(0, settings.margin_left or 0)
|
|
||||||
opts.margin_right = max(0, settings.margin_right or 0)
|
|
||||||
opts.color_scheme = settings.color_scheme
|
|
||||||
opts.base_font_size = max(8, min(settings.base_font_size or 16, 64))
|
|
||||||
opts.user_stylesheet = settings.user_stylesheet or ''
|
|
||||||
opts.hide_tooltips = settings.hide_tooltips
|
|
||||||
opts.cover_preserve_aspect_ratio = v'!!settings.cover_preserve_aspect_ratio'
|
|
||||||
opts.bg_image_fade = settings.bg_image_fade or 'transparent'
|
opts.bg_image_fade = settings.bg_image_fade or 'transparent'
|
||||||
opts.paged_wheel_scrolls_by_screen = v'!!settings.paged_wheel_scrolls_by_screen'
|
opts.color_scheme = settings.color_scheme
|
||||||
|
opts.columns_per_screen = settings.columns_per_screen
|
||||||
|
opts.cover_preserve_aspect_ratio = v'!!settings.cover_preserve_aspect_ratio'
|
||||||
|
opts.hide_tooltips = settings.hide_tooltips
|
||||||
opts.is_dark_theme = v'!!settings.is_dark_theme'
|
opts.is_dark_theme = v'!!settings.is_dark_theme'
|
||||||
opts.override_book_colors = settings.override_book_colors or 'never'
|
opts.lines_per_sec_auto = settings.lines_per_sec_auto
|
||||||
|
opts.lines_per_sec_smooth = settings.lines_per_sec_smooth
|
||||||
|
opts.margin_left = max(0, settings.margin_left)
|
||||||
|
opts.margin_right = max(0, settings.margin_right)
|
||||||
|
opts.override_book_colors = settings.override_book_colors
|
||||||
|
opts.paged_wheel_scrolls_by_screen = v'!!settings.paged_wheel_scrolls_by_screen'
|
||||||
|
opts.scroll_auto_boundary_delay = settings.scroll_auto_boundary_delay
|
||||||
|
opts.scroll_stop_boundaries = v'!!settings.scroll_stop_boundaries'
|
||||||
|
opts.user_stylesheet = settings.user_stylesheet
|
||||||
|
|
||||||
update_settings()
|
update_settings()
|
||||||
|
|
||||||
|
@ -270,6 +270,24 @@ def shortcuts_definition():
|
|||||||
_('Go to a specified book location or position'),
|
_('Go to a specified book location or position'),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
'toggle_autoscroll': desc(
|
||||||
|
"Ctrl+ ",
|
||||||
|
'scroll',
|
||||||
|
_('Toggle auto-scroll'),
|
||||||
|
),
|
||||||
|
|
||||||
|
'scrollspeed_increase': desc(
|
||||||
|
"Alt+ArrowUp",
|
||||||
|
'scroll',
|
||||||
|
_('Auto scroll faster'),
|
||||||
|
),
|
||||||
|
|
||||||
|
'scrollspeed_decrease': desc(
|
||||||
|
"Alt+ArrowDown",
|
||||||
|
'scroll',
|
||||||
|
_('Auto scroll slower'),
|
||||||
|
),
|
||||||
|
|
||||||
}
|
}
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@ from read_book.overlay import Overlay
|
|||||||
from read_book.prefs.colors import resolve_color_scheme
|
from read_book.prefs.colors import resolve_color_scheme
|
||||||
from read_book.prefs.font_size import change_font_size_by
|
from read_book.prefs.font_size import change_font_size_by
|
||||||
from read_book.prefs.head_foot import render_head_foot
|
from read_book.prefs.head_foot import render_head_foot
|
||||||
|
from read_book.prefs.scrolling import change_scroll_speed, MIN_SCROLL_SPEED_AUTO as SCROLL_SPEED_STEP
|
||||||
from read_book.resources import load_resources
|
from read_book.resources import load_resources
|
||||||
from read_book.search import SearchOverlay, find_in_spine
|
from read_book.search import SearchOverlay, find_in_spine
|
||||||
from read_book.shortcuts import create_shortcut_map
|
from read_book.shortcuts import create_shortcut_map
|
||||||
@ -133,7 +134,7 @@ def margin_elem(sd, which, id, onclick, oncontextmenu):
|
|||||||
if onclick:
|
if onclick:
|
||||||
ans.addEventListener('click', onclick)
|
ans.addEventListener('click', onclick)
|
||||||
if oncontextmenu:
|
if oncontextmenu:
|
||||||
ans.addEventListener('contextmenu', onclick)
|
ans.addEventListener('contextmenu', oncontextmenu)
|
||||||
if is_ios and which is 'margin_bottom' and not window.navigator.standalone and not /CriOS\//.test(window.navigator.userAgent):
|
if is_ios and which is 'margin_bottom' and not window.navigator.standalone and not /CriOS\//.test(window.navigator.userAgent):
|
||||||
# On iOS Safari 100vh includes the size of the navbar and there is no way to
|
# On iOS Safari 100vh includes the size of the navbar and there is no way to
|
||||||
# go fullscreen, so to make the bottom bar visible we add a margin to
|
# go fullscreen, so to make the bottom bar visible we add a margin to
|
||||||
@ -200,29 +201,29 @@ class View:
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
handlers = {
|
handlers = {
|
||||||
'ready': self.on_iframe_ready,
|
'bump_font_size': self.bump_font_size,
|
||||||
|
'content_loaded': self.on_content_loaded,
|
||||||
'error': self.on_iframe_error,
|
'error': self.on_iframe_error,
|
||||||
'next_spine_item': self.on_next_spine_item,
|
'find_in_spine': self.on_find_in_spine,
|
||||||
'next_section': self.on_next_section,
|
|
||||||
'lookup_word': self.on_lookup_word,
|
|
||||||
'goto_doc_boundary': def(data): self.goto_doc_boundary(data.start);,
|
'goto_doc_boundary': def(data): self.goto_doc_boundary(data.start);,
|
||||||
|
'handle_keypress': self.on_handle_keypress,
|
||||||
|
'handle_shortcut': self.on_handle_shortcut,
|
||||||
|
'human_scroll': self.on_human_scroll,
|
||||||
|
'lookup_word': self.on_lookup_word,
|
||||||
|
'next_section': self.on_next_section,
|
||||||
|
'next_spine_item': self.on_next_spine_item,
|
||||||
|
'print': self.on_print,
|
||||||
|
'ready': self.on_iframe_ready,
|
||||||
|
'reference_item_changed': self.on_reference_item_changed,
|
||||||
|
'report_cfi': self.on_report_cfi,
|
||||||
|
'request_size': self.on_request_size,
|
||||||
'scroll_to_anchor': self.on_scroll_to_anchor,
|
'scroll_to_anchor': self.on_scroll_to_anchor,
|
||||||
|
'selectionchange': self.on_selection_change,
|
||||||
|
'show_chrome': self.show_chrome,
|
||||||
|
'show_footnote': self.on_show_footnote,
|
||||||
'update_cfi': self.on_update_cfi,
|
'update_cfi': self.on_update_cfi,
|
||||||
'update_progress_frac': self.on_update_progress_frac,
|
'update_progress_frac': self.on_update_progress_frac,
|
||||||
'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,
|
|
||||||
'show_chrome': self.show_chrome,
|
|
||||||
'bump_font_size': self.bump_font_size,
|
|
||||||
'find_in_spine': self.on_find_in_spine,
|
|
||||||
'request_size': self.on_request_size,
|
|
||||||
'show_footnote': self.on_show_footnote,
|
|
||||||
'print': self.on_print,
|
|
||||||
'human_scroll': self.on_human_scroll,
|
|
||||||
'selectionchange': self.on_selection_change,
|
|
||||||
'handle_shortcut': self.on_handle_shortcut,
|
|
||||||
'handle_keypress': self.on_handle_keypress,
|
|
||||||
'reference_item_changed': self.on_reference_item_changed,
|
|
||||||
}
|
}
|
||||||
entry_point = None if runtime.is_standalone_viewer else 'read_book.iframe'
|
entry_point = None if runtime.is_standalone_viewer else 'read_book.iframe'
|
||||||
if runtime.is_standalone_viewer:
|
if runtime.is_standalone_viewer:
|
||||||
@ -338,6 +339,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:
|
||||||
|
if visible:
|
||||||
|
self.iframe_wrapper.send_message('overlay_shown')
|
||||||
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:
|
if ui_operations.overlay_visibility_changed:
|
||||||
ui_operations.overlay_visibility_changed(visible)
|
ui_operations.overlay_visibility_changed(visible)
|
||||||
@ -409,6 +412,10 @@ class View:
|
|||||||
self.iframe_wrapper.send_message('modify_selection', direction='backward', granularity='word')
|
self.iframe_wrapper.send_message('modify_selection', direction='backward', granularity='word')
|
||||||
elif data.name is 'extend_selection_by_word':
|
elif data.name is 'extend_selection_by_word':
|
||||||
self.iframe_wrapper.send_message('modify_selection', direction='forward', granularity='word')
|
self.iframe_wrapper.send_message('modify_selection', direction='forward', granularity='word')
|
||||||
|
elif data.name is 'scrollspeed_increase':
|
||||||
|
self.update_scroll_speed(SCROLL_SPEED_STEP)
|
||||||
|
elif data.name is 'scrollspeed_decrease':
|
||||||
|
self.update_scroll_speed(-SCROLL_SPEED_STEP)
|
||||||
|
|
||||||
def on_selection_change(self, data):
|
def on_selection_change(self, data):
|
||||||
self.currently_showing.selected_text = data.text
|
self.currently_showing.selected_text = data.text
|
||||||
@ -703,6 +710,10 @@ class View:
|
|||||||
'hide_tooltips': sd.get('hide_tooltips'),
|
'hide_tooltips': sd.get('hide_tooltips'),
|
||||||
'cover_preserve_aspect_ratio': sd.get('cover_preserve_aspect_ratio'),
|
'cover_preserve_aspect_ratio': sd.get('cover_preserve_aspect_ratio'),
|
||||||
'paged_wheel_scrolls_by_screen': sd.get('paged_wheel_scrolls_by_screen'),
|
'paged_wheel_scrolls_by_screen': sd.get('paged_wheel_scrolls_by_screen'),
|
||||||
|
'lines_per_sec_auto': sd.get('lines_per_sec_auto'),
|
||||||
|
'lines_per_sec_smooth': sd.get('lines_per_sec_smooth'),
|
||||||
|
'scroll_auto_boundary_delay': sd.get('scroll_auto_boundary_delay'),
|
||||||
|
'scroll_stop_boundaries': sd.get('scroll_stop_boundaries'),
|
||||||
}
|
}
|
||||||
|
|
||||||
def show_name(self, name, initial_position=None):
|
def show_name(self, name, initial_position=None):
|
||||||
@ -993,6 +1004,9 @@ class View:
|
|||||||
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'))
|
||||||
|
|
||||||
|
def update_scroll_speed(self, amt):
|
||||||
|
self.iframe_wrapper.send_message('change_scroll_speed', lines_per_sec_auto=change_scroll_speed(amt))
|
||||||
|
|
||||||
def update_color_scheme(self):
|
def update_color_scheme(self):
|
||||||
cs = self.get_color_scheme(True)
|
cs = self.get_color_scheme(True)
|
||||||
self.iframe_wrapper.send_message('change_color_scheme', color_scheme=cs)
|
self.iframe_wrapper.send_message('change_color_scheme', color_scheme=cs)
|
||||||
|
@ -9,72 +9,80 @@ from ajax import ajax_send
|
|||||||
|
|
||||||
defaults = {
|
defaults = {
|
||||||
# Book list settings
|
# Book list settings
|
||||||
'view_mode': 'cover_grid',
|
'copy_to_library_dupes': 'add;overwrite',
|
||||||
'sort': 'timestamp.desc', # comma separated list of items of the form: field.order
|
|
||||||
'last_sort_order': {},
|
'last_sort_order': {},
|
||||||
'show_all_metadata': False, # show all metadata fields in the book details panel
|
'show_all_metadata': False, # show all metadata fields in the book details panel
|
||||||
'copy_to_library_dupes': 'add;overwrite',
|
'sort': 'timestamp.desc', # comma separated list of items of the form: field.order
|
||||||
|
'view_mode': 'cover_grid',
|
||||||
|
|
||||||
# Tag Browser settings
|
# Tag Browser settings
|
||||||
'partition_method': 'first letter', # other choices: 'disable', 'partition'
|
'and_search_terms': False, # how to add search terms to the search expression from the Tag Browser
|
||||||
'collapse_at': 25, # number of items at which sub-groups are created, 0 to disable
|
'collapse_at': 25, # number of items at which sub-groups are created, 0 to disable
|
||||||
'dont_collapse': '', # comma separated list of category names
|
'dont_collapse': '', # comma separated list of category names
|
||||||
'sort_tags_by': 'name', # other choices: popularity, rating
|
|
||||||
'hide_empty_categories': 'no',
|
'hide_empty_categories': 'no',
|
||||||
'and_search_terms': False, # how to add search terms to the search expression from the Tag Browser
|
'partition_method': 'first letter', # other choices: 'disable', 'partition'
|
||||||
|
'sort_tags_by': 'name', # other choices: popularity, rating
|
||||||
|
|
||||||
# Book reader settings
|
# Book reader settings
|
||||||
'margin_right': 20,
|
|
||||||
'margin_left': 20,
|
|
||||||
'margin_top': 20,
|
|
||||||
'margin_bottom': 20,
|
|
||||||
'read_mode': 'paged',
|
|
||||||
'cover_preserve_aspect_ratio': True,
|
|
||||||
'max_text_height': 0,
|
|
||||||
'max_text_width': 0,
|
|
||||||
'columns_per_screen': {'portrait':0, 'landscape':0},
|
|
||||||
'user_stylesheet': '',
|
|
||||||
'background_image': None,
|
|
||||||
'background_image_style': 'scaled',
|
|
||||||
'background_image_fade': 0,
|
'background_image_fade': 0,
|
||||||
'current_color_scheme': 'system',
|
'background_image_style': 'scaled',
|
||||||
'user_color_schemes': {},
|
'background_image': None,
|
||||||
'override_book_colors': 'never',
|
|
||||||
'base_font_size': 16,
|
'base_font_size': 16,
|
||||||
|
'book_scrollbar': False,
|
||||||
|
'columns_per_screen': {'portrait':0, 'landscape':0},
|
||||||
'controls_help_shown_count': 0,
|
'controls_help_shown_count': 0,
|
||||||
'header': {},
|
'cover_preserve_aspect_ratio': True,
|
||||||
|
'current_color_scheme': 'system',
|
||||||
'footer': {'right': 'progress'},
|
'footer': {'right': 'progress'},
|
||||||
'word_actions': v'[]',
|
'header': {},
|
||||||
'hide_tooltips': False,
|
'hide_tooltips': False,
|
||||||
'keyboard_shortcuts': {},
|
'keyboard_shortcuts': {},
|
||||||
'book_scrollbar': False,
|
'lines_per_sec_auto': 1,
|
||||||
|
'lines_per_sec_smooth': 20,
|
||||||
|
'margin_bottom': 20,
|
||||||
|
'margin_left': 20,
|
||||||
|
'margin_right': 20,
|
||||||
|
'margin_top': 20,
|
||||||
|
'max_text_height': 0,
|
||||||
|
'max_text_width': 0,
|
||||||
|
'override_book_colors': 'never',
|
||||||
|
'paged_margin_clicks_scroll_by_screen': True,
|
||||||
|
'paged_wheel_scrolls_by_screen': False,
|
||||||
|
'read_mode': 'paged',
|
||||||
|
'scroll_auto_boundary_delay': 5,
|
||||||
|
'scroll_stop_boundaries': False,
|
||||||
'standalone_font_settings': {},
|
'standalone_font_settings': {},
|
||||||
'standalone_misc_settings': {},
|
'standalone_misc_settings': {},
|
||||||
'standalone_recently_opened': v'[]',
|
'standalone_recently_opened': v'[]',
|
||||||
'paged_wheel_scrolls_by_screen': False,
|
'user_color_schemes': {},
|
||||||
'paged_margin_clicks_scroll_by_screen': True,
|
'user_stylesheet': '',
|
||||||
|
'word_actions': v'[]',
|
||||||
}
|
}
|
||||||
|
|
||||||
is_local_setting = {
|
is_local_setting = {
|
||||||
'margin_right': True,
|
'background_image_fade': True,
|
||||||
'margin_left': True,
|
'background_image_style': True,
|
||||||
'margin_top': True,
|
'background_image': True,
|
||||||
|
'base_font_size': True,
|
||||||
|
'columns_per_screen': True,
|
||||||
|
'controls_help_shown_count': True,
|
||||||
|
'current_color_scheme': True,
|
||||||
|
'lines_per_sec_auto': True,
|
||||||
|
'lines_per_sec_smooth': True,
|
||||||
'margin_bottom': True,
|
'margin_bottom': True,
|
||||||
'read_mode': 'paged',
|
'margin_left': True,
|
||||||
|
'margin_right': True,
|
||||||
|
'margin_top': True,
|
||||||
'max_text_height': True,
|
'max_text_height': True,
|
||||||
'max_text_width': True,
|
'max_text_width': True,
|
||||||
'columns_per_screen': True,
|
|
||||||
'user_stylesheet': True,
|
|
||||||
'background_image': True,
|
|
||||||
'background_image_style': True,
|
|
||||||
'background_image_fade': True,
|
|
||||||
'current_color_scheme': True,
|
|
||||||
'override_book_colors': True,
|
'override_book_colors': True,
|
||||||
'base_font_size': True,
|
'read_mode': 'paged',
|
||||||
'controls_help_shown_count': True,
|
'scroll_auto_boundary_delay': True,
|
||||||
|
'scroll_stop_boundaries': True,
|
||||||
'standalone_font_settings': True,
|
'standalone_font_settings': True,
|
||||||
'standalone_misc_settings': True,
|
'standalone_misc_settings': True,
|
||||||
'standalone_recently_opened': True,
|
'standalone_recently_opened': True,
|
||||||
|
'user_stylesheet': True,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user