├── plugin-import-name-tts_ebook_viewer.txt ├── README.md ├── config.py ├── hotkeys.py └── __init__.py /plugin-import-name-tts_ebook_viewer.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Important Update: Calibre now supports TTS in the built in reader by default! Use that instead of this plugin. 2 | 3 | 4 | 5 | 6 | # Calibre TTS Ebook Viewer 7 | 8 | A simple plugin for adding TTS support to Calibre's ebook viewer. 9 | 10 | Requires SAPI 5 (already installed on most recent Windows versions). 11 | 12 | Features: 13 | 14 | - Press Play to start reading at the top of the visible page, or resume from last paused position 15 | - Highlights the currently read paragraph and scrolls it into view if needed 16 | - Note: on paged mode, the bottom part of the paragraph may be clipped 17 | - Select mode: When select mode is enabled, click on any paragraph to start reading from that paragraph 18 | - Voice configuration: Configure plugin to set the voice, rate, and volume 19 | - Customizable hotkeys for play/stop/select mode 20 | 21 | To do: 22 | - highlight color options 23 | - Highlight words instead of paragraphs 24 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai 3 | from __future__ import (unicode_literals, division, absolute_import, 4 | print_function) 5 | 6 | __license__ = 'GPL v3' 7 | __copyright__ = '2016, github.com/christineye' 8 | __docformat__ = 'restructuredtext en' 9 | 10 | from PyQt5.Qt import QWidget, QVBoxLayout, QLabel, QLineEdit, QComboBox, QIntValidator, QHBoxLayout, QCheckBox 11 | 12 | from calibre.utils.config import JSONConfig 13 | 14 | # This is where all preferences for this plugin will be stored 15 | # Remember that this name (i.e. plugins/viewer_annotation) is also 16 | # in a global namespace, so make it as unique as possible. 17 | # You should always prefix your config file name with plugins/, 18 | # so as to ensure you dont accidentally clobber a calibre config file 19 | prefs = JSONConfig('plugins/tts_ebook_viewer') 20 | 21 | # Set defaults 22 | import os 23 | prefs.defaults['voice'] = None 24 | prefs.defaults['rate'] = 1 25 | prefs.defaults['volume'] = 100 26 | 27 | from calibre_plugins.tts_ebook_viewer.hotkeys import keycodes, HotkeyWidget, setHotkeyDefault 28 | 29 | setHotkeyDefault(prefs, 'pause', 'space', ctrl=True) 30 | setHotkeyDefault(prefs, 'stop', 'space', ctrl = True, shift=True) 31 | setHotkeyDefault(prefs, 'select', '`') 32 | 33 | 34 | class ConfigWidget(QWidget): 35 | 36 | def __init__(self): 37 | QWidget.__init__(self) 38 | self.l = QVBoxLayout() 39 | self.setLayout(self.l) 40 | 41 | self.label = QLabel("Voice") 42 | self.l.addWidget(self.label) 43 | 44 | self.voiceOptions = QComboBox() 45 | self.l.addWidget(self.voiceOptions) 46 | 47 | self.rateLabel = QLabel("Rate (-10 to 10)") 48 | self.l.addWidget(self.rateLabel) 49 | 50 | self.rateEdit = QLineEdit() 51 | self.rateEdit.setValidator(QIntValidator(-10, 10)) 52 | self.rateEdit.setText(str(prefs['rate'])) 53 | 54 | self.l.addWidget(self.rateEdit) 55 | 56 | self.volumeLabel = QLabel("Volume (0 to 100)") 57 | self.volumeEdit = QLineEdit() 58 | self.volumeEdit.setValidator(QIntValidator(0, 100)) 59 | self.volumeEdit.setText(str(prefs['volume'])) 60 | self.l.addWidget(self.volumeLabel) 61 | self.l.addWidget(self.volumeEdit) 62 | 63 | import win32com.client 64 | self.spVoice = win32com.client.Dispatch("SAPI.SpVoice") 65 | voices = self.spVoice.GetVoices("", "") 66 | 67 | for i in range(voices.Count): 68 | self.voiceOptions.addItem(voices.Item(i).GetDescription()) 69 | 70 | if voices.Item(i).GetDescription() == prefs['voice']: 71 | self.voiceOptions.setCurrentIndex(i) 72 | 73 | self.pauseHotKey = HotkeyWidget(prefs, "pause", "Enable Pause/Play hotkey" ) 74 | self.l.addWidget(self.pauseHotKey) 75 | 76 | self.stopHotKey = HotkeyWidget(prefs, "stop", "Enable Stop hotkey") 77 | self.l.addWidget(self.stopHotKey) 78 | 79 | self.selectHotKey = HotkeyWidget(prefs, "select", "Enable Select Mode hotkey") 80 | self.l.addWidget(self.selectHotKey) 81 | 82 | def save_settings(self): 83 | from calibre_plugins.tts_ebook_viewer.hotkeys import keycodes 84 | 85 | prefs['voice'] = unicode(self.voiceOptions.currentText()) 86 | prefs['rate'] = int(self.rateEdit.text()) 87 | prefs['volume'] = int(self.volumeEdit.text()) 88 | 89 | self.pauseHotKey.save_settings(prefs) 90 | self.stopHotKey.save_settings(prefs) 91 | self.selectHotKey.save_settings(prefs) 92 | 93 | -------------------------------------------------------------------------------- /hotkeys.py: -------------------------------------------------------------------------------- 1 | __license__ = 'GPL v3' 2 | __copyright__ = '2016, github.com/christineye' 3 | __docformat__ = 'restructuredtext en' 4 | 5 | from collections import OrderedDict 6 | 7 | from PyQt5.Qt import QWidget, QVBoxLayout, QLabel, QLineEdit, QComboBox, QIntValidator, QHBoxLayout, QCheckBox 8 | 9 | 10 | 11 | keycodes = OrderedDict([ 12 | ("backspace", 8), 13 | ("tab", 9), 14 | ("enter", 13), 15 | ("caps lock", 20), 16 | ("escape", 27), 17 | ("space", 32), 18 | ("page up", 33), 19 | ("page down", 34), 20 | ("end", 35), 21 | ("home", 36), 22 | ("left arrow", 37), 23 | ("up arrow", 38), 24 | ("right arrow", 39), 25 | ("down arrow", 40), 26 | ("0", 48), 27 | ("1", 49), 28 | ("2", 50), 29 | ("3", 51), 30 | ("4", 52), 31 | ("5", 53), 32 | ("6", 54), 33 | ("7", 55), 34 | ("8", 56), 35 | ("9", 57), 36 | ("a", 65), 37 | ("b", 66), 38 | ("c", 67), 39 | ("d", 68), 40 | ("e", 69), 41 | ("f", 70), 42 | ("g", 71), 43 | ("h", 72), 44 | ("i", 73), 45 | ("j", 74), 46 | ("k", 75), 47 | ("l", 76), 48 | ("m", 77), 49 | ("n", 78), 50 | ("o", 79), 51 | ("p", 80), 52 | ("q", 81), 53 | ("r", 82), 54 | ("s", 83), 55 | ("t", 84), 56 | ("u", 85), 57 | ("v", 86), 58 | ("w", 87), 59 | ("x", 88), 60 | ("y", 89), 61 | ("z", 90), 62 | ("numpad 0", 96), 63 | ("numpad 1", 97), 64 | ("numpad 2", 98), 65 | ("numpad 3", 99), 66 | ("numpad 4", 100), 67 | ("numpad 5", 101), 68 | ("numpad 6", 102), 69 | ("numpad 7", 103), 70 | ("numpad 8", 104), 71 | ("numpad 9", 105), 72 | ("f1", 112), 73 | ("f2", 113), 74 | ("f3", 114), 75 | ("f4", 115), 76 | ("f5", 116), 77 | ("f6", 117), 78 | ("f7", 118), 79 | ("f8", 119), 80 | ("f9", 120), 81 | ("f10", 121), 82 | ("f11", 122), 83 | ("f12", 123), 84 | (";", 186), 85 | ("=", 187), 86 | (",", 188), 87 | (".", 190), 88 | ("`", 192), 89 | ("[", 219), 90 | ("\\", 220), 91 | ("]", 221), 92 | ]) 93 | 94 | def setHotkeyDefault(prefs, configName, keycode, enabled = True, ctrl = False, alt = False, shift = False): 95 | prefs.defaults[configName + '_hotkey_enabled'] = enabled 96 | prefs.defaults[configName + '_hotkey_ctrl'] = ctrl 97 | prefs.defaults[configName + '_hotkey_alt'] = alt 98 | prefs.defaults[configName + '_hotkey_shift'] = shift 99 | prefs.defaults[configName + '_hotkey_keycode'] = keycodes[keycode] 100 | 101 | 102 | class HotkeyWidget(QWidget): 103 | def __init__(self, prefs, configName, title): 104 | QWidget.__init__(self) 105 | self.l = QVBoxLayout() 106 | self.setLayout(self.l) 107 | 108 | self.configName = configName 109 | 110 | self.hotkeyLayout = QHBoxLayout() 111 | self.l.addLayout(self.hotkeyLayout) 112 | 113 | enabledLabel = QLabel(title) 114 | self.hotkeyLayout.addWidget(enabledLabel) 115 | 116 | self.enabledBox = QCheckBox() 117 | self.hotkeyLayout.addWidget(self.enabledBox) 118 | self.enabledBox.setChecked(prefs[configName + '_hotkey_enabled']) 119 | 120 | hotkeyLayout2 = QHBoxLayout() 121 | self.l.addLayout(hotkeyLayout2) 122 | 123 | ctrlLabel = QLabel("Ctrl") 124 | self.ctrlBox = QCheckBox() 125 | self.ctrlBox.setChecked(prefs[configName + '_hotkey_ctrl']) 126 | 127 | ctrlLabel.setBuddy(self.ctrlBox) 128 | hotkeyLayout2.addWidget(ctrlLabel) 129 | hotkeyLayout2.addWidget(self.ctrlBox) 130 | 131 | altLabel = QLabel("Alt") 132 | self.altBox = QCheckBox() 133 | self.altBox.setChecked(prefs[configName + '_hotkey_alt']) 134 | 135 | altLabel.setBuddy(self.altBox) 136 | hotkeyLayout2.addWidget(altLabel) 137 | hotkeyLayout2.addWidget(self.altBox) 138 | 139 | shiftLabel = QLabel("Shift") 140 | self.shiftBox = QCheckBox() 141 | self.shiftBox.setChecked(prefs[configName + '_hotkey_shift']) 142 | 143 | shiftLabel.setBuddy(self.shiftBox) 144 | hotkeyLayout2.addWidget(shiftLabel) 145 | hotkeyLayout2.addWidget(self.shiftBox) 146 | 147 | self.keycodeBox = QComboBox() 148 | for key, value in keycodes.iteritems(): 149 | self.keycodeBox.addItem(key, value) 150 | 151 | index = self.keycodeBox.findData(prefs[configName + '_hotkey_keycode']) 152 | if index != -1: 153 | self.keycodeBox.setCurrentIndex(index) 154 | 155 | hotkeyLayout2.addWidget(self.keycodeBox) 156 | 157 | def save_settings(self, prefs): 158 | prefs[self.configName + '_hotkey_enabled'] = self.enabledBox.isChecked() 159 | prefs[self.configName + '_hotkey_ctrl'] = self.ctrlBox.isChecked() 160 | prefs[self.configName + '_hotkey_alt'] = self.altBox.isChecked() 161 | prefs[self.configName + '_hotkey_shift'] = self.shiftBox.isChecked() 162 | prefs[self.configName + '_hotkey_keycode'] = keycodes[unicode(self.keycodeBox.currentText())] 163 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import (unicode_literals, division, absolute_import, 3 | print_function) 4 | 5 | __license__ = 'GPL v3' 6 | __copyright__ = '2016, github.com/christineye' 7 | __docformat__ = 'restructuredtext en' 8 | 9 | # The class that all Interface Action plugin wrappers must inherit from 10 | from calibre.customize import InterfaceActionBase 11 | from calibre.customize import ViewerPlugin 12 | 13 | from PyQt5.Qt import QStandardItem, QStandardItemModel 14 | import os 15 | import json 16 | 17 | from PyQt5 import QtCore, QtWidgets 18 | from PyQt5.QtCore import Qt 19 | from PyQt5.Qt import ( 20 | QApplication, QWidget, QIcon, QAction, 21 | QPushButton, 22 | QDockWidget, QVBoxLayout, pyqtSlot, 23 | QWebPage, 24 | ) 25 | 26 | 27 | import types 28 | import re 29 | 30 | from calibre_plugins.tts_ebook_viewer.tts_typelib import constants 31 | 32 | 33 | class Responder(QtCore.QObject): 34 | @pyqtSlot(str) 35 | def readText(self, text): 36 | 37 | document = self.parent() 38 | ebookViewer = document.parent().manager 39 | ebookViewer.tts_speaker.readText(text) 40 | 41 | @pyqtSlot(result=bool) 42 | def loadNextPage(self): 43 | document = self.parent() 44 | ebookViewer = document.parent().manager 45 | return ebookViewer.tts_speaker.loadNextPage() 46 | 47 | @pyqtSlot() 48 | def playOrPause(self): 49 | document = self.parent() 50 | ebookViewer = document.parent().manager 51 | return ebookViewer.tts_speaker.playOrPause() 52 | 53 | @pyqtSlot() 54 | def toggleSelectMode(self): 55 | document = self.parent() 56 | ebookViewer = document.parent().manager 57 | return ebookViewer.tts_speaker.toggleSelectMode() 58 | 59 | @pyqtSlot() 60 | def stop(self): 61 | document = self.parent() 62 | ebookViewer = document.parent().manager 63 | return ebookViewer.tts_speaker.stop() 64 | 65 | class TTSSpeaker: 66 | 67 | def __init__(self, ui): 68 | self.isPlaying = False 69 | self.evaljs = None 70 | self.spVoice = None 71 | self.currentPosition = None 72 | self.ebookViewer = ui 73 | self.canResume = False 74 | self.selectMode = False 75 | self.toggleSelectModeButton = None 76 | 77 | def playOrPause(self): 78 | if not self.isPlaying: 79 | if self.canResume: 80 | self.isPlaying = True 81 | self.spVoice.Resume() 82 | else: 83 | self.initializeSpeech() 84 | self.readStartingAtPage() 85 | else: 86 | self.pause() 87 | 88 | def pause(self): 89 | if self.spVoice: 90 | self.isPlaying = False 91 | self.spVoice.Pause() 92 | self.canResume = True 93 | 94 | 95 | def toggleSelectMode(self): 96 | if not self.selectMode: 97 | self.stop() 98 | self.selectMode = True 99 | self.toggleSelectModeButton.setChecked(self.selectMode) 100 | 101 | self.evaljs(''' 102 | $("p").addClass("tts_selectMode"); 103 | $("p").click(function() 104 | { 105 | $(this).addClass("tts_reading") 106 | tts_speaker.readText($(this).text()) 107 | }); 108 | ''') 109 | 110 | else: 111 | self.disableSelectMode() 112 | 113 | 114 | def disableSelectMode(self): 115 | self.selectMode = False 116 | self.toggleSelectModeButton.setChecked(self.selectMode) 117 | 118 | 119 | self.evaljs(''' 120 | $('.tts_selectMode').removeClass("tts_selectMode"); 121 | $("p").unbind('click') 122 | ''') 123 | 124 | def stop(self): 125 | 126 | 127 | self.canResume = False 128 | self.isPlaying = False 129 | if self.spVoice: 130 | self.spVoice.Pause() 131 | self.spVoice = None 132 | 133 | self.evaljs(''' 134 | $currentReading = $(".tts_reading").removeClass("tts_reading") 135 | ''') 136 | 137 | def readText(self, text): 138 | import win32com.client 139 | 140 | self.initializeSpeech() 141 | 142 | self.spVoice.Speak(text, win32com.client.constants.SVSFlagsAsync) 143 | self.isPlaying = True 144 | self.disableSelectMode() 145 | 146 | 147 | 148 | def loadNextPage(self): 149 | if self.ebookViewer.current_index < len(self.ebookViewer.iterator.spine) - 1: 150 | print ("TTS: Loading next document") 151 | self.ebookViewer.next_document() 152 | return True 153 | 154 | else: 155 | print ("TTS: Could not load next page; stopping") 156 | self.isPlaying = False 157 | return False 158 | 159 | def initializeSpeech(self): 160 | 161 | if not self.spVoice: 162 | import win32com.client, weakref 163 | 164 | self.spVoice = win32com.client.Dispatch("SAPI.SpVoice") 165 | 166 | from calibre_plugins.tts_ebook_viewer.config import prefs 167 | 168 | if prefs['voice']: 169 | voice = None 170 | voices = self.spVoice.GetVoices("", "") 171 | 172 | for i in range(voices.Count): 173 | if voices.Item(i).GetDescription() == prefs['voice']: 174 | voice = voices.Item(i) 175 | break 176 | 177 | if voice: 178 | self.spVoice.Voice = voice 179 | else: 180 | prefs['voice'] = None 181 | 182 | self.spVoice.Rate = prefs['rate'] 183 | self.spVoice.Volume = prefs['volume'] 184 | 185 | 186 | self.spVoice.EventInterests = win32com.client.constants.SPEI_END_INPUT_STREAM 187 | self._advise = win32com.client.WithEvents(self.spVoice, SAPI5DriverEventSink) 188 | 189 | self._advise.setDriver(self) 190 | 191 | def readStartingAtPage(self): 192 | if self.evaljs: 193 | self.evaljs(''' 194 | 195 | $currentReading = $(".tts_reading") 196 | if ($currentReading.length < 1) 197 | { 198 | $currentReading = getFirstParagraphInView() 199 | } 200 | else 201 | { 202 | $currentReading = $currentReading[0] 203 | } 204 | 205 | tts_speaker.readText($currentReading.text()); 206 | ''') 207 | 208 | 209 | def OnWord(self, stream, pos, char, length): 210 | pass 211 | 212 | def OnEndStream(self, stream, pos): 213 | self.canResume = False 214 | self.evaljs(''' 215 | $currentReading = $(".tts_reading:first") 216 | $currentReading.removeClass("tts_reading") 217 | 218 | if ($currentReading.next(":visible").length > 0) 219 | { 220 | $currentReading.next(":visible").addClass("tts_reading") 221 | } 222 | else 223 | { 224 | // Look for a cousin element if no sibling 225 | 226 | $parents = $currentReading.parentsUntil("body") 227 | for (var i = 0; i < $parents.length; i++) 228 | { 229 | if ($parents[i].next().length > 0) 230 | { 231 | $parents[i].next().addClass("tts_reading") 232 | break; 233 | } 234 | } 235 | 236 | 237 | } 238 | 239 | $currentReading = $(".tts_reading:first") 240 | 241 | if ($currentReading.length > 0) 242 | { 243 | 244 | if (window.paged_display != null && window.paged_display.in_paged_mode) 245 | { 246 | 247 | $currentReading.each(function(index, element) 248 | { 249 | var br = element.getBoundingClientRect() 250 | var pos = calibre_utils.viewport_to_document(br.left, br.top, element.ownerDocument) 251 | 252 | window.paged_display.scroll_to_xpos(pos[0] + 10) 253 | }) 254 | 255 | } 256 | else 257 | { 258 | if (($currentReading.position().top + $currentReading.height() > $(window).scrollTop() + window.innerHeight ) || 259 | $currentReading.position().top < $(window).scrollTop()) 260 | { 261 | $.scrollTo($currentReading) 262 | } 263 | } 264 | 265 | 266 | } 267 | 268 | 269 | ''') 270 | 271 | if self.isPlaying: 272 | self.evaljs(''' 273 | 274 | if ($currentReading.length > 0) 275 | { 276 | tts_speaker.readText($currentReading.text()); 277 | } 278 | else 279 | { 280 | // Couldn't find a next element, attempt to load next document 281 | 282 | tts_speaker.loadNextPage() 283 | } 284 | ''') 285 | else: 286 | self.evaljs(''' 287 | if ($currentReading.length == 0) 288 | { 289 | // Couldn't find a next element, attempt to load next document 290 | 291 | tts_speaker.loadNextPage() 292 | } 293 | ''') 294 | 295 | 296 | class SAPI5DriverEventSink(object): 297 | def __init__(self): 298 | self._driver = None 299 | 300 | def setDriver(self, driver): 301 | self._driver = driver 302 | 303 | def OnWord(self, stream, pos, char, length): 304 | #self._driver._proxy.notify('started-word', location=char, length=length) 305 | pass 306 | 307 | def OnEndStream(self, stream, pos): 308 | self._driver.OnEndStream(stream, pos) 309 | 310 | class TextToSpeechPlugin(ViewerPlugin): 311 | ''' 312 | 313 | ''' 314 | name = 'TTS Ebook Viewer' 315 | description = 'adds TTS capability to ebook-viewer' 316 | supported_platforms = ['windows'] 317 | author = 'Christine Ye' 318 | version = (0, 0, 2) 319 | minimum_calibre_version = (0, 7, 53) 320 | 321 | 322 | 323 | def customize_ui(self, ui): 324 | self.ebookViewer = ui 325 | 326 | self.tts_speaker = TTSSpeaker(ui) 327 | self.ebookViewer.tts_speaker = self.tts_speaker 328 | 329 | ui.tool_bar.addSeparator() 330 | 331 | 332 | self.speak_button = QAction('play / pause', ui) 333 | ui.tool_bar.addAction(self.speak_button) 334 | self.speak_button.triggered.connect(self.tts_speaker.playOrPause) 335 | self.tts_speaker.toolbarButton = self.speak_button 336 | 337 | self.select_mode_button = QAction('select mode', ui) 338 | ui.tool_bar.addAction(self.select_mode_button) 339 | self.select_mode_button.triggered.connect(self.tts_speaker.toggleSelectMode) 340 | self.select_mode_button.setCheckable(True) 341 | self.select_mode_button.setChecked(False) 342 | self.tts_speaker.toggleSelectModeButton = self.select_mode_button 343 | 344 | self.stop_button = QAction('stop', ui) 345 | ui.tool_bar.addAction(self.stop_button) 346 | self.stop_button.triggered.connect(self.tts_speaker.stop) 347 | 348 | 349 | # HACK? 350 | # append a callback to the javaScriptWindowObjectCleared 351 | # signal receiver. If you don't do this, the `py_annotator` 352 | # object will be empty (has no python functions callable) 353 | # from js 354 | ui.view.document.mainFrame().javaScriptWindowObjectCleared.connect( 355 | self.add_window_objects) 356 | 357 | 358 | 359 | # print ("finished customizing ") 360 | 361 | def add_window_objects(self): 362 | self.ebookViewer.view.document.mainFrame().addToJavaScriptWindowObject('tts_speaker', Responder(self.ebookViewer.view.document)) 363 | 364 | # this function is by far the slowest, and is what causes pauses in the render 365 | def run_javascript(self, evaljs): 366 | ''' 367 | this gets called after load_javascript. 368 | ''' 369 | # inject css 370 | 371 | evaljs(''' 372 | $("").appendTo(document.head) 373 | ''') 374 | 375 | self.evaljs = evaljs 376 | self.tts_speaker.evaljs = evaljs 377 | 378 | def jsbool(pyBool): 379 | return unicode(pyBool).lower() 380 | 381 | 382 | from calibre_plugins.tts_ebook_viewer.config import prefs 383 | if prefs['pause_hotkey_enabled'] or prefs['stop_hotkey_enabled'] or prefs['select_hotkey_enabled']: 384 | hotkeyjs = ''' 385 | $(document).keydown(function(event) 386 | { 387 | if (%s && 388 | event.ctrlKey == %s && 389 | event.altKey == %s && 390 | event.shiftKey == %s && 391 | event.which == %i) 392 | { 393 | tts_speaker.playOrPause() 394 | } 395 | else if (%s && 396 | event.ctrlKey == %s && 397 | event.altKey == %s && 398 | event.shiftKey == %s && 399 | event.which == %i) 400 | { 401 | tts_speaker.stop() 402 | } 403 | else if (%s && 404 | event.ctrlKey == %s && 405 | event.altKey == %s && 406 | event.shiftKey == %s && 407 | event.which == %i) 408 | { 409 | tts_speaker.toggleSelectMode() 410 | } 411 | }) 412 | '''; 413 | 414 | hotkeyArgs = [] 415 | for hotkey in ['pause', 'stop', 'select']: 416 | hotkeyArgs.append(jsbool(prefs[hotkey + '_hotkey_enabled'])) 417 | hotkeyArgs.append(jsbool(prefs[hotkey + '_hotkey_ctrl'])) 418 | hotkeyArgs.append(jsbool(prefs[hotkey + '_hotkey_alt'])) 419 | hotkeyArgs.append(jsbool(prefs[hotkey + '_hotkey_shift'])) 420 | hotkeyArgs.append(prefs[hotkey + '_hotkey_keycode'] ) 421 | 422 | self.evaljs(hotkeyjs % tuple(hotkeyArgs)) 423 | 424 | if self.tts_speaker.isPlaying: 425 | print ("TTS: Start reading automatically") 426 | evaljs(''' 427 | $currentReading = getFirstParagraphInView().addClass("tts_reading") 428 | tts_speaker.readText($currentReading.text()); 429 | ''') 430 | 431 | def load_javascript(self, evaljs): 432 | ''' 433 | from calibre docs: 434 | This method is called every time a new HTML document is 435 | loaded in the viewer. Use it to load javascript libraries 436 | into the viewer. 437 | ''' 438 | evaljs(''' 439 | function getFirstParagraphInView() 440 | { 441 | $p = $("body").find(":visible") 442 | 443 | if (window.paged_display != null && window.paged_display.in_paged_mode) 444 | { 445 | var columnLeft = window.paged_display.current_column_location() 446 | 447 | $p.each(function(index, element) 448 | { 449 | var br = element.getBoundingClientRect() 450 | var pos = calibre_utils.viewport_to_document(br.left, br.top, element.ownerDocument) 451 | 452 | if (pos[0] >= columnLeft && pos[0] < columnLeft + window.paged_display.page_width) 453 | { 454 | $currentReading = $(element); 455 | $currentReading.addClass("tts_reading") 456 | 457 | return false; 458 | } 459 | }) 460 | } 461 | else 462 | { 463 | var $window = $(window); 464 | 465 | var docViewTop = $window.scrollTop(); 466 | var docViewBottom = docViewTop + $window.height(); 467 | 468 | 469 | $p.each(function(index, element) 470 | { 471 | $elem = $(element) 472 | 473 | var elemTop = $elem.offset().top; 474 | var elemBottom = elemTop + $elem.height(); 475 | 476 | if ((elemTop <= docViewBottom) && (elemTop >= docViewTop)) 477 | { 478 | $currentReading = $elem; 479 | $currentReading.addClass("tts_reading") 480 | 481 | 482 | return false; 483 | } 484 | }) 485 | } 486 | 487 | return $currentReading 488 | } 489 | ''') 490 | 491 | 492 | 493 | def is_customizable(self): 494 | ''' 495 | This method must return True to enable customization via 496 | Preferences->Plugins 497 | ''' 498 | return True 499 | 500 | def config_widget(self): 501 | ''' 502 | Implement this method and :meth:`save_settings` in your plugin to 503 | use a custom configuration dialog. 504 | 505 | This method, if implemented, must return a QWidget. The widget can have 506 | an optional method validate() that takes no arguments and is called 507 | immediately after the user clicks OK. Changes are applied if and only 508 | if the method returns True. 509 | 510 | If for some reason you cannot perform the configuration at this time, 511 | return a tuple of two strings (message, details), these will be 512 | displayed as a warning dialog to the user and the process will be 513 | aborted. 514 | 515 | The base class implementation of this method raises NotImplementedError 516 | so by default no user configuration is possible. 517 | ''' 518 | # It is important to put this import statement here rather than at the 519 | # top of the module as importing the config class will also cause the 520 | # GUI libraries to be loaded, which we do not want when using calibre 521 | # from the command line 522 | from calibre_plugins.tts_ebook_viewer.config import ConfigWidget 523 | # print('config config') 524 | return ConfigWidget() 525 | 526 | def save_settings(self, config_widget): 527 | ''' 528 | Save the settings specified by the user with config_widget. 529 | 530 | :param config_widget: The widget returned by :meth:`config_widget`. 531 | ''' 532 | config_widget.save_settings() 533 | --------------------------------------------------------------------------------