mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2025-12-17 18:07:41 -08:00
Compare commits
10 Commits
proper-pro
...
first-run-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54ee406a58 | ||
|
|
f6a1f34864 | ||
|
|
06a162c1f3 | ||
|
|
d074fd5491 | ||
|
|
319e8fe7c9 | ||
|
|
d3302d521f | ||
|
|
5c1bb27d5c | ||
|
|
dde36183ce | ||
|
|
7c7f2dd8d5 | ||
|
|
edb0a954e2 |
@@ -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 |
@@ -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);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "../../window_main.h"
|
||||
#include "background_sources.h"
|
||||
#include "home_styled_button.h"
|
||||
#include "tutorial/tutorial_controller.h"
|
||||
|
||||
#include <QGroupBox>
|
||||
#include <QPainter>
|
||||
@@ -48,6 +49,30 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
|
||||
&HomeWidget::initializeBackgroundFromSource);
|
||||
connect(&SettingsCache::instance(), &SettingsCache::homeTabBackgroundShuffleFrequencyChanged, this,
|
||||
&HomeWidget::onBackgroundShuffleFrequencyChanged);
|
||||
|
||||
tutorialController = new TutorialController(this);
|
||||
auto sequence = TutorialSequence();
|
||||
sequence.addStep({connectButton, "Connect to a server to play here!"});
|
||||
sequence.addStep({visualDeckEditorButton, "Create a new deck from cards in the database here!"});
|
||||
sequence.addStep({visualDeckStorageButton, "Browse the decks in your local collection."});
|
||||
sequence.addStep({visualDatabaseDisplayButton, "View the card database here."});
|
||||
sequence.addStep(
|
||||
{edhrecButton, "Browse EDHRec, an external service designed to provide card recommendations for decks."});
|
||||
sequence.addStep({archidektButton, "Browse Archidekt, an external service that allows users to store "
|
||||
"decklists and import them to your local collection."});
|
||||
sequence.addStep({replaybutton, "View replays of your past games here."});
|
||||
sequence.addStep({exitButton, "Exit the application."});
|
||||
tutorialController->addSequence(sequence);
|
||||
}
|
||||
|
||||
void HomeWidget::showEvent(QShowEvent *event)
|
||||
{
|
||||
QWidget::showEvent(event);
|
||||
if (!tutorialStarted) {
|
||||
tutorialStarted = true;
|
||||
// Start on next event loop iteration so everything is fully painted
|
||||
QTimer::singleShot(3, tutorialController, [this] { tutorialController->start(); });
|
||||
}
|
||||
}
|
||||
|
||||
void HomeWidget::initializeBackgroundFromSource()
|
||||
@@ -181,29 +206,29 @@ QGroupBox *HomeWidget::createButtons()
|
||||
connectButton = new HomeStyledButton("Connect/Play", gradientColors);
|
||||
boxLayout->addWidget(connectButton);
|
||||
|
||||
auto visualDeckEditorButton = new HomeStyledButton(tr("Create New Deck"), gradientColors);
|
||||
visualDeckEditorButton = new HomeStyledButton(tr("Create New Deck"), gradientColors);
|
||||
connect(visualDeckEditorButton, &QPushButton::clicked, tabSupervisor,
|
||||
[this] { tabSupervisor->openDeckInNewTab(nullptr); });
|
||||
boxLayout->addWidget(visualDeckEditorButton);
|
||||
auto visualDeckStorageButton = new HomeStyledButton(tr("Browse Decks"), gradientColors);
|
||||
visualDeckStorageButton = new HomeStyledButton(tr("Browse Decks"), gradientColors);
|
||||
connect(visualDeckStorageButton, &QPushButton::clicked, tabSupervisor,
|
||||
[this] { tabSupervisor->actTabVisualDeckStorage(true); });
|
||||
boxLayout->addWidget(visualDeckStorageButton);
|
||||
auto visualDatabaseDisplayButton = new HomeStyledButton(tr("Browse Card Database"), gradientColors);
|
||||
visualDatabaseDisplayButton = new HomeStyledButton(tr("Browse Card Database"), gradientColors);
|
||||
connect(visualDatabaseDisplayButton, &QPushButton::clicked, tabSupervisor,
|
||||
&TabSupervisor::addVisualDatabaseDisplayTab);
|
||||
boxLayout->addWidget(visualDatabaseDisplayButton);
|
||||
auto edhrecButton = new HomeStyledButton(tr("Browse EDHRec"), gradientColors);
|
||||
edhrecButton = new HomeStyledButton(tr("Browse EDHRec"), gradientColors);
|
||||
connect(edhrecButton, &QPushButton::clicked, tabSupervisor, &TabSupervisor::addEdhrecMainTab);
|
||||
boxLayout->addWidget(edhrecButton);
|
||||
auto archidektButton = new HomeStyledButton(tr("Browse Archidekt"), gradientColors);
|
||||
archidektButton = new HomeStyledButton(tr("Browse Archidekt"), gradientColors);
|
||||
connect(archidektButton, &QPushButton::clicked, tabSupervisor, &TabSupervisor::addArchidektTab);
|
||||
boxLayout->addWidget(archidektButton);
|
||||
auto replaybutton = new HomeStyledButton(tr("View Replays"), gradientColors);
|
||||
replaybutton = new HomeStyledButton(tr("View Replays"), gradientColors);
|
||||
connect(replaybutton, &QPushButton::clicked, tabSupervisor, [this] { tabSupervisor->actTabReplays(true); });
|
||||
boxLayout->addWidget(replaybutton);
|
||||
if (qobject_cast<MainWindow *>(tabSupervisor->parentWidget())) {
|
||||
auto exitButton = new HomeStyledButton(tr("Quit"), gradientColors);
|
||||
exitButton = new HomeStyledButton(tr("Quit"), gradientColors);
|
||||
connect(exitButton, &QPushButton::clicked, qobject_cast<MainWindow *>(tabSupervisor->parentWidget()),
|
||||
&MainWindow::actExit);
|
||||
boxLayout->addWidget(exitButton);
|
||||
|
||||
@@ -24,9 +24,18 @@ public:
|
||||
HomeWidget(QWidget *parent, TabSupervisor *tabSupervisor);
|
||||
void updateRandomCard();
|
||||
QPair<QColor, QColor> extractDominantColors(const QPixmap &pixmap);
|
||||
HomeStyledButton *connectButton;
|
||||
HomeStyledButton *visualDeckEditorButton;
|
||||
HomeStyledButton *visualDeckStorageButton;
|
||||
HomeStyledButton *visualDatabaseDisplayButton;
|
||||
HomeStyledButton *edhrecButton;
|
||||
HomeStyledButton *archidektButton;
|
||||
HomeStyledButton *replaybutton;
|
||||
HomeStyledButton *exitButton;
|
||||
|
||||
public slots:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void showEvent(QShowEvent *event) override;
|
||||
void initializeBackgroundFromSource();
|
||||
void onBackgroundShuffleFrequencyChanged();
|
||||
void updateBackgroundProperties();
|
||||
@@ -39,11 +48,12 @@ private:
|
||||
QTimer *cardChangeTimer;
|
||||
TabSupervisor *tabSupervisor;
|
||||
QPixmap background;
|
||||
TutorialController *tutorialController;
|
||||
bool tutorialStarted = false;
|
||||
CardInfoPictureArtCropWidget *backgroundSourceCard = nullptr;
|
||||
DeckLoader *backgroundSourceDeck;
|
||||
QPixmap overlay;
|
||||
QPair<QColor, QColor> gradientColors;
|
||||
HomeStyledButton *connectButton;
|
||||
};
|
||||
|
||||
#endif // HOME_WIDGET_H
|
||||
|
||||
@@ -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
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "../../interface/pixel_map_generator.h"
|
||||
#include "../../interface/widgets/cards/card_info_frame_widget.h"
|
||||
#include "../../interface/widgets/deck_analytics/deck_analytics_widget.h"
|
||||
#include "../../interface/widgets/general/tutorial/tutorial_controller.h"
|
||||
#include "../../interface/widgets/visual_deck_editor/visual_deck_editor_widget.h"
|
||||
#include "../tab_deck_editor.h"
|
||||
#include "../tab_supervisor.h"
|
||||
@@ -50,6 +51,31 @@ TabDeckEditorVisual::TabDeckEditorVisual(TabSupervisor *_tabSupervisor) : Abstra
|
||||
|
||||
loadLayout();
|
||||
databaseDisplayDockWidget->setHidden(true);
|
||||
tutorialController = new TutorialController(this);
|
||||
|
||||
auto sequence = TutorialSequence();
|
||||
|
||||
sequence.addStep({tabContainer->visualDeckView, "View your deck here.",
|
||||
[this]() { tabContainer->setCurrentWidget(tabContainer->visualDeckView); }});
|
||||
sequence.addStep({printingSelectorDockWidget, "Change the printings in your deck here."});
|
||||
|
||||
tutorialController->addSequence(sequence);
|
||||
|
||||
auto vddSequence = tabContainer->visualDatabaseDisplay->addTutorialSteps();
|
||||
vddSequence.steps.prepend({tabContainer->visualDatabaseDisplay, "View the database here",
|
||||
[this]() { tabContainer->setCurrentWidget(tabContainer->visualDatabaseDisplay); }});
|
||||
|
||||
tutorialController->addSequence(vddSequence);
|
||||
}
|
||||
|
||||
void TabDeckEditorVisual::showEvent(QShowEvent *ev)
|
||||
{
|
||||
QWidget::showEvent(ev);
|
||||
if (!tutorialStarted) {
|
||||
tutorialStarted = true;
|
||||
// Start on next event loop iteration so everything is fully painted
|
||||
QTimer::singleShot(0, tutorialController, [this] { tutorialController->start(); });
|
||||
}
|
||||
}
|
||||
|
||||
/** @brief Creates the central frame containing the tab container. */
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "../tab.h"
|
||||
#include "tab_deck_editor_visual_tab_widget.h"
|
||||
|
||||
class TutorialController;
|
||||
/**
|
||||
* @class TabDeckEditorVisual
|
||||
* @ingroup DeckEditorTabs
|
||||
@@ -55,7 +56,12 @@ class TabDeckEditorVisual : public AbstractTabDeckEditor
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
TutorialController *tutorialController = nullptr;
|
||||
bool tutorialStarted = false;
|
||||
|
||||
protected slots:
|
||||
void showEvent(QShowEvent *ev) override;
|
||||
/**
|
||||
* @brief Load the editor layout from settings.
|
||||
*/
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "../../../filters/syntax_help.h"
|
||||
#include "../../pixel_map_generator.h"
|
||||
#include "../cards/card_info_picture_with_text_overlay_widget.h"
|
||||
#include "../general/tutorial/tutorial_controller.h"
|
||||
#include "../quick_settings/settings_button_widget.h"
|
||||
#include "../utility/custom_line_edit.h"
|
||||
#include "visual_database_display_color_filter_widget.h"
|
||||
@@ -192,6 +193,28 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent,
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
TutorialSequence VisualDatabaseDisplayWidget::addTutorialSteps()
|
||||
{
|
||||
auto sequence = TutorialSequence();
|
||||
sequence.addStep({colorFilterWidget, "Filter the database by colors with these controls"});
|
||||
sequence.addStep({displayModeButton, "You can change back to the old table display-style with this button."});
|
||||
sequence.addStep({filterContainer, "Use these controls for quick access to common filters."});
|
||||
sequence.addStep(
|
||||
{quickFilterSaveLoadWidget, "This button will let you save and load all currently applied filters to files."});
|
||||
sequence.addStep({quickFilterNameWidget,
|
||||
"This button will let you apply name filters. Optionally, you can import every card in "
|
||||
"your deck as a name filter and then save this as a filter using the save/load button "
|
||||
"to make your own quick access collections!"});
|
||||
sequence.addStep({mainTypeFilterWidget, "Use these buttons to quickly filter by card types."});
|
||||
sequence.addStep({quickFilterSubTypeWidget, "This button will let you apply filters for card sub-types."});
|
||||
sequence.addStep(
|
||||
{quickFilterSetWidget,
|
||||
"This button will let you apply filters for card sets. You can also filter to the X most recent sets. "
|
||||
"Filtering to a set will display all printings of a card within that set."});
|
||||
|
||||
return sequence;
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayWidget::initialize()
|
||||
{
|
||||
databaseLoadIndicator->setVisible(false);
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "../cards/card_size_widget.h"
|
||||
#include "../general/layout_containers/flow_widget.h"
|
||||
#include "../general/layout_containers/overlap_control_widget.h"
|
||||
#include "../general/tutorial/tutorial_controller.h"
|
||||
#include "../utility/custom_line_edit.h"
|
||||
#include "visual_database_display_color_filter_widget.h"
|
||||
#include "visual_database_display_filter_save_load_widget.h"
|
||||
@@ -30,6 +31,7 @@
|
||||
#include <libcockatrice/models/deck_list/deck_list_model.h>
|
||||
#include <qscrollarea.h>
|
||||
|
||||
class TutorialController;
|
||||
inline Q_LOGGING_CATEGORY(VisualDatabaseDisplayLog, "visual_database_display");
|
||||
|
||||
class VisualDatabaseDisplayWidget : public QWidget
|
||||
@@ -59,6 +61,7 @@ public:
|
||||
VisualDatabaseDisplayColorFilterWidget *colorFilterWidget;
|
||||
|
||||
public slots:
|
||||
TutorialSequence addTutorialSteps();
|
||||
void searchModelChanged();
|
||||
|
||||
signals:
|
||||
|
||||
@@ -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))),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
```
|
||||
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
|
||||
Reference in New Issue
Block a user