1 /***************************************************************************
2  *   Copyright (C) 1999-2006 by Éric Bischoff <ebischoff@nerim.net>        *
3  *   Copyright (C) 2007 by Albert Astals Cid <aacid@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 
11 /* Top level window */
12 
13 #include "toplevel.h"
14 
15 #include <KMessageBox>
16 #include <KLocalizedString>
17 #include <KLanguageName>
18 #include <KStandardAction>
19 #include <KStandardShortcut>
20 #include <KStandardGameAction>
21 #include <KActionCollection>
22 #include <KToggleAction>
23 #include <KToggleFullScreenAction>
24 #include <KConfigGroup>
25 #include <KSharedConfig>
26 #include <kio/job.h>
27 
28 #include <QApplication>
29 #include <QClipboard>
30 #include <QFileDialog>
31 #include <QFileInfo>
32 #include <QImageWriter>
33 #include <QMimeDatabase>
34 #include <QPrintDialog>
35 #include <QPrinter>
36 #include <QTemporaryFile>
37 #include <QWidgetAction>
38 
39 #include "filefactory.h"
40 #include "playgrounddelegate.h"
41 
42 
43 static const char *DEFAULT_THEME = "default_theme.theme";
44 
45 // Constructor
TopLevel()46 TopLevel::TopLevel()
47   : KXmlGuiWindow(0)
48 {
49   QString board, language;
50 
51   playGround = new PlayGround(this, this);
52   playGround->setObjectName( QStringLiteral( "playGround" ) );
53 
54   soundFactory = new SoundFactory(this);
55 
56   setCentralWidget(playGround);
57 
58   playgroundsGroup = new QActionGroup(this);
59   playgroundsGroup->setExclusive(true);
60 
61   languagesGroup = new QActionGroup(this);
62   languagesGroup->setExclusive(true);
63 
64   setupKAction();
65 
66   playGround->registerPlayGrounds();
67   soundFactory->registerLanguages();
68 
69   readOptions(board, language);
70   changeGameboard(board);
71   changeLanguage(language);
72 }
73 
74 // Destructor
~TopLevel()75 TopLevel::~TopLevel()
76 {
77   delete soundFactory;
78 }
79 
actionSorterByName(QAction * a,QAction * b)80 static bool actionSorterByName(QAction *a, QAction *b)
81 {
82   return a->text().localeAwareCompare(b->text()) < 0;
83 }
84 
85 // Register an available gameboard
registerGameboard(const QString & menuText,const QString & board,const QPixmap & pixmap)86 void TopLevel::registerGameboard(const QString &menuText, const QString &board, const QPixmap &pixmap)
87 {
88   KToggleAction *t = new KToggleAction(menuText, this);
89   actionCollection()->addAction(board, t);
90   connect(t, &KToggleAction::toggled, this, [this, t, board] {
91     if (t->isChecked())
92     {
93       changeGameboard(board);
94     }
95   });
96   playgroundsGroup->addAction(t);
97   QList<QAction*> actionList = playgroundsGroup->actions();
98   std::sort(actionList.begin(), actionList.end(), actionSorterByName);
99   unplugActionList( QStringLiteral( "playgroundList" ) );
100   plugActionList( QStringLiteral( "playgroundList" ), actionList );
101 
102   playgroundCombo->addItem(menuText,QVariant(pixmap));
103   playgroundCombo->setItemData(playgroundCombo->count()-1,QVariant(board),BOARD_THEME);
104 }
105 
106 // Register an available language
registerLanguage(const QString & code,const QString & soundFile,bool enabled)107 void TopLevel::registerLanguage(const QString &code, const QString &soundFile, bool enabled)
108 {
109   KToggleAction *t = new KToggleAction(KLanguageName::nameForCode(code), this);
110   t->setEnabled(enabled);
111   actionCollection()->addAction(soundFile, t);
112   sounds.insert(code, soundFile);
113   connect(t, &KToggleAction::toggled, this, [this, soundFile] {
114     changeLanguage(soundFile);
115   });
116   languagesGroup->addAction(t);
117   QList<QAction*> actionList = languagesGroup->actions();
118   actionList.removeAll(actionCollection()->action(QStringLiteral( "speech_no_sound" )));
119   std::sort(actionList.begin(), actionList.end(), actionSorterByName);
120   unplugActionList( QStringLiteral( "languagesList" ) );
121   plugActionList( QStringLiteral( "languagesList" ), actionList );
122 }
123 
124 // Switch to another gameboard
changeGameboardFromCombo(int index)125 void TopLevel::changeGameboardFromCombo(int index)
126 {
127   QString newBoard = playgroundCombo->itemData(index,BOARD_THEME).toString();
128   changeGameboard(newBoard);
129 }
130 
changeGameboard(const QString & newGameBoard)131 void TopLevel::changeGameboard(const QString &newGameBoard)
132 {
133   if (newGameBoard == playGround->currentGameboard()) return;
134 
135   QString fileToLoad;
136   QFileInfo fi(newGameBoard);
137   if (fi.isRelative())
138   {
139     fileToLoad = FileFactory::locate(QLatin1String( "pics/" ) + newGameBoard);
140   }
141   else
142   {
143     fileToLoad = newGameBoard;
144   }
145 
146   int index = playgroundCombo->findData(fileToLoad, BOARD_THEME);
147   playgroundCombo->setCurrentIndex(index);
148   QAction *action = actionCollection()->action(fileToLoad);
149   if (action && playGround->loadPlayGround(fileToLoad))
150   {
151     action->setChecked(true);
152 
153     // Change gameboard in the remembered options
154     writeOptions();
155   }
156   else
157   {
158     // Something bad just happened, try the default playground
159     if (newGameBoard != QLatin1String(DEFAULT_THEME))
160     {
161       changeGameboard(QLatin1String(DEFAULT_THEME));
162     }
163     else
164     {
165       KMessageBox::error(this, i18n("Error while loading the playground."));
166     }
167   }
168 }
169 
170 // Switch to another language
changeLanguage(const QString & soundFile)171 void TopLevel::changeLanguage(const QString &soundFile)
172 {
173   if (soundFile == soundFactory->currentSoundFile()) return;
174 
175   QString fileToLoad;
176   QFileInfo fi(soundFile);
177   if (fi.isRelative())
178   {
179     fileToLoad = FileFactory::locate(QLatin1String( "sounds/" ) + soundFile);
180   }
181   else
182   {
183     fileToLoad = soundFile;
184   }
185 
186   // Change language effectively
187   QAction *action = actionCollection()->action(fileToLoad);
188   if (action && soundFactory->loadLanguage(fileToLoad))
189   {
190     action->setChecked(true);
191 
192     // Change language in the remembered options
193     writeOptions();
194   }
195   else
196   {
197     KMessageBox::error(this, i18n("Error while loading the sound file."));
198     soundOff();
199   }
200 }
201 
202 // Play a sound
playSound(const QString & ref)203 void TopLevel::playSound(const QString &ref)
204 {
205   soundFactory->playSound(ref);
206 }
207 
208 // Read options from preferences file
readOptions(QString & board,QString & language)209 void TopLevel::readOptions(QString &board, QString &language)
210 {
211   KConfigGroup config(KSharedConfig::openConfig(), "General");
212 
213   QString option = config.readEntry("Sound",  "on" );
214   bool soundEnabled = option.indexOf(QLatin1String( "on" )) == 0;
215   board = config.readEntry("Gameboard", DEFAULT_THEME);
216   language = config.readEntry("Language", "" );
217   bool keepAspectRatio = config.readEntry("KeepAspectRatio", false);
218 
219   if (soundEnabled && language.isEmpty())
220   {
221     const QStringList &systemLanguages = KLocalizedString::languages();
222     for (const QString &systemLanguage : systemLanguages)
223     {
224       QString sound = sounds.value(systemLanguage);
225       if (!sound.isEmpty())
226       {
227         language = sound;
228         break;
229       }
230     }
231     if (language.isEmpty())
232     {
233       language = QStringLiteral("en.soundtheme");
234     }
235   }
236 
237   if (!soundEnabled)
238   {
239     soundOff();
240     language = QString();
241   }
242 
243   lockAspectRatio(keepAspectRatio);
244 }
245 
246 // Write options to preferences file
writeOptions()247 void TopLevel::writeOptions()
248 {
249   KConfigGroup config(KSharedConfig::openConfig(), "General");
250   config.writeEntry("Sound", actionCollection()->action(QStringLiteral( "speech_no_sound" ))->isChecked() ? "off": "on");
251 
252   config.writeEntry("Gameboard", playGround->currentGameboard());
253 
254   config.writeEntry("Language", soundFactory->currentSoundFile());
255 
256   config.writeEntry("KeepAspectRatio", playGround->isAspectRatioLocked());
257 }
258 
259 // KAction initialization (aka menubar + toolbar init)
setupKAction()260 void TopLevel::setupKAction()
261 {
262   QAction *action;
263 
264   //Game
265   KStandardGameAction::gameNew(this, SLOT(fileNew()), actionCollection());
266   KStandardGameAction::load(this, SLOT(fileOpen()), actionCollection());
267   KStandardGameAction::save(this, SLOT(fileSave()), actionCollection());
268   KStandardGameAction::print(this, SLOT(filePrint()), actionCollection());
269   KStandardGameAction::quit(qApp, SLOT(quit()), actionCollection());
270 
271   action = actionCollection()->addAction( QStringLiteral( "game_save_picture" ));
272   action->setText(i18n("Save &as Picture..."));
273   connect(action, &QAction::triggered, this, &TopLevel::filePicture);
274 
275   //Edit
276   action = KStandardAction::copy(this, &TopLevel::editCopy, actionCollection());
277   actionCollection()->addAction(action->objectName(), action);
278 
279   action = KStandardAction::undo(0, 0, actionCollection());
280   playGround->connectUndoAction(action);
281   action = KStandardAction::redo(0, 0, actionCollection());
282   playGround->connectRedoAction(action);
283 
284   //Speech
285   KToggleAction *t = new KToggleAction(i18n("&No Sound"), this);
286   actionCollection()->addAction( QStringLiteral( "speech_no_sound" ), t);
287   connect(t, &QAction::triggered, this, &TopLevel::soundOff);
288   languagesGroup->addAction(t);
289 
290   KStandardAction::fullScreen(this, &TopLevel::toggleFullScreen, this, actionCollection());
291 
292   t = new KToggleAction(i18n("&Lock Aspect Ratio"), this);
293   actionCollection()->addAction( QStringLiteral( "lock_aspect_ratio" ), t);
294   connect(t, &QAction::triggered, this, &TopLevel::lockAspectRatio);
295 
296   playgroundCombo = new KComboBox(this);
297   playgroundCombo->setMinimumWidth(200);
298   playgroundCombo->view()->setMinimumHeight(100);
299   playgroundCombo->view()->setMinimumWidth(200);
300   playgroundCombo->view()->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
301 
302   PlaygroundDelegate *playgroundDelegate = new PlaygroundDelegate(playgroundCombo->view());
303   playgroundCombo->setItemDelegate(playgroundDelegate);
304   connect(playgroundCombo, QOverload<int>::of(&KComboBox::currentIndexChanged), this, &TopLevel::changeGameboardFromCombo);
305   QWidgetAction *widgetAction = new QWidgetAction(this);
306   widgetAction->setDefaultWidget(playgroundCombo);
307   actionCollection()->addAction( QStringLiteral( "playgroundSelection" ),widgetAction);
308 
309   setupGUI(ToolBar | Keys | Save | Create);
310 }
311 
saveNewToolbarConfig()312 void TopLevel::saveNewToolbarConfig()
313 {
314   // this destroys our actions lists ...
315   KXmlGuiWindow::saveNewToolbarConfig();
316   // ... so plug them again
317   plugActionList( QStringLiteral( "playgroundList" ), playgroundsGroup->actions() );
318   plugActionList( QStringLiteral( "languagesList" ), languagesGroup->actions() );
319 }
320 
321 // Reset gameboard
fileNew()322 void TopLevel::fileNew()
323 {
324   playGround->reset();
325 }
326 
327 // Load gameboard
fileOpen()328 void TopLevel::fileOpen()
329 {
330   QUrl url = QFileDialog::getOpenFileUrl(this, i18nc("@title:window", "Load file"), QUrl(),
331                                      i18n("KTuberling files (%1)", QStringLiteral("*.tuberling")));
332 
333   open(url);
334 }
335 
open(const QUrl & url)336 void TopLevel::open(const QUrl &url)
337 {
338   if (url.isEmpty())
339     return;
340 
341   QString name;
342   if (url.isLocalFile()) {
343     // file protocol. We do not need the network
344     name = url.toLocalFile();
345   } else {
346     QTemporaryFile tmpFile;
347     tmpFile.setAutoRemove(false);
348     tmpFile.open();
349     name = tmpFile.fileName();
350     const QUrl dest = QUrl::fromLocalFile(name);
351     KIO::Job *job = KIO::file_copy(url, dest, -1, KIO::Overwrite | KIO::HideProgressInfo);
352     QEventLoop eventLoop;
353     connect(job, &KIO::Job::result, &eventLoop, &QEventLoop::quit);
354     eventLoop.exec(QEventLoop::ExcludeUserInputEvents);
355   }
356 
357   switch(playGround->loadFrom(name))
358   {
359     case PlayGround::NoError:
360      // good
361     break;
362 
363     case PlayGround::OldFileVersionError:
364       KMessageBox::error(this, i18n("The saved file is from an old version of KTuberling and unfortunately cannot be opened with this version."));
365     break;
366 
367     case PlayGround::OtherError:
368       KMessageBox::error(this, i18n("Could not load file."));
369     break;
370   }
371 
372   if (!url.isLocalFile()) {
373     QFile::remove(name);
374   }
375 }
376 
extractSuffixesFromQtPattern(const QString & qtPattern)377 static QStringList extractSuffixesFromQtPattern(const QString &qtPattern)
378 {
379   static const QRegularExpression regexp(".*\\((.*)\\)");
380   const QRegularExpressionMatch match = regexp.match(qtPattern);
381   if (match.hasMatch()) {
382     QStringList suffixes = match.captured(1).split(" ");
383     if (!suffixes.isEmpty()) {
384       for (QString &suffix : suffixes) {
385         suffix = suffix.mid(1); // Remove the * from the start, we want the actual suffix
386       }
387       return suffixes;
388     }
389     qWarning() << "extractSuffixesFromQtPattern suffix split failed" << qtPattern;
390   } else {
391     qWarning() << "extractSuffixesFromQtPattern regexp match failed" << qtPattern;
392   }
393   return { ".report_bug_please" };
394 }
395 
getSaveFileUrl(QWidget * w,const QString & patterns)396 static QUrl getSaveFileUrl(QWidget *w, const QString &patterns)
397 {
398   QString selectedPattern;
399   QUrl url = QFileDialog::getSaveFileUrl( w, QString(), QUrl(), patterns, &selectedPattern );
400 
401   if( url.isEmpty() )
402     return {};
403 
404   // make sure the url ends in one of the extensions of selectedPattern
405   const QStringList selectedSuffixes = extractSuffixesFromQtPattern(selectedPattern);
406   bool validSuffix = false;
407   for (const QString &suffix : selectedSuffixes) {
408     if (url.path().endsWith(suffix)) {
409       validSuffix = true;
410       break;
411     }
412   }
413   // and if it does not add it
414   if (!validSuffix) {
415     url.setPath(url.path() + selectedSuffixes[0]);
416   }
417 
418   return url;
419 }
420 
421 // Save gameboard
fileSave()422 void TopLevel::fileSave()
423 {
424   const QUrl url = getSaveFileUrl( this, i18n("KTuberling files (%1)", QStringLiteral("*.tuberling")) );
425 
426   if (url.isEmpty())
427     return;
428 
429   QTemporaryFile tempFile; // for network saving
430   QString name;
431   if( !url.isLocalFile() )
432   {
433     if (tempFile.open()) name = tempFile.fileName();
434     else
435     {
436       KMessageBox::error(this, i18n("Could not save file."));
437       return;
438     }
439   }
440   else
441   {
442     name = url.toLocalFile();
443   }
444 
445   if( !playGround->saveAs( name ) )
446   {
447     KMessageBox::error(this, i18n("Could not save file."));
448     return;
449   }
450 
451   if( !url.isLocalFile() )
452   {
453     if (!upload(name, url))
454       KMessageBox::error(this, i18n("Could not save file."));
455   }
456 }
457 
458 // Save gameboard as picture
filePicture()459 void TopLevel::filePicture()
460 {
461   const QMimeDatabase mimedb;
462   const QList<QByteArray> imageWriterMimetypes = QImageWriter::supportedMimeTypes();
463   QStringList patterns;
464   for(const auto &mimeName : imageWriterMimetypes)
465   {
466     const QMimeType mime = mimedb.mimeTypeForName(mimeName);
467     if (mime.isValid())
468     {
469       QStringList suffixes;
470       for(const QString &suffix : mime.suffixes())
471       {
472           suffixes << QStringLiteral("*.%1").arg(suffix);
473       }
474 
475       // Favor png
476       const QString pattern = i18nc("%1 is mimetype and %2 is the file extensions", "%1 (%2)", mime.comment(), suffixes.join(' '));
477       if (mimeName == "image/png")
478       {
479         patterns.prepend(pattern);
480       }
481       else
482       {
483         patterns << pattern;
484       }
485     }
486   }
487   const QUrl url = getSaveFileUrl( this, patterns.join(QStringLiteral(";;")) );
488 
489   if( url.isEmpty() )
490     return;
491 
492   QTemporaryFile tempFile; // for network saving
493   QString name;
494   if( !url.isLocalFile() )
495   {
496     tempFile.open();
497     name = tempFile.fileName();
498   }
499   else
500   {
501     name = url.toLocalFile();
502   }
503 
504   QPixmap picture(playGround->getPicture());
505 
506   if (!picture.save(name))
507   {
508     KMessageBox::error
509       (this, i18n("Could not save file."));
510     return;
511   }
512 
513   if( !url.isLocalFile() )
514   {
515     if (!upload(name, url))
516       KMessageBox::error(this, i18n("Could not save file."));
517   }
518 
519 }
520 
521 // Save gameboard as picture
filePrint()522 void TopLevel::filePrint()
523 {
524   QPrinter printer;
525   bool ok;
526 
527   QPrintDialog *printDialog = new QPrintDialog(&printer, this);
528   printDialog->setWindowTitle(i18nc("@title:window", "Print %1", actionCollection()->action(playGround->currentGameboard())->iconText()));
529   ok = printDialog->exec();
530   delete printDialog;
531   if (!ok) return;
532   playGround->repaint();
533   if (!playGround->printPicture(printer))
534     KMessageBox::error(this,
535                          i18n("Could not print picture."));
536   else
537     KMessageBox::information(this,
538                              i18n("Picture successfully printed."));
539 }
540 
541 // Copy modified area to clipboard
editCopy()542 void TopLevel::editCopy()
543 {
544   QClipboard *clipboard = QApplication::clipboard();
545   QPixmap picture(playGround->getPicture());
546 
547   clipboard->setPixmap(picture);
548 }
549 
550 // Toggle sound off
soundOff()551 void TopLevel::soundOff()
552 {
553   actionCollection()->action(QStringLiteral( "speech_no_sound" ))->setChecked(true);
554   writeOptions();
555 }
556 
isSoundEnabled() const557 bool TopLevel::isSoundEnabled() const
558 {
559   return !actionCollection()->action(QStringLiteral( "speech_no_sound" ))->isChecked();
560 }
561 
toggleFullScreen()562 void TopLevel::toggleFullScreen()
563 {
564   KToggleFullScreenAction::setFullScreen( this, actionCollection()->action(QStringLiteral( "fullscreen" ))->isChecked());
565 }
566 
lockAspectRatio(bool lock)567 void TopLevel::lockAspectRatio(bool lock)
568 {
569   actionCollection()->action(QStringLiteral( "lock_aspect_ratio" ))->setChecked(lock);
570   playGround->lockAspectRatio(lock);
571   writeOptions();
572 }
573 
upload(const QString & src,const QUrl & target)574 bool TopLevel::upload(const QString &src, const QUrl &target)
575 {
576   bool success = true;
577   const QUrl srcUrl = QUrl::fromLocalFile(src);
578   KIO::Job *job = KIO::file_copy(srcUrl, target, -1, KIO::Overwrite | KIO::HideProgressInfo);
579   QEventLoop eventLoop;
580   connect(job, &KIO::Job::result, this, [job, &eventLoop, &success] { success = !job->error(); eventLoop.quit(); } );
581   eventLoop.exec(QEventLoop::ExcludeUserInputEvents);
582   return success;
583 }
584