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 #include "phrasebook.h"
22 #include "phrasebookparser.h"
23 
24 #include <QBuffer>
25 #include <QFile>
26 #include <QFileDialog>
27 #include <QFontDatabase>
28 #include <QPainter>
29 #include <QStack>
30 #include <QUrl>
31 #include <QXmlSimpleReader>
32 
33 #include <KActionMenu>
34 #include <KDesktopFile>
35 #include <KIO/StoredTransferJob>
36 #include <KLocalizedString>
37 #include <KMessageBox>
38 
39 
Phrase()40 Phrase::Phrase()
41 {
42     this->phrase.clear();
43     this->shortcut.clear();
44 }
45 
Phrase(const QString & phrase)46 Phrase::Phrase(const QString &phrase)
47 {
48     this->phrase = phrase;
49     this->shortcut.clear();
50 }
51 
Phrase(const QString & phrase,const QString & shortcut)52 Phrase::Phrase(const QString &phrase, const QString &shortcut)
53 {
54     this->phrase = phrase;
55     this->shortcut = shortcut;
56 }
57 
getPhrase() const58 QString Phrase::getPhrase() const
59 {
60     return phrase;
61 }
62 
getShortcut() const63 QString Phrase::getShortcut() const
64 {
65     return shortcut;
66 }
67 
setPhrase(const QString & phrase)68 void Phrase::setPhrase(const QString &phrase)
69 {
70     this->phrase = phrase;
71 }
72 
setShortcut(const QString & shortcut)73 void Phrase::setShortcut(const QString &shortcut)
74 {
75     this->shortcut = shortcut;
76 }
77 
78 // ***************************************************************************
79 
PhraseBookEntry()80 PhraseBookEntry::PhraseBookEntry()
81 {
82     phrase = Phrase();
83     level = 1;
84     isPhraseValue = false;
85 }
86 
PhraseBookEntry(const Phrase & phrase,int level,bool isPhrase)87 PhraseBookEntry::PhraseBookEntry(const Phrase &phrase, int level, bool isPhrase)
88 {
89     this->phrase = phrase;
90     this->level = level;
91     isPhraseValue = isPhrase;
92 }
93 
isPhrase() const94 bool PhraseBookEntry::isPhrase() const
95 {
96     return isPhraseValue;
97 }
98 
getPhrase() const99 Phrase PhraseBookEntry::getPhrase() const
100 {
101     return phrase;
102 }
103 
getLevel() const104 int PhraseBookEntry::getLevel() const
105 {
106     return level;
107 }
108 
109 // ***************************************************************************
110 
print(QPrinter * pPrinter)111 void PhraseBook::print(QPrinter *pPrinter)
112 {
113     QPainter printpainter;
114     printpainter.begin(pPrinter);
115 
116     QRect size = printpainter.viewport();
117     int x = size.x();
118     int y = size.y();
119     int w = size.width();
120     printpainter.setFont(QFont(QFontDatabase::systemFont(QFontDatabase::GeneralFont).family(), 12));
121     QFontMetrics metrics = printpainter.fontMetrics();
122 
123     PhraseBookEntryList::iterator it;
124     for (it = begin(); it != end(); ++it) {
125         QRect rect = metrics.boundingRect(x + 16 * (*it).getLevel(), y,
126                                           w - 16 * (*it).getLevel(), 0,
127                                           Qt::AlignJustify | Qt::TextWordWrap,
128                                           (*it).getPhrase().getPhrase());
129 
130         if (y + rect.height() > size.height()) {
131             pPrinter->newPage();
132             y = 0;
133         }
134         printpainter.drawText(x + 16 * (*it).getLevel(), y,
135                               w - 16 * (*it).getLevel(), rect.height(),
136                               Qt::AlignJustify | Qt::TextWordWrap,
137                               (*it).getPhrase().getPhrase());
138         y += rect.height();
139     }
140 
141     printpainter.end();
142 }
143 
decode(const QString & xml)144 bool PhraseBook::decode(const QString &xml)
145 {
146     QXmlInputSource source;
147     source.setData(xml);
148     return decode(source);
149 }
150 
decode(QXmlInputSource & source)151 bool PhraseBook::decode(QXmlInputSource &source)
152 {
153     PhraseBookParser parser;
154     QXmlSimpleReader reader;
155     reader.setFeature(QStringLiteral("http://qt-project.org/xml/features/report-start-end-entity"), true);
156     reader.setContentHandler(&parser);
157 
158     if (reader.parse(source)) {
159         PhraseBookEntryList::clear();
160         *(PhraseBookEntryList *)this += parser.getPhraseList();
161         return true;
162     } else
163         return false;
164 }
165 
encodeString(const QString & str)166 QByteArray encodeString(const QString &str)
167 {
168     QByteArray res = "";
169     for (int i = 0; i < (int)str.length(); i++) {
170         QChar ch = str.at(i);
171         ushort uc = ch.unicode();
172         QByteArray number; number.setNum(uc);
173         if ((uc > 127) || (uc < 32) || (ch == QLatin1Char('<')) || (ch == QLatin1Char('>')) || (ch == QLatin1Char('&')) || (ch == QLatin1Char(';')))
174             res = res + "&#" + number + ';';
175         else
176             res = res + (char)uc;
177     }
178     return res;
179 }
180 
encode()181 QString PhraseBook::encode()
182 {
183     QString result;
184     result  = QStringLiteral("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
185     result += QLatin1String("<!DOCTYPE phrasebook>\n");
186     result += QLatin1String("<phrasebook>\n");
187 
188     PhraseBookEntryList::iterator it;
189     int level = 0;
190     for (it = begin(); it != end(); ++it) {
191         int newLevel = (*it).getLevel();
192         while (level < newLevel) {
193             result += QLatin1String("<phrasebook>\n");
194             level++;
195         }
196         while (level > newLevel) {
197             result += QLatin1String("</phrasebook>\n");
198             level--;
199         }
200 
201         if ((*it).isPhrase()) {
202             Phrase phrase = (*it).getPhrase();
203             result += QStringLiteral("<phrase shortcut=\"") + QLatin1String(encodeString(phrase.getShortcut()));
204             result += QStringLiteral("\">") + QLatin1String(encodeString(phrase.getPhrase())) + QStringLiteral("</phrase>\n");
205         } else {
206             Phrase phrase = (*it).getPhrase();
207             result += QStringLiteral("<phrasebook name=\"") + QLatin1String(encodeString(phrase.getPhrase())) + QStringLiteral("\">\n");
208             level++;
209         }
210     }
211     while (level > 0) {
212         result += QLatin1String("</phrasebook>\n");
213         level--;
214     }
215     result += QLatin1String("</phrasebook>");
216     return result;
217 }
218 
toStringList()219 QStringList PhraseBook::toStringList()
220 {
221     QStringList result;
222 
223     PhraseBook::iterator it;
224     for (it = begin(); it != end(); ++it) {
225         if ((*it).isPhrase())
226             result += (*it).getPhrase().getPhrase();
227     }
228     return result;
229 }
230 
save(const QUrl & url)231 bool PhraseBook::save(const QUrl &url)
232 {
233     return save(url, url.fileName().endsWith(QLatin1String(".phrasebook")));
234 }
235 
236 
save(QTextStream & stream,bool asPhrasebook)237 void PhraseBook::save(QTextStream &stream, bool asPhrasebook)
238 {
239     if (asPhrasebook)
240         stream << encode();
241     else
242         stream << toStringList().join(QLatin1String("\n"));
243 }
244 
save(const QUrl & url,bool asPhrasebook)245 bool PhraseBook::save(const QUrl &url, bool asPhrasebook)
246 {
247     if (url.isLocalFile()) {
248         QFile file(url.path());
249         if (!file.open(QIODevice::WriteOnly))
250             return false;
251 
252         QTextStream stream(&file);
253         save(stream, asPhrasebook);
254         file.close();
255 
256         if (file.error() != QFile::NoError)
257             return false;
258         else
259             return true;
260     } else {
261         QByteArray data;
262         QTextStream ts(&data);
263         save(ts, asPhrasebook);
264         ts.flush();
265 
266         KIO::StoredTransferJob *uploadJob = KIO::storedPut(data, url, -1);
267         return uploadJob->exec();
268     }
269 }
270 
save(QWidget * parent,const QString & title,QUrl & url,bool phrasebookFirst)271 int PhraseBook::save(QWidget *parent, const QString &title, QUrl &url, bool phrasebookFirst)
272 {
273     // KFileDialog::getSaveUrl(...) is not useful here as we need
274     // to know the requested file type.
275 
276     QString filters;
277     if (phrasebookFirst)
278         filters = i18n("Phrase Books (*.phrasebook);;Plain Text Files (*.txt);;All Files (*)");
279     else
280         filters = i18n("Plain Text Files (*.txt);;Phrase Books (*.phrasebook);;All Files (*)");
281 
282     QFileDialog fdlg(parent, title, QString(), filters);
283     fdlg.setAcceptMode(QFileDialog::AcceptSave);
284 
285     if (fdlg.exec() != QDialog::Accepted || fdlg.selectedUrls().size() < 1) {
286         return 0;
287     }
288 
289     url = fdlg.selectedUrls().at(0);
290 
291     if (url.isEmpty() || !url.isValid()) {
292         return -1;
293     }
294 
295     if (QFile::exists(url.toLocalFile())) {
296         if (KMessageBox::warningContinueCancel(nullptr, QStringLiteral("<qt>%1</qt>").arg(i18n("The file %1 already exists. "
297                                                "Do you want to overwrite it?", url.url())), i18n("File Exists"), KGuiItem(i18n("&Overwrite"))) == KMessageBox::Cancel) {
298             return 0;
299         }
300     }
301 
302     bool result;
303     if (fdlg.selectedNameFilter() == QLatin1String("*.phrasebook")) {
304         if (url.fileName(QUrl::PrettyDecoded).contains(QLatin1Char('.')) == 0) {
305             url = url.adjusted(QUrl::RemoveFilename);
306             url.setPath(url.path() + url.fileName(QUrl::PrettyDecoded) + QStringLiteral(".phrasebook"));
307         } else if (url.fileName(QUrl::PrettyDecoded).rightRef(11).contains(QLatin1String(".phrasebook"), Qt::CaseInsensitive) == 0) {
308             int filetype = KMessageBox::questionYesNoCancel(nullptr, QStringLiteral("<qt>%1</qt>").arg(i18n("Your chosen filename <i>%1</i> has a different extension than <i>.phrasebook</i>. "
309                            "Do you wish to add <i>.phrasebook</i> to the filename?", url.fileName())), i18n("File Extension"), KGuiItem(i18n("Add")), KGuiItem(i18n("Do Not Add")));
310             if (filetype == KMessageBox::Cancel) {
311                 return 0;
312             }
313             if (filetype == KMessageBox::Yes) {
314                 url = url.adjusted(QUrl::RemoveFilename);
315                 url.setPath(url.path() + url.fileName(QUrl::PrettyDecoded) + QStringLiteral(".phrasebook"));
316             }
317         }
318         result = save(url, true);
319     } else if (fdlg.selectedNameFilter() == QLatin1String("*.txt")) {
320         if (url.fileName(QUrl::PrettyDecoded).rightRef(11).contains(QLatin1String(".phrasebook"), Qt::CaseInsensitive) == 0) {
321             result = save(url, false);
322         } else {
323             int filetype = KMessageBox::questionYesNoCancel(nullptr, QStringLiteral("<qt>%1</qt>").arg(i18n("Your chosen filename <i>%1</i> has the extension <i>.phrasebook</i>. "
324                            "Do you wish to save in phrasebook format?", url.fileName())), i18n("File Extension"), KGuiItem(i18n("As Phrasebook")), KGuiItem(i18n("As Plain Text")));
325             if (filetype == KMessageBox::Cancel) {
326                 return 0;
327             }
328             if (filetype == KMessageBox::Yes) {
329                 result = save(url, true);
330             } else {
331                 result = save(url, false);
332             }
333         }
334     } else // file format "All files" requested, so decide by extension
335         result = save(url);
336 
337     if (result)
338         return 1;
339     else
340         return -1;
341 }
342 
open(const QUrl & url)343 bool PhraseBook::open(const QUrl &url)
344 {
345     KIO::StoredTransferJob *downloadJob = KIO::storedGet(url);
346     if (downloadJob->exec()) {
347         // First: try to load it as a normal phrase book
348         QBuffer fileBuffer;
349         fileBuffer.setData(downloadJob->data());
350 
351         QXmlInputSource source(&fileBuffer);
352         bool error = !decode(source);
353 
354         // Second: if the file does not contain a phrase book, load it as
355         // a plain text file
356         if (error) {
357             // Load each line of the plain text file as a new phrase
358 
359             QTextStream stream(&fileBuffer);
360 
361             while (!stream.atEnd()) {
362                 QString s = stream.readLine();
363                 if (!(s.isNull() || s.isEmpty()))
364                     *this += PhraseBookEntry(Phrase(s, QLatin1String("")), 0, true);
365             }
366             error = false;
367         }
368 
369         return !error;
370     }
371     return false;
372 }
373 
standardPhraseBooks()374 StandardBookList PhraseBook::standardPhraseBooks()
375 {
376     // Get all the standard phrasebook filenames in bookPaths.
377     QStringList bookPaths;
378     const QStringList dirs =
379         QStandardPaths::locateAll(QStandardPaths::AppDataLocation, QStringLiteral("books"), QStandardPaths::LocateDirectory);
380     for (const QString &dir : dirs) {
381         const QStringList locales = QDir(dir).entryList(QDir::Dirs | QDir::NoDotAndDotDot);
382         for (const QString &locale: locales) {
383             const QStringList fileNames =
384                 QDir(dir + QLatin1Char('/') + locale).entryList(QStringList() << QStringLiteral("*.phrasebook"));
385             for (const QString &file : fileNames) {
386                 bookPaths.append(dir + QLatin1Char('/') + locale + QLatin1Char('/') + file);
387             }
388         }
389     }
390     QStringList bookNames;
391     QMap<QString, StandardBook> bookMap;
392     QStringList::iterator it;
393     // Iterate over all books creating a phrasebook for each, creating a StandardBook of each.
394     for (it = bookPaths.begin(); it != bookPaths.end(); ++it) {
395         PhraseBook pbook;
396         // Open the phrasebook.
397         if (pbook.open(QUrl::fromLocalFile(*it))) {
398             StandardBook book;
399             book.name = (*pbook.begin()).getPhrase().getPhrase();
400 
401             book.path = displayPath(*it);
402             book.filename = *it;
403 
404             bookNames += book.path + QLatin1Char('/') + book.name;
405             bookMap [book.path + QLatin1Char('/') + book.name] = book;
406         }
407     }
408 
409     bookNames.sort();
410 
411     StandardBookList result;
412     for (it = bookNames.begin(); it != bookNames.end(); ++it)
413         result += bookMap [*it];
414 
415     return result;
416 }
417 
displayPath(const QString & filename)418 QString PhraseBook::displayPath(const QString &filename)
419 {
420     QFileInfo file(filename);
421     QString path = file.path();
422     QString dispPath;
423     int position = path.indexOf(QLatin1String("/kmouth/books/")) + QStringLiteral("/kmouth/books/").length();
424 
425     while (path.length() > position) {
426         file.setFile(path);
427 
428         KDesktopFile *dirDesc = new KDesktopFile(QStandardPaths::GenericDataLocation, path + QStringLiteral("/.directory"));
429         QString name = dirDesc->readName();
430         delete dirDesc;
431 
432         if (name.isNull() || name.isEmpty())
433             dispPath += QLatin1Char('/') + file.fileName();
434         else
435             dispPath += QLatin1Char('/') + name;
436 
437         path = file.path();
438     }
439     return dispPath;
440 }
441 
addToGUI(QMenu * popup,KToolBar * toolbar,KActionCollection * phrases,QObject * receiver,const char * slot) const442 void PhraseBook::addToGUI(QMenu *popup, KToolBar *toolbar, KActionCollection *phrases,
443                           QObject *receiver, const char *slot) const
444 {
445     if ((popup != nullptr) || (toolbar != nullptr)) {
446         QStack<QWidget*> stack;
447         QWidget *parent = popup;
448         int level = 0;
449 
450         QList<PhraseBookEntry>::ConstIterator it;
451         for (it = begin(); it != end(); ++it) {
452             int newLevel = (*it).getLevel();
453             while (newLevel > level) {
454                 KActionMenu *menu = phrases->add<KActionMenu>(QStringLiteral("phrasebook"));
455                 menu->setPopupMode(QToolButton::InstantPopup);
456                 if (parent == popup)
457                     toolbar->addAction(menu);
458                 if (parent != nullptr) {
459                     parent->addAction(menu);
460                     stack.push(parent);
461                 }
462                 parent = menu->menu();
463                 level++;
464             }
465             while (newLevel < level && (parent != popup)) {
466                 parent = stack.pop();
467                 level--;
468             }
469             if ((*it).isPhrase()) {
470                 Phrase phrase = (*it).getPhrase();
471                 QAction *action = new PhraseAction(phrase.getPhrase(),
472                                                    phrase.getShortcut(), receiver, slot, phrases);
473                 if (parent == popup)
474                     toolbar->addAction(action);
475                 if (parent != nullptr)
476                     parent->addAction(action);
477             } else {
478                 Phrase phrase = (*it).getPhrase();
479                 KActionMenu *menu = phrases->add<KActionMenu>(QStringLiteral("phrasebook"));
480                 menu->setText(phrase.getPhrase());
481                 menu->setPopupMode(QToolButton::InstantPopup);
482                 if (parent == popup)
483                     toolbar->addAction(menu);
484                 parent->addAction(menu);
485                 stack.push(parent);
486                 parent = menu->menu();
487                 level++;
488             }
489         }
490     }
491 }
492 
insert(const QString & name,const PhraseBook & book)493 void PhraseBook::insert(const QString &name, const PhraseBook &book)
494 {
495     *this += PhraseBookEntry(Phrase(name), 0, false);
496 
497     QList<PhraseBookEntry>::ConstIterator it;
498     for (it = book.begin(); it != book.end(); ++it) {
499         *this += PhraseBookEntry((*it).getPhrase(), (*it).getLevel() + 1, (*it).isPhrase());
500     }
501 }
502 
503