Cleanup tts manager event system

Fix event stream when resuming after configure
This commit is contained in:
Kovid Goyal 2024-09-03 18:49:46 +05:30
parent f3b44f3614
commit a617af14d8
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
2 changed files with 68 additions and 48 deletions

View File

@ -2,23 +2,20 @@
# License: GPLv3 Copyright: 2024, Kovid Goyal <kovid at kovidgoyal.net>
from qt.core import QAction, QKeySequence, QPlainTextEdit, QSize, Qt, QTextCursor, QTextToSpeech, QToolBar
from typing import Literal
from qt.core import QAction, QKeySequence, QPlainTextEdit, QSize, Qt, QTextCursor, QToolBar
from calibre.gui2 import Application
from calibre.gui2.main_window import MainWindow
from calibre.gui2.tts2.manager import TTSManager
TEXT = '''\
Demonstration 😹 🐈 of DOCX support in calibre
Demonstration 🐈 of DOCX support in calibre
This document demonstrates the ability of the calibre DOCX Input plugin to convert the various typographic features in a Microsoft Word
(2007 and newer) document. Convert this document to a modern ebook format, such as AZW3 for Kindles or EPUB for other ebook readers,
to see it in action.
There is support for images, tables, lists, footnotes, endnotes, links, dropcaps and various types of text and paragraph level formatting.
To see the DOCX conversion in action, simply add this file to calibre using the Add Books button and then click Convert.
Set the output format in the top right corner of the conversion dialog to EPUB or AZW3 and click OK.
'''
@ -27,28 +24,19 @@ class MainWindow(MainWindow):
def __init__(self, text):
super().__init__()
self.display = d = QPlainTextEdit(self)
self.page_count = 1
self.toolbar = tb = QToolBar(self)
self.tts = TTSManager(self)
self.tts.state_changed.connect(self.state_changed, type=Qt.ConnectionType.QueuedConnection)
self.tts.state_event.connect(self.state_event, type=Qt.ConnectionType.QueuedConnection)
self.tts.saying.connect(self.saying)
self.addToolBar(tb)
self.setCentralWidget(d)
d.setPlainText(text)
d.setReadOnly(True)
c = d.textCursor()
c.setPosition(0)
marked_text = []
while True:
marked_text.append(c.position())
if not c.movePosition(QTextCursor.MoveOperation.NextWord, QTextCursor.MoveMode.KeepAnchor):
break
marked_text.append(c.selectedText().replace('\u2029', '\n'))
c.setPosition(c.position())
c.setPosition(0)
self.marked_text = marked_text
self.create_marked_text()
self.play_action = pa = QAction('Play')
pa.setShortcut(QKeySequence(Qt.Key.Key_Space))
pa.triggered.connect(self.toggled)
pa.triggered.connect(self.play_triggerred)
self.toolbar.addAction(pa)
self.stop_action = sa = QAction('Stop')
sa.setShortcut(QKeySequence(Qt.Key.Key_Escape))
@ -67,31 +55,50 @@ class MainWindow(MainWindow):
self.toolbar.addAction(ra)
ra.triggered.connect(self.tts.test_resume_after_reload)
self.state_changed(self.tts.state)
self.resize(self.sizeHint())
def state_changed(self, state):
self.statusBar().showMessage(str(state))
if state in (QTextToSpeech.State.Ready, QTextToSpeech.State.Paused, QTextToSpeech.State.Error):
self.play_action.setChecked(False)
if state is QTextToSpeech.State.Ready:
c = self.display.textCursor()
c.setPosition(0)
self.display.setTextCursor(c)
else:
self.play_action.setChecked(True)
self.stop_action.setEnabled(state in (QTextToSpeech.State.Speaking, QTextToSpeech.State.Synthesizing, QTextToSpeech.State.Paused))
if self.tts.state is QTextToSpeech.State.Paused:
self.play_action.setText('Resume')
elif self.tts.state is QTextToSpeech.State.Speaking:
self.play_action.setText('Pause')
else:
self.play_action.setText('Play')
def create_marked_text(self):
c = self.display.textCursor()
c.setPosition(0)
marked_text = []
while True:
marked_text.append(c.position())
if not c.movePosition(QTextCursor.MoveOperation.NextWord, QTextCursor.MoveMode.KeepAnchor):
break
marked_text.append(c.selectedText().replace('\u2029', '\n'))
c.setPosition(c.position())
c.setPosition(0)
self.marked_text = marked_text
self.display.setTextCursor(c)
def toggled(self):
if self.tts.state is QTextToSpeech.State.Paused:
def next_page(self):
self.page_count += 1
self.display.setPlainText(f'This is page number {self.page_count}. Pages are turned automatically when the end of a page is reached.')
self.create_marked_text()
def update_play_action(self, text):
self.play_action.setText(text)
def state_event(self, ev: Literal['begin', 'end', 'cancel', 'pause', 'resume']):
sb = self.statusBar()
self.statusBar().showMessage((sb.currentMessage() + ' ' + ev).strip())
self.stop_action.setEnabled(ev in ('pause', 'resume', 'begin'))
if ev == 'cancel':
self.update_play_action('Play')
elif ev == 'pause':
self.update_play_action('Resume')
elif ev in ('resume', 'begin'):
self.update_play_action('Pause')
elif ev == 'end':
if self.play_action.text() == 'Pause':
self.next_page()
self.update_play_action('Play')
self.play_triggerred()
def play_triggerred(self):
if self.play_action.text() == 'Resume':
self.tts.resume()
elif self.tts.state is QTextToSpeech.State.Speaking:
elif self.play_action.text() == 'Pause':
self.tts.pause()
else:
self.tts.speak_marked_text(self.marked_text)

View File

@ -111,7 +111,6 @@ class ResumeData:
class TTSManager(QObject):
state_changed = pyqtSignal(QTextToSpeech.State)
state_event = pyqtSignal(str)
saying = pyqtSignal(int, int)
@ -120,6 +119,20 @@ class TTSManager(QObject):
self._tts: 'TTSBackend' | None = None
self.state = QTextToSpeech.State.Ready
self.tracker = Tracker()
self._resuming_after_configure = False
def emit_state_event(self, event: str) -> None:
if self._resuming_after_configure:
if event == 'cancel':
self.state_event.emit(event)
self._resuming_after_configure = False
elif event == 'begin':
self.state_event.emit('resume')
self._resuming_after_configure = False
elif event == 'pause':
self.state_event.emit(event)
else:
self.state_event.emit(event)
@property
def tts(self) -> 'TTSBackend':
@ -160,6 +173,7 @@ class TTSManager(QObject):
rd = ResumeData()
rd.is_speaking = self._tts is not None and self.state in (
QTextToSpeech.State.Speaking, QTextToSpeech.State.Synthesizing, QTextToSpeech.State.Paused)
self._resuming_after_configure = True
if self.state is not QTextToSpeech.State.Paused:
self.tts.pause()
yield rd
@ -216,19 +230,18 @@ class TTSManager(QObject):
prev_state, self.state = self.state, state
if state is QTextToSpeech.State.Error:
error_dialog(self, _('Read aloud failed'), self.tts.error_message(), show=True)
self.state_changed.emit(state)
if state is QTextToSpeech.State.Paused:
self.state_event.emit('pause')
self.emit_state_event('pause')
elif state is QTextToSpeech.State.Speaking:
if prev_state is QTextToSpeech.State.Paused:
self.state_event.emit('resume')
self.emit_state_event('resume')
elif prev_state is QTextToSpeech.State.Ready:
self.state_event.emit('begin')
self.emit_state_event('begin')
elif state is QTextToSpeech.State.Ready:
if prev_state in (QTextToSpeech.State.Paused, QTextToSpeech.State.Speaking):
self.state_event.emit('end')
self.emit_state_event('end')
elif state is QTextToSpeech.State.Error:
self.state_event.emit('cancel')
self.emit_state_event('cancel')
def _saying(self, offset: int, length: int) -> None:
self.tracker.boundary_reached(offset)