/* * This file is part of the KDE/KOffice project. * Copyright (C) 2005, Gary Cramblitt * * @author Gary Cramblitt * @since KOffice 1.5 * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ // TQt includes. #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // KDE includes. #include #include #include #include #include #include #include // KoSpeaker includes. #include "KoSpeaker.h" #include "KoSpeaker.moc" // ------------------ KoSpeakerPrivate ------------------------ class KoSpeakerPrivate { public: KoSpeakerPrivate() : m_versionChecked(false), m_enabled(false), m_speakFlags(0), m_timeout(600), m_timer(0), m_prevPointerWidget(0), m_prevPointerId(-1), m_prevFocusWidget(0), m_prevFocusId(-1), m_prevWidget(0), m_prevId(-1), m_cancelSpeakWidget(false) {} // List of text jobs. TQValueList m_jobNums; // Whether the version of KTTSD has been requested from the daemon. bool m_versionChecked; // KTTSD version string. TQString m_kttsdVersion; // Language code of last spoken text. TQString m_langCode; // Word used before speaking an accelerator letter. TQString m_acceleratorPrefix; // Whether TTS service is available or not. bool m_enabled; // TTS options. uint m_speakFlags; // Timer which implements the polling interval. int m_timeout; TQTimer* m_timer; // Widget and part of widget for 1) last widget under mouse pointer, 2) last widget with focus, and // last widget spoken. TQWidget* m_prevPointerWidget; int m_prevPointerId; TQWidget* m_prevFocusWidget; int m_prevFocusId; TQWidget* m_prevWidget; int m_prevId; // True when cancelSpeakWidget has been called in response to customSpeakWidget signal. bool m_cancelSpeakWidget; }; // ------------------ KoSpeaker ------------------------------- KoSpeaker* KoSpeaker::KSpkr = 0L; KoSpeaker::KoSpeaker() { Q_ASSERT(!KSpkr); KSpkr = this; d = new KoSpeakerPrivate(); readConfig(TDEGlobal::config()); } KoSpeaker::~KoSpeaker() { if (d->m_jobNums.count() > 0) { for (int i = d->m_jobNums.count() - 1; i >= 0; i--) removeText(d->m_jobNums[i]); d->m_jobNums.clear(); } delete d; KSpkr = 0; } bool KoSpeaker::isEnabled() const { return d->m_enabled; } void KoSpeaker::probe() { d->m_timer->stop(); TQWidget* w; TQPoint pos; bool spoke = false; if ( d->m_speakFlags & SpeakFocusWidget ) { w = kapp->focusWidget(); if (w) { spoke = maybeSayWidget(w); if (!spoke) emit customSpeakWidget(w, pos, d->m_speakFlags); } } if ( !spoke && d->m_speakFlags & SpeakPointerWidget ) { pos = TQCursor::pos(); w = kapp->widgetAt(pos, true); if (w) { if (!maybeSayWidget(w, pos)) emit customSpeakWidget(w, pos, d->m_speakFlags); } } d->m_timer->start(d->m_timeout); } void KoSpeaker::queueSpeech(const TQString& msg, const TQString& langCode /*= TQString()*/, bool first /*= true*/) { if (!startKttsd()) return; int jobCount = d->m_jobNums.count(); if (first && jobCount > 0) { for (int i = jobCount - 1; i >= 0; i--) removeText(d->m_jobNums[i]); d->m_jobNums.clear(); jobCount = 0; } TQString s = msg.stripWhiteSpace(); if (s.isEmpty()) return; // kdDebug() << "KoSpeaker::queueSpeech: s = [" << s << "]" << endl; // If no language code given, assume desktop setting. TQString languageCode = langCode; if (langCode.isEmpty()) languageCode = TDEGlobal::locale()->language(); // kdDebug() << "KoSpeaker::queueSpeech:languageCode = " << languageCode << endl; // If KTTSD version is 0.3.5 or later, we can use the appendText method to submit a // single, multi-part text job. Otherwise, must submit separate text jobs. // If language code changes, then must also start a new text job so that it will // be spoken in correct talker. if (getKttsdVersion().isEmpty()) d->m_jobNums.append(setText(s, languageCode)); else { if ((jobCount == 0) || (languageCode != d->m_langCode)) d->m_jobNums.append(setText(s, languageCode)); else appendText(s, d->m_jobNums[jobCount-1]); } d->m_langCode = languageCode; } void KoSpeaker::startSpeech() { for (uint i = 0; i < d->m_jobNums.count(); i++) startText(d->m_jobNums[i]); } void KoSpeaker::readConfig(TDEConfig* config) { delete d->m_timer; d->m_timer = 0; config->setGroup("TTS"); d->m_speakFlags = 0; if (config->readBoolEntry("SpeakPointerWidget", false)) d->m_speakFlags |= SpeakPointerWidget; if (config->readBoolEntry("SpeakFocusWidget", false)) d->m_speakFlags |= SpeakFocusWidget; if (config->readBoolEntry("SpeakTooltips", true)) d->m_speakFlags |= SpeakTooltip; if (config->readBoolEntry("SpeakWhatsThis", false)) d->m_speakFlags |= SpeakWhatsThis; if (config->readBoolEntry("SpeakDisabled", true)) d->m_speakFlags |= SpeakDisabled; if (config->readBoolEntry("SpeakAccelerators", true)) d->m_speakFlags |= SpeakAccelerator; d->m_timeout = config->readNumEntry("PollingInterval", 600); d->m_acceleratorPrefix = config->readEntry("AcceleratorPrefixWord", i18n("Accelerator")); if (d->m_speakFlags & (SpeakPointerWidget | SpeakFocusWidget)) { if (startKttsd()) { d->m_timer = new TQTimer( this ); connect( d->m_timer, TQ_SIGNAL(timeout()), this, TQ_SLOT(probe()) ); d->m_timer->start( d->m_timeout ); } } } bool KoSpeaker::maybeSayWidget(TQWidget* w, const TQPoint& pos /*=TQPoint()*/) { if (!w) return false; int id = -1; TQString text; if (w->inherits("TQViewportWidget")) { w = w->parentWidget(); if (!w) return false; } // Handle widgets that have multiple parts. if ( w->inherits("TQMenuBar") ) { TQMenuBar* menuBar = dynamic_cast(w); if (pos == TQPoint()) { for (uint i = 0; i < menuBar->count(); ++i) if (menuBar->isItemActive(menuBar->idAt(i))) { id = menuBar->idAt(i); break; } } // TODO: This doesn't work. Need way to figure out the TQMenuItem underneath mouse pointer. // id = menuBarItemAt(menuBar, pos); if ( id != -1 ) text = menuBar->text(id); } else if (w->inherits("TQPopupMenu")) { TQPopupMenu* popupMenu = dynamic_cast(w); if (pos == TQPoint()) { for (uint i = 0; i < popupMenu->count(); ++i) if (popupMenu->isItemActive(popupMenu->idAt(i))) { id = popupMenu->idAt(i); break; } } else id = popupMenu->idAt(popupMenu->mapFromGlobal(pos)); if ( id != -1 ) text = popupMenu->text(id); } else if (w->inherits("TQTabBar")) { TQTabBar* tabBar = dynamic_cast(w); TQTab* tab = 0; if (pos == TQPoint()) tab = tabBar->tabAt(tabBar->currentTab()); else tab = tabBar->selectTab(tabBar->mapFromGlobal(pos)); if (tab) { id = tab->identifier(); text = tab->text(); } } else if (w->inherits("TQListView")) { TQListView* lv = dynamic_cast(w); TQListViewItem* item = 0; if (pos == TQPoint()) item = lv->currentItem(); else item = lv->itemAt(lv->viewport()->mapFromGlobal(pos)); if (item) { id = lv->itemPos(item); text = item->text(0); for (int col = 1; col < lv->columns(); ++col) if (!item->text(col).isEmpty()) text += ". " + item->text(col); } } else if (w->inherits("TQListBox")) { TQListBox* lb = dynamic_cast(w); // qt docs say coordinates are in "on-screen" coordinates. What does that mean? TQListBoxItem* item = 0; if (pos == TQPoint()) item = lb->item(lb->currentItem()); else item = lb->itemAt(lb->mapFromGlobal(pos)); if (item) { id = lb->index(item); text = item->text(); } } else if (w->inherits("TQIconView")) { TQIconView* iv = dynamic_cast(w); TQIconViewItem* item = 0; if (pos == TQPoint()) item = iv->currentItem(); else item = iv->findItem(iv->viewportToContents(iv->viewport()->mapFromGlobal(pos))); if (item) { id = item->index(); text = item->text(); } } else if (w->inherits("TQTable")) { TQTable* tbl = dynamic_cast(w); int row = -1; int col = -1; if (pos == TQPoint()) { row = tbl->currentRow(); col = tbl->currentColumn(); } else { TQPoint p = tbl->viewportToContents(tbl->viewport()->mapFromGlobal(pos)); row = tbl->rowAt(p.y()); col = tbl->columnAt(p.x()); } if (row >= 0 && col >= 0) { id = (row * tbl->numCols()) + col; text = tbl->text(row, col); } } else if (w->inherits("TQGridView")) { TQGridView* gv = dynamic_cast(w); // TODO: TQGridView does not have a "current" row or column. Don't think they can even get focus? int row = -1; int col = -1; if (pos != TQPoint()) { TQPoint p = gv->viewportToContents(gv->viewport()->mapFromGlobal(pos)); row = gv->rowAt(p.y()); col = gv->columnAt(p.x()); } if (row >= 0 && col >= 0) id = (row * gv->numCols()) + col; } if (pos == TQPoint()) { if ( w == d->m_prevFocusWidget && id == d->m_prevFocusId) return false; d->m_prevFocusWidget = w; d->m_prevFocusId = id; } else { if ( w == d->m_prevPointerWidget && id == d->m_prevPointerId) return false; d->m_prevPointerWidget = w; d->m_prevPointerId = id; } if (w == d->m_prevWidget && id == d->m_prevId) return false; d->m_prevWidget = w; d->m_prevId = id; // kdDebug() << " w = " << w << endl; d->m_cancelSpeakWidget = false; emit customSpeakNewWidget(w, pos, d->m_speakFlags); if (d->m_cancelSpeakWidget) return true; // Handle simple, single-part widgets. if ( w->inherits("TQButton") ) text = dynamic_cast(w)->text(); else if (w->inherits("TQComboBox")) text = dynamic_cast(w)->currentText(); else if (w->inherits("TQLineEdit")) text = dynamic_cast(w)->text(); else if (w->inherits("TQTextEdit")) text = dynamic_cast(w)->text(); else if (w->inherits("TQLabel")) text = dynamic_cast(w)->text(); else if (w->inherits("TQGroupBox")) { // TODO: Should calculate this number from font size? if (w->mapFromGlobal(pos).y() < 30) text = dynamic_cast(w)->title(); } // else // if (w->inherits("TQWhatsThat")) { // text = dynamic_cast(w)->text(); // } text = text.stripWhiteSpace(); if (!text.isEmpty()) { if (text.right(1) == ".") text += " "; else text += ". "; } if (d->m_speakFlags & SpeakTooltip || text.isEmpty()) { // kdDebug() << "pos = " << pos << endl; // TQPoint p = w->mapFromGlobal(pos); // kdDebug() << "p = " << p << endl; TQString t = TQToolTip::textFor(w, pos); t = t.stripWhiteSpace(); if (!t.isEmpty()) { if (t.right(1) != ".") t += "."; text += t + " "; } } if (d->m_speakFlags & SpeakWhatsThis || text.isEmpty()) { TQString t = TQWhatsThis::textFor(w, pos); t = t.stripWhiteSpace(); if (!t.isEmpty()) { if (t.right(1) != ".") t += "."; text += t + " "; } } if (d->m_speakFlags & SpeakDisabled) { if (!w->isEnabled()) text += i18n("A grayed widget", "Disabled. "); } return sayWidget(text); } bool KoSpeaker::sayWidget(const TQString& msg) { TQString s = msg; if (d->m_speakFlags & SpeakAccelerator) { int amp = s.find("&"); if (amp >= 0) { TQString acc = s.mid(++amp,1); acc = acc.stripWhiteSpace(); if (!acc.isEmpty()) s += ". " + d->m_acceleratorPrefix + " " + acc + "."; } } s.remove("&"); if (TQStyleSheet::mightBeRichText(s)) { // kdDebug() << "richtext" << endl; s.replace(TQRegExp(""), ""); s.replace(TQRegExp(""), ""); s.replace(TQRegExp("<(br|hr)>"), " "); s.replace(TQRegExp( ""), ""); s.replace(TQRegExp(""), ""); s.replace(TQRegExp(""), ""); // Replace with "small frame_text image. " s.replace(TQRegExp(""), "\\1 \\2 image. "); } if (s.isEmpty()) return false; s.replace("Ctrl+", i18n("control plus ")); s.replace("Alt+", i18n("alt plus ")); s.replace("+", i18n(" plus ")); sayScreenReaderOutput(s, ""); return true; } // This doesn't work. Anybody know how to find the menu item underneath mouse pointer // in a TQMenuBar? // int KoSpeaker::menuBarItemAt(TQMenuBar* m, const TQPoint& p) // { // for (uint i = 0; i < m->count(); i++) { // int id = m->idAt(i); // TQMenuItem* mi = m->findItem(id); // TQWidget* w = mi->widget(); // if (w->rect().contains(w->mapFromGlobal(p))) return id; // } // return -1; // } /*static*/ bool KoSpeaker::isKttsdInstalled() { TDETrader::OfferList offers = TDETrader::self()->query("DCOP/Text-to-Speech", "Name == 'KTTSD'"); return (offers.count() > 0); } bool KoSpeaker::startKttsd() { DCOPClient *client = kapp->dcopClient(); // If KTTSD not running, start it. if (!client->isApplicationRegistered("kttsd")) { TQString error; if (kapp->startServiceByDesktopName("kttsd", TQStringList(), &error)) { kdDebug() << "KoSpeaker::startKttsd: error starting KTTSD service: " << error << endl; d->m_enabled = false; } else d->m_enabled = true; } else d->m_enabled = true; return d->m_enabled; } TQString KoSpeaker::getKttsdVersion() { // Determine which version of KTTSD is running. Note that earlier versions of KSpeech interface // did not support version() method, so we must manually marshall this call ourselves. if (d->m_enabled) { if (!d->m_versionChecked) { DCOPClient *client = kapp->dcopClient(); TQByteArray data; TQCString replyType; TQByteArray replyData; if ( client->call("kttsd", "KSpeech", "version()", data, replyType, replyData, true) ) { TQDataStream arg(replyData, IO_ReadOnly); arg >> d->m_kttsdVersion; kdDebug() << "KoSpeaker::startKttsd: KTTSD version = " << d->m_kttsdVersion << endl; } d->m_versionChecked = true; } } return d->m_kttsdVersion; } void KoSpeaker::sayScreenReaderOutput(const TQString &msg, const TQString &talker) { if (msg.isEmpty()) return; DCOPClient *client = kapp->dcopClient(); TQByteArray data; TQCString replyType; TQByteArray replyData; TQDataStream arg(data, IO_WriteOnly); arg << msg << talker; if ( !client->call("kttsd", "KSpeech", "sayScreenReaderOutput(TQString,TQString)", data, replyType, replyData, true) ) { kdDebug() << "KoSpeaker::sayScreenReaderOutput: failed" << endl; } } uint KoSpeaker::setText(const TQString &text, const TQString &talker) { if (text.isEmpty()) return 0; DCOPClient *client = kapp->dcopClient(); TQByteArray data; TQCString replyType; TQByteArray replyData; TQDataStream arg(data, IO_WriteOnly); arg << text << talker; uint jobNum = 0; if ( !client->call("kttsd", "KSpeech", "setText(TQString,TQString)", data, replyType, replyData, true) ) { kdDebug() << "KoSpeaker::sayText: failed" << endl; } else { TQDataStream arg2(replyData, IO_ReadOnly); arg2 >> jobNum; } return jobNum; } int KoSpeaker::appendText(const TQString &text, uint jobNum /*=0*/) { if (text.isEmpty()) return 0; DCOPClient *client = kapp->dcopClient(); TQByteArray data; TQCString replyType; TQByteArray replyData; TQDataStream arg(data, IO_WriteOnly); arg << text << jobNum; int partNum = 0; if ( !client->call("kttsd", "KSpeech", "appendText(TQString,uint)", data, replyType, replyData, true) ) { kdDebug() << "KoSpeaker::appendText: failed" << endl; } else { TQDataStream arg2(replyData, IO_ReadOnly); arg2 >> partNum; } return partNum; } void KoSpeaker::startText(uint jobNum /*=0*/) { DCOPClient *client = kapp->dcopClient(); TQByteArray data; TQCString replyType; TQByteArray replyData; TQDataStream arg(data, IO_WriteOnly); arg << jobNum; if ( !client->call("kttsd", "KSpeech", "startText(uint)", data, replyType, replyData, true) ) { kdDebug() << "KoSpeaker::startText: failed" << endl; } } void KoSpeaker::removeText(uint jobNum /*=0*/) { DCOPClient *client = kapp->dcopClient(); TQByteArray data; TQCString replyType; TQByteArray replyData; TQDataStream arg(data, IO_WriteOnly); arg << jobNum; if ( !client->call("kttsd", "KSpeech", "removeText(uint)", data, replyType, replyData, true) ) { kdDebug() << "KoSpeaker::removeText: failed" << endl; } }