Skip to content

Commit

Permalink
E-book viewer: Read Aloud: Add an option to control the position of t…
Browse files Browse the repository at this point in the history
…he popup control bar. It can now be placed along the top or bottom edges so as to overlap less with text.
  • Loading branch information
kovidgoyal committed Nov 26, 2024
1 parent 6f1d28e commit 9725e92
Show file tree
Hide file tree
Showing 8 changed files with 127 additions and 34 deletions.
39 changes: 39 additions & 0 deletions src/calibre/gui2/tts/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,41 @@ def current_voice_is_downloaded(self) -> bool:
return tts.is_voice_downloaded(v)


class BarPosition(QWidget):

def __init__(self, parent=None):
super().__init__(parent)
self.l = l = QFormLayout(self)
self.choices = c = QComboBox(self)
l.addRow(_('Position of control bar:'), c)
c.addItem(_('Floating with help text'), 'float')
c.addItem(_('Top'), 'top')
c.addItem(_('Bottom'), 'bottom')
c.addItem(_('Top right'), 'top-right')
c.addItem(_('Top left'), 'top-left')
c.addItem(_('Bottom right'), 'bottom-right')
c.addItem(_('Bottom left'), 'bottom-left')
from calibre.gui2.viewer.config import get_session_pref
self.val = get_session_pref('tts_bar_position', 'float', None)

@property
def val(self):
return self.choices.currentData()

@val.setter
def val(self, x):
idx = self.choices.findData(x)
if idx > -1:
self.choices.setCurrentIndex(idx)

def commit(self):
from calibre.gui2.viewer.config import set_session_pref
set_session_pref('tts_bar_position', self.val, None)

def restore_defaults(self):
self.val = 'float'


class ConfigDialog(Dialog):

def __init__(self, parent=None):
Expand All @@ -496,6 +531,8 @@ def setup_ui(self):
l.addWidget(esc)
self.voice_button = b = QPushButton(self)
b.clicked.connect(self.voice_action)
self.bar_position = bp = BarPosition(self)
l.addWidget(bp)
h = QHBoxLayout()
l.addLayout(h)
h.addWidget(b), h.addStretch(10), h.addWidget(self.bb)
Expand All @@ -508,6 +545,7 @@ def setup_ui(self):
def restore_defaults(self):
self.engine_choice.restore_defaults()
self.engine_specific_config.restore_defaults()
self.bar_position.restore_defaults()

def set_engine(self, engine_name: str) -> None:
metadata = self.engine_specific_config.set_engine(engine_name)
Expand Down Expand Up @@ -545,6 +583,7 @@ def accept(self):
else:
prefs.pop('engine', None)
s.save_to_config(prefs)
self.bar_position.commit()
super().accept()


Expand Down
6 changes: 5 additions & 1 deletion src/calibre/gui2/tts/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ class TTSManager(QObject):

state_event = pyqtSignal(str)
saying = pyqtSignal(int, int)
configured = pyqtSignal()

def __init__(self, parent=None):
super().__init__(parent)
Expand Down Expand Up @@ -228,14 +229,17 @@ def configure(self) -> None:
from calibre.gui2.tts.types import widget_parent
with self.resume_after() as rd:
d = ConfigDialog(parent=widget_parent(self))
if d.exec() == QDialog.DialogCode.Accepted and self._tts is not None:
if d.exec() != QDialog.DialogCode.Accepted:
return
if self._tts is not None:
rd.needs_full_resume = True
if d.engine_changed:
if rd.is_speaking:
self.tts.stop()
self._tts = None
else:
self.tts.reload_after_configure()
self.configured.emit()

def _state_changed(self, state: QTextToSpeech.State) -> None:
prev_state, self.state = self.state, state
Expand Down
10 changes: 10 additions & 0 deletions src/calibre/gui2/viewer/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ def get_session_pref(name, default=None, group='standalone_misc_settings'):
return g.get(name, default)


def set_session_pref(name, val=None, group='standalone_misc_settings'):
sd = vprefs['session_data']
g = sd.get(group, {}) if group else sd
if val is None:
g.pop(name, None)
else:
g[name] = val
vprefs['session_data'] = sd


def get_pref_group(name):
sd = vprefs['session_data']
return sd.get(name) or {}
Expand Down
2 changes: 2 additions & 0 deletions src/calibre/gui2/viewer/tts.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class TTS(QObject):

event_received = pyqtSignal(object, object)
settings_changed = pyqtSignal(object)
configured = pyqtSignal()

def __init__(self, parent=None):
super().__init__(parent)
Expand All @@ -25,6 +26,7 @@ def manager(self):
self._manager = TTSManager(self)
self._manager.saying.connect(self.saying)
self._manager.state_event.connect(self.state_event)
self._manager.configured.connect(self.configured)
return self._manager

def shutdown(self):
Expand Down
7 changes: 6 additions & 1 deletion src/calibre/gui2/viewer/web_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
from calibre.ebooks.oeb.polish.utils import guess_type
from calibre.gui2 import choose_images, config, error_dialog, safe_open_url
from calibre.gui2.viewer import link_prefix_for_location_links, performance_monitor, url_for_book_in_library
from calibre.gui2.viewer.config import load_viewer_profiles, save_viewer_profile, viewer_config_dir, vprefs
from calibre.gui2.viewer.config import get_session_pref, load_viewer_profiles, save_viewer_profile, viewer_config_dir, vprefs
from calibre.gui2.viewer.tts import TTS
from calibre.gui2.webengine import RestartingWebEngineView
from calibre.srv.code import get_translations_data
Expand Down Expand Up @@ -299,6 +299,7 @@ class ViewerBridge(Bridge):

create_view = to_js()
start_book_load = to_js()
redraw_tts_bar = to_js()
goto_toc_node = to_js()
goto_cfi = to_js()
full_screen_state_changed = to_js()
Expand Down Expand Up @@ -514,6 +515,7 @@ def __init__(self, parent=None):
self.tts = TTS(self)
self.tts.settings_changed.connect(self.tts_settings_changed)
self.tts.event_received.connect(self.tts_event_received)
self.tts.configured.connect(self.redraw_tts_bar)
self.dead_renderer_error_shown = False
self.render_process_failed.connect(self.render_process_died)
w = self.screen().availableSize().width()
Expand Down Expand Up @@ -769,6 +771,9 @@ def get_current_cfi(self, callback):
def show_home_page(self):
self.execute_when_ready('show_home_page')

def redraw_tts_bar(self):
self.execute_when_ready('redraw_tts_bar', get_session_pref('tts_bar_position', 'float', None))

def change_background_image(self, img_id):
files = choose_images(self, 'viewer-background-image', _('Choose background image'), formats=['png', 'gif', 'jpg', 'jpeg', 'webp'])
if files:
Expand Down
87 changes: 56 additions & 31 deletions src/pyj/read_book/read_aloud.pyj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ from read_book.globals import ui_operations
from read_book.highlights import ICON_SIZE
from read_book.selection_bar import BUTTON_MARGIN, get_margins, map_to_iframe_coords
from read_book.shortcuts import shortcut_for_key_event
from book_list.globals import get_session_data

HIDDEN = 0
WAITING_FOR_PLAY_TO_START = 1
Expand All @@ -26,6 +27,13 @@ def is_flow_mode():
return mode is 'flow'


def bar_class_and_position():
sd = get_session_data()
bp = sd.get('tts_bar_position')
iclass = 'floating' if 'float' in bp else 'docked'
return iclass, bp


class ReadAloud:

dont_hide_on_content_loaded = True
Expand All @@ -38,14 +46,13 @@ class ReadAloud:
container = self.container
container.setAttribute('tabindex', '0')
container.style.overflow = 'hidden'
container.style.justifyContent = 'flex-end'
container.style.alignItems = 'flex-end' if is_flow_mode() else 'flex-start'
container.appendChild(E.div(
id=self.bar_id,
style='border: solid 1px currentColor; border-radius: 5px;'
'display: flex; flex-direction: column; margin: 1rem;'
))
container.appendChild(E.div(id=self.bar_id))
container.appendChild(E.style(
f'#{self.bar_id}.floating'+'{ border: solid 1px currentColor; border-radius: 5px; display: flex;' +
' flex-direction: column; margin: 1rem; }\n\n',

f'#{self.bar_id}.docked'+'{ border-radius: 1em; height: 2em; padding:0.5em; display: flex; justify-content: center; align-items: center; }\n\n',

f'#{self.bar_id}.speaking '+'{ opacity: 0.5 }\n\n',
f'#{self.bar_id}.speaking:hover '+'{ opacity: 1.0 }\n\n',
))
Expand Down Expand Up @@ -95,31 +102,13 @@ class ReadAloud:
def focus(self):
self.container.focus()

def build_bar(self, annot_id):
if self.state is HIDDEN:
return
self.container.style.alignItems = 'flex-end' if is_flow_mode() else 'flex-start'
bar_container = self.bar
if self.state is PLAYING:
bar_container.classList.add('speaking')
else:
bar_container.classList.remove('speaking')
clear(bar_container)
bar_container.style.maxWidth = 'min(40rem, 80vw)'
bar_container.style.backgroundColor = get_color("window-background")
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; font-size: smaller',
E.div()
)
]:
bar_container.appendChild(x)
bar = bar_container.firstChild
def build_docked_bar(self, bar_container, bar_position):
container = self.container
container.style.alignItems = 'flex-end' if 'bottom' in bar_position else 'flex-start'
container.style.justifyContent = 'flex-end' if 'right' in bar_position else ('flex-start' if 'left' in bar_position else 'center')
self.create_buttons(bar_container)

def create_buttons(self, bar):
def cb(name, icon, text):
ans = svgicon(icon, ICON_SIZE, ICON_SIZE, text)
if name:
Expand All @@ -142,6 +131,42 @@ class ReadAloud:
bar.appendChild(cb('faster', 'faster', _('Speed up speech')))
bar.appendChild(cb('configure', 'cogs', _('Configure Read aloud')))
bar.appendChild(cb('hide', 'off', _('Close Read aloud')))

def build_bar(self):
if self.state is HIDDEN:
return
bar_container = self.bar
bar_container.classList.remove('floating')
bar_container.classList.remove('docked')
iclass, bp = bar_class_and_position()
bar_container.classList.add(iclass)
bar_container.style.maxWidth = 'min(40rem, 80vw)'
bar_container.style.backgroundColor = get_color("window-background")
if self.state is PLAYING:
bar_container.classList.add('speaking')
else:
bar_container.classList.remove('speaking')
clear(bar_container)

if iclass is not 'floating':
return self.build_docked_bar(bar_container, bp)

container = self.container
container.style.alignItems = 'flex-end' if is_flow_mode() else 'flex-start'
container.style.justifyContent = 'flex-end'
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; font-size: smaller',
E.div()
)
]:
bar_container.appendChild(x)
self.create_buttons(bar_container.firstChild)

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'
Expand Down
1 change: 1 addition & 0 deletions src/pyj/session.pyj
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ all_settings = {
'gesture_overrides': {'default': {}, 'category': 'read_book'},
'cite_text_template': {'default': '[{text}]({url})', 'category': 'read_book'},
'cite_hl_template': {'default': '[{text}]({url})', 'category': 'read_book'},
'tts_bar_position': {'default': 'float', 'category': 'read_book', 'is_local': False, 'disallowed_in_profile': False},
}

defaults = {}
Expand Down
9 changes: 8 additions & 1 deletion src/pyj/viewer-main.pyj
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,14 @@ def show_home_page():
view.overlay.open_book(False)


@from_python
def redraw_tts_bar(pos):
sd = get_session_data()
sd.set('tts_bar_position', pos)
if view:
view.read_aloud.build_bar()


@from_python
def start_book_load(key, initial_position, pathtoebook, highlights, book_url, reading_rates, book_in_library_url):
xhr = ajax('manifest', manifest_received.bind(None, key, initial_position, pathtoebook, highlights, book_url, reading_rates, book_in_library_url), ok_code=0)
Expand Down Expand Up @@ -503,7 +511,6 @@ if window is window.top:
to_python.on_iframe_ready()
ui_operations.update_reading_rates = def(rates):
to_python.update_reading_rates(rates)

document.body.appendChild(E.div(id='view'))
window.onerror = onerror
create_modal_container()
Expand Down

0 comments on commit 9725e92

Please sign in to comment.