File indexing completed on 2024-09-08 03:46:55
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"