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:
Kovid Goyal 2019-12-28 14:13:47 +05:30
commit fc5f471682
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
7 changed files with 373 additions and 112 deletions

View File

@ -6,10 +6,26 @@ from select import word_at_point
from dom import set_css
from read_book.globals import current_spine_item, get_boss
from read_book.settings import opts
from read_book.viewport import scroll_viewport
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):
scroll_viewport.scroll_to(0, document_height() * frac)
@ -41,15 +57,15 @@ def add_small_scroll(amt):
def report_human_scroll(amt):
h = scroll_viewport.height()
is_large_scroll = (abs(amt) / h) >= 0.5
if amt > 0:
h = scroll_viewport.height()
is_large_scroll = (amt / h) >= 0.5
if is_large_scroll:
clear_small_scrolls()
get_boss().report_human_scroll(amt / document_height())
else:
add_small_scroll(amt)
else:
elif amt is 0 or is_large_scroll:
clear_small_scrolls()
@ -59,8 +75,11 @@ last_change_spine_item_request = {}
def _check_for_scroll_end(func, obj, args, report):
before = window.pageYOffset
func.apply(obj, args)
now = window.performance.now()
scroll_animator.sync(now)
if window.pageYOffset is before:
now = Date.now()
csi = current_spine_item()
if last_change_spine_item_request.name is csi.name and now - last_change_spine_item_request.at < 2000:
return False
@ -95,7 +114,7 @@ def flow_onwheel(evt):
if evt.deltaMode is WheelEvent.DOM_DELTA_PIXEL:
dy = evt.deltaY
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:
dy = (scroll_viewport.height() - 30) * evt.deltaY
if evt.deltaX:
@ -110,24 +129,9 @@ def flow_onwheel(evt):
elif Math.abs(dy) >= 1:
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
def goto_boundary(y):
scroll_viewport.scroll_to(window.pageXOffset, 0)
def goto_boundary(dir):
scroll_viewport.scroll_to(window.pageXOffset, 0 if dir is DIRECTION.Up else document_height())
get_boss().report_human_scroll()
@ -139,16 +143,16 @@ def scroll_by_page(direction):
def handle_shortcut(sc_name, evt):
if sc_name is 'down':
smooth_y_scroll(False)
scroll_animator.start(DIRECTION.Down, False)
return True
if sc_name is 'up':
smooth_y_scroll(True)
scroll_animator.start(DIRECTION.Up, False)
return True
if sc_name is 'start_of_file':
goto_boundary(-1)
goto_boundary(DIRECTION.Up)
return True
if sc_name is 'end_of_file':
goto_boundary(1)
goto_boundary(DIRECTION.Down)
return True
if sc_name is 'left':
window.scrollBy(-15, 0)
@ -168,12 +172,154 @@ def handle_shortcut(sc_name, evt):
if sc_name is 'pagedown':
scroll_by_page(1)
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
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')
# 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:

View File

@ -12,7 +12,7 @@ from read_book.extract import get_elements
from read_book.flow_mode import (
anchor_funcs as flow_anchor_funcs, flow_onwheel, flow_to_scroll_fraction,
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.globals import (
@ -91,27 +91,33 @@ class IframeBoss:
self.forward_keypresses = False
set_boss(self)
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,
'gesture_from_margin': self.gesture_from_margin,
'wheel_from_margin': self.wheel_from_margin,
'change_font_size': self.change_font_size,
'change_scroll_speed': self.change_scroll_speed,
'display': self.display,
'find': self.find,
'window_size': self.received_window_size,
'gesture_from_margin': self.gesture_from_margin,
'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_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.last_window_ypos = 0
self.length_before = None
def on_overlay_shown(self):
if current_layout_mode() is 'flow':
flow_cancel_auto_scroll()
def modify_selection(self, data):
sel = window.getSelection()
sel.modify('extend', data.direction, data.granularity)
@ -243,6 +249,10 @@ class IframeBoss:
opts.base_font_size = data.base_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):
opts.user_stylesheet = data.sheet or ''
apply_stylesheet()

View File

@ -12,17 +12,39 @@ from session import defaults
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():
container = get_container()
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():
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):
container.appendChild(E.div(id=CONTAINER, style='margin: 1rem'))
container = container.lastChild
@ -34,16 +56,54 @@ def create_scrolling_panel(container, apply_func, cancel_func):
ans.checked = True
return E.div(style='margin-top:1ex', E.label(ans, '\xa0' + text))
container.appendChild(E.div(style='margin-top:1ex', _(
'Control how mouse based scrolling works in paged mode')))
def spinner(name, text, **kwargs):
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(
'paged_wheel_scrolls_by_screen', _('Mouse wheel scrolls by screen fulls instead of pages')))
container.appendChild(cb(
'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(
'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))
@ -57,8 +117,8 @@ def commit_scrolling(onchange):
changed = False
for control in container.querySelectorAll('input[name]'):
name = control.getAttribute('name')
val = control.checked
if val is not sd.get(name):
val = control.checked if control.type is 'checkbox' else control.valueAsNumber
if val is not sd.get(name) and control.validity.valid:
sd.set(name, val)
changed = True
if changed:

View File

@ -4,23 +4,28 @@ from __python__ import hash_literals
from elementmaker import E
from read_book.globals import runtime
from session import defaults
opts = {}
def update_settings(settings):
settings = settings or {}
opts.columns_per_screen = settings.columns_per_screen or {'portrait':0, 'landscape':0}
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'
settings = Object.assign({}, defaults, settings)
opts.base_font_size = max(8, min(settings.base_font_size, 64))
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.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()

View File

@ -270,6 +270,24 @@ def shortcuts_definition():
_('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

View File

@ -23,6 +23,7 @@ from read_book.overlay import Overlay
from read_book.prefs.colors import resolve_color_scheme
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.scrolling import change_scroll_speed, MIN_SCROLL_SPEED_AUTO as SCROLL_SPEED_STEP
from read_book.resources import load_resources
from read_book.search import SearchOverlay, find_in_spine
from read_book.shortcuts import create_shortcut_map
@ -133,7 +134,7 @@ def margin_elem(sd, which, id, onclick, oncontextmenu):
if onclick:
ans.addEventListener('click', onclick)
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):
# 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
@ -200,29 +201,29 @@ class View:
),
)
handlers = {
'ready': self.on_iframe_ready,
'bump_font_size': self.bump_font_size,
'content_loaded': self.on_content_loaded,
'error': self.on_iframe_error,
'next_spine_item': self.on_next_spine_item,
'next_section': self.on_next_section,
'lookup_word': self.on_lookup_word,
'find_in_spine': self.on_find_in_spine,
'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,
'selectionchange': self.on_selection_change,
'show_chrome': self.show_chrome,
'show_footnote': self.on_show_footnote,
'update_cfi': self.on_update_cfi,
'update_progress_frac': self.on_update_progress_frac,
'report_cfi': self.on_report_cfi,
'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'
if runtime.is_standalone_viewer:
@ -338,6 +339,8 @@ class View:
def overlay_visibility_changed(self, visible):
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')
if ui_operations.overlay_visibility_changed:
ui_operations.overlay_visibility_changed(visible)
@ -409,6 +412,10 @@ class View:
self.iframe_wrapper.send_message('modify_selection', direction='backward', granularity='word')
elif data.name is 'extend_selection_by_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):
self.currently_showing.selected_text = data.text
@ -703,6 +710,10 @@ class View:
'hide_tooltips': sd.get('hide_tooltips'),
'cover_preserve_aspect_ratio': sd.get('cover_preserve_aspect_ratio'),
'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):
@ -993,6 +1004,9 @@ class View:
def update_font_size(self):
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):
cs = self.get_color_scheme(True)
self.iframe_wrapper.send_message('change_color_scheme', color_scheme=cs)

View File

@ -9,72 +9,80 @@ from ajax import ajax_send
defaults = {
# Book list settings
'view_mode': 'cover_grid',
'sort': 'timestamp.desc', # comma separated list of items of the form: field.order
'copy_to_library_dupes': 'add;overwrite',
'last_sort_order': {},
'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
'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
'dont_collapse': '', # comma separated list of category names
'sort_tags_by': 'name', # other choices: popularity, rating
'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
'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,
'current_color_scheme': 'system',
'user_color_schemes': {},
'override_book_colors': 'never',
'background_image_style': 'scaled',
'background_image': None,
'base_font_size': 16,
'book_scrollbar': False,
'columns_per_screen': {'portrait':0, 'landscape':0},
'controls_help_shown_count': 0,
'header': {},
'cover_preserve_aspect_ratio': True,
'current_color_scheme': 'system',
'footer': {'right': 'progress'},
'word_actions': v'[]',
'header': {},
'hide_tooltips': False,
'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_misc_settings': {},
'standalone_recently_opened': v'[]',
'paged_wheel_scrolls_by_screen': False,
'paged_margin_clicks_scroll_by_screen': True,
'user_color_schemes': {},
'user_stylesheet': '',
'word_actions': v'[]',
}
is_local_setting = {
'margin_right': True,
'margin_left': True,
'margin_top': True,
'background_image_fade': True,
'background_image_style': 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,
'read_mode': 'paged',
'margin_left': True,
'margin_right': True,
'margin_top': True,
'max_text_height': 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,
'base_font_size': True,
'controls_help_shown_count': True,
'read_mode': 'paged',
'scroll_auto_boundary_delay': True,
'scroll_stop_boundaries': True,
'standalone_font_settings': True,
'standalone_misc_settings': True,
'standalone_recently_opened': True,
'user_stylesheet': True,
}