mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
E-book viewer: Read Aloud: Add an option to control the position of the popup control bar. It can now be placed along the top or bottom edges so as to overlap less with text.
This commit is contained in:
parent
6f1d28eafb
commit
9725e92d17
@ -481,6 +481,41 @@ class EngineSpecificConfig(QWidget):
|
|||||||
return tts.is_voice_downloaded(v)
|
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):
|
class ConfigDialog(Dialog):
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
@ -496,6 +531,8 @@ class ConfigDialog(Dialog):
|
|||||||
l.addWidget(esc)
|
l.addWidget(esc)
|
||||||
self.voice_button = b = QPushButton(self)
|
self.voice_button = b = QPushButton(self)
|
||||||
b.clicked.connect(self.voice_action)
|
b.clicked.connect(self.voice_action)
|
||||||
|
self.bar_position = bp = BarPosition(self)
|
||||||
|
l.addWidget(bp)
|
||||||
h = QHBoxLayout()
|
h = QHBoxLayout()
|
||||||
l.addLayout(h)
|
l.addLayout(h)
|
||||||
h.addWidget(b), h.addStretch(10), h.addWidget(self.bb)
|
h.addWidget(b), h.addStretch(10), h.addWidget(self.bb)
|
||||||
@ -508,6 +545,7 @@ class ConfigDialog(Dialog):
|
|||||||
def restore_defaults(self):
|
def restore_defaults(self):
|
||||||
self.engine_choice.restore_defaults()
|
self.engine_choice.restore_defaults()
|
||||||
self.engine_specific_config.restore_defaults()
|
self.engine_specific_config.restore_defaults()
|
||||||
|
self.bar_position.restore_defaults()
|
||||||
|
|
||||||
def set_engine(self, engine_name: str) -> None:
|
def set_engine(self, engine_name: str) -> None:
|
||||||
metadata = self.engine_specific_config.set_engine(engine_name)
|
metadata = self.engine_specific_config.set_engine(engine_name)
|
||||||
@ -545,6 +583,7 @@ class ConfigDialog(Dialog):
|
|||||||
else:
|
else:
|
||||||
prefs.pop('engine', None)
|
prefs.pop('engine', None)
|
||||||
s.save_to_config(prefs)
|
s.save_to_config(prefs)
|
||||||
|
self.bar_position.commit()
|
||||||
super().accept()
|
super().accept()
|
||||||
|
|
||||||
|
|
||||||
|
@ -121,6 +121,7 @@ class TTSManager(QObject):
|
|||||||
|
|
||||||
state_event = pyqtSignal(str)
|
state_event = pyqtSignal(str)
|
||||||
saying = pyqtSignal(int, int)
|
saying = pyqtSignal(int, int)
|
||||||
|
configured = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
@ -228,7 +229,9 @@ class TTSManager(QObject):
|
|||||||
from calibre.gui2.tts.types import widget_parent
|
from calibre.gui2.tts.types import widget_parent
|
||||||
with self.resume_after() as rd:
|
with self.resume_after() as rd:
|
||||||
d = ConfigDialog(parent=widget_parent(self))
|
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
|
rd.needs_full_resume = True
|
||||||
if d.engine_changed:
|
if d.engine_changed:
|
||||||
if rd.is_speaking:
|
if rd.is_speaking:
|
||||||
@ -236,6 +239,7 @@ class TTSManager(QObject):
|
|||||||
self._tts = None
|
self._tts = None
|
||||||
else:
|
else:
|
||||||
self.tts.reload_after_configure()
|
self.tts.reload_after_configure()
|
||||||
|
self.configured.emit()
|
||||||
|
|
||||||
def _state_changed(self, state: QTextToSpeech.State) -> None:
|
def _state_changed(self, state: QTextToSpeech.State) -> None:
|
||||||
prev_state, self.state = self.state, state
|
prev_state, self.state = self.state, state
|
||||||
|
@ -28,6 +28,16 @@ def get_session_pref(name, default=None, group='standalone_misc_settings'):
|
|||||||
return g.get(name, default)
|
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):
|
def get_pref_group(name):
|
||||||
sd = vprefs['session_data']
|
sd = vprefs['session_data']
|
||||||
return sd.get(name) or {}
|
return sd.get(name) or {}
|
||||||
|
@ -13,6 +13,7 @@ class TTS(QObject):
|
|||||||
|
|
||||||
event_received = pyqtSignal(object, object)
|
event_received = pyqtSignal(object, object)
|
||||||
settings_changed = pyqtSignal(object)
|
settings_changed = pyqtSignal(object)
|
||||||
|
configured = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
@ -25,6 +26,7 @@ class TTS(QObject):
|
|||||||
self._manager = TTSManager(self)
|
self._manager = TTSManager(self)
|
||||||
self._manager.saying.connect(self.saying)
|
self._manager.saying.connect(self.saying)
|
||||||
self._manager.state_event.connect(self.state_event)
|
self._manager.state_event.connect(self.state_event)
|
||||||
|
self._manager.configured.connect(self.configured)
|
||||||
return self._manager
|
return self._manager
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
|
@ -41,7 +41,7 @@ from calibre.ebooks.metadata.book.base import field_metadata
|
|||||||
from calibre.ebooks.oeb.polish.utils import guess_type
|
from calibre.ebooks.oeb.polish.utils import guess_type
|
||||||
from calibre.gui2 import choose_images, config, error_dialog, safe_open_url
|
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 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.viewer.tts import TTS
|
||||||
from calibre.gui2.webengine import RestartingWebEngineView
|
from calibre.gui2.webengine import RestartingWebEngineView
|
||||||
from calibre.srv.code import get_translations_data
|
from calibre.srv.code import get_translations_data
|
||||||
@ -299,6 +299,7 @@ class ViewerBridge(Bridge):
|
|||||||
|
|
||||||
create_view = to_js()
|
create_view = to_js()
|
||||||
start_book_load = to_js()
|
start_book_load = to_js()
|
||||||
|
redraw_tts_bar = to_js()
|
||||||
goto_toc_node = to_js()
|
goto_toc_node = to_js()
|
||||||
goto_cfi = to_js()
|
goto_cfi = to_js()
|
||||||
full_screen_state_changed = to_js()
|
full_screen_state_changed = to_js()
|
||||||
@ -514,6 +515,7 @@ class WebView(RestartingWebEngineView):
|
|||||||
self.tts = TTS(self)
|
self.tts = TTS(self)
|
||||||
self.tts.settings_changed.connect(self.tts_settings_changed)
|
self.tts.settings_changed.connect(self.tts_settings_changed)
|
||||||
self.tts.event_received.connect(self.tts_event_received)
|
self.tts.event_received.connect(self.tts_event_received)
|
||||||
|
self.tts.configured.connect(self.redraw_tts_bar)
|
||||||
self.dead_renderer_error_shown = False
|
self.dead_renderer_error_shown = False
|
||||||
self.render_process_failed.connect(self.render_process_died)
|
self.render_process_failed.connect(self.render_process_died)
|
||||||
w = self.screen().availableSize().width()
|
w = self.screen().availableSize().width()
|
||||||
@ -769,6 +771,9 @@ class WebView(RestartingWebEngineView):
|
|||||||
def show_home_page(self):
|
def show_home_page(self):
|
||||||
self.execute_when_ready('show_home_page')
|
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):
|
def change_background_image(self, img_id):
|
||||||
files = choose_images(self, 'viewer-background-image', _('Choose background image'), formats=['png', 'gif', 'jpg', 'jpeg', 'webp'])
|
files = choose_images(self, 'viewer-background-image', _('Choose background image'), formats=['png', 'gif', 'jpg', 'jpeg', 'webp'])
|
||||||
if files:
|
if files:
|
||||||
|
@ -12,6 +12,7 @@ from read_book.globals import ui_operations
|
|||||||
from read_book.highlights import ICON_SIZE
|
from read_book.highlights import ICON_SIZE
|
||||||
from read_book.selection_bar import BUTTON_MARGIN, get_margins, map_to_iframe_coords
|
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
|
||||||
|
from book_list.globals import get_session_data
|
||||||
|
|
||||||
HIDDEN = 0
|
HIDDEN = 0
|
||||||
WAITING_FOR_PLAY_TO_START = 1
|
WAITING_FOR_PLAY_TO_START = 1
|
||||||
@ -26,6 +27,13 @@ def is_flow_mode():
|
|||||||
return mode is 'flow'
|
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:
|
class ReadAloud:
|
||||||
|
|
||||||
dont_hide_on_content_loaded = True
|
dont_hide_on_content_loaded = True
|
||||||
@ -38,14 +46,13 @@ class ReadAloud:
|
|||||||
container = self.container
|
container = self.container
|
||||||
container.setAttribute('tabindex', '0')
|
container.setAttribute('tabindex', '0')
|
||||||
container.style.overflow = 'hidden'
|
container.style.overflow = 'hidden'
|
||||||
container.style.justifyContent = 'flex-end'
|
container.appendChild(E.div(id=self.bar_id))
|
||||||
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.style(
|
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 '+'{ opacity: 0.5 }\n\n',
|
||||||
f'#{self.bar_id}.speaking:hover '+'{ opacity: 1.0 }\n\n',
|
f'#{self.bar_id}.speaking:hover '+'{ opacity: 1.0 }\n\n',
|
||||||
))
|
))
|
||||||
@ -95,31 +102,13 @@ class ReadAloud:
|
|||||||
def focus(self):
|
def focus(self):
|
||||||
self.container.focus()
|
self.container.focus()
|
||||||
|
|
||||||
def build_bar(self, annot_id):
|
def build_docked_bar(self, bar_container, bar_position):
|
||||||
if self.state is HIDDEN:
|
container = self.container
|
||||||
return
|
container.style.alignItems = 'flex-end' if 'bottom' in bar_position else 'flex-start'
|
||||||
self.container.style.alignItems = 'flex-end' if is_flow_mode() else 'flex-start'
|
container.style.justifyContent = 'flex-end' if 'right' in bar_position else ('flex-start' if 'left' in bar_position else 'center')
|
||||||
bar_container = self.bar
|
self.create_buttons(bar_container)
|
||||||
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 create_buttons(self, bar):
|
||||||
def cb(name, icon, text):
|
def cb(name, icon, text):
|
||||||
ans = svgicon(icon, ICON_SIZE, ICON_SIZE, text)
|
ans = svgicon(icon, ICON_SIZE, ICON_SIZE, text)
|
||||||
if name:
|
if name:
|
||||||
@ -142,6 +131,42 @@ class ReadAloud:
|
|||||||
bar.appendChild(cb('faster', 'faster', _('Speed up speech')))
|
bar.appendChild(cb('faster', 'faster', _('Speed up speech')))
|
||||||
bar.appendChild(cb('configure', 'cogs', _('Configure Read aloud')))
|
bar.appendChild(cb('configure', 'cogs', _('Configure Read aloud')))
|
||||||
bar.appendChild(cb('hide', 'off', _('Close 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:
|
if self.state is not WAITING_FOR_PLAY_TO_START:
|
||||||
notes_container = bar_container.lastChild
|
notes_container = bar_container.lastChild
|
||||||
notes_container.style.display = notes_container.previousSibling.style.display = 'block'
|
notes_container.style.display = notes_container.previousSibling.style.display = 'block'
|
||||||
|
@ -80,6 +80,7 @@ all_settings = {
|
|||||||
'gesture_overrides': {'default': {}, 'category': 'read_book'},
|
'gesture_overrides': {'default': {}, 'category': 'read_book'},
|
||||||
'cite_text_template': {'default': '[{text}]({url})', 'category': 'read_book'},
|
'cite_text_template': {'default': '[{text}]({url})', 'category': 'read_book'},
|
||||||
'cite_hl_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 = {}
|
defaults = {}
|
||||||
|
@ -283,6 +283,14 @@ def show_home_page():
|
|||||||
view.overlay.open_book(False)
|
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
|
@from_python
|
||||||
def start_book_load(key, initial_position, pathtoebook, highlights, book_url, reading_rates, book_in_library_url):
|
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)
|
xhr = ajax('manifest', manifest_received.bind(None, key, initial_position, pathtoebook, highlights, book_url, reading_rates, book_in_library_url), ok_code=0)
|
||||||
@ -503,7 +511,6 @@ if window is window.top:
|
|||||||
to_python.on_iframe_ready()
|
to_python.on_iframe_ready()
|
||||||
ui_operations.update_reading_rates = def(rates):
|
ui_operations.update_reading_rates = def(rates):
|
||||||
to_python.update_reading_rates(rates)
|
to_python.update_reading_rates(rates)
|
||||||
|
|
||||||
document.body.appendChild(E.div(id='view'))
|
document.body.appendChild(E.div(id='view'))
|
||||||
window.onerror = onerror
|
window.onerror = onerror
|
||||||
create_modal_container()
|
create_modal_container()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user