mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2025-12-10 06:40:29 -08:00
Compare commits
11 Commits
oracle-mem
...
first-run-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06a162c1f3 | ||
|
|
d074fd5491 | ||
|
|
319e8fe7c9 | ||
|
|
d3302d521f | ||
|
|
5c1bb27d5c | ||
|
|
dde36183ce | ||
|
|
7c7f2dd8d5 | ||
|
|
edb0a954e2 | ||
|
|
0a239712dd | ||
|
|
95c3434205 | ||
|
|
f0be6972cc |
@@ -46,7 +46,8 @@ Latest <kbd>beta</kbd> version:
|
||||
|
||||
- [Magic-Token](https://github.com/Cockatrice/Magic-Token): MtG token data to use in Cockatrice
|
||||
- [Magic-Spoiler](https://github.com/Cockatrice/Magic-Spoiler): Script to generate MtG spoiler data from [MTGJSON](https://github.com/mtgjson/mtgjson) to use in Cockatrice
|
||||
- [cockatrice.github.io](https://github.com/Cockatrice/cockatrice.github.io): Code of the official webpage of the Cockatrice project
|
||||
- [cockatrice.github.io](https://github.com/Cockatrice/cockatrice.github.io): Code of the official Cockatrice webpage
|
||||
- [Cockatrice @Flathub](https://github.com/flathub/io.github.Cockatrice.cockatrice): Configuration for our Linux `flatpak` package
|
||||
|
||||
|
||||
# Community Resources [](https://discord.gg/3Z9yzmA)
|
||||
@@ -54,6 +55,7 @@ Latest <kbd>beta</kbd> version:
|
||||
Join our [Discord community](https://discord.gg/3Z9yzmA) to connect with other projet contributors (`#dev` channel) or fellow users of the app. Come here to talk about the application, features, or just to hang out.
|
||||
- [Official Website](https://cockatrice.github.io)
|
||||
- [Official Wiki](https://github.com/Cockatrice/Cockatrice/wiki)
|
||||
- [Official Code Documentation](https://cockatrice.github.io/docs)
|
||||
- [Official Discord](https://discord.gg/3Z9yzmA)
|
||||
- [reddit r/Cockatrice](https://reddit.com/r/cockatrice)
|
||||
|
||||
|
||||
@@ -168,6 +168,9 @@ set(cockatrice_SOURCES
|
||||
src/interface/widgets/general/layout_containers/flow_widget.cpp
|
||||
src/interface/widgets/general/layout_containers/overlap_control_widget.cpp
|
||||
src/interface/widgets/general/layout_containers/overlap_widget.cpp
|
||||
src/interface/widgets/general/tutorial/tutorial_bubble_widget.cpp
|
||||
src/interface/widgets/general/tutorial/tutorial_controller.cpp
|
||||
src/interface/widgets/general/tutorial/tutorial_overlay.cpp
|
||||
src/interface/widgets/menus/deck_editor_menu.cpp
|
||||
src/interface/widgets/printing_selector/all_zones_card_amount_widget.cpp
|
||||
src/interface/widgets/printing_selector/card_amount_widget.cpp
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
[Rules]
|
||||
# The default log level is info
|
||||
*.debug = false
|
||||
#*.info = true
|
||||
#*.warning = true
|
||||
#*.critical = true
|
||||
#*.fatal = true
|
||||
|
||||
# Uncomment a rule to see debug level logs for that category,
|
||||
# or set <category> = false to disable logging
|
||||
|
||||
@@ -350,11 +350,11 @@
|
||||
style="fill-opacity:1;stroke:black;stroke-width:2.78220296;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
|
||||
d="M 49.84375 1.71875 C 36.719738 1.71875 26.0625 12.375988 26.0625 25.5 C 26.0625 32.977454 29.538325 39.612734 34.9375 43.96875 C 24.439951 49.943698 17.919149 62.196126 14.3125 75.65625 C 9.0380874 95.34065 30.224013 98.21875 49.84375 98.21875 C 69.463486 98.21875 90.549327 94.96715 85.375 75.65625 C 81.693381 61.916246 75.224585 49.827177 64.8125 43.9375 C 70.181573 39.580662 73.59375 32.953205 73.59375 25.5 C 73.59375 12.375988 62.967762 1.71875 49.84375 1.71875 z "
|
||||
transform="translate(0,952.36218)"
|
||||
id="right" />
|
||||
id="left" />
|
||||
<path
|
||||
style="opacity:1;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.73577702;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 51.28696,1001.834 0,-46.98372 1.434151,0.16768 c 5.155008,0.60274 9.462857,2.72154 12.938257,6.36366 4.74393,4.9715 6.87913,11.35611 6.16464,18.43328 -0.53702,5.31935 -3.09008,10.59498 -6.83833,14.13074 l -1.94072,1.83069 3.04083,2.20427 c 3.58084,2.5957 7.18975,6.4912 9.55296,10.3116 4.89572,7.9144 9.23593,21.4918 8.50487,26.6055 -0.81312,5.6877 -5.43872,9.6977 -13.62216,11.8093 -3.80822,0.9826 -7.68056,1.4713 -14.763321,1.8633 l -4.471177,0.2474 0,-46.9837 z"
|
||||
id="left"
|
||||
id="right"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="display:inline;fill:url(#linearGradient3);fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:3.77952756;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
|
||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
@@ -321,11 +321,11 @@
|
||||
style="fill-opacity:1;stroke:black;stroke-width:2.78220296;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
|
||||
d="M 49.84375 1.71875 C 36.719738 1.71875 26.0625 12.375988 26.0625 25.5 C 26.0625 32.977454 29.538325 39.612734 34.9375 43.96875 C 24.439951 49.943698 17.919149 62.196126 14.3125 75.65625 C 9.0380874 95.34065 30.224013 98.21875 49.84375 98.21875 C 69.463486 98.21875 90.549327 94.96715 85.375 75.65625 C 81.693381 61.916246 75.224585 49.827177 64.8125 43.9375 C 70.181573 39.580662 73.59375 32.953205 73.59375 25.5 C 73.59375 12.375988 62.967762 1.71875 49.84375 1.71875 z "
|
||||
transform="translate(0,952.36218)"
|
||||
id="right" />
|
||||
id="left" />
|
||||
<path
|
||||
style="opacity:1;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.73577702;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 51.28696,1001.834 0,-46.98372 1.434151,0.16768 c 5.155008,0.60274 9.462857,2.72154 12.938257,6.36366 4.74393,4.9715 6.87913,11.35611 6.16464,18.43328 -0.53702,5.31935 -3.09008,10.59498 -6.83833,14.13074 l -1.94072,1.83069 3.04083,2.20427 c 3.58084,2.5957 7.18975,6.4912 9.55296,10.3116 4.89572,7.9144 9.23593,21.4918 8.50487,26.6055 -0.81312,5.6877 -5.43872,9.6977 -13.62216,11.8093 -3.80822,0.9826 -7.68056,1.4713 -14.763321,1.8633 l -4.471177,0.2474 0,-46.9837 z"
|
||||
id="left"
|
||||
id="right"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
d="m 46.656521,12.167234 18.055171,18.054184 a 6.6081919,6.6078288 0 0 1 -0.126303,9.352065 6.6804126,6.6800456 0 0 1 -8.233169,1.011048 l -7.944268,7.943843 6.463762,6.445343 a 6.9331851,6.9328042 0 0 1 5.741536,2.022073 l 28.057729,28.092294 a 6.9962797,6.9958953 0 0 1 -9.894222,9.893685 L 50.719018,66.907526 A 7.0595711,7.0591833 0 0 1 49.18433,59.270613 l -5.741527,-5.741238 -7.944298,7.943843 A 6.716523,6.7161541 0 0 1 25.134866,69.832263 L 7.079684,51.778091 a 6.716523,6.7161541 0 0 1 8.39566,-10.345064 L 36.31101,20.59853 a 6.716523,6.7161541 0 0 1 10.345612,-8.431329 z"
|
||||
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
@@ -340,11 +340,11 @@
|
||||
style="fill-opacity:1;stroke:black;stroke-width:2.78220296;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
|
||||
d="M 49.84375 1.71875 C 36.719738 1.71875 26.0625 12.375988 26.0625 25.5 C 26.0625 32.977454 29.538325 39.612734 34.9375 43.96875 C 24.439951 49.943698 17.919149 62.196126 14.3125 75.65625 C 9.0380874 95.34065 30.224013 98.21875 49.84375 98.21875 C 69.463486 98.21875 90.549327 94.96715 85.375 75.65625 C 81.693381 61.916246 75.224585 49.827177 64.8125 43.9375 C 70.181573 39.580662 73.59375 32.953205 73.59375 25.5 C 73.59375 12.375988 62.967762 1.71875 49.84375 1.71875 z "
|
||||
transform="translate(0,952.36218)"
|
||||
id="right" />
|
||||
id="left" />
|
||||
<path
|
||||
style="opacity:1;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.73577702;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 51.28696,1001.834 0,-46.98372 1.434151,0.16768 c 5.155008,0.60274 9.462857,2.72154 12.938257,6.36366 4.74393,4.9715 6.87913,11.35611 6.16464,18.43328 -0.53702,5.31935 -3.09008,10.59498 -6.83833,14.13074 l -1.94072,1.83069 3.04083,2.20427 c 3.58084,2.5957 7.18975,6.4912 9.55296,10.3116 4.89572,7.9144 9.23593,21.4918 8.50487,26.6055 -0.81312,5.6877 -5.43872,9.6977 -13.62216,11.8093 -3.80822,0.9826 -7.68056,1.4713 -14.763321,1.8633 l -4.471177,0.2474 0,-46.9837 z"
|
||||
id="left"
|
||||
id="right"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
@@ -154,8 +154,10 @@ void DeckEditorDeckDockWidget::createDeckDock()
|
||||
&DeckEditorDeckDockWidget::setBannerCard);
|
||||
bannerCardComboBox->setHidden(!SettingsCache::instance().getDeckEditorBannerCardComboBoxVisible());
|
||||
|
||||
deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this, deckModel->getDeckList());
|
||||
deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this, deckModel->getDeckList()->getTags());
|
||||
deckTagsDisplayWidget->setHidden(!SettingsCache::instance().getDeckEditorTagsWidgetVisible());
|
||||
connect(deckTagsDisplayWidget, &DeckPreviewDeckTagsDisplayWidget::tagsChanged, this,
|
||||
&DeckEditorDeckDockWidget::setTags);
|
||||
|
||||
activeGroupCriteriaLabel = new QLabel(this);
|
||||
|
||||
@@ -383,6 +385,13 @@ void DeckEditorDeckDockWidget::setBannerCard(int /* changedIndex */)
|
||||
emit deckModified();
|
||||
}
|
||||
|
||||
void DeckEditorDeckDockWidget::setTags(const QStringList &tags)
|
||||
{
|
||||
deckModel->getDeckList()->setTags(tags);
|
||||
deckEditor->setModified(true);
|
||||
emit deckModified();
|
||||
}
|
||||
|
||||
void DeckEditorDeckDockWidget::syncDeckListBannerCardWithComboBox()
|
||||
{
|
||||
auto [name, id] = bannerCardComboBox->currentData().value<QPair<QString, QString>>();
|
||||
@@ -451,7 +460,7 @@ void DeckEditorDeckDockWidget::syncDisplayWidgetsToModel()
|
||||
sortDeckModelToDeckView();
|
||||
expandAll();
|
||||
|
||||
deckTagsDisplayWidget->setDeckList(deckModel->getDeckList());
|
||||
deckTagsDisplayWidget->setTags(deckModel->getDeckList()->getTags());
|
||||
}
|
||||
|
||||
void DeckEditorDeckDockWidget::sortDeckModelToDeckView()
|
||||
@@ -484,7 +493,7 @@ void DeckEditorDeckDockWidget::cleanDeck()
|
||||
emit deckModified();
|
||||
emit deckChanged();
|
||||
updateBannerCardComboBox();
|
||||
deckTagsDisplayWidget->setDeckList(deckModel->getDeckList());
|
||||
deckTagsDisplayWidget->setTags(deckModel->getDeckList()->getTags());
|
||||
}
|
||||
|
||||
void DeckEditorDeckDockWidget::recursiveExpand(const QModelIndex &index)
|
||||
|
||||
@@ -112,6 +112,7 @@ private slots:
|
||||
void updateName(const QString &name);
|
||||
void updateComments();
|
||||
void setBannerCard(int);
|
||||
void setTags(const QStringList &tags);
|
||||
void syncDeckListBannerCardWithComboBox();
|
||||
void updateHash();
|
||||
void refreshShortcuts();
|
||||
|
||||
@@ -109,7 +109,7 @@ void DlgCreateGame::sharedCtor()
|
||||
gameSetupOptionsLayout->addWidget(startingLifeTotalLabel, 0, 0);
|
||||
gameSetupOptionsLayout->addWidget(startingLifeTotalEdit, 0, 1);
|
||||
gameSetupOptionsLayout->addWidget(shareDecklistsOnLoadCheckBox, 1, 0);
|
||||
if (room->getUserInfo()->user_level() & ServerInfo_User::IsJudge) {
|
||||
if (room && room->getUserInfo()->user_level() & ServerInfo_User::IsJudge) {
|
||||
gameSetupOptionsLayout->addWidget(createGameAsJudgeCheckBox, 2, 0);
|
||||
} else {
|
||||
createGameAsJudgeCheckBox->setChecked(false);
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
#include "tutorial_bubble_widget.h"
|
||||
|
||||
BubbleWidget::BubbleWidget(QWidget *parent) : QFrame(parent)
|
||||
{
|
||||
setFrameStyle(QFrame::StyledPanel | QFrame::Raised);
|
||||
setStyleSheet("background:white; border-radius:8px;");
|
||||
|
||||
QGridLayout *layout = new QGridLayout(this);
|
||||
layout->setContentsMargins(12, 10, 12, 10);
|
||||
layout->setHorizontalSpacing(8);
|
||||
layout->setVerticalSpacing(8);
|
||||
|
||||
counterLabel = new QLabel(this);
|
||||
counterLabel->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
|
||||
|
||||
closeButton = new QPushButton("✕", this);
|
||||
closeButton->setFixedSize(20, 20);
|
||||
|
||||
textLabel = new QLabel(this);
|
||||
textLabel->setWordWrap(true);
|
||||
textLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
textLabel->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
|
||||
textLabel->setStyleSheet("color:black;"); // guard against global styles
|
||||
|
||||
// nav buttons
|
||||
previousSequenceButton = new QPushButton("<<", this);
|
||||
previousStepButton = new QPushButton("<", this);
|
||||
nextStepButton = new QPushButton(">", this);
|
||||
nextSequenceButton = new QPushButton(">>", this);
|
||||
|
||||
QHBoxLayout *navLayout = new QHBoxLayout;
|
||||
navLayout->addStretch();
|
||||
navLayout->addWidget(previousSequenceButton);
|
||||
navLayout->addWidget(previousStepButton);
|
||||
navLayout->addWidget(nextStepButton);
|
||||
navLayout->addWidget(nextSequenceButton);
|
||||
|
||||
// Layout
|
||||
layout->addWidget(counterLabel, 0, 0, Qt::AlignLeft | Qt::AlignVCenter);
|
||||
layout->addItem(new QSpacerItem(10, 10, QSizePolicy::Expanding, QSizePolicy::Minimum), 0, 1);
|
||||
layout->addWidget(closeButton, 0, 2, Qt::AlignRight);
|
||||
layout->addWidget(textLabel, 1, 0, 1, 3);
|
||||
layout->addLayout(navLayout, 2, 0, 1, 3);
|
||||
|
||||
// Make column 1 take extra space so text gets room to expand/wrap
|
||||
layout->setColumnStretch(1, 1);
|
||||
|
||||
// sensible default maximum width for bubble so text will wrap
|
||||
setMaximumWidth(420);
|
||||
}
|
||||
|
||||
void BubbleWidget::setText(const QString &text)
|
||||
{
|
||||
textLabel->setText(text);
|
||||
update();
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#ifndef COCKATRICE_TUTORIAL_BUBBLE_WIDGET_H
|
||||
#define COCKATRICE_TUTORIAL_BUBBLE_WIDGET_H
|
||||
#include <QFrame>
|
||||
#include <QGridLayout>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
|
||||
class BubbleWidget : public QFrame
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
QLabel *textLabel;
|
||||
QLabel *counterLabel;
|
||||
QPushButton *closeButton;
|
||||
QPushButton *previousSequenceButton;
|
||||
QPushButton *previousStepButton;
|
||||
QPushButton *nextStepButton;
|
||||
QPushButton *nextSequenceButton;
|
||||
|
||||
BubbleWidget(QWidget *parent);
|
||||
void setText(const QString &text);
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_TUTORIAL_BUBBLE_WIDGET_H
|
||||
@@ -0,0 +1,181 @@
|
||||
#include "tutorial_controller.h"
|
||||
|
||||
#include <QTimer>
|
||||
|
||||
TutorialController::TutorialController(QWidget *_tutorializedWidget)
|
||||
: QObject(_tutorializedWidget), tutorializedWidget(_tutorializedWidget)
|
||||
{
|
||||
tutorialOverlay = new TutorialOverlay(tutorializedWidget->window());
|
||||
|
||||
// Make it frameless + translucent
|
||||
tutorialOverlay->setWindowFlags(tutorialOverlay->windowFlags() | Qt::FramelessWindowHint);
|
||||
tutorialOverlay->setAttribute(Qt::WA_TranslucentBackground);
|
||||
|
||||
// hide until start
|
||||
tutorialOverlay->hide();
|
||||
|
||||
connect(tutorialOverlay, &TutorialOverlay::nextStep, this, &TutorialController::nextStep);
|
||||
connect(tutorialOverlay, &TutorialOverlay::prevStep, this, &TutorialController::prevStep);
|
||||
connect(tutorialOverlay, &TutorialOverlay::nextSequence, this, &TutorialController::nextSequence);
|
||||
connect(tutorialOverlay, &TutorialOverlay::prevSequence, this, &TutorialController::prevSequence);
|
||||
|
||||
connect(tutorialOverlay, &TutorialOverlay::skipTutorial, this, &TutorialController::exitTutorial);
|
||||
}
|
||||
|
||||
void TutorialController::addSequence(const TutorialSequence &seq)
|
||||
{
|
||||
sequences.append(seq);
|
||||
}
|
||||
|
||||
void TutorialController::start()
|
||||
{
|
||||
if (sequences.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QTimer::singleShot(0, this, [this]() {
|
||||
QWidget *win = tutorializedWidget->window();
|
||||
tutorialOverlay->parentResized();
|
||||
tutorialOverlay->setGeometry(QRect(QPoint(0, 0), win->size()));
|
||||
tutorialOverlay->show();
|
||||
tutorialOverlay->raise();
|
||||
tutorialOverlay->parentResized();
|
||||
|
||||
currentSequence = 0;
|
||||
currentStep = 0;
|
||||
showStep();
|
||||
});
|
||||
}
|
||||
|
||||
void TutorialController::nextStep()
|
||||
{
|
||||
// advance within sequence
|
||||
currentStep++;
|
||||
|
||||
if (currentSequence < 0) {
|
||||
return; // defensive in case we haven't started yet
|
||||
}
|
||||
|
||||
if (currentStep >= sequences[currentSequence].steps.size()) {
|
||||
// advance to next sequence
|
||||
nextSequence();
|
||||
return;
|
||||
}
|
||||
|
||||
showStep();
|
||||
}
|
||||
|
||||
void TutorialController::prevStep()
|
||||
{
|
||||
if (currentSequence < 0) {
|
||||
return; // defensive in case we haven't started yet
|
||||
}
|
||||
|
||||
if (currentStep == 0) {
|
||||
prevSequence();
|
||||
return;
|
||||
}
|
||||
|
||||
currentStep--;
|
||||
showStep();
|
||||
}
|
||||
|
||||
void TutorialController::nextSequence()
|
||||
{
|
||||
if (currentSequence < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// run exit for the last step of the current sequence (showStep handles previous onExit,
|
||||
// but ensure we run it here because we're jumping sequence)
|
||||
// We'll increment sequence and then call showStep which will call the onEnter for the new step.
|
||||
currentSequence++;
|
||||
currentStep = 0;
|
||||
|
||||
if (currentSequence >= sequences.size()) {
|
||||
exitTutorial();
|
||||
return;
|
||||
}
|
||||
|
||||
showStep();
|
||||
}
|
||||
|
||||
void TutorialController::prevSequence()
|
||||
{
|
||||
if (currentSequence <= 0) {
|
||||
// already at first sequence -> stay
|
||||
currentStep = 0;
|
||||
showStep();
|
||||
return;
|
||||
}
|
||||
|
||||
currentSequence--;
|
||||
currentStep = 0;
|
||||
showStep();
|
||||
}
|
||||
|
||||
void TutorialController::exitTutorial()
|
||||
{
|
||||
// Run onExit for the current step if present
|
||||
if (currentSequence >= 0 && currentStep >= 0 && currentSequence < sequences.size() &&
|
||||
currentStep < sequences[currentSequence].steps.size()) {
|
||||
const auto &curStep = sequences[currentSequence].steps[currentStep];
|
||||
if (curStep.onExit) {
|
||||
curStep.onExit();
|
||||
}
|
||||
}
|
||||
|
||||
tutorialOverlay->hide();
|
||||
|
||||
// reset indices so start() can be called again cleanly
|
||||
currentSequence = -1;
|
||||
currentStep = -1;
|
||||
}
|
||||
|
||||
void TutorialController::showStep()
|
||||
{
|
||||
// bounds checks
|
||||
if (currentSequence < 0 || currentSequence >= sequences.size()) {
|
||||
return;
|
||||
}
|
||||
const auto &seq = sequences[currentSequence];
|
||||
if (currentStep < 0 || currentStep >= seq.steps.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// run onExit for the previous step (including if previous step was in previous sequence)
|
||||
if (!(currentSequence == 0 && currentStep == 0)) {
|
||||
int prevSeq = currentSequence;
|
||||
int prevStepIndex = currentStep - 1;
|
||||
if (prevStepIndex < 0) {
|
||||
// previous is last step of previous sequence
|
||||
prevSeq = currentSequence - 1;
|
||||
if (prevSeq >= 0) {
|
||||
prevStepIndex = sequences[prevSeq].steps.size() - 1;
|
||||
} else {
|
||||
prevStepIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (prevSeq >= 0 && prevStepIndex >= 0) {
|
||||
const auto &previousStep = sequences[prevSeq].steps[prevStepIndex];
|
||||
if (previousStep.onExit) {
|
||||
previousStep.onExit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// current step
|
||||
const auto &step = seq.steps[currentStep];
|
||||
|
||||
// Run any action associated with this step
|
||||
if (step.onEnter) {
|
||||
step.onEnter();
|
||||
}
|
||||
|
||||
tutorialOverlay->setTargetWidget(step.targetWidget);
|
||||
tutorialOverlay->setText(step.text);
|
||||
tutorialOverlay->parentResized();
|
||||
tutorialOverlay->raise();
|
||||
tutorialOverlay->update();
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
#ifndef COCKATRICE_TUTORIAL_CONTROLLER_H
|
||||
#define COCKATRICE_TUTORIAL_CONTROLLER_H
|
||||
|
||||
#include "tutorial_overlay.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QVector>
|
||||
#include <functional>
|
||||
|
||||
struct TutorialStep
|
||||
{
|
||||
QWidget *targetWidget;
|
||||
QString text;
|
||||
std::function<void()> onEnter = nullptr; // Optional function to run when this step starts
|
||||
std::function<void()> onExit = nullptr; // Optional function to run when step ends
|
||||
};
|
||||
|
||||
struct TutorialSequence
|
||||
{
|
||||
QString name;
|
||||
QVector<TutorialStep> steps;
|
||||
|
||||
void addStep(const TutorialStep &step)
|
||||
{
|
||||
steps.append(step);
|
||||
}
|
||||
};
|
||||
|
||||
class TutorialController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public slots:
|
||||
void start();
|
||||
void nextStep();
|
||||
void prevStep();
|
||||
void nextSequence();
|
||||
void prevSequence();
|
||||
void exitTutorial();
|
||||
|
||||
public:
|
||||
explicit TutorialController(QWidget *_tutorializedWidget);
|
||||
|
||||
void addSequence(const TutorialSequence &step);
|
||||
|
||||
private:
|
||||
QWidget *tutorializedWidget;
|
||||
QVector<TutorialSequence> sequences;
|
||||
int currentSequence = -1;
|
||||
int currentStep = -1;
|
||||
|
||||
TutorialOverlay *tutorialOverlay;
|
||||
|
||||
void showStep();
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_TUTORIAL_CONTROLLER_H
|
||||
@@ -0,0 +1,178 @@
|
||||
#include "tutorial_overlay.h"
|
||||
|
||||
#include <QLabel>
|
||||
#include <QPaintEvent>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
TutorialOverlay::TutorialOverlay(QWidget *parent) : QWidget(parent, Qt::Window)
|
||||
{
|
||||
setAttribute(Qt::WA_TransparentForMouseEvents, false);
|
||||
setAttribute(Qt::WA_NoSystemBackground, true);
|
||||
setAttribute(Qt::WA_TranslucentBackground, true);
|
||||
setWindowFlags(Qt::Tool | Qt::FramelessWindowHint);
|
||||
|
||||
// This ensures the overlay stays exactly over the parent
|
||||
if (parent) {
|
||||
QRect r = parent->rect();
|
||||
|
||||
// convert the parent’s rect to screen coordinates
|
||||
QPoint globalTopLeft = parent->mapToGlobal(QPoint(0, 0));
|
||||
r.moveTopLeft(globalTopLeft);
|
||||
|
||||
setGeometry(r);
|
||||
}
|
||||
|
||||
bubble = new BubbleWidget(this);
|
||||
bubble->hide();
|
||||
|
||||
connect(bubble->nextStepButton, &QPushButton::clicked, this, &TutorialOverlay::nextStep);
|
||||
connect(bubble->previousStepButton, &QPushButton::clicked, this, &TutorialOverlay::prevStep);
|
||||
connect(bubble->previousSequenceButton, &QPushButton::clicked, this, &TutorialOverlay::prevSequence);
|
||||
connect(bubble->nextSequenceButton, &QPushButton::clicked, this, &TutorialOverlay::nextSequence);
|
||||
connect(bubble->closeButton, &QPushButton::clicked, this, &TutorialOverlay::skipTutorial);
|
||||
}
|
||||
|
||||
void TutorialOverlay::setTargetWidget(QWidget *w)
|
||||
{
|
||||
targetWidget = w;
|
||||
update();
|
||||
}
|
||||
|
||||
void TutorialOverlay::setText(const QString &t)
|
||||
{
|
||||
tutorialText = t;
|
||||
bubble->setText(tutorialText);
|
||||
bubble->adjustSize(); // let layout recalc sizes
|
||||
QSize bsize = bubble->sizeHint();
|
||||
|
||||
const QSize minSize(160, 60);
|
||||
if (bsize.width() < minSize.width()) {
|
||||
bsize.setWidth(minSize.width());
|
||||
}
|
||||
if (bsize.height() < minSize.height()) {
|
||||
bsize.setHeight(minSize.height());
|
||||
}
|
||||
|
||||
// Compute the bubble rect from the current target hole
|
||||
QRect hole = targetRectOnOverlay().adjusted(-6, -6, 6, 6);
|
||||
highlightBubbleRect = computeBubbleRect(hole, bsize);
|
||||
|
||||
bubble->setGeometry(highlightBubbleRect);
|
||||
bubble->raise();
|
||||
bubble->show();
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
void TutorialOverlay::showEvent(QShowEvent *)
|
||||
{
|
||||
raise();
|
||||
}
|
||||
|
||||
void TutorialOverlay::resizeEvent(QResizeEvent *)
|
||||
{
|
||||
update();
|
||||
}
|
||||
|
||||
QRect TutorialOverlay::targetRectOnOverlay() const
|
||||
{
|
||||
if (!targetWidget) {
|
||||
return QRect();
|
||||
}
|
||||
|
||||
// Widget -> global screen coordinates
|
||||
QPoint globalTopLeft = targetWidget->mapToGlobal(QPoint(0, 0));
|
||||
|
||||
// Global -> overlay-local coordinates
|
||||
QPoint localTopLeft = mapFromGlobal(globalTopLeft);
|
||||
|
||||
return QRect(localTopLeft, targetWidget->size());
|
||||
}
|
||||
|
||||
QRect TutorialOverlay::computeBubbleRect(const QRect &hole, const QSize &bubbleSize) const
|
||||
{
|
||||
const int margin = 16;
|
||||
QRect r = rect(); // overlay bounds
|
||||
QRect bubble;
|
||||
|
||||
// Try right
|
||||
bubble = QRect(hole.right() + margin, hole.top(), bubbleSize.width(), bubbleSize.height());
|
||||
if (r.contains(bubble)) {
|
||||
return bubble;
|
||||
}
|
||||
|
||||
// Try left
|
||||
bubble = QRect(hole.left() - margin - bubbleSize.width(), hole.top(), bubbleSize.width(), bubbleSize.height());
|
||||
if (r.contains(bubble)) {
|
||||
return bubble;
|
||||
}
|
||||
|
||||
// Try above, centered
|
||||
bubble = QRect(hole.center().x() - bubbleSize.width() / 2, hole.top() - margin - bubbleSize.height(),
|
||||
bubbleSize.width(), bubbleSize.height());
|
||||
if (r.contains(bubble)) {
|
||||
return bubble;
|
||||
}
|
||||
|
||||
// Try below, centered
|
||||
bubble = QRect(hole.center().x() - bubbleSize.width() / 2, hole.bottom() + margin, bubbleSize.width(),
|
||||
bubbleSize.height());
|
||||
if (r.contains(bubble)) {
|
||||
return bubble;
|
||||
}
|
||||
|
||||
// Last-resort: clamp inside overlay
|
||||
bubble.moveLeft(std::max(r.left(), std::min(bubble.left(), r.right() - bubbleSize.width())));
|
||||
bubble.moveTop(std::max(r.top(), std::min(bubble.top(), r.bottom() - bubbleSize.height())));
|
||||
bubble.setSize(bubbleSize);
|
||||
return bubble;
|
||||
}
|
||||
|
||||
void TutorialOverlay::parentResized()
|
||||
{
|
||||
if (parentWidget()) {
|
||||
QRect r = parentWidget()->rect();
|
||||
QPoint globalTopLeft = parentWidget()->mapToGlobal(QPoint(0, 0));
|
||||
r.moveTopLeft(globalTopLeft);
|
||||
setGeometry(r);
|
||||
}
|
||||
}
|
||||
|
||||
void TutorialOverlay::paintEvent(QPaintEvent *)
|
||||
{
|
||||
QPainter p(this);
|
||||
p.setRenderHint(QPainter::Antialiasing, true);
|
||||
|
||||
QColor overlay(0, 0, 0, 160);
|
||||
p.fillRect(rect(), overlay);
|
||||
|
||||
QRect hole = targetRectOnOverlay().adjusted(-6, -6, 6, 6);
|
||||
if (!hole.isEmpty()) {
|
||||
QPainterPath path;
|
||||
path.addRect(rect());
|
||||
QPainterPath holePath;
|
||||
holePath.addRoundedRect(hole, 8, 8);
|
||||
path = path.subtracted(holePath);
|
||||
|
||||
p.setCompositionMode(QPainter::CompositionMode_Clear);
|
||||
p.fillPath(holePath, Qt::transparent);
|
||||
p.setCompositionMode(QPainter::CompositionMode_SourceOver);
|
||||
}
|
||||
|
||||
// recompute bubble size/position in case available geometry changed:
|
||||
bubble->adjustSize();
|
||||
QSize bsize = bubble->sizeHint();
|
||||
const QSize minSize(160, 60);
|
||||
if (bsize.width() < minSize.width())
|
||||
bsize.setWidth(minSize.width());
|
||||
if (bsize.height() < minSize.height())
|
||||
bsize.setHeight(minSize.height());
|
||||
|
||||
highlightBubbleRect = computeBubbleRect(hole, bsize);
|
||||
bubble->setGeometry(highlightBubbleRect);
|
||||
bubble->raise();
|
||||
bubble->show();
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#ifndef COCKATRICE_TUTORIAL_OVERLAY_H
|
||||
#define COCKATRICE_TUTORIAL_OVERLAY_H
|
||||
|
||||
#include "tutorial_bubble_widget.h"
|
||||
|
||||
#include <QPointer>
|
||||
#include <QWidget>
|
||||
|
||||
class TutorialOverlay : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit TutorialOverlay(QWidget *parent = nullptr);
|
||||
|
||||
void setTargetWidget(QWidget *w);
|
||||
void setText(const QString &t);
|
||||
void parentResized();
|
||||
|
||||
signals:
|
||||
void nextStep();
|
||||
void prevStep();
|
||||
void nextSequence();
|
||||
void prevSequence();
|
||||
void skipTutorial();
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *) override;
|
||||
void resizeEvent(QResizeEvent *) override;
|
||||
void showEvent(QShowEvent *) override;
|
||||
|
||||
private:
|
||||
QRect targetRectOnOverlay() const;
|
||||
QRect computeBubbleRect(const QRect &hole, const QSize &bubbleSize) const;
|
||||
|
||||
QPointer<QWidget> targetWidget;
|
||||
QString tutorialText;
|
||||
|
||||
QRect highlightBubbleRect;
|
||||
BubbleWidget *bubble;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_TUTORIAL_OVERLAY_H
|
||||
@@ -18,6 +18,16 @@ VisualDatabaseDisplayFilterSaveLoadWidget::VisualDatabaseDisplayFilterSaveLoadWi
|
||||
layout = new QVBoxLayout(this);
|
||||
setLayout(layout);
|
||||
|
||||
// Filter search input
|
||||
searchInput = new QLineEdit(this);
|
||||
layout->addWidget(searchInput);
|
||||
|
||||
connect(searchInput, &QLineEdit::textChanged, this, &VisualDatabaseDisplayFilterSaveLoadWidget::applySearchFilter);
|
||||
|
||||
// File list container
|
||||
fileListWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded);
|
||||
layout->addWidget(fileListWidget);
|
||||
|
||||
// Input for filter filename
|
||||
filenameInput = new QLineEdit(this);
|
||||
layout->addWidget(filenameInput);
|
||||
@@ -25,11 +35,12 @@ VisualDatabaseDisplayFilterSaveLoadWidget::VisualDatabaseDisplayFilterSaveLoadWi
|
||||
// Save button
|
||||
saveButton = new QPushButton(this);
|
||||
layout->addWidget(saveButton);
|
||||
connect(saveButton, &QPushButton::clicked, this, &VisualDatabaseDisplayFilterSaveLoadWidget::saveFilter);
|
||||
// Disable save if empty
|
||||
saveButton->setEnabled(false);
|
||||
connect(filenameInput, &QLineEdit::textChanged, this,
|
||||
[this](const QString &text) { saveButton->setEnabled(!text.trimmed().isEmpty()); });
|
||||
|
||||
// File list container
|
||||
fileListWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded);
|
||||
layout->addWidget(fileListWidget);
|
||||
connect(saveButton, &QPushButton::clicked, this, &VisualDatabaseDisplayFilterSaveLoadWidget::saveFilter);
|
||||
|
||||
refreshFilterList(); // Populate the file list on startup
|
||||
retranslateUi();
|
||||
@@ -37,6 +48,7 @@ VisualDatabaseDisplayFilterSaveLoadWidget::VisualDatabaseDisplayFilterSaveLoadWi
|
||||
|
||||
void VisualDatabaseDisplayFilterSaveLoadWidget::retranslateUi()
|
||||
{
|
||||
searchInput->setPlaceholderText(tr("Search filter..."));
|
||||
saveButton->setText(tr("Save Filter"));
|
||||
saveButton->setToolTip(tr("Save all currently applied filters to a file"));
|
||||
filenameInput->setPlaceholderText(tr("Enter filename..."));
|
||||
@@ -112,42 +124,36 @@ void VisualDatabaseDisplayFilterSaveLoadWidget::loadFilter(const QString &filena
|
||||
emit filterModel->layoutChanged();
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayFilterSaveLoadWidget::applySearchFilter(const QString &text)
|
||||
{
|
||||
fileListWidget->clearLayout();
|
||||
|
||||
QString filter = text.trimmed();
|
||||
QStringList filtered = allFilterFiles;
|
||||
|
||||
if (!filter.isEmpty()) {
|
||||
filtered = filtered.filter(QRegularExpression(filter, QRegularExpression::CaseInsensitiveOption));
|
||||
}
|
||||
|
||||
for (const QString &filename : filtered) {
|
||||
FilterDisplayWidget *filterWidget = new FilterDisplayWidget(this, filename, filterModel);
|
||||
fileListWidget->addWidget(filterWidget);
|
||||
|
||||
connect(filterWidget, &FilterDisplayWidget::filterLoadRequested, this,
|
||||
&VisualDatabaseDisplayFilterSaveLoadWidget::loadFilter);
|
||||
|
||||
connect(filterWidget, &FilterDisplayWidget::filterDeleted, this,
|
||||
&VisualDatabaseDisplayFilterSaveLoadWidget::refreshFilterList);
|
||||
}
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayFilterSaveLoadWidget::refreshFilterList()
|
||||
{
|
||||
fileListWidget->clearLayout();
|
||||
// Clear existing widgets
|
||||
for (auto buttonPair : fileButtons) {
|
||||
buttonPair.first->deleteLater();
|
||||
buttonPair.second->deleteLater();
|
||||
}
|
||||
fileButtons.clear(); // Clear the list of buttons
|
||||
fileButtons.clear();
|
||||
|
||||
// Refresh the filter file list
|
||||
QDir dir(SettingsCache::instance().getFiltersPath());
|
||||
QStringList filterFiles = dir.entryList(QStringList() << "*.json", QDir::Files, QDir::Name);
|
||||
allFilterFiles = dir.entryList({"*.json"}, QDir::Files, QDir::Name);
|
||||
|
||||
// Loop through the filter files and create widgets for them
|
||||
for (const QString &filename : filterFiles) {
|
||||
bool alreadyAdded = false;
|
||||
|
||||
// Check if the widget for this filter file already exists to avoid duplicates
|
||||
for (const auto &pair : fileButtons) {
|
||||
if (pair.first->text() == filename) {
|
||||
alreadyAdded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!alreadyAdded) {
|
||||
// Create a new custom widget for the filter
|
||||
FilterDisplayWidget *filterWidget = new FilterDisplayWidget(this, filename, filterModel);
|
||||
fileListWidget->addWidget(filterWidget);
|
||||
|
||||
// Connect signals to handle loading and deletion
|
||||
connect(filterWidget, &FilterDisplayWidget::filterLoadRequested, this,
|
||||
&VisualDatabaseDisplayFilterSaveLoadWidget::loadFilter);
|
||||
connect(filterWidget, &FilterDisplayWidget::filterDeleted, this,
|
||||
&VisualDatabaseDisplayFilterSaveLoadWidget::refreshFilterList);
|
||||
}
|
||||
}
|
||||
applySearchFilter(searchInput->text());
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ public:
|
||||
|
||||
void saveFilter();
|
||||
void loadFilter(const QString &filename);
|
||||
void applySearchFilter(const QString &text);
|
||||
void refreshFilterList();
|
||||
void deleteFilter(const QString &filename, QPushButton *deleteButton);
|
||||
|
||||
@@ -37,9 +38,11 @@ private:
|
||||
FilterTreeModel *filterModel;
|
||||
|
||||
QVBoxLayout *layout;
|
||||
QLineEdit *searchInput;
|
||||
FlowWidget *fileListWidget;
|
||||
QLineEdit *filenameInput;
|
||||
QPushButton *saveButton;
|
||||
FlowWidget *fileListWidget;
|
||||
QStringList allFilterFiles;
|
||||
|
||||
QMap<QString, QPair<QPushButton *, QPushButton *>> fileButtons;
|
||||
};
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
#include "../tabs/visual_deck_editor/tab_deck_editor_visual.h"
|
||||
|
||||
VisualDeckDisplayOptionsWidget::VisualDeckDisplayOptionsWidget(QWidget *parent)
|
||||
#include <libcockatrice/utility/qt_utils.h>
|
||||
|
||||
VisualDeckDisplayOptionsWidget::VisualDeckDisplayOptionsWidget(QWidget *parent) : QWidget(parent)
|
||||
{
|
||||
groupAndSortLayout = new QHBoxLayout(this);
|
||||
groupAndSortLayout->setAlignment(Qt::AlignLeft);
|
||||
@@ -11,23 +13,19 @@ VisualDeckDisplayOptionsWidget::VisualDeckDisplayOptionsWidget(QWidget *parent)
|
||||
groupByLabel = new QLabel(this);
|
||||
|
||||
groupByComboBox = new QComboBox(this);
|
||||
if (auto visualDeckEditorWidget = qobject_cast<VisualDeckEditorWidget *>(parent)) {
|
||||
if (auto tabWidget = qobject_cast<TabDeckEditorVisualTabWidget *>(visualDeckEditorWidget)) {
|
||||
// Inside a central widget QWidget container inside TabDeckEditorVisual
|
||||
if (auto tab = qobject_cast<TabDeckEditorVisual *>(tabWidget->parent()->parent())) {
|
||||
auto originalBox = tab->getDeckDockWidget()->getGroupByComboBox();
|
||||
groupByComboBox->setModel(originalBox->model());
|
||||
groupByComboBox->setModelColumn(originalBox->modelColumn());
|
||||
|
||||
// Original -> clone
|
||||
connect(originalBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
[this](int index) { groupByComboBox->setCurrentIndex(index); });
|
||||
if (auto tab = QtUtils::findParentOfType<TabDeckEditorVisual>(this)) {
|
||||
auto originalBox = tab->getDeckDockWidget()->getGroupByComboBox();
|
||||
groupByComboBox->setModel(originalBox->model());
|
||||
groupByComboBox->setModelColumn(originalBox->modelColumn());
|
||||
|
||||
// Clone -> original
|
||||
connect(groupByComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
[originalBox](int index) { originalBox->setCurrentIndex(index); });
|
||||
}
|
||||
}
|
||||
// Original -> clone
|
||||
connect(originalBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
[this](int index) { groupByComboBox->setCurrentIndex(index); });
|
||||
|
||||
// Clone -> original
|
||||
connect(groupByComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
[originalBox](int index) { originalBox->setCurrentIndex(index); });
|
||||
} else {
|
||||
groupByComboBox->addItem(
|
||||
tr(qPrintable(DeckListModelGroupCriteria::toString(DeckListModelGroupCriteria::MAIN_TYPE))),
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
#include <QHBoxLayout>
|
||||
#include <QMessageBox>
|
||||
|
||||
DeckPreviewDeckTagsDisplayWidget::DeckPreviewDeckTagsDisplayWidget(QWidget *_parent, DeckList *_deckList)
|
||||
: QWidget(_parent), deckList(nullptr)
|
||||
DeckPreviewDeckTagsDisplayWidget::DeckPreviewDeckTagsDisplayWidget(QWidget *_parent, const QStringList &_tags)
|
||||
: QWidget(_parent), currentTags(_tags)
|
||||
{
|
||||
|
||||
setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
|
||||
@@ -27,16 +27,14 @@ DeckPreviewDeckTagsDisplayWidget::DeckPreviewDeckTagsDisplayWidget(QWidget *_par
|
||||
|
||||
flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded);
|
||||
|
||||
if (_deckList) {
|
||||
setDeckList(_deckList);
|
||||
}
|
||||
|
||||
layout->addWidget(flowWidget);
|
||||
|
||||
refreshTags();
|
||||
}
|
||||
|
||||
void DeckPreviewDeckTagsDisplayWidget::setDeckList(DeckList *_deckList)
|
||||
void DeckPreviewDeckTagsDisplayWidget::setTags(const QStringList &_tags)
|
||||
{
|
||||
deckList = _deckList;
|
||||
currentTags = _tags;
|
||||
refreshTags();
|
||||
}
|
||||
|
||||
@@ -44,7 +42,7 @@ void DeckPreviewDeckTagsDisplayWidget::refreshTags()
|
||||
{
|
||||
flowWidget->clearLayout();
|
||||
|
||||
for (const QString &tag : deckList->getTags()) {
|
||||
for (const QString &tag : currentTags) {
|
||||
flowWidget->addWidget(new DeckPreviewTagDisplayWidget(this, tag));
|
||||
}
|
||||
|
||||
@@ -71,7 +69,45 @@ static QStringList getAllFiles(const QString &filePath)
|
||||
return allFiles;
|
||||
}
|
||||
|
||||
bool confirmOverwriteIfExists(QWidget *parent, const QString &filePath)
|
||||
/**
|
||||
* Gets all tags that appear in the deck folder
|
||||
*/
|
||||
static QStringList findAllKnownTags()
|
||||
{
|
||||
QStringList allFiles = getAllFiles(SettingsCache::instance().getDeckPath());
|
||||
|
||||
QStringList knownTags;
|
||||
auto loader = DeckLoader(nullptr);
|
||||
for (const QString &file : allFiles) {
|
||||
loader.loadFromFile(file, DeckLoader::getFormatFromName(file), false);
|
||||
QStringList tags = loader.getDeckList()->getTags();
|
||||
knownTags.append(tags);
|
||||
knownTags.removeDuplicates();
|
||||
}
|
||||
|
||||
return knownTags;
|
||||
}
|
||||
|
||||
void DeckPreviewDeckTagsDisplayWidget::openTagEditDlg()
|
||||
{
|
||||
if (qobject_cast<DeckPreviewWidget *>(parentWidget())) {
|
||||
// If we're the child of a DeckPreviewWidget, then we need to handle conversion
|
||||
auto *deckPreviewWidget = qobject_cast<DeckPreviewWidget *>(parentWidget());
|
||||
|
||||
bool canAddTags = promptFileConversionIfRequired(deckPreviewWidget);
|
||||
|
||||
if (canAddTags) {
|
||||
QStringList knownTags = deckPreviewWidget->visualDeckStorageWidget->tagFilterWidget->getAllKnownTags();
|
||||
execTagDialog(knownTags);
|
||||
}
|
||||
} else {
|
||||
// If we're the child of an AbstractTabDeckEditor, then we don't bother with conversion
|
||||
QStringList knownTags = findAllKnownTags();
|
||||
execTagDialog(knownTags);
|
||||
}
|
||||
}
|
||||
|
||||
static bool confirmOverwriteIfExists(QWidget *parent, const QString &filePath)
|
||||
{
|
||||
QFileInfo fileInfo(filePath);
|
||||
QString newFileName = QDir::toNativeSeparators(fileInfo.path() + "/" + fileInfo.completeBaseName() + ".cod");
|
||||
@@ -86,98 +122,70 @@ bool confirmOverwriteIfExists(QWidget *parent, const QString &filePath)
|
||||
return true; // Safe to proceed
|
||||
}
|
||||
|
||||
void DeckPreviewDeckTagsDisplayWidget::openTagEditDlg()
|
||||
static void convertFileToCockatriceFormat(DeckPreviewWidget *deckPreviewWidget)
|
||||
{
|
||||
if (qobject_cast<DeckPreviewWidget *>(parentWidget())) {
|
||||
auto *deckPreviewWidget = qobject_cast<DeckPreviewWidget *>(parentWidget());
|
||||
QStringList knownTags = deckPreviewWidget->visualDeckStorageWidget->tagFilterWidget->getAllKnownTags();
|
||||
QStringList activeTags = deckList->getTags();
|
||||
deckPreviewWidget->deckLoader->convertToCockatriceFormat(deckPreviewWidget->filePath);
|
||||
deckPreviewWidget->filePath = deckPreviewWidget->deckLoader->getLastLoadInfo().fileName;
|
||||
deckPreviewWidget->refreshBannerCardText();
|
||||
}
|
||||
|
||||
bool canAddTags = true;
|
||||
/**
|
||||
* Checks if the deck's file format supports tags.
|
||||
* If not, then prompt the user for file conversion.
|
||||
* @return whether the resulting file can support adding tags
|
||||
*/
|
||||
bool DeckPreviewDeckTagsDisplayWidget::promptFileConversionIfRequired(DeckPreviewWidget *deckPreviewWidget)
|
||||
{
|
||||
if (DeckLoader::getFormatFromName(deckPreviewWidget->filePath) == DeckLoader::CockatriceFormat) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (DeckLoader::getFormatFromName(deckPreviewWidget->filePath) != DeckLoader::CockatriceFormat) {
|
||||
canAddTags = false;
|
||||
// Retrieve saved preference if the prompt is disabled
|
||||
if (!SettingsCache::instance().getVisualDeckStoragePromptForConversion()) {
|
||||
if (SettingsCache::instance().getVisualDeckStorageAlwaysConvert()) {
|
||||
|
||||
if (!confirmOverwriteIfExists(this, deckPreviewWidget->filePath))
|
||||
return;
|
||||
|
||||
deckPreviewWidget->deckLoader->convertToCockatriceFormat(deckPreviewWidget->filePath);
|
||||
deckPreviewWidget->filePath = deckPreviewWidget->deckLoader->getLastLoadInfo().fileName;
|
||||
deckPreviewWidget->refreshBannerCardText();
|
||||
canAddTags = true;
|
||||
}
|
||||
} else {
|
||||
// Show the dialog to the user
|
||||
DialogConvertDeckToCodFormat conversionDialog(parentWidget());
|
||||
if (conversionDialog.exec() == QDialog::Accepted) {
|
||||
|
||||
if (!confirmOverwriteIfExists(this, deckPreviewWidget->filePath))
|
||||
return;
|
||||
|
||||
deckPreviewWidget->deckLoader->convertToCockatriceFormat(deckPreviewWidget->filePath);
|
||||
deckPreviewWidget->filePath = deckPreviewWidget->deckLoader->getLastLoadInfo().fileName;
|
||||
deckPreviewWidget->refreshBannerCardText();
|
||||
canAddTags = true;
|
||||
|
||||
if (conversionDialog.dontAskAgain()) {
|
||||
SettingsCache::instance().setVisualDeckStoragePromptForConversion(false);
|
||||
SettingsCache::instance().setVisualDeckStorageAlwaysConvert(true);
|
||||
}
|
||||
} else {
|
||||
SettingsCache::instance().setVisualDeckStorageAlwaysConvert(false);
|
||||
|
||||
if (conversionDialog.dontAskAgain()) {
|
||||
SettingsCache::instance().setVisualDeckStoragePromptForConversion(false);
|
||||
} else {
|
||||
SettingsCache::instance().setVisualDeckStoragePromptForConversion(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Retrieve saved preference if the prompt is disabled
|
||||
if (!SettingsCache::instance().getVisualDeckStoragePromptForConversion()) {
|
||||
if (!SettingsCache::instance().getVisualDeckStorageAlwaysConvert()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (canAddTags) {
|
||||
DeckPreviewTagDialog dialog(knownTags, activeTags);
|
||||
if (dialog.exec() == QDialog::Accepted) {
|
||||
QStringList updatedTags = dialog.getActiveTags();
|
||||
deckList->setTags(updatedTags);
|
||||
deckPreviewWidget->deckLoader->saveToFile(deckPreviewWidget->filePath, DeckLoader::CockatriceFormat);
|
||||
refreshTags();
|
||||
}
|
||||
if (!confirmOverwriteIfExists(this, deckPreviewWidget->filePath)) {
|
||||
return false;
|
||||
}
|
||||
} else if (parentWidget()) {
|
||||
// If we're the child of an AbstractTabDeckEditor, we are buried under a ton of childWidgets in the
|
||||
// DeckInfoDock.
|
||||
QWidget *currentParent = parentWidget();
|
||||
while (currentParent) {
|
||||
if (qobject_cast<AbstractTabDeckEditor *>(currentParent)) {
|
||||
break;
|
||||
}
|
||||
currentParent = currentParent->parentWidget();
|
||||
}
|
||||
if (qobject_cast<AbstractTabDeckEditor *>(currentParent)) {
|
||||
auto *deckEditor = qobject_cast<AbstractTabDeckEditor *>(currentParent);
|
||||
QStringList knownTags;
|
||||
QStringList allFiles = getAllFiles(SettingsCache::instance().getDeckPath());
|
||||
DeckLoader loader(this);
|
||||
for (const QString &file : allFiles) {
|
||||
loader.loadFromFile(file, DeckLoader::getFormatFromName(file), false);
|
||||
QStringList tags = loader.getDeckList()->getTags();
|
||||
knownTags.append(tags);
|
||||
knownTags.removeDuplicates();
|
||||
}
|
||||
|
||||
QStringList activeTags = deckList->getTags();
|
||||
convertFileToCockatriceFormat(deckPreviewWidget);
|
||||
return true;
|
||||
}
|
||||
|
||||
DeckPreviewTagDialog dialog(knownTags, activeTags);
|
||||
if (dialog.exec() == QDialog::Accepted) {
|
||||
QStringList updatedTags = dialog.getActiveTags();
|
||||
deckList->setTags(updatedTags);
|
||||
deckEditor->setModified(true);
|
||||
refreshTags();
|
||||
}
|
||||
// Show the dialog to the user
|
||||
DialogConvertDeckToCodFormat conversionDialog(parentWidget());
|
||||
if (conversionDialog.exec() != QDialog::Accepted) {
|
||||
SettingsCache::instance().setVisualDeckStoragePromptForConversion(!conversionDialog.dontAskAgain());
|
||||
SettingsCache::instance().setVisualDeckStorageAlwaysConvert(false);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to convert file
|
||||
if (!confirmOverwriteIfExists(this, deckPreviewWidget->filePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
convertFileToCockatriceFormat(deckPreviewWidget);
|
||||
|
||||
if (conversionDialog.dontAskAgain()) {
|
||||
SettingsCache::instance().setVisualDeckStoragePromptForConversion(false);
|
||||
SettingsCache::instance().setVisualDeckStorageAlwaysConvert(true);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void DeckPreviewDeckTagsDisplayWidget::execTagDialog(const QStringList &knownTags)
|
||||
{
|
||||
DeckPreviewTagDialog dialog(knownTags, currentTags);
|
||||
if (dialog.exec() == QDialog::Accepted) {
|
||||
QStringList updatedTags = dialog.getActiveTags();
|
||||
if (updatedTags != currentTags) {
|
||||
setTags(updatedTags);
|
||||
emit tagsChanged(updatedTags);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,21 +12,31 @@
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
inline bool confirmOverwriteIfExists(QWidget *parent, const QString &filePath);
|
||||
|
||||
class DeckPreviewWidget;
|
||||
class DeckPreviewDeckTagsDisplayWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit DeckPreviewDeckTagsDisplayWidget(QWidget *_parent, DeckList *_deckList);
|
||||
void setDeckList(DeckList *_deckList);
|
||||
void refreshTags();
|
||||
DeckList *deckList;
|
||||
QStringList currentTags;
|
||||
FlowWidget *flowWidget;
|
||||
|
||||
public:
|
||||
explicit DeckPreviewDeckTagsDisplayWidget(QWidget *_parent, const QStringList &_tags);
|
||||
void setTags(const QStringList &_tags);
|
||||
void refreshTags();
|
||||
|
||||
public slots:
|
||||
void openTagEditDlg();
|
||||
|
||||
private:
|
||||
bool promptFileConversionIfRequired(DeckPreviewWidget *deckPreviewWidget);
|
||||
void execTagDialog(const QStringList &knownTags);
|
||||
|
||||
signals:
|
||||
/**
|
||||
* Emitted when the tags have changed due to user interaction.
|
||||
* @param tags The new list of tags.
|
||||
*/
|
||||
void tagsChanged(const QStringList &tags);
|
||||
};
|
||||
#endif // DECK_PREVIEW_DECK_TAGS_DISPLAY_WIDGET_H
|
||||
|
||||
@@ -83,7 +83,8 @@ void DeckPreviewWidget::initializeUi(const bool deckLoadSuccess)
|
||||
setFilePath(deckLoader->getLastLoadInfo().fileName);
|
||||
|
||||
colorIdentityWidget = new ColorIdentityWidget(this, getColorIdentity());
|
||||
deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this, deckLoader->getDeckList());
|
||||
deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this, deckLoader->getDeckList()->getTags());
|
||||
connect(deckTagsDisplayWidget, &DeckPreviewDeckTagsDisplayWidget::tagsChanged, this, &DeckPreviewWidget::setTags);
|
||||
|
||||
bannerCardLabel = new QLabel(this);
|
||||
bannerCardLabel->setObjectName("bannerCardLabel");
|
||||
@@ -307,6 +308,12 @@ void DeckPreviewWidget::imageDoubleClickedEvent(QMouseEvent *event, DeckPreviewC
|
||||
emit deckLoadRequested(filePath);
|
||||
}
|
||||
|
||||
void DeckPreviewWidget::setTags(const QStringList &tags)
|
||||
{
|
||||
deckLoader->getDeckList()->setTags(tags);
|
||||
deckLoader->saveToFile(filePath, DeckLoader::CockatriceFormat);
|
||||
}
|
||||
|
||||
QMenu *DeckPreviewWidget::createRightClickMenu()
|
||||
{
|
||||
auto *menu = new QMenu(this);
|
||||
|
||||
@@ -72,6 +72,8 @@ private:
|
||||
void addSetBannerCardMenu(QMenu *menu);
|
||||
|
||||
private slots:
|
||||
void setTags(const QStringList &tags);
|
||||
|
||||
void actRenameDeck();
|
||||
void actRenameFile();
|
||||
void actDeleteFile();
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
@page developer_reference Developer Reference
|
||||
|
||||
- @subpage logging
|
||||
|
||||
- @subpage primer_cards
|
||||
|
||||
- @subpage card_database_schema_and_parsing
|
||||
|
||||
184
doc/doxygen/extra-pages/developer_documentation/logging.md
Normal file
184
doc/doxygen/extra-pages/developer_documentation/logging.md
Normal file
@@ -0,0 +1,184 @@
|
||||
@page logging Logging
|
||||
|
||||
Cockatrice uses QtLogging from the QtCore module for its logging. See
|
||||
the [official documentation](https://doc.qt.io/qt-6/qtlogging.html) for further details.
|
||||
|
||||
# Log Message Pattern
|
||||
|
||||
Any message logged through the QtLogging system automatically conforms to this message pattern:
|
||||
|
||||
Generic:
|
||||
|
||||
```
|
||||
[<timestamp> <log_level>] [<class:function>] - <message> [<filename>:<line_no>]
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
[2025-12-05 14:48:25.908 I] [MainWindow::startupConfigCheck] - Startup: found config with current version [window_main.cpp:951]
|
||||
```
|
||||
|
||||
For more information, see [Logging Setup](#logging-setup).
|
||||
|
||||
# Log Level and Categories
|
||||
|
||||
\note The default log level for the application is info.
|
||||
|
||||
This means that you should only use qInfo() in production-level code if you are truly sure that this message is
|
||||
beneficial to end-users and other developers. As a general rule, if your functionality logs to info more than twice in
|
||||
response to a user interaction, you are advised to consider moving some of these logs down to the debug level.
|
||||
|
||||
\warning You are strongly advised to avoid the use of the generic logging macros (e.g. qDebug(), qInfo(), qWarn()).
|
||||
|
||||
\note You should instead use the corresponding category logging macros (qCDebug(), qCInfo(), qCWarn()) and define
|
||||
logging
|
||||
categories for your log statements.
|
||||
|
||||
Example:
|
||||
|
||||
```c++
|
||||
in .h
|
||||
|
||||
inline Q_LOGGING_CATEGORY(ExampleCategory, "cockatrice_example_category");
|
||||
inline Q_LOGGING_CATEGORY(ExampleSubCategory, "cockatrice_example_category.sub_category");
|
||||
|
||||
in .cpp
|
||||
|
||||
qCInfo(ExampleCategory) << "Info level logs are usually sent through the main category"
|
||||
qCDebug(ExampleSubCategory) << "Debug level logs are permitted their own category to allow selective silencing"
|
||||
```
|
||||
|
||||
For more information on how to enable or disable logging categories,
|
||||
see [Logging Configuration](#logging-configuration).
|
||||
|
||||
# Logging Configuration
|
||||
|
||||
For configuring our logging, we use the qtlogging.ini, located under cockatrice/resources/config/qtlogging.ini, which is
|
||||
baked into the application in release version and set as the QT_LOGGING_CONF environment variable in main.cpp.
|
||||
|
||||
```c++
|
||||
#ifdef Q_OS_APPLE
|
||||
// <build>/cockatrice/cockatrice.app/Contents/MacOS/cockatrice
|
||||
const QByteArray configPath = "../../../qtlogging.ini";
|
||||
#elif defined(Q_OS_UNIX)
|
||||
// <build>/cockatrice/cockatrice
|
||||
const QByteArray configPath = "./qtlogging.ini";
|
||||
#elif defined(Q_OS_WIN)
|
||||
// <build>/cockatrice/Debug/cockatrice.exe
|
||||
const QByteArray configPath = "../qtlogging.ini";
|
||||
#else
|
||||
const QByteArray configPath = "";
|
||||
#endif
|
||||
|
||||
if (!qEnvironmentVariableIsSet(("QT_LOGGING_CONF"))) {
|
||||
// Set the QT_LOGGING_CONF environment variable
|
||||
qputenv("QT_LOGGING_CONF", configPath);
|
||||
}
|
||||
```
|
||||
|
||||
For more information on how to use this file and on how Qt evaluates which logging rules/file to use, please
|
||||
see the [official Qt documentation](https://doc.qt.io/qt-6/qloggingcategory.html#configuring-categories).
|
||||
|
||||
Some examples:
|
||||
|
||||
```
|
||||
# Turn off all logging except everything from card_picture_loader and all sub categories
|
||||
|
||||
[Rules]
|
||||
# The default log level is info
|
||||
*.debug = false
|
||||
*.info = false
|
||||
*.warning = false
|
||||
*.critical = false
|
||||
*.fatal = false
|
||||
|
||||
card_picture_loader.* = true
|
||||
```
|
||||
|
||||
```
|
||||
# Turn off all logging except info level logs from card_picture_loader and all sub categories
|
||||
|
||||
[Rules]
|
||||
# The default log level is info
|
||||
*.debug = false
|
||||
*.info = false
|
||||
*.warning = false
|
||||
*.critical = false
|
||||
*.fatal = false
|
||||
|
||||
card_picture_loader.*.info = true
|
||||
```
|
||||
|
||||
```
|
||||
[Rules]
|
||||
# Turn on debug level logs for card_picture_loader but keep logging for sub categories suppressed
|
||||
*.debug = false
|
||||
|
||||
card_picture_loader.debug = true
|
||||
```
|
||||
|
||||
```
|
||||
[Rules]
|
||||
# Turn on all logs for worker subcategory of card_picture_loader
|
||||
*.debug = false
|
||||
|
||||
card_picture_loader.worker = true
|
||||
```
|
||||
|
||||
```
|
||||
[Rules]
|
||||
# Turn off some noisy and irrelevant startup logging for local development
|
||||
*.debug = false
|
||||
|
||||
qt_translator = false
|
||||
window_main.* = false
|
||||
release_channel = false
|
||||
spoiler_background_updater = false
|
||||
theme_manager = false
|
||||
sound_engine = false
|
||||
tapped_out_interface = false
|
||||
card_database = false
|
||||
card_database.loading = false
|
||||
card_database.loading.success_or_failure = true
|
||||
cockatrice_xml.* = false
|
||||
```
|
||||
|
||||
# Logging Setup
|
||||
|
||||
This is achieved through our logging setup in @ref main.cpp, where we set the message pattern and install a custom
|
||||
logger which replaces the full file path at the end with just the file name (Qt shows the full and quite lengthy path by
|
||||
default).
|
||||
|
||||
```c++
|
||||
qSetMessagePattern(
|
||||
"\033[0m[%{time yyyy-MM-dd h:mm:ss.zzz} "
|
||||
"%{if-debug}\033[36mD%{endif}%{if-info}\033[32mI%{endif}%{if-warning}\033[33mW%{endif}%{if-critical}\033[31mC%{"
|
||||
"endif}%{if-fatal}\033[1;31mF%{endif}\033[0m] [%{function}] - %{message} [%{file}:%{line}]");
|
||||
QApplication app(argc, argv);
|
||||
|
||||
QObject::connect(&app, &QApplication::lastWindowClosed, &app, &QApplication::quit);
|
||||
|
||||
qInstallMessageHandler(CockatriceLogger);
|
||||
```
|
||||
|
||||
```c++
|
||||
static void CockatriceLogger(QtMsgType type, const QMessageLogContext &ctx, const QString &message)
|
||||
{
|
||||
QString logMessage = qFormatLogMessage(type, ctx, message);
|
||||
|
||||
// Regular expression to match the full path in the square brackets and extract only the filename and line number
|
||||
QRegularExpression regex(R"(\[(?:.:)?[\/\\].*[\/\\]([^\/\\]+\:\d+)\])");
|
||||
QRegularExpressionMatch match = regex.match(logMessage);
|
||||
|
||||
if (match.hasMatch()) {
|
||||
// Extract the filename and line number (e.g., "main.cpp:211")
|
||||
QString filenameLine = match.captured(1);
|
||||
|
||||
// Replace the full path in square brackets with just the filename and line number
|
||||
logMessage.replace(match.captured(0), QString("[%1]").arg(filenameLine));
|
||||
}
|
||||
|
||||
Logger::getInstance().log(type, ctx, logMessage);
|
||||
}
|
||||
```
|
||||
@@ -6,17 +6,12 @@ set(CMAKE_AUTOUIC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
|
||||
set(UTILITY_SOURCES libcockatrice/utility/expression.cpp libcockatrice/utility/levenshtein.cpp
|
||||
libcockatrice/utility/passwordhasher.cpp libcockatrice/utility/system_memory_querier.cpp
|
||||
libcockatrice/utility/passwordhasher.cpp
|
||||
)
|
||||
|
||||
set(UTILITY_HEADERS
|
||||
libcockatrice/utility/color.h
|
||||
libcockatrice/utility/expression.h
|
||||
libcockatrice/utility/levenshtein.h
|
||||
libcockatrice/utility/macros.h
|
||||
libcockatrice/utility/passwordhasher.h
|
||||
libcockatrice/utility/system_memory_querier.h
|
||||
libcockatrice/utility/trice_limits.h
|
||||
libcockatrice/utility/color.h libcockatrice/utility/expression.h libcockatrice/utility/levenshtein.h
|
||||
libcockatrice/utility/macros.h libcockatrice/utility/passwordhasher.h libcockatrice/utility/trice_limits.h
|
||||
)
|
||||
|
||||
add_library(libcockatrice_utility STATIC ${UTILITY_SOURCES} ${UTILITY_HEADERS})
|
||||
|
||||
20
libcockatrice_utility/libcockatrice/utility/qt_utils.h
Normal file
20
libcockatrice_utility/libcockatrice/utility/qt_utils.h
Normal file
@@ -0,0 +1,20 @@
|
||||
#ifndef COCKATRICE_QT_UTILS_H
|
||||
#define COCKATRICE_QT_UTILS_H
|
||||
#include <QObject>
|
||||
|
||||
namespace QtUtils
|
||||
{
|
||||
template <typename T> T *findParentOfType(const QObject *obj)
|
||||
{
|
||||
const QObject *p = obj ? obj->parent() : nullptr;
|
||||
while (p) {
|
||||
if (auto casted = qobject_cast<T *>(const_cast<QObject *>(p))) {
|
||||
return casted;
|
||||
}
|
||||
p = p->parent();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
} // namespace QtUtils
|
||||
|
||||
#endif // COCKATRICE_QT_UTILS_H
|
||||
@@ -1,112 +0,0 @@
|
||||
#include "system_memory_querier.h"
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
#include <QFile>
|
||||
#include <QRegularExpression>
|
||||
#include <QTextStream>
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_MACOS
|
||||
#include <mach/mach.h>
|
||||
#include <sys/sysctl.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
qulonglong SystemMemoryQuerier::totalMemoryBytes()
|
||||
{
|
||||
#if defined(Q_OS_WIN)
|
||||
|
||||
MEMORYSTATUSEX statex;
|
||||
statex.dwLength = sizeof(statex);
|
||||
if (GlobalMemoryStatusEx(&statex))
|
||||
return statex.ullTotalPhys;
|
||||
return 0;
|
||||
|
||||
#elif defined(Q_OS_LINUX)
|
||||
|
||||
QFile file("/proc/meminfo");
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
|
||||
return 0;
|
||||
|
||||
QTextStream in(&file);
|
||||
while (!in.atEnd()) {
|
||||
QString line = in.readLine();
|
||||
if (line.startsWith("MemTotal:")) {
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
||||
QStringList parts = line.split(QRegExp("\\s+"), QString::SkipEmptyParts);
|
||||
#else
|
||||
QStringList parts = line.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts);
|
||||
#endif
|
||||
if (parts.size() >= 2)
|
||||
return parts[1].toULongLong() * 1024; // kB → bytes
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
|
||||
#elif defined(Q_OS_MACOS)
|
||||
|
||||
int mib[2] = {CTL_HW, HW_MEMSIZE};
|
||||
qulonglong memsize = 0;
|
||||
size_t len = sizeof(memsize);
|
||||
|
||||
if (sysctl(mib, 2, &memsize, &len, nullptr, 0) == 0)
|
||||
return memsize;
|
||||
|
||||
return 0;
|
||||
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
qulonglong SystemMemoryQuerier::availableMemoryBytes()
|
||||
{
|
||||
#if defined(Q_OS_WIN)
|
||||
|
||||
MEMORYSTATUSEX statex;
|
||||
statex.dwLength = sizeof(statex);
|
||||
if (GlobalMemoryStatusEx(&statex))
|
||||
return statex.ullAvailPhys;
|
||||
return 0;
|
||||
|
||||
#elif defined(Q_OS_LINUX)
|
||||
|
||||
QFile file("/proc/meminfo");
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
|
||||
return 0;
|
||||
|
||||
QTextStream in(&file);
|
||||
while (!in.atEnd()) {
|
||||
QString line = in.readLine();
|
||||
if (line.startsWith("MemAvailable:")) {
|
||||
QStringList parts = line.split(QRegExp("\\s+"), Qt::SkipEmptyParts);
|
||||
if (parts.size() >= 2)
|
||||
return parts[1].toULongLong() * 1024;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
|
||||
#elif defined(Q_OS_MACOS)
|
||||
|
||||
vm_size_t pageSize;
|
||||
host_page_size(mach_host_self(), &pageSize);
|
||||
|
||||
mach_msg_type_number_t count = HOST_VM_INFO_COUNT;
|
||||
vm_statistics64_data_t vmstat;
|
||||
|
||||
if (host_statistics64(mach_host_self(), HOST_VM_INFO, (host_info64_t)&vmstat, &count) != KERN_SUCCESS)
|
||||
return 0;
|
||||
|
||||
qulonglong freeBytes = (qulonglong)vmstat.free_count * (qulonglong)pageSize;
|
||||
|
||||
return freeBytes;
|
||||
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
#ifndef COCKATRICE_SYSTEM_MEMORY_QUERIER_H
|
||||
#define COCKATRICE_SYSTEM_MEMORY_QUERIER_H
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
class SystemMemoryQuerier
|
||||
{
|
||||
public:
|
||||
static qulonglong totalMemoryBytes();
|
||||
static qulonglong availableMemoryBytes();
|
||||
|
||||
static bool hasAtLeastGiB(int gib)
|
||||
{
|
||||
const qulonglong GiB = 1024ull * 1024ull * 1024ull;
|
||||
return totalMemoryBytes() >= (qulonglong)gib * GiB;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_SYSTEM_MEMORY_QUERIER_H
|
||||
@@ -1,7 +1,6 @@
|
||||
#include "pages.h"
|
||||
|
||||
#include "client/settings/cache_settings.h"
|
||||
#include "libcockatrice/utility/system_memory_querier.h"
|
||||
#include "main.h"
|
||||
#include "oracleimporter.h"
|
||||
#include "oraclewizard.h"
|
||||
@@ -44,7 +43,6 @@
|
||||
#define MTGJSON_V4_URL_COMPONENT "mtgjson.com/files/"
|
||||
#define ALLSETS_URL_FALLBACK "https://www.mtgjson.com/api/v5/AllPrintings.json"
|
||||
#define MTGJSON_VERSION_URL "https://www.mtgjson.com/api/v5/Meta.json"
|
||||
#define MTGXML_URL "https://github.com/ebbit1q/mtgxml/releases/latest/download/mtg.xml.xz"
|
||||
|
||||
#ifdef HAS_LZMA
|
||||
#define ALLSETS_URL "https://www.mtgjson.com/api/v5/AllPrintings.json.xz"
|
||||
@@ -187,23 +185,6 @@ void LoadSetsPage::initializePage()
|
||||
{
|
||||
urlLineEdit->setText(wizard()->settings->value("allsetsurl", ALLSETS_URL).toString());
|
||||
|
||||
// Memory check because Oracle parsing fails on systems with less than 4GiB for MTGJsons allPrintings.json
|
||||
if (!SystemMemoryQuerier::hasAtLeastGiB(4) && urlLineEdit->text() == ALLSETS_URL) {
|
||||
// Ask user whether to switch URL
|
||||
QMessageBox msgBox(
|
||||
QMessageBox::Question, tr("Low Memory Detected"),
|
||||
tr("Your system has less than 4 GiB of memory.\n"
|
||||
"Using the default AllPrintings URL may cause high memory usage and is known to fail as a result.\n\n"
|
||||
"Would you like to switch to the direct-download pre-parsed URL instead? (Updated daily)"),
|
||||
QMessageBox::Yes | QMessageBox::No, this);
|
||||
|
||||
msgBox.setDefaultButton(QMessageBox::Yes);
|
||||
|
||||
if (msgBox.exec() == QMessageBox::Yes) {
|
||||
urlLineEdit->setText(MTGXML_URL);
|
||||
}
|
||||
}
|
||||
|
||||
progressLabel->hide();
|
||||
progressBar->hide();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user