lib

KoSpeaker.cpp

00001 /* 
00002 *   This file is part of the KDE/KOffice project.
00003 *   Copyright (C) 2005, Gary Cramblitt <garycramblitt@comcast.net>
00004 *
00005 *   @author Gary Cramblitt <garycramblitt@comcast.net>
00006 *   @since KOffice 1.5
00007 *
00008 *   This library is free software; you can redistribute it and/or
00009 *   modify it under the terms of the GNU Library General Public
00010 *   License as published by the Free Software Foundation; either
00011 *   version 2 of the License, or (at your option) any later version.
00012 *
00013 *   This library is distributed in the hope that it will be useful,
00014 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
00015 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
00016 *   Library General Public License for more details.
00017 *
00018 *   You should have received a copy of the GNU Library General Public License
00019 *   along with this library; see the file COPYING.LIB.  If not, write to
00020 *   the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
00021 *   Boston, MA 02110-1301, USA.
00022 */
00023 
00024 // Qt includes.
00025 #include <qtimer.h>
00026 #include <qcursor.h>
00027 #include <qtooltip.h>
00028 #include <qwhatsthis.h>
00029 #include <qmenubar.h>
00030 #include <qlabel.h>
00031 #include <qbutton.h>
00032 #include <qcombobox.h>
00033 #include <qtabbar.h>
00034 #include <qgroupbox.h>
00035 #include <qlineedit.h>
00036 #include <qtextedit.h>
00037 #include <qlistview.h>
00038 #include <qlistbox.h>
00039 #include <qiconview.h>
00040 #include <qtable.h>
00041 #include <qgridview.h>
00042 #include <qregexp.h>
00043 #include <qstylesheet.h>
00044 
00045 // KDE includes.
00046 #include <kapplication.h>
00047 #include <klocale.h>
00048 #include <kglobal.h>
00049 #include <dcopclient.h>
00050 #include <kconfig.h>
00051 #include <ktrader.h>
00052 #include <kdebug.h>
00053 
00054 // KoSpeaker includes.
00055 #include "KoSpeaker.h"
00056 #include "KoSpeaker.moc"
00057 
00058 // ------------------ KoSpeakerPrivate ------------------------
00059 
00060 class KoSpeakerPrivate
00061 {
00062 public:
00063     KoSpeakerPrivate() :
00064         m_versionChecked(false),
00065         m_enabled(false),
00066         m_speakFlags(0),
00067         m_timeout(600),
00068         m_timer(0),
00069         m_prevPointerWidget(0),
00070         m_prevPointerId(-1),
00071         m_prevFocusWidget(0),
00072         m_prevFocusId(-1),
00073         m_prevWidget(0),
00074         m_prevId(-1),
00075         m_cancelSpeakWidget(false)
00076         {}
00077 
00078     // List of text jobs.
00079     QValueList<uint> m_jobNums;
00080     // Whether the version of KTTSD has been requested from the daemon.
00081     bool m_versionChecked;
00082     // KTTSD version string.
00083     QString m_kttsdVersion;
00084     // Language code of last spoken text.
00085     QString m_langCode;
00086     // Word used before speaking an accelerator letter.
00087     QString m_acceleratorPrefix;
00088     // Whether TTS service is available or not.
00089     bool m_enabled;
00090     // TTS options.
00091     uint m_speakFlags;
00092     // Timer which implements the polling interval.
00093     int m_timeout;
00094     QTimer* m_timer;
00095     // Widget and part of widget for 1) last widget under mouse pointer, 2) last widget with focus, and
00096     // last widget spoken.
00097     QWidget* m_prevPointerWidget;
00098     int m_prevPointerId;
00099     QWidget* m_prevFocusWidget;
00100     int m_prevFocusId;
00101     QWidget* m_prevWidget;
00102     int m_prevId;
00103     // True when cancelSpeakWidget has been called in response to customSpeakWidget signal.
00104     bool m_cancelSpeakWidget;
00105 };
00106 
00107 // ------------------ KoSpeaker -------------------------------
00108 
00109 KoSpeaker* KoSpeaker::KSpkr = 0L;
00110 
00111 KoSpeaker::KoSpeaker()
00112 {
00113     Q_ASSERT(!KSpkr);
00114     KSpkr = this;
00115     d = new KoSpeakerPrivate();
00116     readConfig(KGlobal::config());
00117 }
00118 
00119 KoSpeaker::~KoSpeaker()
00120 {
00121     if (d->m_jobNums.count() > 0) {
00122         for (int i = d->m_jobNums.count() - 1; i >= 0; i--)
00123             removeText(d->m_jobNums[i]);
00124         d->m_jobNums.clear();
00125     }
00126     delete d;
00127     KSpkr = 0;
00128 }
00129 
00130 bool KoSpeaker::isEnabled() const { return d->m_enabled; }
00131 
00132 void KoSpeaker::probe()
00133 {
00134     d->m_timer->stop();
00135     QWidget* w;
00136     QPoint pos;
00137     bool spoke = false;
00138     if ( d->m_speakFlags & SpeakFocusWidget ) {
00139         w = kapp->focusWidget();
00140         if (w) {
00141             spoke = maybeSayWidget(w);
00142             if (!spoke)
00143                 emit customSpeakWidget(w, pos, d->m_speakFlags);
00144         }
00145     }
00146     if ( !spoke && d->m_speakFlags & SpeakPointerWidget ) {
00147         pos = QCursor::pos();
00148         w = kapp->widgetAt(pos, true);
00149         if (w) {
00150             if (!maybeSayWidget(w, pos))
00151                 emit customSpeakWidget(w, pos, d->m_speakFlags);
00152         }
00153     }
00154     d->m_timer->start(d->m_timeout);
00155 }
00156 
00157 void KoSpeaker::queueSpeech(const QString& msg, const QString& langCode /*= QString()*/, bool first /*= true*/)
00158 {
00159     if (!startKttsd()) return;
00160     int jobCount = d->m_jobNums.count();
00161     if (first && jobCount > 0) {
00162         for (int i = jobCount - 1; i >= 0; i--)
00163             removeText(d->m_jobNums[i]);
00164         d->m_jobNums.clear();
00165         jobCount = 0;
00166     }
00167     QString s = msg.stripWhiteSpace();
00168     if (s.isEmpty()) return;
00169     // kdDebug() << "KoSpeaker::queueSpeech: s = [" << s << "]" << endl;
00170     // If no language code given, assume desktop setting.
00171     QString languageCode = langCode;
00172     if (langCode.isEmpty())
00173         languageCode = KGlobal::locale()->language();
00174     // kdDebug() << "KoSpeaker::queueSpeech:languageCode = " << languageCode << endl;
00175     // If KTTSD version is 0.3.5 or later, we can use the appendText method to submit a
00176     // single, multi-part text job.  Otherwise, must submit separate text jobs.
00177     // If language code changes, then must also start a new text job so that it will
00178     // be spoken in correct talker.
00179     if (getKttsdVersion().isEmpty())
00180         d->m_jobNums.append(setText(s, languageCode));
00181     else {
00182         if ((jobCount == 0) || (languageCode != d->m_langCode))
00183             d->m_jobNums.append(setText(s, languageCode));
00184         else
00185             appendText(s, d->m_jobNums[jobCount-1]);
00186     }
00187     d->m_langCode = languageCode;
00188 }
00189 
00190 void KoSpeaker::startSpeech()
00191 {
00192     for (uint i = 0; i < d->m_jobNums.count(); i++)
00193         startText(d->m_jobNums[i]);
00194 }
00195 
00196 void KoSpeaker::readConfig(KConfig* config)
00197 {
00198     delete d->m_timer;
00199     d->m_timer = 0;
00200     config->setGroup("TTS");
00201     d->m_speakFlags = 0;
00202     if (config->readBoolEntry("SpeakPointerWidget", false)) d->m_speakFlags |= SpeakPointerWidget;
00203     if (config->readBoolEntry("SpeakFocusWidget", false)) d->m_speakFlags |= SpeakFocusWidget;
00204     if (config->readBoolEntry("SpeakTooltips", true)) d->m_speakFlags |= SpeakTooltip;
00205     if (config->readBoolEntry("SpeakWhatsThis", false)) d->m_speakFlags |= SpeakWhatsThis;
00206     if (config->readBoolEntry("SpeakDisabled", true)) d->m_speakFlags |= SpeakDisabled;
00207     if (config->readBoolEntry("SpeakAccelerators", true)) d->m_speakFlags |= SpeakAccelerator;
00208     d->m_timeout = config->readNumEntry("PollingInterval", 600);
00209     d->m_acceleratorPrefix = config->readEntry("AcceleratorPrefixWord", i18n("Accelerator"));
00210     if (d->m_speakFlags & (SpeakPointerWidget | SpeakFocusWidget)) {
00211         if (startKttsd()) {
00212             d->m_timer = new QTimer( this );
00213             connect( d->m_timer, SIGNAL(timeout()), this, SLOT(probe()) );
00214             d->m_timer->start( d->m_timeout );
00215         }
00216     }
00217 }
00218 
00219 bool KoSpeaker::maybeSayWidget(QWidget* w, const QPoint& pos /*=QPoint()*/)
00220 {
00221     if (!w) return false;
00222 
00223     int id = -1;
00224     QString text;
00225 
00226     if (w->inherits("QViewportWidget")) {
00227         w = w->parentWidget();
00228         if (!w) return false;
00229     }
00230 
00231     // Handle widgets that have multiple parts.
00232 
00233     if ( w->inherits("QMenuBar") ) {
00234         QMenuBar* menuBar = dynamic_cast<QMenuBar *>(w);
00235         if (pos == QPoint()) {
00236             for (uint i = 0; i < menuBar->count(); ++i)
00237                 if (menuBar->isItemActive(menuBar->idAt(i))) {
00238                     id = menuBar->idAt(i);
00239                     break;
00240                 }
00241         }
00242             // TODO: This doesn't work.  Need way to figure out the QMenuItem underneath mouse pointer.
00243             // id = menuBarItemAt(menuBar, pos);
00244         if ( id != -1 )
00245             text = menuBar->text(id);
00246     }
00247     else
00248     if (w->inherits("QPopupMenu")) {
00249         QPopupMenu* popupMenu = dynamic_cast<QPopupMenu *>(w);
00250         if (pos == QPoint()) {
00251             for (uint i = 0; i < popupMenu->count(); ++i)
00252                 if (popupMenu->isItemActive(popupMenu->idAt(i))) {
00253                     id = popupMenu->idAt(i);
00254                     break;
00255                 }
00256         } else
00257             id = popupMenu->idAt(popupMenu->mapFromGlobal(pos));
00258         if ( id != -1 )
00259             text = popupMenu->text(id);
00260     }
00261     else
00262     if (w->inherits("QTabBar")) {
00263         QTabBar* tabBar = dynamic_cast<QTabBar *>(w);
00264         QTab* tab = 0;
00265         if (pos == QPoint())
00266             tab = tabBar->tabAt(tabBar->currentTab());
00267         else
00268             tab = tabBar->selectTab(tabBar->mapFromGlobal(pos));
00269         if (tab) {
00270             id = tab->identifier();
00271             text = tab->text();
00272         }
00273     }
00274     else
00275     if (w->inherits("QListView")) {
00276         QListView* lv = dynamic_cast<QListView *>(w);
00277         QListViewItem* item = 0;
00278         if (pos == QPoint())
00279             item = lv->currentItem();
00280         else
00281             item = lv->itemAt(lv->viewport()->mapFromGlobal(pos));
00282         if (item) {
00283             id = lv->itemPos(item);
00284             text = item->text(0);
00285             for (int col = 1; col < lv->columns(); ++col)
00286                 if (!item->text(col).isEmpty()) text += ". " + item->text(col);
00287         }
00288     }
00289     else
00290     if (w->inherits("QListBox")) {
00291         QListBox* lb = dynamic_cast<QListBox *>(w);
00292         // qt docs say coordinates are in "on-screen" coordinates.  What does that mean?
00293         QListBoxItem* item = 0;
00294         if (pos == QPoint())
00295             item = lb->item(lb->currentItem());
00296         else
00297             item = lb->itemAt(lb->mapFromGlobal(pos));
00298         if (item) {
00299             id = lb->index(item);
00300             text = item->text();
00301         }
00302     }
00303     else
00304     if (w->inherits("QIconView")) {
00305         QIconView* iv = dynamic_cast<QIconView *>(w);
00306         QIconViewItem* item = 0;
00307         if (pos == QPoint())
00308             item = iv->currentItem();
00309         else
00310             item = iv->findItem(iv->viewportToContents(iv->viewport()->mapFromGlobal(pos)));
00311         if (item) {
00312             id = item->index();
00313             text = item->text();
00314         }
00315     }
00316     else
00317     if (w->inherits("QTable")) {
00318         QTable* tbl = dynamic_cast<QTable *>(w);
00319         int row = -1;
00320         int col = -1;
00321         if (pos == QPoint()) {
00322             row = tbl->currentRow();
00323             col = tbl->currentColumn();
00324         } else {
00325             QPoint p = tbl->viewportToContents(tbl->viewport()->mapFromGlobal(pos));
00326             row = tbl->rowAt(p.y());
00327             col = tbl->columnAt(p.x());
00328         }
00329         if (row >= 0 && col >= 0) {
00330             id = (row * tbl->numCols()) + col;
00331             text = tbl->text(row, col);
00332         }
00333     }
00334     else
00335     if (w->inherits("QGridView")) {
00336         QGridView* gv = dynamic_cast<QGridView *>(w);
00337         // TODO: QGridView does not have a "current" row or column.  Don't think they can even get focus?
00338         int row = -1;
00339         int col = -1;
00340         if (pos != QPoint()) {
00341             QPoint p = gv->viewportToContents(gv->viewport()->mapFromGlobal(pos));
00342             row = gv->rowAt(p.y());
00343             col = gv->columnAt(p.x());
00344         }
00345         if (row >= 0 && col >= 0)
00346             id = (row * gv->numCols()) + col;
00347     }
00348 
00349     if (pos == QPoint()) {
00350         if ( w == d->m_prevFocusWidget && id == d->m_prevFocusId) return false;
00351         d->m_prevFocusWidget = w;
00352         d->m_prevFocusId = id;
00353     } else {
00354         if ( w == d->m_prevPointerWidget && id == d->m_prevPointerId) return false;
00355         d->m_prevPointerWidget = w;
00356         d->m_prevPointerId = id;
00357     }
00358     if (w == d->m_prevWidget && id == d->m_prevId) return false;
00359     d->m_prevWidget = w;
00360     d->m_prevId = id;
00361 
00362     // kdDebug() << " w = " << w << endl;
00363 
00364     d->m_cancelSpeakWidget = false;
00365     emit customSpeakNewWidget(w, pos, d->m_speakFlags);
00366     if (d->m_cancelSpeakWidget) return true;
00367 
00368     // Handle simple, single-part widgets.
00369     if ( w->inherits("QButton") )
00370         text = dynamic_cast<QButton *>(w)->text();
00371     else
00372     if (w->inherits("QComboBox"))
00373         text = dynamic_cast<QComboBox *>(w)->currentText();
00374     else 
00375     if (w->inherits("QLineEdit"))
00376         text = dynamic_cast<QLineEdit *>(w)->text();
00377     else 
00378     if (w->inherits("QTextEdit"))
00379         text = dynamic_cast<QTextEdit *>(w)->text();
00380     else
00381     if (w->inherits("QLabel"))
00382         text = dynamic_cast<QLabel *>(w)->text();
00383     else
00384     if (w->inherits("QGroupBox")) {
00385         // TODO: Should calculate this number from font size?
00386         if (w->mapFromGlobal(pos).y() < 30)
00387             text = dynamic_cast<QGroupBox *>(w)->title();
00388     }
00389 //    else
00390 //     if (w->inherits("QWhatsThat")) {
00391 //         text = dynamic_cast<QWhatsThat *>(w)->text();
00392 //     }
00393 
00394     text = text.stripWhiteSpace();
00395     if (!text.isEmpty()) {
00396         if (text.right(1) == ".")
00397             text += " ";
00398         else
00399             text += ". ";
00400     }
00401     if (d->m_speakFlags & SpeakTooltip || text.isEmpty()) {
00402         // kdDebug() << "pos = " << pos << endl;
00403         // QPoint p = w->mapFromGlobal(pos);
00404         // kdDebug() << "p = " << p << endl;
00405         QString t = QToolTip::textFor(w, pos);
00406         t = t.stripWhiteSpace();
00407         if (!t.isEmpty()) {
00408             if (t.right(1) != ".") t += ".";
00409             text += t + " ";
00410         }
00411     }
00412 
00413     if (d->m_speakFlags & SpeakWhatsThis || text.isEmpty()) {
00414         QString t = QWhatsThis::textFor(w, pos);
00415         t = t.stripWhiteSpace();
00416         if (!t.isEmpty()) {
00417             if (t.right(1) != ".") t += ".";
00418             text += t + " ";
00419         }
00420     }
00421 
00422     if (d->m_speakFlags & SpeakDisabled) {
00423         if (!w->isEnabled())
00424             text += i18n("A grayed widget", "Disabled. ");
00425     }
00426 
00427     return sayWidget(text);
00428 }
00429 
00430 bool KoSpeaker::sayWidget(const QString& msg)
00431 {
00432     QString s = msg;
00433     if (d->m_speakFlags & SpeakAccelerator) {
00434         int amp = s.find("&");
00435         if (amp >= 0) {
00436             QString acc = s.mid(++amp,1);
00437             acc = acc.stripWhiteSpace();
00438             if (!acc.isEmpty())
00439                 s += ". " + d->m_acceleratorPrefix + " " + acc + ".";
00440         }
00441     }
00442     s.remove("&");
00443     if (QStyleSheet::mightBeRichText(s)) {
00444         // kdDebug() << "richtext" << endl;
00445         s.replace(QRegExp("</?[pbius]>"), "");
00446         s.replace(QRegExp("</?h\\d>"), "");
00447         s.replace(QRegExp("<(br|hr)>"), " ");
00448         s.replace(QRegExp(
00449             "</?(qt|center|li|pre|div|span|em|strong|big|small|sub|sup|code|tt|font|nobr|ul|ol|dl|dt)>"), "");
00450         s.replace(QRegExp("</?(table|tr|th|td).*>"), "");
00451         s.replace(QRegExp("</?a\\s.+>"), "");
00452         // Replace <img source="small|frame_text"> with "small frame_text image. "
00453         s.replace(QRegExp("<img\\s.*(?:source=|src=)\"([^|\"]+)[|]?([^|\"]*)\">"), "\\1 \\2 image. ");
00454     }
00455     if (s.isEmpty()) return false;
00456     s.replace("Ctrl+", i18n("control plus "));
00457     s.replace("Alt+", i18n("alt plus "));
00458     s.replace("+", i18n(" plus "));
00459     sayScreenReaderOutput(s, "");
00460     return true;
00461 }
00462 
00463 // This doesn't work.  Anybody know how to find the menu item underneath mouse pointer
00464 // in a QMenuBar?
00465 // int KoSpeaker::menuBarItemAt(QMenuBar* m, const QPoint& p)
00466 // {
00467 //     for (uint i = 0; i < m->count(); i++) {
00468 //         int id = m->idAt(i);
00469 //         QMenuItem* mi = m->findItem(id);
00470 //         QWidget* w = mi->widget();
00471 //         if (w->rect().contains(w->mapFromGlobal(p))) return id;
00472 //     }
00473 //     return -1;
00474 // }
00475 
00476 /*static*/ bool KoSpeaker::isKttsdInstalled()
00477 {
00478      KTrader::OfferList offers = KTrader::self()->query("DCOP/Text-to-Speech", "Name == 'KTTSD'");
00479      return (offers.count() > 0);
00480 }
00481 
00482 bool KoSpeaker::startKttsd()
00483 {
00484     DCOPClient *client = kapp->dcopClient();
00485     // If KTTSD not running, start it.
00486     if (!client->isApplicationRegistered("kttsd"))
00487     {
00488         QString error;
00489         if (kapp->startServiceByDesktopName("kttsd", QStringList(), &error)) {
00490             kdDebug() << "KoSpeaker::startKttsd: error starting KTTSD service: " << error << endl;
00491             d->m_enabled = false;
00492         } else
00493             d->m_enabled = true;
00494     } else
00495         d->m_enabled = true;
00496     return d->m_enabled;
00497 }
00498 
00499 QString KoSpeaker::getKttsdVersion()
00500 {
00501     // Determine which version of KTTSD is running.  Note that earlier versions of KSpeech interface
00502     // did not support version() method, so we must manually marshall this call ourselves.
00503     if (d->m_enabled) {
00504         if (!d->m_versionChecked) {
00505             DCOPClient *client = kapp->dcopClient();
00506             QByteArray  data;
00507             QCString    replyType;
00508             QByteArray  replyData;
00509             if ( client->call("kttsd", "KSpeech", "version()", data, replyType, replyData, true) ) {
00510                 QDataStream arg(replyData, IO_ReadOnly);
00511                 arg >> d->m_kttsdVersion;
00512                 kdDebug() << "KoSpeaker::startKttsd: KTTSD version = " << d->m_kttsdVersion << endl;
00513             }
00514             d->m_versionChecked = true;
00515         }
00516     }
00517     return d->m_kttsdVersion;
00518 }
00519 
00520 void KoSpeaker::sayScreenReaderOutput(const QString &msg, const QString &talker)
00521 {
00522     if (msg.isEmpty()) return;
00523     DCOPClient *client = kapp->dcopClient();
00524     QByteArray  data;
00525     QCString    replyType;
00526     QByteArray  replyData;
00527     QDataStream arg(data, IO_WriteOnly);
00528     arg << msg << talker;
00529     if ( !client->call("kttsd", "KSpeech", "sayScreenReaderOutput(QString,QString)",
00530         data, replyType, replyData, true) ) {
00531         kdDebug() << "KoSpeaker::sayScreenReaderOutput: failed" << endl;
00532     }
00533 }
00534 
00535 uint KoSpeaker::setText(const QString &text, const QString &talker)
00536 {
00537     if (text.isEmpty()) return 0;
00538     DCOPClient *client = kapp->dcopClient();
00539     QByteArray  data;
00540     QCString    replyType;
00541     QByteArray  replyData;
00542     QDataStream arg(data, IO_WriteOnly);
00543     arg << text << talker;
00544     uint jobNum = 0;
00545     if ( !client->call("kttsd", "KSpeech", "setText(QString,QString)",
00546         data, replyType, replyData, true) ) {
00547         kdDebug() << "KoSpeaker::sayText: failed" << endl;
00548     } else {
00549         QDataStream arg2(replyData, IO_ReadOnly);
00550         arg2 >> jobNum;
00551     }
00552     return jobNum;
00553 }
00554 
00555 int KoSpeaker::appendText(const QString &text, uint jobNum /*=0*/)
00556 {
00557     if (text.isEmpty()) return 0;
00558     DCOPClient *client = kapp->dcopClient();
00559     QByteArray  data;
00560     QCString    replyType;
00561     QByteArray  replyData;
00562     QDataStream arg(data, IO_WriteOnly);
00563     arg << text << jobNum;
00564     int partNum = 0;
00565     if ( !client->call("kttsd", "KSpeech", "appendText(QString,uint)",
00566         data, replyType, replyData, true) ) {
00567         kdDebug() << "KoSpeaker::appendText: failed" << endl;
00568     } else {
00569         QDataStream arg2(replyData, IO_ReadOnly);
00570         arg2 >> partNum;
00571     }
00572     return partNum;
00573 }
00574 
00575 void KoSpeaker::startText(uint jobNum /*=0*/)
00576 {
00577     DCOPClient *client = kapp->dcopClient();
00578     QByteArray  data;
00579     QCString    replyType;
00580     QByteArray  replyData;
00581     QDataStream arg(data, IO_WriteOnly);
00582     arg << jobNum;
00583     if ( !client->call("kttsd", "KSpeech", "startText(uint)",
00584         data, replyType, replyData, true) ) {
00585         kdDebug() << "KoSpeaker::startText: failed" << endl;
00586     }
00587 }
00588 
00589 void KoSpeaker::removeText(uint jobNum /*=0*/)
00590 {
00591     DCOPClient *client = kapp->dcopClient();
00592     QByteArray  data;
00593     QCString    replyType;
00594     QByteArray  replyData;
00595     QDataStream arg(data, IO_WriteOnly);
00596     arg << jobNum;
00597     if ( !client->call("kttsd", "KSpeech", "removeText(uint)",
00598         data, replyType, replyData, true) ) {
00599         kdDebug() << "KoSpeaker::removeText: failed" << endl;
00600     }
00601 }
00602 
KDE Home | KDE Accessibility Home | Description of Access Keys