diff --git a/src/calibre/gui2/tts/linux.py b/src/calibre/gui2/tts/linux.py index a21cad213f..df0ef2813a 100644 --- a/src/calibre/gui2/tts/linux.py +++ b/src/calibre/gui2/tts/linux.py @@ -19,7 +19,7 @@ class Client: def escape_marked_text(cls, text): return prepare_string_for_xml(text) - def __init__(self, dispatch_on_main_thread): + def __init__(self, dispatch_on_main_thread=lambda f: f()): self.status = {'synthesizing': False, 'paused': False} self.dispatch_on_main_thread = dispatch_on_main_thread self.current_marked_text = None @@ -41,7 +41,10 @@ class Client: def __del__(self): if self.ssip_client is not None: - self.ssip_client.cancel() + try: + self.ssip_client.cancel() + except Exception: + pass self.ssip_client.close() self.ssip_client = None shutdown = __del__ diff --git a/src/calibre/gui2/tts/linux_config.py b/src/calibre/gui2/tts/linux_config.py index 2b2170afb0..14b32fb9a0 100644 --- a/src/calibre/gui2/tts/linux_config.py +++ b/src/calibre/gui2/tts/linux_config.py @@ -2,9 +2,10 @@ # vim:fileencoding=utf-8 # License: GPL v3 Copyright: 2020, Kovid Goyal +from contextlib import suppress from PyQt5.Qt import ( - QAbstractTableModel, QComboBox, QFontMetrics, QFormLayout, Qt, QTableView, - QWidget + QAbstractItemView, QAbstractTableModel, QComboBox, QFontMetrics, QFormLayout, Qt, + QTableView, QWidget, QSortFilterProxyModel, QItemSelectionModel ) from calibre.gui2.preferences.look_feel import BusyCursor @@ -12,6 +13,8 @@ from calibre.gui2.preferences.look_feel import BusyCursor class VoicesModel(QAbstractTableModel): + system_default_voice = ('', '', '') + def __init__(self, voice_data, default_output_module, parent=None): super().__init__(parent) self.voice_data = voice_data @@ -19,7 +22,7 @@ class VoicesModel(QAbstractTableModel): self.column_headers = (_('Name'), _('Language'), _('Variant')) def rowCount(self, parent=None): - return len(self.current_voices) + return len(self.current_voices) + 1 def columnCount(self, parent=None): return 3 @@ -32,11 +35,20 @@ class VoicesModel(QAbstractTableModel): def data(self, index, role=Qt.ItemDataRole.DisplayRole): if role == Qt.ItemDataRole.DisplayRole: row = index.row() - try: - data = self.current_voices[row] - return data[index.column()] - except IndexError: - return + with suppress(IndexError): + if row == 0: + return (_('System default'), '', '')[index.column()] + data = self.current_voices[row - 1] + ans = data[index.column()] + if not ans or ans == 'none': + ans = '' + return ans + if role == Qt.ItemDataRole.UserRole: + row = index.row() + with suppress(IndexError): + if row == 0: + return self.system_default_voice + return self.current_voices[row - 1] def change_output_module(self, om): self.beginResetModel() @@ -58,15 +70,87 @@ class Widget(QWidget): self.voice_data = self.tts_client.get_voice_data() self.system_default_output_module = self.tts_client.system_default_output_module om.addItem(_('System default'), self.system_default_output_module) + for x in self.voice_data: + om.addItem(x, x) l.addRow(_('Speech synthesizer:'), om) self.voices = v = QTableView(self) self.voices_model = VoicesModel(self.voice_data, self.system_default_output_module, parent=v) - v.setModel(self.voices_model) + self.proxy_model = p = QSortFilterProxyModel(self) + p.setFilterCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive) + p.setSourceModel(self.voices_model) + v.setModel(p) + v.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) + v.setSortingEnabled(True) v.horizontalHeader().resizeSection(0, QFontMetrics(self.font()).averageCharWidth() * 30) + v.verticalHeader().close() + v.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) + v.sortByColumn(0, Qt.SortOrder.AscendingOrder) + om.currentIndexChanged.connect(self.output_module_changed) l.addRow(v) + self.backend_settings = initial_backend_settings or {} def sizeHint(self): ans = super().sizeHint() ans.setHeight(max(ans.height(), 600)) return ans + + @property + def selected_voice(self): + for x in self.voices.selectedIndexes(): + return x.data(Qt.ItemDataRole.UserRole) + + @selected_voice.setter + def selected_voice(self, val): + val = val or VoicesModel.system_default_voice + idx = self.voices_model.index_for_voice(tuple(val)) + if idx is not None: + idx = self.proxy_model.mapFromSource(idx) + self.voices.selectionModel().select(idx, QItemSelectionModel.SelectionFlag.ClearAndSelect) + self.voices.scrollTo(idx) + + @property + def selected_output_module(self): + return self.output_modules.currentData() + + @selected_output_module.setter + def selected_output_module(self, val): + if not val: + self.output_modules.setCurrentIndex(0) + return + idx = self.output_modules.findData(val) + if idx < 0: + idx = 0 + self.output_modules.setCurrentIndex(idx) + + def output_module_changed(self, idx): + om = self.selected_output_module + self.voices_model.change_output_module(om) + + @property + def backend_settings(self): + ans = {} + om = self.selected_output_module + if om != self.system_default_output_module: + ans['output_module'] = om + voice = self.selected_voice + if voice != VoicesModel.system_default_voice: + ans['voice'] = voice + return ans + + @backend_settings.setter + def backend_settings(self, val): + om = val.get('output_module') or self.system_default_output_module + self.selected_output_module = om + voice = val.get('voice') or VoicesModel.system_default_voice + self.selected_voice = voice + + +if __name__ == '__main__': + from calibre.gui2 import Application + from calibre.gui2.tts.implementation import Client + app = Application([]) + c = Client() + w = Widget(c, {}) + w.show() + app.exec_()