File indexing completed on 2024-04-21 04:05:07

0001 /*
0002     SPDX-FileCopyrightText: 1999-2006 Éric Bischoff <ebischoff@nerim.net>
0003     SPDX-FileCopyrightText: 2007 Albert Astals Cid <aacid@kde.org>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 /* Top level window */
0009 
0010 #include "toplevel.h"
0011 
0012 #include <KMessageBox>
0013 #include <KLocalizedString>
0014 #include <KLanguageName>
0015 #include <KStandardAction>
0016 #include <KStandardShortcut>
0017 #include <KGameStandardAction>
0018 #include <KActionCollection>
0019 #include <KToggleAction>
0020 #include <KToggleFullScreenAction>
0021 #include <KConfigGroup>
0022 #include <KSharedConfig>
0023 #include <KIO/FileCopyJob>
0024 
0025 #include <QActionGroup>
0026 #include <QApplication>
0027 #include <QClipboard>
0028 #include <QFileDialog>
0029 #include <QFileInfo>
0030 #include <QImageWriter>
0031 #include <QMimeDatabase>
0032 #include <QPrintDialog>
0033 #include <QPrinter>
0034 #include <QTemporaryFile>
0035 #include <QWidgetAction>
0036 
0037 #include "filefactory.h"
0038 #include "playgrounddelegate.h"
0039 
0040 
0041 static const char *DEFAULT_THEME = "default_theme.theme";
0042 
0043 // Constructor
0044 TopLevel::TopLevel()
0045   : KXmlGuiWindow(nullptr)
0046 {
0047   QString board, language;
0048 
0049   playGround = new PlayGround(this, this);
0050   playGround->setObjectName( QStringLiteral( "playGround" ) );
0051 
0052   soundFactory = new SoundFactory(this);
0053 
0054   setCentralWidget(playGround);
0055 
0056   playgroundsGroup = new QActionGroup(this);
0057   playgroundsGroup->setExclusive(true);
0058 
0059   languagesGroup = new QActionGroup(this);
0060   languagesGroup->setExclusive(true);
0061 
0062   setupKAction();
0063 
0064   playGround->registerPlayGrounds();
0065   soundFactory->registerLanguages();
0066 
0067   readOptions(board, language);
0068   changeGameboard(board);
0069   changeLanguage(language);
0070 }
0071 
0072 // Destructor
0073 TopLevel::~TopLevel()
0074 {
0075   delete soundFactory;
0076 }
0077 
0078 static bool actionSorterByName(QAction *a, QAction *b)
0079 {
0080   return a->text().localeAwareCompare(b->text()) < 0;
0081 }
0082 
0083 // Register an available gameboard
0084 void TopLevel::registerGameboard(const QString &menuText, const QString &board, const QPixmap &pixmap)
0085 {
0086   KToggleAction *t = new KToggleAction(menuText, this);
0087   actionCollection()->addAction(board, t);
0088   connect(t, &KToggleAction::toggled, this, [this, t, board] {
0089     if (t->isChecked())
0090     {
0091       changeGameboard(board);
0092     }
0093   });
0094   playgroundsGroup->addAction(t);
0095   QList<QAction*> actionList = playgroundsGroup->actions();
0096   std::sort(actionList.begin(), actionList.end(), actionSorterByName);
0097   unplugActionList( QStringLiteral( "playgroundList" ) );
0098   plugActionList( QStringLiteral( "playgroundList" ), actionList );
0099 
0100   playgroundCombo->addItem(menuText,QVariant(pixmap));
0101   playgroundCombo->setItemData(playgroundCombo->count()-1,QVariant(board),BOARD_THEME);
0102 }
0103 
0104 // Register an available language
0105 void TopLevel::registerLanguage(const QString &code, const QString &soundFile, bool enabled)
0106 {
0107   KToggleAction *t = new KToggleAction(KLanguageName::nameForCode(code), this);
0108   t->setEnabled(enabled);
0109   actionCollection()->addAction(soundFile, t);
0110   sounds.insert(code, soundFile);
0111   connect(t, &KToggleAction::toggled, this, [this, soundFile] {
0112     changeLanguage(soundFile);
0113   });
0114   languagesGroup->addAction(t);
0115   QList<QAction*> actionList = languagesGroup->actions();
0116   actionList.removeAll(actionCollection()->action(QStringLiteral( "speech_no_sound" )));
0117   std::sort(actionList.begin(), actionList.end(), actionSorterByName);
0118   unplugActionList( QStringLiteral( "languagesList" ) );
0119   plugActionList( QStringLiteral( "languagesList" ), actionList );
0120 }
0121 
0122 // Switch to another gameboard
0123 void TopLevel::changeGameboardFromCombo(int index)
0124 {
0125   QString newBoard = playgroundCombo->itemData(index,BOARD_THEME).toString();
0126   changeGameboard(newBoard);
0127 }
0128 
0129 void TopLevel::changeGameboard(const QString &newGameBoard)
0130 {
0131   if (newGameBoard == playGround->currentGameboard()) return;
0132 
0133   QString fileToLoad;
0134   QFileInfo fi(newGameBoard);
0135   if (fi.isRelative())
0136   {
0137     fileToLoad = FileFactory::locate(QLatin1String( "pics/" ) + newGameBoard);
0138   }
0139   else
0140   {
0141     fileToLoad = newGameBoard;
0142   }
0143 
0144   int index = playgroundCombo->findData(fileToLoad, BOARD_THEME);
0145   playgroundCombo->setCurrentIndex(index);
0146   QAction *action = actionCollection()->action(fileToLoad);
0147   if (action && playGround->loadPlayGround(fileToLoad))
0148   {
0149     action->setChecked(true);
0150 
0151     // Change gameboard in the remembered options
0152     writeOptions();
0153   }
0154   else
0155   {
0156     // Something bad just happened, try the default playground
0157     if (newGameBoard != QLatin1String(DEFAULT_THEME))
0158     {
0159       changeGameboard(QLatin1String(DEFAULT_THEME));
0160     }
0161     else
0162     {
0163       KMessageBox::error(this, i18n("Error while loading the playground."));
0164     }
0165   }
0166 }
0167 
0168 // Switch to another language
0169 void TopLevel::changeLanguage(const QString &soundFile)
0170 {
0171   if (soundFile == soundFactory->currentSoundFile()) return;
0172 
0173   QString fileToLoad;
0174   QFileInfo fi(soundFile);
0175   if (fi.isRelative())
0176   {
0177     fileToLoad = FileFactory::locate(QLatin1String( "sounds/" ) + soundFile);
0178   }
0179   else
0180   {
0181     fileToLoad = soundFile;
0182   }
0183 
0184   // Change language effectively
0185   QAction *action = actionCollection()->action(fileToLoad);
0186   if (action && soundFactory->loadLanguage(fileToLoad))
0187   {
0188     action->setChecked(true);
0189 
0190     // Change language in the remembered options
0191     writeOptions();
0192   }
0193   else
0194   {
0195     KMessageBox::error(this, i18n("Error while loading the sound file."));
0196     soundOff();
0197   }
0198 }
0199 
0200 // Play a sound
0201 void TopLevel::playSound(const QString &ref)
0202 {
0203   soundFactory->playSound(ref);
0204 }
0205 
0206 // Read options from preferences file
0207 void TopLevel::readOptions(QString &board, QString &language)
0208 {
0209   KConfigGroup config(KSharedConfig::openConfig(), QStringLiteral("General"));
0210 
0211   QString option = config.readEntry("Sound",  "on" );
0212   bool soundEnabled = option.indexOf(QLatin1String( "on" )) == 0;
0213   board = config.readEntry("Gameboard", DEFAULT_THEME);
0214   language = config.readEntry("Language", "" );
0215   bool keepAspectRatio = config.readEntry("KeepAspectRatio", false);
0216 
0217   if (soundEnabled && language.isEmpty())
0218   {
0219     const QStringList &systemLanguages = KLocalizedString::languages();
0220     for (const QString &systemLanguage : systemLanguages)
0221     {
0222       QString sound = sounds.value(systemLanguage);
0223       if (!sound.isEmpty())
0224       {
0225         language = sound;
0226         break;
0227       }
0228     }
0229     if (language.isEmpty())
0230     {
0231       language = QStringLiteral("en.soundtheme");
0232     }
0233   }
0234 
0235   if (!soundEnabled)
0236   {
0237     soundOff();
0238     language = QString();
0239   }
0240 
0241   lockAspectRatio(keepAspectRatio);
0242 }
0243 
0244 // Write options to preferences file
0245 void TopLevel::writeOptions()
0246 {
0247   KConfigGroup config(KSharedConfig::openConfig(), QStringLiteral("General"));
0248   config.writeEntry("Sound", actionCollection()->action(QStringLiteral( "speech_no_sound" ))->isChecked() ? "off": "on");
0249 
0250   config.writeEntry("Gameboard", playGround->currentGameboard());
0251 
0252   config.writeEntry("Language", soundFactory->currentSoundFile());
0253 
0254   config.writeEntry("KeepAspectRatio", playGround->isAspectRatioLocked());
0255 }
0256 
0257 // KAction initialization (aka menubar + toolbar init)
0258 void TopLevel::setupKAction()
0259 {
0260   QAction *action;
0261 
0262   //Game
0263   KGameStandardAction::gameNew(this, &TopLevel::fileNew, actionCollection());
0264   KGameStandardAction::load(this, &TopLevel::fileOpen, actionCollection());
0265   KGameStandardAction::save(this, &TopLevel::fileSave, actionCollection());
0266   KGameStandardAction::print(this, &TopLevel::filePrint, actionCollection());
0267   KGameStandardAction::quit(qApp, &QApplication::quit, actionCollection());
0268 
0269   action = actionCollection()->addAction( QStringLiteral( "game_save_picture" ));
0270   action->setText(i18n("Save &as Picture..."));
0271   connect(action, &QAction::triggered, this, &TopLevel::filePicture);
0272 
0273   //Edit
0274   action = KStandardAction::copy(this, &TopLevel::editCopy, actionCollection());
0275   actionCollection()->addAction(action->objectName(), action);
0276 
0277   action = KStandardAction::undo(nullptr, nullptr, actionCollection());
0278   playGround->connectUndoAction(action);
0279   action = KStandardAction::redo(nullptr, nullptr, actionCollection());
0280   playGround->connectRedoAction(action);
0281 
0282   //Speech
0283   KToggleAction *t = new KToggleAction(i18n("&No Sound"), this);
0284   actionCollection()->addAction( QStringLiteral( "speech_no_sound" ), t);
0285   connect(t, &QAction::triggered, this, &TopLevel::soundOff);
0286   languagesGroup->addAction(t);
0287 
0288   KStandardAction::fullScreen(this, &TopLevel::toggleFullScreen, this, actionCollection());
0289 
0290   t = new KToggleAction(i18n("&Lock Aspect Ratio"), this);
0291   actionCollection()->addAction( QStringLiteral( "lock_aspect_ratio" ), t);
0292   connect(t, &QAction::triggered, this, &TopLevel::lockAspectRatio);
0293 
0294   playgroundCombo = new KComboBox(this);
0295   playgroundCombo->setMinimumWidth(200);
0296   playgroundCombo->view()->setMinimumHeight(100);
0297   playgroundCombo->view()->setMinimumWidth(200);
0298   playgroundCombo->view()->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
0299 
0300   PlaygroundDelegate *playgroundDelegate = new PlaygroundDelegate(playgroundCombo->view());
0301   playgroundCombo->setItemDelegate(playgroundDelegate);
0302   connect(playgroundCombo, &KComboBox::currentIndexChanged, this, &TopLevel::changeGameboardFromCombo);
0303   QWidgetAction *widgetAction = new QWidgetAction(this);
0304   widgetAction->setDefaultWidget(playgroundCombo);
0305   actionCollection()->addAction( QStringLiteral( "playgroundSelection" ),widgetAction);
0306 
0307   setupGUI(ToolBar | Keys | Save | Create);
0308 }
0309 
0310 void TopLevel::saveNewToolbarConfig()
0311 {
0312   // this destroys our actions lists ...
0313   KXmlGuiWindow::saveNewToolbarConfig();
0314   // ... so plug them again
0315   plugActionList( QStringLiteral( "playgroundList" ), playgroundsGroup->actions() );
0316   plugActionList( QStringLiteral( "languagesList" ), languagesGroup->actions() );
0317 }
0318 
0319 // Reset gameboard
0320 void TopLevel::fileNew()
0321 {
0322   playGround->reset();
0323 }
0324 
0325 // Load gameboard
0326 void TopLevel::fileOpen()
0327 {
0328   QUrl url = QFileDialog::getOpenFileUrl(this, i18nc("@title:window", "Load file"), QUrl(),
0329                                      i18n("KTuberling files (%1)", QStringLiteral("*.tuberling")));
0330 
0331   open(url);
0332 }
0333 
0334 void TopLevel::open(const QUrl &url)
0335 {
0336   if (url.isEmpty())
0337     return;
0338 
0339   QString name;
0340   if (url.isLocalFile()) {
0341     // file protocol. We do not need the network
0342     name = url.toLocalFile();
0343   } else {
0344     QTemporaryFile tmpFile;
0345     tmpFile.setAutoRemove(false);
0346     tmpFile.open();
0347     name = tmpFile.fileName();
0348     const QUrl dest = QUrl::fromLocalFile(name);
0349     KIO::Job *job = KIO::file_copy(url, dest, -1, KIO::Overwrite | KIO::HideProgressInfo);
0350     QEventLoop eventLoop;
0351     connect(job, &KIO::Job::result, &eventLoop, &QEventLoop::quit);
0352     eventLoop.exec(QEventLoop::ExcludeUserInputEvents);
0353   }
0354 
0355   switch(playGround->loadFrom(name))
0356   {
0357     case PlayGround::NoError:
0358      // good
0359     break;
0360 
0361     case PlayGround::OldFileVersionError:
0362       KMessageBox::error(this, i18n("The saved file is from an old version of KTuberling and unfortunately cannot be opened with this version."));
0363     break;
0364 
0365     case PlayGround::OtherError:
0366       KMessageBox::error(this, i18n("Could not load file."));
0367     break;
0368   }
0369 
0370   if (!url.isLocalFile()) {
0371     QFile::remove(name);
0372   }
0373 }
0374 
0375 static QStringList extractSuffixesFromQtPattern(const QString &qtPattern)
0376 {
0377   static const QRegularExpression regexp(QStringLiteral(".*\\((.*)\\)"));
0378   const QRegularExpressionMatch match = regexp.match(qtPattern);
0379   if (match.hasMatch()) {
0380     QStringList suffixes = match.captured(1).split(QLatin1Char(' '));
0381     if (!suffixes.isEmpty()) {
0382       for (QString &suffix : suffixes) {
0383         suffix = suffix.mid(1); // Remove the * from the start, we want the actual suffix
0384       }
0385       return suffixes;
0386     }
0387     qWarning() << "extractSuffixesFromQtPattern suffix split failed" << qtPattern;
0388   } else {
0389     qWarning() << "extractSuffixesFromQtPattern regexp match failed" << qtPattern;
0390   }
0391   return { QStringLiteral(".report_bug_please") };
0392 }
0393 
0394 static QUrl getSaveFileUrl(QWidget *w, const QString &patterns)
0395 {
0396   QString selectedPattern;
0397   QUrl url = QFileDialog::getSaveFileUrl( w, QString(), QUrl(), patterns, &selectedPattern );
0398 
0399   if( url.isEmpty() )
0400     return {};
0401 
0402   // make sure the url ends in one of the extensions of selectedPattern
0403   const QStringList selectedSuffixes = extractSuffixesFromQtPattern(selectedPattern);
0404   bool validSuffix = false;
0405   for (const QString &suffix : selectedSuffixes) {
0406     if (url.path().endsWith(suffix)) {
0407       validSuffix = true;
0408       break;
0409     }
0410   }
0411   // and if it does not add it
0412   if (!validSuffix) {
0413     url.setPath(url.path() + selectedSuffixes[0]);
0414   }
0415 
0416   return url;
0417 }
0418 
0419 // Save gameboard
0420 void TopLevel::fileSave()
0421 {
0422   const QUrl url = getSaveFileUrl( this, i18n("KTuberling files (%1)", QStringLiteral("*.tuberling")) );
0423 
0424   if (url.isEmpty())
0425     return;
0426 
0427   QTemporaryFile tempFile; // for network saving
0428   QString name;
0429   if( !url.isLocalFile() )
0430   {
0431     if (tempFile.open()) name = tempFile.fileName();
0432     else
0433     {
0434       KMessageBox::error(this, i18n("Could not save file."));
0435       return;
0436     }
0437   }
0438   else
0439   {
0440     name = url.toLocalFile();
0441   }
0442 
0443   if( !playGround->saveAs( name ) )
0444   {
0445     KMessageBox::error(this, i18n("Could not save file."));
0446     return;
0447   }
0448 
0449   if( !url.isLocalFile() )
0450   {
0451     if (!upload(name, url))
0452       KMessageBox::error(this, i18n("Could not save file."));
0453   }
0454 }
0455 
0456 // Save gameboard as picture
0457 void TopLevel::filePicture()
0458 {
0459   const QMimeDatabase mimedb;
0460   const QList<QByteArray> imageWriterMimetypes = QImageWriter::supportedMimeTypes();
0461   QStringList patterns;
0462   for(const auto &mimeName : imageWriterMimetypes)
0463   {
0464     const QMimeType mime = mimedb.mimeTypeForName(QString::fromLatin1(mimeName));
0465     if (mime.isValid())
0466     {
0467       QStringList suffixes;
0468       for(const QString &suffix : mime.suffixes())
0469       {
0470           suffixes << QStringLiteral("*.%1").arg(suffix);
0471       }
0472 
0473       // Favor png
0474       const QString pattern = i18nc("%1 is mimetype and %2 is the file extensions", "%1 (%2)", mime.comment(), suffixes.join(QLatin1Char(' ')));
0475       if (mimeName == "image/png")
0476       {
0477         patterns.prepend(pattern);
0478       }
0479       else
0480       {
0481         patterns << pattern;
0482       }
0483     }
0484   }
0485   const QUrl url = getSaveFileUrl( this, patterns.join(QStringLiteral(";;")) );
0486 
0487   if( url.isEmpty() )
0488     return;
0489 
0490   QTemporaryFile tempFile; // for network saving
0491   QString name;
0492   if( !url.isLocalFile() )
0493   {
0494     tempFile.open();
0495     name = tempFile.fileName();
0496   }
0497   else
0498   {
0499     name = url.toLocalFile();
0500   }
0501 
0502   QPixmap picture(playGround->getPicture());
0503 
0504   if (!picture.save(name))
0505   {
0506     KMessageBox::error
0507       (this, i18n("Could not save file."));
0508     return;
0509   }
0510 
0511   if( !url.isLocalFile() )
0512   {
0513     if (!upload(name, url))
0514       KMessageBox::error(this, i18n("Could not save file."));
0515   }
0516 
0517 }
0518 
0519 // Save gameboard as picture
0520 void TopLevel::filePrint()
0521 {
0522   QPrinter printer;
0523   bool ok;
0524 
0525   QPrintDialog *printDialog = new QPrintDialog(&printer, this);
0526   printDialog->setWindowTitle(i18nc("@title:window", "Print %1", actionCollection()->action(playGround->currentGameboard())->iconText()));
0527   ok = printDialog->exec();
0528   delete printDialog;
0529   if (!ok) return;
0530   playGround->repaint();
0531   if (!playGround->printPicture(printer))
0532     KMessageBox::error(this,
0533                          i18n("Could not print picture."));
0534   else
0535     KMessageBox::information(this,
0536                              i18n("Picture successfully printed."));
0537 }
0538 
0539 // Copy modified area to clipboard
0540 void TopLevel::editCopy()
0541 {
0542   QClipboard *clipboard = QApplication::clipboard();
0543   QPixmap picture(playGround->getPicture());
0544 
0545   clipboard->setPixmap(picture);
0546 }
0547 
0548 // Toggle sound off
0549 void TopLevel::soundOff()
0550 {
0551   actionCollection()->action(QStringLiteral( "speech_no_sound" ))->setChecked(true);
0552   writeOptions();
0553 }
0554 
0555 bool TopLevel::isSoundEnabled() const
0556 {
0557   return !actionCollection()->action(QStringLiteral( "speech_no_sound" ))->isChecked();
0558 }
0559 
0560 void TopLevel::toggleFullScreen()
0561 {
0562   KToggleFullScreenAction::setFullScreen( this, actionCollection()->action(QStringLiteral( "fullscreen" ))->isChecked());
0563 }
0564 
0565 void TopLevel::lockAspectRatio(bool lock)
0566 {
0567   actionCollection()->action(QStringLiteral( "lock_aspect_ratio" ))->setChecked(lock);
0568   playGround->lockAspectRatio(lock);
0569   writeOptions();
0570 }
0571 
0572 bool TopLevel::upload(const QString &src, const QUrl &target)
0573 {
0574   bool success = true;
0575   const QUrl srcUrl = QUrl::fromLocalFile(src);
0576   KIO::Job *job = KIO::file_copy(srcUrl, target, -1, KIO::Overwrite | KIO::HideProgressInfo);
0577   QEventLoop eventLoop;
0578   connect(job, &KIO::Job::result, this, [job, &eventLoop, &success] { success = !job->error(); eventLoop.quit(); } );
0579   eventLoop.exec(QEventLoop::ExcludeUserInputEvents);
0580   return success;
0581 }
0582 
0583 #include "moc_toplevel.cpp"