Allow tap/click on word to skip to speaking from it

This commit is contained in:
Kovid Goyal 2020-12-04 07:14:33 +05:30
parent 65445427fd
commit fbeb0230ff
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
4 changed files with 35 additions and 46 deletions

View File

@ -28,25 +28,6 @@ def text_nodes_in_range(r):
return ans 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): def first_annot_in_range(r, annot_id_uuid_map):
parent = r.commonAncestorContainer parent = r.commonAncestorContainer
doc = parent.ownerDocument or document doc = parent.ownerDocument or document

View File

@ -7,7 +7,7 @@ from fs_images import fix_fullscreen_svg_images
from gettext import gettext as _ from gettext import gettext as _
from iframe_comm import IframeClient from iframe_comm import IframeClient
from range_utils import ( 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, last_span_for_crw, reset_highlight_counter, select_crw, unwrap_all_crw,
unwrap_crw, wrap_text_in_range unwrap_crw, wrap_text_in_range
) )
@ -64,7 +64,7 @@ from read_book.touch import (
) )
from read_book.viewport import scroll_viewport from read_book.viewport import scroll_viewport
from select import ( 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 from utils import debounce, is_ios
@ -900,15 +900,16 @@ class IframeBoss:
if data.type is 'mark': if data.type is 'mark':
self.mark_word_being_spoken(data.num) self.mark_word_being_spoken(data.num)
elif data.type is 'play': elif data.type is 'play':
if data.x? and data.y?: text_node, offset = None, 0
r = range_for_tts(data.x, data.y) if data.pos:
text_node, offset = first_non_empty_text_node_in_range(r) r = word_at_point(data.pos.x, data.pos.y)
else: if r:
text_node, offset = None, 0 if r.startContainer?.nodeType is Node.TEXT_NODE:
text_node, offset = r.startContainer, r.startOffset
marked_text = tts_data(text_node, offset) marked_text = tts_data(text_node, offset)
sel = window.getSelection() sel = window.getSelection()
sel.removeAllRanges() 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): def mark_word_being_spoken(self, occurrence_number):
self.last_search_at = window.performance.now() self.last_search_at = window.performance.now()

View File

@ -9,7 +9,7 @@ from dom import clear, svgicon, unique_id
from gettext import gettext as _ from gettext import gettext as _
from read_book.globals import runtime, ui_operations from read_book.globals import runtime, ui_operations
from read_book.highlights import ICON_SIZE 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 from read_book.shortcuts import shortcut_for_key_event
HIDDEN = 0 HIDDEN = 0
@ -35,6 +35,7 @@ class ReadAloud:
'display: inline-flex; flex-direction: column; margin: 1rem;' 'display: inline-flex; flex-direction: column; margin: 1rem;'
)) ))
container.addEventListener('keydown', self.on_keydown, {'passive': False}) container.addEventListener('keydown', self.on_keydown, {'passive': False})
container.addEventListener('click', self.container_clicked, {'passive': False})
@property @property
def container(self): def container(self):
@ -83,17 +84,16 @@ class ReadAloud:
return return
bar_container = self.bar bar_container = self.bar
clear(bar_container) 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") bar_container.style.backgroundColor = get_color("window-background")
notes_container = E.div()
for x in [ for x in [
E.div(style='height: 4ex; display: flex; align-items: center; padding: 5px; justify-content: center'), 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.hr(style='border-top: solid 1px; margin: 0; padding: 0; display: none'),
E.div( E.div(
style='display: none; padding: 5px;', style='display: none; padding: 5px; font-size: smaller',
notes_container, E.div()
) )
]: ]:
bar_container.appendChild(x) bar_container.appendChild(x)
@ -118,6 +118,16 @@ class ReadAloud:
else: else:
bar.appendChild(cb('play', 'play', _('Start reading') if self.state is STOPPED else _('Resume reading'))) bar.appendChild(cb('play', 'play', _('Start reading') if self.state is STOPPED else _('Resume reading')))
bar.appendChild(cb('hide', 'close', _('Close Read aloud'))) 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): def play(self):
if self.state is PAUSED: if self.state is PAUSED:
@ -143,6 +153,15 @@ class ReadAloud:
elif self.state is PAUSED or self.state is STOPPED: elif self.state is PAUSED or self.state is STOPPED:
self.play() 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): def on_keydown(self, ev):
ev.stopPropagation(), ev.preventDefault() ev.stopPropagation(), ev.preventDefault()
if ev.key is 'Escape': if ev.key is 'Escape':
@ -183,4 +202,6 @@ class ReadAloud:
def handle_message(self, msg): def handle_message(self, msg):
if msg.type is 'text-extracted': if msg.type is 'text-extracted':
if msg.pos:
self.stop()
ui_operations.tts('play', {'marked_text': msg.marked_text}) ui_operations.tts('play', {'marked_text': msg.marked_text})

View File

@ -191,17 +191,3 @@ def move_end_of_selection(pos, start):
else: else:
if r.endContainer is not p.offsetNode or r.endOffset is not p.offset: if r.endContainer is not p.offsetNode or r.endOffset is not p.offset:
r.setEnd(p.offsetNode, 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