diff --git a/src/pyj/read_book/create_annotation.pyj b/src/pyj/read_book/create_annotation.pyj index 5702510faf..abf4c903b1 100644 --- a/src/pyj/read_book/create_annotation.pyj +++ b/src/pyj/read_book/create_annotation.pyj @@ -2,7 +2,8 @@ # License: GPL v3 Copyright: 2020, Kovid Goyal 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_DRAG = 2 @@ -45,6 +46,7 @@ class CreateAnnotation: self.view = view self.state = WAITING_FOR_CLICK self.left_line_height = self.right_line_height = 8 + self.in_flow_mode = False container = self.container self.position_in_handle = {'x': 0, 'y': 0} @@ -60,6 +62,20 @@ class CreateAnnotation: container.addEventListener('click', self.container_clicked, {'passive': False}) container.addEventListener('mouseup', self.mouseup_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): ev.stopPropagation(), ev.preventDefault() @@ -118,17 +134,28 @@ class CreateAnnotation: @property 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 { - 'start': {'x': Math.round(lh.right), 'y': Math.round(lh.bottom - self.left_line_height // 2)}, - 'end': {'x': Math.round(rh.left), 'y': Math.round(rh.bottom - self.right_line_height // 2)} + 'start': { + '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): - self.container.style.display = 'block' + c = self.container + c.style.display = 'block' + c.focus() 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): self.view.iframe_wrapper.send_message('annotations', type=type, **kw) @@ -142,9 +169,12 @@ class CreateAnnotation: self.hide_handles() if msg.extents.start.x is not None: self.place_handles(msg.extents) + self.in_flow_mode = msg.in_flow_mode elif msg.type is 'position-handles': if self.state is WAITING_FOR_CLICK: self.place_handles(msg.extents) + elif msg.type is 'update-handles': + self.place_handles(msg.extents) else: print('Ignoring annotations message with unknown type:', msg.type) @@ -158,12 +188,12 @@ class CreateAnnotation: def do_it(handle, data): map_from_iframe_coords(data) s = handle.style - s.display = 'block' + 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 = min(max(0, data.y + data.height), window.innerHeight) + bottom = data.y + data.height top = bottom - height s.top = f'{top}px' return s, width diff --git a/src/pyj/read_book/flow_mode.pyj b/src/pyj/read_book/flow_mode.pyj index 80bc81d235..2320483cd2 100644 --- a/src/pyj/read_book/flow_mode.pyj +++ b/src/pyj/read_book/flow_mode.pyj @@ -140,6 +140,16 @@ def scroll_by_page(direction): h = scroll_viewport.height() - 10 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(): return scroll_animator.is_active() diff --git a/src/pyj/read_book/iframe.pyj b/src/pyj/read_book/iframe.pyj index e7ec0e0ca3..2e5581728d 100644 --- a/src/pyj/read_book/iframe.pyj +++ b/src/pyj/read_book/iframe.pyj @@ -5,7 +5,8 @@ from __python__ import bound_methods, hash_literals import traceback from gettext import gettext as _ 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 @@ -17,7 +18,8 @@ from read_book.flow_mode import ( anchor_funcs as flow_anchor_funcs, auto_scroll_action as flow_auto_scroll_action, 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 + 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.globals import ( @@ -34,6 +36,7 @@ from read_book.paged_mode import ( 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, will_columns_per_screen_change ) @@ -207,6 +210,7 @@ class IframeBoss: 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 paged_auto_scroll_action('stop') else: self.do_layout = paged_layout @@ -217,6 +221,7 @@ class IframeBoss: 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 flow_auto_scroll_action('stop') update_settings(data.settings) self.keyboard_shortcut_map = create_shortcut_map(data.settings.keyboard_shortcuts) @@ -605,21 +610,31 @@ class IframeBoss: end_reference_mode() def initiate_creation_of_annotation(self): + self.auto_scroll_action('stop') + in_flow_mode = current_layout_mode() is 'flow' self.send_message( 'annotations', type='create-annotation', - in_flow_mode=current_layout_mode() is 'flow', - extents=selection_extents(), + in_flow_mode=in_flow_mode, + extents=selection_extents(in_flow_mode), ) def annotations_msg_received(self, data): + in_flow_mode = current_layout_mode() is 'flow' if data.type is 'set-selection': set_selections_extents_to(data.extents) elif data.type is 'position-handles-at-point': self.send_message( 'annotations', 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: console.log('Ignoring annotations message to iframe with unknown type: ' + data.type) diff --git a/src/pyj/read_book/paged_mode.pyj b/src/pyj/read_book/paged_mode.pyj index e2bb712378..d5d31e6c15 100644 --- a/src/pyj/read_book/paged_mode.pyj +++ b/src/pyj/read_book/paged_mode.pyj @@ -562,6 +562,14 @@ def scroll_by_page(backward, by_screen): 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): if sc_name is 'up': scroll_by_page(True, True) diff --git a/src/pyj/read_book/view.pyj b/src/pyj/read_book/view.pyj index 30cfd45abd..71c44e7e75 100644 --- a/src/pyj/read_book/view.pyj +++ b/src/pyj/read_book/view.pyj @@ -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%; 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=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( diff --git a/src/pyj/select.pyj b/src/pyj/select.pyj index 0ff69be331..987a022007 100644 --- a/src/pyj/select.pyj +++ b/src/pyj/select.pyj @@ -44,8 +44,11 @@ def word_at_point(x, y): return r -def range_extents(start, end): - ans = {'start': {'x': None, 'y': None, 'height': None}, 'end': {'x': None, 'y': None, 'height': None}} +def range_extents(start, end, in_flow_mode): + 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: return ans start = start.cloneRange() @@ -58,9 +61,11 @@ def range_extents(start, end): if not rects.length: return rect = rects[0] - ans.x = rect.left - ans.y = rect.top + ans.x = Math.round(rect.left) + ans.y = Math.round(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(end, ans.end) @@ -68,16 +73,16 @@ def range_extents(start, end): -def selection_extents(): +def selection_extents(in_flow_mode): sel = window.getSelection() if not sel or not sel.rangeCount: return range_extents() start = sel.getRangeAt(0) 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) if r: 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.x = x ans.end.x = x + ans.start.height * 3 + ans.start.onscreen = ans.end.onscreen = True 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): - start = range_from_point(extents.start.x, extents.start.y) - if start: + if extents.start.onscreen and extents.end.onscreen: + 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) 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)