mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-12-04 20:25:01 -05:00
831 lines
35 KiB
Plaintext
831 lines
35 KiB
Plaintext
# vim:fileencoding=utf-8
|
|
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
|
from __python__ import bound_methods, hash_literals
|
|
|
|
import traceback
|
|
from gettext import gettext as _
|
|
from select import selection_extents, set_selections_extents_to, word_at_point
|
|
|
|
from fs_images import fix_fullscreen_svg_images
|
|
from iframe_comm import IframeClient
|
|
from range_utils import (
|
|
highlight_associated_with_selection, reset_highlight_counter, select_crw,
|
|
unwrap_all_crw, unwrap_crw, wrap_text_in_range
|
|
)
|
|
from read_book.cfi import (
|
|
cfi_for_selection, range_from_cfi, scroll_to as scroll_to_cfi
|
|
)
|
|
from read_book.extract import get_elements
|
|
from read_book.find import reset_find_caches, select_search_result
|
|
from read_book.flow_mode import (
|
|
anchor_funcs as flow_anchor_funcs, auto_scroll_action as flow_auto_scroll_action,
|
|
cancel_drag_scroll as cancel_drag_scroll_flow, ensure_selection_visible,
|
|
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,
|
|
scroll_to_extend_annotation as flow_annotation_scroll,
|
|
start_drag_scroll as start_drag_scroll_flow
|
|
)
|
|
from read_book.footnotes import is_footnote_link
|
|
from read_book.globals import (
|
|
annot_id_uuid_map, clear_annot_id_uuid_map, current_book, current_layout_mode,
|
|
current_spine_item, runtime, set_boss, set_current_spine_item, set_layout_mode,
|
|
set_toc_anchor_map
|
|
)
|
|
from read_book.highlights import highlight_style_as_css
|
|
from read_book.mathjax import apply_mathjax
|
|
from read_book.paged_mode import (
|
|
anchor_funcs as paged_anchor_funcs,
|
|
auto_scroll_action as paged_auto_scroll_action, calc_columns_per_screen,
|
|
cancel_drag_scroll as cancel_drag_scroll_paged, current_cfi,
|
|
get_columns_per_screen_data, handle_gesture as paged_handle_gesture,
|
|
handle_shortcut as paged_handle_shortcut, jump_to_cfi as paged_jump_to_cfi,
|
|
layout as paged_layout, onwheel as paged_onwheel,
|
|
prepare_for_resize as paged_prepare_for_resize, progress_frac,
|
|
reset_paged_mode_globals, resize_done as paged_resize_done,
|
|
scroll_by_page as paged_scroll_by_page, scroll_to_elem,
|
|
scroll_to_extend_annotation as paged_annotation_scroll,
|
|
scroll_to_fraction as paged_scroll_to_fraction, snap_to_selection,
|
|
start_drag_scroll as start_drag_scroll_paged, will_columns_per_screen_change
|
|
)
|
|
from read_book.referencing import (
|
|
elem_for_ref, end_reference_mode, start_reference_mode
|
|
)
|
|
from read_book.resources import finalize_resources, unserialize_html
|
|
from read_book.settings import (
|
|
apply_colors, apply_font_size, apply_settings, apply_stylesheet, opts,
|
|
set_color_scheme_class, set_selection_style, update_settings
|
|
)
|
|
from read_book.shortcuts import (
|
|
create_shortcut_map, keyevent_as_shortcut, shortcut_for_key_event
|
|
)
|
|
from read_book.toc import update_visible_toc_anchors
|
|
from read_book.touch import (
|
|
create_handlers as create_touch_handlers, reset_handlers as reset_touch_handlers
|
|
)
|
|
from read_book.viewport import scroll_viewport
|
|
from utils import debounce, html_escape, is_ios
|
|
|
|
FORCE_FLOW_MODE = False
|
|
CALIBRE_VERSION = '__CALIBRE_VERSION__'
|
|
ERS_SUPPORTED_FEATURES = {'dom-manipulation', 'layout-changes', 'touch-events', 'mouse-events', 'keyboard-events', 'spine-scripting'}
|
|
|
|
|
|
def layout_style():
|
|
return 'scrolling' if current_layout_mode() is 'flow' else 'paginated'
|
|
|
|
|
|
drag_mouse_position = {'x': None, 'y': None}
|
|
|
|
|
|
def cancel_drag_scroll():
|
|
cancel_drag_scroll_flow()
|
|
cancel_drag_scroll_paged()
|
|
|
|
|
|
class EPUBReadingSystem:
|
|
|
|
@property
|
|
def name(self):
|
|
return 'calibre'
|
|
|
|
@property
|
|
def version(self):
|
|
return CALIBRE_VERSION
|
|
|
|
@property
|
|
def layoutStyle(self):
|
|
return layout_style()
|
|
|
|
def hasFeature(self, feature, version):
|
|
return feature in ERS_SUPPORTED_FEATURES
|
|
|
|
def __repr__(self):
|
|
return f'{{name:{self.name}, version:{self.version}, layoutStyle:{self.layoutStyle}}}'
|
|
|
|
def __str__(self):
|
|
return self.__repr__()
|
|
|
|
|
|
class IframeBoss:
|
|
|
|
def __init__(self):
|
|
window.navigator.epubReadingSystem = EPUBReadingSystem()
|
|
self.last_cfi = None
|
|
self.last_search_at = -1001
|
|
self.reference_mode_enabled = False
|
|
self.replace_history_on_next_cfi_update = True
|
|
self.blob_url_map = {}
|
|
self.content_ready = False
|
|
self.last_window_width = self.last_window_height = -1
|
|
self.forward_keypresses = False
|
|
set_boss(self)
|
|
handlers = {
|
|
'change_color_scheme': self.change_color_scheme,
|
|
'change_font_size': self.change_font_size,
|
|
'viewer_font_size_changed': self.viewer_font_size_changed,
|
|
'change_number_of_columns': self.change_number_of_columns,
|
|
'number_of_columns_changed': self.number_of_columns_changed,
|
|
'change_scroll_speed': self.change_scroll_speed,
|
|
'display': self.display,
|
|
'find': self.find,
|
|
'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_reference_mode': self.set_reference_mode,
|
|
'toggle_autoscroll': self.toggle_autoscroll,
|
|
'wheel_from_margin': self.wheel_from_margin,
|
|
'window_size': self.received_window_size,
|
|
'overlay_visibility_changed': self.on_overlay_visibility_changed,
|
|
'show_search_result': self.show_search_result,
|
|
'handle_navigation_shortcut': self.on_handle_navigation_shortcut,
|
|
'annotations': self.annotations_msg_received,
|
|
'copy_selection': self.copy_selection,
|
|
'replace_highlights': self.replace_highlights,
|
|
'clear_selection': def(): window.getSelection().removeAllRanges();,
|
|
}
|
|
self.comm = IframeClient(handlers)
|
|
self.last_window_ypos = 0
|
|
self.length_before = None
|
|
|
|
def on_overlay_visibility_changed(self, data):
|
|
cancel_drag_scroll()
|
|
if data.visible:
|
|
self.forward_keypresses = True
|
|
if self.auto_scroll_action:
|
|
self.auto_scroll_active_before_overlay = self.auto_scroll_action('is_active')
|
|
self.auto_scroll_action('stop')
|
|
else:
|
|
self.forward_keypresses = False
|
|
if self.auto_scroll_active_before_overlay:
|
|
self.auto_scroll_active_before_overlay = undefined
|
|
self.auto_scroll_action('start')
|
|
|
|
def modify_selection(self, data):
|
|
sel = window.getSelection()
|
|
sel.modify('extend', data.direction, data.granularity)
|
|
|
|
def initialize(self, data):
|
|
scroll_viewport.update_window_size(data.width, data.height)
|
|
window.addEventListener('error', self.onerror)
|
|
window.addEventListener('scroll', debounce(self.onscroll, 1000))
|
|
window.addEventListener('scroll', self.no_latency_onscroll)
|
|
window.addEventListener('resize', debounce(self.onresize, 500))
|
|
window.addEventListener('wheel', self.onwheel, {'passive': False})
|
|
window.addEventListener('keydown', self.onkeydown, {'passive': False})
|
|
window.addEventListener('mousemove', self.onmousemove, {'passive': True})
|
|
window.addEventListener('mouseup', self.onmouseup, {'passive': True})
|
|
window.addEventListener('dblclick', self.ondoubleclick, {'passive': True})
|
|
document.documentElement.addEventListener('contextmenu', self.oncontextmenu, {'passive': False})
|
|
document.addEventListener('selectionchange', self.onselectionchange)
|
|
self.color_scheme = data.color_scheme
|
|
create_touch_handlers()
|
|
|
|
def onerror(self, evt):
|
|
msg = evt.message
|
|
script_url = evt.filename
|
|
line_number = evt.lineno
|
|
column_number = evt.colno
|
|
error_object = evt.error
|
|
evt.stopPropagation(), evt.preventDefault()
|
|
if error_object is None:
|
|
# This happens for cross-domain errors (probably javascript injected
|
|
# into the browser via extensions/ userscripts and the like). It also
|
|
# happens all the time when using Chrome on Safari, so ignore this
|
|
# type of error
|
|
console.log(f'Unhandled error from external javascript, ignoring: {msg} {script_url} {line_number}')
|
|
return
|
|
is_internal_error = script_url in ('about:srcdoc', 'userscript:viewer.js')
|
|
if is_internal_error: # dont report errors from scripts in the book itself
|
|
console.log(f'{script_url}: {error_object}')
|
|
try:
|
|
fname = script_url.rpartition('/')[-1] or script_url
|
|
msg = msg + '<br><span style="font-size:smaller">' + 'Error at {}:{}:{}'.format(fname, line_number, column_number or '') + '</span>'
|
|
details = traceback.format_exception(error_object).join('') if error_object else ''
|
|
if details:
|
|
console.log(details)
|
|
self.send_message('error', title=_('Unhandled error'), details=details, msg=msg)
|
|
except:
|
|
console.log('There was an error in the iframe unhandled exception handler')
|
|
else:
|
|
(console.error or console.log)('There was an error in the JavaScript from within the book')
|
|
|
|
def display(self, data):
|
|
cancel_drag_scroll()
|
|
drag_mouse_position.x = drag_mouse_position.y = None
|
|
self.length_before = None
|
|
self.content_ready = False
|
|
clear_annot_id_uuid_map()
|
|
reset_highlight_counter()
|
|
set_toc_anchor_map()
|
|
self.replace_history_on_next_cfi_update = True
|
|
self.book = current_book.book = data.book
|
|
self.reference_mode_enabled = data.reference_mode_enabled
|
|
self.is_titlepage = data.is_titlepage
|
|
spine = self.book.manifest.spine
|
|
index = spine.indexOf(data.name)
|
|
reset_paged_mode_globals()
|
|
set_layout_mode('flow' if FORCE_FLOW_MODE else data.settings.read_mode)
|
|
if current_layout_mode() is 'flow':
|
|
self.do_layout = flow_layout
|
|
self.handle_wheel = flow_onwheel
|
|
self.handle_navigation_shortcut = flow_handle_shortcut
|
|
self._handle_gesture = flow_handle_gesture
|
|
self.to_scroll_fraction = flow_to_scroll_fraction
|
|
self.jump_to_cfi = scroll_to_cfi
|
|
self.anchor_funcs = flow_anchor_funcs
|
|
self.auto_scroll_action = flow_auto_scroll_action
|
|
self.scroll_to_extend_annotation = flow_annotation_scroll
|
|
self.ensure_selection_visible = ensure_selection_visible
|
|
self.start_drag_scroll = start_drag_scroll_flow
|
|
paged_auto_scroll_action('stop')
|
|
else:
|
|
self.do_layout = paged_layout
|
|
self.handle_wheel = paged_onwheel
|
|
self.handle_navigation_shortcut = paged_handle_shortcut
|
|
self.to_scroll_fraction = paged_scroll_to_fraction
|
|
self.jump_to_cfi = paged_jump_to_cfi
|
|
self._handle_gesture = paged_handle_gesture
|
|
self.anchor_funcs = paged_anchor_funcs
|
|
self.auto_scroll_action = paged_auto_scroll_action
|
|
self.scroll_to_extend_annotation = paged_annotation_scroll
|
|
self.ensure_selection_visible = snap_to_selection
|
|
self.start_drag_scroll = start_drag_scroll_paged
|
|
flow_auto_scroll_action('stop')
|
|
update_settings(data.settings)
|
|
self.keyboard_shortcut_map = create_shortcut_map(data.settings.keyboard_shortcuts)
|
|
set_current_spine_item({
|
|
'name':data.name,
|
|
'is_first':index is 0,
|
|
'is_last':index is spine.length - 1,
|
|
'index': index,
|
|
'initial_position':data.initial_position
|
|
})
|
|
self.last_cfi = None
|
|
for name in self.blob_url_map:
|
|
window.URL.revokeObjectURL(self.blob_url_map[name])
|
|
document.body.style.removeProperty('font-family')
|
|
root_data, self.mathjax, self.blob_url_map = finalize_resources(self.book, data.name, data.resource_data)
|
|
self.highlights_to_apply = data.highlights
|
|
unserialize_html(root_data, self.content_loaded, None, data.name)
|
|
|
|
def on_scroll_to_frac(self, data):
|
|
self.to_scroll_fraction(data.frac, False)
|
|
|
|
def toggle_autoscroll(self):
|
|
self.auto_scroll_action('toggle')
|
|
|
|
def handle_gesture(self, gesture):
|
|
if gesture.type is 'show-chrome':
|
|
self.send_message('show_chrome')
|
|
elif gesture.type is 'pinch':
|
|
self.send_message('bump_font_size', increase=gesture.direction is 'out')
|
|
elif gesture.type is 'long-tap':
|
|
self.handle_long_tap(gesture)
|
|
else:
|
|
self._handle_gesture(gesture)
|
|
|
|
def handle_long_tap(self, gesture):
|
|
elements = get_elements(gesture.viewport_x, gesture.viewport_y)
|
|
if elements.img:
|
|
self.send_message('view_image', calibre_src=elements.img)
|
|
return
|
|
if elements.highlight:
|
|
select_crw(elements.crw)
|
|
return
|
|
r = word_at_point(gesture.viewport_x, gesture.viewport_y)
|
|
if r:
|
|
s = document.getSelection()
|
|
s.removeAllRanges()
|
|
s.addRange(r)
|
|
|
|
def gesture_from_margin(self, data):
|
|
self.handle_gesture(data.gesture)
|
|
|
|
def wheel_from_margin(self, data):
|
|
self.onwheel(data.evt)
|
|
|
|
def report_human_scroll(self, scrolled_by_frac):
|
|
self.send_message('human_scroll', scrolled_by_frac=scrolled_by_frac or None)
|
|
|
|
def on_scroll_to_anchor(self, data):
|
|
frag = data.frag
|
|
if frag:
|
|
self.scroll_to_anchor(frag)
|
|
else:
|
|
self.to_scroll_fraction(0.0, False)
|
|
|
|
def on_next_screen(self, data):
|
|
backwards = data.backwards
|
|
if current_layout_mode() is 'flow':
|
|
flow_scroll_by_page(-1 if backwards else 1, data.flip_if_rtl_page_progression)
|
|
else:
|
|
paged_scroll_by_page(backwards, data.all_pages_on_screen, data.flip_if_rtl_page_progression)
|
|
|
|
|
|
def change_font_size(self, data):
|
|
if data.base_font_size? and data.base_font_size != opts.base_font_size:
|
|
opts.base_font_size = data.base_font_size
|
|
apply_font_size()
|
|
if not runtime.is_standalone_viewer:
|
|
# in the standalone viewer this is a separate event as
|
|
# apply_font_size() is a no-op
|
|
self.relayout_on_font_size_change()
|
|
|
|
def viewer_font_size_changed(self, data):
|
|
self.relayout_on_font_size_change()
|
|
|
|
def change_number_of_columns(self, data):
|
|
if current_layout_mode() is 'flow':
|
|
self.send_message('error', title=_('In flow mode'), msg=_(
|
|
'Cannot change number of pages per screen in flow mode, switch to paged mode first.'))
|
|
return
|
|
cdata = get_columns_per_screen_data()
|
|
delta = int(data.delta)
|
|
if delta is 0:
|
|
new_val = 0
|
|
else:
|
|
new_val = max(1, cdata.cps + delta)
|
|
opts.columns_per_screen[cdata.which] = new_val
|
|
self.relayout_on_font_size_change()
|
|
self.send_message('columns_per_screen_changed', which=cdata.which, cps=new_val)
|
|
|
|
def relayout_on_font_size_change(self):
|
|
if current_layout_mode() is not 'flow' and will_columns_per_screen_change():
|
|
self.do_layout(self.is_titlepage)
|
|
if self.last_cfi:
|
|
cfi = self.last_cfi[len('epubcfi(/'):-1].partition('/')[2]
|
|
if cfi:
|
|
paged_jump_to_cfi('/' + cfi)
|
|
self.update_cfi()
|
|
self.update_toc_position()
|
|
|
|
def number_of_columns_changed(self, data):
|
|
opts.columns_per_screen = data.columns_per_screen
|
|
self.relayout_on_font_size_change()
|
|
|
|
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()
|
|
|
|
def change_color_scheme(self, data):
|
|
if data.color_scheme and data.color_scheme.foreground and data.color_scheme.background:
|
|
opts.color_scheme = data.color_scheme
|
|
apply_colors()
|
|
set_color_scheme_class()
|
|
|
|
def content_loaded(self):
|
|
document.documentElement.style.overflow = 'hidden'
|
|
if self.is_titlepage and not opts.cover_preserve_aspect_ratio:
|
|
document.body.classList.add('cover-fill')
|
|
document.body.classList.add(f'calibre-viewer-{layout_style()}')
|
|
set_color_scheme_class()
|
|
if self.reference_mode_enabled:
|
|
start_reference_mode()
|
|
self.last_window_width, self.last_window_height = scroll_viewport.width(), scroll_viewport.height()
|
|
if self.highlights_to_apply:
|
|
self.apply_highlights_on_load(self.highlights_to_apply)
|
|
self.highlights_to_apply = None
|
|
apply_settings()
|
|
fix_fullscreen_svg_images()
|
|
self.do_layout(self.is_titlepage)
|
|
if self.mathjax:
|
|
return apply_mathjax(self.mathjax, self.book.manifest.link_uid, self.content_loaded_stage2)
|
|
# window.setTimeout(self.content_loaded_stage2, 1000)
|
|
self.content_loaded_stage2()
|
|
|
|
def content_loaded_stage2(self):
|
|
reset_find_caches()
|
|
self.connect_links()
|
|
if runtime.is_standalone_viewer:
|
|
self.listen_for_image_double_clicks()
|
|
self.content_ready = True
|
|
# this is the loading styles used to suppress scrollbars during load
|
|
# added in unserialize_html
|
|
document.head.removeChild(document.head.firstChild)
|
|
|
|
csi = current_spine_item()
|
|
if csi.initial_position:
|
|
ipos = csi.initial_position
|
|
self.replace_history_on_next_cfi_update = ipos.replace_history or False
|
|
if ipos.type is 'frac':
|
|
self.to_scroll_fraction(ipos.frac, True)
|
|
elif ipos.type is 'anchor':
|
|
self.scroll_to_anchor(ipos.anchor)
|
|
elif ipos.type is 'ref':
|
|
self.scroll_to_ref(ipos.refnum)
|
|
elif ipos.type is 'cfi':
|
|
self.jump_to_cfi(ipos.cfi)
|
|
elif ipos.type is 'search_result':
|
|
self.show_search_result(ipos, True)
|
|
elif ipos.type is 'search':
|
|
self.find(ipos.search_data, True)
|
|
elif ipos.type is 'edit_annotation':
|
|
window.setTimeout(def():
|
|
self.annotations_msg_received({'type': 'edit-highlight', 'uuid': ipos.uuid})
|
|
, 5)
|
|
spine = self.book.manifest.spine
|
|
files = self.book.manifest.files
|
|
spine_index = csi.index
|
|
self.length_before = 0
|
|
if spine_index > -1:
|
|
for i in range(spine_index):
|
|
si = spine[i]
|
|
if si:
|
|
self.length_before += files[si]?.length or 0
|
|
self.onscroll()
|
|
self.send_message('content_loaded', progress_frac=self.calculate_progress_frac(), file_progress_frac=progress_frac())
|
|
self.last_cfi = None
|
|
self.auto_scroll_action('resume')
|
|
reset_touch_handlers() # Needed to mitigate issue https://bugs.chromium.org/p/chromium/issues/detail?id=464579
|
|
|
|
window.setTimeout(self.update_cfi, 0)
|
|
window.setTimeout(self.update_toc_position, 0)
|
|
|
|
def calculate_progress_frac(self):
|
|
current_name = current_spine_item().name
|
|
files = self.book.manifest.files
|
|
file_length = files[current_name]?.length or 0
|
|
if self.length_before is None:
|
|
return 0
|
|
frac = progress_frac()
|
|
ans = (self.length_before + (file_length * frac)) / self.book.manifest.spine_length
|
|
return ans
|
|
|
|
def get_current_cfi(self, data):
|
|
cfi = current_cfi()
|
|
csi = current_spine_item()
|
|
if cfi and csi:
|
|
index = csi.index
|
|
if index > -1:
|
|
cfi = 'epubcfi(/{}{})'.format(2*(index+1), cfi)
|
|
self.send_message(
|
|
'report_cfi', cfi=cfi, progress_frac=self.calculate_progress_frac(),
|
|
file_progress_frac=progress_frac(), request_id=data.request_id)
|
|
return
|
|
self.send_message(
|
|
'report_cfi', cfi=None, progress_frac=0, file_progress_frac=0, request_id=data.request_id)
|
|
|
|
def update_cfi(self):
|
|
cfi = current_cfi()
|
|
if cfi:
|
|
index = current_spine_item().index
|
|
if index > -1:
|
|
cfi = 'epubcfi(/{}{})'.format(2*(index+1), cfi)
|
|
pf = self.calculate_progress_frac()
|
|
fpf = progress_frac()
|
|
if cfi is not self.last_cfi:
|
|
self.last_cfi = cfi
|
|
self.send_message(
|
|
'update_cfi', cfi=cfi, replace_history=self.replace_history_on_next_cfi_update,
|
|
progress_frac=pf, file_progress_frac=fpf)
|
|
self.replace_history_on_next_cfi_update = True
|
|
else:
|
|
self.send_message(
|
|
'update_progress_frac', progress_frac=pf, file_progress_frac=fpf)
|
|
|
|
def update_toc_position(self):
|
|
visible_anchors = update_visible_toc_anchors(self.book.manifest.toc_anchor_map, self.anchor_funcs)
|
|
self.send_message('update_toc_position', visible_anchors=visible_anchors)
|
|
|
|
def onscroll(self):
|
|
if self.content_ready:
|
|
self.update_cfi()
|
|
self.update_toc_position()
|
|
|
|
def no_latency_onscroll(self):
|
|
pf = self.calculate_progress_frac()
|
|
fpf = progress_frac()
|
|
self.send_message(
|
|
'update_progress_frac', progress_frac=pf, file_progress_frac=fpf)
|
|
sel = window.getSelection()
|
|
if sel and not sel.isCollapsed:
|
|
self.send_message('update_selection_position', selection_extents=selection_extents(current_layout_mode() is 'flow', True))
|
|
|
|
def onresize(self):
|
|
self.send_message('request_size')
|
|
if self.content_ready:
|
|
if is_ios:
|
|
# On iOS window.innerWidth/Height are wrong inside the iframe,
|
|
# so we wait for the reply from request_size
|
|
return
|
|
self.onresize_stage2()
|
|
|
|
def onselectionchange(self):
|
|
if not self.content_ready:
|
|
return
|
|
sel = window.getSelection()
|
|
text = ''
|
|
annot_id = None
|
|
collapsed = not sel or sel.isCollapsed
|
|
if not collapsed:
|
|
text = sel.toString()
|
|
annot_id = highlight_associated_with_selection(sel, annot_id_uuid_map)
|
|
by_search = window.performance.now() - self.last_search_at < 1000
|
|
self.send_message(
|
|
'selectionchange', text=text, empty=v'!!collapsed', annot_id=annot_id,
|
|
drag_mouse_position=drag_mouse_position, selection_change_caused_by_search=by_search,
|
|
selection_extents=selection_extents(current_layout_mode() is 'flow'))
|
|
|
|
def onresize_stage2(self):
|
|
if scroll_viewport.width() is self.last_window_width and scroll_viewport.height() is self.last_window_height:
|
|
# Safari at least, generates lots of spurious resize events
|
|
return
|
|
if current_layout_mode() is not 'flow':
|
|
paged_prepare_for_resize(self.last_window_width, self.last_window_height)
|
|
self.do_layout(self.is_titlepage)
|
|
self.last_window_width, self.last_window_height = scroll_viewport.width(), scroll_viewport.height()
|
|
if self.last_cfi:
|
|
cfi = self.last_cfi[len('epubcfi(/'):-1].partition('/')[2]
|
|
if cfi:
|
|
paged_jump_to_cfi('/' + cfi)
|
|
if current_layout_mode() is not 'flow':
|
|
paged_resize_done()
|
|
self.update_cfi()
|
|
self.update_toc_position()
|
|
sel = window.getSelection()
|
|
if sel and not sel.isCollapsed:
|
|
# update_selection_position has probably already been called by
|
|
# no_latency_onscroll but make sure
|
|
self.send_message('update_selection_position', selection_extents=selection_extents(current_layout_mode() is 'flow'))
|
|
|
|
def received_window_size(self, data):
|
|
scroll_viewport.update_window_size(data.width, data.height)
|
|
if self.content_ready:
|
|
self.onresize_stage2()
|
|
|
|
def onwheel(self, evt):
|
|
if self.content_ready:
|
|
if evt.preventDefault:
|
|
evt.preventDefault()
|
|
if evt.deltaY and evt.ctrlKey and not evt.shiftKey and not evt.altKey and not evt.metaKey:
|
|
self.send_message('handle_shortcut', name='increase_font_size' if evt.deltaY < 0 else 'decrease_font_size')
|
|
else:
|
|
self.handle_wheel(evt)
|
|
|
|
def onmousemove(self, evt):
|
|
if evt.buttons is not 1:
|
|
return
|
|
drag_mouse_position.x = evt.clientX
|
|
drag_mouse_position.y = evt.clientY
|
|
if 0 <= evt.clientY <= window.innerHeight:
|
|
cancel_drag_scroll()
|
|
return
|
|
sel = window.getSelection()
|
|
if not sel:
|
|
cancel_drag_scroll()
|
|
return
|
|
delta = evt.clientY if evt.clientY < 0 else (evt.clientY - window.innerHeight)
|
|
self.start_drag_scroll(delta)
|
|
|
|
def onmouseup(self, evt):
|
|
cancel_drag_scroll()
|
|
drag_mouse_position.x = drag_mouse_position.y = None
|
|
# ensure selection bar is updated
|
|
self.onselectionchange()
|
|
|
|
def ondoubleclick(self, evt):
|
|
self.send_message('annotations', type='double-click')
|
|
|
|
def onkeydown(self, evt):
|
|
if current_layout_mode() is not 'flow' and evt.key is 'Tab':
|
|
# Prevent the TAB key from shifting focus as it causes partial scrolling
|
|
evt.preventDefault()
|
|
if self.forward_keypresses:
|
|
self.send_message('handle_keypress', evt=keyevent_as_shortcut(evt))
|
|
evt.preventDefault(), evt.stopPropagation()
|
|
return
|
|
if self.content_ready:
|
|
sc_name = shortcut_for_key_event(evt, self.keyboard_shortcut_map)
|
|
if sc_name:
|
|
if self.handle_navigation_shortcut(sc_name, evt):
|
|
evt.preventDefault()
|
|
else:
|
|
self.send_message('handle_shortcut', name=sc_name)
|
|
|
|
def on_handle_navigation_shortcut(self, data):
|
|
self.handle_navigation_shortcut(data.name, data.key or {
|
|
'key': '', 'altKey': False, 'ctrlKey': False, 'shiftKey': False, 'metaKey': False})
|
|
|
|
def oncontextmenu(self, evt):
|
|
if self.content_ready:
|
|
evt.preventDefault()
|
|
self.send_message('show_chrome', elements=get_elements(evt.clientX, evt.clientY))
|
|
|
|
def send_message(self, action, **data):
|
|
self.comm.send_message(action, data)
|
|
|
|
def connect_links(self):
|
|
link_attr = 'data-' + self.book.manifest.link_uid
|
|
for a in document.body.querySelectorAll('a[{}]'.format(link_attr)):
|
|
a.addEventListener('click', self.link_activated)
|
|
if runtime.is_standalone_viewer:
|
|
# links with a target get turned into requests to open a new window by Qt
|
|
for a in document.body.querySelectorAll('a[target]'):
|
|
a.removeAttribute('target')
|
|
|
|
def listen_for_image_double_clicks(self):
|
|
for img in document.querySelectorAll('img, image'):
|
|
img.addEventListener('dblclick', self.image_double_clicked, {'passive': True})
|
|
|
|
def image_double_clicked(self, ev):
|
|
img = ev.currentTarget
|
|
if img.dataset.calibreSrc:
|
|
self.send_message('view_image', calibre_src=img.dataset.calibreSrc)
|
|
|
|
def link_activated(self, evt):
|
|
link_attr = 'data-' + self.book.manifest.link_uid
|
|
try:
|
|
data = JSON.parse(evt.currentTarget.getAttribute(link_attr))
|
|
except:
|
|
print('WARNING: Failed to parse link data {}, ignoring'.format(evt.currentTarget?.getAttribute?(link_attr)))
|
|
return
|
|
name, frag = data.name, data.frag
|
|
if not name:
|
|
name = current_spine_item().name
|
|
try:
|
|
is_popup = is_footnote_link(evt.currentTarget, name, frag, current_spine_item().name, self.book.manifest.link_to_map or {})
|
|
except:
|
|
import traceback
|
|
traceback.print_exc()
|
|
is_popup = False
|
|
if is_popup:
|
|
self.send_message('show_footnote', name=name, frag=frag, title=evt.currentTarget.textContent, cols_per_screen=calc_columns_per_screen())
|
|
return
|
|
if name is current_spine_item().name:
|
|
self.replace_history_on_next_cfi_update = False
|
|
self.scroll_to_anchor(frag)
|
|
else:
|
|
self.send_message('scroll_to_anchor', name=name, frag=frag)
|
|
|
|
def scroll_to_anchor(self, frag):
|
|
if frag:
|
|
elem = document.getElementById(frag)
|
|
if not elem:
|
|
c = document.getElementsByName(frag)
|
|
if c and c.length:
|
|
elem = c[0]
|
|
if elem:
|
|
scroll_to_elem(elem)
|
|
else:
|
|
scroll_viewport.scroll_to(0, 0)
|
|
|
|
def scroll_to_ref(self, refnum):
|
|
refnum = int(refnum)
|
|
elem = elem_for_ref(refnum)
|
|
if elem:
|
|
scroll_to_elem(elem)
|
|
|
|
def on_scroll_to_ref(self, data):
|
|
refnum = data.refnum
|
|
if refnum?:
|
|
self.scroll_to_ref(refnum)
|
|
|
|
def find(self, data, from_load):
|
|
self.last_search_at = window.performance.now()
|
|
if data.searched_in_spine:
|
|
window.getSelection().removeAllRanges()
|
|
if window.find(data.text, False, data.backwards, from_load and data.backwards):
|
|
if current_layout_mode() is not 'flow':
|
|
snap_to_selection()
|
|
else:
|
|
if from_load:
|
|
self.send_message('error', title=_('Invisible text'), msg=_(
|
|
'The text <i>{}</i> is present on this page but not visible').format(html_escape(data.text)))
|
|
else:
|
|
self.send_message('find_in_spine', text=data.text, backwards=data.backwards, searched_in_spine=data.searched_in_spine)
|
|
|
|
def show_search_result(self, data, from_load):
|
|
self.last_search_at = window.performance.now()
|
|
if select_search_result(data.search_result):
|
|
self.ensure_selection_visible()
|
|
else:
|
|
self.send_message('search_result_not_found', search_result=data.search_result)
|
|
|
|
def reference_item_changed(self, ref_num_or_none):
|
|
self.send_message('reference_item_changed', refnum=ref_num_or_none, index=current_spine_item().index)
|
|
|
|
def set_reference_mode(self, data):
|
|
self.reference_mode_enabled = data.enabled
|
|
if data.enabled:
|
|
start_reference_mode()
|
|
else:
|
|
end_reference_mode()
|
|
|
|
def annotations_msg_received(self, data):
|
|
if data.type is 'set-selection':
|
|
set_selections_extents_to(data.extents)
|
|
elif data.type is 'set-highlight-style':
|
|
set_selection_style(data.style)
|
|
elif data.type is 'trigger-shortcut':
|
|
self.on_handle_navigation_shortcut(data)
|
|
elif data.type is 'extend-to-paragraph':
|
|
sel = window.getSelection()
|
|
sel.modify('extend', 'forward', 'paragraphboundary')
|
|
end_node, end_offset = sel.focusNode, sel.focusOffset
|
|
sel.modify('extend', 'backward', 'paragraphboundary')
|
|
sel.setBaseAndExtent(sel.focusNode, sel.focusOffset, end_node, end_offset)
|
|
elif data.type is 'edit-highlight':
|
|
crw_ = {v: k for k, v in Object.entries(annot_id_uuid_map)}[data.uuid]
|
|
if crw_ and select_crw(crw_):
|
|
self.ensure_selection_visible()
|
|
window.setTimeout(def():
|
|
self.send_message('annotations', type='edit-highlight')
|
|
, 50)
|
|
elif data.type is 'remove-highlight':
|
|
crw_ = {v: k for k, v in Object.entries(annot_id_uuid_map)}[data.uuid]
|
|
if crw_:
|
|
unwrap_crw(crw_)
|
|
v'delete annot_id_uuid_map[crw_]'
|
|
sel = window.getSelection()
|
|
if not sel.toString():
|
|
# have to remove selection otherwise it remains as the empty
|
|
# string, and the selection bar does not hide itself
|
|
sel.removeAllRanges()
|
|
elif data.type is 'apply-highlight':
|
|
sel = window.getSelection()
|
|
text = sel.toString()
|
|
if not sel.rangeCount:
|
|
return
|
|
bounds = cfi_for_selection()
|
|
style = highlight_style_as_css(data.style, opts.is_dark_theme, opts.color_scheme.foreground)
|
|
cls = 'crw-has-dot' if data.has_notes else None
|
|
annot_id, intersecting_wrappers = wrap_text_in_range(style, None, cls, self.add_highlight_listeners)
|
|
removed_highlights = v'[]'
|
|
if annot_id is not None:
|
|
intersecting_uuids = {annot_id_uuid_map[x]:True for x in intersecting_wrappers}
|
|
if data.existing and intersecting_uuids[data.existing]:
|
|
data.uuid = data.existing
|
|
elif intersecting_wrappers.length is 1 and annot_id_uuid_map[intersecting_wrappers[0]]:
|
|
data.uuid = annot_id_uuid_map[intersecting_wrappers[0]]
|
|
intersecting_wrappers = v'[]'
|
|
removed_highlights = {}
|
|
for crw in intersecting_wrappers:
|
|
unwrap_crw(crw)
|
|
if annot_id_uuid_map[crw] and annot_id_uuid_map[crw] is not data.uuid:
|
|
removed_highlights[annot_id_uuid_map[crw]] = True
|
|
v'delete annot_id_uuid_map[crw]'
|
|
removed_highlights = Object.keys(removed_highlights)
|
|
sel.removeAllRanges()
|
|
annot_id_uuid_map[annot_id] = data.uuid
|
|
self.send_message(
|
|
'annotations',
|
|
type='highlight-applied',
|
|
uuid=data.uuid, ok=annot_id is not None,
|
|
bounds=bounds,
|
|
removed_highlights=removed_highlights,
|
|
highlighted_text=text,
|
|
)
|
|
reset_find_caches()
|
|
else:
|
|
console.log('Ignoring annotations message to iframe with unknown type: ' + data.type)
|
|
|
|
def apply_highlights_on_load(self, highlights):
|
|
clear_annot_id_uuid_map()
|
|
reset_highlight_counter()
|
|
strcmp = v'new Intl.Collator().compare'
|
|
highlights.sort(def (a, b): return strcmp(a.timestamp, b.timestamp);)
|
|
for h in highlights:
|
|
r = range_from_cfi(h.start_cfi, h.end_cfi)
|
|
if not r:
|
|
continue
|
|
style = highlight_style_as_css(h.style, opts.is_dark_theme, opts.color_scheme.foreground)
|
|
cls = 'crw-has-dot' if h.notes else None
|
|
annot_id, intersecting_wrappers = wrap_text_in_range(style, r, cls, self.add_highlight_listeners)
|
|
if annot_id is not None:
|
|
annot_id_uuid_map[annot_id] = h.uuid
|
|
for crw in intersecting_wrappers:
|
|
unwrap_crw(crw)
|
|
v'delete annot_id_uuid_map[crw]'
|
|
|
|
def replace_highlights(self, data):
|
|
highlights = data.highlights
|
|
unwrap_all_crw()
|
|
self.apply_highlights_on_load(highlights or v'[]')
|
|
|
|
def add_highlight_listeners(self, wrapper):
|
|
wrapper.addEventListener('dblclick', self.highlight_wrapper_dblclicked)
|
|
|
|
def highlight_wrapper_dblclicked(self, ev):
|
|
crw = ev.currentTarget.dataset.calibreRangeWrapper
|
|
if crw:
|
|
ev.preventDefault(), ev.stopPropagation()
|
|
select_crw(crw)
|
|
|
|
def copy_selection(self):
|
|
text = window.getSelection().toString()
|
|
if text:
|
|
self.send_message('copy_text_to_clipboard', text=text)
|
|
|
|
def main():
|
|
main.boss = IframeBoss()
|