mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 02:34:06 -04:00
Cleanup tts manager event system
Fix event stream when resuming after configure
This commit is contained in:
parent
f3b44f3614
commit
a617af14d8
@ -2,23 +2,20 @@
|
|||||||
# License: GPLv3 Copyright: 2024, Kovid Goyal <kovid at kovidgoyal.net>
|
# 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 import Application
|
||||||
from calibre.gui2.main_window import MainWindow
|
from calibre.gui2.main_window import MainWindow
|
||||||
from calibre.gui2.tts2.manager import TTSManager
|
from calibre.gui2.tts2.manager import TTSManager
|
||||||
|
|
||||||
TEXT = '''\
|
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
|
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,
|
(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.
|
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):
|
def __init__(self, text):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.display = d = QPlainTextEdit(self)
|
self.display = d = QPlainTextEdit(self)
|
||||||
|
self.page_count = 1
|
||||||
self.toolbar = tb = QToolBar(self)
|
self.toolbar = tb = QToolBar(self)
|
||||||
self.tts = TTSManager(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.tts.saying.connect(self.saying)
|
||||||
self.addToolBar(tb)
|
self.addToolBar(tb)
|
||||||
self.setCentralWidget(d)
|
self.setCentralWidget(d)
|
||||||
d.setPlainText(text)
|
d.setPlainText(text)
|
||||||
d.setReadOnly(True)
|
d.setReadOnly(True)
|
||||||
c = d.textCursor()
|
self.create_marked_text()
|
||||||
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.play_action = pa = QAction('Play')
|
self.play_action = pa = QAction('Play')
|
||||||
pa.setShortcut(QKeySequence(Qt.Key.Key_Space))
|
pa.setShortcut(QKeySequence(Qt.Key.Key_Space))
|
||||||
pa.triggered.connect(self.toggled)
|
pa.triggered.connect(self.play_triggerred)
|
||||||
self.toolbar.addAction(pa)
|
self.toolbar.addAction(pa)
|
||||||
self.stop_action = sa = QAction('Stop')
|
self.stop_action = sa = QAction('Stop')
|
||||||
sa.setShortcut(QKeySequence(Qt.Key.Key_Escape))
|
sa.setShortcut(QKeySequence(Qt.Key.Key_Escape))
|
||||||
@ -67,31 +55,50 @@ class MainWindow(MainWindow):
|
|||||||
self.toolbar.addAction(ra)
|
self.toolbar.addAction(ra)
|
||||||
ra.triggered.connect(self.tts.test_resume_after_reload)
|
ra.triggered.connect(self.tts.test_resume_after_reload)
|
||||||
|
|
||||||
self.state_changed(self.tts.state)
|
|
||||||
self.resize(self.sizeHint())
|
self.resize(self.sizeHint())
|
||||||
|
|
||||||
def state_changed(self, state):
|
def create_marked_text(self):
|
||||||
self.statusBar().showMessage(str(state))
|
c = self.display.textCursor()
|
||||||
if state in (QTextToSpeech.State.Ready, QTextToSpeech.State.Paused, QTextToSpeech.State.Error):
|
c.setPosition(0)
|
||||||
self.play_action.setChecked(False)
|
marked_text = []
|
||||||
if state is QTextToSpeech.State.Ready:
|
while True:
|
||||||
c = self.display.textCursor()
|
marked_text.append(c.position())
|
||||||
c.setPosition(0)
|
if not c.movePosition(QTextCursor.MoveOperation.NextWord, QTextCursor.MoveMode.KeepAnchor):
|
||||||
self.display.setTextCursor(c)
|
break
|
||||||
else:
|
marked_text.append(c.selectedText().replace('\u2029', '\n'))
|
||||||
self.play_action.setChecked(True)
|
c.setPosition(c.position())
|
||||||
self.stop_action.setEnabled(state in (QTextToSpeech.State.Speaking, QTextToSpeech.State.Synthesizing, QTextToSpeech.State.Paused))
|
c.setPosition(0)
|
||||||
if self.tts.state is QTextToSpeech.State.Paused:
|
self.marked_text = marked_text
|
||||||
self.play_action.setText('Resume')
|
self.display.setTextCursor(c)
|
||||||
elif self.tts.state is QTextToSpeech.State.Speaking:
|
|
||||||
self.play_action.setText('Pause')
|
|
||||||
else:
|
|
||||||
self.play_action.setText('Play')
|
|
||||||
|
|
||||||
def toggled(self):
|
def next_page(self):
|
||||||
if self.tts.state is QTextToSpeech.State.Paused:
|
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()
|
self.tts.resume()
|
||||||
elif self.tts.state is QTextToSpeech.State.Speaking:
|
elif self.play_action.text() == 'Pause':
|
||||||
self.tts.pause()
|
self.tts.pause()
|
||||||
else:
|
else:
|
||||||
self.tts.speak_marked_text(self.marked_text)
|
self.tts.speak_marked_text(self.marked_text)
|
||||||
|
@ -111,7 +111,6 @@ class ResumeData:
|
|||||||
|
|
||||||
class TTSManager(QObject):
|
class TTSManager(QObject):
|
||||||
|
|
||||||
state_changed = pyqtSignal(QTextToSpeech.State)
|
|
||||||
state_event = pyqtSignal(str)
|
state_event = pyqtSignal(str)
|
||||||
saying = pyqtSignal(int, int)
|
saying = pyqtSignal(int, int)
|
||||||
|
|
||||||
@ -120,6 +119,20 @@ class TTSManager(QObject):
|
|||||||
self._tts: 'TTSBackend' | None = None
|
self._tts: 'TTSBackend' | None = None
|
||||||
self.state = QTextToSpeech.State.Ready
|
self.state = QTextToSpeech.State.Ready
|
||||||
self.tracker = Tracker()
|
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
|
@property
|
||||||
def tts(self) -> 'TTSBackend':
|
def tts(self) -> 'TTSBackend':
|
||||||
@ -160,6 +173,7 @@ class TTSManager(QObject):
|
|||||||
rd = ResumeData()
|
rd = ResumeData()
|
||||||
rd.is_speaking = self._tts is not None and self.state in (
|
rd.is_speaking = self._tts is not None and self.state in (
|
||||||
QTextToSpeech.State.Speaking, QTextToSpeech.State.Synthesizing, QTextToSpeech.State.Paused)
|
QTextToSpeech.State.Speaking, QTextToSpeech.State.Synthesizing, QTextToSpeech.State.Paused)
|
||||||
|
self._resuming_after_configure = True
|
||||||
if self.state is not QTextToSpeech.State.Paused:
|
if self.state is not QTextToSpeech.State.Paused:
|
||||||
self.tts.pause()
|
self.tts.pause()
|
||||||
yield rd
|
yield rd
|
||||||
@ -216,19 +230,18 @@ class TTSManager(QObject):
|
|||||||
prev_state, self.state = self.state, state
|
prev_state, self.state = self.state, state
|
||||||
if state is QTextToSpeech.State.Error:
|
if state is QTextToSpeech.State.Error:
|
||||||
error_dialog(self, _('Read aloud failed'), self.tts.error_message(), show=True)
|
error_dialog(self, _('Read aloud failed'), self.tts.error_message(), show=True)
|
||||||
self.state_changed.emit(state)
|
|
||||||
if state is QTextToSpeech.State.Paused:
|
if state is QTextToSpeech.State.Paused:
|
||||||
self.state_event.emit('pause')
|
self.emit_state_event('pause')
|
||||||
elif state is QTextToSpeech.State.Speaking:
|
elif state is QTextToSpeech.State.Speaking:
|
||||||
if prev_state is QTextToSpeech.State.Paused:
|
if prev_state is QTextToSpeech.State.Paused:
|
||||||
self.state_event.emit('resume')
|
self.emit_state_event('resume')
|
||||||
elif prev_state is QTextToSpeech.State.Ready:
|
elif prev_state is QTextToSpeech.State.Ready:
|
||||||
self.state_event.emit('begin')
|
self.emit_state_event('begin')
|
||||||
elif state is QTextToSpeech.State.Ready:
|
elif state is QTextToSpeech.State.Ready:
|
||||||
if prev_state in (QTextToSpeech.State.Paused, QTextToSpeech.State.Speaking):
|
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:
|
elif state is QTextToSpeech.State.Error:
|
||||||
self.state_event.emit('cancel')
|
self.emit_state_event('cancel')
|
||||||
|
|
||||||
def _saying(self, offset: int, length: int) -> None:
|
def _saying(self, offset: int, length: int) -> None:
|
||||||
self.tracker.boundary_reached(offset)
|
self.tracker.boundary_reached(offset)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user