Compare commits

..

11 Commits

Author SHA1 Message Date
Lukas Brübach
06a162c1f3 Bubbling.
Took 16 seconds

# Commit time for manual adjustment:
# Took 8 seconds

Took 16 seconds

# Commit time for manual adjustment:
# Took 6 seconds

Took 9 seconds

# Commit time for manual adjustment:
# Took 8 seconds


Took 14 seconds
2025-12-07 22:58:05 +01:00
Lukas Brübach
d074fd5491 Improve with sequencing and better rendering.
Took 3 minutes


Took 18 seconds
2025-12-07 22:03:25 +01:00
Brübach, Lukas
319e8fe7c9 [App] First-run tutorial 2025-12-06 22:19:59 +01:00
Lily Huang
d3302d521f Fix flipped svg for donator/judge/vip (#6400) 2025-12-06 14:09:55 +01:00
tooomm
5c1bb27d5c README: Add code docs + flathub repo links (#6384)
* Add code docs + flathub repo links

* Update README.md
2025-12-05 23:28:25 +01:00
BruebachL
dde36183ce [VDE] Proper parent lookup syncs group-by box again (#6396)
* [VDE] Proper parent lookup syncs group-by box again

* [VDE] Proper lib inclusion.

* [VDE] Lint.
2025-12-05 23:27:27 +01:00
BruebachL
7c7f2dd8d5 [Doxygen] Logging (#6399)
* [Doxygen] Logging

Took 50 minutes

Took 36 seconds

* [Doxygen] Newline.

Took 2 minutes

* [Doxygen] Add another example.

Took 7 minutes

* [Doxygen] \note and \warning

Took 4 minutes

Took 32 seconds

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2025-12-05 18:42:45 +01:00
BruebachL
edb0a954e2 [GameInformation] Check for existence of room for create as judge checkbox (#6398) 2025-12-05 17:26:35 +01:00
BruebachL
0a239712dd [VDD] Add search bar for filters. (#6389)
* [VDD] Add search bar for filters.

* Update cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_save_load_widget.cpp

Co-authored-by: RickyRister <42636155+RickyRister@users.noreply.github.com>

---------

Co-authored-by: RickyRister <42636155+RickyRister@users.noreply.github.com>
2025-12-04 23:14:19 +01:00
RickyRister
95c3434205 [TagDisplayWidget] Refactor to just store tags and use signals (#6395) 2025-12-04 10:26:39 -08:00
RickyRister
f0be6972cc [TagsDisplayWidget] cleanup refactor (#6394)
* Make fields private

* Move method to static

* clean up code

* move code
2025-12-04 09:40:24 -08:00
29 changed files with 967 additions and 325 deletions

View File

@@ -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 [![Discord](https://img.shields.io/discord/314987288398659595?label=Discord&logo=discord&logoColor=white)](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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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();

View File

@@ -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);

View File

@@ -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();
}

View File

@@ -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

View File

@@ -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();
}

View File

@@ -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

View File

@@ -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 parents 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();
}

View File

@@ -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

View File

@@ -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());
}

View File

@@ -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;
};

View File

@@ -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))),

View File

@@ -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);
}
}
}
}

View File

@@ -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

View File

@@ -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);

View File

@@ -72,6 +72,8 @@ private:
void addSetBannerCardMenu(QMenu *menu);
private slots:
void setTags(const QStringList &tags);
void actRenameDeck();
void actRenameFile();
void actDeleteFile();

View File

@@ -1,5 +1,7 @@
@page developer_reference Developer Reference
- @subpage logging
- @subpage primer_cards
- @subpage card_database_schema_and_parsing

View 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);
}
```

View File

@@ -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})

View 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

View File

@@ -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
}

View File

@@ -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

View File

@@ -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();