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