diff --git a/src/pyj/read_book/flow_mode.pyj b/src/pyj/read_book/flow_mode.pyj index d98c548adb..6660bd9a31 100644 --- a/src/pyj/read_book/flow_mode.pyj +++ b/src/pyj/read_book/flow_mode.pyj @@ -2,6 +2,8 @@ # License: GPL v3 Copyright: 2016, Kovid Goyal from __python__ import bound_methods, hash_literals +from select import word_at_point + from dom import set_css from keycodes import get_key from read_book.globals import get_boss @@ -164,6 +166,14 @@ def handle_gesture(gesture): scroll_by_page(True) elif gesture.type is 'next-page': scroll_by_page(False) + elif gesture.type is 'long-tap': + r = word_at_point(gesture.viewport_x, gesture.viewport_y) + if r: + s = document.getSelection() + s.removeAllRanges() + s.addRange(r) + get_boss().send_message('lookup_word', word=str(r)) + anchor_funcs = { 'pos_for_elem': def pos_for_elem(elem): diff --git a/src/pyj/read_book/overlay.pyj b/src/pyj/read_book/overlay.pyj index b98693bd06..4b68732740 100644 --- a/src/pyj/read_book/overlay.pyj +++ b/src/pyj/read_book/overlay.pyj @@ -16,6 +16,7 @@ from read_book.goto import create_goto_panel from read_book.prefs.font_size import create_font_size_panel from read_book.prefs.main import create_prefs_panel from read_book.toc import create_toc_panel +from read_book.word_actions import create_word_actions_panel from session import get_device_uuid from widgets import create_button, create_spinner @@ -309,6 +310,26 @@ class TOCOverlay: # {{{ self.overlay.view.goto_named_destination(dest, frag) # }}} +class WordActionsOverlay: # {{{ + + def __init__(self, word, overlay): + self.word = word + self.overlay = overlay + + def on_container_click(self, evt): + pass # Dont allow panel to be closed by a click + + def show(self, container): + container.style.backgroundColor = get_color('window-background') + container.appendChild(E.div( + style='padding: 1ex 1em; border-bottom: solid 1px currentColor; display:flex; justify-content: space-between', + E.h2(_('Lookup: {}').format(self.word)), + E.div(svgicon('close'), style='cursor:pointer', onclick=def(event):event.preventDefault(), event.stopPropagation(), self.overlay.hide_current_panel(event);, class_='simple-link'), + )) + container.appendChild(E.div()) + create_word_actions_panel(container, self.word, self.overlay.hide) +# }}} + class PrefsOverlay: # {{{ def __init__(self, overlay): @@ -330,13 +351,15 @@ class PrefsOverlay: # {{{ # }}} -class FontSizeOverlay: +class FontSizeOverlay: # {{{ def __init__(self, overlay): self.overlay = overlay def show(self, container): create_font_size_panel(container, self.overlay.hide_current_panel) +# }}} + class Overlay: @@ -439,3 +462,8 @@ class Overlay: self.hide_current_panel() self.panels.push(FontSizeOverlay(self)) self.show_current_panel() + + def show_word_actions(self, word): + self.hide_current_panel() + self.panels.push(WordActionsOverlay(word, self)) + self.show_current_panel() diff --git a/src/pyj/read_book/paged_mode.pyj b/src/pyj/read_book/paged_mode.pyj index 357ea383e0..0209169c15 100644 --- a/src/pyj/read_book/paged_mode.pyj +++ b/src/pyj/read_book/paged_mode.pyj @@ -4,6 +4,7 @@ from __python__ import hash_literals import traceback from elementmaker import E +from select import word_at_point from dom import set_css from keycodes import get_key @@ -507,6 +508,13 @@ def handle_gesture(gesture): scroll_by_page(True, False) elif gesture.type is 'next-page': scroll_by_page(False, False) + elif gesture.type is 'long-tap': + r = word_at_point(gesture.viewport_x, gesture.viewport_y) + if r: + s = document.getSelection() + s.removeAllRanges() + s.addRange(r) + get_boss().send_message('lookup_word', word=str(r)) anchor_funcs = { diff --git a/src/pyj/read_book/touch.pyj b/src/pyj/read_book/touch.pyj index 930839028c..83c05f6883 100644 --- a/src/pyj/read_book/touch.pyj +++ b/src/pyj/read_book/touch.pyj @@ -10,6 +10,7 @@ HOLD_THRESHOLD = 750 # milliseconds TAP_THRESHOLD = 7 # pixels TAP_LINK_THRESHOLD = 5 # pixels PINCH_THRESHOLD = 10 # pixels +LONG_TAP_THRESHOLD = 500 # milliseconds gesture_id = 0 @@ -39,7 +40,7 @@ def max_displacement(points): def interpret_single_gesture(touch, gesture_id): max_x_displacement = max_displacement(touch.viewport_x) max_y_displacement = max_displacement(touch.viewport_y) - ans = {'active':touch.active, 'is_held':touch.is_held, 'id':gesture_id} + ans = {'active':touch.active, 'is_held':touch.is_held, 'id':gesture_id, 'start_time': touch.ctime} if max(max_x_displacement, max_y_displacement) < TAP_THRESHOLD: ans.type = 'tap' ans.viewport_x = touch.viewport_x[0] @@ -234,15 +235,10 @@ class BookTouchHandler(TouchHandler): gesture.from_side_margin = self.for_side_margin if gesture.type is 'tap': if gesture.is_held: - if not self.for_side_margin and not self.handled_tap_hold: + if not self.for_side_margin and not self.handled_tap_hold and window.performance.now() - gesture.start_time >= LONG_TAP_THRESHOLD: self.handled_tap_hold = True - fake_click = new MouseEvent('click', { - 'clientX': gesture.viewport_x, 'clientY':gesture.viewport_y, 'buttons': 1, - 'view':window.self, 'bubbles': True, 'cancelable': True, - }) - elem = document.elementFromPoint(fake_click.clientX, fake_click.clientY) - if elem: - elem.dispatchEvent(fake_click) + gesture.type = 'long-tap' + get_boss().handle_gesture(gesture) return if not gesture.active: if self.for_side_margin or not tap_on_link(gesture): diff --git a/src/pyj/read_book/view.pyj b/src/pyj/read_book/view.pyj index 579d61f937..e4eeb350fb 100644 --- a/src/pyj/read_book/view.pyj +++ b/src/pyj/read_book/view.pyj @@ -150,6 +150,7 @@ class View: '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, 'goto_doc_boundary': def(data): self.goto_doc_boundary(data.start);, 'scroll_to_anchor': self.on_scroll_to_anchor, 'update_cfi': self.on_update_cfi, @@ -174,6 +175,9 @@ class View: def iframe(self): return self.iframe_wrapper.iframe + def on_lookup_word(self, data): + self.overlay.show_word_actions(data.word) + def left_margin_clicked(self, event): if event.button is 0: event.preventDefault(), event.stopPropagation() diff --git a/src/pyj/read_book/word_actions.pyj b/src/pyj/read_book/word_actions.pyj new file mode 100644 index 0000000000..9c4e57d800 --- /dev/null +++ b/src/pyj/read_book/word_actions.pyj @@ -0,0 +1,26 @@ +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2018, Kovid Goyal +from __python__ import bound_methods, hash_literals + +from gettext import gettext as _ +from book_list.item_list import create_item, create_item_list + + +def lookup(close_panel_func, url): + close_panel_func() + window.open(url, '_blank') + + +def lookup_items(word, close_panel_func): + eword = encodeURIComponent(word) + items = [ + create_item(_('Lookup in Google dictionary'), lookup.bind(word, close_panel_func, f'https://google.com/search?q=define:{eword}')), + create_item(_('Lookup in Wordnik'), lookup.bind(word, close_panel_func, f'https://www.wordnik.com/words/{eword}')), + create_item(_('Search the internet'), lookup.bind(word, close_panel_func, f'https://google.com/search?q={eword}')), + ] + return items + + +def create_word_actions_panel(container, word, close_panel_func): + list_container = container.lastChild + create_item_list(list_container, lookup_items(word, close_panel_func)) diff --git a/src/pyj/select.pyj b/src/pyj/select.pyj new file mode 100644 index 0000000000..02713ec71d --- /dev/null +++ b/src/pyj/select.pyj @@ -0,0 +1,44 @@ +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2018, Kovid Goyal +from __python__ import bound_methods, hash_literals + + +def range_from_point(x, y): + r = None + if document.caretPositionFromPoint: + p = document.caretPositionFromPoint(x, y) + if p: + r = document.createRange() + r.setStart(p.offsetNode, p.offset) + r.collapse(True) + elif document.caretRangeFromPoint: + r = document.caretRangeFromPoint(x, y) + return r + + +def word_boundary_regex(): + ans = word_boundary_regex.ans + if ans is undefined: + ans = word_boundary_regex.ans = /[\s!-#%-\x2A,-/:;\x3F@\x5B-\x5D_\x7B}\u00A1\u00A7\u00AB\u00B6\u00B7\u00BB\u00BF\u037E\u0387\u055A-\u055F\u0589\u058A\u05BE\u05C0\u05C3\u05C6\u05F3\u05F4\u0609\u060A\u060C\u060D\u061B\u061E\u061F\u066A-\u066D\u06D4\u0700-\u070D\u07F7-\u07F9\u0830-\u083E\u085E\u0964\u0965\u0970\u0AF0\u0DF4\u0E4F\u0E5A\u0E5B\u0F04-\u0F12\u0F14\u0F3A-\u0F3D\u0F85\u0FD0-\u0FD4\u0FD9\u0FDA\u104A-\u104F\u10FB\u1360-\u1368\u1400\u166D\u166E\u169B\u169C\u16EB-\u16ED\u1735\u1736\u17D4-\u17D6\u17D8-\u17DA\u1800-\u180A\u1944\u1945\u1A1E\u1A1F\u1AA0-\u1AA6\u1AA8-\u1AAD\u1B5A-\u1B60\u1BFC-\u1BFF\u1C3B-\u1C3F\u1C7E\u1C7F\u1CC0-\u1CC7\u1CD3\u2010-\u2027\u2030-\u2043\u2045-\u2051\u2053-\u205E\u207D\u207E\u208D\u208E\u2329\u232A\u2768-\u2775\u27C5\u27C6\u27E6-\u27EF\u2983-\u2998\u29D8-\u29DB\u29FC\u29FD\u2CF9-\u2CFC\u2CFE\u2CFF\u2D70\u2E00-\u2E2E\u2E30-\u2E3B\u3001-\u3003\u3008-\u3011\u3014-\u301F\u3030\u303D\u30A0\u30FB\uA4FE\uA4FF\uA60D-\uA60F\uA673\uA67E\uA6F2-\uA6F7\uA874-\uA877\uA8CE\uA8CF\uA8F8-\uA8FA\uA92E\uA92F\uA95F\uA9C1-\uA9CD\uA9DE\uA9DF\uAA5C-\uAA5F\uAADE\uAADF\uAAF0\uAAF1\uABEB\uFD3E\uFD3F\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE61\uFE63\uFE68\uFE6A\uFE6B\uFF01-\uFF03\uFF05-\uFF0A\uFF0C-\uFF0F\uFF1A\uFF1B\uFF1F\uFF20\uFF3B-\uFF3D\uFF3F\uFF5B\uFF5D\uFF5F-\uFF65]/ + return ans + + +def expand_offset_to_word(string, offset): + start = offset + pat = word_boundary_regex() + while start >= 1 and not pat.test(string.charAt(start - 1)): + start -= 1 + end, sz = offset, string.length + while end < sz and not pat.test(string.charAt(end)): + end += 1 + return {'word': string[start:end], 'start': start, 'end': end} + + +def word_at_point(x, y): + r = range_from_point(x, y) + if r and r.startContainer.nodeType is 3: + word_info = expand_offset_to_word(r.startContainer.data, r.startOffset) + if word_info.word: + r.setStart(r.startContainer, word_info.start) + r.setEnd(r.startContainer, word_info.end) + return r