1 /***************************************************************************
2  *   Copyright (C) 2008 by Max-Wilhelm Bruker                              *
3  *   brukie@gmx.net                                                        *
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  *   This program is distributed in the hope that it will be useful,       *
11  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
12  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
13  *   GNU General Public License for more details.                          *
14  *                                                                         *
15  *   You should have received a copy of the GNU General Public License     *
16  *   along with this program; if not, write to the                         *
17  *   Free Software Foundation, Inc.,                                       *
18  *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
19  ***************************************************************************/
20 #include "window_main.h"
21 
22 #include "carddatabase.h"
23 #include "dlg_connect.h"
24 #include "dlg_edit_tokens.h"
25 #include "dlg_forgotpasswordchallenge.h"
26 #include "dlg_forgotpasswordrequest.h"
27 #include "dlg_forgotpasswordreset.h"
28 #include "dlg_manage_sets.h"
29 #include "dlg_register.h"
30 #include "dlg_settings.h"
31 #include "dlg_tip_of_the_day.h"
32 #include "dlg_update.h"
33 #include "dlg_viewlog.h"
34 #include "localclient.h"
35 #include "localserver.h"
36 #include "localserverinterface.h"
37 #include "logger.h"
38 #include "main.h"
39 #include "pb/event_connection_closed.pb.h"
40 #include "pb/event_server_shutdown.pb.h"
41 #include "pb/game_replay.pb.h"
42 #include "pb/room_commands.pb.h"
43 #include "remoteclient.h"
44 #include "settingscache.h"
45 #include "tab_game.h"
46 #include "tab_supervisor.h"
47 #include "version_string.h"
48 
49 #include <QAction>
50 #include <QApplication>
51 #include <QCloseEvent>
52 #include <QDateTime>
53 #include <QDesktopServices>
54 #include <QFile>
55 #include <QFileDialog>
56 #include <QInputDialog>
57 #include <QMenu>
58 #include <QMenuBar>
59 #include <QMessageBox>
60 #include <QPixmapCache>
61 #include <QSystemTrayIcon>
62 #include <QThread>
63 #include <QTimer>
64 #include <QtConcurrent>
65 #include <QtNetwork>
66 
67 #define GITHUB_PAGES_URL "https://cockatrice.github.io"
68 #define GITHUB_CONTRIBUTORS_URL "https://github.com/Cockatrice/Cockatrice/graphs/contributors?type=c"
69 #define GITHUB_CONTRIBUTE_URL "https://github.com/Cockatrice/Cockatrice#cockatrice"
70 #define GITHUB_TRANSIFEX_TRANSLATORS_URL "https://github.com/Cockatrice/Cockatrice/wiki/Translator-Hall-of-Fame"
71 #define GITHUB_TRANSLATOR_FAQ_URL "https://github.com/Cockatrice/Cockatrice/wiki/Translation-FAQ"
72 #define GITHUB_ISSUES_URL "https://github.com/Cockatrice/Cockatrice/issues"
73 #define GITHUB_TROUBLESHOOTING_URL "https://github.com/Cockatrice/Cockatrice/wiki/Troubleshooting"
74 #define GITHUB_FAQ_URL "https://github.com/Cockatrice/Cockatrice/wiki/Frequently-Asked-Questions"
75 
76 const QString MainWindow::appName = "Cockatrice";
77 const QStringList MainWindow::fileNameFilters = QStringList() << QObject::tr("Cockatrice card database (*.xml)")
78                                                               << QObject::tr("All files (*.*)");
79 
updateTabMenu(const QList<QMenu * > & newMenuList)80 void MainWindow::updateTabMenu(const QList<QMenu *> &newMenuList)
81 {
82     for (auto &tabMenu : tabMenus)
83         menuBar()->removeAction(tabMenu->menuAction());
84     tabMenus = newMenuList;
85     for (auto &tabMenu : tabMenus)
86         menuBar()->insertMenu(helpMenu->menuAction(), tabMenu);
87 }
88 
processConnectionClosedEvent(const Event_ConnectionClosed & event)89 void MainWindow::processConnectionClosedEvent(const Event_ConnectionClosed &event)
90 {
91     client->disconnectFromServer();
92     QString reasonStr;
93     switch (event.reason()) {
94         case Event_ConnectionClosed::USER_LIMIT_REACHED:
95             reasonStr = tr("The server has reached its maximum user capacity, please check back later.");
96             break;
97         case Event_ConnectionClosed::TOO_MANY_CONNECTIONS:
98             reasonStr = tr("There are too many concurrent connections from your address.");
99             break;
100         case Event_ConnectionClosed::BANNED: {
101             reasonStr = tr("Banned by moderator");
102             if (event.has_end_time())
103                 reasonStr.append("\n" +
104                                  tr("Expected end time: %1").arg(QDateTime::fromTime_t(event.end_time()).toString()));
105             else
106                 reasonStr.append("\n" + tr("This ban lasts indefinitely."));
107             if (event.has_reason_str())
108                 reasonStr.append("\n\n" + QString::fromStdString(event.reason_str()));
109             break;
110         }
111         case Event_ConnectionClosed::SERVER_SHUTDOWN:
112             reasonStr = tr("Scheduled server shutdown.");
113             break;
114         case Event_ConnectionClosed::USERNAMEINVALID:
115             reasonStr = tr("Invalid username.");
116             break;
117         case Event_ConnectionClosed::LOGGEDINELSEWERE:
118             reasonStr = tr("You have been logged out due to logging in at another location.");
119             break;
120         default:
121             reasonStr = QString::fromStdString(event.reason_str());
122     }
123     QMessageBox::critical(this, tr("Connection closed"),
124                           tr("The server has terminated your connection.\nReason: %1").arg(reasonStr));
125 }
126 
processServerShutdownEvent(const Event_ServerShutdown & event)127 void MainWindow::processServerShutdownEvent(const Event_ServerShutdown &event)
128 {
129     serverShutdownMessageBox.setInformativeText(tr("The server is going to be restarted in %n minute(s).\nAll running "
130                                                    "games will be lost.\nReason for shutdown: %1",
131                                                    "", event.minutes())
132                                                     .arg(QString::fromStdString(event.reason())));
133     serverShutdownMessageBox.setIconPixmap(QPixmap("theme:cockatrice").scaled(64, 64));
134     serverShutdownMessageBox.setText(tr("Scheduled server shutdown"));
135     serverShutdownMessageBox.setWindowModality(Qt::ApplicationModal);
136     serverShutdownMessageBox.setVisible(true);
137 }
138 
statusChanged(ClientStatus _status)139 void MainWindow::statusChanged(ClientStatus _status)
140 {
141     setClientStatusTitle();
142     switch (_status) {
143         case StatusDisconnected:
144             tabSupervisor->stop();
145             aSinglePlayer->setEnabled(true);
146             aConnect->setEnabled(true);
147             aRegister->setEnabled(true);
148             aDisconnect->setEnabled(false);
149             break;
150         case StatusLoggingIn:
151             aSinglePlayer->setEnabled(false);
152             aConnect->setEnabled(false);
153             aRegister->setEnabled(false);
154             aDisconnect->setEnabled(true);
155             break;
156         case StatusConnecting:
157         case StatusRegistering:
158         case StatusLoggedIn:
159         default:
160             break;
161     }
162 }
163 
userInfoReceived(const ServerInfo_User & info)164 void MainWindow::userInfoReceived(const ServerInfo_User &info)
165 {
166     tabSupervisor->start(info);
167 }
168 
registerAccepted()169 void MainWindow::registerAccepted()
170 {
171     QMessageBox::information(this, tr("Success"), tr("Registration accepted.\nWill now login."));
172 }
173 
registerAcceptedNeedsActivate()174 void MainWindow::registerAcceptedNeedsActivate()
175 {
176     // nothing
177 }
178 
activateAccepted()179 void MainWindow::activateAccepted()
180 {
181     QMessageBox::information(this, tr("Success"), tr("Account activation accepted.\nWill now login."));
182 }
183 
184 // Actions
185 
actConnect()186 void MainWindow::actConnect()
187 {
188     dlgConnect = new DlgConnect(this);
189     connect(dlgConnect, SIGNAL(sigStartForgotPasswordRequest()), this, SLOT(actForgotPasswordRequest()));
190 
191     if (dlgConnect->exec()) {
192         client->connectToServer(dlgConnect->getHost(), static_cast<unsigned int>(dlgConnect->getPort()),
193                                 dlgConnect->getPlayerName(), dlgConnect->getPassword());
194     }
195 }
196 
actRegister()197 void MainWindow::actRegister()
198 {
199     DlgRegister dlg(this);
200     if (dlg.exec()) {
201         client->registerToServer(dlg.getHost(), static_cast<unsigned int>(dlg.getPort()), dlg.getPlayerName(),
202                                  dlg.getPassword(), dlg.getEmail(), dlg.getGender(), dlg.getCountry(),
203                                  dlg.getRealName());
204     }
205 }
206 
actDisconnect()207 void MainWindow::actDisconnect()
208 {
209     client->disconnectFromServer();
210 }
211 
actSinglePlayer()212 void MainWindow::actSinglePlayer()
213 {
214     bool ok;
215     int numberPlayers =
216         QInputDialog::getInt(this, tr("Number of players"), tr("Please enter the number of players."), 1, 1, 8, 1, &ok);
217     if (!ok)
218         return;
219 
220     aConnect->setEnabled(false);
221     aRegister->setEnabled(false);
222     aSinglePlayer->setEnabled(false);
223 
224     localServer = new LocalServer(this);
225     LocalServerInterface *mainLsi = localServer->newConnection();
226     LocalClient *mainClient =
227         new LocalClient(mainLsi, tr("Player %1").arg(1), SettingsCache::instance().getClientID(), this);
228     QList<AbstractClient *> localClients;
229     localClients.append(mainClient);
230 
231     for (int i = 0; i < numberPlayers - 1; ++i) {
232         LocalServerInterface *slaveLsi = localServer->newConnection();
233         LocalClient *slaveClient =
234             new LocalClient(slaveLsi, tr("Player %1").arg(i + 2), SettingsCache::instance().getClientID(), this);
235         localClients.append(slaveClient);
236     }
237     tabSupervisor->startLocal(localClients);
238 
239     Command_CreateGame createCommand;
240     createCommand.set_max_players(static_cast<google::protobuf::uint32>(numberPlayers));
241     mainClient->sendCommand(LocalClient::prepareRoomCommand(createCommand, 0));
242 }
243 
actWatchReplay()244 void MainWindow::actWatchReplay()
245 {
246     QFileDialog dlg(this, tr("Load replay"));
247     dlg.setDirectory(SettingsCache::instance().getReplaysPath());
248     dlg.setNameFilters(QStringList() << QObject::tr("Cockatrice replays (*.cor)"));
249     if (!dlg.exec())
250         return;
251 
252     QString fileName = dlg.selectedFiles().at(0);
253     QFile file(fileName);
254     if (!file.open(QIODevice::ReadOnly))
255         return;
256     QByteArray buf = file.readAll();
257     file.close();
258 
259     replay = new GameReplay;
260     replay->ParseFromArray(buf.data(), buf.size());
261 
262     tabSupervisor->openReplay(replay);
263 }
264 
localGameEnded()265 void MainWindow::localGameEnded()
266 {
267     delete localServer;
268     localServer = nullptr;
269 
270     aConnect->setEnabled(true);
271     aRegister->setEnabled(true);
272     aSinglePlayer->setEnabled(true);
273 }
274 
actDeckEditor()275 void MainWindow::actDeckEditor()
276 {
277     tabSupervisor->addDeckEditorTab(nullptr);
278 }
279 
actFullScreen(bool checked)280 void MainWindow::actFullScreen(bool checked)
281 {
282     if (checked)
283         setWindowState(windowState() | Qt::WindowFullScreen);
284     else
285         setWindowState(windowState() & ~Qt::WindowFullScreen);
286 }
287 
actSettings()288 void MainWindow::actSettings()
289 {
290     DlgSettings dlg(this);
291     dlg.exec();
292 }
293 
actExit()294 void MainWindow::actExit()
295 {
296     close();
297 }
298 
actAbout()299 void MainWindow::actAbout()
300 {
301     QMessageBox mb(
302         QMessageBox::NoIcon, tr("About Cockatrice"),
303         QString("<font size=\"8\"><b>Cockatrice</b></font> (" + QString::fromStdString(BUILD_ARCHITECTURE) + ")<br>" +
304                 tr("Version") + QString(" %1").arg(VERSION_STRING) + "<br><br><b><a href='" + GITHUB_PAGES_URL + "'>" +
305                 tr("Cockatrice Webpage") + "</a></b><br>" + "<br><b>" + tr("Project Manager:") +
306                 "</b><br>Zach Halpern<br><br>" + "<b>" + tr("Past Project Managers:") +
307                 "</b><br>Gavin Bisesi<br>Max-Wilhelm Bruker<br>Marcus Schütz<br><br>" + "<b>" + tr("Developers:") +
308                 "</b><br>" + "<a href='" + GITHUB_CONTRIBUTORS_URL + "'>" + tr("Our Developers") + "</a><br>" +
309                 "<a href='" + GITHUB_CONTRIBUTE_URL + "'>" + tr("Help Develop!") + "</a><br><br>" + "<b>" +
310                 tr("Translators:") + "</b><br>" + "<a href='" + GITHUB_TRANSIFEX_TRANSLATORS_URL + "'>" +
311                 tr("Our Translators") + "</a><br>" + "<a href='" + GITHUB_TRANSLATOR_FAQ_URL + "'>" +
312                 tr("Help Translate!") + "</a><br><br>" + "<b>" + tr("Support:") + "</b><br>" + "<a href='" +
313                 GITHUB_ISSUES_URL + "'>" + tr("Report an Issue") + "</a><br>" + "<a href='" +
314                 GITHUB_TROUBLESHOOTING_URL + "'>" + tr("Troubleshooting") + "</a><br>" + "<a href='" + GITHUB_FAQ_URL +
315                 "'>" + tr("F.A.Q.") + "</a><br>"),
316         QMessageBox::Ok, this);
317     mb.setIconPixmap(QPixmap("theme:cockatrice").scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation));
318     mb.setTextInteractionFlags(Qt::TextBrowserInteraction);
319     mb.exec();
320 }
321 
actTips()322 void MainWindow::actTips()
323 {
324     if (tip != nullptr) {
325         delete tip;
326         tip = nullptr;
327     }
328     tip = new DlgTipOfTheDay(this);
329     if (tip->successfulInit) {
330         tip->show();
331     }
332 }
333 
actUpdate()334 void MainWindow::actUpdate()
335 {
336     DlgUpdate dlg(this);
337     dlg.exec();
338 }
339 
actViewLog()340 void MainWindow::actViewLog()
341 {
342     if (logviewDialog == nullptr) {
343         logviewDialog = new DlgViewLog(this);
344     }
345 
346     logviewDialog->show();
347     logviewDialog->raise();
348     logviewDialog->activateWindow();
349 }
350 
serverTimeout()351 void MainWindow::serverTimeout()
352 {
353     QMessageBox::critical(this, tr("Error"), tr("Server timeout"));
354     actConnect();
355 }
356 
loginError(Response::ResponseCode r,QString reasonStr,quint32 endTime,QList<QString> missingFeatures)357 void MainWindow::loginError(Response::ResponseCode r,
358                             QString reasonStr,
359                             quint32 endTime,
360                             QList<QString> missingFeatures)
361 {
362     switch (r) {
363         case Response::RespClientUpdateRequired: {
364             QString formattedMissingFeatures;
365             formattedMissingFeatures = "Missing Features: ";
366             for (int i = 0; i < missingFeatures.size(); ++i)
367                 formattedMissingFeatures.append(QString("\n     %1").arg(QChar(0x2022)) + " " +
368                                                 missingFeatures.value(i));
369 
370             QMessageBox msgBox;
371             msgBox.setIcon(QMessageBox::Critical);
372             msgBox.setWindowTitle(tr("Failed Login"));
373             msgBox.setText(tr("Your client seems to be missing features this server requires for connection.") +
374                            "\n\n" + tr("To update your client, go to 'Help -> Check for Client Updates'."));
375             msgBox.setDetailedText(formattedMissingFeatures);
376             msgBox.exec();
377             break;
378         }
379         case Response::RespWrongPassword:
380             QMessageBox::critical(
381                 this, tr("Error"),
382                 tr("Incorrect username or password. Please check your authentication information and try again."));
383             break;
384         case Response::RespWouldOverwriteOldSession:
385             QMessageBox::critical(this, tr("Error"),
386                                   tr("There is already an active session using this user name.\nPlease close that "
387                                      "session first and re-login."));
388             break;
389         case Response::RespUserIsBanned: {
390             QString bannedStr;
391             if (endTime)
392                 bannedStr = tr("You are banned until %1.").arg(QDateTime::fromTime_t(endTime).toString());
393             else
394                 bannedStr = tr("You are banned indefinitely.");
395             if (!reasonStr.isEmpty())
396                 bannedStr.append("\n\n" + reasonStr);
397 
398             QMessageBox::critical(this, tr("Error"), bannedStr);
399             break;
400         }
401         case Response::RespUsernameInvalid: {
402             QMessageBox::critical(this, tr("Error"), extractInvalidUsernameMessage(reasonStr));
403             break;
404         }
405         case Response::RespRegistrationRequired:
406             if (QMessageBox::question(this, tr("Error"),
407                                       tr("This server requires user registration. Do you want to register now?"),
408                                       QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
409                 actRegister();
410             }
411             break;
412         case Response::RespClientIdRequired:
413             QMessageBox::critical(
414                 this, tr("Error"),
415                 tr("This server requires client IDs. Your client is either failing to generate an ID or you are "
416                    "running a modified client.\nPlease close and reopen your client to try again."));
417             break;
418         case Response::RespContextError:
419             QMessageBox::critical(this, tr("Error"),
420                                   tr("An internal error has occurred, please close and reopen Cockatrice before trying "
421                                      "again.\nIf the error persists, ensure you are running the latest version of the "
422                                      "software and if needed contact the software developers."));
423             break;
424         case Response::RespAccountNotActivated: {
425             bool ok = false;
426             QString token = QInputDialog::getText(this, tr("Account activation"),
427                                                   tr("Your account has not been activated yet.\nYou need to provide "
428                                                      "the activation token received in the activation email."),
429                                                   QLineEdit::Normal, QString(), &ok);
430             if (ok && !token.isEmpty()) {
431                 client->activateToServer(token);
432                 return;
433             }
434             client->disconnectFromServer();
435             break;
436         }
437         case Response::RespServerFull: {
438             QMessageBox::critical(this, tr("Server Full"),
439                                   tr("The server has reached its maximum user capacity, please check back later."));
440             break;
441         }
442         default:
443             QMessageBox::critical(this, tr("Error"),
444                                   tr("Unknown login error: %1").arg(static_cast<int>(r)) +
445                                       tr("\nThis usually means that your client version is out of date, and the server "
446                                          "sent a reply your client doesn't understand."));
447             break;
448     }
449     actConnect();
450 }
451 
extractInvalidUsernameMessage(QString & in)452 QString MainWindow::extractInvalidUsernameMessage(QString &in)
453 {
454     QString out = tr("Invalid username.") + "<br/>";
455     QStringList rules = in.split(QChar('|'));
456     if (rules.size() == 7 || rules.size() == 9) {
457         out += tr("Your username must respect these rules:") + "<ul>";
458 
459         out += "<li>" + tr("is %1 - %2 characters long").arg(rules.at(0)).arg(rules.at(1)) + "</li>";
460         out += "<li>" + tr("can %1 contain lowercase characters").arg((rules.at(2).toInt() > 0) ? "" : tr("NOT")) +
461                "</li>";
462         out += "<li>" + tr("can %1 contain uppercase characters").arg((rules.at(3).toInt() > 0) ? "" : tr("NOT")) +
463                "</li>";
464         out +=
465             "<li>" + tr("can %1 contain numeric characters").arg((rules.at(4).toInt() > 0) ? "" : tr("NOT")) + "</li>";
466 
467         if (rules.at(6).size() > 0)
468             out += "<li>" + tr("can contain the following punctuation: %1").arg(rules.at(6).toHtmlEscaped()) + "</li>";
469 
470         out += "<li>" +
471                tr("first character can %1 be a punctuation mark").arg((rules.at(5).toInt() > 0) ? "" : tr("NOT")) +
472                "</li>";
473 
474         if (rules.size() == 9) {
475             if (rules.at(7).size() > 0)
476                 out += "<li>" + tr("can not contain any of the following words: %1").arg(rules.at(7).toHtmlEscaped()) +
477                        "</li>";
478 
479             if (rules.at(8).size() > 0)
480                 out += "<li>" +
481                        tr("can not match any of the following expressions: %1").arg(rules.at(8).toHtmlEscaped()) +
482                        "</li>";
483         }
484 
485         out += "</ul>";
486     } else {
487         out += tr("You may only use A-Z, a-z, 0-9, _, ., and - in your username.");
488     }
489 
490     return out;
491 }
492 
registerError(Response::ResponseCode r,QString reasonStr,quint32 endTime)493 void MainWindow::registerError(Response::ResponseCode r, QString reasonStr, quint32 endTime)
494 {
495     switch (r) {
496         case Response::RespRegistrationDisabled:
497             QMessageBox::critical(this, tr("Registration denied"),
498                                   tr("Registration is currently disabled on this server"));
499             break;
500         case Response::RespUserAlreadyExists:
501             QMessageBox::critical(this, tr("Registration denied"),
502                                   tr("There is already an existing account with the same user name."));
503             break;
504         case Response::RespEmailRequiredToRegister:
505             QMessageBox::critical(this, tr("Registration denied"),
506                                   tr("It's mandatory to specify a valid email address when registering."));
507             break;
508         case Response::RespEmailBlackListed:
509             QMessageBox::critical(
510                 this, tr("Registration denied"),
511                 tr("The email address provider used during registration has been blacklisted for use on this server."));
512             break;
513         case Response::RespTooManyRequests:
514             QMessageBox::critical(
515                 this, tr("Registration denied"),
516                 tr("It appears you are attempting to register a new account on this server yet you already have an "
517                    "account registered with the email provided. This server restricts the number of accounts a user "
518                    "can register per address.  Please contact the server operator for further assistance or to obtain "
519                    "your credential information."));
520             break;
521         case Response::RespPasswordTooShort:
522             QMessageBox::critical(this, tr("Registration denied"), tr("Password too short."));
523             break;
524         case Response::RespUserIsBanned: {
525             QString bannedStr;
526             if (endTime)
527                 bannedStr = tr("You are banned until %1.").arg(QDateTime::fromTime_t(endTime).toString());
528             else
529                 bannedStr = tr("You are banned indefinitely.");
530             if (!reasonStr.isEmpty())
531                 bannedStr.append("\n\n" + reasonStr);
532 
533             QMessageBox::critical(this, tr("Error"), bannedStr);
534             break;
535         }
536         case Response::RespUsernameInvalid: {
537             QMessageBox::critical(this, tr("Error"), extractInvalidUsernameMessage(reasonStr));
538             break;
539         }
540         case Response::RespRegistrationFailed:
541             QMessageBox::critical(this, tr("Error"), tr("Registration failed for a technical problem on the server."));
542             break;
543         default:
544             QMessageBox::critical(this, tr("Error"),
545                                   tr("Unknown registration error: %1").arg(static_cast<int>(r)) +
546                                       tr("\nThis usually means that your client version is out of date, and the server "
547                                          "sent a reply your client doesn't understand."));
548     }
549     actRegister();
550 }
551 
activateError()552 void MainWindow::activateError()
553 {
554     QMessageBox::critical(this, tr("Error"), tr("Account activation failed"));
555     client->disconnectFromServer();
556     actConnect();
557 }
558 
socketError(const QString & errorStr)559 void MainWindow::socketError(const QString &errorStr)
560 {
561     QMessageBox::critical(this, tr("Error"), tr("Socket error: %1").arg(errorStr));
562     actConnect();
563 }
564 
protocolVersionMismatch(int localVersion,int remoteVersion)565 void MainWindow::protocolVersionMismatch(int localVersion, int remoteVersion)
566 {
567     if (localVersion > remoteVersion)
568         QMessageBox::critical(this, tr("Error"),
569                               tr("You are trying to connect to an obsolete server. Please downgrade your Cockatrice "
570                                  "version or connect to a suitable server.\nLocal version is %1, remote version is %2.")
571                                   .arg(localVersion)
572                                   .arg(remoteVersion));
573     else
574         QMessageBox::critical(this, tr("Error"),
575                               tr("Your Cockatrice client is obsolete. Please update your Cockatrice version.\nLocal "
576                                  "version is %1, remote version is %2.")
577                                   .arg(localVersion)
578                                   .arg(remoteVersion));
579 }
580 
setClientStatusTitle()581 void MainWindow::setClientStatusTitle()
582 {
583     switch (client->getStatus()) {
584         case StatusConnecting:
585             setWindowTitle(appName + " - " + tr("Connecting to %1...").arg(client->peerName()));
586             break;
587         case StatusRegistering:
588             setWindowTitle(appName + " - " +
589                            tr("Registering to %1 as %2...").arg(client->peerName()).arg(client->getUserName()));
590             break;
591         case StatusDisconnected:
592             setWindowTitle(appName + " - " + tr("Disconnected"));
593             break;
594         case StatusLoggingIn:
595             setWindowTitle(appName + " - " + tr("Connected, logging in at %1").arg(client->peerName()));
596             break;
597         case StatusLoggedIn:
598             setWindowTitle(client->getUserName() + "@" + client->peerName());
599             break;
600         case StatusRequestingForgotPassword:
601             setWindowTitle(
602                 appName + " - " +
603                 tr("Requesting forgotten password to %1 as %2...").arg(client->peerName()).arg(client->getUserName()));
604             break;
605         case StatusSubmitForgotPasswordChallenge:
606             setWindowTitle(
607                 appName + " - " +
608                 tr("Requesting forgotten password to %1 as %2...").arg(client->peerName()).arg(client->getUserName()));
609             break;
610         case StatusSubmitForgotPasswordReset:
611             setWindowTitle(
612                 appName + " - " +
613                 tr("Requesting forgotten password to %1 as %2...").arg(client->peerName()).arg(client->getUserName()));
614             break;
615         default:
616             setWindowTitle(appName);
617     }
618 }
619 
retranslateUi()620 void MainWindow::retranslateUi()
621 {
622     setClientStatusTitle();
623 
624     aConnect->setText(tr("&Connect..."));
625     aDisconnect->setText(tr("&Disconnect"));
626     aSinglePlayer->setText(tr("Start &local game..."));
627     aWatchReplay->setText(tr("&Watch replay..."));
628     aDeckEditor->setText(tr("&Deck editor"));
629     aFullScreen->setText(tr("&Full screen"));
630     aRegister->setText(tr("&Register to server..."));
631     aSettings->setText(tr("&Settings..."));
632     aSettings->setIcon(QPixmap("theme:icons/settings"));
633     aExit->setText(tr("&Exit"));
634 
635 #if defined(__APPLE__) /* For OSX */
636     cockatriceMenu->setTitle(tr("A&ctions"));
637 #else
638     cockatriceMenu->setTitle(tr("&Cockatrice"));
639 #endif
640 
641     dbMenu->setTitle(tr("C&ard Database"));
642     aManageSets->setText(tr("&Manage sets..."));
643     aEditTokens->setText(tr("Edit custom &tokens..."));
644     aOpenCustomFolder->setText(tr("Open custom image folder"));
645     aOpenCustomsetsFolder->setText(tr("Open custom sets folder"));
646     aAddCustomSet->setText(tr("Add custom sets/cards"));
647 
648     helpMenu->setTitle(tr("&Help"));
649     aAbout->setText(tr("&About Cockatrice"));
650     aTips->setText(tr("&Tip of the Day"));
651     aUpdate->setText(tr("Check for Client Updates"));
652     aCheckCardUpdates->setText(tr("Check for Card Updates..."));
653     aViewLog->setText(tr("View &Debug Log"));
654     tabSupervisor->retranslateUi();
655 }
656 
createActions()657 void MainWindow::createActions()
658 {
659     aConnect = new QAction(this);
660     connect(aConnect, SIGNAL(triggered()), this, SLOT(actConnect()));
661     aDisconnect = new QAction(this);
662     aDisconnect->setEnabled(false);
663     connect(aDisconnect, SIGNAL(triggered()), this, SLOT(actDisconnect()));
664     aSinglePlayer = new QAction(this);
665     connect(aSinglePlayer, SIGNAL(triggered()), this, SLOT(actSinglePlayer()));
666     aWatchReplay = new QAction(this);
667     connect(aWatchReplay, SIGNAL(triggered()), this, SLOT(actWatchReplay()));
668     aDeckEditor = new QAction(this);
669     connect(aDeckEditor, SIGNAL(triggered()), this, SLOT(actDeckEditor()));
670     aFullScreen = new QAction(this);
671     aFullScreen->setCheckable(true);
672     connect(aFullScreen, SIGNAL(toggled(bool)), this, SLOT(actFullScreen(bool)));
673     aRegister = new QAction(this);
674     connect(aRegister, SIGNAL(triggered()), this, SLOT(actRegister()));
675     aSettings = new QAction(this);
676     connect(aSettings, SIGNAL(triggered()), this, SLOT(actSettings()));
677     aExit = new QAction(this);
678     connect(aExit, SIGNAL(triggered()), this, SLOT(actExit()));
679 
680     aManageSets = new QAction(QString(), this);
681     connect(aManageSets, SIGNAL(triggered()), this, SLOT(actManageSets()));
682     aEditTokens = new QAction(QString(), this);
683     connect(aEditTokens, SIGNAL(triggered()), this, SLOT(actEditTokens()));
684     aOpenCustomFolder = new QAction(QString(), this);
685     connect(aOpenCustomFolder, SIGNAL(triggered()), this, SLOT(actOpenCustomFolder()));
686     aOpenCustomsetsFolder = new QAction(QString(), this);
687     connect(aOpenCustomsetsFolder, SIGNAL(triggered()), this, SLOT(actOpenCustomsetsFolder()));
688     aAddCustomSet = new QAction(QString(), this);
689     connect(aAddCustomSet, SIGNAL(triggered()), this, SLOT(actAddCustomSet()));
690 
691     aAbout = new QAction(this);
692     connect(aAbout, SIGNAL(triggered()), this, SLOT(actAbout()));
693     aTips = new QAction(this);
694     connect(aTips, SIGNAL(triggered()), this, SLOT(actTips()));
695     aUpdate = new QAction(this);
696     connect(aUpdate, SIGNAL(triggered()), this, SLOT(actUpdate()));
697     aCheckCardUpdates = new QAction(this);
698     connect(aCheckCardUpdates, SIGNAL(triggered()), this, SLOT(actCheckCardUpdates()));
699     aViewLog = new QAction(this);
700     connect(aViewLog, SIGNAL(triggered()), this, SLOT(actViewLog()));
701 
702 #if defined(__APPLE__) /* For OSX */
703     aSettings->setMenuRole(QAction::PreferencesRole);
704     aExit->setMenuRole(QAction::QuitRole);
705     aAbout->setMenuRole(QAction::AboutRole);
706 
707     Q_UNUSED(QT_TRANSLATE_NOOP("QMenuBar", "Services"));
708     Q_UNUSED(QT_TRANSLATE_NOOP("QMenuBar", "Hide %1"));
709     Q_UNUSED(QT_TRANSLATE_NOOP("QMenuBar", "Hide Others"));
710     Q_UNUSED(QT_TRANSLATE_NOOP("QMenuBar", "Show All"));
711     Q_UNUSED(QT_TRANSLATE_NOOP("QMenuBar", "Preferences..."));
712     Q_UNUSED(QT_TRANSLATE_NOOP("QMenuBar", "Quit %1"));
713     Q_UNUSED(QT_TRANSLATE_NOOP("QMenuBar", "About %1"));
714 #endif
715     // translate Qt's dialogs "default button text"; list taken from QPlatformTheme::defaultStandardButtonText()
716     Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "OK"));
717     Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Save"));
718     Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Save All"));
719     Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Open"));
720     Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "&Yes"));
721     Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Yes to &All"));
722     Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "&No"));
723     Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "N&o to All"));
724     Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Abort"));
725     Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Retry"));
726     Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Ignore"));
727     Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Close"));
728     Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Cancel"));
729     Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Discard"));
730     Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Help"));
731     Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Apply"));
732     Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Reset"));
733     Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Restore Defaults"));
734 }
735 
createMenus()736 void MainWindow::createMenus()
737 {
738     cockatriceMenu = menuBar()->addMenu(QString());
739     cockatriceMenu->addAction(aConnect);
740     cockatriceMenu->addAction(aDisconnect);
741     cockatriceMenu->addAction(aRegister);
742     cockatriceMenu->addSeparator();
743     cockatriceMenu->addAction(aSinglePlayer);
744     cockatriceMenu->addAction(aWatchReplay);
745     cockatriceMenu->addSeparator();
746     cockatriceMenu->addAction(aDeckEditor);
747     cockatriceMenu->addSeparator();
748     cockatriceMenu->addAction(aFullScreen);
749     cockatriceMenu->addSeparator();
750     cockatriceMenu->addAction(aSettings);
751     cockatriceMenu->addAction(aExit);
752 
753     dbMenu = menuBar()->addMenu(QString());
754     dbMenu->addAction(aManageSets);
755     dbMenu->addAction(aEditTokens);
756     dbMenu->addSeparator();
757     dbMenu->addAction(aOpenCustomFolder);
758     dbMenu->addAction(aOpenCustomsetsFolder);
759     dbMenu->addAction(aAddCustomSet);
760 
761     helpMenu = menuBar()->addMenu(QString());
762     helpMenu->addAction(aAbout);
763     helpMenu->addAction(aTips);
764     helpMenu->addSeparator();
765     helpMenu->addAction(aUpdate);
766     helpMenu->addAction(aCheckCardUpdates);
767     helpMenu->addSeparator();
768     helpMenu->addAction(aViewLog);
769 }
770 
MainWindow(QWidget * parent)771 MainWindow::MainWindow(QWidget *parent)
772     : QMainWindow(parent), localServer(nullptr), bHasActivated(false), askedForDbUpdater(false),
773       cardUpdateProcess(nullptr), logviewDialog(nullptr)
774 {
775     connect(&SettingsCache::instance(), SIGNAL(pixmapCacheSizeChanged(int)), this, SLOT(pixmapCacheSizeChanged(int)));
776     pixmapCacheSizeChanged(SettingsCache::instance().getPixmapCacheSize());
777 
778     client = new RemoteClient;
779     connect(client, SIGNAL(connectionClosedEventReceived(const Event_ConnectionClosed &)), this,
780             SLOT(processConnectionClosedEvent(const Event_ConnectionClosed &)));
781     connect(client, SIGNAL(serverShutdownEventReceived(const Event_ServerShutdown &)), this,
782             SLOT(processServerShutdownEvent(const Event_ServerShutdown &)));
783     connect(client, SIGNAL(loginError(Response::ResponseCode, QString, quint32, QList<QString>)), this,
784             SLOT(loginError(Response::ResponseCode, QString, quint32, QList<QString>)));
785     connect(client, SIGNAL(socketError(const QString &)), this, SLOT(socketError(const QString &)));
786     connect(client, SIGNAL(serverTimeout()), this, SLOT(serverTimeout()));
787     connect(client, SIGNAL(statusChanged(ClientStatus)), this, SLOT(statusChanged(ClientStatus)));
788     connect(client, SIGNAL(protocolVersionMismatch(int, int)), this, SLOT(protocolVersionMismatch(int, int)));
789     connect(client, SIGNAL(userInfoChanged(const ServerInfo_User &)), this,
790             SLOT(userInfoReceived(const ServerInfo_User &)), Qt::BlockingQueuedConnection);
791     connect(client, SIGNAL(notifyUserAboutUpdate()), this, SLOT(notifyUserAboutUpdate()));
792     connect(client, SIGNAL(registerAccepted()), this, SLOT(registerAccepted()));
793     connect(client, SIGNAL(registerAcceptedNeedsActivate()), this, SLOT(registerAcceptedNeedsActivate()));
794     connect(client, SIGNAL(registerError(Response::ResponseCode, QString, quint32)), this,
795             SLOT(registerError(Response::ResponseCode, QString, quint32)));
796     connect(client, SIGNAL(activateAccepted()), this, SLOT(activateAccepted()));
797     connect(client, SIGNAL(activateError()), this, SLOT(activateError()));
798     connect(client, SIGNAL(sigForgotPasswordSuccess()), this, SLOT(forgotPasswordSuccess()));
799     connect(client, SIGNAL(sigForgotPasswordError()), this, SLOT(forgotPasswordError()));
800     connect(client, SIGNAL(sigPromptForForgotPasswordReset()), this, SLOT(promptForgotPasswordReset()));
801     connect(client, SIGNAL(sigPromptForForgotPasswordChallenge()), this, SLOT(promptForgotPasswordChallenge()));
802 
803     clientThread = new QThread(this);
804     client->moveToThread(clientThread);
805     clientThread->start();
806 
807     createActions();
808     createMenus();
809 
810     tabSupervisor = new TabSupervisor(client);
811     connect(tabSupervisor, SIGNAL(setMenu(QList<QMenu *>)), this, SLOT(updateTabMenu(QList<QMenu *>)));
812     connect(tabSupervisor, SIGNAL(localGameEnded()), this, SLOT(localGameEnded()));
813     connect(tabSupervisor, SIGNAL(showWindowIfHidden()), this, SLOT(showWindowIfHidden()));
814     tabSupervisor->addDeckEditorTab(nullptr);
815 
816     setCentralWidget(tabSupervisor);
817 
818     retranslateUi();
819 
820     if (!restoreGeometry(SettingsCache::instance().getMainWindowGeometry())) {
821         setWindowState(Qt::WindowMaximized);
822     }
823     aFullScreen->setChecked(static_cast<bool>(windowState() & Qt::WindowFullScreen));
824 
825     if (QSystemTrayIcon::isSystemTrayAvailable()) {
826         createTrayActions();
827         createTrayIcon();
828     }
829 
830     connect(&SettingsCache::instance().shortcuts(), SIGNAL(shortCutChanged()), this, SLOT(refreshShortcuts()));
831     refreshShortcuts();
832 
833     connect(db, SIGNAL(cardDatabaseLoadingFailed()), this, SLOT(cardDatabaseLoadingFailed()));
834     connect(db, SIGNAL(cardDatabaseNewSetsFound(int, QStringList)), this,
835             SLOT(cardDatabaseNewSetsFound(int, QStringList)));
836     connect(db, SIGNAL(cardDatabaseAllNewSetsEnabled()), this, SLOT(cardDatabaseAllNewSetsEnabled()));
837 
838     tip = new DlgTipOfTheDay();
839 
840     // run startup check async
841     QTimer::singleShot(0, this, &MainWindow::startupConfigCheck);
842 }
843 
startupConfigCheck()844 void MainWindow::startupConfigCheck()
845 {
846     if (SettingsCache::instance().getClientVersion() == CLIENT_INFO_NOT_SET) {
847         // no config found, 99% new clean install
848         qDebug() << "Startup: old client version empty, assuming first start after clean install";
849         alertForcedOracleRun(VERSION_STRING, false);
850         SettingsCache::instance().setClientVersion(VERSION_STRING);
851     } else if (SettingsCache::instance().getClientVersion() != VERSION_STRING) {
852         // config found, from another (presumably older) version
853         qDebug() << "Startup: old client version" << SettingsCache::instance().getClientVersion()
854                  << "differs, assuming first start after update";
855         if (SettingsCache::instance().getNotifyAboutNewVersion()) {
856             alertForcedOracleRun(VERSION_STRING, true);
857         } else {
858             QtConcurrent::run(db, &CardDatabase::loadCardDatabases);
859         }
860         SettingsCache::instance().setClientVersion(VERSION_STRING);
861     } else {
862         // previous config from this version found
863         qDebug() << "Startup: found config with current version";
864         QtConcurrent::run(db, &CardDatabase::loadCardDatabases);
865 
866         // Run the tips dialog only on subsequent startups.
867         // On the first run after an install/update the startup is already crowded enough
868         if (tip->successfulInit && SettingsCache::instance().getShowTipsOnStartup() && tip->newTipsAvailable) {
869             tip->raise();
870             tip->show();
871         }
872     }
873 }
874 
alertForcedOracleRun(const QString & version,bool isUpdate)875 void MainWindow::alertForcedOracleRun(const QString &version, bool isUpdate)
876 {
877     if (isUpdate) {
878         QMessageBox::information(this, tr("New Version"),
879                                  tr("Congratulations on updating to Cockatrice %1!\n"
880                                     "Oracle will now launch to update your card database.")
881                                      .arg(version));
882     } else {
883         QMessageBox::information(this, tr("Cockatrice installed"),
884                                  tr("Congratulations on installing Cockatrice %1!\n"
885                                     "Oracle will now launch to install the initial card database.")
886                                      .arg(version));
887     }
888 
889     actCheckCardUpdates();
890     actCheckServerUpdates();
891 }
892 
~MainWindow()893 MainWindow::~MainWindow()
894 {
895     if (tip != nullptr) {
896         delete tip;
897         tip = nullptr;
898     }
899     if (trayIcon) {
900         trayIcon->hide();
901         trayIcon->deleteLater();
902     }
903 
904     client->deleteLater();
905     clientThread->wait();
906 }
907 
createTrayIcon()908 void MainWindow::createTrayIcon()
909 {
910     trayIconMenu = new QMenu(this);
911     trayIconMenu->addAction(closeAction);
912 
913     trayIcon = new QSystemTrayIcon(this);
914     trayIcon->setContextMenu(trayIconMenu);
915     trayIcon->setIcon(QPixmap("theme:cockatrice"));
916     trayIcon->show();
917 
918     connect(trayIcon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), this,
919             SLOT(iconActivated(QSystemTrayIcon::ActivationReason)));
920 }
921 
iconActivated(QSystemTrayIcon::ActivationReason reason)922 void MainWindow::iconActivated(QSystemTrayIcon::ActivationReason reason)
923 {
924     if (reason == QSystemTrayIcon::DoubleClick) {
925         if (windowState() != Qt::WindowMinimized && windowState() != Qt::WindowMinimized + Qt::WindowMaximized)
926             showMinimized();
927         else {
928             showNormal();
929             QApplication::setActiveWindow(this);
930         }
931     }
932 }
933 
promptForgotPasswordChallenge()934 void MainWindow::promptForgotPasswordChallenge()
935 {
936     DlgForgotPasswordChallenge dlg(this);
937     if (dlg.exec())
938         client->submitForgotPasswordChallengeToServer(dlg.getHost(), static_cast<unsigned int>(dlg.getPort()),
939                                                       dlg.getPlayerName(), dlg.getEmail());
940 }
941 
createTrayActions()942 void MainWindow::createTrayActions()
943 {
944     closeAction = new QAction(tr("&Exit"), this);
945     connect(closeAction, SIGNAL(triggered()), this, SLOT(close()));
946 }
947 
closeEvent(QCloseEvent * event)948 void MainWindow::closeEvent(QCloseEvent *event)
949 {
950     // workaround Qt bug where closeEvent gets called twice
951     static bool bClosingDown = false;
952     if (bClosingDown)
953         return;
954     bClosingDown = true;
955 
956     if (!tabSupervisor->closeRequest()) {
957         event->ignore();
958         bClosingDown = false;
959         return;
960     }
961     tip->close();
962 
963     event->accept();
964     SettingsCache::instance().setMainWindowGeometry(saveGeometry());
965     tabSupervisor->deleteLater();
966 }
967 
changeEvent(QEvent * event)968 void MainWindow::changeEvent(QEvent *event)
969 {
970     if (event->type() == QEvent::LanguageChange)
971         retranslateUi();
972     else if (event->type() == QEvent::ActivationChange) {
973         if (isActiveWindow() && !bHasActivated) {
974             bHasActivated = true;
975             if (!connectTo.isEmpty()) {
976                 qDebug() << "Command line connect to " << connectTo;
977                 client->connectToServer(connectTo.host(), connectTo.port(), connectTo.userName(), connectTo.password());
978             } else if (SettingsCache::instance().servers().getAutoConnect()) {
979                 qDebug() << "Attempting auto-connect...";
980                 DlgConnect dlg(this);
981                 client->connectToServer(dlg.getHost(), static_cast<unsigned int>(dlg.getPort()), dlg.getPlayerName(),
982                                         dlg.getPassword());
983             }
984         }
985     }
986 
987     QMainWindow::changeEvent(event);
988 }
989 
pixmapCacheSizeChanged(int newSizeInMBs)990 void MainWindow::pixmapCacheSizeChanged(int newSizeInMBs)
991 {
992     // qDebug() << "Setting pixmap cache size to " << value << " MBs";
993     // translate MBs to KBs
994     QPixmapCache::setCacheLimit(newSizeInMBs * 1024);
995 }
996 
showWindowIfHidden()997 void MainWindow::showWindowIfHidden()
998 {
999     // keep the previous window state
1000     setWindowState(windowState() & ~Qt::WindowMinimized);
1001     show();
1002 }
1003 
cardDatabaseLoadingFailed()1004 void MainWindow::cardDatabaseLoadingFailed()
1005 {
1006     if (askedForDbUpdater) {
1007         return;
1008     }
1009     askedForDbUpdater = true;
1010     QMessageBox msgBox;
1011     msgBox.setWindowTitle(tr("Card database"));
1012     msgBox.setIcon(QMessageBox::Question);
1013     msgBox.setText(tr("Cockatrice is unable to load the card database.\n"
1014                       "Do you want to update your card database now?\n"
1015                       "If unsure or first time user, choose \"Yes\""));
1016 
1017     QPushButton *yesButton = msgBox.addButton(tr("Yes"), QMessageBox::YesRole);
1018     msgBox.addButton(tr("No"), QMessageBox::NoRole);
1019     QPushButton *settingsButton = msgBox.addButton(tr("Open settings"), QMessageBox::ActionRole);
1020     msgBox.setDefaultButton(yesButton);
1021 
1022     msgBox.exec();
1023 
1024     if (msgBox.clickedButton() == yesButton) {
1025         actCheckCardUpdates();
1026     } else if (msgBox.clickedButton() == settingsButton) {
1027         actSettings();
1028     }
1029 }
1030 
cardDatabaseNewSetsFound(int numUnknownSets,QStringList unknownSetsNames)1031 void MainWindow::cardDatabaseNewSetsFound(int numUnknownSets, QStringList unknownSetsNames)
1032 {
1033     QMessageBox msgBox;
1034     msgBox.setWindowTitle(tr("New sets found"));
1035     msgBox.setIcon(QMessageBox::Question);
1036     msgBox.setText(tr("%n new set(s) found in the card database\n"
1037                       "Set code(s): %1\n"
1038                       "Do you want to enable it/them?",
1039                       "", numUnknownSets)
1040                        .arg(unknownSetsNames.join(", ")));
1041 
1042     QPushButton *yesButton = msgBox.addButton(tr("Yes"), QMessageBox::YesRole);
1043     QPushButton *noButton = msgBox.addButton(tr("No"), QMessageBox::NoRole);
1044     QPushButton *settingsButton = msgBox.addButton(tr("View sets"), QMessageBox::ActionRole);
1045     msgBox.setDefaultButton(yesButton);
1046 
1047     msgBox.exec();
1048 
1049     if (msgBox.clickedButton() == yesButton) {
1050         db->enableAllUnknownSets();
1051         QtConcurrent::run(db, &CardDatabase::loadCardDatabases);
1052     } else if (msgBox.clickedButton() == noButton) {
1053         db->markAllSetsAsKnown();
1054     } else if (msgBox.clickedButton() == settingsButton) {
1055         db->markAllSetsAsKnown();
1056         actManageSets();
1057     }
1058 }
1059 
cardDatabaseAllNewSetsEnabled()1060 void MainWindow::cardDatabaseAllNewSetsEnabled()
1061 {
1062     QMessageBox::information(
1063         this, tr("Welcome"),
1064         tr("Hi! It seems like you're running this version of Cockatrice for the first time.\nAll the sets in the card "
1065            "database have been enabled.\nRead more about changing the set order or disabling specific sets and "
1066            "consequent effects in the \"Manage Sets\" dialog."));
1067     actManageSets();
1068 }
1069 
1070 /* CARD UPDATER */
actCheckCardUpdates()1071 void MainWindow::actCheckCardUpdates()
1072 {
1073     if (cardUpdateProcess) {
1074         QMessageBox::information(this, tr("Information"), tr("A card database update is already running."));
1075         return;
1076     }
1077 
1078     cardUpdateProcess = new QProcess(this);
1079     connect(cardUpdateProcess, SIGNAL(error(QProcess::ProcessError)), this,
1080             SLOT(cardUpdateError(QProcess::ProcessError)));
1081     connect(cardUpdateProcess, SIGNAL(finished(int, QProcess::ExitStatus)), this,
1082             SLOT(cardUpdateFinished(int, QProcess::ExitStatus)));
1083 
1084     // full "run the update" command; leave empty if not present
1085     QString updaterCmd;
1086     QString binaryName;
1087     QDir dir = QDir(QApplication::applicationDirPath());
1088 
1089 #if defined(Q_OS_MAC)
1090     /*
1091      * bypass app translocation: quarantined application will be started from a temporary directory eg.
1092      * /private/var/folders/tk/qx76cyb50jn5dvj7rrgfscz40000gn/T/AppTranslocation/A0CBBD5A-9264-4106-8547-36B84DB161E2/d/oracle/
1093      */
1094     if (dir.absolutePath().startsWith("/private/var/folders")) {
1095         dir.setPath("/Applications/");
1096     } else {
1097         // exit from the Cockatrice application bundle
1098         dir.cdUp();
1099         dir.cdUp();
1100         dir.cdUp();
1101     }
1102 
1103     binaryName = getCardUpdaterBinaryName();
1104 
1105     dir.cd(binaryName + ".app");
1106     dir.cd("Contents");
1107     dir.cd("MacOS");
1108 #elif defined(Q_OS_WIN)
1109     binaryName = getCardUpdaterBinaryName() + ".exe";
1110 #else
1111     binaryName = getCardUpdaterBinaryName();
1112 #endif
1113 
1114     if (dir.exists(binaryName)) {
1115         updaterCmd = dir.absoluteFilePath(binaryName);
1116     } else { // try and find the directory oracle is stored in the build directory
1117         QDir findLocalDir(dir);
1118         findLocalDir.cdUp();
1119         findLocalDir.cd(getCardUpdaterBinaryName());
1120         if (findLocalDir.exists(binaryName)) {
1121             dir = findLocalDir;
1122             updaterCmd = dir.absoluteFilePath(binaryName);
1123         }
1124     }
1125 
1126     if (updaterCmd.isEmpty()) {
1127         QMessageBox::warning(this, tr("Error"),
1128                              tr("Unable to run the card database updater: ") + dir.absoluteFilePath(binaryName));
1129         exitCardDatabaseUpdate();
1130         return;
1131     }
1132 
1133     cardUpdateProcess->start(updaterCmd, QStringList());
1134 }
1135 
exitCardDatabaseUpdate()1136 void MainWindow::exitCardDatabaseUpdate()
1137 {
1138     cardUpdateProcess->deleteLater();
1139     cardUpdateProcess = nullptr;
1140 
1141     QtConcurrent::run(db, &CardDatabase::loadCardDatabases);
1142 }
1143 
cardUpdateError(QProcess::ProcessError err)1144 void MainWindow::cardUpdateError(QProcess::ProcessError err)
1145 {
1146     QString error;
1147     switch (err) {
1148         case QProcess::FailedToStart:
1149             error = tr("failed to start.");
1150             break;
1151         case QProcess::Crashed:
1152             error = tr("crashed.");
1153             break;
1154         case QProcess::Timedout:
1155             error = tr("timed out.");
1156             break;
1157         case QProcess::WriteError:
1158             error = tr("write error.");
1159             break;
1160         case QProcess::ReadError:
1161             error = tr("read error.");
1162             break;
1163         case QProcess::UnknownError:
1164         default:
1165             error = tr("unknown error.");
1166             break;
1167     }
1168 
1169     exitCardDatabaseUpdate();
1170     QMessageBox::warning(this, tr("Error"), tr("The card database updater exited with an error: %1").arg(error));
1171 }
1172 
cardUpdateFinished(int,QProcess::ExitStatus)1173 void MainWindow::cardUpdateFinished(int, QProcess::ExitStatus)
1174 {
1175     exitCardDatabaseUpdate();
1176 }
1177 
actCheckServerUpdates()1178 void MainWindow::actCheckServerUpdates()
1179 {
1180     auto hps = new HandlePublicServers(this);
1181     hps->downloadPublicServers();
1182     connect(hps, &HandlePublicServers::sigPublicServersDownloadedSuccessfully, [=]() { hps->deleteLater(); });
1183 }
1184 
refreshShortcuts()1185 void MainWindow::refreshShortcuts()
1186 {
1187     ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts();
1188     aConnect->setShortcuts(shortcuts.getShortcut("MainWindow/aConnect"));
1189     aDisconnect->setShortcuts(shortcuts.getShortcut("MainWindow/aDisconnect"));
1190     aSinglePlayer->setShortcuts(shortcuts.getShortcut("MainWindow/aSinglePlayer"));
1191     aWatchReplay->setShortcuts(shortcuts.getShortcut("MainWindow/aWatchReplay"));
1192     aDeckEditor->setShortcuts(shortcuts.getShortcut("MainWindow/aDeckEditor"));
1193     aFullScreen->setShortcuts(shortcuts.getShortcut("MainWindow/aFullScreen"));
1194     aRegister->setShortcuts(shortcuts.getShortcut("MainWindow/aRegister"));
1195     aSettings->setShortcuts(shortcuts.getShortcut("MainWindow/aSettings"));
1196     aExit->setShortcuts(shortcuts.getShortcut("MainWindow/aExit"));
1197     aManageSets->setShortcuts(shortcuts.getShortcut("MainWindow/aManageSets"));
1198     aEditTokens->setShortcuts(shortcuts.getShortcut("MainWindow/aEditTokens"));
1199     aOpenCustomFolder->setShortcuts(shortcuts.getShortcut("MainWindow/aOpenCustomFolder"));
1200     aCheckCardUpdates->setShortcuts(shortcuts.getShortcut("MainWindow/aCheckCardUpdates"));
1201 }
1202 
notifyUserAboutUpdate()1203 void MainWindow::notifyUserAboutUpdate()
1204 {
1205     QMessageBox::information(
1206         this, tr("Information"),
1207         tr("This server supports additional features that your client doesn't have.\nThis is most likely not a "
1208            "problem, but this message might mean there is a new version of Cockatrice available or this server is "
1209            "running a custom or pre-release version.\n\nTo update your client, go to Help -> Check for Updates."));
1210 }
1211 
actOpenCustomFolder()1212 void MainWindow::actOpenCustomFolder()
1213 {
1214     QString dir = SettingsCache::instance().getCustomPicsPath();
1215     QDesktopServices::openUrl(QUrl::fromLocalFile(dir));
1216 }
1217 
actOpenCustomsetsFolder()1218 void MainWindow::actOpenCustomsetsFolder()
1219 {
1220     QString dir = SettingsCache::instance().getCustomCardDatabasePath();
1221     QDesktopServices::openUrl(QUrl::fromLocalFile(dir));
1222 }
1223 
actAddCustomSet()1224 void MainWindow::actAddCustomSet()
1225 {
1226     QFileDialog dialog(this, tr("Load sets/cards"), QDir::homePath());
1227     dialog.setNameFilters(MainWindow::fileNameFilters);
1228     if (!dialog.exec()) {
1229         return;
1230     }
1231 
1232     QString fullFilePath = dialog.selectedFiles().at(0);
1233 
1234     if (!QFile::exists(fullFilePath)) {
1235         QMessageBox::warning(this, tr("Load sets/cards"), tr("Selected file cannot be found."));
1236         return;
1237     }
1238 
1239     if (QFileInfo(fullFilePath).suffix() != "xml") // fileName = *.xml
1240     {
1241         QMessageBox::warning(this, tr("Load sets/cards"), tr("You can only import XML databases at this time."));
1242         return;
1243     }
1244 
1245     QDir dir = SettingsCache::instance().getCustomCardDatabasePath();
1246     int nextPrefix = getNextCustomSetPrefix(dir);
1247 
1248     bool res;
1249 
1250     QString fileName = QFileInfo(fullFilePath).fileName();
1251     if (fileName.compare("spoiler.xml", Qt::CaseInsensitive) == 0) {
1252         /*
1253          * If the file being added is "spoiler.xml"
1254          * then we'll want to overwrite the old version
1255          * and replace it with the new one
1256          */
1257         if (QFile::exists(dir.absolutePath() + "/spoiler.xml")) {
1258             QFile::remove(dir.absolutePath() + "/spoiler.xml");
1259         }
1260 
1261         res = QFile::copy(fullFilePath, dir.absolutePath() + "/spoiler.xml");
1262     } else {
1263         res = QFile::copy(fullFilePath, dir.absolutePath() + "/" + (nextPrefix > 9 ? "" : "0") +
1264                                             QString::number(nextPrefix) + "." + fileName);
1265     }
1266 
1267     if (res) {
1268         QMessageBox::information(
1269             this, tr("Load sets/cards"),
1270             tr("The new sets/cards have been added successfully.\nCockatrice will now reload the card database."));
1271         QtConcurrent::run(db, &CardDatabase::loadCardDatabases);
1272     } else {
1273         QMessageBox::warning(this, tr("Load sets/cards"), tr("Sets/cards failed to import."));
1274     }
1275 }
1276 
getNextCustomSetPrefix(QDir dataDir)1277 int MainWindow::getNextCustomSetPrefix(QDir dataDir)
1278 {
1279     QStringList files = dataDir.entryList();
1280     int maxIndex = 0;
1281 
1282     QStringList::const_iterator filesIterator;
1283     for (filesIterator = files.constBegin(); filesIterator != files.constEnd(); ++filesIterator) {
1284         int fileIndex = (*filesIterator).split(".").at(0).toInt();
1285         if (fileIndex > maxIndex)
1286             maxIndex = fileIndex;
1287     }
1288 
1289     return maxIndex + 1;
1290 }
1291 
actManageSets()1292 void MainWindow::actManageSets()
1293 {
1294     wndSets = new WndSets(this);
1295     wndSets->show();
1296 }
1297 
actEditTokens()1298 void MainWindow::actEditTokens()
1299 {
1300     DlgEditTokens dlg(this);
1301     dlg.exec();
1302     db->saveCustomTokensToFile();
1303 }
1304 
actForgotPasswordRequest()1305 void MainWindow::actForgotPasswordRequest()
1306 {
1307     DlgForgotPasswordRequest dlg(this);
1308     if (dlg.exec())
1309         client->requestForgotPasswordToServer(dlg.getHost(), static_cast<unsigned int>(dlg.getPort()),
1310                                               dlg.getPlayerName());
1311 }
1312 
forgotPasswordSuccess()1313 void MainWindow::forgotPasswordSuccess()
1314 {
1315     QMessageBox::information(
1316         this, tr("Forgot Password"),
1317         tr("Your password has been reset successfully, you can now log in using the new credentials."));
1318     SettingsCache::instance().servers().setFPHostName("");
1319     SettingsCache::instance().servers().setFPPort("");
1320     SettingsCache::instance().servers().setFPPlayerName("");
1321 }
1322 
forgotPasswordError()1323 void MainWindow::forgotPasswordError()
1324 {
1325     QMessageBox::warning(
1326         this, tr("Forgot Password"),
1327         tr("Failed to reset user account password, please contact the server operator to reset your password."));
1328     SettingsCache::instance().servers().setFPHostName("");
1329     SettingsCache::instance().servers().setFPPort("");
1330     SettingsCache::instance().servers().setFPPlayerName("");
1331 }
1332 
promptForgotPasswordReset()1333 void MainWindow::promptForgotPasswordReset()
1334 {
1335     QMessageBox::information(this, tr("Forgot Password"),
1336                              tr("Activation request received, please check your email for an activation token."));
1337     DlgForgotPasswordReset dlg(this);
1338     if (dlg.exec()) {
1339         client->submitForgotPasswordResetToServer(dlg.getHost(), static_cast<unsigned int>(dlg.getPort()),
1340                                                   dlg.getPlayerName(), dlg.getToken(), dlg.getPassword());
1341     }
1342 }
1343