// // KMTTSWindowController.swift // PDF Reader Pro // // Created by liujiajie on 2023/12/12. // import Cocoa typealias TTSCloseWindowCallback = (_ isCloseWindow: Bool) -> Void let minSpeed: Float = 0.6 let maxSpeed: Float = 5.0 let standardSpeed: Float = 175.0 class KMTTSWindowController: NSWindowController, KMTTSManagerDelegate, NSWindowDelegate, NSTextFieldDelegate{ var pdfView: CPDFView! var closeWindowCallback: TTSCloseWindowCallback? @IBOutlet var sontinuouButton: NSButton! @IBOutlet var speedLabel: NSTextField! @IBOutlet var languageLabel: NSTextField! @IBOutlet var languageComboBox: NSPopUpButton! @IBOutlet var speedTextField: NSTextField! @IBOutlet var speedSlider: NSSlider! @IBOutlet var speedStepper: NSStepper! @IBOutlet var nextButton: KMToolbarItem! @IBOutlet var forwardButton: KMToolbarItem! @IBOutlet var playButton: KMToolbarItem! @IBOutlet var speedBox: NSBox! @IBOutlet var sppendCountLabel: NSTextField! var pdfSelection: CPDFSelection? var currentPageIndex: Int = 0 var isChangePage = false var voiceArrays = NSMutableArray() static let share = KMTTSWindowController() convenience init() { self.init(windowNibName: "KMTTSWindowController") } deinit { DistributedNotificationCenter.default().removeObserver(self) NotificationCenter.default.removeObserver(self) } override func windowDidLoad() { super.windowDidLoad() self.window?.title = NSLocalizedString("TTS", comment: "") self.speedStepper.minValue = Double(minSpeed) self.speedStepper.maxValue = Double(maxSpeed); self.speedSlider.minValue = Double(minSpeed); self.speedSlider.maxValue = Double(maxSpeed); self.nextButton.toolTip = KMLocalizedString("Next Page", nil) self.forwardButton.toolTip = KMLocalizedString("Previous Page", nil) self.speedSlider.isEnabled = true self.speedStepper.isEnabled = true self.playButton.toolTip = KMLocalizedString("Play", nil) self.sppendCountLabel.stringValue = KMLocalizedString("SpeedX", nil) let str = String(format: "%.1f", KMTTSManager.defalutManager.rate/Float(standardSpeed)) self.speedStepper.stringValue = str self.speedTextField.stringValue = str self.speedSlider.stringValue = str self.sontinuouButton.title = KMLocalizedString("Continuous Reading", nil); self.speedLabel.stringValue = KMLocalizedString("Speed", nil) + ":" self.languageLabel.stringValue = KMLocalizedString("Language", nil) + ":" let array = KMTTSManager.defalutManager.availableVoices() let currentVoicName = KMTTSManager.defalutManager.voice() var currentIndex = 0 let menu = NSMenu() for voiceType: NSSpeechSynthesizer.VoiceName in array { let voiceDic = NSMutableDictionary() // let dic = KMTTSManager.defalutManager.attributesForVoice(voiceType) let voiceLocaleIdentifier = ( NSSpeechSynthesizer.attributes( forVoice: voiceType )[ NSSpeechSynthesizer.VoiceAttributeKey.localeIdentifier ] as! String ) let name = self.switchLanguage(withCode: voiceLocaleIdentifier) voiceDic["voiceType"] = voiceType voiceDic["voiceName"] = name voiceArrays.add(voiceDic) } voiceArrays = self.sortArray(voiceArrays) for i in 0.. 0 { languageStaring = "\(name)(\(voiceName))" } let font = NSFont.systemFont(ofSize: 12.0) let namefont = NSFont.systemFont(ofSize: 10.0) let textRowSpace = NSMutableAttributedString(string: languageStaring ?? "") textRowSpace.addAttribute(.font, value: font, range: NSRange(location: 0, length: name.count)) textRowSpace.addAttribute(.font, value: namefont, range: NSRange(location: name.count, length: languageStaring!.count - name.count)) let item = NSMenuItem() item.attributedTitle = textRowSpace menu.addItem(item) } self.languageComboBox.menu = menu self.languageComboBox.selectItem(at: currentIndex) KMTTSManager.defalutManager.delegate = self self.updateViewColor() self.window?.standardWindowButton(.zoomButton)?.isHidden = true if KMTTSManager.defalutManager.isContinue { self.sontinuouButton.state = .on } else { self.sontinuouButton.state = .off } DistributedNotificationCenter.default().addObserver(self, selector: #selector(themeChanged(notification:)), name: NSNotification.Name("AppleInterfaceThemeChangedNotification"), object: nil) NotificationCenter.default.addObserver(self, selector: #selector(handlePageChangedNotification), name: NSNotification.Name.CPDFViewPageChanged, object: self.pdfView) } func updateViewColor() { var viewBackcolor = NSColor(red: 255.0/255.0, green: 255.0/255.0, blue: 255.0/255.0, alpha: 1.0) var textColor = NSColor(red: 51.0/255.0, green: 51.0/255.0, blue: 51.0/255.0, alpha: 1.0) var contentColor = NSColor(red: 102.0/255.0, green: 102.0/255.0, blue: 102.0/255.0, alpha: 1.0) var fillColor = NSColor.white if #available(macOS 10.14, *) { let appearanceName = NSApp.effectiveAppearance.bestMatch(from: [.aqua, .darkAqua]) if appearanceName == .darkAqua { viewBackcolor = NSColor(red: 69.0/255.0, green: 69.0/255.0, blue: 71.0/255.0, alpha: 1.0) textColor = NSColor(red: 255.0/255.0, green: 255.0/255.0, blue: 255.0/255.0, alpha: 1.0) contentColor = NSColor(red: 255.0/255.0, green: 255.0/255.0, blue: 255.0/255.0, alpha: 0.6) fillColor = NSColor(red: 101.0/255.0, green: 101.0/255.0, blue: 101.0/255.0, alpha: 1.0) } } self.window?.backgroundColor = viewBackcolor self.window?.isMovableByWindowBackground = true self.languageLabel.textColor = contentColor self.speedLabel.textColor = contentColor self.speedTextField.textColor = textColor self.speedBox.fillColor = fillColor self.sontinuouButton.setTitleColor(contentColor) } @objc func themeChanged(notification: NSNotification) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { self.updateViewColor() } } @objc func handlePageChangedNotification(notification: NSNotification) { guard let PDFView = notification.object as? CPDFView else { return } if PDFView == self.pdfView { if self.pdfView.canGoToNextPage() { self.nextButton.isEnabled = true } else{ self.nextButton.isEnabled = false } if self.pdfView.canGoToPreviousPage() { self.forwardButton.isEnabled = true } else { self.forwardButton.isEnabled = false } } } func sortArray(_ array: NSArray) -> NSMutableArray { let sortDesc = [NSSortDescriptor(key: "voiceName", ascending: true)] let sortedArr = array.sortedArray(using: sortDesc) as NSArray let tArray = NSMutableArray(array: sortedArr) return tArray } func windowShouldClose(_ sender: NSWindow) -> Bool { self.stopSpeaking() if let callBlack = closeWindowCallback { callBlack(true) } return true } override func showWindow(_ sender: Any?) { super.showWindow(sender) self.isChangePage = false if self.pdfView.canGoToNextPage() { self.nextButton.isEnabled = true } else{ self.nextButton.isEnabled = false } if self.pdfView.canGoToPreviousPage() { self.forwardButton.isEnabled = true } else { self.forwardButton.isEnabled = false } } func stopSpeaking() { if KMTTSManager.defalutManager.isSpeaking() || KMTTSManager.defalutManager.isPaused { self.isChangePage = true KMTTSManager.defalutManager.stopSpeaking() self.speedSlider.isEnabled = true self.speedStepper.isEnabled = true self.playButton.image = NSImage(named: "KMImageNameTTSStop") self.playButton.toolTip = NSLocalizedString("Play", comment: "") self.pdfView.setHighlightedSelections([]) } self.pdfSelection = nil self.pdfView = nil } func quikeStartSpeakingPDFPage(_ page: CPDFPage) { if KMTTSManager.defalutManager.isSpeaking() || KMTTSManager.defalutManager.isPaused { KMTTSManager.defalutManager.stopSpeaking() self.isChangePage = true } self.currentPageIndex = Int(self.pdfView.document?.index(for: page) ?? 0) self.startSpeakingPDFPage(page) } func startSpeakingPDFPage(_ page: CPDFPage) { let dex = self.languageComboBox.indexOfSelectedItem if dex >= 0 && dex < self.voiceArrays.count { let dic: NSMutableDictionary = self.voiceArrays[dex] as! NSMutableDictionary if let voiceName = dic["voiceType"] { KMTTSManager.defalutManager.setVoice(voice: voiceName as! NSSpeechSynthesizer.VoiceName) } } KMTTSManager.defalutManager.rate = self.speedSlider.floatValue * Float(standardSpeed) let isSuccess = KMTTSManager.defalutManager.startSpeakingPDFPage(page) if isSuccess { self.playButton.image = NSImage(named: "KMImageNameTTSPause") self.playButton.toolTip = NSLocalizedString("Pause", comment: "") self.speedSlider.isEnabled = false self.speedStepper.isEnabled = false } else { self.playButton.image = NSImage(named: "KMImageNameTTSStop") self.playButton.toolTip = NSLocalizedString("Play", comment: "") self.speedSlider.isEnabled = true self.speedStepper.isEnabled = true } } func startSpeakingPDFSelection(_ selection: CPDFSelection) { if KMTTSManager.defalutManager.isSpeaking() || KMTTSManager.defalutManager.isPaused { KMTTSManager.defalutManager.stopSpeaking() self.isChangePage = true } self.pdfSelection = selection self.pdfView?.setCurrentSelection(nil, animate: false) let dex = self.languageComboBox.indexOfSelectedItem if dex >= 0 && dex < self.voiceArrays.count { let dic: NSMutableDictionary = self.voiceArrays[dex] as! NSMutableDictionary if let voiceName = dic["voiceType"] as? NSSpeechSynthesizer.VoiceName { KMTTSManager.defalutManager.setVoice(voice: voiceName) } } KMTTSManager.defalutManager.rate = self.speedSlider.floatValue * Float(standardSpeed) let isSuccess = KMTTSManager.defalutManager.startSpeakingPDFSelection(selection) if isSuccess { self.speedSlider.isEnabled = false self.speedStepper.isEnabled = false self.playButton.image = NSImage(named: "KMImageNameTTSPause") self.playButton.toolTip = NSLocalizedString("Pause", comment: "") } else { self.speedSlider.isEnabled = true self.speedStepper.isEnabled = true self.playButton.image = NSImage(named: "KMImageNameTTSStop") self.playButton.toolTip = NSLocalizedString("Play", comment: "") } } @IBAction func buttonItemClick_Next(_ sender: NSButton) { if (KMTTSManager.defalutManager.isSpeaking()) { var currentPageNum = self.currentPageIndex; var page: CPDFPage? if (currentPageNum + 1 < self.pdfView.document.pageCount) { page = self.pdfView.document.page(at: UInt(currentPageNum+1)) self.currentPageIndex = currentPageNum + 1; } else { page = self.pdfView.document.page(at: self.pdfView.document.pageCount - 1) self.currentPageIndex = Int(self.pdfView.document.pageCount - 1) } self.pdfView.go(to: page) self.isChangePage = true self.startSpeakingPDFPage(page!) } } @IBAction func buttonItemClick_Forward(_ sender: NSButton) { if (KMTTSManager.defalutManager.isSpeaking()) { var currentPageNum = self.currentPageIndex; var page: CPDFPage? if (currentPageNum - 1 < 0) { page = self.pdfView.document.page(at: 0) self.currentPageIndex = 0; } else { page = self.pdfView.document.page(at: UInt(currentPageNum - 1)) self.currentPageIndex = currentPageNum - 1 } self.pdfView.go(to: page) self.isChangePage = true self.startSpeakingPDFPage(page!) } } @IBAction func buttonItemClick_Play(_ sender: KMToolbarItem) { if (KMTTSManager.defalutManager.isPaused) { self.playButton.image = NSImage(named: "KMImageNameTTSPause") self.playButton.toolTip = KMLocalizedString("Pause", nil) self.speedSlider.isEnabled = false self.speedStepper.isEnabled = false KMTTSManager.defalutManager.continueSpeaking() } else if (KMTTSManager.defalutManager.isSpeaking()) { self.isChangePage = true self.playButton.image = NSImage(named: "KMImageNameTTSStop") self.playButton.toolTip = KMLocalizedString("Play", nil) self.speedSlider.isEnabled = true self.speedStepper.isEnabled = true self.pdfView.setHighlightedSelection(nil, animated: false) KMTTSManager.defalutManager.pauseSpeaking() } else { if ((self.pdfSelection) != nil) { self.startSpeakingPDFSelection(self.pdfSelection!) } else { let page = self.pdfView.currentPage() self.currentPageIndex = self.pdfView.currentPageIndex if page != nil { self.startSpeakingPDFPage(page!) } } } } @IBAction func buttonItemClick_Continue(_ sender: NSButton) { KMTTSManager.defalutManager.isContinue = !(KMTTSManager.defalutManager.isContinue) } @IBAction func stepperItemClick_Speed(_ sender: NSButton) { var rate = self.speedStepper.floatValue let str = String(format: "%.1f", rate) self.speedStepper.stringValue = str self.speedTextField.stringValue = str self.speedSlider.stringValue = str rate = Float(standardSpeed) * rate KMTTSManager.defalutManager.rate = rate } @IBAction func sliderItemClick_Speed(_ sender: NSButton) { var rate: CGFloat = CGFloat(self.speedSlider.floatValue) let str = String(format: "%.1f", rate) self.speedStepper.stringValue = str self.speedTextField.stringValue = str self.speedSlider.stringValue = str rate = CGFloat(standardSpeed) * rate KMTTSManager.defalutManager.rate = Float(rate) } @IBAction func buttonItemClick_Language(_ sender: NSButton) { if KMTTSManager.defalutManager.isSpeaking() { self.isChangePage = true if let pdfSelection = self.pdfSelection { self.startSpeakingPDFSelection(pdfSelection) } else { let currentPageInd = self.currentPageIndex if currentPageInd < (self.pdfView.document.pageCount - 1) { let page = self.pdfView.document.page(at: UInt(currentPageInd)) self.startSpeakingPDFPage(page!) } } } } override func mouseDown(with event: NSEvent) { super.mouseDown(with: event) self.window?.makeFirstResponder(self) } //MARK: KMTTSManagerDelegate func ttsMananger(_ tts: KMTTSManager, willSpeak selection: CPDFSelection) { // if (selection != nil) { self.pdfView?.setHighlightedSelection(selection, animated: false) // } } func ttsManangerDidFinishSpeech(_ tts: KMTTSManager) -> Bool { var isFinish = true if self.pdfSelection != nil { self.pdfSelection = nil self.pdfView.setHighlightedSelections(nil) self.playButton.image = NSImage(named: "KMImageNameTTSStop") self.speedSlider.isEnabled = true self.speedStepper.isEnabled = true self.playButton.toolTip = KMLocalizedString("Play", nil) } else { if KMTTSManager.defalutManager.isContinue { if !isChangePage { if let pdfSelection = self.pdfSelection { self.pdfView.setHighlightedSelections(nil) self.playButton.image = NSImage(named: "KMImageNameTTSStop") self.speedSlider.isEnabled = true self.speedStepper.isEnabled = true self.playButton.toolTip = KMLocalizedString("Play", nil) } else { let currentPageIndex = self.currentPageIndex if currentPageIndex + 1 < self.pdfView.document.pageCount { let page = self.pdfView.document.page(at: UInt(currentPageIndex+1)) self.currentPageIndex = currentPageIndex+1 self.pdfView.go(to: page) self.startSpeakingPDFPage(page!) isFinish = false } else { self.pdfView.setHighlightedSelections(nil) self.playButton.image = NSImage(named: "KMImageNameTTSStop") self.speedSlider.isEnabled = true self.speedStepper.isEnabled = true self.playButton.toolTip = KMLocalizedString("Play", nil) } } } } else { if !isChangePage { self.speedSlider.isEnabled = true self.speedStepper.isEnabled = true self.pdfView.setHighlightedSelections(nil) self.playButton.image = NSImage(named: "KMImageNameTTSStop") self.playButton.toolTip = KMLocalizedString("Play", nil) } } if isChangePage { isChangePage = false } } return isFinish } func ttsManangerdidErrorSpeech(_ tts: KMTTSManager, message: String) { } //MARK: NSTextFieldDelegate func controlTextDidEndEditing(_ obj: Notification) { if let textField = obj.object as? NSTextField { if textField == self.speedTextField { var rate: Float = Float(textField.stringValue) ?? 0 if maxSpeed - rate < 0 { rate = maxSpeed } else if rate - minSpeed < 0 { rate = minSpeed } self.speedStepper.stringValue = String(format: "%.1f", rate) self.speedTextField.stringValue = String(format: "%.1f", rate) self.speedSlider.stringValue = String(format: "%.1f", rate) rate = standardSpeed * rate KMTTSManager.defalutManager.rate = rate } } } func switchLanguage(withCode code: String) -> String { var language = code if code == "ar_SA" { language = "Arabic (Saudi Arabia)" } else if code == "cs_CZ" { language = "Czech (Czech republic)" } else if code == "da_DK" { language = "Danish (Denmark)" } else if code == "de_DE" { language = "German (Germany)" } else if code == "el_GR" { language = "Greek (Greece)" } else if code == "en_AU" { language = "English (Australia)" } else if code == "en_GB" { language = "English (UK)" } else if code == "en_IE" { language = "English (Ireland)" } else if code == "en_US" { language = "English" } else if code == "en_ZA" { language = "English (South Africa)" } else if code == "es_ES" { language = "Spanish (Spain)" } else if code == "es_MX" { language = "Spanish (Mexico)" } else if code == "fi_FI" { language = "Finnish (Finland)" } else if code == "fr_CA" { language = "French (Canada)" } else if code == "fr_FR" { language = "French (France)" } else if code == "he_IL" { language = "Hebrew" } else if code == "hi_IN" { language = "Hindi (India)" } else if code == "hu_HU" { language = "Hungarian (Hungary)" } else if code == "id_ID" { language = "Indonesian (Indonesia)" } else if code == "it_IT" { language = "Italian (Italy)" } else if code == "ja_JP" { language = "日本語" } else if code == "ko_KR" { language = "Korean (South Korea)" } else if code == "nl_BE" { language = "Dutch (Belgium)" } else if code == "nl_NL" { language = "Dutch (Holland)" } else if code == "nb_NO" { language = "Norwegian (Norway)" } else if code == "pl_PL" { language = "Polish (Poland)" } else if code == "pt_BR" { language = "Portuguese (Brazil)" } else if code == "pt_PT" { language = "Portuguese (Portugal)" } else if code == "ro_RO" { language = "Romanian (Romania)" } else if code == "ru_RU" { language = "Russian (Russia)" } else if code == "sk_SK" { language = "Slovakia (Slovakia)" } else if code == "sv_SE" { language = "Swe (Sweden)" } else if code == "th_TH" { language = "Thai (Thailand)" } else if code == "tr_TR" { language = "Turkish (Turkey)" } else if code == "zh_CN" { language = "简体中文" } else if code == "zh_HK" { language = "繁體中文 (香港)" } else if code == "zh_TW" { language = "繁體中文 (台灣)" } else if code == "en_IN" { language = "English" } return language } }