1 #include "dlg_settings.h"
2 
3 #include "carddatabase.h"
4 #include "main.h"
5 #include "releasechannel.h"
6 #include "sequenceEdit/sequenceedit.h"
7 #include "settingscache.h"
8 #include "soundengine.h"
9 #include "spoilerbackgroundupdater.h"
10 #include "thememanager.h"
11 
12 #include <QAction>
13 #include <QApplication>
14 #include <QCheckBox>
15 #include <QCloseEvent>
16 #include <QComboBox>
17 #include <QDebug>
18 #include <QDesktopWidget>
19 #include <QDialogButtonBox>
20 #include <QFileDialog>
21 #include <QGridLayout>
22 #include <QGroupBox>
23 #include <QHeaderView>
24 #include <QInputDialog>
25 #include <QLabel>
26 #include <QLineEdit>
27 #include <QListWidget>
28 #include <QMessageBox>
29 #include <QPushButton>
30 #include <QRadioButton>
31 #include <QScreen>
32 #include <QSlider>
33 #include <QSpinBox>
34 #include <QStackedWidget>
35 #include <QToolBar>
36 #include <QTranslator>
37 #include <QTreeWidget>
38 #include <QTreeWidgetItem>
39 
40 #define WIKI_CUSTOM_PIC_URL "https://github.com/Cockatrice/Cockatrice/wiki/Custom-Picture-Download-URLs"
41 #define WIKI_CUSTOM_SHORTCUTS "https://github.com/Cockatrice/Cockatrice/wiki/Custom-Keyboard-Shortcuts"
42 
GeneralSettingsPage()43 GeneralSettingsPage::GeneralSettingsPage()
44 {
45     QString setLanguage = SettingsCache::instance().getLang();
46     QStringList qmFiles = findQmFiles();
47     for (int i = 0; i < qmFiles.size(); i++) {
48         QString langName = languageName(qmFiles[i]);
49         languageBox.addItem(langName, qmFiles[i]);
50         if ((qmFiles[i] == setLanguage) ||
51             (setLanguage.isEmpty() && langName == QCoreApplication::translate("i18n", DEFAULT_LANG_NAME)))
52             languageBox.setCurrentIndex(i);
53     }
54 
55     // updates
56     QList<ReleaseChannel *> channels = SettingsCache::instance().getUpdateReleaseChannels();
57     foreach (ReleaseChannel *chan, channels) {
58         updateReleaseChannelBox.insertItem(chan->getIndex(), tr(chan->getName().toUtf8()));
59     }
60     updateReleaseChannelBox.setCurrentIndex(SettingsCache::instance().getUpdateReleaseChannel()->getIndex());
61 
62     updateNotificationCheckBox.setChecked(SettingsCache::instance().getNotifyAboutUpdates());
63     newVersionOracleCheckBox.setChecked(SettingsCache::instance().getNotifyAboutNewVersion());
64 
65     // pixmap cache
66     pixmapCacheEdit.setMinimum(PIXMAPCACHE_SIZE_MIN);
67     // 2047 is the max value to avoid overflowing of QPixmapCache::setCacheLimit(int size)
68     pixmapCacheEdit.setMaximum(PIXMAPCACHE_SIZE_MAX);
69     pixmapCacheEdit.setSingleStep(64);
70     pixmapCacheEdit.setValue(SettingsCache::instance().getPixmapCacheSize());
71     pixmapCacheEdit.setSuffix(" MB");
72 
73     showTipsOnStartup.setChecked(SettingsCache::instance().getShowTipsOnStartup());
74 
75     connect(&languageBox, SIGNAL(currentIndexChanged(int)), this, SLOT(languageBoxChanged(int)));
76     connect(&pixmapCacheEdit, SIGNAL(valueChanged(int)), &SettingsCache::instance(), SLOT(setPixmapCacheSize(int)));
77     connect(&updateReleaseChannelBox, SIGNAL(currentIndexChanged(int)), &SettingsCache::instance(),
78             SLOT(setUpdateReleaseChannel(int)));
79     connect(&updateNotificationCheckBox, SIGNAL(stateChanged(int)), &SettingsCache::instance(),
80             SLOT(setNotifyAboutUpdate(int)));
81     connect(&newVersionOracleCheckBox, SIGNAL(stateChanged(int)), &SettingsCache::instance(),
82             SLOT(setNotifyAboutNewVersion(int)));
83     connect(&showTipsOnStartup, SIGNAL(clicked(bool)), &SettingsCache::instance(), SLOT(setShowTipsOnStartup(bool)));
84 
85     auto *personalGrid = new QGridLayout;
86     personalGrid->addWidget(&languageLabel, 0, 0);
87     personalGrid->addWidget(&languageBox, 0, 1);
88     personalGrid->addWidget(&updateReleaseChannelLabel, 1, 0);
89     personalGrid->addWidget(&updateReleaseChannelBox, 1, 1);
90     personalGrid->addWidget(&pixmapCacheLabel, 2, 0);
91     personalGrid->addWidget(&pixmapCacheEdit, 2, 1);
92     personalGrid->addWidget(&updateNotificationCheckBox, 3, 0, 1, 2);
93     personalGrid->addWidget(&newVersionOracleCheckBox, 4, 0, 1, 2);
94     personalGrid->addWidget(&showTipsOnStartup, 5, 0, 1, 2);
95 
96     personalGroupBox = new QGroupBox;
97     personalGroupBox->setLayout(personalGrid);
98 
99     deckPathEdit = new QLineEdit(SettingsCache::instance().getDeckPath());
100     deckPathEdit->setReadOnly(true);
101     QPushButton *deckPathButton = new QPushButton("...");
102     connect(deckPathButton, SIGNAL(clicked()), this, SLOT(deckPathButtonClicked()));
103 
104     replaysPathEdit = new QLineEdit(SettingsCache::instance().getReplaysPath());
105     replaysPathEdit->setReadOnly(true);
106     QPushButton *replaysPathButton = new QPushButton("...");
107     connect(replaysPathButton, SIGNAL(clicked()), this, SLOT(replaysPathButtonClicked()));
108 
109     picsPathEdit = new QLineEdit(SettingsCache::instance().getPicsPath());
110     picsPathEdit->setReadOnly(true);
111     QPushButton *picsPathButton = new QPushButton("...");
112     connect(picsPathButton, SIGNAL(clicked()), this, SLOT(picsPathButtonClicked()));
113 
114     cardDatabasePathEdit = new QLineEdit(SettingsCache::instance().getCardDatabasePath());
115     cardDatabasePathEdit->setReadOnly(true);
116     QPushButton *cardDatabasePathButton = new QPushButton("...");
117     connect(cardDatabasePathButton, SIGNAL(clicked()), this, SLOT(cardDatabasePathButtonClicked()));
118 
119     customCardDatabasePathEdit = new QLineEdit(SettingsCache::instance().getCustomCardDatabasePath());
120     customCardDatabasePathEdit->setReadOnly(true);
121     QPushButton *customCardDatabasePathButton = new QPushButton("...");
122     connect(customCardDatabasePathButton, SIGNAL(clicked()), this, SLOT(customCardDatabaseButtonClicked()));
123 
124     tokenDatabasePathEdit = new QLineEdit(SettingsCache::instance().getTokenDatabasePath());
125     tokenDatabasePathEdit->setReadOnly(true);
126     QPushButton *tokenDatabasePathButton = new QPushButton("...");
127     connect(tokenDatabasePathButton, SIGNAL(clicked()), this, SLOT(tokenDatabasePathButtonClicked()));
128 
129     if (SettingsCache::instance().getIsPortableBuild()) {
130         deckPathEdit->setEnabled(false);
131         replaysPathEdit->setEnabled(false);
132         picsPathEdit->setEnabled(false);
133         cardDatabasePathEdit->setEnabled(false);
134         customCardDatabasePathEdit->setEnabled(false);
135         tokenDatabasePathEdit->setEnabled(false);
136 
137         deckPathButton->setVisible(false);
138         replaysPathButton->setVisible(false);
139         picsPathButton->setVisible(false);
140         cardDatabasePathButton->setVisible(false);
141         customCardDatabasePathEdit->setVisible(false);
142         tokenDatabasePathButton->setVisible(false);
143     }
144 
145     auto *pathsGrid = new QGridLayout;
146     pathsGrid->addWidget(&deckPathLabel, 0, 0);
147     pathsGrid->addWidget(deckPathEdit, 0, 1);
148     pathsGrid->addWidget(deckPathButton, 0, 2);
149     pathsGrid->addWidget(&replaysPathLabel, 1, 0);
150     pathsGrid->addWidget(replaysPathEdit, 1, 1);
151     pathsGrid->addWidget(replaysPathButton, 1, 2);
152     pathsGrid->addWidget(&picsPathLabel, 2, 0);
153     pathsGrid->addWidget(picsPathEdit, 2, 1);
154     pathsGrid->addWidget(picsPathButton, 2, 2);
155     pathsGrid->addWidget(&cardDatabasePathLabel, 3, 0);
156     pathsGrid->addWidget(cardDatabasePathEdit, 3, 1);
157     pathsGrid->addWidget(cardDatabasePathButton, 3, 2);
158     pathsGrid->addWidget(&customCardDatabasePathLabel, 4, 0);
159     pathsGrid->addWidget(customCardDatabasePathEdit, 4, 1);
160     pathsGrid->addWidget(customCardDatabasePathButton, 4, 2);
161     pathsGrid->addWidget(&tokenDatabasePathLabel, 5, 0);
162     pathsGrid->addWidget(tokenDatabasePathEdit, 5, 1);
163     pathsGrid->addWidget(tokenDatabasePathButton, 5, 2);
164     pathsGroupBox = new QGroupBox;
165     pathsGroupBox->setLayout(pathsGrid);
166 
167     auto *mainLayout = new QVBoxLayout;
168     mainLayout->addWidget(personalGroupBox);
169     mainLayout->addWidget(pathsGroupBox);
170 
171     setLayout(mainLayout);
172 }
173 
findQmFiles()174 QStringList GeneralSettingsPage::findQmFiles()
175 {
176     QDir dir(translationPath);
177     QStringList fileNames = dir.entryList(QStringList(translationPrefix + "_*.qm"), QDir::Files, QDir::Name);
178     fileNames.replaceInStrings(QRegExp(translationPrefix + "_(.*)\\.qm"), "\\1");
179     return fileNames;
180 }
181 
languageName(const QString & qmFile)182 QString GeneralSettingsPage::languageName(const QString &qmFile)
183 {
184     if (qmFile == DEFAULT_LANG_CODE)
185         return DEFAULT_LANG_NAME;
186 
187     QTranslator translator;
188     translator.load(translationPrefix + "_" + qmFile + ".qm", translationPath);
189 
190     return translator.translate("i18n", DEFAULT_LANG_NAME);
191 }
192 
deckPathButtonClicked()193 void GeneralSettingsPage::deckPathButtonClicked()
194 {
195     QString path = QFileDialog::getExistingDirectory(this, tr("Choose path"), deckPathEdit->text());
196     if (path.isEmpty())
197         return;
198 
199     deckPathEdit->setText(path);
200     SettingsCache::instance().setDeckPath(path);
201 }
202 
replaysPathButtonClicked()203 void GeneralSettingsPage::replaysPathButtonClicked()
204 {
205     QString path = QFileDialog::getExistingDirectory(this, tr("Choose path"), replaysPathEdit->text());
206     if (path.isEmpty())
207         return;
208 
209     replaysPathEdit->setText(path);
210     SettingsCache::instance().setReplaysPath(path);
211 }
212 
picsPathButtonClicked()213 void GeneralSettingsPage::picsPathButtonClicked()
214 {
215     QString path = QFileDialog::getExistingDirectory(this, tr("Choose path"), picsPathEdit->text());
216     if (path.isEmpty())
217         return;
218 
219     picsPathEdit->setText(path);
220     SettingsCache::instance().setPicsPath(path);
221 }
222 
cardDatabasePathButtonClicked()223 void GeneralSettingsPage::cardDatabasePathButtonClicked()
224 {
225     QString path = QFileDialog::getOpenFileName(this, tr("Choose path"), cardDatabasePathEdit->text());
226     if (path.isEmpty())
227         return;
228 
229     cardDatabasePathEdit->setText(path);
230     SettingsCache::instance().setCardDatabasePath(path);
231 }
232 
customCardDatabaseButtonClicked()233 void GeneralSettingsPage::customCardDatabaseButtonClicked()
234 {
235     QString path = QFileDialog::getExistingDirectory(this, tr("Choose path"), customCardDatabasePathEdit->text());
236     if (path.isEmpty())
237         return;
238 
239     customCardDatabasePathEdit->setText(path);
240     SettingsCache::instance().setCustomCardDatabasePath(path);
241 }
242 
tokenDatabasePathButtonClicked()243 void GeneralSettingsPage::tokenDatabasePathButtonClicked()
244 {
245     QString path = QFileDialog::getOpenFileName(this, tr("Choose path"), tokenDatabasePathEdit->text());
246     if (path.isEmpty())
247         return;
248 
249     tokenDatabasePathEdit->setText(path);
250     SettingsCache::instance().setTokenDatabasePath(path);
251 }
252 
languageBoxChanged(int index)253 void GeneralSettingsPage::languageBoxChanged(int index)
254 {
255     SettingsCache::instance().setLang(languageBox.itemData(index).toString());
256 }
257 
retranslateUi()258 void GeneralSettingsPage::retranslateUi()
259 {
260     personalGroupBox->setTitle(tr("Personal settings"));
261     languageLabel.setText(tr("Language:"));
262 
263     if (SettingsCache::instance().getIsPortableBuild()) {
264         pathsGroupBox->setTitle(tr("Paths (editing disabled in portable mode)"));
265     } else {
266         pathsGroupBox->setTitle(tr("Paths"));
267     }
268 
269     deckPathLabel.setText(tr("Decks directory:"));
270     replaysPathLabel.setText(tr("Replays directory:"));
271     picsPathLabel.setText(tr("Pictures directory:"));
272     cardDatabasePathLabel.setText(tr("Card database:"));
273     customCardDatabasePathLabel.setText(tr("Custom database directory:"));
274     tokenDatabasePathLabel.setText(tr("Token database:"));
275     pixmapCacheLabel.setText(tr("Picture cache size:"));
276     updateReleaseChannelLabel.setText(tr("Update channel"));
277     updateNotificationCheckBox.setText(tr("Notify if a feature supported by the server is missing in my client"));
278     newVersionOracleCheckBox.setText(tr("Automatically run Oracle when running a new version of Cockatrice"));
279     showTipsOnStartup.setText(tr("Show tips on startup"));
280 }
281 
AppearanceSettingsPage()282 AppearanceSettingsPage::AppearanceSettingsPage()
283 {
284     QString themeName = SettingsCache::instance().getThemeName();
285 
286     QStringList themeDirs = themeManager->getAvailableThemes().keys();
287     for (int i = 0; i < themeDirs.size(); i++) {
288         themeBox.addItem(themeDirs[i]);
289         if (themeDirs[i] == themeName)
290             themeBox.setCurrentIndex(i);
291     }
292 
293     connect(&themeBox, SIGNAL(currentIndexChanged(int)), this, SLOT(themeBoxChanged(int)));
294 
295     auto *themeGrid = new QGridLayout;
296     themeGrid->addWidget(&themeLabel, 0, 0);
297     themeGrid->addWidget(&themeBox, 0, 1);
298 
299     themeGroupBox = new QGroupBox;
300     themeGroupBox->setLayout(themeGrid);
301 
302     displayCardNamesCheckBox.setChecked(SettingsCache::instance().getDisplayCardNames());
303     connect(&displayCardNamesCheckBox, SIGNAL(stateChanged(int)), &SettingsCache::instance(),
304             SLOT(setDisplayCardNames(int)));
305 
306     cardScalingCheckBox.setChecked(SettingsCache::instance().getScaleCards());
307     connect(&cardScalingCheckBox, SIGNAL(stateChanged(int)), &SettingsCache::instance(), SLOT(setCardScaling(int)));
308 
309     auto *cardsGrid = new QGridLayout;
310     cardsGrid->addWidget(&displayCardNamesCheckBox, 0, 0, 1, 2);
311     cardsGrid->addWidget(&cardScalingCheckBox, 1, 0, 1, 2);
312 
313     cardsGroupBox = new QGroupBox;
314     cardsGroupBox->setLayout(cardsGrid);
315 
316     horizontalHandCheckBox.setChecked(SettingsCache::instance().getHorizontalHand());
317     connect(&horizontalHandCheckBox, SIGNAL(stateChanged(int)), &SettingsCache::instance(),
318             SLOT(setHorizontalHand(int)));
319 
320     leftJustifiedHandCheckBox.setChecked(SettingsCache::instance().getLeftJustified());
321     connect(&leftJustifiedHandCheckBox, SIGNAL(stateChanged(int)), &SettingsCache::instance(),
322             SLOT(setLeftJustified(int)));
323 
324     auto *handGrid = new QGridLayout;
325     handGrid->addWidget(&horizontalHandCheckBox, 0, 0, 1, 2);
326     handGrid->addWidget(&leftJustifiedHandCheckBox, 1, 0, 1, 2);
327 
328     handGroupBox = new QGroupBox;
329     handGroupBox->setLayout(handGrid);
330 
331     invertVerticalCoordinateCheckBox.setChecked(SettingsCache::instance().getInvertVerticalCoordinate());
332     connect(&invertVerticalCoordinateCheckBox, SIGNAL(stateChanged(int)), &SettingsCache::instance(),
333             SLOT(setInvertVerticalCoordinate(int)));
334 
335     minPlayersForMultiColumnLayoutEdit.setMinimum(2);
336     minPlayersForMultiColumnLayoutEdit.setValue(SettingsCache::instance().getMinPlayersForMultiColumnLayout());
337     connect(&minPlayersForMultiColumnLayoutEdit, SIGNAL(valueChanged(int)), &SettingsCache::instance(),
338             SLOT(setMinPlayersForMultiColumnLayout(int)));
339     minPlayersForMultiColumnLayoutLabel.setBuddy(&minPlayersForMultiColumnLayoutEdit);
340 
341     connect(&maxFontSizeForCardsEdit, SIGNAL(valueChanged(int)), &SettingsCache::instance(), SLOT(setMaxFontSize(int)));
342     maxFontSizeForCardsEdit.setValue(SettingsCache::instance().getMaxFontSize());
343     maxFontSizeForCardsLabel.setBuddy(&maxFontSizeForCardsEdit);
344     maxFontSizeForCardsEdit.setMinimum(9);
345     maxFontSizeForCardsEdit.setMaximum(100);
346 
347     auto *tableGrid = new QGridLayout;
348     tableGrid->addWidget(&invertVerticalCoordinateCheckBox, 0, 0, 1, 2);
349     tableGrid->addWidget(&minPlayersForMultiColumnLayoutLabel, 1, 0, 1, 1);
350     tableGrid->addWidget(&minPlayersForMultiColumnLayoutEdit, 1, 1, 1, 1);
351     tableGrid->addWidget(&maxFontSizeForCardsLabel, 2, 0, 1, 1);
352     tableGrid->addWidget(&maxFontSizeForCardsEdit, 2, 1, 1, 1);
353 
354     tableGroupBox = new QGroupBox;
355     tableGroupBox->setLayout(tableGrid);
356 
357     auto *mainLayout = new QVBoxLayout;
358     mainLayout->addWidget(themeGroupBox);
359     mainLayout->addWidget(cardsGroupBox);
360     mainLayout->addWidget(handGroupBox);
361     mainLayout->addWidget(tableGroupBox);
362 
363     setLayout(mainLayout);
364 }
365 
themeBoxChanged(int index)366 void AppearanceSettingsPage::themeBoxChanged(int index)
367 {
368     QStringList themeDirs = themeManager->getAvailableThemes().keys();
369     if (index >= 0 && index < themeDirs.count())
370         SettingsCache::instance().setThemeName(themeDirs.at(index));
371 }
372 
retranslateUi()373 void AppearanceSettingsPage::retranslateUi()
374 {
375     themeGroupBox->setTitle(tr("Theme settings"));
376     themeLabel.setText(tr("Current theme:"));
377 
378     cardsGroupBox->setTitle(tr("Card rendering"));
379     displayCardNamesCheckBox.setText(tr("Display card names on cards having a picture"));
380     cardScalingCheckBox.setText(tr("Scale cards on mouse over"));
381 
382     handGroupBox->setTitle(tr("Hand layout"));
383     horizontalHandCheckBox.setText(tr("Display hand horizontally (wastes space)"));
384     leftJustifiedHandCheckBox.setText(tr("Enable left justification"));
385 
386     tableGroupBox->setTitle(tr("Table grid layout"));
387     invertVerticalCoordinateCheckBox.setText(tr("Invert vertical coordinate"));
388     minPlayersForMultiColumnLayoutLabel.setText(tr("Minimum player count for multi-column layout:"));
389     maxFontSizeForCardsLabel.setText(tr("Maximum font size for information displayed on cards:"));
390 }
391 
UserInterfaceSettingsPage()392 UserInterfaceSettingsPage::UserInterfaceSettingsPage()
393 {
394     notificationsEnabledCheckBox.setChecked(SettingsCache::instance().getNotificationsEnabled());
395     connect(&notificationsEnabledCheckBox, SIGNAL(stateChanged(int)), &SettingsCache::instance(),
396             SLOT(setNotificationsEnabled(int)));
397     connect(&notificationsEnabledCheckBox, SIGNAL(stateChanged(int)), this, SLOT(setSpecNotificationEnabled(int)));
398 
399     specNotificationsEnabledCheckBox.setChecked(SettingsCache::instance().getSpectatorNotificationsEnabled());
400     specNotificationsEnabledCheckBox.setEnabled(SettingsCache::instance().getNotificationsEnabled());
401     connect(&specNotificationsEnabledCheckBox, SIGNAL(stateChanged(int)), &SettingsCache::instance(),
402             SLOT(setSpectatorNotificationsEnabled(int)));
403 
404     buddyConnectNotificationsEnabledCheckBox.setChecked(
405         SettingsCache::instance().getBuddyConnectNotificationsEnabled());
406     buddyConnectNotificationsEnabledCheckBox.setEnabled(SettingsCache::instance().getNotificationsEnabled());
407     connect(&buddyConnectNotificationsEnabledCheckBox, SIGNAL(stateChanged(int)), &SettingsCache::instance(),
408             SLOT(setBuddyConnectNotificationsEnabled(int)));
409 
410     doubleClickToPlayCheckBox.setChecked(SettingsCache::instance().getDoubleClickToPlay());
411     connect(&doubleClickToPlayCheckBox, SIGNAL(stateChanged(int)), &SettingsCache::instance(),
412             SLOT(setDoubleClickToPlay(int)));
413 
414     playToStackCheckBox.setChecked(SettingsCache::instance().getPlayToStack());
415     connect(&playToStackCheckBox, SIGNAL(stateChanged(int)), &SettingsCache::instance(), SLOT(setPlayToStack(int)));
416 
417     annotateTokensCheckBox.setChecked(SettingsCache::instance().getAnnotateTokens());
418     connect(&annotateTokensCheckBox, SIGNAL(stateChanged(int)), &SettingsCache::instance(),
419             SLOT(setAnnotateTokens(int)));
420 
421     useTearOffMenusCheckBox.setChecked(SettingsCache::instance().getUseTearOffMenus());
422     connect(&useTearOffMenusCheckBox, &QCheckBox::stateChanged, &SettingsCache::instance(),
423             [](int state) { SettingsCache::instance().setUseTearOffMenus(state == Qt::Checked); });
424 
425     auto *generalGrid = new QGridLayout;
426     generalGrid->addWidget(&doubleClickToPlayCheckBox, 0, 0);
427     generalGrid->addWidget(&playToStackCheckBox, 1, 0);
428     generalGrid->addWidget(&annotateTokensCheckBox, 2, 0);
429     generalGrid->addWidget(&useTearOffMenusCheckBox, 3, 0);
430 
431     generalGroupBox = new QGroupBox;
432     generalGroupBox->setLayout(generalGrid);
433 
434     auto *notificationsGrid = new QGridLayout;
435     notificationsGrid->addWidget(&notificationsEnabledCheckBox, 0, 0);
436     notificationsGrid->addWidget(&specNotificationsEnabledCheckBox, 1, 0);
437     notificationsGrid->addWidget(&buddyConnectNotificationsEnabledCheckBox, 2, 0);
438 
439     notificationsGroupBox = new QGroupBox;
440     notificationsGroupBox->setLayout(notificationsGrid);
441 
442     tapAnimationCheckBox.setChecked(SettingsCache::instance().getTapAnimation());
443     connect(&tapAnimationCheckBox, SIGNAL(stateChanged(int)), &SettingsCache::instance(), SLOT(setTapAnimation(int)));
444 
445     auto *animationGrid = new QGridLayout;
446     animationGrid->addWidget(&tapAnimationCheckBox, 0, 0);
447 
448     animationGroupBox = new QGroupBox;
449     animationGroupBox->setLayout(animationGrid);
450 
451     auto *mainLayout = new QVBoxLayout;
452     mainLayout->addWidget(generalGroupBox);
453     mainLayout->addWidget(notificationsGroupBox);
454     mainLayout->addWidget(animationGroupBox);
455 
456     setLayout(mainLayout);
457 }
458 
setSpecNotificationEnabled(int i)459 void UserInterfaceSettingsPage::setSpecNotificationEnabled(int i)
460 {
461     specNotificationsEnabledCheckBox.setEnabled(i != 0);
462 }
463 
retranslateUi()464 void UserInterfaceSettingsPage::retranslateUi()
465 {
466     generalGroupBox->setTitle(tr("General interface settings"));
467     doubleClickToPlayCheckBox.setText(tr("&Double-click cards to play them (instead of single-click)"));
468     playToStackCheckBox.setText(tr("&Play all nonlands onto the stack (not the battlefield) by default"));
469     annotateTokensCheckBox.setText(tr("Annotate card text on tokens"));
470     useTearOffMenusCheckBox.setText(tr("Use tear-off menus, allowing right click menus to persist on screen"));
471     notificationsGroupBox->setTitle(tr("Notifications settings"));
472     notificationsEnabledCheckBox.setText(tr("Enable notifications in taskbar"));
473     specNotificationsEnabledCheckBox.setText(tr("Notify in the taskbar for game events while you are spectating"));
474     buddyConnectNotificationsEnabledCheckBox.setText(tr("Notify in the taskbar when users in your buddy list connect"));
475     animationGroupBox->setTitle(tr("Animation settings"));
476     tapAnimationCheckBox.setText(tr("&Tap/untap animation"));
477 }
478 
DeckEditorSettingsPage()479 DeckEditorSettingsPage::DeckEditorSettingsPage()
480 {
481     picDownloadCheckBox.setChecked(SettingsCache::instance().getPicDownload());
482     connect(&picDownloadCheckBox, SIGNAL(stateChanged(int)), &SettingsCache::instance(), SLOT(setPicDownload(int)));
483 
484     urlLinkLabel.setTextInteractionFlags(Qt::LinksAccessibleByMouse);
485     urlLinkLabel.setOpenExternalLinks(true);
486 
487     connect(&clearDownloadedPicsButton, SIGNAL(clicked()), this, SLOT(clearDownloadedPicsButtonClicked()));
488     connect(&resetDownloadURLs, SIGNAL(clicked()), this, SLOT(resetDownloadedURLsButtonClicked()));
489 
490     auto *lpGeneralGrid = new QGridLayout;
491     auto *lpSpoilerGrid = new QGridLayout;
492 
493     mcDownloadSpoilersCheckBox.setChecked(SettingsCache::instance().getDownloadSpoilersStatus());
494 
495     mpSpoilerSavePathLineEdit = new QLineEdit(SettingsCache::instance().getSpoilerCardDatabasePath());
496     mpSpoilerSavePathLineEdit->setReadOnly(true);
497     mpSpoilerPathButton = new QPushButton("...");
498     connect(mpSpoilerPathButton, SIGNAL(clicked()), this, SLOT(spoilerPathButtonClicked()));
499 
500     updateNowButton = new QPushButton(tr("Update Spoilers"));
501     updateNowButton->setFixedWidth(150);
502     connect(updateNowButton, SIGNAL(clicked()), this, SLOT(updateSpoilers()));
503 
504     // Update the GUI depending on if the box is ticked or not
505     setSpoilersEnabled(mcDownloadSpoilersCheckBox.isChecked());
506 
507     urlList = new QListWidget;
508     urlList->setSelectionMode(QAbstractItemView::SingleSelection);
509     urlList->setAlternatingRowColors(true);
510     urlList->setDragEnabled(true);
511     urlList->setDragDropMode(QAbstractItemView::InternalMove);
512     connect(urlList->model(), SIGNAL(rowsMoved(const QModelIndex, int, int, const QModelIndex, int)), this,
513             SLOT(urlListChanged(const QModelIndex, int, int, const QModelIndex, int)));
514 
515     for (int i = 0; i < SettingsCache::instance().downloads().getCount(); i++)
516         urlList->addItem(SettingsCache::instance().downloads().getDownloadUrlAt(i));
517 
518     auto aAdd = new QAction(this);
519     aAdd->setIcon(QPixmap("theme:icons/increment"));
520     connect(aAdd, SIGNAL(triggered()), this, SLOT(actAddURL()));
521     auto aEdit = new QAction(this);
522     aEdit->setIcon(QPixmap("theme:icons/pencil"));
523     connect(aEdit, SIGNAL(triggered()), this, SLOT(actEditURL()));
524     auto aRemove = new QAction(this);
525     aRemove->setIcon(QPixmap("theme:icons/decrement"));
526     connect(aRemove, SIGNAL(triggered()), this, SLOT(actRemoveURL()));
527 
528     auto *messageToolBar = new QToolBar;
529     messageToolBar->setOrientation(Qt::Vertical);
530     messageToolBar->addAction(aAdd);
531     messageToolBar->addAction(aRemove);
532     messageToolBar->addAction(aEdit);
533     messageToolBar->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding);
534 
535     auto *messageListLayout = new QHBoxLayout;
536     messageListLayout->addWidget(messageToolBar);
537     messageListLayout->addWidget(urlList);
538 
539     // Top Layout
540     lpGeneralGrid->addWidget(&picDownloadCheckBox, 0, 0);
541     lpGeneralGrid->addWidget(&resetDownloadURLs, 0, 1);
542     lpGeneralGrid->addLayout(messageListLayout, 1, 0, 1, 2);
543     lpGeneralGrid->addWidget(&urlLinkLabel, 2, 0);
544     lpGeneralGrid->addWidget(&clearDownloadedPicsButton, 2, 1);
545 
546     // Spoiler Layout
547     lpSpoilerGrid->addWidget(&mcDownloadSpoilersCheckBox, 0, 0);
548     lpSpoilerGrid->addWidget(&mcSpoilerSaveLabel, 1, 0);
549     lpSpoilerGrid->addWidget(mpSpoilerSavePathLineEdit, 1, 1);
550     lpSpoilerGrid->addWidget(mpSpoilerPathButton, 1, 2);
551     lpSpoilerGrid->addWidget(&lastUpdatedLabel, 2, 0);
552     lpSpoilerGrid->addWidget(updateNowButton, 2, 1);
553     lpSpoilerGrid->addWidget(&infoOnSpoilersLabel, 3, 0, 1, 3, Qt::AlignTop);
554 
555     // On a change to the check box, hide/unhide the other fields
556     connect(&mcDownloadSpoilersCheckBox, SIGNAL(toggled(bool)), &SettingsCache::instance(),
557             SLOT(setDownloadSpoilerStatus(bool)));
558     connect(&mcDownloadSpoilersCheckBox, SIGNAL(toggled(bool)), this, SLOT(setSpoilersEnabled(bool)));
559 
560     mpGeneralGroupBox = new QGroupBox;
561     mpGeneralGroupBox->setLayout(lpGeneralGrid);
562 
563     mpSpoilerGroupBox = new QGroupBox;
564     mpSpoilerGroupBox->setLayout(lpSpoilerGrid);
565 
566     auto *lpMainLayout = new QVBoxLayout;
567     lpMainLayout->addWidget(mpGeneralGroupBox);
568     lpMainLayout->addWidget(mpSpoilerGroupBox);
569 
570     setLayout(lpMainLayout);
571 }
572 
resetDownloadedURLsButtonClicked()573 void DeckEditorSettingsPage::resetDownloadedURLsButtonClicked()
574 {
575     SettingsCache::instance().downloads().clear();
576     urlList->clear();
577     urlList->addItems(SettingsCache::instance().downloads().getAllURLs());
578     QMessageBox::information(this, tr("Success"), tr("Download URLs have been reset."));
579 }
580 
clearDownloadedPicsButtonClicked()581 void DeckEditorSettingsPage::clearDownloadedPicsButtonClicked()
582 {
583     QString picsPath = SettingsCache::instance().getPicsPath() + "/downloadedPics/";
584     QStringList dirs = QDir(picsPath).entryList(QDir::AllDirs | QDir::NoDotAndDotDot);
585     bool outerSuccessRemove = true;
586     for (const auto &dir : dirs) {
587         QString currentPath = picsPath + dir + "/";
588         QStringList files = QDir(currentPath).entryList(QDir::Files);
589         bool innerSuccessRemove = true;
590         for (int j = 0; j < files.length(); j++) {
591             if (!QDir(currentPath).remove(files.at(j))) {
592                 qInfo() << "Failed to remove " + currentPath.toUtf8() + files.at(j).toUtf8();
593                 outerSuccessRemove = false;
594                 innerSuccessRemove = false;
595             }
596             qInfo() << "Removed " << currentPath << files.at(j);
597         }
598 
599         if (innerSuccessRemove) {
600             bool success = QDir(picsPath).rmdir(dir);
601             if (!success) {
602                 qInfo() << "Failed to remove inner directory" << picsPath;
603             } else {
604                 qInfo() << "Removed" << currentPath;
605             }
606         }
607     }
608     if (outerSuccessRemove) {
609         QMessageBox::information(this, tr("Success"), tr("Downloaded card pictures have been reset."));
610     } else {
611         QMessageBox::critical(this, tr("Error"), tr("One or more downloaded card pictures could not be cleared."));
612     }
613 }
614 
actAddURL()615 void DeckEditorSettingsPage::actAddURL()
616 {
617     bool ok;
618     QString msg = QInputDialog::getText(this, tr("Add URL"), tr("URL:"), QLineEdit::Normal, QString(), &ok);
619     if (ok) {
620         urlList->addItem(msg);
621         storeSettings();
622     }
623 }
624 
actRemoveURL()625 void DeckEditorSettingsPage::actRemoveURL()
626 {
627     if (urlList->currentItem() != nullptr) {
628         delete urlList->takeItem(urlList->currentRow());
629         storeSettings();
630     }
631 }
632 
actEditURL()633 void DeckEditorSettingsPage::actEditURL()
634 {
635     if (urlList->currentItem()) {
636         QString oldText = urlList->currentItem()->text();
637         bool ok;
638         QString msg = QInputDialog::getText(this, tr("Edit URL"), tr("URL:"), QLineEdit::Normal, oldText, &ok);
639         if (ok) {
640             urlList->currentItem()->setText(msg);
641             storeSettings();
642         }
643     }
644 }
645 
storeSettings()646 void DeckEditorSettingsPage::storeSettings()
647 {
648     qInfo() << "URL Priority Reset";
649     SettingsCache::instance().downloads().clear();
650     for (int i = 0; i < urlList->count(); i++) {
651         qInfo() << "Priority" << i << ":" << urlList->item(i)->text();
652         SettingsCache::instance().downloads().setDownloadUrlAt(i, urlList->item(i)->text());
653     }
654 }
655 
urlListChanged(const QModelIndex &,int,int,const QModelIndex &,int)656 void DeckEditorSettingsPage::urlListChanged(const QModelIndex &, int, int, const QModelIndex &, int)
657 {
658     storeSettings();
659 }
660 
updateSpoilers()661 void DeckEditorSettingsPage::updateSpoilers()
662 {
663     // Disable the button so the user can only press it once at a time
664     updateNowButton->setDisabled(true);
665     updateNowButton->setText(tr("Updating..."));
666 
667     // Create a new SBU that will act as if the client was just reloaded
668     auto *sbu = new SpoilerBackgroundUpdater();
669     connect(sbu, SIGNAL(spoilerCheckerDone()), this, SLOT(unlockSettings()));
670     connect(sbu, SIGNAL(spoilersUpdatedSuccessfully()), this, SLOT(unlockSettings()));
671 }
672 
unlockSettings()673 void DeckEditorSettingsPage::unlockSettings()
674 {
675     updateNowButton->setDisabled(false);
676     updateNowButton->setText(tr("Update Spoilers"));
677 }
678 
getLastUpdateTime()679 QString DeckEditorSettingsPage::getLastUpdateTime()
680 {
681     QString fileName = SettingsCache::instance().getSpoilerCardDatabasePath();
682     QFileInfo fi(fileName);
683     QDir fileDir(fi.path());
684     QFile file(fileName);
685 
686     if (file.exists()) {
687         return fi.lastModified().toString("MMM d, hh:mm");
688     }
689 
690     return QString();
691 }
692 
spoilerPathButtonClicked()693 void DeckEditorSettingsPage::spoilerPathButtonClicked()
694 {
695     QString lsPath = QFileDialog::getExistingDirectory(this, tr("Choose path"), mpSpoilerSavePathLineEdit->text());
696     if (lsPath.isEmpty()) {
697         return;
698     }
699 
700     mpSpoilerSavePathLineEdit->setText(lsPath + "/spoiler.xml");
701     SettingsCache::instance().setSpoilerDatabasePath(lsPath + "/spoiler.xml");
702 }
703 
setSpoilersEnabled(bool anInput)704 void DeckEditorSettingsPage::setSpoilersEnabled(bool anInput)
705 {
706     msDownloadSpoilersLabel.setEnabled(anInput);
707     mcSpoilerSaveLabel.setEnabled(anInput);
708     mpSpoilerSavePathLineEdit->setEnabled(anInput);
709     mpSpoilerPathButton->setEnabled(anInput);
710     lastUpdatedLabel.setEnabled(anInput);
711     updateNowButton->setEnabled(anInput);
712     infoOnSpoilersLabel.setEnabled(anInput);
713 
714     if (!anInput) {
715         SpoilerBackgroundUpdater::deleteSpoilerFile();
716     }
717 }
718 
retranslateUi()719 void DeckEditorSettingsPage::retranslateUi()
720 {
721     mpGeneralGroupBox->setTitle(tr("URL Download Priority"));
722     mpSpoilerGroupBox->setTitle(tr("Spoilers"));
723     mcDownloadSpoilersCheckBox.setText(tr("Download Spoilers Automatically"));
724     mcSpoilerSaveLabel.setText(tr("Spoiler Location:"));
725     lastUpdatedLabel.setText(tr("Last Change") + ": " + getLastUpdateTime());
726     infoOnSpoilersLabel.setText(tr("Spoilers download automatically on launch") + "\n" +
727                                 tr("Press the button to manually update without relaunching") + "\n\n" +
728                                 tr("Do not close settings until manual update is complete"));
729     picDownloadCheckBox.setText(tr("Download card pictures on the fly"));
730     urlLinkLabel.setText(QString("<a href='%1'>%2</a>").arg(WIKI_CUSTOM_PIC_URL).arg(tr("How to add a custom URL")));
731     clearDownloadedPicsButton.setText(tr("Delete Downloaded Images"));
732     resetDownloadURLs.setText(tr("Reset Download URLs"));
733 }
734 
MessagesSettingsPage()735 MessagesSettingsPage::MessagesSettingsPage()
736 {
737     chatMentionCheckBox.setChecked(SettingsCache::instance().getChatMention());
738     connect(&chatMentionCheckBox, SIGNAL(stateChanged(int)), &SettingsCache::instance(), SLOT(setChatMention(int)));
739 
740     chatMentionCompleterCheckbox.setChecked(SettingsCache::instance().getChatMentionCompleter());
741     connect(&chatMentionCompleterCheckbox, SIGNAL(stateChanged(int)), &SettingsCache::instance(),
742             SLOT(setChatMentionCompleter(int)));
743 
744     ignoreUnregUsersMainChat.setChecked(SettingsCache::instance().getIgnoreUnregisteredUsers());
745     ignoreUnregUserMessages.setChecked(SettingsCache::instance().getIgnoreUnregisteredUserMessages());
746     connect(&ignoreUnregUsersMainChat, SIGNAL(stateChanged(int)), &SettingsCache::instance(),
747             SLOT(setIgnoreUnregisteredUsers(int)));
748     connect(&ignoreUnregUserMessages, SIGNAL(stateChanged(int)), &SettingsCache::instance(),
749             SLOT(setIgnoreUnregisteredUserMessages(int)));
750 
751     invertMentionForeground.setChecked(SettingsCache::instance().getChatMentionForeground());
752     connect(&invertMentionForeground, SIGNAL(stateChanged(int)), this, SLOT(updateTextColor(int)));
753 
754     invertHighlightForeground.setChecked(SettingsCache::instance().getChatHighlightForeground());
755     connect(&invertHighlightForeground, SIGNAL(stateChanged(int)), this, SLOT(updateTextHighlightColor(int)));
756 
757     mentionColor = new QLineEdit();
758     mentionColor->setText(SettingsCache::instance().getChatMentionColor());
759     updateMentionPreview();
760     connect(mentionColor, SIGNAL(textChanged(QString)), this, SLOT(updateColor(QString)));
761 
762     messagePopups.setChecked(SettingsCache::instance().getShowMessagePopup());
763     connect(&messagePopups, SIGNAL(stateChanged(int)), &SettingsCache::instance(), SLOT(setShowMessagePopups(int)));
764 
765     mentionPopups.setChecked(SettingsCache::instance().getShowMentionPopup());
766     connect(&mentionPopups, SIGNAL(stateChanged(int)), &SettingsCache::instance(), SLOT(setShowMentionPopups(int)));
767 
768     roomHistory.setChecked(SettingsCache::instance().getRoomHistory());
769     connect(&roomHistory, SIGNAL(stateChanged(int)), &SettingsCache::instance(), SLOT(setRoomHistory(int)));
770 
771     customAlertString = new QLineEdit();
772     customAlertString->setPlaceholderText(tr("Word1 Word2 Word3"));
773     customAlertString->setText(SettingsCache::instance().getHighlightWords());
774     connect(customAlertString, SIGNAL(textChanged(QString)), &SettingsCache::instance(),
775             SLOT(setHighlightWords(QString)));
776 
777     auto *chatGrid = new QGridLayout;
778     chatGrid->addWidget(&chatMentionCheckBox, 0, 0);
779     chatGrid->addWidget(&invertMentionForeground, 0, 1);
780     chatGrid->addWidget(mentionColor, 0, 2);
781     chatGrid->addWidget(&chatMentionCompleterCheckbox, 1, 0);
782     chatGrid->addWidget(&ignoreUnregUsersMainChat, 2, 0);
783     chatGrid->addWidget(&hexLabel, 1, 2);
784     chatGrid->addWidget(&ignoreUnregUserMessages, 3, 0);
785     chatGrid->addWidget(&messagePopups, 4, 0);
786     chatGrid->addWidget(&mentionPopups, 5, 0);
787     chatGrid->addWidget(&roomHistory, 6, 0);
788     chatGroupBox = new QGroupBox;
789     chatGroupBox->setLayout(chatGrid);
790 
791     highlightColor = new QLineEdit();
792     highlightColor->setText(SettingsCache::instance().getChatHighlightColor());
793     updateHighlightPreview();
794     connect(highlightColor, SIGNAL(textChanged(QString)), this, SLOT(updateHighlightColor(QString)));
795 
796     auto *highlightNotice = new QGridLayout;
797     highlightNotice->addWidget(highlightColor, 0, 2);
798     highlightNotice->addWidget(&invertHighlightForeground, 0, 1);
799     highlightNotice->addWidget(&hexHighlightLabel, 1, 2);
800     highlightNotice->addWidget(customAlertString, 0, 0);
801     highlightNotice->addWidget(&customAlertStringLabel, 1, 0);
802     highlightGroupBox = new QGroupBox;
803     highlightGroupBox->setLayout(highlightNotice);
804 
805     messageList = new QListWidget;
806 
807     int count = SettingsCache::instance().messages().getCount();
808     for (int i = 0; i < count; i++)
809         messageList->addItem(SettingsCache::instance().messages().getMessageAt(i));
810 
811     aAdd = new QAction(this);
812     aAdd->setIcon(QPixmap("theme:icons/increment"));
813     aAdd->setStatusTip(tr("Add New URL"));
814 
815     connect(aAdd, SIGNAL(triggered()), this, SLOT(actAdd()));
816     aEdit = new QAction(this);
817     aEdit->setIcon(QPixmap("theme:icons/pencil"));
818     aEdit->setStatusTip(tr("Edit URL"));
819     connect(aEdit, SIGNAL(triggered()), this, SLOT(actEdit()));
820     aRemove = new QAction(this);
821     aRemove->setIcon(QPixmap("theme:icons/decrement"));
822     aRemove->setStatusTip(tr("Remove URL"));
823     connect(aRemove, SIGNAL(triggered()), this, SLOT(actRemove()));
824 
825     auto *messageToolBar = new QToolBar;
826     messageToolBar->setOrientation(Qt::Vertical);
827     messageToolBar->addAction(aAdd);
828     messageToolBar->addAction(aRemove);
829     messageToolBar->addAction(aEdit);
830     messageToolBar->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding);
831 
832     auto *messageListLayout = new QHBoxLayout;
833     messageListLayout->addWidget(messageToolBar);
834     messageListLayout->addWidget(messageList);
835 
836     messageShortcuts = new QGroupBox;
837     messageShortcuts->setLayout(messageListLayout);
838 
839     auto *mainLayout = new QVBoxLayout;
840 
841     mainLayout->addWidget(messageShortcuts);
842     mainLayout->addWidget(chatGroupBox);
843     mainLayout->addWidget(highlightGroupBox);
844 
845     setLayout(mainLayout);
846 
847     retranslateUi();
848 }
849 
updateColor(const QString & value)850 void MessagesSettingsPage::updateColor(const QString &value)
851 {
852     QColor colorToSet;
853     colorToSet.setNamedColor("#" + value);
854     if (colorToSet.isValid()) {
855         SettingsCache::instance().setChatMentionColor(value);
856         updateMentionPreview();
857     }
858 }
859 
updateHighlightColor(const QString & value)860 void MessagesSettingsPage::updateHighlightColor(const QString &value)
861 {
862     QColor colorToSet;
863     colorToSet.setNamedColor("#" + value);
864     if (colorToSet.isValid()) {
865         SettingsCache::instance().setChatHighlightColor(value);
866         updateHighlightPreview();
867     }
868 }
869 
updateTextColor(int value)870 void MessagesSettingsPage::updateTextColor(int value)
871 {
872     SettingsCache::instance().setChatMentionForeground(value);
873     updateMentionPreview();
874 }
875 
updateTextHighlightColor(int value)876 void MessagesSettingsPage::updateTextHighlightColor(int value)
877 {
878     SettingsCache::instance().setChatHighlightForeground(value);
879     updateHighlightPreview();
880 }
881 
updateMentionPreview()882 void MessagesSettingsPage::updateMentionPreview()
883 {
884     mentionColor->setStyleSheet(
885         "QLineEdit{background:#" + SettingsCache::instance().getChatMentionColor() +
886         ";color: " + (SettingsCache::instance().getChatMentionForeground() ? "white" : "black") + ";}");
887 }
888 
updateHighlightPreview()889 void MessagesSettingsPage::updateHighlightPreview()
890 {
891     highlightColor->setStyleSheet(
892         "QLineEdit{background:#" + SettingsCache::instance().getChatHighlightColor() +
893         ";color: " + (SettingsCache::instance().getChatHighlightForeground() ? "white" : "black") + ";}");
894 }
895 
storeSettings()896 void MessagesSettingsPage::storeSettings()
897 {
898     SettingsCache::instance().messages().setCount(messageList->count());
899     for (int i = 0; i < messageList->count(); i++)
900         SettingsCache::instance().messages().setMessageAt(i, messageList->item(i)->text());
901 }
902 
actAdd()903 void MessagesSettingsPage::actAdd()
904 {
905     bool ok;
906     QString msg = QInputDialog::getText(this, tr("Add message"), tr("Message:"), QLineEdit::Normal, QString(), &ok);
907     if (ok) {
908         messageList->addItem(msg);
909         storeSettings();
910     }
911 }
912 
actEdit()913 void MessagesSettingsPage::actEdit()
914 {
915     if (messageList->currentItem()) {
916         QString oldText = messageList->currentItem()->text();
917         bool ok;
918         QString msg = QInputDialog::getText(this, tr("Edit message"), tr("Message:"), QLineEdit::Normal, oldText, &ok);
919         if (ok) {
920             messageList->currentItem()->setText(msg);
921             storeSettings();
922         }
923     }
924 }
925 
actRemove()926 void MessagesSettingsPage::actRemove()
927 {
928     if (messageList->currentItem() != nullptr) {
929         delete messageList->takeItem(messageList->currentRow());
930         storeSettings();
931     }
932 }
933 
retranslateUi()934 void MessagesSettingsPage::retranslateUi()
935 {
936     chatGroupBox->setTitle(tr("Chat settings"));
937     highlightGroupBox->setTitle(tr("Custom alert words"));
938     chatMentionCheckBox.setText(tr("Enable chat mentions"));
939     chatMentionCompleterCheckbox.setText(tr("Enable mention completer"));
940     messageShortcuts->setTitle(tr("In-game message macros"));
941     ignoreUnregUsersMainChat.setText(tr("Ignore chat room messages sent by unregistered users"));
942     ignoreUnregUserMessages.setText(tr("Ignore private messages sent by unregistered users"));
943     invertMentionForeground.setText(tr("Invert text color"));
944     invertHighlightForeground.setText(tr("Invert text color"));
945     messagePopups.setText(tr("Enable desktop notifications for private messages"));
946     mentionPopups.setText(tr("Enable desktop notification for mentions"));
947     roomHistory.setText(tr("Enable room message history on join"));
948     hexLabel.setText(tr("(Color is hexadecimal)"));
949     hexHighlightLabel.setText(tr("(Color is hexadecimal)"));
950     customAlertStringLabel.setText(tr("Separate words with a space, alphanumeric characters only"));
951 }
952 
SoundSettingsPage()953 SoundSettingsPage::SoundSettingsPage()
954 {
955     soundEnabledCheckBox.setChecked(SettingsCache::instance().getSoundEnabled());
956     connect(&soundEnabledCheckBox, SIGNAL(stateChanged(int)), &SettingsCache::instance(), SLOT(setSoundEnabled(int)));
957 
958     QString themeName = SettingsCache::instance().getSoundThemeName();
959 
960     QStringList themeDirs = soundEngine->getAvailableThemes().keys();
961     for (int i = 0; i < themeDirs.size(); i++) {
962         themeBox.addItem(themeDirs[i]);
963         if (themeDirs[i] == themeName)
964             themeBox.setCurrentIndex(i);
965     }
966 
967     connect(&themeBox, SIGNAL(currentIndexChanged(int)), this, SLOT(themeBoxChanged(int)));
968     connect(&soundTestButton, SIGNAL(clicked()), soundEngine, SLOT(testSound()));
969 
970     masterVolumeSlider = new QSlider(Qt::Horizontal);
971     masterVolumeSlider->setMinimum(0);
972     masterVolumeSlider->setMaximum(100);
973     masterVolumeSlider->setValue(SettingsCache::instance().getMasterVolume());
974     masterVolumeSlider->setToolTip(QString::number(SettingsCache::instance().getMasterVolume()));
975     connect(&SettingsCache::instance(), SIGNAL(masterVolumeChanged(int)), this, SLOT(masterVolumeChanged(int)));
976     connect(masterVolumeSlider, SIGNAL(sliderReleased()), soundEngine, SLOT(testSound()));
977     connect(masterVolumeSlider, SIGNAL(valueChanged(int)), &SettingsCache::instance(), SLOT(setMasterVolume(int)));
978 
979     masterVolumeSpinBox = new QSpinBox();
980     masterVolumeSpinBox->setMinimum(0);
981     masterVolumeSpinBox->setMaximum(100);
982     masterVolumeSpinBox->setValue(SettingsCache::instance().getMasterVolume());
983     connect(masterVolumeSlider, SIGNAL(valueChanged(int)), masterVolumeSpinBox, SLOT(setValue(int)));
984     connect(masterVolumeSpinBox, SIGNAL(valueChanged(int)), masterVolumeSlider, SLOT(setValue(int)));
985 
986     auto *soundGrid = new QGridLayout;
987     soundGrid->addWidget(&soundEnabledCheckBox, 0, 0, 1, 3);
988     soundGrid->addWidget(&masterVolumeLabel, 1, 0);
989     soundGrid->addWidget(masterVolumeSlider, 1, 1);
990     soundGrid->addWidget(masterVolumeSpinBox, 1, 2);
991     soundGrid->addWidget(&themeLabel, 2, 0);
992     soundGrid->addWidget(&themeBox, 2, 1);
993     soundGrid->addWidget(&soundTestButton, 3, 1);
994 
995     soundGroupBox = new QGroupBox;
996     soundGroupBox->setLayout(soundGrid);
997 
998     auto *mainLayout = new QVBoxLayout;
999     mainLayout->addWidget(soundGroupBox);
1000 
1001     setLayout(mainLayout);
1002 }
1003 
themeBoxChanged(int index)1004 void SoundSettingsPage::themeBoxChanged(int index)
1005 {
1006     QStringList themeDirs = soundEngine->getAvailableThemes().keys();
1007     if (index >= 0 && index < themeDirs.count())
1008         SettingsCache::instance().setSoundThemeName(themeDirs.at(index));
1009 }
1010 
masterVolumeChanged(int value)1011 void SoundSettingsPage::masterVolumeChanged(int value)
1012 {
1013     masterVolumeSlider->setToolTip(QString::number(value));
1014 }
1015 
retranslateUi()1016 void SoundSettingsPage::retranslateUi()
1017 {
1018     soundEnabledCheckBox.setText(tr("Enable &sounds"));
1019     themeLabel.setText(tr("Current sounds theme:"));
1020     soundTestButton.setText(tr("Test system sound engine"));
1021     soundGroupBox->setTitle(tr("Sound settings"));
1022     masterVolumeLabel.setText(tr("Master volume"));
1023 }
1024 
ShortcutSettingsPage()1025 ShortcutSettingsPage::ShortcutSettingsPage()
1026 {
1027     // table
1028     shortcutsTable = new QTreeWidget();
1029     shortcutsTable->setColumnCount(2);
1030     shortcutsTable->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
1031     shortcutsTable->setUniformRowHeights(true);
1032     shortcutsTable->setAlternatingRowColors(true);
1033     shortcutsTable->header()->resizeSection(0, shortcutsTable->width() / 3 * 2);
1034 
1035     // edit widget
1036     currentActionGroupLabel = new QLabel(this);
1037     currentActionGroupName = new QLabel(this);
1038     currentActionLabel = new QLabel(this);
1039     currentActionName = new QLabel(this);
1040     currentShortcutLabel = new QLabel(this);
1041     editTextBox = new SequenceEdit("", this);
1042     shortcutsTable->installEventFilter(editTextBox);
1043 
1044     // buttons
1045     faqLabel = new QLabel(this);
1046     faqLabel->setTextInteractionFlags(Qt::LinksAccessibleByMouse);
1047     faqLabel->setOpenExternalLinks(true);
1048 
1049     btnResetAll = new QPushButton(this);
1050     btnClearAll = new QPushButton(this);
1051 
1052     btnResetAll->setIcon(QPixmap("theme:icons/update"));
1053     btnClearAll->setIcon(QPixmap("theme:icons/clearsearch"));
1054 
1055     // layout
1056     auto *editLayout = new QGridLayout;
1057     editLayout->addWidget(currentActionGroupLabel, 0, 0);
1058     editLayout->addWidget(currentActionGroupName, 0, 1);
1059     editLayout->addWidget(currentActionLabel, 1, 0);
1060     editLayout->addWidget(currentActionName, 1, 1);
1061     editLayout->addWidget(currentShortcutLabel, 2, 0);
1062     editLayout->addWidget(editTextBox, 2, 1);
1063 
1064     editShortcutGroupBox = new QGroupBox;
1065     editShortcutGroupBox->setLayout(editLayout);
1066 
1067     auto *buttonsLayout = new QHBoxLayout;
1068     buttonsLayout->addWidget(faqLabel);
1069     buttonsLayout->addWidget(btnResetAll);
1070     buttonsLayout->addWidget(btnClearAll);
1071 
1072     auto *mainLayout = new QVBoxLayout;
1073     mainLayout->addWidget(shortcutsTable);
1074     mainLayout->addWidget(editShortcutGroupBox);
1075     mainLayout->addLayout(buttonsLayout);
1076 
1077     setLayout(mainLayout);
1078 
1079     connect(btnResetAll, SIGNAL(clicked()), this, SLOT(resetShortcuts()));
1080     connect(btnClearAll, SIGNAL(clicked()), this, SLOT(clearShortcuts()));
1081     connect(shortcutsTable, SIGNAL(currentItemChanged(QTreeWidgetItem *, QTreeWidgetItem *)), this,
1082             SLOT(currentItemChanged(QTreeWidgetItem *, QTreeWidgetItem *)));
1083     connect(&SettingsCache::instance().shortcuts(), SIGNAL(shortCutChanged()), this, SLOT(refreshShortcuts()));
1084 
1085     createShortcuts();
1086 }
1087 
currentItemChanged(QTreeWidgetItem * current,QTreeWidgetItem *)1088 void ShortcutSettingsPage::currentItemChanged(QTreeWidgetItem *current, QTreeWidgetItem * /*previous */)
1089 {
1090     if (current == nullptr) {
1091         currentActionGroupName->setText("");
1092         currentActionName->setText("");
1093         editTextBox->setShortcutName("");
1094     } else {
1095         QString key = current->data(2, Qt::DisplayRole).toString();
1096         QString group = SettingsCache::instance().shortcuts().getShortcut(key).getGroupName();
1097         QString action = SettingsCache::instance().shortcuts().getShortcut(key).getName();
1098         currentActionGroupName->setText(group);
1099         currentActionName->setText(action);
1100         editTextBox->setShortcutName(key);
1101     }
1102 }
1103 
resetShortcuts()1104 void ShortcutSettingsPage::resetShortcuts()
1105 {
1106     if (QMessageBox::question(this, tr("Restore all default shortcuts"),
1107                               tr("Do you really want to restore all default shortcuts?")) == QMessageBox::Yes) {
1108         SettingsCache::instance().shortcuts().resetAllShortcuts();
1109     }
1110 }
1111 
createShortcuts()1112 void ShortcutSettingsPage::createShortcuts()
1113 {
1114     QHash<QString, QTreeWidgetItem *> parentItems;
1115     QTreeWidgetItem *curParent = nullptr;
1116     for (const auto &key : SettingsCache::instance().shortcuts().getAllShortcutKeys()) {
1117         QString name = SettingsCache::instance().shortcuts().getShortcut(key).getName();
1118         QString group = SettingsCache::instance().shortcuts().getShortcut(key).getGroupName();
1119         QString shortcut = SettingsCache::instance().shortcuts().getShortcutString(key);
1120 
1121         if (parentItems.contains(group)) {
1122             curParent = parentItems.value(group);
1123         } else {
1124             curParent = new QTreeWidgetItem((QTreeWidget *)nullptr, QStringList({group, "", ""}));
1125             static QFont font = curParent->font(0);
1126             font.setBold(true);
1127             curParent->setFont(0, font);
1128             parentItems.insert(group, curParent);
1129         }
1130 
1131         new QTreeWidgetItem(curParent, QStringList({name, shortcut, key}));
1132     }
1133     shortcutsTable->clear();
1134     shortcutsTable->insertTopLevelItems(0, parentItems.values());
1135     shortcutsTable->setCurrentItem(nullptr);
1136     shortcutsTable->expandAll();
1137     shortcutsTable->sortItems(0, Qt::AscendingOrder);
1138 }
1139 
refreshShortcuts()1140 void ShortcutSettingsPage::refreshShortcuts()
1141 {
1142     QTreeWidgetItem *curParent = nullptr;
1143     QTreeWidgetItem *curChild = nullptr;
1144     for (int i = 0; i < shortcutsTable->topLevelItemCount(); ++i) {
1145         curParent = shortcutsTable->topLevelItem(i);
1146         for (int j = 0; j < curParent->childCount(); ++j) {
1147             curChild = curParent->child(j);
1148             QString key = curChild->data(2, Qt::DisplayRole).toString();
1149             QString name = SettingsCache::instance().shortcuts().getShortcut(key).getName();
1150             QString shortcut = SettingsCache::instance().shortcuts().getShortcutString(key);
1151             curChild->setText(0, name);
1152             curChild->setText(1, shortcut);
1153 
1154             if (j == 0) {
1155                 // the first child also updates the parent's group name
1156                 QString group = SettingsCache::instance().shortcuts().getShortcut(key).getGroupName();
1157                 curParent->setText(0, group);
1158             }
1159         }
1160     }
1161     shortcutsTable->sortItems(0, Qt::AscendingOrder);
1162     currentItemChanged(shortcutsTable->currentItem(), nullptr);
1163 }
1164 
clearShortcuts()1165 void ShortcutSettingsPage::clearShortcuts()
1166 {
1167     if (QMessageBox::question(this, tr("Clear all default shortcuts"),
1168                               tr("Do you really want to clear all shortcuts?")) == QMessageBox::Yes) {
1169         SettingsCache::instance().shortcuts().clearAllShortcuts();
1170     }
1171 }
1172 
retranslateUi()1173 void ShortcutSettingsPage::retranslateUi()
1174 {
1175     shortcutsTable->setHeaderLabels(QStringList() << tr("Action") << tr("Shortcut"));
1176     refreshShortcuts();
1177 
1178     currentActionGroupLabel->setText(tr("Section:"));
1179     currentActionLabel->setText(tr("Action:"));
1180     currentShortcutLabel->setText(tr("Shortcut:"));
1181     editTextBox->retranslateUi();
1182     faqLabel->setText(QString("<a href='%1'>%2</a>").arg(WIKI_CUSTOM_SHORTCUTS).arg(tr("How to set custom shortcuts")));
1183     btnResetAll->setText("Restore all default shortcuts");
1184     btnClearAll->setText("Clear all shortcuts");
1185 }
1186 
DlgSettings(QWidget * parent)1187 DlgSettings::DlgSettings(QWidget *parent) : QDialog(parent)
1188 {
1189 #if (QT_VERSION >= QT_VERSION_CHECK(5, 13, 0))
1190     QRect rec = qApp->primaryScreen()->availableGeometry();
1191 #else
1192     QRect rec = QApplication::desktop()->availableGeometry();
1193 #endif
1194     this->setMinimumSize(rec.width() / 2, rec.height() - 100);
1195     this->setBaseSize(rec.width(), rec.height());
1196 
1197     connect(&SettingsCache::instance(), SIGNAL(langChanged()), this, SLOT(updateLanguage()));
1198 
1199     contentsWidget = new QListWidget;
1200     contentsWidget->setViewMode(QListView::IconMode);
1201     contentsWidget->setIconSize(QSize(58, 50));
1202     contentsWidget->setMovement(QListView::Static);
1203     contentsWidget->setMinimumHeight(85);
1204     contentsWidget->setMaximumHeight(85);
1205     contentsWidget->setSpacing(5);
1206 
1207     pagesWidget = new QStackedWidget;
1208     pagesWidget->addWidget(new GeneralSettingsPage);
1209     pagesWidget->addWidget(new AppearanceSettingsPage);
1210     pagesWidget->addWidget(new UserInterfaceSettingsPage);
1211     pagesWidget->addWidget(new DeckEditorSettingsPage);
1212     pagesWidget->addWidget(new MessagesSettingsPage);
1213     pagesWidget->addWidget(new SoundSettingsPage);
1214     pagesWidget->addWidget(new ShortcutSettingsPage);
1215 
1216     createIcons();
1217     contentsWidget->setCurrentRow(0);
1218 
1219     auto *vboxLayout = new QVBoxLayout;
1220     vboxLayout->addWidget(contentsWidget);
1221     vboxLayout->addWidget(pagesWidget);
1222 
1223     auto *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok);
1224     connect(buttonBox, SIGNAL(accepted()), this, SLOT(close()));
1225 
1226     auto *mainLayout = new QVBoxLayout;
1227     mainLayout->addLayout(vboxLayout);
1228     mainLayout->addSpacing(2);
1229     mainLayout->addWidget(buttonBox);
1230     setLayout(mainLayout);
1231 
1232     retranslateUi();
1233 
1234     adjustSize();
1235 }
1236 
createIcons()1237 void DlgSettings::createIcons()
1238 {
1239     generalButton = new QListWidgetItem(contentsWidget);
1240     generalButton->setTextAlignment(Qt::AlignHCenter);
1241     generalButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
1242     generalButton->setIcon(QPixmap("theme:config/general"));
1243 
1244     appearanceButton = new QListWidgetItem(contentsWidget);
1245     appearanceButton->setTextAlignment(Qt::AlignHCenter);
1246     appearanceButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
1247     appearanceButton->setIcon(QPixmap("theme:config/appearance"));
1248 
1249     userInterfaceButton = new QListWidgetItem(contentsWidget);
1250     userInterfaceButton->setTextAlignment(Qt::AlignHCenter);
1251     userInterfaceButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
1252     userInterfaceButton->setIcon(QPixmap("theme:config/interface"));
1253 
1254     deckEditorButton = new QListWidgetItem(contentsWidget);
1255     deckEditorButton->setTextAlignment(Qt::AlignHCenter);
1256     deckEditorButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
1257     deckEditorButton->setIcon(QPixmap("theme:config/deckeditor"));
1258 
1259     messagesButton = new QListWidgetItem(contentsWidget);
1260     messagesButton->setTextAlignment(Qt::AlignHCenter);
1261     messagesButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
1262     messagesButton->setIcon(QPixmap("theme:config/messages"));
1263 
1264     soundButton = new QListWidgetItem(contentsWidget);
1265     soundButton->setTextAlignment(Qt::AlignHCenter);
1266     soundButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
1267     soundButton->setIcon(QPixmap("theme:config/sound"));
1268 
1269     shortcutsButton = new QListWidgetItem(contentsWidget);
1270     shortcutsButton->setTextAlignment(Qt::AlignHCenter);
1271     shortcutsButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
1272     shortcutsButton->setIcon(QPixmap("theme:config/shorcuts"));
1273 
1274     connect(contentsWidget, SIGNAL(currentItemChanged(QListWidgetItem *, QListWidgetItem *)), this,
1275             SLOT(changePage(QListWidgetItem *, QListWidgetItem *)));
1276 }
1277 
changePage(QListWidgetItem * current,QListWidgetItem * previous)1278 void DlgSettings::changePage(QListWidgetItem *current, QListWidgetItem *previous)
1279 {
1280     if (!current)
1281         current = previous;
1282 
1283     pagesWidget->setCurrentIndex(contentsWidget->row(current));
1284 }
1285 
setTab(int index)1286 void DlgSettings::setTab(int index)
1287 {
1288     if (index <= contentsWidget->count() - 1 && index >= 0) {
1289         changePage(contentsWidget->item(index), contentsWidget->currentItem());
1290         contentsWidget->setCurrentRow(index);
1291     }
1292 }
1293 
updateLanguage()1294 void DlgSettings::updateLanguage()
1295 {
1296     qApp->removeTranslator(translator); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast)
1297     installNewTranslator();
1298 }
1299 
changeEvent(QEvent * event)1300 void DlgSettings::changeEvent(QEvent *event)
1301 {
1302     if (event->type() == QEvent::LanguageChange)
1303         retranslateUi();
1304     QDialog::changeEvent(event);
1305 }
1306 
closeEvent(QCloseEvent * event)1307 void DlgSettings::closeEvent(QCloseEvent *event)
1308 {
1309     bool showLoadError = true;
1310     QString loadErrorMessage = tr("Unknown Error loading card database");
1311     LoadStatus loadStatus = db->getLoadStatus();
1312     qDebug() << "Card Database load status: " << loadStatus;
1313     switch (loadStatus) {
1314         case Ok:
1315             showLoadError = false;
1316             break;
1317         case Invalid:
1318             loadErrorMessage = tr("Your card database is invalid.\n\n"
1319                                   "Cockatrice may not function correctly with an invalid database\n\n"
1320                                   "You may need to rerun oracle to update your card database.\n\n"
1321                                   "Would you like to change your database location setting?");
1322             break;
1323         case VersionTooOld:
1324             loadErrorMessage = tr("Your card database version is too old.\n\n"
1325                                   "This can cause problems loading card information or images\n\n"
1326                                   "Usually this can be fixed by rerunning oracle to to update your card database.\n\n"
1327                                   "Would you like to change your database location setting?");
1328             break;
1329         case NotLoaded:
1330             loadErrorMessage = tr("Your card database did not finish loading\n\n"
1331                                   "Please file a ticket at https://github.com/Cockatrice/Cockatrice/issues with your "
1332                                   "cards.xml attached\n\n"
1333                                   "Would you like to change your database location setting?");
1334             break;
1335         case FileError:
1336             loadErrorMessage = tr("File Error loading your card database.\n\n"
1337                                   "Would you like to change your database location setting?");
1338             break;
1339         case NoCards:
1340             loadErrorMessage = tr("Your card database was loaded but contains no cards.\n\n"
1341                                   "Would you like to change your database location setting?");
1342             break;
1343         default:
1344             loadErrorMessage = tr("Unknown card database load status\n\n"
1345                                   "Please file a ticket at https://github.com/Cockatrice/Cockatrice/issues\n\n"
1346                                   "Would you like to change your database location setting?");
1347 
1348             break;
1349     }
1350 
1351     if (showLoadError) {
1352         if (QMessageBox::critical(this, tr("Error"), loadErrorMessage, QMessageBox::Yes | QMessageBox::No) ==
1353             QMessageBox::Yes) {
1354             event->ignore();
1355             return;
1356         }
1357     }
1358 
1359     if (!QDir(SettingsCache::instance().getDeckPath()).exists() || SettingsCache::instance().getDeckPath().isEmpty()) {
1360         // TODO: Prompt to create it
1361         if (QMessageBox::critical(
1362                 this, tr("Error"),
1363                 tr("The path to your deck directory is invalid. Would you like to go back and set the correct path?"),
1364                 QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
1365             event->ignore();
1366             return;
1367         }
1368     }
1369 
1370     if (!QDir(SettingsCache::instance().getPicsPath()).exists() || SettingsCache::instance().getPicsPath().isEmpty()) {
1371         // TODO: Prompt to create it
1372         if (QMessageBox::critical(this, tr("Error"),
1373                                   tr("The path to your card pictures directory is invalid. Would you like to go back "
1374                                      "and set the correct path?"),
1375                                   QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
1376             event->ignore();
1377             return;
1378         }
1379     }
1380     event->accept();
1381 }
1382 
retranslateUi()1383 void DlgSettings::retranslateUi()
1384 {
1385     setWindowTitle(tr("Settings"));
1386 
1387     generalButton->setText(tr("General"));
1388     appearanceButton->setText(tr("Appearance"));
1389     userInterfaceButton->setText(tr("User Interface"));
1390     deckEditorButton->setText(tr("Card Sources"));
1391     messagesButton->setText(tr("Chat"));
1392     soundButton->setText(tr("Sound"));
1393     shortcutsButton->setText(tr("Shortcuts"));
1394 
1395     for (int i = 0; i < pagesWidget->count(); i++)
1396         dynamic_cast<AbstractSettingsPage *>(pagesWidget->widget(i))->retranslateUi();
1397 
1398     contentsWidget->reset();
1399 }
1400