Implement drag scrolling for highlight handles

This commit is contained in:
Kovid Goyal 2020-07-08 14:04:08 +05:30
parent 86f5976ea5
commit 1148208f45
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
4 changed files with 138 additions and 95 deletions

View File

@ -177,6 +177,7 @@ def map_to_iframe_coords(point):
BAR_SIZE = 32 BAR_SIZE = 32
DRAG_SCROLL_ZONE_MIN_HEIGHT = 10
def create_bar(): def create_bar():
@ -194,6 +195,8 @@ class CreateAnnotation:
def __init__(self, view): def __init__(self, view):
self.view = view self.view = view
self.active_touch = None self.active_touch = None
self.drag_scroll_timer = None
self.last_drag_scroll_at = -100000
self.editing_annot_uuid = None self.editing_annot_uuid = None
self.current_notes = '' self.current_notes = ''
self.annotations_manager = self.view.annotations_manager self.annotations_manager = self.view.annotations_manager
@ -227,7 +230,7 @@ class CreateAnnotation:
tb = create_bar() tb = create_bar()
container.appendChild(tb) container.appendChild(tb)
button(tb, 'close', _('Cancel creation of highlight'), self.hide) button(tb, 'close', _('Cancel creation of highlight'), self.hide)
button(tb, 'chevron-up', _('Scroll up'), self.scroll_up) button(tb, 'chevron-up', _('Scroll up'), self.button_scroll.bind(None, True))
tb.appendChild(E.span(style=f'height: {tb.style.height}')) tb.appendChild(E.span(style=f'height: {tb.style.height}'))
button(tb.lastChild, 'trash', _('Remove this highlight'), self.delete_highlight) button(tb.lastChild, 'trash', _('Remove this highlight'), self.delete_highlight)
tb.lastChild.appendChild(E.span('\xa0\xa0\xa0')) tb.lastChild.appendChild(E.span('\xa0\xa0\xa0'))
@ -243,7 +246,7 @@ class CreateAnnotation:
bb = create_bar() bb = create_bar()
container.appendChild(bb) container.appendChild(bb)
button(bb, 'fg', _('Change highlight color'), self.choose_color) button(bb, 'fg', _('Change highlight color'), self.choose_color)
button(bb, 'chevron-down', _('Scroll down'), self.scroll_down) button(bb, 'chevron-down', _('Scroll down'), self.button_scroll)
button(bb, 'pencil', _('Add a note'), self.add_notes) button(bb, 'pencil', _('Add a note'), self.add_notes)
sd = get_session_data() sd = get_session_data()
@ -272,12 +275,6 @@ class CreateAnnotation:
def copy_to_clipboard(self): def copy_to_clipboard(self):
self.view.iframe_wrapper.send_message('copy_selection') self.view.iframe_wrapper.send_message('copy_selection')
def scroll_up(self):
self.send_message('scroll', backwards=True)
def scroll_down(self):
self.send_message('scroll', backwards=False)
@property @property
def middle(self): def middle(self):
return document.getElementById(self.middle_id) return document.getElementById(self.middle_id)
@ -455,12 +452,16 @@ class CreateAnnotation:
if sc_name is 'show_chrome': if sc_name is 'show_chrome':
self.hide() self.hide()
elif sc_name in ('up', 'down', 'pageup', 'pagedown'): elif sc_name in ('up', 'down', 'pageup', 'pagedown'):
self.send_message('scroll', backwards=bool('up' in sc_name)) backwards = 'up' in sc_name
if 'page' in sc_name or not self.in_flow_mode:
self.paged_scroll(backwards)
else:
self.send_drag_scroll_message(backwards, 'left' if backwards else 'right', False)
elif sc_name in ('left', 'right'): elif sc_name in ('left', 'right'):
if self.in_flow_mode: if self.in_flow_mode:
self.send_message('perp-scroll', backwards=bool(sc_name is 'left')) self.send_message('perp-scroll', backwards=bool(sc_name is 'left'))
else: else:
self.send_message('scroll', backwards=bool(sc_name is 'left')) self.paged_scroll(sc_name is 'left')
def container_clicked(self, ev): def container_clicked(self, ev):
ev.stopPropagation(), ev.preventDefault() ev.stopPropagation(), ev.preventDefault()
@ -495,9 +496,46 @@ class CreateAnnotation:
self.start_handle_drag(touch, ev.currentTarget.id) self.start_handle_drag(touch, ev.currentTarget.id)
break break
def button_scroll(self, backwards):
if self.in_flow_mode:
self.send_drag_scroll_message(backwards, 'left' if backwards else 'right', False)
else:
self.paged_scroll(backwards)
def paged_scroll(self, backwards):
self.send_message('paged-scroll', backwards=backwards)
def run_drag_scroll(self, mouse_y, top, bottom):
backwards = mouse_y <= top
self.do_one_drag_scroll(backwards, top - mouse_y if backwards else mouse_y - bottom)
def do_one_drag_scroll(self, backwards, distance_from_boundary):
window.clearTimeout(self.drag_scroll_timer)
self.drag_scroll_timer = None
if self.state not in (DRAGGING_RIGHT, DRAGGING_LEFT):
return
interval = 100 if self.in_flow_mode else 1200
self.drag_scroll_timer = window.setTimeout(self.do_one_drag_scroll.bind(None, backwards, distance_from_boundary), interval)
now = window.performance.now()
if now - self.last_drag_scroll_at > interval:
self.send_drag_scroll_message(backwards, 'left' if self.state is DRAGGING_LEFT else 'right', True)
self.last_drag_scroll_at = now
def send_drag_scroll_message(self, backwards, handle, extend_selection):
self.send_message(
'drag-scroll', backwards=backwards, handle=handle, extents=self.current_handle_position,
extend_selection=extend_selection)
def end_drag_scroll(self):
if self.drag_scroll_timer is not None:
window.clearTimeout(self.drag_scroll_timer)
self.drag_scroll_timer = None
self.last_drag_scroll_at = -10000
def mouseup_on_container(self, ev): def mouseup_on_container(self, ev):
if self.state in (DRAGGING_RIGHT, DRAGGING_LEFT): if self.state in (DRAGGING_RIGHT, DRAGGING_LEFT):
self.state = WAITING_FOR_DRAG self.state = WAITING_FOR_DRAG
self.end_drag_scroll()
ev.preventDefault(), ev.stopPropagation() ev.preventDefault(), ev.stopPropagation()
def touchend_on_container(self, ev): def touchend_on_container(self, ev):
@ -507,6 +545,7 @@ class CreateAnnotation:
if touch.identifier is self.active_touch: if touch.identifier is self.active_touch:
self.active_touch = None self.active_touch = None
self.state = WAITING_FOR_DRAG self.state = WAITING_FOR_DRAG
self.end_drag_scroll()
return return
def handle_moved(self, ev): def handle_moved(self, ev):
@ -518,6 +557,16 @@ class CreateAnnotation:
pos.start = map_to_iframe_coords(pos.start) pos.start = map_to_iframe_coords(pos.start)
pos.end = map_to_iframe_coords(pos.end) pos.end = map_to_iframe_coords(pos.end)
self.send_message('set-selection', extents=pos) self.send_message('set-selection', extents=pos)
c = self.container
rect = c.getBoundingClientRect()
t = document.getElementById('book-top-margin').offsetHeight
top = rect.top + max(t, DRAG_SCROLL_ZONE_MIN_HEIGHT)
t = document.getElementById('book-bottom-margin').offsetHeight
bottom = rect.bottom - max(t, DRAG_SCROLL_ZONE_MIN_HEIGHT)
if ev.clientY < top or ev.clientY > bottom:
self.run_drag_scroll(ev.clientY, top, bottom)
else:
self.end_drag_scroll()
def mousemove_on_container(self, ev): def mousemove_on_container(self, ev):
if self.state not in (DRAGGING_RIGHT, DRAGGING_LEFT): if self.state not in (DRAGGING_RIGHT, DRAGGING_LEFT):
@ -626,17 +675,10 @@ class CreateAnnotation:
self.editing_annot_uuid = msg.existing or None self.editing_annot_uuid = msg.existing or None
if self.editing_annot_uuid: if self.editing_annot_uuid:
self.current_notes = self.annotations_manager.notes_for_highlight(self.editing_annot_uuid) or '' self.current_notes = self.annotations_manager.notes_for_highlight(self.editing_annot_uuid) or ''
elif msg.type is 'scrolled':
self.place_handles_after_scroll(msg.extents, msg.handle, msg.extended)
elif msg.type is 'update-handles': elif msg.type is 'update-handles':
self.place_handles(msg.extents) self.place_handles(msg.extents)
if msg.from_scroll and not msg.selection_extended:
middle = map_from_iframe_coords({
'x': msg.page_rect.left + msg.page_rect.width // 2,
'y': msg.page_rect.top + msg.page_rect.height // 2
})
handle = self.left_handle if msg.backwards else self.right_handle
handle.style.display = 'block'
handle.style.left = f'{middle.x}px'
handle.style.top = f'{middle.y}px'
elif msg.type is 'highlight-applied': elif msg.type is 'highlight-applied':
if not msg.ok: if not msg.ok:
return error_dialog( return error_dialog(
@ -653,30 +695,42 @@ class CreateAnnotation:
self.left_handle.style.display = 'none' self.left_handle.style.display = 'none'
self.right_handle.style.display = 'none' self.right_handle.style.display = 'none'
def place_single_handle(self, handle, data):
map_from_iframe_coords(data)
s = handle.style
s.display = 'block' if data.onscreen else 'none'
height = data.height * 3
width = data.height * 2
s.width = f'{width}px'
s.height = f'{height}px'
bottom = data.y + data.height
top = bottom - height
s.top = f'{top}px'
if handle.id is self.left_handle_id:
s.left = (data.x - width) + 'px'
else:
s.left = data.x + 'px'
def place_handles(self, extents): def place_handles(self, extents):
lh, rh = self.left_handle, self.right_handle self.place_single_handle(self.left_handle, extents.start)
self.place_single_handle(self.right_handle, extents.end)
def do_it(handle, data):
map_from_iframe_coords(data)
s = handle.style
s.display = 'block' if data.onscreen else 'none'
height = data.height * 3
width = data.height * 2
s.width = f'{width}px'
s.height = f'{height}px'
bottom = data.y + data.height
top = bottom - height
s.top = f'{top}px'
return s, width
style, width = do_it(lh, extents.start)
style.left = (extents.start.x - width) + 'px'
style, width = do_it(rh, extents.end)
style.left = extents.end.x + 'px'
self.state = WAITING_FOR_DRAG self.state = WAITING_FOR_DRAG
self.left_line_height = extents.start.height self.left_line_height = extents.start.height
self.right_line_height = extents.end.height self.right_line_height = extents.end.height
def place_handles_after_scroll(self, extents, handle, extended):
if extended:
if handle is 'right':
h = self.left_handle
data = extents.start
else:
h = self.right_handle
data = extents.end
self.place_single_handle(h, data)
else:
self.place_single_handle(self.left_handle, extents.start)
self.place_single_handle(self.right_handle, extents.end)
class ViewAnnotation: class ViewAnnotation:

View File

@ -123,13 +123,15 @@ 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): def scroll_to_extend_annotation(backward, horizontal, by_page):
direction = -1 if backward else 1 direction = -1 if backward else 1
h = line_height()
if by_page:
h = (window.innerWidth if horizontal else window.innerHeight) - h
if horizontal: if horizontal:
before = window.pageXOffset before = window.pageXOffset
window.scrollBy(15 * direction, 0) window.scrollBy(h * direction, 0)
return window.pageXOffset is not before return window.pageXOffset is not before
h = scroll_viewport.height() - 10
before = window.pageYOffset before = window.pageYOffset
window.scrollBy(0, h * direction) window.scrollBy(0, h * direction)
return window.pageYOffset is not before return window.pageYOffset is not before

View File

@ -5,7 +5,7 @@ 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 (
extend_selection_after_scroll, selection_extents, selection_extents_at_point, extend_selection_to_limit, selection_extents, selection_extents_at_point,
set_selections_extents_to, word_at_point set_selections_extents_to, word_at_point
) )
@ -38,11 +38,11 @@ from read_book.mathjax import apply_mathjax
from read_book.paged_mode import ( from read_book.paged_mode import (
anchor_funcs as paged_anchor_funcs, anchor_funcs as paged_anchor_funcs,
auto_scroll_action as paged_auto_scroll_action, calc_columns_per_screen, auto_scroll_action as paged_auto_scroll_action, calc_columns_per_screen,
current_cfi, current_page_width, get_columns_per_screen_data, current_cfi, get_columns_per_screen_data, handle_gesture as paged_handle_gesture,
handle_gesture as paged_handle_gesture, handle_shortcut as paged_handle_shortcut, handle_shortcut as paged_handle_shortcut, jump_to_cfi as paged_jump_to_cfi,
jump_to_cfi as paged_jump_to_cfi, layout as paged_layout, layout as paged_layout, onwheel as paged_onwheel,
onwheel as paged_onwheel, prepare_for_resize as paged_prepare_for_resize, prepare_for_resize as paged_prepare_for_resize, progress_frac,
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_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,
@ -707,13 +707,21 @@ class IframeBoss:
extents = selection_extents(in_flow_mode) extents = selection_extents(in_flow_mode)
self.send_message( self.send_message(
'annotations', type='position-handles', extents=extents, existing=annot_id_uuid_map[annot_id]) 'annotations', type='position-handles', extents=extents, existing=annot_id_uuid_map[annot_id])
elif data.type is 'scroll': elif data.type is 'drag-scroll':
if self.scroll_to_extend_annotation(data.backwards): if self.scroll_to_extend_annotation(data.backwards):
page_rect = {'width': current_page_width()} if data.extend_selection:
extended = extend_selection_after_scroll(data.backwards, in_flow_mode, page_rect) extend_selection_to_limit(data.handle is 'left', data.backwards)
extents = selection_extents(in_flow_mode)
self.send_message( self.send_message(
'annotations', type='update-handles', extents=selection_extents(in_flow_mode), 'annotations', type='scrolled',
backwards=data.backwards, from_scroll=True, selection_extended=extended, page_rect=page_rect backwards=data.backwards, handle=data.handle, extents=extents, extended=data.extend_selection
)
elif data.type is 'paged-scroll':
if self.scroll_to_extend_annotation(data.backwards, False, True):
extents = selection_extents(in_flow_mode)
self.send_message(
'annotations', type='scrolled', backwards=data.backwards, handle=None,
extents=extents, extended=False
) )
elif data.type is 'perp-scroll': elif data.type is 'perp-scroll':
if in_flow_mode and flow_annotation_scroll(data.backwards, True): if in_flow_mode and flow_annotation_scroll(data.backwards, True):

View File

@ -98,53 +98,32 @@ def selection_extents_at_point(x, y, in_flow_mode):
return ans return ans
def extend_selection_after_scroll(backwards, in_flow_mode, page_rect): def range_at_limit(invert_x, invert_y):
page_rect.top = page_rect.left = 0 step = 10
page_rect.bottom = window.innerHeight for y in range(0, window.innerHeight + step, step):
page_rect.right = page_rect.width if invert_y:
if not in_flow_mode and not backwards: y = max(0, window.innerHeight - y)
page_rect.right = window.innerWidth for x in range(0, window.innerWidth + step, step):
page_rect.left = window.innerWidth - page_rect.width if invert_x:
page_rect.width = page_rect.right - page_rect.left x = max(0, window.innerWidth - x)
page_rect.height = page_rect.bottom - page_rect.top r = range_from_point(x, y)
if r:
return r
def extend_selection_to_limit(left, top):
sel = window.getSelection() sel = window.getSelection()
if not sel.rangeCount: if not sel or not sel.rangeCount or sel.isCollapsed:
return False
new_limit = range_at_limit(not left, not top)
if not new_limit:
return False return False
if in_flow_mode:
page_rect.width = window.innerWidth
r = sel.getRangeAt(0) r = sel.getRangeAt(0)
q = r.cloneRange() if left:
q.collapse(backwards) r.setStart(new_limit.startContainer, new_limit.startOffset)
rects = q.getClientRects() else:
if not rects.length: r.setEnd(new_limit.startContainer, new_limit.startOffset)
return False return True
rect = rects[0]
in_page_already = rect.left >= page_rect.left and rect.top >= page_rect.top and rect.bottom <= page_rect.bottom and rect.right <= page_rect.right
if in_page_already:
return True
dx = page_rect.width // 10
dy = page_rect.height // 10
middle_x = page_rect.left + page_rect.width // 2
for yi in range(1, 10):
if backwards:
y = page_rect.bottom - dy * yi
else:
y = page_rect.top + dy * yi
for xi in range(4):
xvals = v'[-1, 1]' if xi else v'[1]'
for xw in xvals:
x = middle_x + xw * dx
p = range_from_point(x, y)
if p:
if backwards:
r.setStart(p.startContainer, p.startOffset)
else:
r.setEnd(p.startContainer, p.startOffset)
return True
# could not find any content on page
return False
def set_selections_extents_to(extents): def set_selections_extents_to(extents):