Compare commits

..

No commits in common. "96b0888a7365744ee26e845fe0d389a98132d0b4" and "fea12a74e2df0dd18dd65a3d2178d2002f289d17" have entirely different histories.

17 changed files with 22 additions and 268 deletions

View file

@ -12,7 +12,6 @@
#include "libcockatrice/card/printing/exact_card.h"
#include "libcockatrice/utility/card_ref.h"
#include <QApplication>
#include <QBrush>
#include <QChar>
#include <QCursor>
@ -29,7 +28,6 @@
#include <QPixmap>
#include <QPointF>
#include <QRectF>
#include <QTimer>
#include <QTransform>
#include <QVariant>
#include <QtGlobal>
@ -44,7 +42,7 @@ class QSizeF;
AbstractCardItem::AbstractCardItem(QGraphicsItem *parent, const CardRef &cardRef, PlayerLogic *_owner, int _id)
: ArrowTarget(_owner, parent), id(_id), cardRef(cardRef), tapped(false), facedown(false), tapAngle(0),
bgColor(Qt::transparent), isHovered(false), realZValue(0), pendingSelectionCollapse(false)
bgColor(Qt::transparent), isHovered(false), realZValue(0)
{
setCursor(Qt::OpenHandCursor);
setFlag(ItemIsSelectable);
@ -346,16 +344,6 @@ void AbstractCardItem::setFaceDown(bool _facedown)
void AbstractCardItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
cancelPendingSelectionCollapse();
if (scene() != nullptr) {
// A new press starts a new gesture: no card may collapse the selection out from under it afterwards.
for (QGraphicsItem *selectedItem : scene()->selectedItems()) {
auto *selectedCard = qgraphicsitem_cast<AbstractCardItem *>(selectedItem);
if (selectedCard != nullptr && selectedCard != this) {
selectedCard->cancelPendingSelectionCollapse();
}
}
}
if ((event->modifiers() & Qt::AltModifier) && event->button() == Qt::LeftButton) {
emit cardShiftClicked(cardRef.name);
} else if ((event->modifiers() & Qt::ControlModifier)) {
@ -363,10 +351,6 @@ void AbstractCardItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
} else if (!isSelected()) {
scene()->clearSelection();
setSelected(true);
} else if (event->button() == Qt::LeftButton && scene()->selectedItems().count() > 1) {
// Part of a multi-selection; collapse only if this turns out to be a click rather than a drag,
// so that dragging the whole selection keeps it intact.
pendingSelectionCollapse = true;
}
if (event->button() == Qt::LeftButton) {
setCursor(Qt::ClosedHandCursor);
@ -378,32 +362,6 @@ void AbstractCardItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
void AbstractCardItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
if (pendingSelectionCollapse) {
pendingSelectionCollapse = false;
bool dragged = (event->screenPos() - event->buttonDownScreenPos(event->button())).manhattanLength() >=
2 * QApplication::startDragDistance();
if (!dragged) {
if (doubleClickActsOnSelection()) {
// A double-click may still arrive and must act on the whole multi-selection, so collapse only
// once the double-click window has passed without one. The generation guard (and the receiver
// context) drops the collapse if the item is gone, deselected, or a double-click cancelled it;
// the count guard drops it if the selection changed meanwhile (rubber band, ctrl-toggle).
const int generation = ++selectionCollapseGeneration;
const int armedSelectionCount = scene()->selectedItems().count();
QTimer::singleShot(QApplication::doubleClickInterval(), this, [this, generation, armedSelectionCount] {
if (generation == selectionCollapseGeneration && scene() != nullptr && isSelected() &&
scene()->selectedItems().count() == armedSelectionCount && armedSelectionCount > 1) {
scene()->clearSelection();
setSelected(true);
}
});
} else {
scene()->clearSelection();
setSelected(true);
}
}
}
if (event->button() == Qt::MiddleButton) {
emit deleteCardInfoPopup(cardRef.name);
}
@ -412,17 +370,6 @@ void AbstractCardItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
event->accept();
}
void AbstractCardItem::cancelPendingSelectionCollapse()
{
pendingSelectionCollapse = false;
++selectionCollapseGeneration;
}
bool AbstractCardItem::doubleClickActsOnSelection() const
{
return false;
}
void AbstractCardItem::processHoverEvent()
{
emit hovered(this);

View file

@ -41,8 +41,6 @@ protected:
private:
bool isHovered;
qreal realZValue;
bool pendingSelectionCollapse;
int selectionCollapseGeneration = 0; ///< Guards the deferred collapse timer against stale firings
private slots:
void pixmapUpdated();
@ -132,8 +130,6 @@ public:
}
void setFaceDown(bool _facedown);
void processHoverEvent();
/** @brief Drop a pending collapse and disarm any deferred collapse timer. */
void cancelPendingSelectionCollapse();
void deleteCardInfoPopup()
{
emit deleteCardInfoPopup(cardRef.name);
@ -148,16 +144,6 @@ protected:
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;
QVariant itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value) override;
void cacheBgColor();
/**
* @brief Whether a double-click on this item acts on the whole selection.
*
* When true, a release without drag defers the multi-selection collapse
* until the double-click window passes, so a following double-click still
* sees every selected card. Subclasses that handle double-clicks on the
* selection must return true here and call
* cancelPendingSelectionCollapse() from their double-click handler.
*/
virtual bool doubleClickActsOnSelection() const;
};
#endif

View file

@ -518,9 +518,6 @@ void CardItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
void CardItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
{
// The preceding release deferred its multi-selection collapse for exactly this: the double-click plays
// every selected card, so the collapse must not run.
cancelPendingSelectionCollapse();
if ((event->modifiers() != Qt::AltModifier) && (event->buttons() == Qt::LeftButton) &&
(SettingsCache::instance().userInterface().getDoubleClickToPlay())) {
handleClickedToPlay(event->modifiers().testFlag(Qt::ShiftModifier));
@ -528,11 +525,6 @@ void CardItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
event->accept();
}
bool CardItem::doubleClickActsOnSelection() const
{
return SettingsCache::instance().userInterface().getDoubleClickToPlay();
}
bool CardItem::animationEvent()
{
int rotation = ROTATION_DEGREES_PER_FRAME;

View file

@ -175,7 +175,6 @@ protected:
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;
void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override;
QVariant itemChange(GraphicsItemChange change, const QVariant &value) override;
bool doubleClickActsOnSelection() const override;
};
#endif

View file

@ -182,15 +182,6 @@ void DeckView::mouseDoubleClickEvent(QMouseEvent *event)
QList<MoveCard_ToZone> result;
QList<QGraphicsItem *> sel = scene()->selectedItems();
// The double-click goes to the view, not the card, so cancel any pending
// selection collapse on the cards before reading the selection.
for (auto *item : sel) {
if (auto *c = dynamic_cast<DeckViewCard *>(item)) {
c->cancelPendingSelectionCollapse();
}
}
sel = scene()->selectedItems();
for (int i = 0; i < sel.size(); i++) {
auto *c = static_cast<DeckViewCard *>(sel.at(i));
auto *zone = static_cast<DeckViewCardContainer *>(c->parentItem());

View file

@ -49,10 +49,6 @@ public:
{
return originZone;
}
bool doubleClickActsOnSelection() const override
{
return true;
}
protected:
void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override;

View file

@ -127,7 +127,6 @@ void GameView::startRubberBand(const QPointF &_selectionOrigin)
selectionOrigin = _selectionOrigin;
previousBandRect = QRect();
rubberBand->setGeometry(QRect(mapFromScene(selectionOrigin), QSize(0, 0)));
rubberBand->raise();
rubberBand->show();
}

View file

@ -23,7 +23,6 @@
#include <QGroupBox>
#include <QMessageBox>
#include <QObject>
#include <QPushButton>
#include <QSharedPointer>
#include <QString>
#include <QStringList>
@ -85,9 +84,10 @@ AppearanceSettingsPage::AppearanceSettingsPage()
const QString currentStyle = cfg.styleName;
const int styleSeedIdx = currentStyle.isEmpty() ? 0 : styleCombo.findData(currentStyle);
styleCombo.setCurrentIndex(styleSeedIdx >= 0 ? styleSeedIdx : 0);
previousStyle = styleCombo.currentData().toString();
connect(&styleCombo, &QComboBox::currentIndexChanged, this, &AppearanceSettingsPage::styleComboChanged);
connect(&styleCombo, &QComboBox::currentIndexChanged, this,
[this] { themeManager->setStyleName(styleCombo.currentData().toString()); });
connect(&styleCombo, &QComboBox::currentIndexChanged, this, &AppearanceSettingsPage::updateStyleWarning);
connect(themeManager, &ThemeManager::themeChanged, this, [this, dirPath] {
const QString newDir = themeManager->getAvailableThemes().value(SettingsCache::instance().getThemeName());
@ -104,7 +104,6 @@ AppearanceSettingsPage::AppearanceSettingsPage()
const int styleIdx = currentStyle.isEmpty() ? 0 : styleCombo.findData(currentStyle);
styleCombo.setCurrentIndex(styleIdx >= 0 ? styleIdx : 0);
styleCombo.blockSignals(false);
previousStyle = styleCombo.currentData().toString();
updateStyleWarning();
});
@ -468,88 +467,12 @@ void AppearanceSettingsPage::updateHomeTabSettingsVisibility()
homeTabDisplayCardNameCheckBox.setVisible(visible);
}
static bool isBrokenStyle(const QString &style)
{
return style.compare("windows11", Qt::CaseInsensitive) == 0 ||
style.compare("windowsvista", Qt::CaseInsensitive) == 0;
}
static int findFusionIndex(const QComboBox &combo)
{
int fusionIdx = combo.findData(QStringLiteral("Fusion"));
if (fusionIdx >= 0) {
return fusionIdx;
}
for (int i = 0; i < combo.count(); ++i) {
if (combo.itemData(i).toString().compare(QStringLiteral("Fusion"), Qt::CaseInsensitive) == 0) {
return i;
}
}
return -1;
}
void AppearanceSettingsPage::updateStyleWarning()
{
styleWarningLabel.setVisible(isBrokenStyle(styleCombo.currentData().toString()));
}
void AppearanceSettingsPage::styleComboChanged(int index)
{
if (index < 0) {
return;
}
const QString newStyle = styleCombo.itemData(index).toString();
if (!isBrokenStyle(newStyle)) {
themeManager->setStyleName(newStyle);
previousStyle = newStyle;
updateStyleWarning();
return;
}
QMessageBox msgBox(this);
msgBox.setWindowTitle(tr("Broken style"));
msgBox.setIcon(QMessageBox::Warning);
msgBox.setText(tr("The \"%1\" style is broken and can break the game view: dragging cards across zones can "
"shrink the board to a tiny grey window that can only be fixed by rejoining the game.")
.arg(newStyle));
msgBox.setInformativeText(
tr("It can also render controls such as combo boxes unreadable, especially in dark mode.\n\n"
"Fusion is the recommended style on every platform. Switch to Fusion instead?"));
int fusionIdx = findFusionIndex(styleCombo);
QPushButton *fusionButton = nullptr;
if (fusionIdx >= 0) {
fusionButton = msgBox.addButton(tr("Use Fusion instead"), QMessageBox::AcceptRole);
}
QPushButton *keepButton = msgBox.addButton(tr("Use %1 anyway").arg(newStyle), QMessageBox::DestructiveRole);
msgBox.addButton(QMessageBox::Cancel);
if (fusionButton != nullptr) {
msgBox.setDefaultButton(fusionButton);
} else {
msgBox.setDefaultButton(keepButton);
}
msgBox.exec();
const QAbstractButton *clicked = msgBox.clickedButton();
if (fusionButton != nullptr && clicked == fusionButton) {
styleCombo.setCurrentIndex(fusionIdx);
return;
}
if (clicked == keepButton) {
themeManager->setStyleName(newStyle);
previousStyle = newStyle;
updateStyleWarning();
return;
}
// Cancel: revert to the previously applied style without re-prompting
styleCombo.blockSignals(true);
const int previousIdx = styleCombo.findData(previousStyle);
styleCombo.setCurrentIndex(previousIdx >= 0 ? previousIdx : 0);
styleCombo.blockSignals(false);
updateStyleWarning();
const QString style = styleCombo.currentData().toString();
const bool broken =
style.compare("windows11", Qt::CaseInsensitive) == 0 || style.compare("windowsvista", Qt::CaseInsensitive) == 0;
styleWarningLabel.setVisible(broken);
}
void AppearanceSettingsPage::showShortcutsChanged(QT_STATE_CHANGED_T value)
@ -615,10 +538,9 @@ void AppearanceSettingsPage::retranslateUi()
styleComboLabel.setText(tr("Active theme style:"));
styleCombo.setToolTip(tr("Qt widget style saved to this theme (\"System\" lets the application decide)"));
styleWarningLabel.setText(
tr("Warning: the Windows 11 and Windows Vista styles are broken and can break the game view (dragging "
"cards can shrink the board to a tiny grey window that requires rejoining to fix), and can render "
"controls such as combo boxes unreadable, especially in dark mode. Fusion is the recommended style on "
"every platform: it is Qt's cross-platform style and the one Cockatrice is developed against."));
tr("Warning: the Windows 11 and Windows Vista styles are buggy and can render controls such as combo boxes "
"unreadable, especially in dark mode. Fusion is the recommended style on every platform: it is Qt's "
"cross-platform style and the one Cockatrice is developed against."));
updateStyleWarning();
editPaletteButton.setText(tr("Edit theme palette"));

View file

@ -23,7 +23,6 @@ private slots:
void editPalette();
void updateHomeTabSettingsVisibility();
void updateStyleWarning();
void styleComboChanged(int index);
void showShortcutsChanged(QT_STATE_CHANGED_T enabled);
void overrideAllCardArtWithPersonalPreferenceToggled(QT_STATE_CHANGED_T enabled);
@ -41,7 +40,6 @@ private:
QComboBox styleCombo;
QLabel styleWarningLabel;
QPushButton editPaletteButton;
QString previousStyle;
QLabel homeTabBackgroundSourceLabel;
QComboBox homeTabBackgroundSourceBox;

View file

@ -1253,7 +1253,7 @@ void MainWindow::createCardUpdateProcess(bool background)
<< dir.absoluteFilePath(binaryName);
QMessageBox::warning(this, tr("Error"),
tr("Unable to run the card database updater: ") + dir.absoluteFilePath(binaryName));
exitCardDatabaseUpdate(false);
exitCardDatabaseUpdate();
emit cardDatabaseUpdateFinished(false);
return;
}
@ -1330,7 +1330,7 @@ void MainWindow::cardUpdateStandardError()
}
}
void MainWindow::exitCardDatabaseUpdate(bool doReload)
void MainWindow::exitCardDatabaseUpdate()
{
if (!cardUpdateProcess) {
return;
@ -1339,11 +1339,9 @@ void MainWindow::exitCardDatabaseUpdate(bool doReload)
cardUpdateProcess = nullptr;
statusBar()->clearMessage();
if (doReload) {
// This can be the first successful load of the session (e.g. the onboarding
// download), so it must own the set check for that load.
reloadCardDatabaseAndCheckSets();
}
// This can be the first successful load of the session (e.g. the onboarding
// download), so it must own the set check for that load.
reloadCardDatabaseAndCheckSets();
}
void MainWindow::reloadCardDatabaseAndCheckSets()
@ -1389,7 +1387,7 @@ void MainWindow::cardUpdateError(QProcess::ProcessError err)
}
qCWarning(CardDatabaseUpdateLog) << "The card database updater exited with an error:" << error;
exitCardDatabaseUpdate(false);
exitCardDatabaseUpdate();
QMessageBox::warning(this, tr("Error"), tr("The card database updater exited with an error:\n%1").arg(error));
emit cardDatabaseUpdateFinished(false);
}
@ -1402,14 +1400,12 @@ void MainWindow::cardUpdateFinished(int exitCode, QProcess::ExitStatus exitStatu
cardUpdateProgressOutput(); // drain any progress lines not yet parsed
const bool success = (exitStatus == QProcess::NormalExit) && (exitCode == 0);
if (success) {
if (exitStatus == QProcess::NormalExit) {
SettingsCache::instance().updates().setLastCardUpdateCheck(QDateTime::currentDateTime().date());
} else if (exitStatus == QProcess::NormalExit) {
statusBar()->showMessage(tr("Card database update cancelled."), 10000);
}
qCInfo(CardDatabaseUpdateLog) << "The card database updater finished with exit code" << exitCode << "exit status"
<< static_cast<int>(exitStatus) << "success:" << success;
exitCardDatabaseUpdate(success);
exitCardDatabaseUpdate();
notifyCardUpdateFinishedFromTray(success);
emit cardDatabaseUpdateFinished(success);
}

View file

@ -157,7 +157,7 @@ private:
return "oracle";
}
void createCardUpdateProcess(bool background = false);
void exitCardDatabaseUpdate(bool doReload);
void exitCardDatabaseUpdate();
/**
* @brief Reloads the card database on a worker thread and runs checkUnknownSets() once the finished
* snapshot has been swapped in.

View file

@ -85,8 +85,5 @@ int main(int argc, char *argv[])
QTimer::singleShot(0, &wizard, [&wizard]() { wizard.runInBackground(); });
}
const int execResult = app.exec();
// A cancel only counts when nothing was written yet: closing the wizard
// after the card database was already saved on disk is still an update.
return (wizard.wasCancelled() && !wizard.hasSavedDatabase()) ? 1 : execResult;
return app.exec();
}

View file

@ -128,20 +128,12 @@ void OracleWizard::accept()
void OracleWizard::reject()
{
cancelled = true;
// The wizard is being closed while a page may still run a worker on the
// importer. Ask it to stop before the wizard (and the importer child) is
// destroyed, so the worker thread never touches freed memory.
if (auto *active = dynamic_cast<OracleWizardPage *>(currentPage())) {
active->cancelWork();
}
const auto replies = nam->findChildren<QNetworkReply *>();
for (QNetworkReply *reply : replies) {
reply->abort();
}
QWizard::reject();
}

View file

@ -20,19 +20,6 @@ public:
explicit OracleWizard(QWidget *parent = nullptr);
void accept() override;
void reject() override;
bool wasCancelled()
{
return cancelled;
}
/** @brief Records that the card database was written to disk, so a later cancel still counts as an update. */
void markDatabaseSaved()
{
databaseSaved = true;
}
bool hasSavedDatabase()
{
return databaseSaved;
}
void enableButtons();
void disableButtons();
void retranslateUi();
@ -80,8 +67,6 @@ private:
QByteArray tokensData;
QString cardSourceUrl;
QString cardSourceVersion;
bool cancelled = false;
bool databaseSaved = false;
void migrateOracleSettings();
/**

View file

@ -484,10 +484,6 @@ void LoadSetsPage::actDownloadFinishedSetsFile()
{
// check for a reply
auto *reply = dynamic_cast<QNetworkReply *>(sender());
if (wizard()->wasCancelled()) {
reply->deleteLater();
return;
}
auto errorCode = reply->error();
if (errorCode != QNetworkReply::NoError) {
QMessageBox::critical(this, tr("Error"), tr("Network error: %1.").arg(reply->errorString()));
@ -899,7 +895,6 @@ bool SaveSetsPage::validatePage()
return false;
}
wizard()->markDatabaseSaved();
return true;
}

View file

@ -178,10 +178,6 @@ void SimpleDownloadFilePage::actDownloadFinished()
{
// check for a reply
auto *reply = dynamic_cast<QNetworkReply *>(sender());
if (wizard()->wasCancelled()) {
reply->deleteLater();
return;
}
QNetworkReply::NetworkError errorCode = reply->error();
if (errorCode != QNetworkReply::NoError) {
QMessageBox::critical(this, tr("Error"), tr("Network error: %1.").arg(reply->errorString()));
@ -246,7 +242,6 @@ bool SimpleDownloadFilePage::saveToFile()
// clean saved downloadData
downloadData = QByteArray();
wizard()->markDatabaseSaved();
return true;
}

View file

@ -13,7 +13,6 @@
#include <QImage>
#include <QImageWriter>
#include <QLoggingCategory>
#include <QScopeGuard>
#include <QSharedPointer>
#include <QStandardPaths>
#include <QString>
@ -224,43 +223,8 @@ int main(int argc, char **argv)
}
QCoreApplication app(argc, argv);
// Every TEST() runs as its own process and CI runs them in parallel (ctest -j4),
// while the picture paths round-trip through the process-global SettingsCache
// file. Parallel cases must therefore never share one settings directory: on
// platforms where QStandardPaths ignores the $HOME override above (macOS
// resolves the home directory via system APIs), every case would otherwise read
// and write the same paths.ini, and a sibling's setPicsPath can land between our
// own set and the loader's get — so the loader searches a foreign (or empty)
// directory and flakily returns a null image. Scope the settings to this process
// with a pid-unique application name; PIDs are never reused by live processes.
QCoreApplication::setOrganizationName(QStringLiteral("CockatriceTest"));
QCoreApplication::setApplicationName(
QStringLiteral("loader-local-matching-test-%1").arg(QCoreApplication::applicationPid()));
QLoggingCategory::setFilterRules("card_picture_loader.*=false\nsettings_cache.*=false");
::testing::InitGoogleTest(&argc, argv);
// CMake's gtest discovery runs this binary with --gtest_list_tests, which lists
// without running any test body. Arming the cleanup guard here would make it the
// sole SettingsCache::instance() call in that mode, instantiating the
// Q_GLOBAL_STATIC singleton (and its QNetworkAccessManagers) just to tear down at
// exit — which LeakSanitizer reports as leaks and fails discovery on the sanitizer
// leg. Skip the sandbox cleanup entirely when only listing.
if (::testing::GTEST_FLAG(list_tests)) {
return RUN_ALL_TESTS();
}
// Best-effort cleanup of this process's settings sandbox. QStandardPaths test
// mode is never auto-cleaned, and on macOS the $HOME override above is ignored,
// so without this every run litters ~/.qttest with one pid-unique directory.
// Safe under parallel runs: the pid-unique application name above guarantees
// this directory belongs to this process alone.
const auto cleanupSandbox = qScopeGuard([] {
QDir(SettingsCache::instance().getDataPath()).removeRecursively();
QDir(SettingsCache::instance().getCachePath()).removeRecursively();
});
return RUN_ALL_TESTS();
}
}