mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-07 10:14:46 -04:00
Browser viewer: Allow long tapping a word to look it up in a dictionary or search the internet for it. Fixes #1738995 [[Browser Viewer] not possible to select text on Android](https://bugs.launchpad.net/calibre/+bug/1738995)
This commit is contained in:
parent
57a027c31e
commit
2e8692bfcb
@ -2,6 +2,8 @@
|
|||||||
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
from __python__ import bound_methods, hash_literals
|
from __python__ import bound_methods, hash_literals
|
||||||
|
|
||||||
|
from select import word_at_point
|
||||||
|
|
||||||
from dom import set_css
|
from dom import set_css
|
||||||
from keycodes import get_key
|
from keycodes import get_key
|
||||||
from read_book.globals import get_boss
|
from read_book.globals import get_boss
|
||||||
@ -164,6 +166,14 @@ def handle_gesture(gesture):
|
|||||||
scroll_by_page(True)
|
scroll_by_page(True)
|
||||||
elif gesture.type is 'next-page':
|
elif gesture.type is 'next-page':
|
||||||
scroll_by_page(False)
|
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 = {
|
anchor_funcs = {
|
||||||
'pos_for_elem': def pos_for_elem(elem):
|
'pos_for_elem': def pos_for_elem(elem):
|
||||||
|
@ -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.font_size import create_font_size_panel
|
||||||
from read_book.prefs.main import create_prefs_panel
|
from read_book.prefs.main import create_prefs_panel
|
||||||
from read_book.toc import create_toc_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 session import get_device_uuid
|
||||||
from widgets import create_button, create_spinner
|
from widgets import create_button, create_spinner
|
||||||
|
|
||||||
@ -309,6 +310,26 @@ class TOCOverlay: # {{{
|
|||||||
self.overlay.view.goto_named_destination(dest, frag)
|
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: # {{{
|
class PrefsOverlay: # {{{
|
||||||
|
|
||||||
def __init__(self, overlay):
|
def __init__(self, overlay):
|
||||||
@ -330,13 +351,15 @@ class PrefsOverlay: # {{{
|
|||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
class FontSizeOverlay:
|
class FontSizeOverlay: # {{{
|
||||||
|
|
||||||
def __init__(self, overlay):
|
def __init__(self, overlay):
|
||||||
self.overlay = overlay
|
self.overlay = overlay
|
||||||
|
|
||||||
def show(self, container):
|
def show(self, container):
|
||||||
create_font_size_panel(container, self.overlay.hide_current_panel)
|
create_font_size_panel(container, self.overlay.hide_current_panel)
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
|
||||||
class Overlay:
|
class Overlay:
|
||||||
|
|
||||||
@ -439,3 +462,8 @@ class Overlay:
|
|||||||
self.hide_current_panel()
|
self.hide_current_panel()
|
||||||
self.panels.push(FontSizeOverlay(self))
|
self.panels.push(FontSizeOverlay(self))
|
||||||
self.show_current_panel()
|
self.show_current_panel()
|
||||||
|
|
||||||
|
def show_word_actions(self, word):
|
||||||
|
self.hide_current_panel()
|
||||||
|
self.panels.push(WordActionsOverlay(word, self))
|
||||||
|
self.show_current_panel()
|
||||||
|
@ -4,6 +4,7 @@ from __python__ import hash_literals
|
|||||||
|
|
||||||
import traceback
|
import traceback
|
||||||
from elementmaker import E
|
from elementmaker import E
|
||||||
|
from select import word_at_point
|
||||||
|
|
||||||
from dom import set_css
|
from dom import set_css
|
||||||
from keycodes import get_key
|
from keycodes import get_key
|
||||||
@ -507,6 +508,13 @@ def handle_gesture(gesture):
|
|||||||
scroll_by_page(True, False)
|
scroll_by_page(True, False)
|
||||||
elif gesture.type is 'next-page':
|
elif gesture.type is 'next-page':
|
||||||
scroll_by_page(False, False)
|
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 = {
|
anchor_funcs = {
|
||||||
|
@ -10,6 +10,7 @@ HOLD_THRESHOLD = 750 # milliseconds
|
|||||||
TAP_THRESHOLD = 7 # pixels
|
TAP_THRESHOLD = 7 # pixels
|
||||||
TAP_LINK_THRESHOLD = 5 # pixels
|
TAP_LINK_THRESHOLD = 5 # pixels
|
||||||
PINCH_THRESHOLD = 10 # pixels
|
PINCH_THRESHOLD = 10 # pixels
|
||||||
|
LONG_TAP_THRESHOLD = 500 # milliseconds
|
||||||
|
|
||||||
gesture_id = 0
|
gesture_id = 0
|
||||||
|
|
||||||
@ -39,7 +40,7 @@ def max_displacement(points):
|
|||||||
def interpret_single_gesture(touch, gesture_id):
|
def interpret_single_gesture(touch, gesture_id):
|
||||||
max_x_displacement = max_displacement(touch.viewport_x)
|
max_x_displacement = max_displacement(touch.viewport_x)
|
||||||
max_y_displacement = max_displacement(touch.viewport_y)
|
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:
|
if max(max_x_displacement, max_y_displacement) < TAP_THRESHOLD:
|
||||||
ans.type = 'tap'
|
ans.type = 'tap'
|
||||||
ans.viewport_x = touch.viewport_x[0]
|
ans.viewport_x = touch.viewport_x[0]
|
||||||
@ -234,15 +235,10 @@ class BookTouchHandler(TouchHandler):
|
|||||||
gesture.from_side_margin = self.for_side_margin
|
gesture.from_side_margin = self.for_side_margin
|
||||||
if gesture.type is 'tap':
|
if gesture.type is 'tap':
|
||||||
if gesture.is_held:
|
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
|
self.handled_tap_hold = True
|
||||||
fake_click = new MouseEvent('click', {
|
gesture.type = 'long-tap'
|
||||||
'clientX': gesture.viewport_x, 'clientY':gesture.viewport_y, 'buttons': 1,
|
get_boss().handle_gesture(gesture)
|
||||||
'view':window.self, 'bubbles': True, 'cancelable': True,
|
|
||||||
})
|
|
||||||
elem = document.elementFromPoint(fake_click.clientX, fake_click.clientY)
|
|
||||||
if elem:
|
|
||||||
elem.dispatchEvent(fake_click)
|
|
||||||
return
|
return
|
||||||
if not gesture.active:
|
if not gesture.active:
|
||||||
if self.for_side_margin or not tap_on_link(gesture):
|
if self.for_side_margin or not tap_on_link(gesture):
|
||||||
|
@ -150,6 +150,7 @@ class View:
|
|||||||
'error': self.on_iframe_error,
|
'error': self.on_iframe_error,
|
||||||
'next_spine_item': self.on_next_spine_item,
|
'next_spine_item': self.on_next_spine_item,
|
||||||
'next_section': self.on_next_section,
|
'next_section': self.on_next_section,
|
||||||
|
'lookup_word': self.on_lookup_word,
|
||||||
'goto_doc_boundary': def(data): self.goto_doc_boundary(data.start);,
|
'goto_doc_boundary': def(data): self.goto_doc_boundary(data.start);,
|
||||||
'scroll_to_anchor': self.on_scroll_to_anchor,
|
'scroll_to_anchor': self.on_scroll_to_anchor,
|
||||||
'update_cfi': self.on_update_cfi,
|
'update_cfi': self.on_update_cfi,
|
||||||
@ -174,6 +175,9 @@ class View:
|
|||||||
def iframe(self):
|
def iframe(self):
|
||||||
return self.iframe_wrapper.iframe
|
return self.iframe_wrapper.iframe
|
||||||
|
|
||||||
|
def on_lookup_word(self, data):
|
||||||
|
self.overlay.show_word_actions(data.word)
|
||||||
|
|
||||||
def left_margin_clicked(self, event):
|
def left_margin_clicked(self, event):
|
||||||
if event.button is 0:
|
if event.button is 0:
|
||||||
event.preventDefault(), event.stopPropagation()
|
event.preventDefault(), event.stopPropagation()
|
||||||
|
26
src/pyj/read_book/word_actions.pyj
Normal file
26
src/pyj/read_book/word_actions.pyj
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# vim:fileencoding=utf-8
|
||||||
|
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
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))
|
44
src/pyj/select.pyj
Normal file
44
src/pyj/select.pyj
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
# vim:fileencoding=utf-8
|
||||||
|
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
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
|
Loading…
x
Reference in New Issue
Block a user