mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Handle multi-page highlight creation
This commit is contained in:
parent
9323019776
commit
8dc0e875c9
@ -2,7 +2,8 @@
|
|||||||
# License: GPL v3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
|
# License: GPL v3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
from __python__ import bound_methods, hash_literals
|
from __python__ import bound_methods, hash_literals
|
||||||
|
|
||||||
from dom import svgicon, ensure_id
|
from dom import ensure_id, svgicon
|
||||||
|
from read_book.shortcuts import shortcut_for_key_event
|
||||||
|
|
||||||
WAITING_FOR_CLICK = 1
|
WAITING_FOR_CLICK = 1
|
||||||
WAITING_FOR_DRAG = 2
|
WAITING_FOR_DRAG = 2
|
||||||
@ -45,6 +46,7 @@ class CreateAnnotation:
|
|||||||
self.view = view
|
self.view = view
|
||||||
self.state = WAITING_FOR_CLICK
|
self.state = WAITING_FOR_CLICK
|
||||||
self.left_line_height = self.right_line_height = 8
|
self.left_line_height = self.right_line_height = 8
|
||||||
|
self.in_flow_mode = False
|
||||||
container = self.container
|
container = self.container
|
||||||
self.position_in_handle = {'x': 0, 'y': 0}
|
self.position_in_handle = {'x': 0, 'y': 0}
|
||||||
|
|
||||||
@ -60,6 +62,20 @@ class CreateAnnotation:
|
|||||||
container.addEventListener('click', self.container_clicked, {'passive': False})
|
container.addEventListener('click', self.container_clicked, {'passive': False})
|
||||||
container.addEventListener('mouseup', self.mouseup_on_container, {'passive': False})
|
container.addEventListener('mouseup', self.mouseup_on_container, {'passive': False})
|
||||||
container.addEventListener('mousemove', self.mousemove_on_container, {'passive': False})
|
container.addEventListener('mousemove', self.mousemove_on_container, {'passive': False})
|
||||||
|
container.addEventListener('keydown', self.on_keydown, {'passive': False})
|
||||||
|
|
||||||
|
def on_keydown(self, ev):
|
||||||
|
ev.stopPropagation(), ev.preventDefault()
|
||||||
|
sc_name = shortcut_for_key_event(ev, self.view.keyboard_shortcut_map)
|
||||||
|
if sc_name is 'show_chrome':
|
||||||
|
self.hide()
|
||||||
|
elif sc_name in ('up', 'down', 'pageup', 'pagedown'):
|
||||||
|
self.send_message('scroll', backwards=bool('up' in sc_name))
|
||||||
|
elif sc_name in ('left', 'right'):
|
||||||
|
if self.in_flow_mode:
|
||||||
|
self.send_message('perp-scroll', backwards=bool(sc_name is 'left'))
|
||||||
|
else:
|
||||||
|
self.send_message('scroll', backwards=bool(sc_name is 'left'))
|
||||||
|
|
||||||
def container_clicked(self, ev):
|
def container_clicked(self, ev):
|
||||||
ev.stopPropagation(), ev.preventDefault()
|
ev.stopPropagation(), ev.preventDefault()
|
||||||
@ -118,17 +134,28 @@ class CreateAnnotation:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def current_handle_position(self):
|
def current_handle_position(self):
|
||||||
lh, rh = self.left_handle.getBoundingClientRect(), self.right_handle.getBoundingClientRect()
|
lh, rh = self.left_handle, self.right_handle
|
||||||
|
lbr, rbr = self.left_handle.getBoundingClientRect(), self.right_handle.getBoundingClientRect()
|
||||||
return {
|
return {
|
||||||
'start': {'x': Math.round(lh.right), 'y': Math.round(lh.bottom - self.left_line_height // 2)},
|
'start': {
|
||||||
'end': {'x': Math.round(rh.left), 'y': Math.round(rh.bottom - self.right_line_height // 2)}
|
'onscreen': lh.style.display is not 'none',
|
||||||
|
'x': Math.round(lbr.right), 'y': Math.round(lbr.bottom - self.left_line_height // 2)
|
||||||
|
},
|
||||||
|
'end': {
|
||||||
|
'onscreen': rh.style.display is not 'none',
|
||||||
|
'x': Math.round(rbr.left), 'y': Math.round(rbr.bottom - self.right_line_height // 2)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def show(self):
|
def show(self):
|
||||||
self.container.style.display = 'block'
|
c = self.container
|
||||||
|
c.style.display = 'block'
|
||||||
|
c.focus()
|
||||||
|
|
||||||
def hide(self):
|
def hide(self):
|
||||||
self.container.style.display = 'none'
|
if self.is_visible:
|
||||||
|
self.container.style.display = 'none'
|
||||||
|
self.view.focus_iframe()
|
||||||
|
|
||||||
def send_message(self, type, **kw):
|
def send_message(self, type, **kw):
|
||||||
self.view.iframe_wrapper.send_message('annotations', type=type, **kw)
|
self.view.iframe_wrapper.send_message('annotations', type=type, **kw)
|
||||||
@ -142,9 +169,12 @@ class CreateAnnotation:
|
|||||||
self.hide_handles()
|
self.hide_handles()
|
||||||
if msg.extents.start.x is not None:
|
if msg.extents.start.x is not None:
|
||||||
self.place_handles(msg.extents)
|
self.place_handles(msg.extents)
|
||||||
|
self.in_flow_mode = msg.in_flow_mode
|
||||||
elif msg.type is 'position-handles':
|
elif msg.type is 'position-handles':
|
||||||
if self.state is WAITING_FOR_CLICK:
|
if self.state is WAITING_FOR_CLICK:
|
||||||
self.place_handles(msg.extents)
|
self.place_handles(msg.extents)
|
||||||
|
elif msg.type is 'update-handles':
|
||||||
|
self.place_handles(msg.extents)
|
||||||
else:
|
else:
|
||||||
print('Ignoring annotations message with unknown type:', msg.type)
|
print('Ignoring annotations message with unknown type:', msg.type)
|
||||||
|
|
||||||
@ -158,12 +188,12 @@ class CreateAnnotation:
|
|||||||
def do_it(handle, data):
|
def do_it(handle, data):
|
||||||
map_from_iframe_coords(data)
|
map_from_iframe_coords(data)
|
||||||
s = handle.style
|
s = handle.style
|
||||||
s.display = 'block'
|
s.display = 'block' if data.onscreen else 'none'
|
||||||
height = data.height * 3
|
height = data.height * 3
|
||||||
width = data.height * 2
|
width = data.height * 2
|
||||||
s.width = f'{width}px'
|
s.width = f'{width}px'
|
||||||
s.height = f'{height}px'
|
s.height = f'{height}px'
|
||||||
bottom = min(max(0, data.y + data.height), window.innerHeight)
|
bottom = data.y + data.height
|
||||||
top = bottom - height
|
top = bottom - height
|
||||||
s.top = f'{top}px'
|
s.top = f'{top}px'
|
||||||
return s, width
|
return s, width
|
||||||
|
@ -140,6 +140,16 @@ def scroll_by_page(direction):
|
|||||||
h = scroll_viewport.height() - 10
|
h = scroll_viewport.height() - 10
|
||||||
window.scrollBy(0, h * direction)
|
window.scrollBy(0, h * direction)
|
||||||
|
|
||||||
|
def scroll_to_extend_annotation(backward, horizontal):
|
||||||
|
direction = -1 if backward else 1
|
||||||
|
if horizontal:
|
||||||
|
before = window.pageXOffset
|
||||||
|
window.scrollBy(15 * direction, 0)
|
||||||
|
return window.pageXOffset is not before
|
||||||
|
h = scroll_viewport.height() - 10
|
||||||
|
before = window.pageYOffset
|
||||||
|
window.scrollBy(0, h * direction)
|
||||||
|
return window.pageYOffset is not before
|
||||||
|
|
||||||
def is_auto_scroll_active():
|
def is_auto_scroll_active():
|
||||||
return scroll_animator.is_active()
|
return scroll_animator.is_active()
|
||||||
|
@ -5,7 +5,8 @@ from __python__ import bound_methods, hash_literals
|
|||||||
import traceback
|
import traceback
|
||||||
from gettext import gettext as _
|
from gettext import gettext as _
|
||||||
from select import (
|
from select import (
|
||||||
selection_extents, selection_extents_at_point, set_selections_extents_to
|
extend_selection_after_scroll, selection_extents, selection_extents_at_point,
|
||||||
|
set_selections_extents_to
|
||||||
)
|
)
|
||||||
|
|
||||||
from fs_images import fix_fullscreen_svg_images
|
from fs_images import fix_fullscreen_svg_images
|
||||||
@ -17,7 +18,8 @@ from read_book.flow_mode import (
|
|||||||
anchor_funcs as flow_anchor_funcs, auto_scroll_action as flow_auto_scroll_action,
|
anchor_funcs as flow_anchor_funcs, auto_scroll_action as flow_auto_scroll_action,
|
||||||
ensure_selection_visible, flow_onwheel, flow_to_scroll_fraction,
|
ensure_selection_visible, 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,
|
||||||
|
scroll_to_extend_annotation as flow_annotation_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 (
|
||||||
@ -34,6 +36,7 @@ from read_book.paged_mode import (
|
|||||||
prepare_for_resize as paged_prepare_for_resize, progress_frac,
|
prepare_for_resize as paged_prepare_for_resize, progress_frac,
|
||||||
reset_paged_mode_globals, resize_done as paged_resize_done,
|
reset_paged_mode_globals, resize_done as paged_resize_done,
|
||||||
scroll_by_page as paged_scroll_by_page, scroll_to_elem,
|
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,
|
scroll_to_fraction as paged_scroll_to_fraction, snap_to_selection,
|
||||||
will_columns_per_screen_change
|
will_columns_per_screen_change
|
||||||
)
|
)
|
||||||
@ -207,6 +210,7 @@ class IframeBoss:
|
|||||||
self.jump_to_cfi = scroll_to_cfi
|
self.jump_to_cfi = scroll_to_cfi
|
||||||
self.anchor_funcs = flow_anchor_funcs
|
self.anchor_funcs = flow_anchor_funcs
|
||||||
self.auto_scroll_action = flow_auto_scroll_action
|
self.auto_scroll_action = flow_auto_scroll_action
|
||||||
|
self.scroll_to_extend_annotation = flow_annotation_scroll
|
||||||
paged_auto_scroll_action('stop')
|
paged_auto_scroll_action('stop')
|
||||||
else:
|
else:
|
||||||
self.do_layout = paged_layout
|
self.do_layout = paged_layout
|
||||||
@ -217,6 +221,7 @@ class IframeBoss:
|
|||||||
self._handle_gesture = paged_handle_gesture
|
self._handle_gesture = paged_handle_gesture
|
||||||
self.anchor_funcs = paged_anchor_funcs
|
self.anchor_funcs = paged_anchor_funcs
|
||||||
self.auto_scroll_action = paged_auto_scroll_action
|
self.auto_scroll_action = paged_auto_scroll_action
|
||||||
|
self.scroll_to_extend_annotation = paged_annotation_scroll
|
||||||
flow_auto_scroll_action('stop')
|
flow_auto_scroll_action('stop')
|
||||||
update_settings(data.settings)
|
update_settings(data.settings)
|
||||||
self.keyboard_shortcut_map = create_shortcut_map(data.settings.keyboard_shortcuts)
|
self.keyboard_shortcut_map = create_shortcut_map(data.settings.keyboard_shortcuts)
|
||||||
@ -605,21 +610,31 @@ class IframeBoss:
|
|||||||
end_reference_mode()
|
end_reference_mode()
|
||||||
|
|
||||||
def initiate_creation_of_annotation(self):
|
def initiate_creation_of_annotation(self):
|
||||||
|
self.auto_scroll_action('stop')
|
||||||
|
in_flow_mode = current_layout_mode() is 'flow'
|
||||||
self.send_message(
|
self.send_message(
|
||||||
'annotations',
|
'annotations',
|
||||||
type='create-annotation',
|
type='create-annotation',
|
||||||
in_flow_mode=current_layout_mode() is 'flow',
|
in_flow_mode=in_flow_mode,
|
||||||
extents=selection_extents(),
|
extents=selection_extents(in_flow_mode),
|
||||||
)
|
)
|
||||||
|
|
||||||
def annotations_msg_received(self, data):
|
def annotations_msg_received(self, data):
|
||||||
|
in_flow_mode = current_layout_mode() is 'flow'
|
||||||
if data.type is 'set-selection':
|
if data.type is 'set-selection':
|
||||||
set_selections_extents_to(data.extents)
|
set_selections_extents_to(data.extents)
|
||||||
elif data.type is 'position-handles-at-point':
|
elif data.type is 'position-handles-at-point':
|
||||||
self.send_message(
|
self.send_message(
|
||||||
'annotations',
|
'annotations',
|
||||||
type='position-handles',
|
type='position-handles',
|
||||||
extents=selection_extents_at_point(data.x, data.y))
|
extents=selection_extents_at_point(data.x, data.y, in_flow_mode))
|
||||||
|
elif data.type is 'scroll':
|
||||||
|
if self.scroll_to_extend_annotation(data.backwards):
|
||||||
|
extend_selection_after_scroll(data.backwards, in_flow_mode)
|
||||||
|
self.send_message('annotations', type='update-handles', extents=selection_extents(in_flow_mode))
|
||||||
|
elif data.type is 'perp-scroll':
|
||||||
|
if in_flow_mode and flow_annotation_scroll(data.backwards, True):
|
||||||
|
self.send_message('annotations', type='update-handles', extents=selection_extents(in_flow_mode))
|
||||||
else:
|
else:
|
||||||
console.log('Ignoring annotations message to iframe with unknown type: ' + data.type)
|
console.log('Ignoring annotations message to iframe with unknown type: ' + data.type)
|
||||||
|
|
||||||
|
@ -562,6 +562,14 @@ def scroll_by_page(backward, by_screen):
|
|||||||
scroll_to_xpos(pos)
|
scroll_to_xpos(pos)
|
||||||
|
|
||||||
|
|
||||||
|
def scroll_to_extend_annotation(backward):
|
||||||
|
pos = previous_col_location() if backward else next_col_location()
|
||||||
|
if pos is -1:
|
||||||
|
return False
|
||||||
|
scroll_to_xpos(pos)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def handle_shortcut(sc_name, evt):
|
def handle_shortcut(sc_name, evt):
|
||||||
if sc_name is 'up':
|
if sc_name is 'up':
|
||||||
scroll_by_page(True, True)
|
scroll_by_page(True, True)
|
||||||
|
@ -219,7 +219,7 @@ class View:
|
|||||||
E.div(style='position: absolute; top:0; left:0; width: 100%; height: 100%; display:none', id='book-content-popup-overlay'), # content popup overlay
|
E.div(style='position: absolute; top:0; left:0; width: 100%; height: 100%; display:none', id='book-content-popup-overlay'), # content popup overlay
|
||||||
E.div(style='position: absolute; top:0; left:0; width: 100%; height: 100%; overflow: auto; display:none', id='book-overlay'), # main overlay
|
E.div(style='position: absolute; top:0; left:0; width: 100%; height: 100%; overflow: auto; display:none', id='book-overlay'), # main overlay
|
||||||
E.div(style='position: absolute; top:0; left:0; width: 100%; height: 100%; display:none', id='controls-help-overlay'), # controls help overlay
|
E.div(style='position: absolute; top:0; left:0; width: 100%; height: 100%; display:none', id='controls-help-overlay'), # controls help overlay
|
||||||
E.div(style='position: absolute; top:0; left:0; width: 100%; height: 100%; display:none', id=CreateAnnotation.container_id), # create annotation overlay
|
E.div(style='position: absolute; top:0; left:0; width: 100%; height: 100%; display:none', id=CreateAnnotation.container_id, tabindex='0'), # create annotation overlay
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
E.div(
|
E.div(
|
||||||
|
@ -44,8 +44,11 @@ def word_at_point(x, y):
|
|||||||
return r
|
return r
|
||||||
|
|
||||||
|
|
||||||
def range_extents(start, end):
|
def range_extents(start, end, in_flow_mode):
|
||||||
ans = {'start': {'x': None, 'y': None, 'height': None}, 'end': {'x': None, 'y': None, 'height': None}}
|
ans = {
|
||||||
|
'start': {'x': None, 'y': None, 'height': None, 'onscreen': False},
|
||||||
|
'end': {'x': None, 'y': None, 'height': None, 'onscreen': False}
|
||||||
|
}
|
||||||
if not start or not end:
|
if not start or not end:
|
||||||
return ans
|
return ans
|
||||||
start = start.cloneRange()
|
start = start.cloneRange()
|
||||||
@ -58,9 +61,11 @@ def range_extents(start, end):
|
|||||||
if not rects.length:
|
if not rects.length:
|
||||||
return
|
return
|
||||||
rect = rects[0]
|
rect = rects[0]
|
||||||
ans.x = rect.left
|
ans.x = Math.round(rect.left)
|
||||||
ans.y = rect.top
|
ans.y = Math.round(rect.top)
|
||||||
ans.height = rect.bottom - rect.top
|
ans.height = rect.bottom - rect.top
|
||||||
|
if rect.right <= window.innerWidth and rect.bottom <= window.innerHeight and rect.left >= 0 and rect.top >= 0:
|
||||||
|
ans.onscreen = True
|
||||||
|
|
||||||
for_boundary(start, ans.start)
|
for_boundary(start, ans.start)
|
||||||
for_boundary(end, ans.end)
|
for_boundary(end, ans.end)
|
||||||
@ -68,16 +73,16 @@ def range_extents(start, end):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
def selection_extents():
|
def selection_extents(in_flow_mode):
|
||||||
sel = window.getSelection()
|
sel = window.getSelection()
|
||||||
if not sel or not sel.rangeCount:
|
if not sel or not sel.rangeCount:
|
||||||
return range_extents()
|
return range_extents()
|
||||||
start = sel.getRangeAt(0)
|
start = sel.getRangeAt(0)
|
||||||
end = sel.getRangeAt(sel.rangeCount - 1)
|
end = sel.getRangeAt(sel.rangeCount - 1)
|
||||||
return range_extents(start, end)
|
return range_extents(start, end, in_flow_mode)
|
||||||
|
|
||||||
|
|
||||||
def selection_extents_at_point(x, y):
|
def selection_extents_at_point(x, y, in_flow_mode):
|
||||||
r = word_at_point(x, y)
|
r = word_at_point(x, y)
|
||||||
if r:
|
if r:
|
||||||
sel = window.getSelection()
|
sel = window.getSelection()
|
||||||
@ -89,17 +94,65 @@ def selection_extents_at_point(x, y):
|
|||||||
ans.start.height = ans.end.height = parseInt(window.getComputedStyle(document.body).fontSize) + 4
|
ans.start.height = ans.end.height = parseInt(window.getComputedStyle(document.body).fontSize) + 4
|
||||||
ans.start.x = x
|
ans.start.x = x
|
||||||
ans.end.x = x + ans.start.height * 3
|
ans.end.x = x + ans.start.height * 3
|
||||||
|
ans.start.onscreen = ans.end.onscreen = True
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
|
|
||||||
|
def extend_selection_after_scroll(backwards, in_flow_mode):
|
||||||
|
sel = window.getSelection()
|
||||||
|
if not sel.rangeCount:
|
||||||
|
return
|
||||||
|
r = sel.getRangeAt(0)
|
||||||
|
q = r.cloneRange()
|
||||||
|
q.collapse(backwards)
|
||||||
|
rects = r.getClientRects()
|
||||||
|
if not rects.length:
|
||||||
|
return
|
||||||
|
rect = rects[0]
|
||||||
|
if backwards:
|
||||||
|
if in_flow_mode and rect.bottom <= window.innerHeight:
|
||||||
|
return
|
||||||
|
if not in_flow_mode and rect.right <= window.innerWidth:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
if in_flow_mode and rect.top >= 0:
|
||||||
|
return
|
||||||
|
if not in_flow_mode and rect.left >= 0:
|
||||||
|
return
|
||||||
|
x = window.innerWidth // 2
|
||||||
|
y = window.innerHeight // 3
|
||||||
|
if backwards:
|
||||||
|
y *= 2
|
||||||
|
p = range_from_point(x, y)
|
||||||
|
if p:
|
||||||
|
if backwards:
|
||||||
|
r.setStart(p.startContainer, p.startOffset)
|
||||||
|
else:
|
||||||
|
r.setEnd(p.startContainer, p.startOffset)
|
||||||
|
|
||||||
|
|
||||||
def set_selections_extents_to(extents):
|
def set_selections_extents_to(extents):
|
||||||
start = range_from_point(extents.start.x, extents.start.y)
|
if extents.start.onscreen and extents.end.onscreen:
|
||||||
if start:
|
start = range_from_point(extents.start.x, extents.start.y)
|
||||||
|
if start:
|
||||||
|
end = range_from_point(extents.end.x, extents.end.y)
|
||||||
|
if end:
|
||||||
|
r = document.createRange()
|
||||||
|
r.setStart(start.startContainer, start.startOffset)
|
||||||
|
r.setEnd(end.startContainer, end.startOffset)
|
||||||
|
sel = window.getSelection()
|
||||||
|
sel.removeAllRanges()
|
||||||
|
sel.addRange(r)
|
||||||
|
return
|
||||||
|
sel = window.getSelection()
|
||||||
|
if not sel.rangeCount:
|
||||||
|
return
|
||||||
|
r = sel.getRangeAt(0)
|
||||||
|
if extents.start.onscreen:
|
||||||
|
start = range_from_point(extents.start.x, extents.start.y)
|
||||||
|
if start:
|
||||||
|
r.setStart(start.startContainer, start.startOffset)
|
||||||
|
if extents.end.onscreen:
|
||||||
end = range_from_point(extents.end.x, extents.end.y)
|
end = range_from_point(extents.end.x, extents.end.y)
|
||||||
if end:
|
if end:
|
||||||
r = document.createRange()
|
|
||||||
r.setStart(start.startContainer, start.startOffset)
|
|
||||||
r.setEnd(end.startContainer, end.startOffset)
|
r.setEnd(end.startContainer, end.startOffset)
|
||||||
sel = window.getSelection()
|
|
||||||
sel.removeAllRanges()
|
|
||||||
sel.addRange(r)
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user