From fbeb0230ff2db5cf00de16e5dcb55b9025b09af7 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 4 Dec 2020 07:14:33 +0530 Subject: [PATCH] Allow tap/click on word to skip to speaking from it --- src/pyj/range_utils.pyj | 19 ------------------- src/pyj/read_book/iframe.pyj | 17 +++++++++-------- src/pyj/read_book/read_aloud.pyj | 31 ++++++++++++++++++++++++++----- src/pyj/select.pyj | 14 -------------- 4 files changed, 35 insertions(+), 46 deletions(-) diff --git a/src/pyj/range_utils.pyj b/src/pyj/range_utils.pyj index b0f55fe812..55cc7603fd 100644 --- a/src/pyj/range_utils.pyj +++ b/src/pyj/range_utils.pyj @@ -28,25 +28,6 @@ def text_nodes_in_range(r): return ans -def first_non_empty_text_node_in_range(r): - parent = r.commonAncestorContainer - doc = parent.ownerDocument or document - iterator = doc.createNodeIterator(parent) - in_range = False - while True: - node = iterator.nextNode() - if not node: - break - if not in_range and node.isSameNode(r.startContainer): - in_range = True - if in_range: - if is_non_empty_text_node(node): - return node - if node.isSameNode(r.endContainer): - break - - - def first_annot_in_range(r, annot_id_uuid_map): parent = r.commonAncestorContainer doc = parent.ownerDocument or document diff --git a/src/pyj/read_book/iframe.pyj b/src/pyj/read_book/iframe.pyj index 6c192f13d4..f1591b2ebb 100644 --- a/src/pyj/read_book/iframe.pyj +++ b/src/pyj/read_book/iframe.pyj @@ -7,7 +7,7 @@ from fs_images import fix_fullscreen_svg_images from gettext import gettext as _ from iframe_comm import IframeClient from range_utils import ( - first_non_empty_text_node_in_range, highlight_associated_with_selection, + highlight_associated_with_selection, last_span_for_crw, reset_highlight_counter, select_crw, unwrap_all_crw, unwrap_crw, wrap_text_in_range ) @@ -64,7 +64,7 @@ from read_book.touch import ( ) from read_book.viewport import scroll_viewport from select import ( - move_end_of_selection, range_for_tts, selection_extents, word_at_point + move_end_of_selection, selection_extents, word_at_point ) from utils import debounce, is_ios @@ -900,15 +900,16 @@ class IframeBoss: if data.type is 'mark': self.mark_word_being_spoken(data.num) elif data.type is 'play': - if data.x? and data.y?: - r = range_for_tts(data.x, data.y) - text_node, offset = first_non_empty_text_node_in_range(r) - else: - text_node, offset = None, 0 + text_node, offset = None, 0 + if data.pos: + r = word_at_point(data.pos.x, data.pos.y) + if r: + if r.startContainer?.nodeType is Node.TEXT_NODE: + text_node, offset = r.startContainer, r.startOffset marked_text = tts_data(text_node, offset) sel = window.getSelection() sel.removeAllRanges() - self.send_message('tts', type='text-extracted', marked_text=marked_text) + self.send_message('tts', type='text-extracted', marked_text=marked_text, pos=data.pos) def mark_word_being_spoken(self, occurrence_number): self.last_search_at = window.performance.now() diff --git a/src/pyj/read_book/read_aloud.pyj b/src/pyj/read_book/read_aloud.pyj index 2c087869f3..f994c2af0f 100644 --- a/src/pyj/read_book/read_aloud.pyj +++ b/src/pyj/read_book/read_aloud.pyj @@ -9,7 +9,7 @@ from dom import clear, svgicon, unique_id from gettext import gettext as _ from read_book.globals import runtime, ui_operations from read_book.highlights import ICON_SIZE -from read_book.selection_bar import BUTTON_MARGIN +from read_book.selection_bar import BUTTON_MARGIN, get_margins, map_to_iframe_coords from read_book.shortcuts import shortcut_for_key_event HIDDEN = 0 @@ -35,6 +35,7 @@ class ReadAloud: 'display: inline-flex; flex-direction: column; margin: 1rem;' )) container.addEventListener('keydown', self.on_keydown, {'passive': False}) + container.addEventListener('click', self.container_clicked, {'passive': False}) @property def container(self): @@ -83,17 +84,16 @@ class ReadAloud: return bar_container = self.bar clear(bar_container) - bar_container.style.maxWidth = 'min(50rem, 90vw)' if self.supports_css_min_max else '50rem' + bar_container.style.maxWidth = 'min(40rem, 80vw)' if self.supports_css_min_max else '40rem' bar_container.style.backgroundColor = get_color("window-background") - notes_container = E.div() for x in [ E.div(style='height: 4ex; display: flex; align-items: center; padding: 5px; justify-content: center'), E.hr(style='border-top: solid 1px; margin: 0; padding: 0; display: none'), E.div( - style='display: none; padding: 5px;', - notes_container, + style='display: none; padding: 5px; font-size: smaller', + E.div() ) ]: bar_container.appendChild(x) @@ -118,6 +118,16 @@ class ReadAloud: else: bar.appendChild(cb('play', 'play', _('Start reading') if self.state is STOPPED else _('Resume reading'))) bar.appendChild(cb('hide', 'close', _('Close Read aloud'))) + if self.state is not WAITING_FOR_PLAY_TO_START: + notes_container = bar_container.lastChild + notes_container.style.display = notes_container.previousSibling.style.display = 'block' + notes_container = notes_container.lastChild + if self.state is STOPPED: + notes_container.textContent = _('Tap/click on a word to start from there') + elif self.state is PLAYING: + notes_container.textContent = _('Tap/click on a word to skip to it') + else: + notes_container.textContent = _('Tap/click on a word to continue from there') def play(self): if self.state is PAUSED: @@ -143,6 +153,15 @@ class ReadAloud: elif self.state is PAUSED or self.state is STOPPED: self.play() + def container_clicked(self, ev): + if ev.button is not 0: + return + ev.stopPropagation(), ev.preventDefault() + margins = get_margins() + pos = {'x': ev.clientX, 'y': ev.clientY} + pos = map_to_iframe_coords(pos, margins) + self.send_message('play', pos=pos) + def on_keydown(self, ev): ev.stopPropagation(), ev.preventDefault() if ev.key is 'Escape': @@ -183,4 +202,6 @@ class ReadAloud: def handle_message(self, msg): if msg.type is 'text-extracted': + if msg.pos: + self.stop() ui_operations.tts('play', {'marked_text': msg.marked_text}) diff --git a/src/pyj/select.pyj b/src/pyj/select.pyj index 383970c736..35102bf199 100644 --- a/src/pyj/select.pyj +++ b/src/pyj/select.pyj @@ -191,17 +191,3 @@ def move_end_of_selection(pos, start): else: if r.endContainer is not p.offsetNode or r.endOffset is not p.offset: r.setEnd(p.offsetNode, p.offset) - - -def range_for_tts(x, y): - p = None - if x? and y?: - p = caret_position_from_point(x, y) - if not p: - p = caret_position_from_point(0, 0) - if not p: - p = {'offsetNode': document.body, 'offset': 0} - r = document.createRange() - r.setStart(p.offsetNode, p.offset) - r.setEndAfter(document.body) - return r