Compare commits

...

3 Commits

Author SHA1 Message Date
BruebachL
a799cd097a [PrintingSelector] Sync modified and history state on bulk selection (#6379)
* [PrintingSelector] Emit deckModified when using bulk selection

* [PrintingSelector] Hook up history manager.

* [PrintingSelector] Remember card amount.

* Return early.

Took 18 minutes

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2025-12-03 08:23:34 +01:00
RickyRister
b4e3f2cba9 [Oracle] Support importing tokens and spoilers from local file (#6387) 2025-12-03 00:19:56 -05:00
RickyRister
658ae83157 [DeckList] Make DeckList not a QObject (#6383) 2025-12-03 00:18:46 -05:00
14 changed files with 164 additions and 93 deletions

View File

@@ -424,7 +424,6 @@ void DeckEditorDeckDockWidget::setDeck(DeckLoader *_deck)
deckLoader->setParent(this);
deckModel->setDeckList(deckLoader->getDeckList());
connect(deckLoader, &DeckLoader::deckLoaded, deckModel, &DeckListModel::rebuildTree);
connect(deckLoader->getDeckList(), &DeckList::deckHashChanged, deckModel, &DeckListModel::deckHashChanged);
emit requestDeckHistoryClear();
historyManagerWidget->setDeckListModel(deckModel);
@@ -452,7 +451,7 @@ void DeckEditorDeckDockWidget::syncDisplayWidgetsToModel()
sortDeckModelToDeckView();
expandAll();
deckTagsDisplayWidget->connectDeckList(deckModel->getDeckList());
deckTagsDisplayWidget->setDeckList(deckModel->getDeckList());
}
void DeckEditorDeckDockWidget::sortDeckModelToDeckView()
@@ -485,7 +484,7 @@ void DeckEditorDeckDockWidget::cleanDeck()
emit deckModified();
emit deckChanged();
updateBannerCardComboBox();
deckTagsDisplayWidget->connectDeckList(deckModel->getDeckList());
deckTagsDisplayWidget->setDeckList(deckModel->getDeckList());
}
void DeckEditorDeckDockWidget::recursiveExpand(const QModelIndex &index)

View File

@@ -145,31 +145,49 @@ void DlgSelectSetForCards::retranslateUi()
void DlgSelectSetForCards::actOK()
{
QMap<QString, QStringList> modifiedSetsAndCardsMap = getModifiedCards();
if (modifiedSetsAndCardsMap.isEmpty()) {
accept(); // Nothing to do
} else {
emit deckAboutToBeModified(tr("Bulk modified printings."));
}
for (QString modifiedSet : modifiedSetsAndCardsMap.keys()) {
for (QString card : modifiedSetsAndCardsMap.value(modifiedSet)) {
QModelIndex find_card = model->findCard(card, DECK_ZONE_MAIN);
if (!find_card.isValid()) {
continue;
}
int amount =
model->data(find_card.siblingAtColumn(DeckListModelColumns::CARD_AMOUNT), Qt::DisplayRole).toInt();
model->removeRow(find_card.row(), find_card.parent());
CardInfoPtr cardInfo = CardDatabaseManager::query()->getCardInfo(card);
PrintingInfo printing = CardDatabaseManager::query()->getSpecificPrinting(card, modifiedSet, "");
model->addCard(ExactCard(cardInfo, printing), DECK_ZONE_MAIN);
for (int i = 0; i < amount; i++) {
model->addCard(ExactCard(cardInfo, printing), DECK_ZONE_MAIN);
}
}
}
if (!modifiedSetsAndCardsMap.isEmpty()) {
emit deckModified();
}
accept();
}
void DlgSelectSetForCards::actClear()
{
emit deckAboutToBeModified(tr("Cleared all printing information."));
DeckLoader::clearSetNamesAndNumbers(model->getDeckList());
emit deckModified();
accept();
}
void DlgSelectSetForCards::actSetAllToPreferred()
{
emit deckAboutToBeModified(tr("Set all printings to preferred."));
DeckLoader::clearSetNamesAndNumbers(model->getDeckList());
DeckLoader::setProviderIdToPreferredPrinting(model->getDeckList());
emit deckModified();
accept();
}

View File

@@ -37,6 +37,8 @@ public:
signals:
void widgetOrderChanged();
void orderChanged();
void deckAboutToBeModified(const QString &reason);
void deckModified();
public slots:
void actOK();

View File

@@ -40,6 +40,11 @@ public:
return deckModel;
}
[[nodiscard]] AbstractTabDeckEditor *getDeckEditor() const
{
return deckEditor;
}
public slots:
void retranslateUi();
void updateDisplay();

View File

@@ -1,6 +1,7 @@
#include "printing_selector_card_selection_widget.h"
#include "../../../interface/widgets/dialogs/dlg_select_set_for_cards.h"
#include "../tabs/abstract_tab_deck_editor.h"
/**
* @brief Constructs a PrintingSelectorCardSelectionWidget for navigating through cards in the deck.
@@ -48,6 +49,10 @@ void PrintingSelectorCardSelectionWidget::connectSignals()
void PrintingSelectorCardSelectionWidget::selectSetForCards()
{
auto *setSelectionDialog = new DlgSelectSetForCards(nullptr, parent->getDeckModel());
connect(setSelectionDialog, &DlgSelectSetForCards::deckAboutToBeModified, parent->getDeckEditor(),
&AbstractTabDeckEditor::onDeckHistorySaveRequested);
connect(setSelectionDialog, &DlgSelectSetForCards::deckModified, parent->getDeckEditor(),
&AbstractTabDeckEditor::onDeckModified);
if (!setSelectionDialog->exec()) {
return;
}

View File

@@ -28,21 +28,15 @@ DeckPreviewDeckTagsDisplayWidget::DeckPreviewDeckTagsDisplayWidget(QWidget *_par
flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded);
if (_deckList) {
connectDeckList(_deckList);
setDeckList(_deckList);
}
layout->addWidget(flowWidget);
}
void DeckPreviewDeckTagsDisplayWidget::connectDeckList(DeckList *_deckList)
void DeckPreviewDeckTagsDisplayWidget::setDeckList(DeckList *_deckList)
{
if (deckList) {
disconnect(deckList, &DeckList::deckTagsChanged, this, &DeckPreviewDeckTagsDisplayWidget::refreshTags);
}
deckList = _deckList;
connect(deckList, &DeckList::deckTagsChanged, this, &DeckPreviewDeckTagsDisplayWidget::refreshTags);
refreshTags();
}
@@ -150,6 +144,7 @@ void DeckPreviewDeckTagsDisplayWidget::openTagEditDlg()
QStringList updatedTags = dialog.getActiveTags();
deckList->setTags(updatedTags);
deckPreviewWidget->deckLoader->saveToFile(deckPreviewWidget->filePath, DeckLoader::CockatriceFormat);
refreshTags();
}
}
} else if (parentWidget()) {
@@ -181,6 +176,7 @@ void DeckPreviewDeckTagsDisplayWidget::openTagEditDlg()
QStringList updatedTags = dialog.getActiveTags();
deckList->setTags(updatedTags);
deckEditor->setModified(true);
refreshTags();
}
}
}

View File

@@ -21,7 +21,7 @@ class DeckPreviewDeckTagsDisplayWidget : public QWidget
public:
explicit DeckPreviewDeckTagsDisplayWidget(QWidget *_parent, DeckList *_deckList);
void connectDeckList(DeckList *_deckList);
void setDeckList(DeckList *_deckList);
void refreshTags();
DeckList *deckList;
FlowWidget *flowWidget;

View File

@@ -87,6 +87,12 @@ DeckList::DeckList()
root = new InnerDecklistNode;
}
DeckList::DeckList(const DeckList &other)
: metadata(other.metadata), sideboardPlans(other.sideboardPlans), root(new InnerDecklistNode(other.getRoot())),
cachedDeckHash(other.cachedDeckHash)
{
}
DeckList::DeckList(const QString &nativeString)
{
root = new InnerDecklistNode;
@@ -443,11 +449,8 @@ bool DeckList::loadFromStream_Plain(QTextStream &in, bool preserveMetadata)
cardName.replace(diff.key(), diff.value());
}
// Resolve complete card name, this function does nothing if the name is not found
cardName = getCompleteCardName(cardName);
// Determine the zone (mainboard/sideboard)
QString zoneName = getCardZoneFromName(cardName, sideboard ? DECK_ZONE_SIDE : DECK_ZONE_MAIN);
QString zoneName = sideboard ? DECK_ZONE_SIDE : DECK_ZONE_MAIN;
// make new entry in decklist
new DecklistCardNode(cardName, amount, getZoneObjFromName(zoneName), -1, setCode, collectorNumber);
@@ -708,12 +711,11 @@ QString DeckList::getDeckHash() const
}
/**
* Invalidates the cached deckHash and emits the deckHashChanged signal.
* Invalidates the cached deckHash.
*/
void DeckList::refreshDeckHash()
{
cachedDeckHash = QString();
emit deckHashChanged();
}
/**

View File

@@ -1,5 +1,5 @@
/**
* @file decklist.h
* @file deck_list.h
* @brief Defines the DeckList class and supporting types for managing a full
* deck structure including cards, zones, sideboard plans, and
* serialization to/from multiple formats. This is a logic class which
@@ -93,7 +93,7 @@ public:
* @brief Represents a complete deck, including metadata, zones, cards,
* and sideboard plans.
*
* A DeckList is a QObject wrapper around an `InnerDecklistNode` tree,
* A DeckList is a wrapper around an `InnerDecklistNode` tree,
* enriched with metadata like deck name, comments, tags, banner card,
* and multiple sideboard plans.
*
@@ -110,10 +110,6 @@ public:
* - Owns the root `InnerDecklistNode` tree.
* - Owns `SideboardPlan` instances stored in `sideboardPlans`.
*
* ### Signals:
* - @c deckHashChanged() — emitted when the deck contents change.
* - @c deckTagsChanged() — emitted when tags are added/removed.
*
* ### Example workflow:
* ```
* DeckList deck;
@@ -123,10 +119,8 @@ public:
* deck.saveToFile_Native(device);
* ```
*/
class DeckList : public QObject
class DeckList
{
Q_OBJECT
public:
struct Metadata
{
@@ -158,37 +152,7 @@ private:
static void getCardRefListHelper(InnerDecklistNode *item, QList<CardRef> &result);
InnerDecklistNode *getZoneObjFromName(const QString &zoneName);
protected:
/**
* @brief Map a card name to its zone.
* Override in subclasses for format-specific logic.
* @param cardName Card being placed.
* @param currentZoneName Zone candidate.
* @return Zone name to use.
*/
virtual QString getCardZoneFromName(const QString /*cardName*/, QString currentZoneName)
{
return currentZoneName;
}
/**
* @brief Produce the complete display name of a card.
* Override in subclasses to add set suffixes or annotations.
* @param cardName Base name.
* @return Full display name.
*/
virtual QString getCompleteCardName(const QString &cardName) const
{
return cardName;
}
signals:
/// Emitted when the deck hash changes.
void deckHashChanged();
/// Emitted when the deck tags are modified.
void deckTagsChanged();
public slots:
public:
/// @name Metadata setters
///@{
void setName(const QString &_name = QString())
@@ -202,17 +166,14 @@ public slots:
void setTags(const QStringList &_tags = QStringList())
{
metadata.tags = _tags;
emit deckTagsChanged();
}
void addTag(const QString &_tag)
{
metadata.tags.append(_tag);
emit deckTagsChanged();
}
void clearTags()
{
metadata.tags.clear();
emit deckTagsChanged();
}
void setBannerCard(const CardRef &_bannerCard = {})
{
@@ -224,15 +185,13 @@ public slots:
}
///@}
public:
/// @brief Construct an empty deck.
explicit DeckList();
/// @brief Delete copy constructor.
DeckList(const DeckList &) = delete;
DeckList &operator=(const DeckList &) = delete;
/// @brief Copy constructor (deep copies the node tree)
DeckList(const DeckList &other);
/// @brief Construct from a serialized native-format string.
explicit DeckList(const QString &nativeString);
~DeckList() override;
virtual ~DeckList();
/// @name Metadata getters
/// The individual metadata getters still exist for backwards compatibility.

View File

@@ -5,8 +5,11 @@
DeckListModel::DeckListModel(QObject *parent)
: QAbstractItemModel(parent), lastKnownColumn(1), lastKnownOrder(Qt::AscendingOrder)
{
// This class will leak the decklist object. We cannot safely delete it in the dtor because the deckList field is a
// non-owning pointer and another deckList might have been assigned to it.
// `DeckListModel::cleanList` also leaks for the same reason.
// TODO: fix the leak
deckList = new DeckList;
deckList->setParent(this);
root = new InnerDecklistNode;
}
@@ -284,6 +287,7 @@ bool DeckListModel::setData(const QModelIndex &index, const QVariant &value, con
emitRecursiveUpdates(index);
deckList->refreshDeckHash();
emit deckHashChanged();
emit dataChanged(index, index);
return true;
@@ -422,6 +426,7 @@ QModelIndex DeckListModel::addCard(const ExactCard &card, const QString &zoneNam
cardNode->setCardCollectorNumber(printingInfo.getProperty("num"));
cardNode->setCardProviderId(printingInfo.getProperty("uuid"));
deckList->refreshDeckHash();
emit deckHashChanged();
}
sort(lastKnownColumn, lastKnownOrder);
emitRecursiveUpdates(parentIndex);

View File

@@ -672,13 +672,21 @@ QString LoadTokensPage::getFileType()
return tr("XML; token database (*.xml)");
}
QString LoadTokensPage::getFilePromptName()
{
return tr("tokens");
}
void LoadTokensPage::retranslateUi()
{
setTitle(tr("Tokens import"));
setSubTitle(tr("Please specify a compatible source for token data."));
urlLabel->setText(tr("Download URL:"));
urlRadioButton->setText(tr("Download URL:"));
fileRadioButton->setText(tr("Local file:"));
urlButton->setText(tr("Restore default URL"));
fileButton->setText(tr("Choose file..."));
pathLabel->setText(tr("The token database will be saved at the following location:") + "<br>" +
SettingsCache::instance().getTokenDatabasePath());
defaultPathCheckBox->setText(tr("Save to a custom path (not recommended)"));
@@ -709,13 +717,21 @@ QString LoadSpoilersPage::getFileType()
return tr("XML; spoiler database (*.xml)");
}
QString LoadSpoilersPage::getFilePromptName()
{
return tr("spoiler");
}
void LoadSpoilersPage::retranslateUi()
{
setTitle(tr("Spoilers import"));
setSubTitle(tr("Please specify a compatible source for spoiler data."));
urlLabel->setText(tr("Download URL:"));
urlRadioButton->setText(tr("Download URL:"));
fileRadioButton->setText(tr("Local file:"));
urlButton->setText(tr("Restore default URL"));
fileButton->setText(tr("Choose file..."));
pathLabel->setText(tr("The spoiler database will be saved at the following location:") + "<br>" +
SettingsCache::instance().getSpoilerCardDatabasePath());
defaultPathCheckBox->setText(tr("Save to a custom path (not recommended)"));

View File

@@ -131,6 +131,7 @@ protected:
QString getDefaultSavePath() override;
QString getWindowTitle() override;
QString getFileType() override;
QString getFilePromptName() override;
};
class LoadTokensPage : public SimpleDownloadFilePage
@@ -148,6 +149,7 @@ protected:
QString getDefaultSavePath() override;
QString getWindowTitle() override;
QString getFileType() override;
QString getFilePromptName() override;
void initializePage() override;
};

View File

@@ -12,32 +12,43 @@
#include <QNetworkReply>
#include <QProgressBar>
#include <QPushButton>
#include <QRadioButton>
#include <QtGui>
SimpleDownloadFilePage::SimpleDownloadFilePage(QWidget *parent) : OracleWizardPage(parent)
{
urlLabel = new QLabel(this);
urlRadioButton = new QRadioButton(this);
fileRadioButton = new QRadioButton(this);
urlLineEdit = new QLineEdit(this);
fileLineEdit = new QLineEdit(this);
progressLabel = new QLabel(this);
progressBar = new QProgressBar(this);
urlRadioButton->setChecked(true);
urlButton = new QPushButton(this);
connect(urlButton, &QPushButton::clicked, this, &SimpleDownloadFilePage::actRestoreDefaultUrl);
defaultPathCheckBox = new QCheckBox(this);
fileButton = new QPushButton(this);
connect(fileButton, &QPushButton::clicked, this, &SimpleDownloadFilePage::actLoadCardFile);
defaultPathCheckBox = new QCheckBox(this);
pathLabel = new QLabel(this);
pathLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
auto *layout = new QGridLayout(this);
layout->addWidget(urlLabel, 0, 0);
layout->addWidget(urlRadioButton, 0, 0);
layout->addWidget(urlLineEdit, 0, 1);
layout->addWidget(urlButton, 1, 1, Qt::AlignRight);
layout->addWidget(pathLabel, 2, 0, 1, 2);
layout->addWidget(defaultPathCheckBox, 3, 0, 1, 2);
layout->addWidget(progressLabel, 4, 0);
layout->addWidget(progressBar, 4, 1);
layout->addWidget(fileRadioButton, 2, 0);
layout->addWidget(fileLineEdit, 2, 1);
layout->addWidget(fileButton, 3, 1, Qt::AlignRight);
layout->addWidget(pathLabel, 4, 0, 1, 2);
layout->addWidget(defaultPathCheckBox, 5, 0, 1, 2);
layout->addWidget(progressLabel, 6, 0);
layout->addWidget(progressBar, 6, 1);
setLayout(layout);
}
@@ -56,6 +67,31 @@ void SimpleDownloadFilePage::actRestoreDefaultUrl()
urlLineEdit->setText(getDefaultUrl());
}
void SimpleDownloadFilePage::actLoadCardFile()
{
QFileDialog dialog(this, tr("Load %1 file").arg(getFilePromptName()));
dialog.setFileMode(QFileDialog::ExistingFile);
QString extensions = "*.json *.xml";
#ifdef HAS_ZLIB
extensions += " *.zip";
#endif
#ifdef HAS_LZMA
extensions += " *.xz";
#endif
dialog.setNameFilter(tr("%1 file (%1)").arg(getFilePromptName(), extensions));
if (!fileLineEdit->text().isEmpty() && QFile::exists(fileLineEdit->text())) {
dialog.selectFile(fileLineEdit->text());
}
if (!dialog.exec()) {
return;
}
fileLineEdit->setText(dialog.selectedFiles().at(0));
}
bool SimpleDownloadFilePage::validatePage()
{
// if data has already been downloaded, pass directly to the "save" step
@@ -68,22 +104,41 @@ bool SimpleDownloadFilePage::validatePage()
}
}
QUrl url = QUrl::fromUserInput(urlLineEdit->text());
if (!url.isValid()) {
QMessageBox::critical(this, tr("Error"), tr("The provided URL is not valid: ") + url.toString());
return false;
// else, try to import sets
if (urlRadioButton->isChecked()) {
QUrl url = QUrl::fromUserInput(urlLineEdit->text());
if (!url.isValid()) {
QMessageBox::critical(this, tr("Error"), tr("The provided URL is not valid: ") + url.toString());
return false;
}
progressLabel->setText(tr("Downloading (0MB)"));
// show an infinite progressbar
progressBar->setMaximum(0);
progressBar->setMinimum(0);
progressBar->setValue(0);
progressLabel->show();
progressBar->show();
wizard()->disableButtons();
downloadFile(url);
} else if (fileRadioButton->isChecked()) {
QFile cardFile(fileLineEdit->text());
if (!cardFile.exists()) {
QMessageBox::critical(this, tr("Error"), tr("Please choose a file."));
return false;
}
if (!cardFile.open(QIODevice::ReadOnly)) {
QMessageBox::critical(nullptr, tr("Error"), tr("Cannot open file '%1'.").arg(fileLineEdit->text()));
return false;
}
downloadData = cardFile.readAll();
wizard()->next();
}
progressLabel->setText(tr("Downloading (0MB)"));
// show an infinite progressbar
progressBar->setMaximum(0);
progressBar->setMinimum(0);
progressBar->setValue(0);
progressLabel->show();
progressBar->show();
wizard()->disableButtons();
downloadFile(url);
return false;
}

View File

@@ -3,6 +3,8 @@
#include <QWizardPage>
class QFile;
class QRadioButton;
class OracleWizard;
class QCheckBox;
class QLabel;
@@ -43,15 +45,19 @@ protected:
virtual QString getDefaultSavePath() = 0;
virtual QString getWindowTitle() = 0;
virtual QString getFileType() = 0;
virtual QString getFilePromptName() = 0;
bool saveToFile();
bool internalSaveToFile(const QString &fileName);
protected:
QByteArray downloadData;
QLabel *urlLabel;
QLabel *pathLabel;
QRadioButton *urlRadioButton;
QRadioButton *fileRadioButton;
QLineEdit *urlLineEdit;
QLineEdit *fileLineEdit;
QPushButton *urlButton;
QPushButton *fileButton;
QLabel *pathLabel;
QLabel *progressLabel;
QProgressBar *progressBar;
QCheckBox *defaultPathCheckBox;
@@ -60,6 +66,7 @@ signals:
void parsedDataReady();
private slots:
void actRestoreDefaultUrl();
void actLoadCardFile();
void actDownloadProgress(qint64 received, qint64 total);
void actDownloadFinished();
};