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