/* * This file is part of Auralquiz * Copyright 2011-2017 JanKusanagi JRR * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . */ #include "auralwindow.h" /***************************************************************************************** * * Constructor */ AuralWindow::AuralWindow(QWidget *parent) : QWidget(parent) { this->setWindowTitle("Auralquiz"); this->setWindowIcon(QIcon(":/icon/64x64/auralquiz.png")); QSettings settings; this->resize(settings.value("size", QSize(800, 540)).toSize()); this->firstRun = settings.value("firstRun", true).toBool(); if (firstRun) { qDebug() << "This is the first run"; QMessageBox::about(this, "Auralquiz - " + tr("First usage"), tr("This seems to be the first time you use Auralquiz.\n" "Before playing, your music will be analyzed.\n" "If needed, you should click the Options button " "and select the folder where your " "Ogg, FLAC and MP3 files are stored.\n\n" "This folder, and sub-folders will be scanned " "so Auralquiz can generate questions and answers " "about your music.\n" "\n" "You need files correctly tagged in order for " "the game to work correctly.\n" "\n" "The scan can take some time, and the program " "will not be responsive until it is complete. " "Please be patient.")); } useOwnColorTheme = settings.value("useOwnColorTheme", false).toBool(); if (useOwnColorTheme) { qDebug() << "Using own color theme"; } else { qDebug() << "Using system colors"; } QString defaultMusicDirectory; #if QT_VERSION < QT_VERSION_CHECK(5, 0, 0) defaultMusicDirectory = QDesktopServices::storageLocation(QDesktopServices::MusicLocation); #else defaultMusicDirectory = QStandardPaths::standardLocations(QStandardPaths::MusicLocation).first(); #endif // If MusicLocation is the same as HOME, don't use it. #if QT_VERSION < QT_VERSION_CHECK(5, 0, 0) if (defaultMusicDirectory == QDesktopServices::storageLocation(QDesktopServices::HomeLocation)) { defaultMusicDirectory.clear(); } #else if (defaultMusicDirectory == QStandardPaths::standardLocations(QStandardPaths::HomeLocation).first()) { defaultMusicDirectory.clear(); } #endif musicDirectory = settings.value("musicDirectory", defaultMusicDirectory).toString(); if (!musicDirectory.isEmpty() && !musicDirectory.endsWith("/")) { // Adding final "/" if it's not present in chosen path musicDirectory.append("/"); } qDebug() << "Music directory:" << this->musicDirectory; // Get data directory path #if QT_VERSION < QT_VERSION_CHECK(5, 0, 0) this->dataDirectory = QDesktopServices::storageLocation(QDesktopServices::DataLocation); #else this->dataDirectory = QStandardPaths::standardLocations(QStandardPaths::DataLocation).first(); #endif qDebug() << "Data directory:" << this->dataDirectory; // Create data directory if needed, to store music info later... QDir dataDir; if (!dataDir.exists(this->dataDirectory)) { qDebug() << "Data directory did not exist. Creating..."; if (dataDir.mkpath(this->dataDirectory)) { qDebug() << this->dataDirectory << "directory created successfully!"; } else { qDebug() << this->dataDirectory << "directory could NOT be created"; } } difficultyLevel = settings.value("difficultyLevel", 2).toInt(); // normal(2) by default numQuestions = settings.value("numQuestions", 25).toInt(); // 25 questions by default numPlayers = settings.value("numPlayers", 1).toInt(); // 1 player by default qDebug() << "Phonon runtime version:" << Phonon::phononVersion(); qDebug() << "Built with Phonon:" << PHONON_VERSION_STR; qDebug() << "* Backend::audioEffects:" << Phonon::BackendCapabilities::availableAudioEffects(); qDebug() << "* Backend::mimeTypes:" << Phonon::BackendCapabilities::availableMimeTypes(); //qDebug() << "* Backend::audioOutputDevices:" // << Phonon::BackendCapabilities::availableAudioOutputDevices(); playing = false; mainLayout = new QVBoxLayout(this); mainLayout->setContentsMargins(3, 1, 3, 1); initWelcomeScreen(); initPlayingScreen(); this->playerNames.clear(); playerNames = settings.value("playerNames", QStringList() << "1" << "2" // overriden << "3" << "4" // in OptionsDialog << "5" << "6" << "7" << "8").toStringList(); this->setLayout(mainLayout); musicAnalyzer = new MusicAnalyzer(musicDirectory, dataDirectory, musicFiles, this); connect(musicAnalyzer, SIGNAL(setStartGameButton(bool,QString,QString)), this, SLOT(modifyStartGameButton(bool,QString,QString))); postInitTimer = new QTimer(this); postInitTimer->setSingleShot(true); postInitTimer->setInterval(500); connect(postInitTimer, SIGNAL(timeout()), musicAnalyzer, SLOT(loadSongList())); postInitTimer->start(); // Call loadSongList() from the timer, to avoid the // first-run analyzing all songs without visible window // this timer will call createSongList() to reload music, after config update postConfigUpdatedTimer = new QTimer(this); postConfigUpdatedTimer->setSingleShot(true); postConfigUpdatedTimer->setInterval(500); connect(postConfigUpdatedTimer, SIGNAL(timeout()), musicAnalyzer, SLOT(createSongList())); // Timer used to show the Ranking window after a moment rankingTimer = new QTimer(this); // TEMPORARY ranking tests - START #if 0 this->goodAnswers.clear(); goodAnswers << 3 << 17 << 3 << 4 << 5 << 6 << 8 << 8; this->badAnswers.clear(); badAnswers << 11 << 22 << 33 << 44 << 55 << 66 << 77 << 88; this->timedOutAnswers.clear(); timedOutAnswers << 111 << 222 << 333 << 444 << 555 << 666 << 777 << 888; this->score.clear(); score << goodAnswers[0]*123 << goodAnswers[1]*123 << goodAnswers[2]*123 << goodAnswers[3]*123 << goodAnswers[4]*123 << goodAnswers[5]*123 << goodAnswers[6]*123 << goodAnswers[7]*123; qDebug() << "Testing scores:" << score; Ranking *rankingTest; rankingTest = new Ranking(this->score.length(), this->playerNames, this->score, this->goodAnswers, this->badAnswers, this->timedOutAnswers); rankingTest->show(); qDebug() << "test ranking created and shown"; #endif // TEMPORARY ranking tests - END } /********************************************************************************* * * Destructor */ AuralWindow::~AuralWindow() { qDebug() << "AuralWindow destroyed"; } /******************************************************************************* * * Shuffle song list */ void AuralWindow::shuffleMusicFiles() { uint randomSeed; randomSeed = (QTime::currentTime().hour()) + (QTime::currentTime().minute() * 4) + (QTime::currentTime().second() * 5) + (QTime::currentTime().msec() * 6); randomSeed *= 8; randomSeed += qrand() % (randomSeed / (QTime::currentTime().second()+1)); qsrand(randomSeed); // ensure decent randomness based on current time int newPosition; for (int counter=0; counter != musicFiles[0].length(); ++counter) { newPosition = qrand() % musicFiles[0].length(); musicFiles[0].swap(0, newPosition); // filename musicFiles[1].swap(0, newPosition); // artist musicFiles[2].swap(0, newPosition); // title } qDebug() << "Music Files shuffled. randomSeed:" << randomSeed; } /* * Update statistics panel */ void AuralWindow::updateStatistics() { QString statsTable; statsTable = "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "
" + tr("Good") + " " + QString("%1").arg(this->goodAnswers[currentPlayer]) + "
" + tr("Bad") + " " + QString("%1").arg(this->badAnswers[currentPlayer]) + "
" + tr("Timed out") + " " + QString("%1").arg(this->timedOutAnswers[currentPlayer]) + "
"; this->statisticsLabel->setText(statsTable); } /***************************************************************************************** * * Set up the welcome screen, with the logo and main menu */ void AuralWindow::initWelcomeScreen() { qDebug() << "Init welcome screen"; optionsDialog = new OptionsDialog(this); connect(optionsDialog, SIGNAL(optionsChanged(bool,QString,bool,int,int,int,QStringList,bool)), this, SLOT(updateConfig(bool,QString,bool,int,int,int,QStringList,bool))); logoLabel = new QLabel(this); logoLabel->setPixmap(QPixmap(":/images/logo.png")); startGameButton = new QPushButton(QIcon::fromTheme("media-playback-start", QIcon(":/images/button-arrow.png")), "\n" + tr("&Start game") + "\n", this); connect(startGameButton, SIGNAL(clicked()), optionsDialog, SLOT(showPlayMode())); startGameButton->setDisabled(true); configureButton = new QPushButton(QIcon::fromTheme("configure", QIcon(":/images/button-configure.png")), tr("&Options"), this); connect(configureButton, SIGNAL(clicked()), optionsDialog, SLOT(showConfigMode())); aboutButton = new QPushButton(QIcon::fromTheme("help-about"), tr("&About..."), this); connect(aboutButton, SIGNAL(clicked()), this, SLOT(showAbout())); quitButton = new QPushButton(QIcon::fromTheme("application-exit"), tr("&Quit"), this); connect(quitButton, SIGNAL(clicked()), this, SLOT(close())); // Control+Q is handled separately to avoid disabling Alt+Q (or translated equivalent) quitAction = new QAction(this); quitAction->setShortcut(QKeySequence("Ctrl+Q")); connect(quitAction, SIGNAL(triggered()), this, SLOT(close())); this->addAction(quitAction); welcomeLayout = new QVBoxLayout(); welcomeLayout->setAlignment(Qt::AlignHCenter); welcomeLayout->addWidget(logoLabel); welcomeLayout->addSpacing(24); welcomeLayout->addWidget(startGameButton); welcomeLayout->addSpacing(16); welcomeLayout->addWidget(configureButton); welcomeLayout->addWidget(aboutButton); welcomeLayout->addWidget(quitButton); welcomeLayout->addSpacing(16); welcomeWidget = new QWidget(this); welcomeWidget->setLayout(welcomeLayout); mainLayout->addWidget(welcomeWidget); } /***************************************************************************************** * * Set up the playing screen, with progress bar, time bar, score, etc. */ void AuralWindow::initPlayingScreen() { qDebug() << "Init playing screen"; playingWidget = new QWidget(this); QFont playerFont; playerFont.setPointSize(12); playerFont.setBold(true); playerNameLabel = new QLabel(":: PLAYER ::", this); playerNameLabel->setAlignment(Qt::AlignRight | Qt::AlignTop); playerNameLabel->setFont(playerFont); QFont questionFont; questionFont.setPointSize(20); questionFont.setBold(true); questionLabel = new QLabel(":: QUESTION ::", this); questionLabel->setAlignment(Qt::AlignCenter); questionLabel->setFont(questionFont); questionLabel->setFrameStyle(QFrame::Raised | QFrame::StyledPanel); QFont infoFont; infoFont.setPointSize(14); infoFont.setBold(true); infoFont.setItalic(true); infoLabel = new QLabel(":: INFO ::", this); infoLabel->setAlignment(Qt::AlignCenter); infoLabel->setWordWrap(true); infoLabel->setFont(infoFont); gameTimer = new QTimer(this); gameTimer->setInterval(100); // every 100ms, so it moves fast connect(gameTimer, SIGNAL(timeout()), this, SLOT(timerTick())); // This will call newQuestion preQuestionTimer = new QTimer(this); preQuestionTimer->setSingleShot(true); connect(preQuestionTimer, SIGNAL(timeout()), this, SLOT(newQuestion())); // This will call preQuestion postQuestionTimer = new QTimer(this); postQuestionTimer->setSingleShot(true); postQuestionTimer->setInterval(1500); // 1,5 seconds connect(postQuestionTimer, SIGNAL(timeout()), this, SLOT(preQuestion())); ///////////////////////////////////////////////////////////////////// Right timeBar = new QProgressBar(this); timeBar->setOrientation(Qt::Vertical); timeBar->setFormat(tr("Time")); // "%v seconds" // timeBar's range() and value() will be set upon game start, on toggleScreen() timeBar->setToolTip("" // HTMLized for wordwrap + tr("Remaining time to answer this question")); //////////////////////////////////////////////////////////////////// Bottom gameProgressBar = new QProgressBar(this); gameProgressBar->setFormat(tr("%v out of %m questions - %p%")); // gameProgressBar's range() and value() will be set upon game start, on toggleScreen() gameProgressBar->setToolTip("" + tr("How many questions you've answered")); gameScoreLabel = new QLabel(tr("Score"), this); gameScoreLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter); gameScore = new QLCDNumber(5, this); gameScore->setSegmentStyle(QLCDNumber::Flat); gameScore->display(0); gameScore->setToolTip("" + tr("Your current score")); statusLabel = new QLabel(this); endGameButton = new QPushButton(QIcon::fromTheme("media-playback-stop", QIcon(":/images/button-cancel.png")), tr("&End game"), this); endGameButton->setFlat(true); connect(endGameButton, SIGNAL(clicked()), this, SLOT(confirmEndGame())); ///////////////////////////////////////////////////////////////// Left side aniNoteMovie.setFileName(":/images/aninote.gif"); aniNoteLabel = new QLabel(this); aniNoteLabel->setMovie(&aniNoteMovie); aniNoteLabel->setFrameStyle(QFrame::StyledPanel | QFrame::Raised); aniNoteLabel->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); QFont statsFont; statsFont.setPointSize(12); statisticsLabel = new QLabel("**STATS**", this); statisticsLabel->setFrameStyle(QFrame::StyledPanel | QFrame::Sunken); statisticsLabel->setFont(statsFont); statisticsBoxLayout = new QVBoxLayout(); statisticsBoxLayout->addWidget(statisticsLabel); statisticsBox = new QGroupBox(tr("Statistics"), this); statisticsBox->setLayout(statisticsBoxLayout); //////////////////////////////////////////////////////////////////// Center //// Add the 4 answer buttons, and their font answerButton[0] = new QPushButton(QIcon::fromTheme("arrow-right", QIcon(":/images/button-arrow.png")), "ANSWER 1 ----------------", this); connect(answerButton[0], SIGNAL(clicked()), this, SLOT(answer1())); answerButton[1] = new QPushButton(QIcon::fromTheme("arrow-right", QIcon(":/images/button-arrow.png")), "ANSWER 2 ----------------", this); connect(answerButton[1], SIGNAL(clicked()), this, SLOT(answer2())); answerButton[2] = new QPushButton(QIcon::fromTheme("arrow-right", QIcon(":/images/button-arrow.png")), "ANSWER 3 ----------------", this); connect(answerButton[2], SIGNAL(clicked()), this, SLOT(answer3())); answerButton[3] = new QPushButton(QIcon::fromTheme("arrow-right", QIcon(":/images/button-arrow.png")), "ANSWER 4 ----------------", this); connect(answerButton[3], SIGNAL(clicked()), this, SLOT(answer4())); QFont buttonFont; buttonFont.setPointSize(13); buttonFont.setBold(true); // set font and minimum height in all 4 buttons for (int counter = 0; counter != 4; ++counter) { answerButton[counter]->setFont(buttonFont); // Set a minimum width, so it's ok for most titles/names answerButton[counter]->setMinimumWidth(512); // Make buttons use all space available answerButton[counter]->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); } /* Matching QActions to allow independent shortcuts. * * Using these independent shortcuts avoids a problem with autogenerated * shortcuts under Plasma 5. * * Shortcut blocks are 1/2/3/4, Z/X/C/V, 7/8/9/0 and U/I/O/P * */ answerAction1 = new QAction(this); answerAction1->setShortcuts(QList() << Qt::Key_1 << Qt::Key_Z << Qt::Key_7 << Qt::Key_U); connect(answerAction1, SIGNAL(triggered()), this, SLOT(answer1())); answerAction2 = new QAction(this); answerAction2->setShortcuts(QList() << Qt::Key_2 << Qt::Key_X << Qt::Key_8 << Qt::Key_I); connect(answerAction2, SIGNAL(triggered()), this, SLOT(answer2())); answerAction3 = new QAction(this); answerAction3->setShortcuts(QList() << Qt::Key_3 << Qt::Key_C << Qt::Key_9 << Qt::Key_O); connect(answerAction3, SIGNAL(triggered()), this, SLOT(answer3())); answerAction4 = new QAction(this); answerAction4->setShortcuts(QList() << Qt::Key_4 << Qt::Key_V << Qt::Key_0 << Qt::Key_P); connect(answerAction4, SIGNAL(triggered()), this, SLOT(answer4())); this->addAction(answerAction1); this->addAction(answerAction2); this->addAction(answerAction3); this->addAction(answerAction4); // Add the AnswerBox, used in the highest difficulty mode, type-the-answer answerBox = new AnswerBox(this); answerBox->setFont(buttonFont); answerBox->setMinimumWidth(512); connect(answerBox, SIGNAL(answered(bool)), this, SLOT(answerFromAnswerBox(bool))); // Disable player input initially, to avoid problems in the title screen this->enablePlayerInput(false); /////////////////////////////////////////////////////////////////// Layouts playingTopLayout = new QVBoxLayout(); playingTopLayout->setContentsMargins(0, 0, 0, 0); playingTopLayout->addWidget(playerNameLabel); playingTopLayout->addWidget(questionLabel, 1); playingTopLayout->addWidget(infoLabel, 1); statisticsLayout = new QVBoxLayout(); statisticsLayout->addWidget(aniNoteLabel, 1, Qt::AlignTop | Qt::AlignHCenter); statisticsLayout->addSpacing(2); statisticsLayout->addStretch(1); statisticsLayout->addWidget(statisticsBox, 1); answersLayout = new QVBoxLayout(); answersLayout->addWidget(answerButton[0], 4); answersLayout->addSpacing(2); answersLayout->addStretch(1); answersLayout->addWidget(answerButton[1], 4); answersLayout->addSpacing(2); answersLayout->addStretch(1); answersLayout->addWidget(answerButton[2], 4); answersLayout->addSpacing(2); answersLayout->addStretch(1); answersLayout->addWidget(answerButton[3], 4); // TMP FIXME: adding answerBox with great stretch factor to make it look almost centered answersLayout->addWidget(answerBox, 22, Qt::AlignVCenter); playingMiddleLayout = new QHBoxLayout(); playingMiddleLayout->addLayout(statisticsLayout, 2); playingMiddleLayout->addSpacing(4); playingMiddleLayout->addLayout(answersLayout, 9); playingMiddleLayout->addSpacing(4); playingMiddleLayout->addWidget(timeBar); playingProgressLayout = new QHBoxLayout(); playingProgressLayout->addWidget(gameProgressBar); playingProgressLayout->addSpacing(32); playingProgressLayout->addWidget(gameScoreLabel); playingProgressLayout->addSpacing(16); playingProgressLayout->addWidget(gameScore); playingBottomLayout = new QHBoxLayout(); playingBottomLayout->addWidget(statusLabel, 1); playingBottomLayout->addWidget(endGameButton, 0, Qt::AlignRight); playingLayout = new QVBoxLayout(); playingLayout->setAlignment(Qt::AlignRight); playingLayout->addLayout(playingTopLayout); playingLayout->addSpacing(2); playingLayout->addStretch(0); playingLayout->addLayout(playingMiddleLayout, 1); playingLayout->addSpacing(8); playingLayout->addStretch(0); playingLayout->addLayout(playingProgressLayout); playingLayout->addSpacing(4); playingLayout->addStretch(0); playingLayout->addLayout(playingBottomLayout); playingWidget->setLayout(playingLayout); mainLayout->addWidget(playingWidget); if (useOwnColorTheme) { this->setThemedColors(); } musicPlayer = Phonon::createPlayer(Phonon::MusicCategory, Phonon::MediaSource()); qDebug() << "Phonon::createPlayer()->isValid()? " << musicPlayer->isValid(); if (!musicPlayer->isValid()) { QMessageBox::warning(this, tr("Sound system error"), tr("There seems to be a problem with " "your sound system.") + "

" + tr("Maybe you don't have any Phonon backends " "installed.")); } connect(musicPlayer, SIGNAL(stateChanged(Phonon::State,Phonon::State)), this, SLOT(playerStateChanged(Phonon::State,Phonon::State))); playingWidget->hide(); } /****************************************************************************** * * Switch between the welcome screen and the playing screen, in either way * */ void AuralWindow::toggleScreen() { if (!playing) { // START game! qDebug() << "Starting game"; welcomeWidget->hide(); shuffleMusicFiles(); // Reset everything... currentMusicFile = 0; this->gameProgressBar->setRange(0, this->numQuestions); this->gameProgressBar->setValue(0); this->gameScore->display(0); // reset score maxTime = 500 / (difficultyLevel + 1); // In millisec/10 if (difficultyLevel == 5) { maxTime += 100; // Extra time in Hardcore/type-the-answer mode! } warningTime = maxTime / 4; dangerTime = maxTime / 8; //this->pieceDuration = maxTime - (maxTime / (difficultyLevel+1)); this->pieceDuration = maxTime - ( 90 / (difficultyLevel+1) ); // +1 becausec difficulty level can be 0 if (difficultyLevel == 5) { pieceDuration -= 5; // Extra duration in Hardcore level! } qDebug() << "Max/Warning/Danger times:" << maxTime/6 << warningTime/6 << dangerTime/6 << "secs"; qDebug() << "Piece duration:" << (maxTime - pieceDuration) / 6 << "secs"; timeBar->setRange(0, this->maxTime); timeBar->setValue(this->maxTime); if (difficultyLevel < 5) // Regular mode: show buttons, hide AnswerBox { answerButton[0]->show(); answerButton[1]->show(); answerButton[2]->show(); answerButton[3]->show(); answerBox->hide(); } else // Type-the-answer mode: hide buttons and show AnswerBox { answerBox->show(); answerButton[0]->hide(); answerButton[1]->hide(); answerButton[2]->hide(); answerButton[3]->hide(); } this->enablePlayerInput(true); if (numPlayers > 1) { this->playerNameLabel->show(); } else { this->playerNameLabel->hide(); } // Reset stats for all players this->score.clear(); this->goodAnswers.clear(); this->badAnswers.clear(); this->timedOutAnswers.clear(); for (int counter = 0; counter != numPlayers; ++counter) { score << 0; goodAnswers << 0; badAnswers << 0; timedOutAnswers << 0; } this->currentPlayer = 0; QStringList startStrings; // Randomly choose from a list of "get ready" strings startStrings << tr("Starting!") << tr("Let's go!") << tr("GO!!") << tr("Good luck!"); this->infoLabel->setStyleSheet("background: qradialgradient(spread:pad, " "cx:0.5, cy:0.5, radius:0.5, fx:0.5, " "fy:0.5, stop:0 rgba(0, 64, 64, 255) " "stop:1 rgba(0, 255, 255, 255)); " "color: white"); this->infoLabel->setText(startStrings.at(qrand() % startStrings.length())); QString totalSongs = QLocale::system() .toString(this->musicFiles[0].length()); this->statusLabel->setText(tr("%1 songs available").arg(totalSongs)); playingWidget->show(); playing = true; this->updateStatistics(); newQuestion(); } else { // STOP game qDebug() << "Stopping game"; musicPlayer->stop(); // stop music, if any gameTimer->stop(); preQuestionTimer->stop(); // These 2 lines fix problems when clicking postQuestionTimer->stop(); // "End Game" right after answering // (Music in title screen) this->enablePlayerInput(false); playingWidget->hide(); welcomeWidget->show(); playing = false; } } /* * Set styles to app-specific, or set them empty for user/system-defined * */ void AuralWindow::setThemedColors() // everything here is quite temporary { // set transparency this->setWindowOpacity(0.98); // set dark-blue style to the program in general this->setStyleSheet("background: qlineargradient(spread:pad, " "x1:0, y1:0, x2:1, y2:1, " "stop:0.0 rgba(20, 40, 40, 255), " "stop:1.0 rgba(20, 40, 120, 255) );" "color: qlineargradient(spread:pad, " "x1:0, y1:0, x2:1, y2:1, " "stop:0.0 rgba(140, 140, 220, 255), " "stop:0.5 rgba(160, 250, 160, 255), " "stop:1.0 rgba(220, 140, 140, 255) );"); logoLabel->setStyleSheet("background: transparent"); aniNoteLabel->setStyleSheet("background: transparent"); // set colors on answer buttons and answer box answerButton[0]->setStyleSheet("background: #20DD20; color: darkBlue"); answerButton[1]->setStyleSheet("background: #20CC20; color: darkBlue"); answerButton[2]->setStyleSheet("background: #20BB20; color: darkBlue"); answerButton[3]->setStyleSheet("background: #20AA20; color: darkBlue"); answerBox->setStyleSheet("background: #20AA20; color: darkBlue"); // set color of LCD score indicator gameScoreLabel->setStyleSheet("background: transparent"); gameScore->setStyleSheet("background: black;" "color: lightBlue"); statisticsBox->setStyleSheet("background: #400000"); statisticsLabel->setStyleSheet("background: darkRed;" "color: white"); statusLabel->setStyleSheet("background: transparent"); } void AuralWindow::enablePlayerInput(bool state) { if (difficultyLevel < 5) { // Keyboard keys 1~4, Z~V, 7~0, U~P, and buttons answerAction1->setEnabled(state); answerAction2->setEnabled(state); answerAction3->setEnabled(state); answerAction4->setEnabled(state); this->answerButton[0]->setEnabled(state); this->answerButton[1]->setEnabled(state); this->answerButton[2]->setEnabled(state); this->answerButton[3]->setEnabled(state); } else { this->answerBox->setEnabled(state); if (state) { this->answerBox->setFocus(); } } // Always disable the QActions when disabling input; otherwise, they can cause crashes if (!state) { answerAction1->setDisabled(true); answerAction2->setDisabled(true); answerAction3->setDisabled(true); answerAction4->setDisabled(true); } } /////////////////////////////////////////////////////////////////////////////// ////////////////////////////////// SLOTS ////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// // Used from newQuestion(), via SIGNAL void AuralWindow::playerStateChanged(Phonon::State newState, Phonon::State oldState) { qDebug() << "playerStateChanged() (5 means error) -> " << oldState << ">" << newState; //if (oldState != Phonon::PlayingState if ((oldState == Phonon::LoadingState || oldState == Phonon::PausedState) && newState == Phonon::StoppedState) { qDebug() << "Gone from LoadingState to StoppedState"; this->nextSongLoaded(); } if (newState == Phonon::PausedState) { qDebug() << "Entered PausedState"; } if (musicPlayer->errorType() != 0) // if some error { qDebug() << "playerStateChanged(), some error!! State:" << newState; qDebug() << "Error string and type:" << musicPlayer->errorString() << musicPlayer->errorType(); QMessageBox::warning(this, tr("Error playing sound"), tr("An error occurred while playing sound.\n" "The error message was: %1\n\n" "Maybe your Phonon-backend does not have " "support for the MP3 file " "format.").arg(musicPlayer->errorString())); } } /*****************************************************************************/ /* * Decrease time left to guess song. Called by gameTimer * */ void AuralWindow::timerTick() { if (timeBar->value() == this->maxTime-1) // Just once upon start { // Seek to a random part of the music file qint64 seekTime = 10000; // Start at least at 00:10 // add random ms avoiding last 50 sec, resulting in range 00:10 -> END -01:00 seekTime += qrand() % (musicPlayer->totalTime() - 50000); qDebug() << "Seeking to:" << seekTime / 1000 << "sec /" << "Seekable:" << musicPlayer->isSeekable() << "/ Total time:" << musicPlayer->totalTime() / 1000; musicPlayer->play(); //musicPlayer->pause(); /* * play() BEFORE seek() * because gstreamer-backend seeks to 0 again on play() apparently... */ musicPlayer->seek(seekTime); // TMPFIX, add error control //musicPlayer->play(); } timeBar->setValue(timeBar->value() - 1); // Stop music earlier than the time limit to answer the question if (timeBar->value() == this->pieceDuration) { this->musicPlayer->stop(); this->aniNoteLabel->movie()->stop(); // TMPFIX qDebug() << "totalTime, again:" << musicPlayer->totalTime() / 1000; // TMP tests } // Clear colored info label (good/bad), soon after new question if (timeBar->value() == this->maxTime - 10) // very very TMPFIX { this->infoLabel->setText(""); this->infoLabel->setStyleSheet("background: transparent"); } // if remaining time is low, change timeBar colors if (timeBar->value() == this->warningTime) // warning level { timeBar->setStyleSheet("background-color: yellow"); // orange? } if (timeBar->value() == this->dangerTime) // danger level { timeBar->setStyleSheet("background-color: red"); // darkRed? } // time to answer ended if (timeBar->value() == 0) { qDebug() << "Time's up!"; musicPlayer->stop(); this->infoLabel->setStyleSheet("background: " "qradialgradient(spread:pad, " "cx:0.5, cy:0.5, radius:0.5, " "fx:0.5, fy:0.5, stop:0 " "rgba(64, 0, 0, 255) stop:1 " "rgba(128, 0, 0, 255)); color: white"); this->infoLabel->setText(tr("Time's up!!") + " " + tr("The answer was:") + " " + musicFiles[questionType].at(currentMusicFile)); answerQuestion(0); // 0 means it's timeout, not a button } } /* * Update configuration from optionsDialog SIGNAL * */ void AuralWindow::updateConfig(bool startGame, QString directory, bool forceReload, int difficulty, int questions, int players, QStringList playerNameList, bool ownColors) { bool mustReload = false; if (this->musicDirectory != directory) { qDebug() << "musicDirectory has changed, mustReload = true"; qDebug() << "musicDirectory:" << musicDirectory << "; directory:" << directory; mustReload = true; // if music directory's been changed, reload collection info } this->musicDirectory = directory; this->musicAnalyzer->setMusicDirectory(musicDirectory); // inform M.A. about it this->difficultyLevel = difficulty; this->numQuestions = questions; this->numPlayers = players; this->playerNames.clear(); playerNames = playerNameList; this->useOwnColorTheme = ownColors; qDebug() << "Updated config with: " << startGame << directory << forceReload << difficulty << questions << players << playerNameList << ownColors; // reload music collection information here if needed if (mustReload || forceReload) { qDebug() << "Reloading music collection information"; // this->createSongList(); // replaced by a call from a timer postConfigUpdatedTimer->start(); // will call createSongList() in 500ms } // if optionsDialog was called in PlayMode, start game now if (startGame) { if (numPlayers > 1) // in multiplayer, give extra time to prepare { this->preQuestionTimer->setInterval(2000); // 2 sec // FIXME: make configurable? } else { this->preQuestionTimer->setInterval(500); // half a sec } this->toggleScreen(); // Start game! } } /* * About... box */ void AuralWindow::showAbout() { QMessageBox::about(this, tr("About Auralquiz"), QString("Auralquiz v%1") .arg(qApp->applicationVersion()) + "
" "Copyright 2011-2017 JanKusanagi" "
" "
" "" "https://jancoding.wordpress.com/auralquiz" "
" "
" + tr("Auralquiz loads all your music from a specified " "folder and asks questions about it, playing a " "short piece of each music file as clue.") + "
" "
" + tr("English translation by JanKusanagi.", "TRANSLATORS: Change this with your language and name. " "Feel free to add your contact information, such as e-mail. " "If there was another translator before you, add your " "name after theirs ;)") + "
" "
" + tr("Thanks to all the testers, translators and packagers, " "who help make Auralquiz better!") + "
" "
" + tr("Auralquiz is licensed under the GNU GPL license.") + "
" + tr("Main screen image and some icons are based on " "Oxygen icons, under LGPL license.") + "
" "
" "" "GNU GPL v2 - " "" "GNU LGPL v2.1" "
" "
" "" "techbase.kde.org/Projects/Oxygen" "
" "
" + QString("Qt v%1 ~ Phonon v%2") .arg(qVersion()) .arg(Phonon::phononVersion())); } void AuralWindow::modifyStartGameButton(bool enabledState, QString text, QString tooltip) { this->startGameButton->setEnabled(enabledState); if (!text.isEmpty()) { this->startGameButton->setText("\n" + text + "\n"); this->startGameButton->setToolTip("" // HTMLize, so it gets wordwrap + tooltip); } this->startGameButton->setFocus(); } /****************************************************************************** * * Prepare a new question, its answers, and play the new song file */ void AuralWindow::newQuestion() { qDebug() << "newQuestion()"; timeBar->setValue(this->maxTime); timeBar->setStyleSheet(""); this->correctAnswer = (qrand() % 4) + 1; // 1, 2, 3, 4; 0 would mean TIMED UP //qDebug() << "correctAnswer:" << this->correctAnswer; questionType = (currentMusicFile & 1) + 1; // quite TMPFIX, odd=artist, even=title //qDebug() << "questionType: " << questionType; if (questionType == 1) { questionLabel->setText(tr("Who plays this song?")); } else { questionLabel->setText(tr("What's the title of this song?")); } // show current player name in multiplayer mode // FIXME: and avatar! if (numPlayers > 1) { playerNameLabel->setText(tr("Player %1 of %2").arg(currentPlayer + 1) .arg(numPlayers) + " -- " + QString("%1").arg(playerNames.at(currentPlayer))); gameScore->display(score.at(currentPlayer)); } QString buttonTexts[4]; int answerCount = 0; // FIXME: These checks should be moved to the analyzer, in createSongList() int tries = 0; while (answerCount < 4) { buttonTexts[answerCount] = musicFiles[questionType] .at(qrand() % this->musicFiles[0].length()); //qDebug() << "answerCount" << answerCount; // If text in current button is NOT the correct answer (lowercase comparison!) if (buttonTexts[answerCount].toLower() != musicFiles[questionType] .at(currentMusicFile).toLower()) { bool isGood = true; for (int previousAnswer = 0; previousAnswer != answerCount; ++previousAnswer) { // Compare strings in LOWERCASE (Metallica ~= metallica) if (buttonTexts[answerCount].toLower() != buttonTexts[previousAnswer].toLower()) { isGood = true; //qDebug() << "is GOOD" << previousAnswer; //qDebug() << "is GOOD" << buttonTexts[answerCount] // << buttonTexts[previousAnswer]; } else { isGood = false; //qDebug() << "is NOT good; duplicated, break" << previousAnswer; //qDebug() << "is NOT good" << buttonTexts[answerCount] // << buttonTexts[previousAnswer]; break; } } if (isGood) { // Set random artists/titles as labels on buttons answerButton[answerCount]->setText(buttonTexts[answerCount]); ++answerCount; tries = 0; } //qDebug() << "buttonTexts[answerCount]:" << buttonTexts[answerCount]; } ++tries; /* TMPFIX Temporary way of catching infinite loops if there are not enough different artists or titles */ if (tries > 50) { // FIXME: these checks should be done after createSongList() qDebug() << "Not enough different titles or artists to create question!"; answerCount = 999; // "break" } } // quite hacky, TMPFIX if (tries < 50) // if no problem with duplicates, meaning we have enough valid files { // Set correct artist/title on one button answerButton[correctAnswer-1]->setText(musicFiles[questionType].at(currentMusicFile)); // replace "&" by "&&" in buttons, so it doesn't turn into an accelerator // Also truncate if answer is too long for (int counter=0; counter != 4; ++counter) { QString fixedAnswer = answerButton[counter]->text(); fixedAnswer.replace(QRegExp("&"), "&&"); // fix non-accelerator fixedAnswer.truncate(64); // avoid wiiiiide buttons // FIXME 1.1.x: truncate position should be calculated, not hardcoded // For now, 64 seems sane answerButton[counter]->setText(fixedAnswer); } // Set answer for AnswerBox too answerBox->setAnswer(musicFiles[questionType].at(currentMusicFile)); // Re-enable stuff to answer this->enablePlayerInput(true); musicPlayer->stop(); musicPlayer->clear(); // Stop playing file and clear queues qDebug() << "musicPlayer stopped and cleared"; musicPlayer->setCurrentSource(QUrl("file://" + musicFiles[0].at(currentMusicFile))); qDebug() << "newQuestion(): musicPlayer->setCurrentSource() done"; qDebug() << "seekable?" << this->musicPlayer->isSeekable(); // checks needed for gstreamer qDebug() << "PhononState should change now; " "with Gstreamer-backend it might stop here (BUG)"; } else { this->toggleScreen(); // Recursive!! :P QMessageBox::critical(this, tr("Not enough music files"), tr("You don't have enough music files, " "or from enough different artists.\n" "You need music from at least 5 different artists " "to be able to generate questions.")); } } /* * Called from PlayerStateChanged() */ void AuralWindow::nextSongLoaded() { qDebug() << "(playerStateChanged) > nextSongLoaded()"; /* Put into PlayingState so seeking and getting total time works correctly later * * A little hacky and maybe unreliable with some backends (TMP / FIXME) * */ musicPlayer->play(); musicPlayer->pause(); // Pause immediately, so the user can't hear anything this->aniNoteLabel->movie()->start(); gameTimer->start(); // will call "timerTick()" } void AuralWindow::preQuestion() { if (this->gameProgressBar->value() < this->numQuestions) { // There are more questions! ++currentMusicFile; if (currentMusicFile == this->musicFiles[0].length()) { currentMusicFile = 0; qDebug() << "Not enough music files to avoid repeating; back to 1st"; } this->updateStatistics(); infoLabel->setStyleSheet("background: qradialgradient(spread:pad, " "cx:0.5, cy:0.5, radius:0.5, fx:0.5, fy:0.5, " "stop:0 rgba(0, 0, 128, 255) stop:1 " "rgba(0, 128, 255, 127)); color: white"); if (numPlayers > 1) { infoLabel->setText(tr("Go, %1!", "%1=player's name") .arg(playerNames.at(currentPlayer))); } else { infoLabel->setText(tr("Next!")); } preQuestionTimer->start(); // this will call newQuestion() } else // No more questions! { qDebug() << "End of questions"; // Show the ranking window ranking = new Ranking(numPlayers, playerNames, score, goodAnswers, badAnswers, timedOutAnswers, this); connect(ranking, SIGNAL(closed()), this, SLOT(killRanking())); rankingTimer->setSingleShot(true); connect(rankingTimer, SIGNAL(timeout()), ranking, SLOT(show())); rankingTimer->start(2000); // Wait 2 seconds before showing the ranking this->infoLabel->setStyleSheet("background: qradialgradient(spread:pad, " "cx:0.5, cy:0.5, radius:0.5, fx:0.5, " "fy:0.5, stop:0 rgba(0, 0, 128, 255) " "stop:1 rgba(0, 128, 255, 255)); " "color: white"); this->infoLabel->setText(tr("All done!")); } } /************************************************************************************** * * Check if answer is correct, give score, and get new question */ void AuralWindow::answerQuestion(int numAnswer) { qDebug() << "Answered question with button" << numAnswer; this->enablePlayerInput(false); this->aniNoteLabel->movie()->stop(); musicPlayer->stop(); // stop music // This helps Phonon's stateChanged(), avoids silent songs gameTimer->stop(); int questionPoints; if (timeBar->value() != 0) { //questionPoints = (100 * difficultyLevel) + (timeBar->value() / 2); // OLD questionPoints = 50 * difficultyLevel+1; questionPoints += timeBar->value() / 2; questionPoints += 100; } else { questionPoints = 0; this->timedOutAnswers[currentPlayer]++; qDebug() << "Answered by timeout"; } if (questionPoints != 0) { if (numAnswer == correctAnswer) { QStringList rightAnswerStrings; rightAnswerStrings << tr("Correct!!") << tr("Yeah!") << tr("That's right!") << tr("Good!") << tr("Great!"); this->infoLabel->setStyleSheet("background: " "qradialgradient(spread:pad, " "cx:0.5, cy:0.5, radius:0.5, " "fx:0.5, fy:0.5, stop:0 " "rgba(0, 192, 0, 255) " "stop:1 rgba(0, 255, 0, 0)); " "color: white"); this->infoLabel->setText(rightAnswerStrings .at(qrand() % rightAnswerStrings.length()) + " ~ " + tr("%1 by %2", "1=song title, 2=author name") .arg("\"" + musicFiles[2].at(currentMusicFile) + "\"") .arg("\"" + musicFiles[1].at(currentMusicFile) + "\"") + " ~ " + tr("+%1 points!").arg(questionPoints)); this->score[currentPlayer] += questionPoints; gameScore->display(score.at(currentPlayer)); this->goodAnswers[currentPlayer]++; qDebug() << "correct!!" << questionPoints; } else { QStringList wrongAnswerStrings; wrongAnswerStrings << tr("Wrong!") << tr("No!") << tr("That's not it.") << tr("Ooops!") << tr("FAIL!"); this->infoLabel->setStyleSheet("background: " "qradialgradient(spread:pad, " "cx:0.5, cy:0.5, radius:0.5, " "fx:0.5, fy:0.5, stop:0 " "rgba(192, 0, 0, 255) " "stop:1 rgba(255, 0, 0, 0)); " "color: white"); this->infoLabel->setText(wrongAnswerStrings .at(qrand() % wrongAnswerStrings.length()) + " " + tr("Correct answer was:") + " " + musicFiles[questionType].at(currentMusicFile)); // quite TMPFIX this->badAnswers[currentPlayer]++; qDebug() << "wrong!"; } } this->updateStatistics(); if (numPlayers > 1) { this->currentPlayer++; if (currentPlayer == numPlayers) { this->currentPlayer = 0; // back to first player qDebug() << "Next question, back to Player 1"; } } if (currentPlayer == 0) { // Always on single-player, and after all players answer, in multi gameProgressBar->setValue(gameProgressBar->value() + 1); } postQuestionTimer->start(); // will call preQuestion() } // These below... suck void AuralWindow::answer1() { answerQuestion(1); } void AuralWindow::answer2() { answerQuestion(2); } void AuralWindow::answer3() { answerQuestion(3); } void AuralWindow::answer4() { answerQuestion(4); } // This one doesn't suck, actually! void AuralWindow::answerFromAnswerBox(bool correct) { if (correct) { answerQuestion(this->correctAnswer); } else { answerQuestion(-1); // Answer incorrectly } } void AuralWindow::confirmEndGame() { int confirm = QMessageBox::question(this, tr("End game?"), tr("Are you sure you want to end " "this game?"), tr("&Yes, end it"), tr("&No"), "", 1, 1); if (confirm == 0 && this->playing) // Ignore if game ended by timeout { // while the dialog was present qDebug() << "Ending game..."; this->toggleScreen(); } } void AuralWindow::killRanking() { qDebug() << "deleting Ranking object"; ranking->deleteLater(); this->toggleScreen(); } /////////////////////////////////////////////////////////////////////////////// //////////////////////////////// PROTECTED //////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// /****************************************************************************** * * Store app config upon exit */ void AuralWindow::closeEvent(QCloseEvent *event) { QSettings settings; settings.setValue("firstRun", false); settings.setValue("size", this->size()); settings.setValue("useOwnColorTheme", this->useOwnColorTheme); settings.setValue("musicDirectory", this->musicDirectory); settings.setValue("numQuestions", this->numQuestions); settings.setValue("difficultyLevel", this->difficultyLevel); settings.setValue("numPlayers", this->numPlayers); settings.setValue("playerNames", this->playerNames); qDebug("closeEvent: config saved"); event->accept(); }