1 /***************************************************************************
2  *   Copyright (C) 2002 by Gunnar Schmi Dt <kmouth@schmi-dt.de             *
3  *             (C) 2015 by Jeremy Whiting <jpwhiting@kde.org>              *
4  *                                                                         *
5  *   This program is free software; you can redistribute it and/or modify  *
6  *   it under the terms of the GNU General Public License as published by  *
7  *   the Free Software Foundation; either version 2 of the License, or     *
8  *   (at your option) any later version.                                   *
9  *                                                                         *
10  *   This program is distributed in the hope that it will be useful,       *
11  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
12  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
13  *   GNU General Public License for more details.                          *
14  *                                                                         *
15  *   You should have received a copy of the GNU General Public License     *
16  *   along with this program; if not, write to the                         *
17  *   Free Software Foundation, Inc.,                                       *
18  *   51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.          *
19  ***************************************************************************/
20 
21 // application specific includes
22 #include "phraselist.h"
23 
24 // include files for Qt
25 #include <QApplication>
26 #include <QClipboard>
27 #include <QFileDialog>
28 #include <QHBoxLayout>
29 #include <QKeyEvent>
30 #include <QListView>
31 #include <QPushButton>
32 #include <QStandardItem>
33 #include <QVBoxLayout>
34 
35 // include files for KDE
36 #include <KComboBox>
37 #include <KConfigGroup>
38 #include <QIcon>
39 #include <KLineEdit>
40 #include <KLocalizedString>
41 #include <KMessageBox>
42 #include <KXMLGUIFactory>
43 
44 #include "kmouth.h"
45 #include "texttospeechsystem.h"
46 #include "phrasebook/phrasebook.h"
47 #include "wordcompletion/wordcompletion.h"
48 
PhraseList(QWidget * parent,const QString & name)49 PhraseList::PhraseList(QWidget *parent, const QString &name) : QWidget(parent)
50 {
51     Q_UNUSED(name);
52     isInSlot = false;
53     // FIXME: Remove or change PaletteBase to Qt::OpaqueMode?
54     // setBackgroundMode(PaletteBase);
55     QVBoxLayout *layout = new QVBoxLayout(this);
56 
57     m_listView = new QListView(this);
58     m_model = new QStandardItemModel(this);
59     m_listView->setModel(m_model);
60     m_listView->setFocusPolicy(Qt::NoFocus);
61     m_listView->setSelectionMode(QAbstractItemView::ExtendedSelection);
62     m_listView->setWhatsThis(i18n("This list contains the history of spoken sentences. You can select sentences and press the speak button for re-speaking."));
63     layout->addWidget(m_listView);
64 
65     QHBoxLayout *rowLayout = new QHBoxLayout();
66     layout->addLayout(rowLayout);
67 
68     completion = new WordCompletion();
69 
70     dictionaryCombo = new KComboBox(this);
71     configureCompletionCombo(completion->wordLists());
72     rowLayout->addWidget(dictionaryCombo);
73 
74     lineEdit = new KLineEdit(this);
75     lineEdit->setFocusPolicy(Qt::StrongFocus);
76     lineEdit->setFrame(true);
77     lineEdit->setEchoMode(QLineEdit::Normal);
78     lineEdit->setCompletionObject(completion);
79     lineEdit->setAutoDeleteCompletionObject(true);
80     lineEdit->setWhatsThis(i18n("Into this edit field you can type a phrase. Click on the speak button in order to speak the entered phrase."));
81     rowLayout->addWidget(lineEdit);
82     lineEdit->setFocus();
83 
84     QIcon icon = QIcon::fromTheme(QStringLiteral("text-speak"));
85     speakButton = new QPushButton(icon, i18n("&Speak"), this);
86     speakButton->setFocusPolicy(Qt::NoFocus);
87     speakButton->setAutoDefault(false);
88     speakButton->setWhatsThis(i18n("Speaks the currently active sentence(s). If there is some text in the edit field it is spoken. Otherwise the selected sentences in the history (if any) are spoken."));
89     rowLayout->addWidget(speakButton);
90 
91     connect(dictionaryCombo, QOverload<const QString &>::of(&KComboBox::textActivated), completion, &WordCompletion::setWordList);
92     connect(completion, &WordCompletion::wordListsChanged, this, &PhraseList::configureCompletionCombo);
93 
94     connect(m_listView->selectionModel(),  &QItemSelectionModel::selectionChanged, this, &PhraseList::selectionChanged);
95     connect(m_listView,  &QWidget::customContextMenuRequested, this, &PhraseList::contextMenuRequested);
96     connect(lineEdit, &KLineEdit::returnKeyPressed, this, &PhraseList::lineEntered);
97     connect(lineEdit, &QLineEdit::textChanged, this, &PhraseList::textChanged);
98     connect(speakButton, &QAbstractButton::clicked, this, &PhraseList::speak);
99 }
100 
~PhraseList()101 PhraseList::~PhraseList()
102 {
103     delete speakButton;
104     delete m_model;
105     delete lineEdit;
106 }
107 
print(QPrinter * pPrinter)108 void PhraseList::print(QPrinter *pPrinter)
109 {
110     PhraseBook book;
111     QStandardItem *rootItem = m_model->invisibleRootItem();
112     int count = rootItem->rowCount();
113     for (int i = 0; i < count; ++i) {
114         QStandardItem *item = rootItem->child(i);
115         book += PhraseBookEntry(Phrase(item->text()));
116     }
117 
118     book.print(pPrinter);
119 }
120 
getListSelection()121 QStringList PhraseList::getListSelection()
122 {
123     QStringList res = QStringList();
124 
125     QStandardItem *rootItem = m_model->invisibleRootItem();
126     int count = rootItem->rowCount();
127     QItemSelectionModel *selection = m_listView->selectionModel();
128     for (int i = 0; i < count; ++i) {
129         QStandardItem *item = rootItem->child(i);
130         if (selection->isSelected(m_model->indexFromItem(item)))
131             res += item->text();
132     }
133 
134     return res;
135 }
136 
existListSelection()137 bool PhraseList::existListSelection()
138 {
139     return m_listView->selectionModel()->hasSelection();
140 }
141 
existEditSelection()142 bool PhraseList::existEditSelection()
143 {
144     return lineEdit->hasSelectedText();
145 }
146 
enableMenuEntries()147 void PhraseList::enableMenuEntries()
148 {
149     bool deselected = false;
150     bool selected = existListSelection();
151     QStandardItem *rootItem = m_model->invisibleRootItem();
152     int count = rootItem->rowCount();
153     QItemSelectionModel *selection = m_listView->selectionModel();
154     for (int i = 0; i < count; ++i) {
155         QStandardItem *item = rootItem->child(i);
156         if (!selection->isSelected(m_model->indexFromItem(item))) {
157             deselected = true;
158             break;
159         }
160     }
161     KMouthApp *theApp = (KMouthApp *) parentWidget();
162     theApp->enableMenuEntries(selected, deselected);
163 }
164 
configureCompletion()165 void PhraseList::configureCompletion()
166 {
167     completion->configure();
168 }
169 
configureCompletionCombo(const QStringList & list)170 void PhraseList::configureCompletionCombo(const QStringList &list)
171 {
172     QString current = completion->currentWordList();
173     dictionaryCombo->clear();
174     if (list.isEmpty())
175         dictionaryCombo->hide();
176     else if (list.count() == 1) {
177         dictionaryCombo->addItems(list);
178         dictionaryCombo->setCurrentIndex(0);
179         dictionaryCombo->hide();
180     } else {
181         dictionaryCombo->addItems(list);
182         dictionaryCombo->show();
183 
184         QStringList::ConstIterator it;
185         int i = 0;
186         for (it = list.begin(), i = 0; it != list.end(); ++it, ++i) {
187             if (current == *it) {
188                 dictionaryCombo->setCurrentIndex(i);
189                 return;
190             }
191         }
192     }
193 }
194 
saveCompletionOptions()195 void PhraseList::saveCompletionOptions()
196 {
197     KConfigGroup cg(KSharedConfig::openConfig(), "General Options");
198     cg.writeEntry("Show speak button", speakButton->isVisible() || !lineEdit->isVisible());
199 
200     KConfigGroup cg2(KSharedConfig::openConfig(), "Completion");
201     cg2.writeEntry("Mode", static_cast<int>(lineEdit->completionMode()));
202     cg2.writeEntry("List", completion->currentWordList());
203 }
204 
readCompletionOptions()205 void PhraseList::readCompletionOptions()
206 {
207     KConfigGroup cg(KSharedConfig::openConfig(), "General Options");
208     if (!cg.readEntry("Show speak button", true))
209         speakButton->hide();
210 
211     if (KSharedConfig::openConfig()->hasGroup("Completion")) {
212         KConfigGroup cg2(KSharedConfig::openConfig(), "Completion");
213         //int mode = cg2.readEntry("Mode", int(KGlobalSettings::completionMode()));
214         //lineEdit->setCompletionMode(static_cast<KGlobalSettings::Completion>(mode));
215 
216         QString current = cg2.readEntry("List", QString());
217         const QStringList list = completion->wordLists();
218         QStringList::ConstIterator it;
219         int i = 0;
220         for (it = list.constBegin(), i = 0; it != list.constEnd(); ++it, ++i) {
221             if (current == *it) {
222                 dictionaryCombo->setCurrentIndex(i);
223                 return;
224             }
225         }
226     }
227 }
228 
saveWordCompletion()229 void PhraseList::saveWordCompletion()
230 {
231     completion->save();
232 }
233 
234 
selectAllEntries()235 void PhraseList::selectAllEntries()
236 {
237     m_listView->selectAll();
238 }
239 
deselectAllEntries()240 void PhraseList::deselectAllEntries()
241 {
242     m_listView->clearSelection();
243 }
244 
speak()245 void PhraseList::speak()
246 {
247     QString phrase = lineEdit->text();
248     if (phrase.isNull() || phrase.isEmpty())
249         speakListSelection();
250     else {
251         insertIntoPhraseList(phrase, true);
252         speakPhrase(phrase);
253     }
254 }
255 
cut()256 void PhraseList::cut()
257 {
258     if (lineEdit->hasSelectedText())
259         lineEdit->cut();
260     else
261         cutListSelection();
262 }
263 
copy()264 void PhraseList::copy()
265 {
266     if (lineEdit->hasSelectedText())
267         lineEdit->copy();
268     else
269         copyListSelection();
270 }
271 
paste()272 void PhraseList::paste()
273 {
274     lineEdit->paste();
275 }
276 
insert(const QString & s)277 void PhraseList::insert(const QString &s)
278 {
279     setEditLineText(s);
280 }
281 
speakListSelection()282 void PhraseList::speakListSelection()
283 {
284     speakPhrase(getListSelection().join(QLatin1String("\n")));
285 }
286 
removeListSelection()287 void PhraseList::removeListSelection()
288 {
289     if (m_listView->selectionModel()->hasSelection()) {
290         QList<QModelIndex> selected = m_listView->selectionModel()->selectedRows();
291         std::sort(selected.begin(), selected.end());
292         // Iterate over the rows backwards so we don't modify the .row of any indexes in selected.
293         for (int i = selected.size() - 1; i >= 0; --i) {
294             QModelIndex index = selected.at(i);
295             m_model->removeRows(index.row(), 1);
296         }
297     }
298     enableMenuEntries();
299 }
300 
cutListSelection()301 void PhraseList::cutListSelection()
302 {
303     copyListSelection();
304     removeListSelection();
305 }
306 
copyListSelection()307 void PhraseList::copyListSelection()
308 {
309     QApplication::clipboard()->setText(getListSelection().join(QLatin1String("\n")));
310 }
311 
lineEntered(const QString & phrase)312 void PhraseList::lineEntered(const QString &phrase)
313 {
314     if (phrase.isNull() || phrase.isEmpty())
315         speakListSelection();
316     else {
317         insertIntoPhraseList(phrase, true);
318         speakPhrase(phrase);
319     }
320 }
321 
speakPhrase(const QString & phrase)322 void PhraseList::speakPhrase(const QString &phrase)
323 {
324     QApplication::setOverrideCursor(Qt::WaitCursor);
325     KMouthApp *theApp = (KMouthApp *) parentWidget();
326     QString language = completion->languageOfWordList(completion->currentWordList());
327     theApp->getTTSSystem()->speak(phrase, language);
328     QApplication::restoreOverrideCursor();
329 }
330 
insertIntoPhraseList(const QString & phrase,bool clearEditLine)331 void PhraseList::insertIntoPhraseList(const QString &phrase, bool clearEditLine)
332 {
333     int lastLine = m_model->rowCount() - 1;
334     if ((lastLine == -1) || (phrase != m_model->data(m_model->index(lastLine, 0)).toString())) {
335         QStandardItem *item = new QStandardItem(phrase);
336         m_model->appendRow(item);
337         if (clearEditLine)
338             completion->addSentence(phrase);
339     }
340 
341     if (clearEditLine) {
342         lineEdit->selectAll();
343         line.clear();
344     }
345     enableMenuEntries();
346 }
347 
contextMenuRequested(const QPoint & pos)348 void PhraseList::contextMenuRequested(const QPoint &pos)
349 {
350     QString name;
351     if (existListSelection())
352         name = QStringLiteral("phraselist_selection_popup");
353     else
354         name = QStringLiteral("phraselist_popup");
355 
356     KMouthApp *theApp = (KMouthApp *) parentWidget();
357     KXMLGUIFactory *factory = theApp->factory();
358     QMenu *popup = (QMenu *)factory->container(name, theApp);
359     if (popup != nullptr) {
360         popup->exec(pos, nullptr);
361     }
362 }
363 
textChanged(const QString & s)364 void PhraseList::textChanged(const QString &s)
365 {
366     if (!isInSlot) {
367         isInSlot = true;
368         line = s;
369         m_listView->setCurrentIndex(m_model->index(m_model->rowCount() - 1, 0));
370         m_listView->clearSelection();
371         isInSlot = false;
372     }
373 }
374 
selectionChanged()375 void PhraseList::selectionChanged()
376 {
377     if (!isInSlot) {
378         isInSlot = true;
379 
380         QStringList sel = getListSelection();
381 
382         if (sel.empty())
383             setEditLineText(line);
384         else if (sel.count() == 1)
385             setEditLineText(sel.first());
386         else {
387             setEditLineText(QLatin1String(""));
388         }
389         isInSlot = false;
390     }
391     enableMenuEntries();
392 }
393 
setEditLineText(const QString & s)394 void PhraseList::setEditLineText(const QString &s)
395 {
396     lineEdit->end(false);
397     while (!(lineEdit->text().isNull() || lineEdit->text().isEmpty()))
398         lineEdit->backspace();
399     lineEdit->insert(s);
400 }
401 
keyPressEvent(QKeyEvent * e)402 void PhraseList::keyPressEvent(QKeyEvent *e)
403 {
404     if (e->key() == Qt::Key_Up) {
405         bool selected = m_listView->selectionModel()->hasSelection();
406 
407         if (!selected) {
408             m_listView->setCurrentIndex(m_model->index(m_model->rowCount() - 1, 0));
409             //listBox->ensureCurrentVisible ();
410         } else {
411             int curr = m_listView->currentIndex().row();
412 
413             if (curr == -1) {
414                 isInSlot = true;
415                 m_listView->clearSelection();
416                 isInSlot = false;
417                 curr = m_model->rowCount() - 1;
418                 m_listView->setCurrentIndex(m_model->index(curr, 0));
419                 //listBox->ensureCurrentVisible ();
420             } else if (curr != 0) {
421                 isInSlot = true;
422                 m_listView->clearSelection();
423                 isInSlot = false;
424                 m_listView->setCurrentIndex(m_model->index(curr - 1, 0));
425                 //listBox->ensureCurrentVisible ();
426             }
427         }
428 
429         e->accept();
430     } else if (e->key() == Qt::Key_Down) {
431         bool selected = m_listView->selectionModel()->hasSelection();
432 
433         if (selected) {
434             int curr = m_listView->currentIndex().row();
435 
436             if (curr == (int)m_model->rowCount() - 1) {
437                 m_listView->clearSelection();
438             } else if (curr != -1) {
439                 isInSlot = true;
440                 m_listView->clearSelection();
441                 isInSlot = false;
442                 m_listView->setCurrentIndex(m_model->index(curr + 1, 0));
443                 //listBox->ensureCurrentVisible ();
444             }
445         }
446         e->accept();
447     } else if (e->modifiers() & Qt::ControlModifier) {
448         if (e->key() == Qt::Key_C) {
449             copy();
450             e->accept();
451         } else if (e->key() == Qt::Key_X) {
452             cut();
453             e->accept();
454         }
455     } else
456         e->ignore();
457 }
458 
save()459 void PhraseList::save()
460 {
461     // We want to save a history of spoken sentences here. However, as
462     // the class PhraseBook does already provide a method for saving
463     // phrase books in both the phrase book format and plain text file
464     // format we use that method here.
465 
466     PhraseBook book;
467     QStandardItem *rootItem = m_model->invisibleRootItem();
468     int count = m_model->rowCount();
469     for (int i = 0; i < count; ++i) {
470         QStandardItem *item = rootItem->child(i);
471         book += PhraseBookEntry(Phrase(item->text()));
472     }
473 
474     QUrl url;
475     if (book.save(this, i18n("Save As"), url, false) == -1)
476         KMessageBox::sorry(this, i18n("There was an error saving file\n%1", url.url()));
477 }
478 
open()479 void PhraseList::open()
480 {
481     QUrl url = QFileDialog::getOpenFileUrl(this, i18n("Open File as History"), QUrl(),
482                                            i18n("All Files (*);;Phrase Books (*.phrasebook);;Plain Text Files (*.txt)"));
483 
484     if (!url.isEmpty())
485         open(url);
486 }
487 
open(const QUrl & url)488 void PhraseList::open(const QUrl &url)
489 {
490     // We want to open a history of spoken sentences here. However, as
491     // the class PhraseBook does already provide a method for opening
492     // both phrase books and plain text files we use that method here.
493 
494     PhraseBook book;
495     if (book.open(url)) {
496         // convert PhraseBookEntryList -> QStringList
497         QStringList list = book.toStringList();
498         m_model->clear();
499         QStringList::iterator it;
500         for (it = list.begin(); it != list.end(); ++it)
501             insertIntoPhraseList(*it, false);
502     } else
503         KMessageBox::sorry(this, i18n("There was an error loading file\n%1", url.toDisplayString()));
504 }
505 
506