Merge branch 'master' into tooomm-qt5

This commit is contained in:
tooomm 2026-07-03 23:02:35 +02:00
commit ae9ce701f4
290 changed files with 10683 additions and 4451 deletions

View file

@ -9,6 +9,7 @@
#include <QFutureWatcher>
#include <QPrinter>
#include <QRegularExpression>
#include <QSaveFile>
#include <QStringList>
#include <QTextCursor>
#include <QTextDocument>
@ -129,7 +130,10 @@ std::optional<LoadedDeck> DeckLoader::loadFromRemote(const QString &nativeString
std::optional<LoadedDeck::LoadInfo>
DeckLoader::saveToFile(const DeckList &deck, const QString &fileName, DeckFileFormat::Format fmt)
{
QFile file(fileName);
// Use QSaveFile so that a failed write (e.g. a full disk) leaves the existing deck untouched
// instead of truncating it to a 0-byte file. The target is only replaced once every byte has
// been flushed successfully in commit().
QSaveFile file(fileName);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
qCWarning(DeckLoaderLog) << "Could not create or open file:" << fileName;
return std::nullopt;
@ -145,15 +149,19 @@ DeckLoader::saveToFile(const DeckList &deck, const QString &fileName, DeckFileFo
break;
}
file.flush();
file.close();
qCInfo(DeckLoaderLog) << "Saved deck to " << fileName << "with format" << fmt << "-" << success;
if (!success) {
file.cancelWriting();
qCWarning(DeckLoaderLog) << "Failed to serialize deck for file:" << fileName;
return std::nullopt;
}
if (!file.commit()) {
qCWarning(DeckLoaderLog) << "Failed to save deck to " << fileName << ":" << file.errorString();
return std::nullopt;
}
qCInfo(DeckLoaderLog) << "Saved deck to " << fileName << "with format" << fmt;
LoadedDeck::LoadInfo lastLoadInfo = {fileName, fmt};
return lastLoadInfo;
}
@ -196,38 +204,44 @@ bool DeckLoader::updateLastLoadedTimestamp(LoadedDeck &deck)
QDateTime originalTimestamp = fileInfo.lastModified();
// Open the file for writing
QFile file(fileName);
// Use QSaveFile so that a failed write (e.g. a full disk) cannot truncate an existing deck to a
// 0-byte file while merely bumping its timestamp.
QSaveFile file(fileName);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
qCWarning(DeckLoaderLog) << "Failed to open file for writing:" << fileName;
return false;
}
bool result = false;
// Perform file modifications
deck.deckList.setLastLoadedTimestamp(QDateTime::currentDateTime().toString());
result = deck.deckList.saveToFile_Native(&file);
file.close(); // Close the file to ensure changes are flushed
if (result) {
// Re-open the file and set the original timestamp
if (!file.open(QIODevice::ReadWrite)) {
qCWarning(DeckLoaderLog) << "Failed to re-open file to set timestamp:" << fileName;
return false;
}
if (!file.setFileTime(originalTimestamp, QFileDevice::FileModificationTime)) {
qCWarning(DeckLoaderLog) << "Failed to set modification time for file:" << fileName;
file.close();
return false;
}
file.close();
if (!deck.deckList.saveToFile_Native(&file)) {
file.cancelWriting();
qCWarning(DeckLoaderLog) << "Failed to serialize deck for file:" << fileName;
return false;
}
return result;
if (!file.commit()) {
qCWarning(DeckLoaderLog) << "Failed to update timestamp for file:" << fileName << ":" << file.errorString();
return false;
}
// Re-open the file and restore the original timestamp, so that updating the lastLoadedTimestamp
// does not change the file's modification time.
QFile timestampFile(fileName);
if (!timestampFile.open(QIODevice::ReadWrite)) {
qCWarning(DeckLoaderLog) << "Failed to re-open file to set timestamp:" << fileName;
return false;
}
if (!timestampFile.setFileTime(originalTimestamp, QFileDevice::FileModificationTime)) {
qCWarning(DeckLoaderLog) << "Failed to set modification time for file:" << fileName;
timestampFile.close();
return false;
}
timestampFile.close();
return true;
}
static QString getDomainForWebsite(DeckLoader::DecklistWebsite website)
@ -444,51 +458,54 @@ bool DeckLoader::convertToCockatriceFormat(LoadedDeck &deck)
return false;
}
// Determine the format before touching any file, so an already-converted or
// unsupported deck never truncates or deletes anything.
switch (DeckFileFormat::getFormatFromName(fileName)) {
case DeckFileFormat::PlainText:
break;
case DeckFileFormat::Cockatrice:
qCInfo(DeckLoaderLog) << "File is already in Cockatrice format. No conversion needed.";
return true;
default:
qCWarning(DeckLoaderLog) << "Unsupported file format for conversion:" << fileName;
return false;
}
// Change the file extension to .cod
QFileInfo fileInfo(fileName);
QString newFileName = QDir::toNativeSeparators(fileInfo.path() + "/" + fileInfo.completeBaseName() + ".cod");
// Open the new file for writing
QFile file(newFileName);
// Use QSaveFile so a failed write (e.g. a full disk) cannot leave a 0-byte .cod
// behind and then delete the original deck.
QSaveFile file(newFileName);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
qCWarning(DeckLoaderLog) << "Failed to open file for writing:" << newFileName;
return false;
}
bool result = false;
// Perform file modifications based on the detected format
switch (DeckFileFormat::getFormatFromName(fileName)) {
case DeckFileFormat::PlainText:
// Save in Cockatrice's native format
result = deck.deckList.saveToFile_Native(&file);
break;
case DeckFileFormat::Cockatrice:
qCInfo(DeckLoaderLog) << "File is already in Cockatrice format. No conversion needed.";
result = true;
break;
default:
qCWarning(DeckLoaderLog) << "Unsupported file format for conversion:" << fileName;
result = false;
break;
if (!deck.deckList.saveToFile_Native(&file)) {
file.cancelWriting();
qCWarning(DeckLoaderLog) << "Failed to serialize deck for file:" << newFileName;
return false;
}
file.close();
// Delete the old file if conversion was successful
if (result) {
if (!QFile::remove(fileName)) {
qCWarning(DeckLoaderLog) << "Failed to delete original file:" << fileName;
} else {
qCInfo(DeckLoaderLog) << "Original file deleted successfully:" << fileName;
}
deck.lastLoadInfo = {
.fileName = newFileName,
.fileFormat = DeckFileFormat::Cockatrice,
};
if (!file.commit()) {
qCWarning(DeckLoaderLog) << "Failed to convert deck to " << newFileName << ":" << file.errorString();
return false;
}
return result;
// Conversion succeeded: delete the original file.
if (!QFile::remove(fileName)) {
qCWarning(DeckLoaderLog) << "Failed to delete original file:" << fileName;
} else {
qCInfo(DeckLoaderLog) << "Original file deleted successfully:" << fileName;
}
deck.lastLoadInfo = {
.fileName = newFileName,
.fileFormat = DeckFileFormat::Cockatrice,
};
return true;
}
void DeckLoader::printDeckListNode(QTextCursor *cursor, const InnerDecklistNode *node)

View file

@ -271,6 +271,9 @@ void ThemeManager::applyStyleAndPalette(const QString &themeName,
const PaletteConfig &palCfg,
const QString &activeScheme)
{
#if (QT_VERSION < QT_VERSION_CHECK(6, 5, 0))
Q_UNUSED(activeScheme)
#endif
QString styleName = themeCfg.styleName;
if (styleName.isEmpty() || styleName.compare("Default", Qt::CaseInsensitive) == 0) {
if (themeName == FUSION_THEME_NAME) {
@ -396,6 +399,7 @@ static QString roleBgName(ThemeManager::Role role)
default:
Q_ASSERT(false);
return {};
}
}

View file

@ -58,16 +58,6 @@ void CardGroupDisplayWidget::mousePressEvent(QMouseEvent *event)
}
}
void CardGroupDisplayWidget::onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card)
{
emit cardClicked(event, card);
}
void CardGroupDisplayWidget::onHover(const ExactCard &card)
{
emit cardHovered(card);
}
void CardGroupDisplayWidget::onSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
{
auto proxyModel = qobject_cast<QAbstractProxyModel *>(selectionModel->model());
@ -154,8 +144,8 @@ QWidget *CardGroupDisplayWidget::constructWidgetForIndex(QPersistentModelIndex i
widget->setScaleFactor(cardSizeWidget->getSlider()->value());
widget->setCard(CardDatabaseManager::query()->getCard({cardName, cardProviderId}));
connect(widget, &CardInfoPictureWithTextOverlayWidget::imageClicked, this, &CardGroupDisplayWidget::onClick);
connect(widget, &CardInfoPictureWithTextOverlayWidget::hoveredOnCard, this, &CardGroupDisplayWidget::onHover);
connect(widget, &CardInfoPictureWithTextOverlayWidget::cardClicked, this, &CardGroupDisplayWidget::cardClicked);
connect(widget, &CardInfoPictureWithTextOverlayWidget::hoveredOnCard, this, &CardGroupDisplayWidget::cardHovered);
connect(cardSizeWidget->getSlider(), &QSlider::valueChanged, widget, &CardInfoPictureWidget::setScaleFactor);
indexToWidgetMap[index].append(widget);

View file

@ -48,8 +48,6 @@ public:
public slots:
void mousePressEvent(QMouseEvent *event) override;
void onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card);
void onHover(const ExactCard &card);
virtual QWidget *constructWidgetForIndex(QPersistentModelIndex index);
virtual void updateCardDisplays();
virtual void onCardAddition(const QModelIndex &parent, int first, int last);
@ -59,7 +57,7 @@ public slots:
void resizeEvent(QResizeEvent *event) override;
signals:
void cardClicked(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card);
void cardClicked(QMouseEvent *event, const ExactCard &card);
void cardHovered(const ExactCard &card);
void cleanupRequested(CardGroupDisplayWidget *cardGroupDisplayWidget);

View file

@ -1,6 +1,6 @@
#include "card_info_display_widget.h"
#include "../../../game/board/card_item.h"
#include "../../../game_graphics/board/card_item.h"
#include "card_info_picture_widget.h"
#include "card_info_text_widget.h"

View file

@ -1,7 +1,7 @@
#include "card_info_frame_widget.h"
#include "../../../client/settings/cache_settings.h"
#include "../../../game/board/card_item.h"
#include "../../../game_graphics/board/card_item.h"
#include "card_info_display_widget.h"
#include "card_info_picture_widget.h"
#include "card_info_text_widget.h"

View file

@ -1,7 +1,7 @@
#include "card_info_picture_widget.h"
#include "../../../client/settings/cache_settings.h"
#include "../../../game/board/card_item.h"
#include "../../../game_graphics/board/card_item.h"
#include "../../../interface/card_picture_loader/card_picture_loader.h"
#include "../../../interface/widgets/tabs/tab_supervisor.h"
#include "../../window_main.h"
@ -341,7 +341,7 @@ void CardInfoPictureWidget::mousePressEvent(QMouseEvent *event)
createRightClickMenu()->popup(QCursor::pos());
}
emit cardClicked(event);
emit cardClicked(event, exactCard);
}
void CardInfoPictureWidget::hideEvent(QHideEvent *event)
@ -427,13 +427,13 @@ QMenu *CardInfoPictureWidget::createAddToOpenDeckMenu()
QAction *addCard = addCardMenu->addAction(tr("Mainboard"));
connect(addCard, &QAction::triggered, this, [this, deckEditorTab] {
deckEditorTab->updateCard(exactCard);
deckEditorTab->actAddCard(exactCard);
deckEditorTab->addCard(exactCard, DECK_ZONE_MAIN);
});
QAction *addCardSideboard = addCardMenu->addAction(tr("Sideboard"));
connect(addCardSideboard, &QAction::triggered, this, [this, deckEditorTab] {
deckEditorTab->updateCard(exactCard);
deckEditorTab->actAddCardToSideboard(exactCard);
deckEditorTab->addCard(exactCard, DECK_ZONE_SIDE);
});
}

View file

@ -43,7 +43,7 @@ signals:
void hoveredOnCard(const ExactCard &hoveredCard);
void cardScaleFactorChanged(int _scale);
void cardChanged(const ExactCard &card);
void cardClicked(QMouseEvent *event);
void cardClicked(QMouseEvent *event, const ExactCard &card);
protected:
void resizeEvent(QResizeEvent *event) override;

View file

@ -93,7 +93,7 @@ void CardInfoPictureWithTextOverlayWidget::setHighlighted(bool _highlighted)
void CardInfoPictureWithTextOverlayWidget::mousePressEvent(QMouseEvent *event)
{
emit imageClicked(event, this);
emit cardClicked(event, getCard());
}
/**

View file

@ -35,8 +35,6 @@ public:
void setHighlighted(bool _highlighted);
[[nodiscard]] QSize sizeHint() const override;
signals:
void imageClicked(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance);
protected:
void paintEvent(QPaintEvent *event) override;

View file

@ -1,6 +1,6 @@
#include "card_info_text_widget.h"
#include "../../../game/board/card_item.h"
#include "../../../game_graphics/board/card_item.h"
#include <QGridLayout>
#include <QLabel>

View file

@ -51,7 +51,7 @@ DeckCardZoneDisplayWidget::DeckCardZoneDisplayWidget(QWidget *parent,
// User Interaction
// =====================================================================================================================
void DeckCardZoneDisplayWidget::onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card)
void DeckCardZoneDisplayWidget::onClick(QMouseEvent *event, const ExactCard &card)
{
emit cardClicked(event, card, zoneName);
}

View file

@ -42,7 +42,7 @@ public:
void addCardsToOverlapWidget();
public slots:
void onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card);
void onClick(QMouseEvent *event, const ExactCard &card);
void onHover(const ExactCard &card);
void cleanupInvalidCardGroup(CardGroupDisplayWidget *displayWidget);
void constructAppropriateWidget(QPersistentModelIndex index);
@ -55,7 +55,7 @@ public slots:
void onCategoryRemoval(const QModelIndex &parent, int first, int last);
signals:
void cardClicked(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card, QString zoneName);
void cardClicked(QMouseEvent *event, const ExactCard &card, const QString &zoneName);
void cardHovered(const ExactCard &card);
void activeSortCriteriaChanged(QStringList activeSortCriteria);
void requestCleanup(DeckCardZoneDisplayWidget *displayWidget);

View file

@ -0,0 +1,167 @@
#include "card_database_view.h"
#include "../../../client/settings/cache_settings.h"
#include "card_database_display_model.h"
#include "card_database_model.h"
#include <QApplication>
#include <QClipboard>
#include <QHeaderView>
#include <QMenu>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/card/relation/card_relation.h>
#include <libcockatrice/deck_list/tree/inner_deck_list_node.h>
static bool canBeCommander(const CardInfo &cardInfo)
{
return (cardInfo.getCardType().contains("Legendary", Qt::CaseInsensitive) &&
cardInfo.getCardType().contains("Creature", Qt::CaseInsensitive)) ||
cardInfo.getText().contains("can be your commander", Qt::CaseInsensitive);
}
CardDatabaseView::CardDatabaseView(QWidget *parent, CardDatabaseDisplayModel *model)
: QTreeView(parent), databaseDisplayModel(model)
{
// set up object
setUniformRowHeights(true);
setRootIsDecorated(false);
setAlternatingRowColors(true);
setSortingEnabled(true);
sortByColumn(0, Qt::AscendingOrder);
QTreeView::setModel(databaseDisplayModel);
setContextMenuPolicy(Qt::CustomContextMenu);
connect(databaseDisplayModel, &CardDatabaseDisplayModel::modelDirty, this,
&CardDatabaseView::resetSelectionIfEmpty);
connect(this, &QTreeView::customContextMenuRequested, this, &CardDatabaseView::openCustomMenu);
connect(selectionModel(), &QItemSelectionModel::currentRowChanged, this, &CardDatabaseView::updateCard);
connect(this, &QTreeView::doubleClicked, this, &CardDatabaseView::actDoubleClick);
// layout settings
QByteArray dbHeaderState = SettingsCache::instance().layouts().getDeckEditorDbHeaderState();
if (dbHeaderState.isNull()) {
// first run
setColumnWidth(0, 200);
} else {
header()->restoreState(dbHeaderState);
}
connect(header(), &QHeaderView::geometriesChanged, this, &CardDatabaseView::saveDbHeaderState);
// create key filters
searchKeySignals.setObjectName("searchKeySignals");
connect(&searchKeySignals, &KeySignals::onEnter, this, [this] { addCard(DECK_ZONE_MAIN); });
connect(&searchKeySignals, &KeySignals::onCtrlAltEqual, this, [this] { addCard(DECK_ZONE_MAIN); });
connect(&searchKeySignals, &KeySignals::onCtrlAltRBracket, this, [this] { addCard(DECK_ZONE_SIDE); });
connect(&searchKeySignals, &KeySignals::onCtrlAltMinus, this, [this] { decrementCard(DECK_ZONE_MAIN); });
connect(&searchKeySignals, &KeySignals::onCtrlAltLBracket, this, [this] { decrementCard(DECK_ZONE_SIDE); });
connect(&searchKeySignals, &KeySignals::onCtrlAltEnter, this, [this] { addCard(DECK_ZONE_SIDE); });
connect(&searchKeySignals, &KeySignals::onCtrlEnter, this, [this] { addCard(DECK_ZONE_SIDE); });
connect(&searchKeySignals, &KeySignals::onCtrlC, this, &CardDatabaseView::copyDatabaseCellContents);
}
QString CardDatabaseView::currentCardName() const
{
const QModelIndex currentIndex = selectionModel()->currentIndex();
if (!currentIndex.isValid()) {
return {};
}
return currentIndex.siblingAtColumn(CardDatabaseModel::NameColumn).data().toString();
}
void CardDatabaseView::actDoubleClick()
{
if (QApplication::keyboardModifiers() & Qt::ControlModifier) {
addCard(DECK_ZONE_SIDE);
} else {
addCard(DECK_ZONE_MAIN);
}
}
void CardDatabaseView::addCard(const QString &zoneName)
{
emit cardAdded(currentCardName(), zoneName);
}
void CardDatabaseView::decrementCard(const QString &zoneName)
{
emit cardDecremented(currentCardName(), zoneName);
}
void CardDatabaseView::updateCard(const QModelIndex &current, const QModelIndex & /*previous*/)
{
if (!current.isValid()) {
return;
}
const QString cardName = current.siblingAtColumn(CardDatabaseModel::NameColumn).data().toString();
if (!current.model()->hasChildren(current.siblingAtColumn(CardDatabaseModel::NameColumn))) {
emit cardChanged(cardName);
}
}
void CardDatabaseView::resetSelectionIfEmpty()
{
QModelIndexList sel = selectionModel()->selectedRows();
if (sel.isEmpty() && databaseDisplayModel->rowCount() > 0) {
selectionModel()->setCurrentIndex(databaseDisplayModel->index(0, 0),
QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows);
}
}
void CardDatabaseView::copyDatabaseCellContents() const
{
auto _data = selectionModel()->currentIndex().data();
QApplication::clipboard()->setText(_data.toString());
}
void CardDatabaseView::saveDbHeaderState()
{
SettingsCache::instance().layouts().setDeckEditorDbHeaderState(header()->saveState());
}
void CardDatabaseView::openCustomMenu(QPoint point)
{
CardInfoPtr card = CardDatabaseManager::query()->getCardInfo(currentCardName());
if (!card) {
return;
}
QMenu menu;
// add to deck and sideboard options
QAction *addToDeck = menu.addAction(tr("Add to Deck"));
QAction *addToSideboard = menu.addAction(tr("Add to Sideboard"));
QAction *selectPrinting = menu.addAction(tr("Select Printing"));
connect(addToDeck, &QAction::triggered, this, [this, card] { emit cardAdded(card->getName(), DECK_ZONE_MAIN); });
connect(addToSideboard, &QAction::triggered, this,
[this, card] { emit cardAdded(card->getName(), DECK_ZONE_SIDE); });
connect(selectPrinting, &QAction::triggered, this, &CardDatabaseView::selectPrintingClicked);
if (canBeCommander(*card)) {
QAction *edhRecCommander = menu.addAction(tr("Show on EDHRec (Commander)"));
connect(edhRecCommander, &QAction::triggered, this, [this, card] { emit edhrecClicked(card, true); });
}
QAction *edhRecCard = menu.addAction(tr("Show on EDHRec (Card)"));
connect(edhRecCard, &QAction::triggered, this, [this, card] { emit edhrecClicked(card, false); });
// filling out the related cards submenu
auto *relatedMenu = new QMenu(tr("Show Related cards"));
menu.addMenu(relatedMenu);
auto relatedCards = card->getAllRelatedCards();
if (relatedCards.isEmpty()) {
relatedMenu->setDisabled(true);
} else {
for (const CardRelation *rel : relatedCards) {
const QString &relatedCardName = rel->getName();
QAction *relatedCard = relatedMenu->addAction(relatedCardName);
connect(relatedCard, &QAction::triggered, this,
[this, relatedCardName] { emit relatedCardClicked(relatedCardName); });
}
}
menu.exec(mapToGlobal(point));
}

View file

@ -0,0 +1,59 @@
#ifndef COCKATRICE_CARD_DATABASE_VIEW_H
#define COCKATRICE_CARD_DATABASE_VIEW_H
#include "../../key_signals.h"
#include <QTreeView>
#include <libcockatrice/card/card_info.h>
class CardDatabaseModel;
class CardDatabaseDisplayModel;
/**
* @brief The card database table.
*/
class CardDatabaseView : public QTreeView
{
Q_OBJECT
KeySignals searchKeySignals;
CardDatabaseDisplayModel *databaseDisplayModel;
public:
explicit CardDatabaseView(QWidget *parent, CardDatabaseDisplayModel *model);
QString currentCardName() const;
/**
* @brief Get the KeySignals that are connected to this view.
* You can install the KeySignals as an eventFilter to capture keyboard shortcuts for adding and decrementing cards.
*/
KeySignals *getKeySignals()
{
return &searchKeySignals;
}
signals:
void cardChanged(const QString &cardName);
void cardAdded(const QString &cardName, const QString &zoneName);
void cardDecremented(const QString &cardName, const QString &zoneName);
void edhrecClicked(const CardInfoPtr &cardInfo, bool isCommander);
void selectPrintingClicked();
void relatedCardClicked(const QString &relatedCard);
private slots:
void actDoubleClick();
void addCard(const QString &zoneName);
void decrementCard(const QString &zoneName);
void updateCard(const QModelIndex &current, const QModelIndex &);
void resetSelectionIfEmpty();
void copyDatabaseCellContents() const;
void saveDbHeaderState();
void openCustomMenu(QPoint point);
};
#endif // COCKATRICE_CARD_DATABASE_VIEW_H

View file

@ -13,7 +13,7 @@ DeckEditorCardDatabaseDockWidget::DeckEditorCardDatabaseDockWidget(AbstractTabDe
void DeckEditorCardDatabaseDockWidget::createDatabaseDisplayDock(AbstractTabDeckEditor *deckEditor)
{
databaseDisplayWidget = new DeckEditorDatabaseDisplayWidget(this, deckEditor);
databaseDisplayWidget = new DeckEditorDatabaseDisplayWidget(this, deckEditor->databaseModel);
auto *frame = new QVBoxLayout;
frame->setObjectName("databaseDisplayFrame");
@ -29,19 +29,16 @@ void DeckEditorCardDatabaseDockWidget::createDatabaseDisplayDock(AbstractTabDeck
// connect signals
connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::cardChanged, deckEditor,
&AbstractTabDeckEditor::updateCard);
connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::addCardToMainDeck, deckEditor,
&AbstractTabDeckEditor::actAddCard);
connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::addCardToSideboard, deckEditor,
&AbstractTabDeckEditor::actAddCardToSideboard);
connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::decrementCardFromMainDeck, deckEditor,
&AbstractTabDeckEditor::actDecrementCard);
connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::decrementCardFromSideboard, deckEditor,
&AbstractTabDeckEditor::actDecrementCardFromSideboard);
}
CardDatabase *DeckEditorCardDatabaseDockWidget::getDatabase() const
{
return databaseDisplayWidget->databaseModel->getDatabase();
connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::cardAdded, deckEditor,
&AbstractTabDeckEditor::addCard);
connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::cardDecremented, deckEditor,
&AbstractTabDeckEditor::decrementCard);
connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::edhrecRequested, deckEditor,
&AbstractTabDeckEditor::openEdhrecTab);
connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::printingSelectorRequested, deckEditor,
&AbstractTabDeckEditor::showPrintingSelector);
connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::cardInfoRequested, deckEditor,
&AbstractTabDeckEditor::updateCardInfo);
}
void DeckEditorCardDatabaseDockWidget::retranslateUi()

View file

@ -17,7 +17,6 @@ public:
DeckEditorDatabaseDisplayWidget *databaseDisplayWidget;
CardDatabase *getDatabase() const;
void setFilterTree(FilterTree *filterTree);
public slots:

View file

@ -5,24 +5,17 @@
#include "../../../interface/widgets/tabs/abstract_tab_deck_editor.h"
#include "../../../interface/widgets/tabs/tab_supervisor.h"
#include "../../pixel_map_generator.h"
#include "card_database_view.h"
#include <QClipboard>
#include <QHeaderView>
#include <QMenu>
#include <QToolButton>
#include <QTreeView>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/card/relation/card_relation.h>
static bool canBeCommander(const CardInfo &cardInfo)
{
return (cardInfo.getCardType().contains("Legendary", Qt::CaseInsensitive) &&
cardInfo.getCardType().contains("Creature", Qt::CaseInsensitive)) ||
cardInfo.getText().contains("can be your commander", Qt::CaseInsensitive);
}
DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(QWidget *parent, AbstractTabDeckEditor *deckEditor)
: QWidget(parent), deckEditor(deckEditor)
DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(QWidget *parent, CardDatabaseModel *databaseModel)
: QWidget(parent)
{
setObjectName("databaseDisplayWidget");
@ -36,62 +29,34 @@ DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(QWidget *parent
searchEdit->setClearButtonEnabled(true);
searchEdit->addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition);
auto help = searchEdit->addAction(QPixmap("theme:icons/info"), QLineEdit::TrailingPosition);
searchEdit->installEventFilter(&searchKeySignals);
setFocusProxy(searchEdit);
setFocusPolicy(Qt::ClickFocus);
searchKeySignals.setObjectName("searchKeySignals");
connect(searchEdit, &SearchLineEdit::textChanged, this, &DeckEditorDatabaseDisplayWidget::updateSearch);
connect(&searchKeySignals, &KeySignals::onEnter, this, &DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck);
connect(&searchKeySignals, &KeySignals::onCtrlAltEqual, this,
&DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck);
connect(&searchKeySignals, &KeySignals::onCtrlAltRBracket, this,
&DeckEditorDatabaseDisplayWidget::actAddCardToSideboard);
connect(&searchKeySignals, &KeySignals::onCtrlAltMinus, this,
&DeckEditorDatabaseDisplayWidget::actDecrementCardFromMainDeck);
connect(&searchKeySignals, &KeySignals::onCtrlAltLBracket, this,
&DeckEditorDatabaseDisplayWidget::actDecrementCardFromSideboard);
connect(&searchKeySignals, &KeySignals::onCtrlAltEnter, this,
&DeckEditorDatabaseDisplayWidget::actAddCardToSideboard);
connect(&searchKeySignals, &KeySignals::onCtrlEnter, this, &DeckEditorDatabaseDisplayWidget::actAddCardToSideboard);
connect(&searchKeySignals, &KeySignals::onCtrlC, this, &DeckEditorDatabaseDisplayWidget::copyDatabaseCellContents);
connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(searchEdit); });
databaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), true, this);
databaseModel->setObjectName("databaseModel");
databaseDisplayModel = new CardDatabaseDisplayModel(this);
databaseDisplayModel->setObjectName("databaseDisplayModel");
databaseDisplayModel->setSourceModel(databaseModel);
databaseDisplayModel->setFilterKeyColumn(0);
databaseView = new QTreeView(this);
databaseView = new CardDatabaseView(this, databaseDisplayModel);
databaseView->setObjectName("databaseView");
databaseView->setFocusProxy(searchEdit);
databaseView->setUniformRowHeights(true);
databaseView->setRootIsDecorated(false);
databaseView->setAlternatingRowColors(true);
databaseView->setSortingEnabled(true);
databaseView->sortByColumn(0, Qt::AscendingOrder);
databaseView->setModel(databaseDisplayModel);
databaseView->setContextMenuPolicy(Qt::CustomContextMenu);
connect(databaseView, &QTreeView::customContextMenuRequested, this,
&DeckEditorDatabaseDisplayWidget::databaseCustomMenu);
connect(databaseView->selectionModel(), &QItemSelectionModel::currentRowChanged, this,
&DeckEditorDatabaseDisplayWidget::updateCard);
connect(databaseView, &QTreeView::doubleClicked, this, &DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck);
QByteArray dbHeaderState = SettingsCache::instance().layouts().getDeckEditorDbHeaderState();
if (dbHeaderState.isNull()) {
// first run
databaseView->setColumnWidth(0, 200);
} else {
databaseView->header()->restoreState(dbHeaderState);
}
connect(databaseView->header(), &QHeaderView::geometriesChanged, this,
&DeckEditorDatabaseDisplayWidget::saveDbHeaderState);
searchEdit->setTreeView(databaseView);
searchEdit->installEventFilter(databaseView->getKeySignals());
connect(searchEdit, &SearchLineEdit::textChanged, databaseDisplayModel, &CardDatabaseDisplayModel::setStringFilter);
connect(databaseView, &CardDatabaseView::cardAdded, this, &DeckEditorDatabaseDisplayWidget::addCard);
connect(databaseView, &CardDatabaseView::cardDecremented, this, &DeckEditorDatabaseDisplayWidget::decrementCard);
connect(databaseView, &CardDatabaseView::cardChanged, this, &DeckEditorDatabaseDisplayWidget::updateCard);
connect(databaseView, &CardDatabaseView::edhrecClicked, this, &DeckEditorDatabaseDisplayWidget::edhrecRequested);
connect(databaseView, &CardDatabaseView::selectPrintingClicked, this,
&DeckEditorDatabaseDisplayWidget::printingSelectorRequested);
connect(databaseView, &CardDatabaseView::relatedCardClicked, this,
&DeckEditorDatabaseDisplayWidget::onRelatedCardClicked);
aAddCard = new QAction(QString(), this);
aAddCard->setIcon(QPixmap("theme:icons/arrow_right_green"));
@ -117,121 +82,39 @@ DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(QWidget *parent
retranslateUi();
}
void DeckEditorDatabaseDisplayWidget::updateSearch(const QString &search)
{
databaseDisplayModel->setStringFilter(search);
QModelIndexList sel = databaseView->selectionModel()->selectedRows();
if (sel.isEmpty() && databaseDisplayModel->rowCount()) {
databaseView->selectionModel()->setCurrentIndex(databaseDisplayModel->index(0, 0),
QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows);
}
}
void DeckEditorDatabaseDisplayWidget::clearAllDatabaseFilters()
{
databaseDisplayModel->clearFilterAll();
searchEdit->setText("");
}
void DeckEditorDatabaseDisplayWidget::updateCard(const QModelIndex &current, const QModelIndex & /*previous*/)
{
if (!current.isValid()) {
return;
}
const QString cardName = current.siblingAtColumn(CardDatabaseModel::NameColumn).data().toString();
if (!current.model()->hasChildren(current.siblingAtColumn(CardDatabaseModel::NameColumn))) {
emit cardChanged(CardDatabaseManager::query()->getPreferredCard(cardName));
}
}
void DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck()
{
highlightAllSearchEdit();
emit addCardToMainDeck(currentCard());
addCard(databaseView->currentCardName(), DECK_ZONE_MAIN);
}
void DeckEditorDatabaseDisplayWidget::actAddCardToSideboard()
{
addCard(databaseView->currentCardName(), DECK_ZONE_SIDE);
}
void DeckEditorDatabaseDisplayWidget::addCard(const QString &cardName, const QString &zoneName)
{
highlightAllSearchEdit();
emit addCardToSideboard(currentCard());
ExactCard exactCard = CardDatabaseManager::query()->getPreferredCard(cardName);
emit cardAdded(exactCard, zoneName);
}
void DeckEditorDatabaseDisplayWidget::actDecrementCardFromMainDeck()
void DeckEditorDatabaseDisplayWidget::decrementCard(const QString &cardName, const QString &zoneName)
{
emit decrementCardFromMainDeck(currentCard());
ExactCard exactCard = CardDatabaseManager::query()->getPreferredCard(cardName);
emit cardDecremented(exactCard, zoneName);
}
void DeckEditorDatabaseDisplayWidget::actDecrementCardFromSideboard()
void DeckEditorDatabaseDisplayWidget::updateCard(const QString &cardName)
{
emit decrementCardFromSideboard(currentCard());
}
ExactCard DeckEditorDatabaseDisplayWidget::currentCard() const
{
const QModelIndex currentIndex = databaseView->selectionModel()->currentIndex();
if (!currentIndex.isValid()) {
return {};
}
const QString cardName = currentIndex.siblingAtColumn(CardDatabaseModel::NameColumn).data().toString();
return CardDatabaseManager::query()->getPreferredCard(cardName);
}
void DeckEditorDatabaseDisplayWidget::databaseCustomMenu(QPoint point)
{
QMenu menu;
ExactCard card = currentCard();
if (card) {
// add to deck and sideboard options
QAction *addToDeck, *addToSideboard, *selectPrinting, *edhRecCommander, *edhRecCard;
addToDeck = menu.addAction(tr("Add to Deck"));
addToSideboard = menu.addAction(tr("Add to Sideboard"));
selectPrinting = menu.addAction(tr("Select Printing"));
connect(selectPrinting, &QAction::triggered, this, [this, card] { deckEditor->showPrintingSelector(); });
if (canBeCommander(card.getInfo())) {
edhRecCommander = menu.addAction(tr("Show on EDHRec (Commander)"));
connect(edhRecCommander, &QAction::triggered, this,
[this, card] { deckEditor->getTabSupervisor()->addEdhrecTab(card.getCardPtr(), true); });
}
edhRecCard = menu.addAction(tr("Show on EDHRec (Card)"));
connect(addToDeck, &QAction::triggered, this, &DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck);
connect(addToSideboard, &QAction::triggered, this, &DeckEditorDatabaseDisplayWidget::actAddCardToSideboard);
connect(edhRecCard, &QAction::triggered, this,
[this, card] { deckEditor->getTabSupervisor()->addEdhrecTab(card.getCardPtr()); });
// filling out the related cards submenu
auto *relatedMenu = new QMenu(tr("Show Related cards"));
menu.addMenu(relatedMenu);
auto relatedCards = card.getInfo().getAllRelatedCards();
if (relatedCards.isEmpty()) {
relatedMenu->setDisabled(true);
} else {
for (const CardRelation *rel : relatedCards) {
const QString &relatedCardName = rel->getName();
QAction *relatedCard = relatedMenu->addAction(relatedCardName);
connect(
relatedCard, &QAction::triggered, deckEditor->cardInfoDockWidget->cardInfo,
[this, relatedCardName] { deckEditor->cardInfoDockWidget->cardInfo->setCard(relatedCardName); });
}
}
menu.exec(databaseView->mapToGlobal(point));
}
}
void DeckEditorDatabaseDisplayWidget::copyDatabaseCellContents()
{
auto _data = databaseView->selectionModel()->currentIndex().data();
QApplication::clipboard()->setText(_data.toString());
}
void DeckEditorDatabaseDisplayWidget::saveDbHeaderState()
{
SettingsCache::instance().layouts().setDeckEditorDbHeaderState(databaseView->header()->saveState());
ExactCard exactCard = CardDatabaseManager::query()->getPreferredCard(cardName);
emit cardChanged(exactCard);
}
void DeckEditorDatabaseDisplayWidget::setFilterTree(FilterTree *filterTree)
@ -248,4 +131,10 @@ void DeckEditorDatabaseDisplayWidget::retranslateUi()
void DeckEditorDatabaseDisplayWidget::highlightAllSearchEdit()
{
searchEdit->setSelection(0, searchEdit->text().length());
}
void DeckEditorDatabaseDisplayWidget::onRelatedCardClicked(const QString &relatedCard)
{
ExactCard exactCard = CardDatabaseManager::query()->guessCard({relatedCard});
emit cardInfoRequested(exactCard);
}

View file

@ -9,7 +9,6 @@
#define DECK_EDITOR_DATABASE_DISPLAY_WIDGET_H
#include "../../../interface/widgets/tabs/abstract_tab_deck_editor.h"
#include "../../key_signals.h"
#include "../utility/custom_line_edit.h"
#include <QHBoxLayout>
@ -17,45 +16,44 @@
#include <libcockatrice/models/database/card_database_display_model.h>
#include <libcockatrice/models/database/card_database_model.h>
class CardDatabaseView;
class AbstractTabDeckEditor;
class DeckEditorDatabaseDisplayWidget : public QWidget
{
Q_OBJECT
public:
explicit DeckEditorDatabaseDisplayWidget(QWidget *parent, AbstractTabDeckEditor *deckEditor);
AbstractTabDeckEditor *deckEditor;
CardDatabaseModel *databaseModel;
CardDatabaseDisplayModel *databaseDisplayModel;
explicit DeckEditorDatabaseDisplayWidget(QWidget *parent, CardDatabaseModel *databaseModel);
QTreeView *getDatabaseView()
CardDatabaseView *getDatabaseView() const
{
return databaseView;
}
public slots:
ExactCard currentCard() const;
void setFilterTree(FilterTree *filterTree);
void clearAllDatabaseFilters();
void updateSearch(const QString &search);
void updateCard(const QModelIndex &current, const QModelIndex &);
void actAddCardToMainDeck();
void actAddCardToSideboard();
void actDecrementCardFromMainDeck();
void actDecrementCardFromSideboard();
void databaseCustomMenu(QPoint point);
void copyDatabaseCellContents();
void addCard(const QString &cardName, const QString &zoneName);
void decrementCard(const QString &cardName, const QString &zoneName);
void updateCard(const QString &cardName);
signals:
void addCardToMainDeck(const ExactCard &card);
void addCardToSideboard(const ExactCard &card);
void decrementCardFromMainDeck(const ExactCard &card);
void decrementCardFromSideboard(const ExactCard &card);
void cardAdded(const ExactCard &card, const QString &zoneName);
void cardDecremented(const ExactCard &card, const QString &zoneName);
void cardChanged(const ExactCard &_card);
void edhrecRequested(const CardInfoPtr &cardInfo, bool isCommander);
void printingSelectorRequested();
void cardInfoRequested(const ExactCard &card);
private:
KeySignals searchKeySignals;
QTreeView *databaseView;
CardDatabaseDisplayModel *databaseDisplayModel;
CardDatabaseView *databaseView;
QHBoxLayout *searchLayout;
SearchLineEdit *searchEdit;
QAction *aAddCard, *aAddCardToSideboard;
@ -66,7 +64,8 @@ private:
private slots:
void retranslateUi();
void saveDbHeaderState();
void onRelatedCardClicked(const QString &relatedCard);
};
#endif // DECK_EDITOR_DATABASE_DISPLAY_WIDGET_H

View file

@ -11,7 +11,7 @@
#include <QSplitter>
#include <QTextEdit>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/utility/trice_limits.h>
#include <libcockatrice/utility/string_limits.h>
static int findRestoreIndex(const CardRef &wanted, const QComboBox *combo)
{

View file

@ -255,6 +255,10 @@ bool DeckStateManager::swapCardAtIndex(const QModelIndex &idx)
}
QString zoneName = gparent.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString();
// tokens have no swap target
if (zoneName == DECK_ZONE_TOKENS) {
return false;
}
QString otherZoneName = zoneName == DECK_ZONE_MAIN ? DECK_ZONE_SIDE : DECK_ZONE_MAIN;
QString reason = tr("Moved to %1 1 × \"%2\" (%3)") //

View file

@ -12,7 +12,7 @@
#include <QMessageBox>
#include <QPushButton>
#include <QRadioButton>
#include <libcockatrice/utility/trice_limits.h>
#include <libcockatrice/utility/string_limits.h>
DlgConnect::DlgConnect(QWidget *parent) : QDialog(parent)
{

View file

@ -17,7 +17,7 @@
#include <QSpinBox>
#include <libcockatrice/protocol/pb/serverinfo_game.pb.h>
#include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/utility/trice_limits.h>
#include <libcockatrice/utility/string_limits.h>
void DlgCreateGame::sharedCtor()
{

View file

@ -8,7 +8,7 @@
#include <QLabel>
#include <QPushButton>
#include <QVBoxLayout>
#include <libcockatrice/utility/trice_limits.h>
#include <libcockatrice/utility/string_limits.h>
DlgEditAvatar::DlgEditAvatar(QWidget *parent) : QDialog(parent), image()
{

View file

@ -7,7 +7,7 @@
#include <QHBoxLayout>
#include <QLabel>
#include <QMessageBox>
#include <libcockatrice/utility/trice_limits.h>
#include <libcockatrice/utility/string_limits.h>
DlgEditPassword::DlgEditPassword(QWidget *parent) : QDialog(parent)
{

View file

@ -19,7 +19,7 @@
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/models/database/card_database_model.h>
#include <libcockatrice/models/database/token/token_edit_model.h>
#include <libcockatrice/utility/trice_limits.h>
#include <libcockatrice/utility/string_limits.h>
DlgEditTokens::DlgEditTokens(QWidget *parent) : QDialog(parent), currentCard(nullptr)
{

View file

@ -6,7 +6,7 @@
#include <QGridLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <libcockatrice/utility/trice_limits.h>
#include <libcockatrice/utility/string_limits.h>
DlgEditUser::DlgEditUser(QWidget *parent, QString email, QString country, QString realName) : QDialog(parent)
{

View file

@ -7,7 +7,7 @@
#include <QHBoxLayout>
#include <QLabel>
#include <QMessageBox>
#include <libcockatrice/utility/trice_limits.h>
#include <libcockatrice/utility/string_limits.h>
DlgForgotPasswordChallenge::DlgForgotPasswordChallenge(QWidget *parent) : QDialog(parent)
{

View file

@ -7,7 +7,7 @@
#include <QHBoxLayout>
#include <QLabel>
#include <QMessageBox>
#include <libcockatrice/utility/trice_limits.h>
#include <libcockatrice/utility/string_limits.h>
DlgForgotPasswordRequest::DlgForgotPasswordRequest(QWidget *parent) : QDialog(parent)
{

View file

@ -7,7 +7,7 @@
#include <QHBoxLayout>
#include <QLabel>
#include <QMessageBox>
#include <libcockatrice/utility/trice_limits.h>
#include <libcockatrice/utility/string_limits.h>
DlgForgotPasswordReset::DlgForgotPasswordReset(QWidget *parent) : QDialog(parent)
{

View file

@ -62,7 +62,7 @@ WndSets::WndSets(QWidget *parent) : QMainWindow(parent)
// search field
searchField = new LineEditUnfocusable;
searchField->setObjectName("searchEdit");
searchField->setPlaceholderText(tr("Search by set name, code, or type"));
searchField->setPlaceholderText(tr("Search by set name, code, type, or release date"));
searchField->addAction(QPixmap("theme:icons/search"), LineEditUnfocusable::LeadingPosition);
searchField->setClearButtonEnabled(true);
setFocusProxy(searchField);

View file

@ -8,7 +8,7 @@
#include <QHBoxLayout>
#include <QLabel>
#include <QMessageBox>
#include <libcockatrice/utility/trice_limits.h>
#include <libcockatrice/utility/string_limits.h>
DlgRegister::DlgRegister(QWidget *parent) : QDialog(parent)
{

View file

@ -219,10 +219,25 @@ void DlgUpdate::downloadError(const QString &errorString)
void DlgUpdate::downloadSuccessful(const QUrl &filepath)
{
setLabel(tr("Installing..."));
QString installerPath = filepath.toLocalFile();
QString appDir = QDir::toNativeSeparators(QCoreApplication::applicationDirPath());
QProcess process;
process.setProgram(installerPath);
// NSIS needs the /D= argument to be an UNQUOTED string, even if it contains spaces. Qt likes to quote arguments if
// they contain spaces, so we use the windows exclusive QProcess::setNativeArguments in the only case where this is
// relevant, which preserves the argument unquoted.
#ifdef Q_OS_WIN
process.setNativeArguments(QString("/R /D=%1").arg(appDir));
#else
// Linux/macOS: normal argument passing (not relevant since they update differently.)
process.setArguments({"/R", QString("/D=%1").arg(appDir)});
#endif
// Try to open the installer. If it opens, quit Cockatrice
if (QProcess::startDetached(filepath.toLocalFile(),
QStringList()
<< "/R" << QString("/D=%1").arg(QCoreApplication::applicationDirPath()))) {
if (process.startDetached()) {
QMetaObject::invokeMethod(static_cast<MainWindow *>(parent()), "close", Qt::QueuedConnection);
qCInfo(DlgUpdateLog) << "Opened downloaded update file successfully - closing Cockatrice";
close();

View file

@ -193,6 +193,8 @@ void DeckEditorMenu::refreshShortcuts()
aEditDeckInClipboardRaw->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aEditDeckInClipboardRaw"));
aPrintDeck->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aPrintDeck"));
aLoadDeckFromWebsite->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aLoadDeckFromWebsite"));
aExportDeckDecklist->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aExportDeckDecklist"));
aExportDeckDecklistXyz->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aExportDeckDecklistXyz"));
aAnalyzeDeckDeckstats->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aAnalyzeDeck"));

View file

@ -8,7 +8,7 @@
* @brief Constructor for the AllZonesCardAmountWidget class.
*
* Initializes the widget with its layout and sets up the connections and necessary
* UI elements for managing card counts in both the mainboard and sideboard zones.
* UI elements for managing card counts in all the mainboard, tokensboard and sideboard zones.
*
* @param parent The parent widget.
* @param deckStateManager Pointer to the DeckStateManager
@ -31,13 +31,28 @@ AllZonesCardAmountWidget::AllZonesCardAmountWidget(QWidget *parent,
buttonBoxMainboard = new CardAmountWidget(this, deckStateManager, cardSizeSlider, rootCard, DECK_ZONE_MAIN);
zoneLabelSideboard = new ShadowBackgroundLabel(this, tr("Sideboard"));
buttonBoxSideboard = new CardAmountWidget(this, deckStateManager, cardSizeSlider, rootCard, DECK_ZONE_SIDE);
zoneLabelTokensboard = new ShadowBackgroundLabel(this, tr("Tokens"));
buttonBoxTokensboard = new CardAmountWidget(this, deckStateManager, cardSizeSlider, rootCard, DECK_ZONE_TOKENS);
layout->addWidget(zoneLabelMainboard, 0, Qt::AlignHCenter | Qt::AlignBottom);
layout->addWidget(buttonBoxMainboard, 0, Qt::AlignHCenter | Qt::AlignTop);
layout->addSpacing(25);
layout->addSpacing(12);
layout->addWidget(zoneLabelTokensboard, 0, Qt::AlignHCenter | Qt::AlignBottom);
layout->addWidget(buttonBoxTokensboard, 0, Qt::AlignHCenter | Qt::AlignTop);
layout->addSpacing(13);
layout->addWidget(zoneLabelSideboard, 0, Qt::AlignHCenter | Qt::AlignBottom);
layout->addWidget(buttonBoxSideboard, 0, Qt::AlignHCenter | Qt::AlignTop);
// Show Tokens buttons for token cards, Mainboard/Sideboard for non-token cards
bool isToken = rootCard.getInfo().getIsToken();
zoneLabelMainboard->setVisible(!isToken);
buttonBoxMainboard->setVisible(!isToken);
zoneLabelTokensboard->setVisible(isToken);
buttonBoxTokensboard->setVisible(isToken);
zoneLabelSideboard->setVisible(!isToken);
buttonBoxSideboard->setVisible(!isToken);
connect(cardSizeSlider, &QSlider::valueChanged, this, &AllZonesCardAmountWidget::adjustFontSize);
QTimer::singleShot(10, this, [this]() { adjustFontSize(this->cardSizeSlider->value()); });
@ -67,15 +82,17 @@ void AllZonesCardAmountWidget::adjustFontSize(int scalePercentage)
zoneLabelFont.setPointSize(newFontSize);
zoneLabelMainboard->setFont(zoneLabelFont);
zoneLabelSideboard->setFont(zoneLabelFont);
zoneLabelTokensboard->setFont(zoneLabelFont);
// Repaint the widget (if necessary)
repaint();
}
void AllZonesCardAmountWidget::setAmounts(int mainboardAmount, int sideboardAmount)
void AllZonesCardAmountWidget::setAmounts(int mainboardAmount, int sideboardAmount, int tokensboardAmount)
{
buttonBoxMainboard->setAmount(mainboardAmount);
buttonBoxSideboard->setAmount(sideboardAmount);
buttonBoxTokensboard->setAmount(tokensboardAmount);
}
/**
@ -99,11 +116,21 @@ int AllZonesCardAmountWidget::getSideboardAmount()
}
/**
* @brief Checks if the amount is at least one in either the mainboard or sideboard.
* @brief Gets the card count in the tokensboard zone.
*
* @return The number of cards in the tokensboard.
*/
int AllZonesCardAmountWidget::getTokensboardAmount()
{
return buttonBoxTokensboard->getAmount();
}
/**
* @brief Checks if the amount is at least one in either the mainboard or sideboard or tokensboard.
*/
bool AllZonesCardAmountWidget::isNonZero()
{
return getMainboardAmount() > 0 || getSideboardAmount() > 0;
return getMainboardAmount() > 0 || getSideboardAmount() > 0 || getTokensboardAmount() > 0;
}
/**

View file

@ -23,13 +23,14 @@ public:
const ExactCard &rootCard);
int getMainboardAmount();
int getSideboardAmount();
int getTokensboardAmount();
bool isNonZero();
void enterEvent(QEnterEvent *event) override;
public slots:
void adjustFontSize(int scalePercentage);
void setAmounts(int mainboardAmount, int sideboardAmount);
void setAmounts(int mainboardAmount, int sideboardAmount, int tokensboardAmount);
private:
QVBoxLayout *layout;
@ -38,6 +39,8 @@ private:
CardAmountWidget *buttonBoxMainboard;
QLabel *zoneLabelSideboard;
CardAmountWidget *buttonBoxSideboard;
QLabel *zoneLabelTokensboard;
CardAmountWidget *buttonBoxTokensboard;
};
#endif // ALL_ZONES_CARD_AMOUNT_WIDGET_H

View file

@ -11,7 +11,7 @@
* @param parent The parent widget.
* @param cardSizeSlider Pointer to the QSlider for adjusting font size.
* @param rootCard The root card to manage within the widget.
* @param zoneName The zone name (e.g., DECK_ZONE_MAIN or DECK_ZONE_SIDE).
* @param zoneName The zone name (e.g., DECK_ZONE_MAIN , DECK_ZONE_SIDE, or DECK_ZONE_TOKENS).
*/
CardAmountWidget::CardAmountWidget(QWidget *parent,
DeckStateManager *deckStateManager,
@ -36,13 +36,16 @@ CardAmountWidget::CardAmountWidget(QWidget *parent,
incrementButton->setFixedSize(parentWidget()->size().width() / 3, parentWidget()->size().height() / 9);
decrementButton->setFixedSize(parentWidget()->size().width() / 3, parentWidget()->size().height() / 9);
// Set up connections based on the zone (Mainboard or Sideboard)
// Set up connections based on the zone (Mainboard, Sideboard, or Tokensboard)
if (zoneName == DECK_ZONE_MAIN) {
connect(incrementButton, &QPushButton::clicked, this, &CardAmountWidget::addPrintingMainboard);
connect(decrementButton, &QPushButton::clicked, this, &CardAmountWidget::removePrintingMainboard);
} else if (zoneName == DECK_ZONE_SIDE) {
connect(incrementButton, &QPushButton::clicked, this, &CardAmountWidget::addPrintingSideboard);
connect(decrementButton, &QPushButton::clicked, this, &CardAmountWidget::removePrintingSideboard);
} else if (zoneName == DECK_ZONE_TOKENS) {
connect(incrementButton, &QPushButton::clicked, this, &CardAmountWidget::addPrintingTokensboard);
connect(decrementButton, &QPushButton::clicked, this, &CardAmountWidget::removePrintingTokensboard);
}
cardCountInZone = new QLabel(QString::number(amount), this);
@ -137,6 +140,19 @@ void CardAmountWidget::updateCardCount()
layout->activate();
}
static QString zoneLogName(const QString &zone)
{
if (zone == DECK_ZONE_MAIN) {
return "mainboard";
} else if (zone == DECK_ZONE_SIDE) {
return "sideboard";
} else if (zone == DECK_ZONE_TOKENS) {
return "tokens";
} else {
return "unknown";
}
}
static QModelIndex addAndReplacePrintings(DeckListModel *model,
const QModelIndex &existing,
const ExactCard &rootCard,
@ -161,9 +177,9 @@ static QModelIndex addAndReplacePrintings(DeckListModel *model,
}
/**
* @brief Adds a printing of the card to the specified zone (Mainboard or Sideboard).
* @brief Adds a printing of the card to the specified zone (Mainboard, Sideboard, or Tokensboard).
*
* @param zone The zone to add the card to (DECK_ZONE_MAIN or DECK_ZONE_SIDE).
* @param zone The zone to add the card to (DECK_ZONE_MAIN, DECK_ZONE_SIDE, or DECK_ZONE_TOKENS).
*/
void CardAmountWidget::addPrinting(const QString &zone)
{
@ -183,12 +199,13 @@ void CardAmountWidget::addPrinting(const QString &zone)
}
}
QString zoneName = zoneLogName(zone);
QString reason = QString("Added %1 copies of '%2 (%3) %4' to %5 [ProviderID: %6]%7")
.arg(1 + extraCopies)
.arg(rootCard.getName())
.arg(rootCard.getPrinting().getSet()->getShortName())
.arg(rootCard.getPrinting().getProperty("num"))
.arg(zone == DECK_ZONE_MAIN ? "mainboard" : "sideboard")
.arg(zoneName)
.arg(rootCard.getPrinting().getUuid())
.arg(replacingProviderless ? " (replaced providerless printings)" : "");
@ -218,6 +235,14 @@ void CardAmountWidget::addPrintingSideboard()
addPrinting(DECK_ZONE_SIDE);
}
/**
* @brief Adds a printing to the tokens zone.
*/
void CardAmountWidget::addPrintingTokensboard()
{
addPrinting(DECK_ZONE_TOKENS);
}
/**
* @brief Removes a printing from the mainboard zone.
*/
@ -234,18 +259,27 @@ void CardAmountWidget::removePrintingSideboard()
decrementCardHelper(DECK_ZONE_SIDE);
}
/**
* @brief Removes a printing from the tokens zone.
*/
void CardAmountWidget::removePrintingTokensboard()
{
decrementCardHelper(DECK_ZONE_TOKENS);
}
/**
* @brief Helper function to decrement the card count for a given zone.
*
* @param zone The zone from which to remove the card (DECK_ZONE_MAIN or DECK_ZONE_SIDE).
* @param zone The zone from which to remove the card (DECK_ZONE_MAIN, DECK_ZONE_SIDE, or DECK_ZONE_TOKENS).
*/
void CardAmountWidget::decrementCardHelper(const QString &zone)
{
QString zoneName = zoneLogName(zone);
QString reason = QString("Removed 1 copy of '%1 (%2) %3' from %4 [ProviderID: %5]")
.arg(rootCard.getName())
.arg(rootCard.getPrinting().getSet()->getShortName())
.arg(rootCard.getPrinting().getProperty("num"))
.arg(zone == DECK_ZONE_MAIN ? "mainboard" : "sideboard")
.arg(zoneName)
.arg(rootCard.getPrinting().getUuid());
deckStateManager->modifyDeck(reason, [this, &zone](auto model) {

View file

@ -60,8 +60,10 @@ private:
private slots:
void addPrintingMainboard();
void addPrintingSideboard();
void addPrintingTokensboard();
void removePrintingMainboard();
void removePrintingSideboard();
void removePrintingTokensboard();
void adjustFontSize(int scalePercentage);
};

View file

@ -105,23 +105,30 @@ void PrintingSelector::printingsInDeckChanged()
}
/**
* @return A map of uuid to amounts (main, side).
* @return A map of uuid to amounts (main, side, tokens).
*/
static QMap<QString, QPair<int, int>> tallyUuidCounts(const DeckListModel *model, const QString &cardName)
static QMap<QString, ZoneCounts> tallyUuidCounts(const DeckListModel *model, const QString &cardName)
{
QMap<QString, QPair<int, int>> map;
QMap<QString, ZoneCounts> map;
auto mainNodes = model->getCardNodesForZone(DECK_ZONE_MAIN);
for (auto &node : mainNodes) {
if (node->getName() == cardName) {
map[node->getCardProviderId()].first += node->getNumber();
map[node->getCardProviderId()].mainboard += node->getNumber();
}
}
auto sideNodes = model->getCardNodesForZone(DECK_ZONE_SIDE);
for (auto &node : sideNodes) {
if (node->getName() == cardName) {
map[node->getCardProviderId()].second += node->getNumber();
map[node->getCardProviderId()].sideboard += node->getNumber();
}
}
auto tokensNodes = model->getCardNodesForZone(DECK_ZONE_TOKENS);
for (auto &node : tokensNodes) {
if (node->getName() == cardName) {
map[node->getCardProviderId()].tokensboard += node->getNumber();
}
}

View file

@ -22,6 +22,13 @@
#define BATCH_SIZE 10
struct ZoneCounts
{
int mainboard = 0;
int sideboard = 0;
int tokensboard = 0;
};
class DeckStateManager;
class PrintingSelectorCardSearchWidget;
class PrintingSelectorCardSelectionWidget;
@ -59,9 +66,9 @@ signals:
/**
* The amounts of the printings in the deck has changed
* @param uuidToAmounts Map of uuids to the amounts (maindeck, sideboard) in the deck
* @param uuidToAmounts Map of uuids to the amounts (maindeck, sideboard, tokensboard) in the deck
*/
void cardAmountsChanged(const QMap<QString, QPair<int, int>> &uuidToAmounts);
void cardAmountsChanged(const QMap<QString, ZoneCounts> &uuidToAmounts);
private:
QVBoxLayout *layout;

View file

@ -67,10 +67,10 @@ void PrintingSelectorCardDisplayWidget::clampSetNameToPicture()
update();
}
void PrintingSelectorCardDisplayWidget::updateCardAmounts(const QMap<QString, QPair<int, int>> &uuidToAmounts)
void PrintingSelectorCardDisplayWidget::updateCardAmounts(const QMap<QString, ZoneCounts> &uuidToAmounts)
{
auto [main, side] = uuidToAmounts.value(rootCard.getPrinting().getUuid());
overlayWidget->updateCardAmounts(main, side);
auto counts = uuidToAmounts.value(rootCard.getPrinting().getUuid());
overlayWidget->updateCardAmounts(counts.mainboard, counts.sideboard, counts.tokensboard);
}
void PrintingSelectorCardDisplayWidget::resizeEvent(QResizeEvent *event)

View file

@ -27,7 +27,7 @@ public:
public slots:
void clampSetNameToPicture();
void updateCardAmounts(const QMap<QString, QPair<int, int>> &uuidToAmounts);
void updateCardAmounts(const QMap<QString, ZoneCounts> &uuidToAmounts);
void resizeEvent(QResizeEvent *event) override;

View file

@ -112,9 +112,11 @@ void PrintingSelectorCardOverlayWidget::enterEvent(QEnterEvent *event)
updateVisibility();
}
void PrintingSelectorCardOverlayWidget::updateCardAmounts(int mainboardAmount, int sideboardAmount)
void PrintingSelectorCardOverlayWidget::updateCardAmounts(int mainboardAmount,
int sideboardAmount,
int tokensboardAmount)
{
allZonesCardAmountWidget->setAmounts(mainboardAmount, sideboardAmount);
allZonesCardAmountWidget->setAmounts(mainboardAmount, sideboardAmount, tokensboardAmount);
updateVisibility();
}
@ -169,8 +171,8 @@ void PrintingSelectorCardOverlayWidget::updatePinBadgeVisibility()
/**
* @brief Handles the mouse leave event when the cursor leaves the overlay widget area.
*
* When the cursor leaves the widget, the card amount widget is hidden if both the mainboard and sideboard
* amounts are zero.
* When the cursor leaves the widget, the card amount widget is hidden if all of the mainboard, sideboard, and
* tokensboard amounts are zero.
*
* @param event The event triggered when the mouse leaves the widget.
*/

View file

@ -35,7 +35,7 @@ signals:
void cardPreferenceChanged();
public slots:
void updateCardAmounts(int mainboardAmount, int sideboardAmount);
void updateCardAmounts(int mainboardAmount, int sideboardAmount, int tokensboardAmount);
private slots:
void updateVisibility();

View file

@ -98,8 +98,7 @@ ReplayManager::ReplayManager(TabGame *parent, GameReplay *_replay)
void ReplayManager::replayNextEvent(EventProcessingOptions options)
{
game->getGame()->getGameEventHandler()->processGameEventContainer(
replay->event_list(timelineWidget->getCurrentEvent()), nullptr, options);
emit eventReplayed(replay->event_list(timelineWidget->getCurrentEvent()), options);
}
void ReplayManager::replayFinished()

View file

@ -27,6 +27,7 @@ public:
signals:
void requestChatAndPhaseReset();
void eventReplayed(const GameEventContainer &cont, EventProcessingOptions options);
private:
// Replay related members

View file

@ -0,0 +1,48 @@
#include "user_avatar_provider.h"
#include <libcockatrice/network/client/abstract/abstract_client.h>
#include <libcockatrice/protocol/pb/response_get_user_info.pb.h>
#include <libcockatrice/protocol/pending_command.h>
UserAvatarProvider::UserAvatarProvider(AbstractClient *client, QObject *parent) : QObject(parent), client(client)
{
}
const QMap<QString, QPixmap> &UserAvatarProvider::cache() const
{
return avatarCache;
}
void UserAvatarProvider::requestAvatar(const QString &userName)
{
if (avatarCache.contains(userName) || pending.contains(userName)) {
return;
}
pending.insert(userName);
Command_GetUserInfo cmd;
cmd.set_user_name(userName.toStdString());
PendingCommand *pend = client->prepareSessionCommand(cmd);
connect(pend, &PendingCommand::finished, this, [this, userName](const Response &r) {
pending.remove(userName);
const auto &response = r.GetExtension(Response_GetUserInfo::ext);
const auto &user = response.user_info();
const std::string &bmp = user.avatar_bmp();
QPixmap avatar;
if (!bmp.empty() &&
avatar.loadFromData(reinterpret_cast<const uchar *>(bmp.data()), static_cast<uint>(bmp.size()))) {
avatarCache.insert(userName, avatar);
} else {
avatarCache.insert(userName, QPixmap());
}
emit avatarUpdated(userName);
});
client->sendCommand(pend);
}

View file

@ -0,0 +1,30 @@
#ifndef COCKATRICE_USER_AVATAR_PROVIDER_H
#define COCKATRICE_USER_AVATAR_PROVIDER_H
#include <QMap>
#include <QObject>
#include <QPixmap>
#include <QSet>
class AbstractClient;
class UserAvatarProvider : public QObject
{
Q_OBJECT
public:
explicit UserAvatarProvider(AbstractClient *client, QObject *parent = nullptr);
void requestAvatar(const QString &userName);
const QMap<QString, QPixmap> &cache() const;
signals:
void avatarUpdated(const QString &userName);
private:
AbstractClient *client;
QMap<QString, QPixmap> avatarCache;
QSet<QString> pending;
};
#endif // COCKATRICE_USER_AVATAR_PROVIDER_H

View file

@ -0,0 +1,147 @@
#include "user_card_art_provider.h"
#include "../../../card_picture_loader/card_picture_loader.h"
#include <QPointer>
#include <libcockatrice/card/database/card_database_manager.h>
static QString makeKey(const QString &user, const QString &card, const QString &providerId)
{
return user + u'|' + card + u'|' + providerId;
}
UserCardArtProvider::UserCardArtProvider(QObject *parent) : QObject(parent)
{
dbReady = (CardDatabaseManager::getInstance()->getLoadStatus() == LoadStatus::Ok);
if (!dbReady) {
connect(CardDatabaseManager::getInstance(), &CardDatabase::cardDatabaseLoadingFinished, this,
&UserCardArtProvider::onDatabaseReady);
}
}
void UserCardArtProvider::onDatabaseReady()
{
dbReady = true;
processQueue();
}
const QMap<QString, QPixmap> &UserCardArtProvider::cache() const
{
return cardArtCache;
}
void UserCardArtProvider::requestCardArt(const QString &userName, const QString &cardName, const QString &providerId)
{
if (cardName.isEmpty()) {
return;
}
const QString key = makeKey(userName, cardName, providerId);
if (cardArtCache.contains(key) || pending.contains(key)) {
return;
}
pending.insert(key);
queue.enqueue(key);
processQueue();
}
QPixmap UserCardArtProvider::cropCardArt(const QPixmap &fullRes)
{
const QSize sz = fullRes.size();
const int marginX = sz.width() * 0.07;
const int topMargin = sz.height() * 0.11;
const int bottomMargin = sz.height() * 0.45;
const QRect foilRect(marginX, topMargin, sz.width() - 2 * marginX, sz.height() - topMargin - bottomMargin);
return fullRes.copy(foilRect.intersected(fullRes.rect()));
}
void UserCardArtProvider::insertIntoCache(const QString &key, const QPixmap &pixmap)
{
if (!cardArtCache.contains(key)) {
cacheInsertionOrder.append(key);
while (cacheInsertionOrder.size() > MaxCacheEntries) {
const QString evicted = cacheInsertionOrder.takeFirst();
cardArtCache.remove(evicted);
}
}
cardArtCache.insert(key, pixmap);
}
void UserCardArtProvider::processQueue()
{
if (!dbReady) {
return;
}
while (!queue.isEmpty()) {
const QString key = queue.dequeue();
const QStringList parts = key.split(u'|');
if (parts.size() != 3) {
pending.remove(key);
continue;
}
const QString userName = parts.at(0);
const QString cardName = parts.at(1);
const QString providerId = parts.at(2);
ExactCard card = CardDatabaseManager::query()->getCard({cardName, providerId});
if (!card) {
pending.remove(key);
continue;
}
QPixmap fullRes;
CardPictureLoader::getPixmap(fullRes, card, QSize(745, 1040));
// Synchronous hit (already loaded/on disk)
if (!fullRes.isNull()) {
insertIntoCache(key, cropCardArt(fullRes));
pending.remove(key);
emit cardArtUpdated(userName);
continue;
}
// Async load required.
QPointer<UserCardArtProvider> self(this);
auto conn = std::make_shared<QMetaObject::Connection>();
*conn = connect(card.getCardPtr().data(), &CardInfo::pixmapUpdated, this,
[self, key, userName, card, conn]() mutable {
if (!self) {
return;
}
QObject::disconnect(*conn);
QPixmap fullRes;
CardPictureLoader::getPixmap(fullRes, card, QSize(745, 1040));
if (!fullRes.isNull()) {
self->insertIntoCache(key, self->cropCardArt(fullRes));
} else {
self->insertIntoCache(key, QPixmap());
}
self->pending.remove(key);
emit self->cardArtUpdated(userName);
// Resume processing remaining queued items.
self->processQueue();
});
// Stop here. We'll continue when the async load finishes.
return;
}
}

View file

@ -0,0 +1,39 @@
#ifndef COCKATRICE_USER_CARD_ART_PROVIDER_H
#define COCKATRICE_USER_CARD_ART_PROVIDER_H
#include <QMap>
#include <QObject>
#include <QPixmap>
#include <QQueue>
#include <QSet>
class UserCardArtProvider : public QObject
{
Q_OBJECT
public:
explicit UserCardArtProvider(QObject *parent = nullptr);
void requestCardArt(const QString &userName, const QString &cardName, const QString &providerId);
const QMap<QString, QPixmap> &cache() const;
static QPixmap cropCardArt(const QPixmap &fullRes);
signals:
void cardArtUpdated(const QString &userName);
public slots:
void onDatabaseReady();
private:
bool dbReady = false;
static constexpr int MaxCacheEntries = 300;
QList<QString> cacheInsertionOrder; // FIFO eviction
QMap<QString, QPixmap> cardArtCache;
QSet<QString> pending;
QQueue<QString> queue;
void processQueue();
void insertIntoCache(const QString &key, const QPixmap &pixmap);
};
#endif // COCKATRICE_USER_CARD_ART_PROVIDER_H

View file

@ -0,0 +1,339 @@
#include "user_card_settings_dialog.h"
#include "../../../card_picture_loader/card_picture_loader.h"
#include "card/card_completer_proxy_model.h"
#include "card/card_search_model.h"
#include "card_database_display_model.h"
#include "card_database_model.h"
#include "user_card_art_provider.h"
#include "user_list_painter.h"
#include <QCompleter>
#include <QDialogButtonBox>
#include <QDoubleSpinBox>
#include <QFormLayout>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QPainter>
#include <QPainterPath>
#include <QPushButton>
#include <QRegularExpression>
#include <QVBoxLayout>
#include <libcockatrice/card/database/card_database_manager.h>
CardArtPreviewWidget::CardArtPreviewWidget(QWidget *parent) : QWidget(parent)
{
setMinimumSize(400, 72);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
}
void CardArtPreviewWidget::setPixmap(const QPixmap &pixmap)
{
sourcePixmap = pixmap;
update();
}
void CardArtPreviewWidget::setParams(const CardArtParams &p)
{
params = p;
update();
}
void CardArtPreviewWidget::paintEvent(QPaintEvent *)
{
QPainter painter(this);
painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::TextAntialiasing);
const QRect rect = this->rect();
const QColor accentColor(100, 116, 139);
const QRectF cardRect = QRectF(rect).adjusted(3, 2, -3, -2);
QLinearGradient bg(cardRect.topLeft(), cardRect.topRight());
bg.setColorAt(0, accentColor.darker(320));
bg.setColorAt(1, QColor(18, 22, 30));
painter.setPen(Qt::NoPen);
painter.setBrush(bg);
painter.drawRoundedRect(cardRect, 6, 6);
painter.setBrush(accentColor);
painter.drawRoundedRect(QRectF(cardRect.left(), cardRect.top(), 3, cardRect.height()), 2, 2);
if (sourcePixmap.isNull()) {
painter.setPen(QColor(150, 150, 150));
painter.drawText(rect, Qt::AlignCenter, tr("No card selected"));
return;
}
UserListPainter::drawCardArt(&painter, rect, rect.right() - 4,
QString(), // userName not needed for override path
nullptr, // no cache
params,
&sourcePixmap // direct pixmap
);
// Avatar placeholder so the left-margin interaction is visible
const int avatarX = rect.left() + 14;
const int avatarY = rect.top() + (rect.height() - 36) / 2;
const QRect avatarRect(avatarX, avatarY, 36, 36);
QPainterPath clip;
clip.addEllipse(avatarRect);
painter.save();
painter.setClipPath(clip);
painter.setBrush(accentColor.darker(200));
painter.setPen(Qt::NoPen);
painter.drawEllipse(avatarRect);
painter.restore();
painter.setPen(QPen(QColor(70, 80, 95), 2));
painter.setBrush(Qt::NoBrush);
painter.drawEllipse(avatarRect.adjusted(-1, -1, 1, 1));
}
UserCardArtSettingsDialog::UserCardArtSettingsDialog(const CardArtParams &initial, QWidget *parent)
: QDialog(parent), currentParams(initial)
{
setWindowTitle(tr("Card Art Settings"));
setMinimumWidth(500);
setupUi();
// Seed UI from initial params
if (!initial.cardName.isEmpty()) {
searchBar->setText(initial.cardName);
onCardNameChanged(initial.cardName);
}
marginLSpin->setValue(initial.marginPctL);
marginRSpin->setValue(initial.marginPctR);
verticalOffsetSpin->setValue(initial.verticalOffset);
zoomSpin->setValue(initial.zoom);
}
CardArtParams UserCardArtSettingsDialog::params() const
{
return currentParams;
}
QDoubleSpinBox *UserCardArtSettingsDialog::makeSpinBox(double min, double max, double value, double step)
{
auto *spin = new QDoubleSpinBox;
spin->setRange(min, max);
spin->setSingleStep(step);
spin->setDecimals(3);
spin->setValue(value);
return spin;
}
void UserCardArtSettingsDialog::initializeSearchBar()
{
searchBar = new QLineEdit;
searchBar->setPlaceholderText(tr("Type a card name..."));
cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this);
cardDatabaseDisplayModel = new CardDatabaseDisplayModel(this);
cardDatabaseDisplayModel->setSourceModel(cardDatabaseModel);
searchModel = new CardSearchModel(cardDatabaseDisplayModel, this);
proxyModel = new CardCompleterProxyModel(this);
proxyModel->setSourceModel(searchModel);
proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
proxyModel->setFilterRole(Qt::DisplayRole);
completer = new QCompleter(proxyModel, this);
completer->setCompletionRole(Qt::DisplayRole);
completer->setCompletionMode(QCompleter::PopupCompletion);
completer->setCaseSensitivity(Qt::CaseInsensitive);
completer->setFilterMode(Qt::MatchContains);
completer->setMaxVisibleItems(15);
searchBar->setCompleter(completer);
connect(searchBar, &QLineEdit::textEdited, searchModel, &CardSearchModel::updateSearchResults);
connect(searchBar, &QLineEdit::textEdited, this, [this](const QString &text) {
const QString pattern = ".*" + QRegularExpression::escape(text) + ".*";
proxyModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption));
if (!text.isEmpty()) {
completer->complete();
}
});
connect(completer, static_cast<void (QCompleter::*)(const QString &)>(&QCompleter::activated), this,
[this](const QString &completion) {
if (searchBar->text() != completion) {
searchBar->setText(completion);
searchBar->setCursorPosition(searchBar->text().length());
}
onCardNameChanged(completion);
});
// Also trigger a load when the user hits Return on a typed name
connect(searchBar, &QLineEdit::returnPressed, this, [this]() { onCardNameChanged(searchBar->text()); });
}
void UserCardArtSettingsDialog::setupUi()
{
initializeSearchBar();
providerComboBox = new QComboBox;
connect(providerComboBox, &QComboBox::currentIndexChanged, this, [this]() {
currentParams.cardProviderId = providerComboBox->currentData().toString();
reloadPreview();
onParamChanged();
});
marginLSpin = makeSpinBox(0.0, 0.95, currentParams.marginPctL, 0.01);
marginRSpin = makeSpinBox(0.0, 0.95, currentParams.marginPctR, 0.01);
verticalOffsetSpin = makeSpinBox(0.0, 1.0, currentParams.verticalOffset, 0.01);
zoomSpin = makeSpinBox(0.1, 4.0, currentParams.zoom, 0.05);
auto *form = new QFormLayout;
form->addRow(tr("Card name:"), searchBar);
form->addRow(tr("Card ProviderId:"), providerComboBox);
form->addRow(tr("Left margin (%):"), marginLSpin);
form->addRow(tr("Right margin (%):"), marginRSpin);
form->addRow(tr("Vertical offset:"), verticalOffsetSpin);
form->addRow(tr("Zoom:"), zoomSpin);
auto *controlsGroup = new QGroupBox(tr("Parameters"));
controlsGroup->setLayout(form);
preview = new CardArtPreviewWidget;
auto *previewLayout = new QVBoxLayout;
previewLayout->addWidget(preview);
auto *previewGroup = new QGroupBox(tr("Preview"));
previewGroup->setLayout(previewLayout);
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
auto *removeBtn = new QPushButton(tr("Remove Banner Card"));
buttons->addButton(removeBtn, QDialogButtonBox::ResetRole);
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
connect(removeBtn, &QPushButton::clicked, this, [this]() {
currentParams = CardArtParams{}; // empty cardName signals removal
accept();
});
auto *root = new QVBoxLayout;
root->addWidget(controlsGroup);
root->addWidget(previewGroup);
root->addWidget(buttons);
setLayout(root);
connect(marginLSpin, &QDoubleSpinBox::valueChanged, this, &UserCardArtSettingsDialog::onParamChanged);
connect(marginRSpin, &QDoubleSpinBox::valueChanged, this, &UserCardArtSettingsDialog::onParamChanged);
connect(verticalOffsetSpin, &QDoubleSpinBox::valueChanged, this, &UserCardArtSettingsDialog::onParamChanged);
connect(zoomSpin, &QDoubleSpinBox::valueChanged, this, &UserCardArtSettingsDialog::onParamChanged);
}
void UserCardArtSettingsDialog::populateProviderCombo(const QString &cardName)
{
providerComboBox->clear();
auto card = CardDatabaseManager::query()->getCard({cardName});
const auto &sets = card.getInfo().getSets();
for (const auto &printings : sets) {
for (const auto &p : printings) {
QString setName = p.getSet()->getLongName();
QString collector = p.getProperty("num");
QString uuid = p.getUuid();
QString label = setName;
if (!collector.isEmpty()) {
label += " #" + collector;
}
providerComboBox->addItem(label, uuid);
}
}
}
void UserCardArtSettingsDialog::onCardNameChanged(const QString &name)
{
if (name.isEmpty()) {
currentPixmap = QPixmap();
preview->setPixmap(currentPixmap);
return;
}
const ExactCard card = CardDatabaseManager::query()->getCard({name});
if (!card) {
currentPixmap = QPixmap();
preview->setPixmap(currentPixmap);
providerComboBox->clear();
return;
}
currentParams.cardName = name;
populateProviderCombo(name);
if (providerComboBox->count() == 0) {
// No printings found for this card; nothing to preview.
currentPixmap = QPixmap();
preview->setPixmap(currentPixmap);
currentParams.cardProviderId.clear();
return;
}
currentParams.cardProviderId = providerComboBox->currentData().toString();
reloadPreview();
}
void UserCardArtSettingsDialog::reloadPreview()
{
if (currentParams.cardName.isEmpty()) {
return;
}
ExactCard card = CardDatabaseManager::query()->getCard({currentParams.cardName, currentParams.cardProviderId});
if (!card) {
return;
}
// CardPictureLoader::getPixmap() is async on a cache miss: it enqueues a
// background download and returns a null pixmap immediately. When that
// download finishes, CardPictureLoader::imageLoaded() caches the result
// and calls card.emitPixmapUpdated(), which emits pixmapUpdated() on the
// underlying CardInfo (see exact_card.h). Listen for that, scoped to
// whichever CardInfo we just asked for, so the preview catches up once
// the image actually arrives instead of staying on the placeholder.
//
// Disconnect any previous listener first -- otherwise switching cards
// repeatedly stacks up connections to old CardInfo objects, each of
// which would still fire reloadPreview() (harmlessly, but wastefully)
// whenever ITS art finishes loading later.
disconnect(pixmapUpdatedConnection);
QPixmap fullRes;
CardPictureLoader::getPixmap(fullRes, card, QSize(745, 1040));
if (fullRes.isNull()) {
// Not loaded yet -- wait for the signal instead of giving up.
// card.getCardPtr() is a CardInfoPtr (QSharedPointer<CardInfo>);
// .data() gives the raw QObject* needed for connect().
CardInfo *cardInfo = card.getCardPtr().data();
if (cardInfo) {
pixmapUpdatedConnection = connect(cardInfo, &CardInfo::pixmapUpdated, this, [this]() { reloadPreview(); });
}
return;
}
currentPixmap = UserCardArtProvider::cropCardArt(fullRes);
preview->setPixmap(currentPixmap);
preview->setParams(currentParams);
}
void UserCardArtSettingsDialog::onParamChanged()
{
currentParams.marginPctL = marginLSpin->value();
currentParams.marginPctR = marginRSpin->value();
currentParams.verticalOffset = verticalOffsetSpin->value();
currentParams.zoom = zoomSpin->value();
preview->setParams(currentParams);
}

View file

@ -0,0 +1,77 @@
#ifndef COCKATRICE_USER_CARD_ART_SETTINGS_DIALOG_H
#define COCKATRICE_USER_CARD_ART_SETTINGS_DIALOG_H
#include "user_list_painter.h"
#include <QComboBox>
#include <QDialog>
#include <QPixmap>
class QCompleter;
class QLineEdit;
class QDoubleSpinBox;
class CardDatabaseModel;
class CardDatabaseDisplayModel;
class CardSearchModel;
class CardCompleterProxyModel;
class CardArtPreviewWidget : public QWidget
{
Q_OBJECT
public:
explicit CardArtPreviewWidget(QWidget *parent = nullptr);
void setPixmap(const QPixmap &pixmap);
void setParams(const CardArtParams &params);
protected:
void paintEvent(QPaintEvent *event) override;
private:
QPixmap sourcePixmap;
CardArtParams params;
};
class UserCardArtSettingsDialog : public QDialog
{
Q_OBJECT
public:
explicit UserCardArtSettingsDialog(const CardArtParams &initial = {}, QWidget *parent = nullptr);
CardArtParams params() const;
private slots:
void onCardNameChanged(const QString &name);
void reloadPreview();
void onParamChanged();
private:
void setupUi();
void populateProviderCombo(const QString &cardName);
void initializeSearchBar();
QDoubleSpinBox *makeSpinBox(double min, double max, double value, double step);
QLineEdit *searchBar;
QCompleter *completer;
CardDatabaseModel *cardDatabaseModel;
CardDatabaseDisplayModel *cardDatabaseDisplayModel;
CardSearchModel *searchModel;
CardCompleterProxyModel *proxyModel;
QComboBox *providerComboBox;
QMetaObject::Connection pixmapUpdatedConnection;
QDoubleSpinBox *marginLSpin;
QDoubleSpinBox *marginRSpin;
QDoubleSpinBox *verticalOffsetSpin;
QDoubleSpinBox *zoomSpin;
CardArtPreviewWidget *preview;
QPixmap currentPixmap;
CardArtParams currentParams;
};
#endif // COCKATRICE_USER_CARD_ART_SETTINGS_DIALOG_H

View file

@ -476,10 +476,15 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
client->sendCommand(client->prepareSessionCommand(cmd));
} else if (actionClicked == aKick) {
Command_KickFromGame cmd;
cmd.set_player_id(playerId);
auto result = QMessageBox::question(static_cast<QWidget *>(parent()), tr("Kick Player"),
tr("Are you sure you want to kick this player from the game?"),
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
if (result == QMessageBox::Yes) {
Command_KickFromGame cmd;
cmd.set_player_id(playerId);
game->getGameEventHandler()->sendGameCommand(cmd);
game->getGameEventHandler()->sendGameCommand(cmd);
}
} else if (actionClicked == aBan) {
Command_GetUserInfo cmd;
cmd.set_user_name(userName.toStdString());
@ -537,3 +542,113 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
delete menu;
}
void UserContextMenu::execChat(const QString &userName)
{
emit openMessageDialog(userName, true);
}
void UserContextMenu::execDetails(const QString &userName)
{
auto *w = new UserInfoBox(client, false, static_cast<QWidget *>(parent()),
Qt::Dialog | Qt::WindowTitleHint | Qt::CustomizeWindowHint | Qt::WindowCloseButtonHint);
w->setAttribute(Qt::WA_DeleteOnClose);
w->updateInfo(userName);
}
void UserContextMenu::execShowGames(const QString &userName)
{
Command_GetGamesOfUser cmd;
cmd.set_user_name(userName.toStdString());
PendingCommand *pend = client->prepareSessionCommand(cmd);
connect(pend, &PendingCommand::finished, this, &UserContextMenu::gamesOfUserReceived);
client->sendCommand(pend);
}
void UserContextMenu::execAddToBuddy(const QString &userName)
{
Command_AddToList cmd;
cmd.set_list("buddy");
cmd.set_user_name(userName.toStdString());
client->sendCommand(client->prepareSessionCommand(cmd));
}
void UserContextMenu::execRemoveFromBuddy(const QString &userName)
{
Command_RemoveFromList cmd;
cmd.set_list("buddy");
cmd.set_user_name(userName.toStdString());
client->sendCommand(client->prepareSessionCommand(cmd));
}
void UserContextMenu::execAddToIgnore(const QString &userName)
{
Command_AddToList cmd;
cmd.set_list("ignore");
cmd.set_user_name(userName.toStdString());
client->sendCommand(client->prepareSessionCommand(cmd));
}
void UserContextMenu::execRemoveFromIgnore(const QString &userName)
{
Command_RemoveFromList cmd;
cmd.set_list("ignore");
cmd.set_user_name(userName.toStdString());
client->sendCommand(client->prepareSessionCommand(cmd));
}
void UserContextMenu::execBan(const QString &userName)
{
Command_GetUserInfo cmd;
cmd.set_user_name(userName.toStdString());
PendingCommand *pend = client->prepareSessionCommand(cmd);
connect(pend, &PendingCommand::finished, this, &UserContextMenu::banUser_processUserInfoResponse);
client->sendCommand(pend);
}
void UserContextMenu::execWarn(const QString &userName)
{
Command_GetUserInfo cmd;
cmd.set_user_name(userName.toStdString());
PendingCommand *pend = client->prepareSessionCommand(cmd);
connect(pend, &PendingCommand::finished, this, &UserContextMenu::warnUser_processUserInfoResponse);
client->sendCommand(pend);
}
void UserContextMenu::execBanHistory(const QString &userName)
{
Command_GetBanHistory cmd;
cmd.set_user_name(userName.toStdString());
PendingCommand *pend = client->prepareModeratorCommand(cmd);
connect(pend, &PendingCommand::finished, this, &UserContextMenu::banUserHistory_processResponse);
client->sendCommand(pend);
}
void UserContextMenu::execWarnHistory(const QString &userName)
{
Command_GetWarnHistory cmd;
cmd.set_user_name(userName.toStdString());
PendingCommand *pend = client->prepareModeratorCommand(cmd);
connect(pend, &PendingCommand::finished, this, &UserContextMenu::warnUserHistory_processResponse);
client->sendCommand(pend);
}
void UserContextMenu::execAdminNotes(const QString &userName)
{
Command_GetAdminNotes cmd;
cmd.set_user_name(userName.toStdString());
auto *pend = client->prepareModeratorCommand(cmd);
connect(pend, &PendingCommand::finished, this, &UserContextMenu::getAdminNotes_processResponse);
client->sendCommand(pend);
}
void UserContextMenu::execAdjustMod(const QString &userName, bool shouldBeMod, bool shouldBeJudge)
{
Command_AdjustMod cmd;
cmd.set_user_name(userName.toStdString());
cmd.set_should_be_mod(shouldBeMod);
cmd.set_should_be_judge(shouldBeJudge);
PendingCommand *pend = client->prepareAdminCommand(cmd);
connect(pend, &PendingCommand::finished, this, &UserContextMenu::adjustMod_processUserResponse);
client->sendCommand(pend);
}

View file

@ -74,6 +74,27 @@ public:
int playerId,
const QString &deckHash,
ChatView *chatView = nullptr);
const UserListProxy *getUserListProxy() const
{
return userListProxy;
}
// Individual action entry points — used by UserInfoPopup to trigger
// actions without re-running the full context menu flow.
void execChat(const QString &userName);
void execDetails(const QString &userName);
void execShowGames(const QString &userName);
void execAddToBuddy(const QString &userName);
void execRemoveFromBuddy(const QString &userName);
void execAddToIgnore(const QString &userName);
void execRemoveFromIgnore(const QString &userName);
void execBan(const QString &userName);
void execWarn(const QString &userName);
void execBanHistory(const QString &userName);
void execWarnHistory(const QString &userName);
void execAdminNotes(const QString &userName);
void execAdjustMod(const QString &userName, bool shouldBeMod, bool shouldBeJudge);
};
#endif

View file

@ -5,6 +5,7 @@
#include "../../interface/widgets/dialogs/dlg_edit_password.h"
#include "../../interface/widgets/dialogs/dlg_edit_user.h"
#include "../../interface/widgets/utility/get_text_with_max.h"
#include "user_card_settings_dialog.h"
#include <QDateTime>
#include <QGridLayout>
@ -61,11 +62,13 @@ UserInfoBox::UserInfoBox(AbstractClient *_client, bool _editable, QWidget *paren
buttonsLayout->addWidget(&editButton);
buttonsLayout->addWidget(&passwordButton);
buttonsLayout->addWidget(&avatarButton);
buttonsLayout->addWidget(&bannerCardButton);
mainLayout->addLayout(buttonsLayout, 7, 0, 1, 3);
connect(&editButton, &QPushButton::clicked, this, &UserInfoBox::actEdit);
connect(&passwordButton, &QPushButton::clicked, this, &UserInfoBox::actPassword);
connect(&avatarButton, &QPushButton::clicked, this, &UserInfoBox::actAvatar);
connect(&bannerCardButton, &QPushButton::clicked, this, &UserInfoBox::actBannerCard);
}
setWindowTitle(tr("User Information"));
@ -83,26 +86,21 @@ void UserInfoBox::retranslateUi()
editButton.setText(tr("Edit"));
passwordButton.setText(tr("Change password"));
avatarButton.setText(tr("Change avatar"));
}
/**
* Creates the default profile pic that is used when the user doesn't have a custom pic
*/
static QPixmap createDefaultAvatar(int height, const ServerInfo_User &user)
{
return UserLevelPixmapGenerator::generatePixmap(height, UserLevelFlags(user.user_level()), user.pawn_colors(),
false, QString::fromStdString(user.privlevel()));
bannerCardButton.setText(tr("Edit Banner Card"));
}
void UserInfoBox::updateInfo(const ServerInfo_User &user)
{
currentUserInfo = &user;
currentUserInfo = user;
hasUserInfo = true;
const UserLevelFlags userLevel(user.user_level());
pawnColors = user.pawn_colors();
privLevel = QString::fromStdString(user.privlevel());
const std::string &bmp = user.avatar_bmp();
if (!avatarPixmap.loadFromData((const uchar *)bmp.data(), static_cast<uint>(bmp.size()))) {
avatarPixmap = createDefaultAvatar(64, user);
avatarPixmap = UserLevelPixmapGenerator::generatePixmap(64, userLevel, pawnColors, false, privLevel);
hasAvatar = false;
} else {
hasAvatar = true;
@ -120,8 +118,7 @@ void UserInfoBox::updateInfo(const ServerInfo_User &user)
countryLabel3.setText("");
}
userLevelIcon.setPixmap(UserLevelPixmapGenerator::generatePixmap(15, userLevel, user.pawn_colors(), false,
QString::fromStdString(user.privlevel())));
userLevelIcon.setPixmap(UserLevelPixmapGenerator::generatePixmap(15, userLevel, pawnColors, false, privLevel));
QString userLevelText;
if (userLevel.testFlag(ServerInfo_User::IsAdmin)) {
userLevelText = tr("Administrator");
@ -316,6 +313,49 @@ void UserInfoBox::actAvatar()
client->sendCommand(pend);
}
void UserInfoBox::actBannerCard()
{
CardArtParams initial;
if (hasUserInfo && currentUserInfo.has_card_art_params()) {
const auto &cap = currentUserInfo.card_art_params();
initial.cardName = QString::fromStdString(cap.card_name());
initial.marginPctL = cap.margin_pct_l();
initial.marginPctR = cap.margin_pct_r();
initial.verticalOffset = cap.vertical_offset();
initial.zoom = cap.zoom();
}
UserCardArtSettingsDialog dlg(initial, this);
if (dlg.exec() != QDialog::Accepted) {
return;
}
const CardArtParams p = dlg.params();
Command_SetCardArtParams cmd;
cmd.set_card_name(p.cardName.toStdString());
if (!p.cardName.isEmpty()) {
cmd.set_card_provider_id(p.cardProviderId.toStdString());
cmd.set_margin_pct_l(p.marginPctL);
cmd.set_margin_pct_r(p.marginPctR);
cmd.set_vertical_offset(p.verticalOffset);
cmd.set_zoom(p.zoom);
}
PendingCommand *pend = client->prepareSessionCommand(cmd);
connect(pend, &PendingCommand::finished, this, [p, this](const Response &r) {
if (r.response_code() != Response::RespOk) {
QMessageBox::critical(this, tr("Error"),
tr("The selected card is blacklisted on this server or another error occurred."));
} else {
updateInfo(nameLabel.text()); // re-fetch so currentUserInfo reflects the change
QMessageBox::information(this, tr("Information"),
p.cardName.isEmpty() ? tr("Banner card removed.") : tr("Banner card updated."));
}
});
client->sendCommand(pend);
}
void UserInfoBox::processEditResponse(const Response &r)
{
switch (r.response_code()) {
@ -373,7 +413,7 @@ void UserInfoBox::processAvatarResponse(const Response &r)
break;
case Response::RespInternalError:
default:
QMessageBox::critical(this, tr("Error"), tr("An error occured while trying to updater your avatar."));
QMessageBox::critical(this, tr("Error"), tr("An error occured while trying to update your avatar."));
break;
}
}
@ -385,7 +425,7 @@ void UserInfoBox::resizeEvent(QResizeEvent *event)
resizedPixmap = avatarPixmap.scaled(avatarPic.size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
} else {
int height = qMin(avatarPic.size().width(), avatarPic.size().height());
resizedPixmap = createDefaultAvatar(height, *currentUserInfo);
resizedPixmap = UserLevelPixmapGenerator::generatePixmap(height, userLevel, pawnColors, false, privLevel);
}
avatarPic.setPixmap(resizedPixmap);

View file

@ -11,8 +11,10 @@
#include <QLabel>
#include <QPushButton>
#include <QWidget>
#include <libcockatrice/network/server/remote/user_level.h>
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
#include <libcockatrice/utility/days_years_between.h>
class ServerInfo_User;
class AbstractClient;
class Response;
@ -24,10 +26,14 @@ private:
bool editable;
QLabel avatarPic, userLevelIcon, nameLabel, realNameLabel1, realNameLabel2, countryLabel1, countryLabel2,
countryLabel3, userLevelLabel1, userLevelLabel2, accountAgeLabel1, accountAgeLabel2;
QPushButton editButton, passwordButton, avatarButton;
QPushButton editButton, passwordButton, avatarButton, bannerCardButton;
QPixmap avatarPixmap;
bool hasAvatar;
const ServerInfo_User *currentUserInfo;
ServerInfo_User currentUserInfo;
bool hasUserInfo = false;
UserLevelFlags userLevel;
ServerInfo_User::PawnColorsOverride pawnColors;
QString privLevel;
static QString getAgeString(int ageSeconds);
@ -35,12 +41,6 @@ public:
UserInfoBox(AbstractClient *_client, bool editable, QWidget *parent = nullptr, Qt::WindowFlags flags = {});
void retranslateUi();
inline static QPair<int, int> getDaysAndYearsBetween(const QDate &then, const QDate &now)
{
int years = now.addDays(1 - then.dayOfYear()).year() - then.year(); // there is no yearsTo
int days = then.addYears(years).daysTo(now);
return {days, years};
}
private slots:
void processResponse(const Response &r);
void processEditResponse(const Response &r);
@ -51,6 +51,7 @@ private slots:
void actEditInternal(const Response &r);
void actPassword();
void actAvatar();
void actBannerCard();
public slots:
void updateInfo(const ServerInfo_User &user);
void updateInfo(const QString &userName);

View file

@ -0,0 +1,656 @@
#include "user_info_popup.h"
#include "../../interface/pixel_map_generator.h"
#include "../../interface/widgets/tabs/tab_supervisor.h"
#include "user_list_painter.h"
#include <QApplication>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QPainter>
#include <QPainterPath>
#include <QPropertyAnimation>
#include <QPushButton>
#include <QScreen>
#include <QScrollBar>
#include <QStandardItem>
#include <QStyledItemDelegate>
#include <QVBoxLayout>
#include <libcockatrice/network/client/abstract/abstract_client.h>
#include <libcockatrice/protocol/pb/commands.pb.h>
#include <libcockatrice/protocol/pb/response_get_games_of_user.pb.h>
#include <libcockatrice/protocol/pending_command.h>
// ── Compact game row delegate ─────────────────────────────────────────────────
class PopupGameDelegate : public QStyledItemDelegate
{
public:
using QStyledItemDelegate::QStyledItemDelegate;
QSize sizeHint(const QStyleOptionViewItem &, const QModelIndex &) const override
{
return QSize(0, 38);
}
void paint(QPainter *p, const QStyleOptionViewItem &option, const QModelIndex &index) const override
{
const QVariant var = index.data(PopupRoles::GameData);
if (!var.isValid()) {
QStyledItemDelegate::paint(p, option, index);
return;
}
p->save();
p->setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing);
const QRect rect = option.rect;
const ServerInfo_Game game = var.value<ServerInfo_Game>();
const bool selected = option.state & QStyle::State_Selected;
p->fillRect(rect, selected ? QColor(35, 45, 62) : QColor(14, 18, 26));
// State colour dot
const QColor dot = game.started() ? QColor(239, 68, 68)
: (game.player_count() >= game.max_players()) ? QColor(249, 115, 22)
: game.with_password() ? QColor(59, 130, 246)
: QColor(34, 197, 94);
p->setPen(Qt::NoPen);
p->setBrush(dot);
p->drawEllipse(QRectF(rect.left() + 9, rect.top() + (rect.height() - 8) / 2.0, 8, 8));
// Game title (bold, elided)
QFont tf = option.font;
tf.setBold(true);
p->setFont(tf);
p->setPen(QColor(205, 215, 230));
const int textX = rect.left() + 26;
const int countW = 52;
const int titleW = rect.width() - textX - countW - 6;
p->drawText(QRect(textX, rect.top(), titleW, rect.height()), Qt::AlignVCenter | Qt::AlignLeft,
QFontMetrics(tf).elidedText(QString::fromStdString(game.description()), Qt::ElideRight, titleW));
// Player count
const bool full = game.player_count() >= game.max_players();
p->setFont(option.font);
p->setPen(full ? QColor(249, 115, 22) : QColor(110, 128, 150));
p->drawText(QRect(rect.right() - countW - 4, rect.top(), countW, rect.height()),
Qt::AlignVCenter | Qt::AlignRight,
QStringLiteral("%1/%2").arg(game.player_count()).arg(game.max_players()));
// Row separator
p->setPen(QColor(24, 32, 44));
p->drawLine(rect.bottomLeft(), rect.bottomRight());
p->restore();
}
};
// ── UserInfoHeaderWidget ──────────────────────────────────────────────────────
UserInfoHeaderWidget::UserInfoHeaderWidget(QWidget *parent) : QWidget(parent)
{
setFixedHeight(HeaderHeight);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
}
void UserInfoHeaderWidget::setUserData(const ServerInfo_User &user,
bool online,
const QPixmap &avatar,
const QPixmap &cardArt,
const CardArtParams &params)
{
m_user = user;
m_online = online;
m_avatar = avatar;
m_cardArt = cardArt;
m_params = params;
update();
}
void UserInfoHeaderWidget::paintEvent(QPaintEvent *)
{
QPainter p(this);
p.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::TextAntialiasing);
const QRect rect = this->rect();
const UserLevelFlags level(m_user.user_level());
const QString userName = QString::fromStdString(m_user.name());
const QString privLevel = QString::fromStdString(m_user.privlevel());
// Dark base
p.fillRect(rect, QColor(14, 18, 26));
// ── Card art background ───────────────────────────────────────────────────
if (!m_cardArt.isNull()) {
const int w = rect.width();
const int h = rect.height();
const int mL = qRound(w * m_params.marginPctL);
const int mR = qRound(w * m_params.marginPctR);
const int dW = w - mL - mR;
const double base = qMax(double(dW) / m_cardArt.width(), double(h) / m_cardArt.height());
const double scale = base * m_params.zoom;
const int sW = qRound(m_cardArt.width() * scale);
const int sH = qRound(m_cardArt.height() * scale);
const QPixmap scaled = m_cardArt.scaled(sW, sH, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
const int srcX = (sW - dW) / 2;
const int srcY = qBound(0, qRound((sH - h) * m_params.verticalOffset), qMax(0, sH - h));
QImage img = scaled.copy(srcX, srcY, dW, h).toImage().convertToFormat(QImage::Format_ARGB32_Premultiplied);
{
QPainter mask(&img);
mask.setCompositionMode(QPainter::CompositionMode_DestinationIn);
QLinearGradient g(0, 0, img.width(), 0);
g.setColorAt(0.00, Qt::transparent);
g.setColorAt(0.18, Qt::white);
g.setColorAt(0.82, Qt::white);
g.setColorAt(1.00, Qt::transparent);
mask.fillRect(img.rect(), g);
}
p.setOpacity(0.48);
p.drawImage(mL, 0, img);
p.setOpacity(1.0);
}
// Bottom gradient overlay so avatar and text are always legible
{
QLinearGradient ov(0, 0, 0, rect.height());
ov.setColorAt(0.0, QColor(14, 18, 26, 0));
ov.setColorAt(0.55, QColor(14, 18, 26, 110));
ov.setColorAt(1.0, QColor(14, 18, 26, 230));
p.fillRect(rect, ov);
}
// ── Avatar ────────────────────────────────────────────────────────────────
const QColor accent = [&]() -> QColor {
if (level.testFlag(ServerInfo_User::IsAdmin)) {
return QColor(245, 158, 11);
}
if (level.testFlag(ServerInfo_User::IsModerator)) {
return QColor(59, 130, 246);
}
if (level.testFlag(ServerInfo_User::IsJudge)) {
return QColor(168, 85, 247);
}
return QColor(100, 116, 139);
}();
const int ax = LeftPad;
const int ay = rect.height() - AvatarSize - 10;
const QRect ar(ax, ay, AvatarSize, AvatarSize);
QPainterPath clip;
clip.addEllipse(ar);
p.save();
p.setClipPath(clip);
if (!m_avatar.isNull()) {
p.drawPixmap(ar, m_avatar.scaled(ar.size(), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation));
} else {
p.setPen(Qt::NoPen);
p.setBrush(accent.darker(200));
p.drawEllipse(ar);
const QPixmap pawn =
UserLevelPixmapGenerator::generatePixmap(AvatarPawnSize, level, m_user.pawn_colors(), false, privLevel);
p.drawPixmap(ar.center().x() - AvatarPawnSize / 2, ar.center().y() - AvatarPawnSize / 2, pawn);
}
p.restore();
// Status ring
p.setPen(QPen(m_online ? QColor(34, 197, 94) : QColor(70, 80, 95), 2.5));
p.setBrush(Qt::NoBrush);
p.drawEllipse(QRectF(ar).adjusted(-1.25, -1.25, 1.25, 1.25));
// ── Username + badge ──────────────────────────────────────────────────────
const int tx = ax + AvatarSize + AvatarToTextGap;
const int tw = rect.width() - tx - 8;
QFont nf = font();
nf.setBold(true);
nf.setPointSizeF(nf.pointSizeF() * 1.12);
p.setFont(nf);
p.setPen(m_online ? QColor(220, 228, 240) : QColor(90, 100, 115));
p.drawText(QRect(tx, ay, tw, AvatarSize / 2 + 4), Qt::AlignBottom | Qt::AlignLeft,
QFontMetrics(nf).elidedText(userName, Qt::ElideRight, tw));
// Level / priv badge
struct
{
QString text;
QColor color;
} badge;
if (level.testFlag(ServerInfo_User::IsAdmin)) {
badge = {"ADMIN", QColor(245, 158, 11)};
} else if (level.testFlag(ServerInfo_User::IsModerator)) {
badge = {"MOD", QColor(59, 130, 246)};
} else if (level.testFlag(ServerInfo_User::IsJudge)) {
badge = {"JUDGE", QColor(168, 85, 247)};
} else if (privLevel == "VIP") {
badge = {"VIP", QColor(20, 184, 166)};
} else if (privLevel == "DONATOR") {
badge = {"DONATOR", QColor(249, 115, 22)};
}
if (!badge.text.isEmpty()) {
QFont bf = font();
bf.setPointSizeF(bf.pointSizeF() * 0.70);
bf.setBold(true);
p.setFont(bf);
const QFontMetrics bfm(bf);
const int bw = bfm.horizontalAdvance(badge.text) + 10;
const QRect br(tx, ay + AvatarSize / 2 + 6, bw, 15);
p.setPen(Qt::NoPen);
p.setBrush(badge.color.darker(160));
p.drawRoundedRect(br, 3, 3);
p.setPen(badge.color.lighter(150));
p.drawText(br, Qt::AlignCenter, badge.text);
}
}
// ── UserInfoPopup ─────────────────────────────────────────────────────────────
UserInfoPopup::UserInfoPopup(TabSupervisor *ts,
AbstractClient *client,
const QMap<QString, QPixmap> *avatarCache,
const QMap<QString, QPixmap> *cardArtCache,
const QMap<QString, CardArtParams> *cardArtParamsMap,
QWidget *parent)
: QFrame(parent, Qt::Tool | Qt::FramelessWindowHint), m_ts(ts), m_client(client), m_avatarCache(avatarCache),
m_cardArtCache(cardArtCache), m_cardArtParamsMap(cardArtParamsMap)
{
setAttribute(Qt::WA_ShowWithoutActivating);
setFixedWidth(PopupWidth);
setFrameShape(QFrame::NoFrame);
buildUi();
}
void UserInfoPopup::buildUi()
{
setStyleSheet(QStringLiteral("UserInfoPopup {"
" background:#0e1218;"
" border:1px solid #1e2838;"
" border-radius:8px;"
"}"));
auto *root = new QVBoxLayout(this);
root->setContentsMargins(0, 0, 0, 0);
root->setSpacing(0);
// Header
m_header = new UserInfoHeaderWidget(this);
root->addWidget(m_header);
// Action area — rebuilt per user
m_actionArea = new QWidget(this);
m_actionArea->setStyleSheet(QStringLiteral("background:#0e1218;"));
root->addWidget(m_actionArea);
// Thin separator
auto *sep = new QFrame(this);
sep->setFrameShape(QFrame::HLine);
sep->setStyleSheet(QStringLiteral("color:#1a2434; margin: 0 8px;"));
root->addWidget(sep);
// Games header row
auto *gh = new QHBoxLayout;
gh->setContentsMargins(10, 4, 8, 2);
auto *gl = new QLabel(tr("Games"), this);
gl->setStyleSheet(QStringLiteral("color:#6882a0; font-size:11px; font-weight:bold; background:transparent;"));
gh->addWidget(gl);
gh->addStretch();
m_refreshBtn = new QPushButton(QStringLiteral(""), this);
m_refreshBtn->setFixedSize(20, 20);
m_refreshBtn->setFlat(true);
m_refreshBtn->setStyleSheet(
QStringLiteral("QPushButton{color:#6882a0;border:none;font-size:14px;background:transparent;}"
"QPushButton:hover{color:white;}"));
connect(m_refreshBtn, &QPushButton::clicked, this, &UserInfoPopup::refreshGames);
gh->addWidget(m_refreshBtn);
root->addLayout(gh);
// Status label
m_gamesStatus = new QLabel(this);
m_gamesStatus->setAlignment(Qt::AlignCenter);
m_gamesStatus->setStyleSheet(
QStringLiteral("color:#3a4a5e; font-size:11px; padding:10px; background:transparent;"));
root->addWidget(m_gamesStatus);
// Games list
m_gamesModel = new QStandardItemModel(this);
m_gamesView = new QListView(this);
m_gamesView->setModel(m_gamesModel);
m_gamesView->setItemDelegate(new PopupGameDelegate(m_gamesView));
m_gamesView->setFrameShape(QFrame::NoFrame);
m_gamesView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_gamesView->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
m_gamesView->setMaximumHeight(220);
m_gamesView->setStyleSheet(QStringLiteral("QListView{background:#0e1218;border:none;}"
"QListView::item:selected{background:#232e42;}"));
m_gamesView->setContextMenuPolicy(Qt::CustomContextMenu);
connect(m_gamesView, &QListView::customContextMenuRequested, this, &UserInfoPopup::onGamesContextMenu);
root->addWidget(m_gamesView);
// Close button — positioned absolutely in the top-right corner
m_closeBtn = new QPushButton(QStringLiteral(""), this);
m_closeBtn->setFixedSize(22, 22);
m_closeBtn->setFlat(true);
m_closeBtn->setStyleSheet(QStringLiteral("QPushButton{background:rgba(14,18,26,180);color:#607080;"
"border:none;border-radius:11px;font-size:10px;}"
"QPushButton:hover{color:white;background:rgba(200,50,50,200);}"));
connect(m_closeBtn, &QPushButton::clicked, this, &UserInfoPopup::closeRequested);
}
// ── Action button factory ─────────────────────────────────────────────────────
static QPushButton *makeBtn(const QString &label, const QString &tip, QWidget *p)
{
auto *b = new QPushButton(label, p);
b->setToolTip(tip);
b->setFixedHeight(26);
b->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
b->setStyleSheet(QStringLiteral("QPushButton{"
" background:#192030;color:#b8c8de;border:1px solid #263040;"
" border-radius:4px;font-size:11px;padding:0 4px;"
"}"
"QPushButton:hover{background:#223050;color:white;}"
"QPushButton:pressed{background:#162030;}"
"QPushButton:disabled{color:#384858;border-color:#192030;}"));
return b;
}
void UserInfoPopup::rebuildActionButtons(const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored)
{
// Clear previous contents
delete m_actionArea->layout();
const auto old = m_actionArea->findChildren<QPushButton *>(QString{}, Qt::FindDirectChildrenOnly);
for (auto *w : old) {
w->deleteLater();
}
const QString name = QString::fromStdString(userInfo.name());
const auto ownLevel = UserLevelFlags(m_ts->getUserInfo()->user_level());
const bool isSelf = (name == QString::fromStdString(m_ts->getUserInfo()->name()));
const bool isMod = ownLevel.testFlag(ServerInfo_User::IsModerator);
const bool isAdmin = ownLevel.testFlag(ServerInfo_User::IsAdmin);
const auto their = UserLevelFlags(userInfo.user_level());
const bool isReg = their.testFlag(ServerInfo_User::IsRegistered);
auto *grid = new QGridLayout(m_actionArea);
grid->setContentsMargins(8, 6, 8, 6);
grid->setSpacing(4);
int row = 0, col = 0;
const int cols = 3;
auto add = [&](QPushButton *btn) {
grid->addWidget(btn, row, col);
if (++col == cols) {
col = 0;
++row;
}
};
// ── Always visible ────────────────────────────────────────────────────────
auto *chat = makeBtn(tr("Chat"), tr("Open private chat"), m_actionArea);
chat->setEnabled(!isSelf && online);
connect(chat, &QPushButton::clicked, this, [this, name] { emit chatRequested(name); });
add(chat);
auto *prof = makeBtn(tr("Profile"), tr("View user profile"), m_actionArea);
connect(prof, &QPushButton::clicked, this, [this, name] { emit detailsRequested(name); });
add(prof);
auto *games = makeBtn(tr("Games"), tr("Show this user's games"), m_actionArea);
games->setEnabled(!isSelf && online);
connect(games, &QPushButton::clicked, this, [this, name] { emit showGamesRequested(name); });
add(games);
// ── Buddy / ignore (registered users only) ────────────────────────────────
if (!isSelf && isReg) {
if (isBuddy) {
auto *b = makeBtn(tr(" Buddy"), tr("Remove from buddy list"), m_actionArea);
connect(b, &QPushButton::clicked, this, [this, name] { emit removeBuddyRequested(name); });
add(b);
} else {
auto *b = makeBtn(tr("+ Buddy"), tr("Add to buddy list"), m_actionArea);
connect(b, &QPushButton::clicked, this, [this, name] { emit addBuddyRequested(name); });
add(b);
}
if (isIgnored) {
auto *b = makeBtn(tr(" Ignore"), tr("Remove from ignore list"), m_actionArea);
connect(b, &QPushButton::clicked, this, [this, name] { emit removeIgnoreRequested(name); });
add(b);
} else {
auto *b = makeBtn(tr("+ Ignore"), tr("Add to ignore list"), m_actionArea);
connect(b, &QPushButton::clicked, this, [this, name] { emit addIgnoreRequested(name); });
add(b);
}
}
// ── Moderator actions ─────────────────────────────────────────────────────
if (!isSelf && (isMod || isAdmin)) {
if (col != 0) {
++row;
col = 0;
} // start mod section on a fresh row
auto *ban = makeBtn(tr("Ban"), tr("Ban from server"), m_actionArea);
auto *warn = makeBtn(tr("Warn"), tr("Warn user"), m_actionArea);
auto *bLog = makeBtn(tr("Ban log"), tr("View ban history"), m_actionArea);
auto *wLog = makeBtn(tr("Warn log"), tr("View warning history"), m_actionArea);
connect(ban, &QPushButton::clicked, this, [this, name] { emit banRequested(name); });
connect(warn, &QPushButton::clicked, this, [this, name] { emit warnRequested(name); });
connect(bLog, &QPushButton::clicked, this, [this, name] { emit banHistoryRequested(name); });
connect(wLog, &QPushButton::clicked, this, [this, name] { emit warnHistoryRequested(name); });
add(ban);
add(warn);
add(bLog);
add(wLog);
}
// ── Admin actions ─────────────────────────────────────────────────────────
if (!isSelf && isAdmin) {
auto *notes = makeBtn(tr("Notes"), tr("View admin notes"), m_actionArea);
connect(notes, &QPushButton::clicked, this, [this, name] { emit adminNotesRequested(name); });
add(notes);
if (their.testFlag(ServerInfo_User::IsModerator)) {
auto *b = makeBtn(tr(" Mod"), tr("Demote from moderator"), m_actionArea);
connect(b, &QPushButton::clicked, this, [this, name] { emit demoteFromModRequested(name); });
add(b);
} else if (isReg) {
auto *b = makeBtn(tr("+ Mod"), tr("Promote to moderator"), m_actionArea);
connect(b, &QPushButton::clicked, this, [this, name] { emit promoteToModRequested(name); });
add(b);
}
if (their.testFlag(ServerInfo_User::IsJudge)) {
auto *b = makeBtn(tr(" Judge"), tr("Demote from judge"), m_actionArea);
connect(b, &QPushButton::clicked, this, [this, name] { emit demoteFromJudgeRequested(name); });
add(b);
} else if (isReg) {
auto *b = makeBtn(tr("+ Judge"), tr("Promote to judge"), m_actionArea);
connect(b, &QPushButton::clicked, this, [this, name] { emit promoteToJudgeRequested(name); });
add(b);
}
}
m_actionArea->adjustSize();
}
void UserInfoPopup::updateActionButtons(const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored)
{
rebuildActionButtons(userInfo, online, isBuddy, isIgnored);
adjustSize();
}
void UserInfoPopup::onGamesContextMenu(const QPoint &pos)
{
const QModelIndex idx = m_gamesView->indexAt(pos);
if (!idx.isValid()) {
return;
}
const QVariant var = idx.data(PopupRoles::GameData);
if (!var.isValid()) {
return;
}
const ServerInfo_Game game = var.value<ServerInfo_Game>();
QMenu menu(this);
menu.setStyleSheet(
QStringLiteral("QMenu{background:#12182a;color:#c8d8ec;border:1px solid #1e2838;border-radius:4px;}"
"QMenu::item:selected{background:#223050;}"));
const bool canJoin = !game.started() && game.player_count() < game.max_players();
QAction *join = menu.addAction(tr("Join game"));
join->setEnabled(canJoin);
QAction *spec = nullptr;
if (game.spectators_allowed()) {
spec = menu.addAction(tr("Spectate"));
}
const QAction *chosen = menu.exec(m_gamesView->viewport()->mapToGlobal(pos));
if (!chosen) {
return;
}
if (chosen == join) {
emit joinGameRequested(game.game_id(), game.room_id(), false);
} else if (spec && chosen == spec) {
emit joinGameRequested(game.game_id(), game.room_id(), true);
}
}
// ── showForUser ───────────────────────────────────────────────────────────────
void UserInfoPopup::showForUser(const QString &userName,
const ServerInfo_User &userInfo,
bool online,
bool isBuddy,
bool isIgnored)
{
m_currentUser = userName;
m_currentUserInfo = userInfo;
m_currentOnline = online;
// Header
const QPixmap avatar = m_avatarCache ? m_avatarCache->value(userName) : QPixmap{};
const CardArtParams params = (m_cardArtParamsMap && m_cardArtParamsMap->contains(userName))
? m_cardArtParamsMap->value(userName)
: CardArtParams{};
const QString artKey = userName + u'|' + params.cardName + u'|' + params.cardProviderId;
const QPixmap cardArt = (m_cardArtCache && !params.cardName.isEmpty()) ? m_cardArtCache->value(artKey) : QPixmap{};
m_header->setUserData(userInfo, online, avatar, cardArt, params);
// Actions
rebuildActionButtons(userInfo, online, isBuddy, isIgnored);
// Games list reset
m_gamesModel->clear();
m_gamesView->hide();
m_gamesStatus->setText(tr("Loading games…"));
m_gamesStatus->show();
// Close button — top-right corner, above everything
m_closeBtn->move(PopupWidth - m_closeBtn->width() - 6, 6);
m_closeBtn->raise();
adjustSize();
fetchGames();
}
// ── Games fetch ───────────────────────────────────────────────────────────────
void UserInfoPopup::fetchGames()
{
if (!m_client || m_currentUser.isEmpty()) {
return;
}
Command_GetGamesOfUser cmd;
cmd.set_user_name(m_currentUser.toStdString());
const QString snapshot = m_currentUser;
PendingCommand *pend = m_client->prepareSessionCommand(cmd);
connect(pend, &PendingCommand::finished, this,
[this, snapshot](const Response &r) { onGamesReceived(r, snapshot); });
m_client->sendCommand(pend);
}
void UserInfoPopup::onGamesReceived(const Response &r, const QString &forUser)
{
if (forUser != m_currentUser) {
return; // stale response — different user showing now
}
m_gamesModel->clear();
if (r.response_code() != Response::RespOk) {
m_gamesStatus->setText(tr("Could not load games."));
m_gamesStatus->show();
m_gamesView->hide();
return;
}
const auto &resp = r.GetExtension(Response_GetGamesOfUser::ext);
if (resp.game_list_size() == 0) {
m_gamesStatus->setText(tr("No active games."));
m_gamesStatus->show();
m_gamesView->hide();
return;
}
for (int i = 0; i < resp.game_list_size(); ++i) {
auto *item = new QStandardItem;
item->setData(QVariant::fromValue(resp.game_list(i)), PopupRoles::GameData);
item->setEditable(false);
m_gamesModel->appendRow(item);
}
m_gamesStatus->hide();
m_gamesView->show();
// Fit exactly to the number of visible rows, scroll when more than 5
constexpr int rowH = 38; // must match PopupGameDelegate::sizeHint
constexpr int maxRows = 5;
const int count = m_gamesModel->rowCount();
const int visible = qMin(count, maxRows);
m_gamesView->setFixedHeight(visible * rowH + 2);
m_gamesView->setVerticalScrollBarPolicy(count > maxRows ? Qt::ScrollBarAlwaysOn : Qt::ScrollBarAlwaysOff);
adjustSize();
}
void UserInfoPopup::refreshGames()
{
m_gamesModel->clear();
m_gamesView->hide();
m_gamesStatus->setText(tr("Loading games…"));
m_gamesStatus->show();
fetchGames();
}
// ── Mouse events ──────────────────────────────────────────────────────────────
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
void UserInfoPopup::enterEvent(QEnterEvent *e)
{
QFrame::enterEvent(e);
emit mouseEnteredPopup();
}
#else
void UserInfoPopup::enterEvent(QEvent *e)
{
QFrame::enterEvent(e);
emit mouseEnteredPopup();
}
#endif
void UserInfoPopup::leaveEvent(QEvent *e)
{
QFrame::leaveEvent(e);
emit mouseLeftPopup();
}

View file

@ -0,0 +1,181 @@
#ifndef COCKATRICE_USER_INFO_POPUP_H
#define COCKATRICE_USER_INFO_POPUP_H
#include "../../interface/widgets/server/game_type_map.h"
#include "user_list_painter.h"
#include <QFrame>
#include <QListView>
#include <QMap>
#include <QPixmap>
#include <QStandardItemModel>
#include <libcockatrice/network/server/remote/user_level.h>
#include <libcockatrice/protocol/pb/response.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_game.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
class AbstractClient;
class QLabel;
class QPushButton;
class TabSupervisor;
// ── Roles ─────────────────────────────────────────────────────────────────────
namespace PopupRoles
{
constexpr int GameData = Qt::UserRole + 10;
}
// ── Header widget ─────────────────────────────────────────────────────────────
/**
* @class UserInfoHeaderWidget
* @brief Paints the enlarged banner card art + circular avatar section at the
* top of the UserInfoPopup.
*
* Layout mirrors UserListPainter but at a larger scale: the card art fills the
* full width as a semi-transparent background, a bottom gradient ensures the
* avatar and username text remain legible, and the status ring colour matches
* the UserListPainter convention.
*/
class UserInfoHeaderWidget : public QWidget
{
Q_OBJECT
static constexpr int HeaderHeight = 130;
static constexpr int AvatarSize = 68;
static constexpr int AvatarPawnSize = 46;
static constexpr int LeftPad = 14;
static constexpr int AvatarToTextGap = 10;
public:
explicit UserInfoHeaderWidget(QWidget *parent = nullptr);
void setUserData(const ServerInfo_User &user,
bool online,
const QPixmap &avatar,
const QPixmap &cardArt,
const CardArtParams &params);
protected:
void paintEvent(QPaintEvent *e) override;
private:
ServerInfo_User m_user;
bool m_online = false;
QPixmap m_avatar;
QPixmap m_cardArt;
CardArtParams m_params;
};
// ── Main popup ────────────────────────────────────────────────────────────────
/**
* @class UserInfoPopup
* @brief Floating panel showing an enlarged user card, quick action buttons,
* and a live scrollable games list.
*
* Lifecycle (mirrors DeckEditorDeckDockWidget):
* - showForUser() populate, position externally, call show()
* - mouseEnteredPopup / mouseLeftPopup caller manages hide timer
* - closeRequested() emitted by the internal close button
*
* The popup is a Qt::Tool frameless child so windowOpacity animations and
* move() in screen coordinates work identically to CardInfoPictureEnlargedWidget.
*
* Action signals map 1-to-1 to UserContextMenu::exec*() methods so all action
* logic stays in one place.
*/
class UserInfoPopup : public QFrame
{
Q_OBJECT
static constexpr int PopupWidth = 316;
public:
explicit UserInfoPopup(TabSupervisor *tabSupervisor,
AbstractClient *client,
const QMap<QString, QPixmap> *avatarCache,
const QMap<QString, QPixmap> *cardArtCache,
const QMap<QString, CardArtParams> *cardArtParamsMap,
QWidget *parent);
/**
* Populate the popup for @p userName and kick off a game list fetch.
* Call show() / move() externally after this.
*/
void
showForUser(const QString &userName, const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored);
void fetchGames();
[[nodiscard]] QString currentUser() const
{
return m_currentUser;
}
/** Called when buddy/ignore status changes externally while popup is open. */
void updateActionButtons(const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored);
signals:
void mouseEnteredPopup();
void mouseLeftPopup();
void closeRequested();
/** Emitted when the user requests joining or spectating a game in the list. */
void joinGameRequested(int gameId, int roomId, bool asSpectator);
// ── Action signals — connect to UserContextMenu::exec*() ──────────────────
void chatRequested(const QString &userName);
void detailsRequested(const QString &userName);
void showGamesRequested(const QString &userName);
void addBuddyRequested(const QString &userName);
void removeBuddyRequested(const QString &userName);
void addIgnoreRequested(const QString &userName);
void removeIgnoreRequested(const QString &userName);
void banRequested(const QString &userName);
void warnRequested(const QString &userName);
void banHistoryRequested(const QString &userName);
void warnHistoryRequested(const QString &userName);
void adminNotesRequested(const QString &userName);
void promoteToModRequested(const QString &userName);
void demoteFromModRequested(const QString &userName);
void promoteToJudgeRequested(const QString &userName);
void demoteFromJudgeRequested(const QString &userName);
protected:
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
void enterEvent(QEnterEvent *e) override;
#else
void enterEvent(QEvent *e) override;
#endif
void leaveEvent(QEvent *e) override;
private slots:
void refreshGames();
void onGamesReceived(const Response &r, const QString &forUser);
void onGamesContextMenu(const QPoint &pos);
private:
void buildUi();
void rebuildActionButtons(const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored);
TabSupervisor *m_ts;
AbstractClient *m_client;
const QMap<QString, QPixmap> *m_avatarCache;
const QMap<QString, QPixmap> *m_cardArtCache;
const QMap<QString, CardArtParams> *m_cardArtParamsMap;
QString m_currentUser;
ServerInfo_User m_currentUserInfo;
bool m_currentOnline = false;
UserInfoHeaderWidget *m_header;
QWidget *m_actionArea; ///< rebuilt per user
QListView *m_gamesView;
QStandardItemModel *m_gamesModel;
QLabel *m_gamesStatus;
QPushButton *m_closeBtn;
QPushButton *m_refreshBtn;
};
#endif // COCKATRICE_USER_INFO_POPUP_H

View file

@ -42,6 +42,9 @@ void UserListManager::handleDisconnect()
delete ownUserInfo;
ownUserInfo = nullptr;
// Full rebuild — all lists are gone
emit listReset();
}
void UserListManager::setOwnUserInfo(const ServerInfo_User &userInfo)
@ -63,74 +66,77 @@ void UserListManager::processListUsersResponse(const Response &response)
const int userListSize = resp.user_list_size();
for (int i = 0; i < userListSize; ++i) {
const ServerInfo_User &info = resp.user_list(i);
const QString &userName = QString::fromStdString(info.name());
onlineUsers.insert(userName, info);
onlineUsers.insert(QString::fromStdString(info.name()), info);
}
// Bulk load complete — widgets rebuild once from the now-populated map
emit listReset();
}
void UserListManager::processUserJoinedEvent(const Event_UserJoined &event)
{
const auto &info = event.user_info();
const QString &userName = QString::fromStdString(info.name());
onlineUsers.insert(userName, info);
const QString name = QString::fromStdString(info.name());
onlineUsers.insert(name, info);
emit userJoinedOnline(info);
}
void UserListManager::processUserLeftEvent(const Event_UserLeft &event)
{
const auto &userName = QString::fromStdString(event.name());
onlineUsers.remove(userName);
const QString name = QString::fromStdString(event.name());
onlineUsers.remove(name);
emit userLeftOnline(name);
}
void UserListManager::buddyListReceived(const QList<ServerInfo_User> &_buddyList)
{
for (const auto &user : _buddyList) {
const auto &userName = QString::fromStdString(user.name());
buddyUsers.insert(userName, user);
buddyUsers.insert(QString::fromStdString(user.name()), user);
}
// Bulk load — one reset covers all newly added entries
emit listReset();
}
void UserListManager::ignoreListReceived(const QList<ServerInfo_User> &_ignoreList)
{
for (const auto &user : _ignoreList) {
const auto &userName = QString::fromStdString(user.name());
ignoredUsers.insert(userName, user);
ignoredUsers.insert(QString::fromStdString(user.name()), user);
}
// Bulk load — one reset covers all newly added entries
emit listReset();
}
void UserListManager::processAddToListEvent(const Event_AddToList &event)
{
const auto &user = event.user_info();
const auto &userName = QString::fromStdString(user.name());
const QString userName = QString::fromStdString(user.name());
const QString listType = QString::fromStdString(event.list_name());
const auto &userListType = QString::fromStdString(event.list_name());
QMap<QString, ServerInfo_User> *userMap;
if (userListType == "buddy") {
userMap = &buddyUsers;
} else if (userListType == "ignore") {
userMap = &ignoredUsers;
} else {
return;
if (listType == "buddy") {
buddyUsers.insert(userName, user);
emit addedToBuddyList(user);
} else if (listType == "ignore") {
ignoredUsers.insert(userName, user);
emit addedToIgnoreList(user);
}
userMap->insert(userName, user);
}
void UserListManager::processRemoveFromListEvent(const Event_RemoveFromList &event)
{
const auto &userListType = QString::fromStdString(event.list_name());
const auto &userName = QString::fromStdString(event.user_name());
const QString listType = QString::fromStdString(event.list_name());
const QString userName = QString::fromStdString(event.user_name());
QMap<QString, ServerInfo_User> *userMap;
if (userListType == "buddy") {
userMap = &buddyUsers;
} else if (userListType == "ignore") {
userMap = &ignoredUsers;
} else {
return;
if (listType == "buddy") {
buddyUsers.remove(userName);
emit removedFromBuddyList(userName);
} else if (listType == "ignore") {
ignoredUsers.remove(userName);
emit removedFromIgnoreList(userName);
}
userMap->remove(userName);
}
bool UserListManager::isOwnUserRegistered() const
@ -155,16 +161,9 @@ bool UserListManager::isUserIgnored(const QString &userName) const
const ServerInfo_User *UserListManager::getOnlineUser(const QString &userName) const
{
const QString &userNameToMatchLower = userName.toLower();
const auto it =
std::find_if(onlineUsers.begin(), onlineUsers.end(), [&userNameToMatchLower](const ServerInfo_User &user) {
return userNameToMatchLower == QString::fromStdString(user.name()).toLower();
});
if (it != onlineUsers.end()) {
return &*it;
}
return nullptr;
const QString lower = userName.toLower();
const auto it = std::find_if(onlineUsers.begin(), onlineUsers.end(), [&lower](const ServerInfo_User &user) {
return lower == QString::fromStdString(user.name()).toLower();
});
return it != onlineUsers.end() ? &*it : nullptr;
}

View file

@ -47,15 +47,17 @@ public:
explicit UserListManager(AbstractClient *_client, QObject *parent = nullptr);
~UserListManager() override;
[[nodiscard]] QMap<QString, ServerInfo_User> getAllUsersList() const
[[nodiscard]] const QMap<QString, ServerInfo_User> &getAllUsersList() const
{
return onlineUsers;
}
[[nodiscard]] QMap<QString, ServerInfo_User> getBuddyList() const
[[nodiscard]] const QMap<QString, ServerInfo_User> &getBuddyList() const
{
return buddyUsers;
}
[[nodiscard]] QMap<QString, ServerInfo_User> getIgnoreList() const
[[nodiscard]] const QMap<QString, ServerInfo_User> &getIgnoreList() const
{
return ignoredUsers;
}
@ -71,8 +73,26 @@ public slots:
void handleDisconnect();
signals:
void userLeft(const QString &userName);
void userJoined(const ServerInfo_User &userInfo);
/**
* The entire list needs to be rebuilt from scratch.
* Fired on disconnect, reconnect, and initial bulk loads
* (Command_ListUsers response, initial buddy/ignore lists).
*/
void listReset();
// ── Online user presence ──────────────────────────────────────────────────
/** A user came online (or joined the room). Full ServerInfo_User available. */
void userJoinedOnline(const ServerInfo_User &user);
/** A user went offline (or left the room). */
void userLeftOnline(const QString &userName);
// ── Buddy list mutations (individual, post-login) ─────────────────────────
void addedToBuddyList(const ServerInfo_User &user);
void removedFromBuddyList(const QString &userName);
// ── Ignore list mutations (individual, post-login) ────────────────────────
void addedToIgnoreList(const ServerInfo_User &user);
void removedFromIgnoreList(const QString &userName);
};
#endif // COCKATRICE_USER_LIST_MANAGER_H

View file

@ -0,0 +1,342 @@
#include "user_list_painter.h"
#include "../../interface/pixel_map_generator.h"
#include <QAbstractScrollArea>
#include <QPainter>
#include <QPainterPath>
#include <QScrollBar>
#include <QStyle>
#include <QStyleOptionViewItem>
static constexpr int RowHeight = 72;
static constexpr int AvatarSize = 36;
static constexpr int LeftPadding = 14;
static constexpr int TextSpacing = 10;
QSize UserListPainter::sizeHint()
{
return QSize(0, RowHeight);
}
QColor UserListPainter::getAccentColor(const UserLevelFlags &userLevel, bool online)
{
QColor accentColor;
if (userLevel.testFlag(ServerInfo_User::IsAdmin)) {
accentColor = QColor(245, 158, 11);
} else if (userLevel.testFlag(ServerInfo_User::IsModerator)) {
accentColor = QColor(59, 130, 246);
} else if (userLevel.testFlag(ServerInfo_User::IsJudge)) {
accentColor = QColor(168, 85, 247);
} else {
accentColor = QColor(100, 116, 139);
}
if (!online) {
accentColor = accentColor.darker(160);
}
return accentColor;
}
int UserListPainter::getCardRight(const QStyleOptionViewItem &option, const QRect &rect)
{
int scrollBarWidth = 0;
if (const auto *scrollArea = qobject_cast<const QAbstractScrollArea *>(option.widget)) {
const QScrollBar *sb = scrollArea->verticalScrollBar();
if (sb && sb->isVisible()) {
scrollBarWidth = sb->width();
}
}
const int viewportRight = option.widget ? option.widget->width() - scrollBarWidth : rect.right();
return qMin(rect.right(), viewportRight - 4);
}
void UserListPainter::drawBackground(QPainter *painter,
const QRectF &cardRect,
const QColor &accentColor,
bool selected)
{
QLinearGradient bg(cardRect.topLeft(), cardRect.topRight());
bg.setColorAt(0, selected ? accentColor.darker(130) : accentColor.darker(320));
bg.setColorAt(1, selected ? QColor(40, 48, 60) : QColor(18, 22, 30));
painter->setPen(Qt::NoPen);
painter->setBrush(bg);
painter->drawRoundedRect(cardRect, 6, 6);
painter->setBrush(accentColor);
painter->drawRoundedRect(QRectF(cardRect.left(), cardRect.top(), 3, cardRect.height()), 2, 2);
}
static QString makeKey(const QString &user, const QString &card, const QString &providerId)
{
return user + u'|' + card + u'|' + providerId;
}
void UserListPainter::drawCardArt(QPainter *painter,
const QRect &rect,
int cardRight,
const QString &userName,
const QMap<QString, QPixmap> *cardArtCache,
const CardArtParams &params,
const QPixmap *overridePixmap = nullptr)
{
QPixmap art;
if (overridePixmap && !overridePixmap->isNull()) {
art = *overridePixmap;
} else {
if (!cardArtCache) {
return;
}
const QString key = makeKey(userName, params.cardName, params.cardProviderId);
if (!cardArtCache->contains(key)) {
return;
}
art = cardArtCache->value(key);
}
if (art.isNull()) {
return;
}
const int cardH = rect.height() - 4;
const int totalW = cardRight - rect.left();
const int marginL = qRound(totalW * params.marginPctL);
const int marginR = qRound(totalW * params.marginPctR);
const int drawW = totalW - marginL - marginR;
const double basescale = qMax(double(drawW) / art.width(), double(cardH) / art.height());
const double scale = basescale * params.zoom;
const int scaledW = qRound(art.width() * scale);
const int scaledH = qRound(art.height() * scale);
const QPixmap scaled = art.scaled(scaledW, scaledH, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
const int srcX = (scaledW - drawW) / 2;
const int srcY = qRound((scaledH - cardH) * params.verticalOffset);
// Clamp srcY so we never copy outside the pixmap bounds
const int safeSrcY = qBound(0, srcY, qMax(0, scaledH - cardH));
QImage img =
scaled.copy(srcX, safeSrcY, drawW, cardH).toImage().convertToFormat(QImage::Format_ARGB32_Premultiplied);
{
QPainter mask(&img);
mask.setCompositionMode(QPainter::CompositionMode_DestinationIn);
QLinearGradient grad(0, 0, img.width(), 0);
grad.setColorAt(0.00, Qt::transparent);
grad.setColorAt(0.22, Qt::white);
grad.setColorAt(0.78, Qt::white);
grad.setColorAt(1.00, Qt::transparent);
mask.fillRect(img.rect(), grad);
}
painter->setOpacity(0.55);
painter->drawImage(rect.left() + marginL, rect.top() + 2, img);
painter->setOpacity(1.0);
}
QRect UserListPainter::getAvatarRect(const QRect &rect)
{
const int avatarX = rect.left() + LeftPadding;
const int avatarY = rect.top() + (rect.height() - AvatarSize) / 2;
return QRect(avatarX, avatarY, AvatarSize, AvatarSize);
}
void UserListPainter::drawAvatar(QPainter *painter,
const QRect &avatarRect,
const QString &userName,
const QColor &accentColor,
const UserLevelFlags &userLevel,
const ServerInfo_User &userInfo,
const QString &privLevel,
const QMap<QString, QPixmap> *avatarCache)
{
QPainterPath clipPath;
clipPath.addEllipse(avatarRect);
painter->save();
painter->setClipPath(clipPath);
bool drewAvatar = false;
if (avatarCache && avatarCache->contains(userName)) {
const QPixmap &avatar = avatarCache->value(userName);
if (!avatar.isNull()) {
painter->drawPixmap(
avatarRect, avatar.scaled(avatarRect.size(), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation));
drewAvatar = true;
}
}
if (!drewAvatar) {
painter->setBrush(accentColor.darker(200));
painter->setPen(Qt::NoPen);
painter->drawEllipse(avatarRect);
const QPixmap pawn =
UserLevelPixmapGenerator::generatePixmap(24, userLevel, userInfo.pawn_colors(), false, privLevel);
painter->drawPixmap(avatarRect.center().x() - 12, avatarRect.center().y() - 12, pawn);
}
painter->restore();
}
void UserListPainter::drawStatusRing(QPainter *painter, const QRect &avatarRect, bool online)
{
const QColor statusColor = online ? QColor(34, 197, 94) : QColor(70, 80, 95);
painter->setPen(QPen(statusColor, 2));
painter->setBrush(Qt::NoBrush);
painter->drawEllipse(avatarRect.adjusted(-1, -1, 1, 1));
}
void UserListPainter::drawUserName(QPainter *painter,
const QStyleOptionViewItem &option,
const QRect &rect,
int cardRight,
int textX,
const QString &userName,
bool online,
bool selected)
{
QFont nameFont = option.font;
nameFont.setBold(true);
painter->setFont(nameFont);
const QRect nameRect(textX, rect.top() + 8, cardRight - textX - 10, 20);
const QString elidedName = QFontMetrics(nameFont).elidedText(userName, Qt::ElideRight, cardRight - textX - 10);
painter->setPen(QColor(0, 0, 0, 200));
painter->drawText(nameRect.translated(1, 1), Qt::AlignVCenter | Qt::AlignLeft, elidedName);
painter->setPen(online ? (selected ? Qt::white : QColor(226, 232, 240)) : QColor(90, 100, 115));
painter->drawText(nameRect, Qt::AlignVCenter | Qt::AlignLeft, elidedName);
}
void UserListPainter::drawCountryFlag(QPainter *painter, const QRect &rect, int textX, const ServerInfo_User &userInfo)
{
const QPixmap flag = CountryPixmapGenerator::generatePixmap(13, QString::fromStdString(userInfo.country()));
if (!flag.isNull()) {
painter->drawPixmap(textX, rect.top() + 46, flag);
}
}
QList<UserListPainter::Badge> UserListPainter::buildBadges(const UserLevelFlags &userLevel, const QString &privLevel)
{
QList<Badge> badges;
if (userLevel.testFlag(ServerInfo_User::IsAdmin)) {
badges << Badge{"ADMIN", QColor(245, 158, 11)};
} else if (userLevel.testFlag(ServerInfo_User::IsModerator)) {
badges << Badge{"MOD", QColor(59, 130, 246)};
} else if (userLevel.testFlag(ServerInfo_User::IsJudge)) {
badges << Badge{"JUDGE", QColor(168, 85, 247)};
}
if (privLevel == "VIP") {
badges << Badge{"VIP", QColor(20, 184, 166)};
} else if (privLevel == "DONATOR") {
badges << Badge{"DONATOR", QColor(249, 115, 22)};
}
return badges;
}
void UserListPainter::drawBadges(QPainter *painter,
const QStyleOptionViewItem &option,
const QRect &rect,
int cardRight,
const QList<Badge> &badges,
bool online)
{
if (badges.isEmpty()) {
return;
}
QFont badgeFont = option.font;
badgeFont.setPointSizeF(badgeFont.pointSizeF() * 0.68);
badgeFont.setBold(true);
painter->setFont(badgeFont);
QFontMetrics fm(badgeFont);
int totalBadgeW = 0;
for (const Badge &b : badges) {
totalBadgeW += fm.horizontalAdvance(b.text) + 8 + 4;
}
totalBadgeW -= 4;
int bx = cardRight - 6 - totalBadgeW;
for (const Badge &b : badges) {
const QColor col = online ? b.color : b.color.darker(180);
const int bw = fm.horizontalAdvance(b.text) + 8;
const QRect br(bx, rect.top() + 44, bw, 13);
painter->setPen(Qt::NoPen);
painter->setBrush(col.darker(online ? 160 : 220));
painter->drawRoundedRect(br, 3, 3);
painter->setPen(col.lighter(online ? 160 : 100));
painter->drawText(br, Qt::AlignCenter, b.text);
bx += bw + 4;
}
}
void UserListPainter::paint(QPainter *painter,
const QStyleOptionViewItem &option,
const QModelIndex &index,
const ServerInfo_User &userInfo,
const QMap<QString, QPixmap> *avatarCache,
const QMap<QString, QPixmap> *cardArtCache,
const QMap<QString, CardArtParams> *cardArtParamsMap)
{
painter->save();
painter->setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::TextAntialiasing);
const QRect rect = option.rect;
const bool online = index.data(Qt::UserRole + 1).toBool();
const bool selected = option.state & QStyle::State_Selected;
const UserLevelFlags userLevel(userInfo.user_level());
const QString userName = QString::fromStdString(userInfo.name());
const QString privLevel = QString::fromStdString(userInfo.privlevel());
const QColor accentColor = getAccentColor(userLevel, online);
const QRectF cardRect = QRectF(rect).adjusted(3, 2, -3, -2);
const int cardRight = getCardRight(option, rect);
const CardArtParams params = (cardArtParamsMap && cardArtParamsMap->contains(userName))
? cardArtParamsMap->value(userName)
: CardArtParams{};
drawBackground(painter, cardRect, accentColor, selected);
drawCardArt(painter, rect, cardRight, userName, cardArtCache, params);
const QRect avatarRect = getAvatarRect(rect);
drawAvatar(painter, avatarRect, userName, accentColor, userLevel, userInfo, privLevel, avatarCache);
drawStatusRing(painter, avatarRect, online);
const int textX = avatarRect.right() + TextSpacing;
drawUserName(painter, option, rect, cardRight, textX, userName, online, selected);
drawCountryFlag(painter, rect, textX, userInfo);
const QList<Badge> badges = buildBadges(userLevel, privLevel);
drawBadges(painter, option, rect, cardRight, badges, online);
painter->restore();
}

View file

@ -0,0 +1,87 @@
#ifndef COCKATRICE_USER_LIST_PAINTER_H
#define COCKATRICE_USER_LIST_PAINTER_H
#include "user_level.h"
#include <QColor>
#include <QList>
#include <QMap>
#include <QPixmap>
#include <QRect>
#include <QSize>
class QPainter;
class QModelIndex;
class QStyleOptionViewItem;
class ServerInfo_User;
struct CardArtParams
{
QString cardName = "";
QString cardProviderId = "";
double marginPctL = 0.33;
double marginPctR = 0.02;
double verticalOffset = 0.35;
double zoom = 1.0;
};
class UserListPainter
{
public:
static void paint(QPainter *painter,
const QStyleOptionViewItem &option,
const QModelIndex &index,
const ServerInfo_User &userInfo,
const QMap<QString, QPixmap> *avatarCache,
const QMap<QString, QPixmap> *cardArtCache,
const QMap<QString, CardArtParams> *cardArtParamsMap);
static QSize sizeHint();
static void drawCardArt(QPainter *painter,
const QRect &rect,
int cardRight,
const QString &userName,
const QMap<QString, QPixmap> *cardArtCache,
const CardArtParams &params,
const QPixmap *overridePixmap);
private:
struct Badge
{
QString text;
QColor color;
};
static QColor getAccentColor(const UserLevelFlags &userLevel, bool online);
static int getCardRight(const QStyleOptionViewItem &option, const QRect &rect);
static void drawBackground(QPainter *painter, const QRectF &cardRect, const QColor &accentColor, bool selected);
static QRect getAvatarRect(const QRect &rect);
static void drawAvatar(QPainter *painter,
const QRect &avatarRect,
const QString &userName,
const QColor &accentColor,
const UserLevelFlags &userLevel,
const ServerInfo_User &userInfo,
const QString &privLevel,
const QMap<QString, QPixmap> *avatarCache);
static void drawStatusRing(QPainter *painter, const QRect &avatarRect, bool online);
static void drawUserName(QPainter *painter,
const QStyleOptionViewItem &option,
const QRect &rect,
int cardRight,
int textX,
const QString &userName,
bool online,
bool selected);
static void drawCountryFlag(QPainter *painter, const QRect &rect, int textX, const ServerInfo_User &userInfo);
static QList<Badge> buildBadges(const UserLevelFlags &userLevel, const QString &privLevel);
static void drawBadges(QPainter *painter,
const QStyleOptionViewItem &option,
const QRect &rect,
int cardRight,
const QList<Badge> &badges,
bool online);
};
#endif // COCKATRICE_USER_LIST_PAINTER_H

View file

@ -1,10 +1,13 @@
#include "user_list_widget.h"
#include "../../../../client/settings/cache_settings.h"
#include "../../../card_picture_loader/card_picture_loader.h"
#include "../../interface/pixel_map_generator.h"
#include "../../interface/widgets/tabs/tab_account.h"
#include "../../interface/widgets/tabs/tab_supervisor.h"
#include "../game_selector.h"
#include "user_context_menu.h"
#include "user_list_painter.h"
#include <QApplication>
#include <QCheckBox>
@ -15,14 +18,19 @@
#include <QLineEdit>
#include <QMessageBox>
#include <QMouseEvent>
#include <QPainter>
#include <QPainterPath>
#include <QPlainTextEdit>
#include <QPushButton>
#include <QRadioButton>
#include <QSpinBox>
#include <QWidget>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/network/client/abstract/abstract_client.h>
#include <libcockatrice/protocol/pb/response_get_games_of_user.pb.h>
#include <libcockatrice/utility/trice_limits.h>
#include <libcockatrice/protocol/pb/response_get_user_info.pb.h>
#include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/utility/string_limits.h>
BanDialog::BanDialog(const ServerInfo_User &info, QWidget *parent) : QDialog(parent)
{
@ -308,7 +316,18 @@ QString AdminNotesDialog::getNotes() const
return notes->toPlainText();
}
UserListItemDelegate::UserListItemDelegate(QObject *const parent) : QStyledItemDelegate(parent)
namespace UserListRoles
{
constexpr int Online = Qt::UserRole + 1;
constexpr int UserInfo = Qt::UserRole + 2;
} // namespace UserListRoles
UserListItemDelegate::UserListItemDelegate(QObject *const parent,
const QMap<QString, QPixmap> *avatarCache,
const QMap<QString, QPixmap> *cardArtCache,
const QMap<QString, CardArtParams> *cardArtParamsMap)
: QStyledItemDelegate(parent), avatarCache(avatarCache), cardArtCache(cardArtCache),
cardArtParamsMap(cardArtParamsMap)
{
}
@ -327,6 +346,32 @@ bool UserListItemDelegate::editorEvent(QEvent *event,
return QStyledItemDelegate::editorEvent(event, model, option, index);
}
QSize UserListItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
{
if (!SettingsCache::instance().getStyleUserList()) {
return QStyledItemDelegate::sizeHint(option, index);
}
return UserListPainter::sizeHint();
}
void UserListItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
if (!SettingsCache::instance().getStyleUserList()) {
QStyledItemDelegate::paint(painter, option, index);
return;
}
const QVariant var = index.data(UserListRoles::UserInfo);
if (!var.isValid()) {
QStyledItemDelegate::paint(painter, option, index);
return;
}
UserListPainter::paint(painter, option, index, var.value<ServerInfo_User>(), avatarCache, cardArtCache,
cardArtParamsMap);
}
UserListTWI::UserListTWI(const ServerInfo_User &_userInfo) : QTreeWidgetItem(Type)
{
setUserInfo(_userInfo);
@ -343,11 +388,12 @@ void UserListTWI::setUserInfo(const ServerInfo_User &_userInfo)
setData(2, Qt::UserRole, QString::fromStdString(userInfo.name()));
setData(2, Qt::DisplayRole, QString::fromStdString(userInfo.name()));
setData(3, Qt::InitialSortOrderRole, QString::fromStdString(userInfo.privlevel()));
setData(0, UserListRoles::UserInfo, QVariant::fromValue(userInfo));
}
void UserListTWI::setOnline(bool online)
{
setData(0, Qt::UserRole + 1, online);
setData(0, UserListRoles::Online, online);
setData(2, Qt::ForegroundRole, online ? qApp->palette().brush(QPalette::WindowText) : QBrush(Qt::gray));
}
@ -366,8 +412,8 @@ void UserListTWI::setOnline(bool online)
bool UserListTWI::operator<(const QTreeWidgetItem &other) const
{
// Sort by online/offline
if (data(0, Qt::UserRole + 1) != other.data(0, Qt::UserRole + 1)) {
return data(0, Qt::UserRole + 1).toBool();
if (data(0, UserListRoles::Online) != other.data(0, UserListRoles::Online)) {
return data(0, UserListRoles::Online).toBool();
}
const auto &lhsUserLevelFlags = UserLevelFlags(data(0, Qt::UserRole).toInt());
@ -414,20 +460,100 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
QWidget *parent)
: QGroupBox(parent), tabSupervisor(_tabSupervisor), client(_client), type(_type), onlineCount(0)
{
itemDelegate = new UserListItemDelegate(this);
avatarProvider = new UserAvatarProvider(client, this);
cardArtProvider = new UserCardArtProvider(this);
itemDelegate =
new UserListItemDelegate(this, &avatarProvider->cache(), &cardArtProvider->cache(), &cardArtParamsMap);
userContextMenu = new UserContextMenu(tabSupervisor, this);
connect(userContextMenu, &UserContextMenu::openMessageDialog, this, &UserListWidget::openMessageDialog);
userTree = new QTreeWidget;
userTree->setColumnCount(3);
userTree->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
userTree->setColumnCount(4); // 0=display, 1=flag(hidden), 2=name(hidden), 3=privlevel(hidden)
userTree->header()->setSectionResizeMode(0, QHeaderView::Stretch);
userTree->header()->setMinimumSectionSize(0);
userTree->setHeaderHidden(true);
userTree->setRootIsDecorated(false);
userTree->setIconSize(QSize(20, 18));
userTree->setItemDelegate(itemDelegate);
userTree->setAlternatingRowColors(true);
userTree->hideColumn(1);
userTree->hideColumn(2);
userTree->hideColumn(3);
connect(userTree, &QTreeWidget::itemActivated, this, &UserListWidget::userClicked);
userTree->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
userTree->header()->setStretchLastSection(true);
// ── Hover popup ───────────────────────────────────────────────────────────
m_userInfoPopup = new UserInfoPopup(tabSupervisor, tabSupervisor->getClient(), &avatarProvider->cache(),
&cardArtProvider->cache(), &cardArtParamsMap,
window()); // parented to main window so it floats above siblings
m_userInfoPopup->hide();
m_userInfoPopup->setWindowOpacity(0.0);
m_userInfoPopup->installEventFilter(this);
connectPopupSignals();
m_showPopupTimer = new QTimer(this);
m_showPopupTimer->setSingleShot(true);
m_showPopupTimer->setInterval(280);
connect(m_showPopupTimer, &QTimer::timeout, this, [this] {
if (!m_hoveredUser.isEmpty()) {
showPopupForUser(m_hoveredUser);
}
});
m_hidePopupTimer = new QTimer(this);
m_hidePopupTimer->setSingleShot(true);
m_hidePopupTimer->setInterval(160);
connect(m_hidePopupTimer, &QTimer::timeout, this, [this] {
if (!m_popupPinned && !m_userInfoPopup->underMouse() && !userTree->underMouse()) {
hidePopup();
}
});
userTree->setMouseTracking(true);
userTree->viewport()->setMouseTracking(true);
userTree->viewport()->installEventFilter(this);
// Pin on item click
connect(userTree, &QTreeWidget::itemClicked, this, [this](QTreeWidgetItem *item, int) {
if (!SettingsCache::instance().getStyleUserList()) {
return;
}
const QString name = static_cast<UserListTWI *>(item)->getUserInfo().name().c_str();
m_popupPinned = false; // reset so showPopupForUser can update
showPopupForUser(name);
m_popupPinned = true; // pin after showing
});
connect(userTree->selectionModel(), &QItemSelectionModel::selectionChanged, this,
[this](const QItemSelection &sel, const QItemSelection &) {
// if (m_rebuildingTree) return;
if (sel.isEmpty() && m_popupPinned) {
m_popupPinned = false;
hidePopup();
}
});
// Hide popup when list scrolls (reference row has moved)
connect(userTree->verticalScrollBar(), &QScrollBar::valueChanged, this, [this] {
m_showPopupTimer->stop();
hidePopup(true);
});
// Forward join requests from popup upward
connect(m_userInfoPopup, &UserInfoPopup::joinGameRequested, this, &UserListWidget::joinGameRequested);
connect(avatarProvider, &UserAvatarProvider::avatarUpdated, this,
[this](const QString &) { userTree->viewport()->update(); });
connect(cardArtProvider, &UserCardArtProvider::cardArtUpdated, this,
[this](const QString &) { userTree->viewport()->update(); });
connect(&SettingsCache::instance(), &SettingsCache::styleUserListChanged, this, &UserListWidget::applyDisplayMode);
applyDisplayMode();
QVBoxLayout *vbox = new QVBoxLayout;
vbox->addWidget(userTree);
@ -437,6 +563,296 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
retranslateUi();
}
void UserListWidget::bind(UserListManager *mgr)
{
manager = mgr;
// ── Full rebuild: disconnect / reconnect / bulk initial load ──────────────
connect(manager, &UserListManager::listReset, this, &UserListWidget::rebuild);
// ── Online users list (AllUsersList / RoomList) ───────────────────────────
if (type == AllUsersList || type == RoomList) {
connect(manager, &UserListManager::userJoinedOnline, this,
[this](const ServerInfo_User &user) { processUserInfo(user, true); });
connect(manager, &UserListManager::userLeftOnline, this, [this](const QString &name) { deleteUser(name); });
}
// ── Buddy list ────────────────────────────────────────────────────────────
if (type == BuddyList) {
connect(manager, &UserListManager::addedToBuddyList, this, [this](const ServerInfo_User &user) {
const QString name = QString::fromStdString(user.name());
processUserInfo(user, manager->getOnlineUser(name) != nullptr);
});
connect(manager, &UserListManager::removedFromBuddyList, this,
[this](const QString &name) { deleteUser(name); });
// Track online presence changes for buddies already in the tree
connect(manager, &UserListManager::userJoinedOnline, this, [this](const ServerInfo_User &user) {
const QString name = QString::fromStdString(user.name());
if (users.contains(name)) {
users[name]->setUserInfo(user);
setUserOnline(name, true);
}
});
connect(manager, &UserListManager::userLeftOnline, this, [this](const QString &name) {
if (users.contains(name)) {
setUserOnline(name, false);
}
});
}
// ── Ignore list ───────────────────────────────────────────────────────────
if (type == IgnoreList) {
connect(manager, &UserListManager::addedToIgnoreList, this, [this](const ServerInfo_User &user) {
const QString name = QString::fromStdString(user.name());
processUserInfo(user, manager->getOnlineUser(name) != nullptr);
});
connect(manager, &UserListManager::removedFromIgnoreList, this,
[this](const QString &name) { deleteUser(name); });
}
// ── Popup button refresh ──────────────────────────────────────────────────
// Any buddy/ignore mutation while the popup is open refreshes its buttons
auto refreshIfPopupOpen = [this](const QString &name) {
if (m_userInfoPopup && m_userInfoPopup->isVisible() && m_userInfoPopup->currentUser() == name) {
refreshPopupButtons(name);
}
};
auto refreshCurrentPopup = [refreshIfPopupOpen](const ServerInfo_User &u) {
refreshIfPopupOpen(QString::fromStdString(u.name()));
};
connect(manager, &UserListManager::addedToBuddyList, this, refreshCurrentPopup);
connect(manager, &UserListManager::removedFromBuddyList, this, refreshIfPopupOpen);
connect(manager, &UserListManager::addedToIgnoreList, this, refreshCurrentPopup);
connect(manager, &UserListManager::removedFromIgnoreList, this, refreshIfPopupOpen);
connect(manager, &UserListManager::userJoinedOnline, this, refreshCurrentPopup);
connect(manager, &UserListManager::userLeftOnline, this, refreshIfPopupOpen);
rebuild();
}
void UserListWidget::refreshPopupButtons(const QString &userName)
{
UserListTWI *item = users.value(userName);
if (!item) {
return;
}
const UserListProxy *proxy = tabSupervisor->getUserListManager();
const bool online = item->data(0, UserListRoles::Online).toBool();
const bool isBuddy = proxy->isUserBuddy(userName);
const bool isIgn = proxy->isUserIgnored(userName);
m_userInfoPopup->updateActionButtons(item->getUserInfo(), online, isBuddy, isIgn);
positionPopup(userName); // height may have changed — reposition
}
void UserListWidget::hideEvent(QHideEvent *e)
{
QGroupBox::hideEvent(e);
m_showPopupTimer->stop();
m_hidePopupTimer->stop();
hidePopup(true);
}
void UserListWidget::applyDisplayMode()
{
const bool styled = SettingsCache::instance().getStyleUserList();
if (styled) {
userTree->header()->setSectionResizeMode(0, QHeaderView::Stretch);
userTree->hideColumn(1);
userTree->hideColumn(2);
userTree->hideColumn(3);
} else {
userTree->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
userTree->showColumn(1);
userTree->showColumn(2);
userTree->hideColumn(3);
}
userTree->viewport()->update();
}
void UserListWidget::connectPopupSignals()
{
connect(m_userInfoPopup, &UserInfoPopup::closeRequested, this, [this] {
m_popupPinned = false;
hidePopup(true);
});
connect(m_userInfoPopup, &UserInfoPopup::mouseEnteredPopup, m_hidePopupTimer, &QTimer::stop);
connect(m_userInfoPopup, &UserInfoPopup::mouseLeftPopup, this, [this] {
if (!m_popupPinned) {
m_hidePopupTimer->start();
}
});
// Wire all action signals to UserContextMenu::exec*()
connect(m_userInfoPopup, &UserInfoPopup::chatRequested, userContextMenu, &UserContextMenu::execChat);
connect(m_userInfoPopup, &UserInfoPopup::detailsRequested, userContextMenu, &UserContextMenu::execDetails);
connect(m_userInfoPopup, &UserInfoPopup::showGamesRequested, userContextMenu, &UserContextMenu::execShowGames);
connect(m_userInfoPopup, &UserInfoPopup::addBuddyRequested, userContextMenu, &UserContextMenu::execAddToBuddy);
connect(m_userInfoPopup, &UserInfoPopup::removeBuddyRequested, userContextMenu,
&UserContextMenu::execRemoveFromBuddy);
connect(m_userInfoPopup, &UserInfoPopup::addIgnoreRequested, userContextMenu, &UserContextMenu::execAddToIgnore);
connect(m_userInfoPopup, &UserInfoPopup::removeIgnoreRequested, userContextMenu,
&UserContextMenu::execRemoveFromIgnore);
connect(m_userInfoPopup, &UserInfoPopup::banRequested, userContextMenu, &UserContextMenu::execBan);
connect(m_userInfoPopup, &UserInfoPopup::warnRequested, userContextMenu, &UserContextMenu::execWarn);
connect(m_userInfoPopup, &UserInfoPopup::banHistoryRequested, userContextMenu, &UserContextMenu::execBanHistory);
connect(m_userInfoPopup, &UserInfoPopup::warnHistoryRequested, userContextMenu, &UserContextMenu::execWarnHistory);
connect(m_userInfoPopup, &UserInfoPopup::adminNotesRequested, userContextMenu, &UserContextMenu::execAdminNotes);
connect(m_userInfoPopup, &UserInfoPopup::promoteToModRequested, this,
[this](const QString &n) { userContextMenu->execAdjustMod(n, true, false); });
connect(m_userInfoPopup, &UserInfoPopup::demoteFromModRequested, this,
[this](const QString &n) { userContextMenu->execAdjustMod(n, false, false); });
connect(m_userInfoPopup, &UserInfoPopup::promoteToJudgeRequested, this,
[this](const QString &n) { userContextMenu->execAdjustMod(n, false, true); });
connect(m_userInfoPopup, &UserInfoPopup::demoteFromJudgeRequested, this,
[this](const QString &n) { userContextMenu->execAdjustMod(n, false, false); });
}
bool UserListWidget::eventFilter(QObject *obj, QEvent *event)
{
if (obj == userTree->viewport()) {
if (event->type() == QEvent::MouseMove) {
if (!SettingsCache::instance().getStyleUserList()) {
return QGroupBox::eventFilter(obj, event);
}
auto *me = static_cast<QMouseEvent *>(event);
auto *twi = static_cast<UserListTWI *>(userTree->itemAt(me->pos()));
const QString hovName = twi ? QString::fromStdString(twi->getUserInfo().name()) : QString{};
if (hovName != m_hoveredUser) {
m_hoveredUser = hovName;
if (!hovName.isEmpty()) {
m_hidePopupTimer->stop();
if (!m_popupPinned) {
m_showPopupTimer->start();
}
} else {
m_showPopupTimer->stop();
if (!m_popupPinned) {
m_hidePopupTimer->start();
}
}
}
} else if (event->type() == QEvent::Leave) {
m_hoveredUser.clear();
m_showPopupTimer->stop();
if (!m_popupPinned) {
m_hidePopupTimer->start();
}
}
}
return QGroupBox::eventFilter(obj, event);
}
void UserListWidget::showPopupForUser(const QString &userName)
{
UserListTWI *item = users.value(userName);
if (!item) {
return;
}
const ServerInfo_User &info = item->getUserInfo();
const bool online = item->data(0, UserListRoles::Online).toBool();
const bool isBuddy = userContextMenu->getUserListProxy()->isUserBuddy(userName);
const bool isIgn = userContextMenu->getUserListProxy()->isUserIgnored(userName);
m_userInfoPopup->showForUser(userName, info, online, isBuddy, isIgn);
// Realize the native window at opacity 0 before positioning so that:
// 1) move() applies to an existing native handle (not overridden by Qt's
// default centering logic on first show)
// 2) adjustSize() inside positionPopup() can measure the final laid-out
// geometry correctly
m_userInfoPopup->setWindowOpacity(0.0);
m_userInfoPopup->show();
m_userInfoPopup->raise();
positionPopup(userName); // geometry is now accurate; move() sticks
auto *fade = new QPropertyAnimation(m_userInfoPopup, "windowOpacity", m_userInfoPopup);
fade->setDuration(120);
fade->setStartValue(0.0);
fade->setEndValue(1.0);
fade->start(QAbstractAnimation::DeleteWhenStopped);
}
void UserListWidget::positionPopup(const QString &userName)
{
UserListTWI *item = users.value(userName);
if (!item) {
return;
}
QWidget *vp = userTree->viewport();
const QRect itemR = userTree->visualItemRect(item);
const QPoint itemTL = vp->mapToGlobal(itemR.topLeft());
const QPoint vpTL = vp->mapToGlobal(vp->rect().topLeft());
const QPoint vpTR = vp->mapToGlobal(vp->rect().topRight());
m_userInfoPopup->adjustSize();
const int popW = m_userInfoPopup->width();
const int popH = m_userInfoPopup->height();
const int margin = 12;
const QRect screen = QGuiApplication::primaryScreen()->availableGeometry();
// ── X: prefer the side with more space ───────────────────────────────────
const int spaceLeft = vpTL.x() - screen.left() - margin;
const int spaceRight = screen.right() - vpTR.x() - margin;
int x;
if (spaceLeft >= spaceRight) {
x = (spaceLeft >= popW) ? (vpTL.x() - margin - popW) : (vpTR.x() + margin);
} else {
x = (spaceRight >= popW) ? (vpTR.x() + margin) : (vpTL.x() - margin - popW);
}
x = qBound(screen.left() + margin, x, screen.right() - popW - margin);
// ── Y: grow down if there's room, otherwise grow up ───────────────────────
const int itemTopY = itemTL.y();
const int spaceBelow = screen.bottom() - itemTopY - margin;
const int spaceAbove = itemTopY - screen.top() - margin;
int y;
if (spaceBelow >= popH) {
y = itemTopY; // top edges align, popup grows downward
} else if (spaceAbove >= popH) {
y = itemTopY - popH; // bottom of popup meets top of item, grows upward
} else {
// Neither side fits cleanly — pick the roomier side and let clamp handle the rest
y = (spaceBelow >= spaceAbove) ? itemTopY : (itemTopY - popH);
}
y = qBound(screen.top() + margin, y, screen.bottom() - popH - margin);
m_userInfoPopup->move(x, y);
}
void UserListWidget::hidePopup(bool immediate)
{
m_showPopupTimer->stop();
m_hidePopupTimer->stop();
if (!m_userInfoPopup->isVisible()) {
return;
}
if (immediate) {
m_userInfoPopup->hide();
return;
}
// Fade out
auto *fade = new QPropertyAnimation(m_userInfoPopup, "windowOpacity", m_userInfoPopup);
fade->setDuration(100);
fade->setStartValue(m_userInfoPopup->windowOpacity());
fade->setEndValue(0.0);
connect(fade, &QPropertyAnimation::finished, m_userInfoPopup, &QWidget::hide);
fade->start(QAbstractAnimation::DeleteWhenStopped);
}
void UserListWidget::retranslateUi()
{
userContextMenu->retranslateUi();
@ -457,9 +873,60 @@ void UserListWidget::retranslateUi()
updateCount();
}
void UserListWidget::rebuild()
{
userTree->clear();
users.clear();
cardArtParamsMap.clear();
onlineCount = 0;
if (!manager) {
return;
}
const QMap<QString, ServerInfo_User> *source = nullptr;
switch (type) {
case AllUsersList:
case RoomList:
source = &manager->getAllUsersList();
break;
case BuddyList:
source = &manager->getBuddyList();
break;
case IgnoreList:
source = &manager->getIgnoreList();
break;
}
for (auto it = source->cbegin(); it != source->cend(); ++it) {
processUserInfo(it.value(), manager->getOnlineUser(it.key()) != nullptr);
}
sortItems();
}
void UserListWidget::processUserInfo(const ServerInfo_User &user, bool online)
{
const QString userName = QString::fromStdString(user.name());
// Always update params from the latest ServerInfo_User, whether the
// item is new or existing, so a live server-push refreshes the rendering.
if (user.has_card_art_params()) {
const auto &cap = user.card_art_params();
CardArtParams params;
params.cardName = QString::fromStdString(cap.card_name());
params.cardProviderId = QString::fromStdString(cap.card_provider_id());
params.marginPctL = cap.margin_pct_l();
params.marginPctR = cap.margin_pct_r();
params.verticalOffset = cap.vertical_offset();
params.zoom = cap.zoom();
cardArtParamsMap.insert(userName, params);
cardArtProvider->requestCardArt(userName, params.cardName, params.cardProviderId);
} else {
cardArtParamsMap.remove(userName); // clear stale params on removal
}
UserListTWI *item = users.value(userName);
if (item) {
item->setUserInfo(user);
@ -471,25 +938,28 @@ void UserListWidget::processUserInfo(const ServerInfo_User &user, bool online)
++onlineCount;
}
updateCount();
avatarProvider->requestAvatar(userName);
}
item->setOnline(online);
sortItems();
userTree->viewport()->update();
}
bool UserListWidget::deleteUser(const QString &userName)
{
UserListTWI *twi = users.value(userName);
if (twi) {
users.remove(userName);
userTree->takeTopLevelItem(userTree->indexOfTopLevelItem(twi));
if (twi->data(0, Qt::UserRole + 1).toBool()) {
--onlineCount;
}
delete twi;
updateCount();
return true;
if (!twi) {
return false;
}
return false;
users.remove(userName);
userTree->takeTopLevelItem(userTree->indexOfTopLevelItem(twi));
if (twi->data(0, Qt::UserRole + 1).toBool()) {
--onlineCount;
}
delete twi;
updateCount();
return true;
}
void UserListWidget::setUserOnline(const QString &userName, bool online)
@ -533,5 +1003,5 @@ void UserListWidget::showContextMenu(const QPoint &pos, const QModelIndex &index
void UserListWidget::sortItems()
{
userTree->sortItems(1, Qt::AscendingOrder);
userTree->sortItems(0, Qt::AscendingOrder);
}

View file

@ -7,9 +7,17 @@
#ifndef USERLIST_H
#define USERLIST_H
#include "../../cards/card_info_picture_art_crop_widget.h"
#include "user_avatar_provider.h"
#include "user_card_art_provider.h"
#include "user_info_popup.h"
#include "user_list_manager.h"
#include "user_list_painter.h"
#include <QComboBox>
#include <QDialog>
#include <QGroupBox>
#include <QQueue>
#include <QStyledItemDelegate>
#include <QTextEdit>
#include <QTreeWidgetItem>
@ -94,12 +102,21 @@ public:
class UserListItemDelegate : public QStyledItemDelegate
{
const QMap<QString, QPixmap> *avatarCache;
const QMap<QString, QPixmap> *cardArtCache;
const QMap<QString, CardArtParams> *cardArtParamsMap;
public:
explicit UserListItemDelegate(QObject *const parent);
explicit UserListItemDelegate(QObject *const parent,
const QMap<QString, QPixmap> *avatarCache,
const QMap<QString, QPixmap> *cardArtCache,
const QMap<QString, CardArtParams> *cardArtParamsMap);
bool editorEvent(QEvent *event,
QAbstractItemModel *model,
const QStyleOptionViewItem &option,
const QModelIndex &index) override;
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
};
class UserListTWI : public QTreeWidgetItem
@ -131,6 +148,22 @@ public:
};
private:
UserListManager *manager = nullptr;
UserAvatarProvider *avatarProvider = nullptr;
UserCardArtProvider *cardArtProvider = nullptr;
QMap<QString, CardArtParams> cardArtParamsMap;
// ── Hover popup ───────────────────────────────────────────────────────────
UserInfoPopup *m_userInfoPopup = nullptr;
QTimer *m_showPopupTimer = nullptr;
QTimer *m_hidePopupTimer = nullptr;
QString m_hoveredUser;
bool m_popupPinned = false;
void showPopupForUser(const QString &userName);
void hidePopup(bool immediate = false);
void positionPopup(const QString &userName);
void connectPopupSignals();
QMap<QString, UserListTWI *> users;
TabSupervisor *tabSupervisor;
AbstractClient *client;
@ -141,6 +174,7 @@ private:
int onlineCount;
QString titleStr;
void updateCount();
void refreshPopupButtons(const QString &userName);
private slots:
void userClicked(QTreeWidgetItem *item, int column);
signals:
@ -149,13 +183,18 @@ signals:
void removeBuddy(const QString &userName);
void addIgnore(const QString &userName);
void removeIgnore(const QString &userName);
void joinGameRequested(int gameId, int roomId, bool asSpectator);
public:
UserListWidget(TabSupervisor *_tabSupervisor,
AbstractClient *_client,
UserListType _type,
QWidget *parent = nullptr);
void bind(UserListManager *mgr);
void applyDisplayMode();
bool eventFilter(QObject *obj, QEvent *event) override;
void retranslateUi();
void rebuild();
void processUserInfo(const ServerInfo_User &user, bool online);
bool deleteUser(const QString &userName);
void setUserOnline(const QString &userName, bool online);
@ -165,6 +204,9 @@ public:
}
void showContextMenu(const QPoint &pos, const QModelIndex &index);
void sortItems();
protected:
void hideEvent(QHideEvent *e) override;
};
#endif

View file

@ -111,6 +111,15 @@ AppearanceSettingsPage::AppearanceSettingsPage()
homeTabGroupBox = new QGroupBox;
homeTabGroupBox->setLayout(homeTabGrid);
styleUserListCheckBox.setChecked(settings.getStyleUserList());
connect(&styleUserListCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings, &SettingsCache::setStyleUserList);
auto stylingTabGrid = new QGridLayout;
stylingTabGrid->addWidget(&styleUserListCheckBox, 0, 0, 1, 2);
stylingGroupBox = new QGroupBox;
stylingGroupBox->setLayout(stylingTabGrid);
// Menu settings
showShortcutsCheckBox.setChecked(settings.getShowShortcuts());
connect(&showShortcutsCheckBox, &QCheckBox::QT_STATE_CHANGED, this, &AppearanceSettingsPage::showShortcutsChanged);
@ -284,6 +293,7 @@ AppearanceSettingsPage::AppearanceSettingsPage()
auto *mainLayout = new QVBoxLayout;
mainLayout->addWidget(themeGroupBox);
mainLayout->addWidget(homeTabGroupBox);
mainLayout->addWidget(stylingGroupBox);
mainLayout->addWidget(menuGroupBox);
mainLayout->addWidget(printingsGroupBox);
mainLayout->addWidget(cardsGroupBox);
@ -398,6 +408,9 @@ void AppearanceSettingsPage::retranslateUi()
homeTabBackgroundShuffleFrequencySpinBox.setSpecialValueText(tr("Disabled"));
homeTabDisplayCardNameCheckBox.setText(tr("Display card name of background in bottom right"));
stylingGroupBox->setTitle(tr("Styling settings"));
styleUserListCheckBox.setText(tr("Style user list"));
menuGroupBox->setTitle(tr("Menu settings"));
showShortcutsCheckBox.setText(tr("Show keyboard shortcuts in right-click menus"));
showGameSelectorFilterToolbarCheckBox.setText(tr("Show game filter toolbar above list in room tab"));

View file

@ -37,6 +37,7 @@ private:
QLabel homeTabBackgroundShuffleFrequencyLabel;
QSpinBox homeTabBackgroundShuffleFrequencySpinBox;
QCheckBox homeTabDisplayCardNameCheckBox;
QCheckBox styleUserListCheckBox;
QLabel minPlayersForMultiColumnLayoutLabel;
QLabel maxFontSizeForCardsLabel;
QCheckBox showShortcutsCheckBox;
@ -58,6 +59,7 @@ private:
QCheckBox invertVerticalCoordinateCheckBox;
QGroupBox *themeGroupBox;
QGroupBox *homeTabGroupBox;
QGroupBox *stylingGroupBox;
QGroupBox *menuGroupBox;
QGroupBox *printingsGroupBox;
QGroupBox *cardsGroupBox;

View file

@ -6,6 +6,7 @@
#include <QGridLayout>
#include <QLineEdit>
#include <QToolBar>
#include <libcockatrice/utility/string_limits.h>
MessagesSettingsPage::MessagesSettingsPage()
{
@ -22,10 +23,14 @@ MessagesSettingsPage::MessagesSettingsPage()
ignoreUnregUsersMainChat.setChecked(SettingsCache::instance().getIgnoreUnregisteredUsers());
ignoreUnregUserMessages.setChecked(SettingsCache::instance().getIgnoreUnregisteredUserMessages());
ignoreNonBuddyUserMessages.setChecked(SettingsCache::instance().getIgnoreNonBuddyUserMessages());
connect(&ignoreUnregUsersMainChat, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(),
&SettingsCache::setIgnoreUnregisteredUsers);
connect(&ignoreUnregUserMessages, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(),
&SettingsCache::setIgnoreUnregisteredUserMessages);
connect(&ignoreNonBuddyUserMessages, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(),
&SettingsCache::setIgnoreNonBuddyUserMessages);
invertMentionForeground.setChecked(SettingsCache::instance().getChatMentionForeground());
connect(&invertMentionForeground, &QCheckBox::QT_STATE_CHANGED, this, &MessagesSettingsPage::updateTextColor);
@ -62,9 +67,10 @@ MessagesSettingsPage::MessagesSettingsPage()
chatGrid->addWidget(&ignoreUnregUsersMainChat, 2, 0);
chatGrid->addWidget(&hexLabel, 1, 2);
chatGrid->addWidget(&ignoreUnregUserMessages, 3, 0);
chatGrid->addWidget(&messagePopups, 4, 0);
chatGrid->addWidget(&mentionPopups, 5, 0);
chatGrid->addWidget(&roomHistory, 6, 0);
chatGrid->addWidget(&ignoreNonBuddyUserMessages, 4, 0);
chatGrid->addWidget(&messagePopups, 5, 0);
chatGrid->addWidget(&mentionPopups, 6, 0);
chatGrid->addWidget(&roomHistory, 7, 0);
chatGroupBox = new QGroupBox;
chatGroupBox->setLayout(chatGrid);
@ -237,6 +243,7 @@ void MessagesSettingsPage::retranslateUi()
QString("<a href='%1'>%2</a>").arg(WIKI_CUSTOM_SHORTCUTS).arg(tr("How to use in-game message macros")));
ignoreUnregUsersMainChat.setText(tr("Ignore chat room messages sent by unregistered users"));
ignoreUnregUserMessages.setText(tr("Ignore private messages sent by unregistered users"));
ignoreNonBuddyUserMessages.setText(tr("Ignore private messages sent by non-buddy users"));
invertMentionForeground.setText(tr("Invert text color"));
invertHighlightForeground.setText(tr("Invert text color"));
messagePopups.setText(tr("Enable desktop notifications for private messages"));

View file

@ -36,6 +36,7 @@ private:
QCheckBox invertHighlightForeground;
QCheckBox ignoreUnregUsersMainChat;
QCheckBox ignoreUnregUserMessages;
QCheckBox ignoreNonBuddyUserMessages;
QCheckBox messagePopups;
QCheckBox mentionPopups;
QCheckBox roomHistory;

View file

@ -68,10 +68,18 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage()
connect(&showTotalSelectionCountCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(),
&SettingsCache::setShowTotalSelectionCount);
showSubtypeSelectionTallyCheckBox.setChecked(SettingsCache::instance().getShowSubtypeSelectionTally());
connect(&showSubtypeSelectionTallyCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(),
&SettingsCache::setShowSubtypeSelectionTally);
useTearOffMenusCheckBox.setChecked(SettingsCache::instance().getUseTearOffMenus());
connect(&useTearOffMenusCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(),
[](const QT_STATE_CHANGED_T state) { SettingsCache::instance().setUseTearOffMenus(state == Qt::Checked); });
keepGameChatFocusCheckBox.setChecked(SettingsCache::instance().getKeepGameChatFocus());
connect(&keepGameChatFocusCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(),
&SettingsCache::setKeepGameChatFocus);
auto *generalGrid = new QGridLayout;
generalGrid->addWidget(&doubleClickToPlayCheckBox, 0, 0);
generalGrid->addWidget(&clickPlaysAllSelectedCheckBox, 1, 0);
@ -82,7 +90,9 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage()
generalGrid->addWidget(&annotateTokensCheckBox, 6, 0);
generalGrid->addWidget(&showDragSelectionCountCheckBox, 7, 0);
generalGrid->addWidget(&showTotalSelectionCountCheckBox, 8, 0);
generalGrid->addWidget(&useTearOffMenusCheckBox, 9, 0);
generalGrid->addWidget(&showSubtypeSelectionTallyCheckBox, 9, 0);
generalGrid->addWidget(&useTearOffMenusCheckBox, 10, 0);
generalGrid->addWidget(&keepGameChatFocusCheckBox, 11, 0);
generalGroupBox = new QGroupBox;
generalGroupBox->setLayout(generalGrid);
@ -204,9 +214,13 @@ void UserInterfaceSettingsPage::retranslateUi()
closeEmptyCardViewCheckBox.setText(tr("Close card view window when last card is removed"));
focusCardViewSearchBarCheckBox.setText(tr("Auto focus search bar when card view window is opened"));
annotateTokensCheckBox.setText(tr("Annotate card text on tokens"));
showDragSelectionCountCheckBox.setText(tr("Show selection counter during drag selection"));
showTotalSelectionCountCheckBox.setText(tr("Show total selection counter"));
showDragSelectionCountCheckBox.setText(tr("Show selection count during drag selection"));
showTotalSelectionCountCheckBox.setText(tr("Show total selection count"));
showSubtypeSelectionTallyCheckBox.setText(tr("Show subtype breakdown in selection tally"));
useTearOffMenusCheckBox.setText(tr("Use tear-off menus, allowing right click menus to persist on screen"));
keepGameChatFocusCheckBox.setText(
tr("Keep game chat focused when clicking in game (Note: disables card view search bar)"));
notificationsGroupBox->setTitle(tr("Notifications settings"));
notificationsEnabledCheckBox.setText(tr("Enable notifications in taskbar"));
specNotificationsEnabledCheckBox.setText(tr("Notify in the taskbar for game events while you are spectating"));

View file

@ -29,7 +29,9 @@ private:
QCheckBox annotateTokensCheckBox;
QCheckBox showDragSelectionCountCheckBox;
QCheckBox showTotalSelectionCountCheckBox;
QCheckBox showSubtypeSelectionTallyCheckBox;
QCheckBox useTearOffMenusCheckBox;
QCheckBox keepGameChatFocusCheckBox;
QCheckBox tapAnimationCheckBox;
QCheckBox openDeckInNewTabCheckBox;
QLabel visualDeckStoragePromptForConversionLabel;

View file

@ -43,7 +43,7 @@
#include <libcockatrice/protocol/pb/command_deck_upload.pb.h>
#include <libcockatrice/protocol/pb/response.pb.h>
#include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/utility/trice_limits.h>
#include <libcockatrice/utility/string_limits.h>
/**
* @brief Constructs the AbstractTabDeckEditor.
@ -56,6 +56,9 @@ AbstractTabDeckEditor::AbstractTabDeckEditor(TabSupervisor *_tabSupervisor) : Ta
deckStateManager = new DeckStateManager(this);
databaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), true, this);
databaseModel->setObjectName("databaseModel");
cardDatabaseDockWidget = new DeckEditorCardDatabaseDockWidget(this);
deckDockWidget = new DeckEditorDeckDockWidget(this);
cardInfoDockWidget = new DeckEditorCardInfoDockWidget(this);
@ -105,16 +108,17 @@ void AbstractTabDeckEditor::registerDockWidget(QMenu *_viewMenu, QDockWidget *wi
dockToActions.insert(widget, {menu, aVisible, aFloating, defaultSize});
}
/**
* @brief Updates the card info dock and printing selector.
* @param card The card to display.
*/
void AbstractTabDeckEditor::updateCard(const ExactCard &card)
{
cardInfoDockWidget->updateCard(card);
printingSelectorDockWidget->printingSelector->setCard(card.getCardPtr());
}
void AbstractTabDeckEditor::updateCardInfo(const ExactCard &card)
{
cardInfoDockWidget->updateCard(card);
}
/** @brief Placeholder: called when the deck changes. */
void AbstractTabDeckEditor::onDeckChanged()
{
@ -129,47 +133,14 @@ void AbstractTabDeckEditor::onDeckModified()
emit tabTextChanged(this, getTabText());
}
/**
* @brief Helper for adding a card to a deck zone.
* @param card Card to add.
* @param zoneName Zone to add the card to.
*/
void AbstractTabDeckEditor::addCardHelper(const ExactCard &card, const QString &zoneName)
void AbstractTabDeckEditor::addCard(const ExactCard &card, const QString &zoneName)
{
deckStateManager->addCard(card, zoneName);
}
/**
* @brief Adds a card to the main deck or sideboard depending on Ctrl key.
*/
void AbstractTabDeckEditor::actAddCard(const ExactCard &card)
void AbstractTabDeckEditor::decrementCard(const ExactCard &card, const QString &zoneName)
{
if (QApplication::keyboardModifiers() & Qt::ControlModifier) {
actAddCardToSideboard(card);
} else {
addCardHelper(card, DECK_ZONE_MAIN);
}
deckMenu->setSaveStatus(true);
}
/** @brief Adds a card to the sideboard explicitly. */
void AbstractTabDeckEditor::actAddCardToSideboard(const ExactCard &card)
{
addCardHelper(card, DECK_ZONE_SIDE);
deckMenu->setSaveStatus(true);
}
/** @brief Decrements a card from the main deck. */
void AbstractTabDeckEditor::actDecrementCard(const ExactCard &card)
{
deckStateManager->decrementCard(card, DECK_ZONE_MAIN);
}
/** @brief Decrements a card from the sideboard. */
void AbstractTabDeckEditor::actDecrementCardFromSideboard(const ExactCard &card)
{
deckStateManager->decrementCard(card, DECK_ZONE_SIDE);
deckStateManager->decrementCard(card, zoneName);
}
/**
@ -571,14 +542,14 @@ void AbstractTabDeckEditor::actExportDeckDecklistXyz()
/** @brief Analyzes the deck using DeckStats. */
void AbstractTabDeckEditor::actAnalyzeDeckDeckstats()
{
auto *interface = new DeckStatsInterface(*cardDatabaseDockWidget->getDatabase(), this);
auto *interface = new DeckStatsInterface(this);
interface->analyzeDeck(deckStateManager->getDeckList());
}
/** @brief Analyzes the deck using TappedOut. */
void AbstractTabDeckEditor::actAnalyzeDeckTappedout()
{
auto *interface = new TappedOutInterface(*cardDatabaseDockWidget->getDatabase(), this);
auto *interface = new TappedOutInterface(this);
interface->analyzeDeck(deckStateManager->getDeckList());
}
@ -621,3 +592,15 @@ bool AbstractTabDeckEditor::closeRequest()
}
return close();
}
void AbstractTabDeckEditor::showPrintingSelector()
{
printingSelectorDockWidget->printingSelector->setCard(cardInfoDockWidget->cardInfo->getCard().getCardPtr());
printingSelectorDockWidget->printingSelector->updateDisplay();
printingSelectorDockWidget->setVisible(true);
}
void AbstractTabDeckEditor::openEdhrecTab(const CardInfoPtr &info, bool isCommander)
{
getTabSupervisor()->addEdhrecTab(info, isCommander);
}

View file

@ -77,8 +77,8 @@ class QAction;
*
* **Key Methods:**
*
* - actAddCard(const ExactCard &card) Adds a card to the deck.
* - actDecrementCard(const ExactCard &card) Removes a single instance of a card from the deck.
* - addCard(const ExactCard &card, const QString &zoneName) Adds a card to the deck.
* - decrementCard(const ExactCard &card, const QString &zoneName) Removes a single instance of a card from the deck.
* - actRemoveCard() Removes the currently selected card from the deck.
* - actSaveDeckAs() Performs a "Save As" action for the deck.
* - updateCard(const ExactCard &card) Updates the currently displayed card info in the dock.
@ -126,6 +126,7 @@ public:
// UI Elements
DeckStateManager *deckStateManager;
CardDatabaseModel *databaseModel; ///< Card database
DeckEditorMenu *deckMenu; ///< Menu for deck operations
DeckEditorCardDatabaseDockWidget *cardDatabaseDockWidget; ///< Database dock
DeckEditorCardInfoDockWidget *cardInfoDockWidget; ///< Card info dock
@ -140,22 +141,35 @@ public slots:
/** @brief Called when the deck is modified. */
virtual void onDeckModified();
/** @brief Updates the card info panel.
* @param card The card to display.
/**
* @brief Updates the card info dock and printing selector.
* @param card The card to display.
*/
void updateCard(const ExactCard &card);
/** @brief Adds a card to the main deck or sideboard based on Ctrl key. */
void actAddCard(const ExactCard &card);
/**
* @brief Updates just the card info dock
* @param card The card to display
*/
void updateCardInfo(const ExactCard &card);
/** @brief Adds a card to the sideboard explicitly. */
void actAddCardToSideboard(const ExactCard &card);
/**
* @brief Adds a card to the given zone
* @param card Card to add.
* @param zoneName Zone to add the card to.
*/
void addCard(const ExactCard &card, const QString &zoneName);
/** @brief Decrements a card from the main deck. */
void actDecrementCard(const ExactCard &card);
/** @brief Decrements a card from the sideboard. */
void actDecrementCardFromSideboard(const ExactCard &card);
/**
* @brief Decrements a card from the given zone
*
* Use an ExactCard with empty PrintingInfo if you want to remove a card by name regardless of printing.
* Otherwise, it won't remove anything unless there's an exact printing match.
*
* @param card Card to decrement.
* @param zoneName Zone to decrement from.
*/
void decrementCard(const ExactCard &card, const QString &zoneName);
/** @brief Opens a recently opened deck file. */
void actOpenRecent(const QString &fileName);
@ -166,8 +180,15 @@ public slots:
/** @brief Requests closing the tab. */
bool closeRequest() override;
/** @brief Shows the printing selector dock. Pure virtual. */
virtual void showPrintingSelector() = 0;
/** @brief Shows the printing selector dock and updates it with the current card. */
void showPrintingSelector();
/**
* @brief Opens an EDHRec tab for the given card
* @param info The card
* @param isCommander The type of search
*/
void openEdhrecTab(const CardInfoPtr &info, bool isCommander);
signals:
/** @brief Emitted when a deck should be opened in a new editor tab. */
@ -293,9 +314,6 @@ protected:
*/
QMessageBox *createSaveConfirmationWindow();
/** @brief Helper function to add a card to a specific deck zone. */
void addCardHelper(const ExactCard &card, const QString &zoneName);
/** @brief Opens a deck from a file. */
virtual void openDeckFromFile(const QString &fileName, DeckOpenLocation deckOpenLocation);

View file

@ -17,7 +17,7 @@
#include <libcockatrice/protocol/pb/response_list_users.pb.h>
#include <libcockatrice/protocol/pb/session_commands.pb.h>
#include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/utility/trice_limits.h>
#include <libcockatrice/utility/string_limits.h>
TabAccount::TabAccount(TabSupervisor *_tabSupervisor, AbstractClient *_client, const ServerInfo_User &userInfo)
: Tab(_tabSupervisor), client(_client)

View file

@ -13,7 +13,7 @@
#include <libcockatrice/protocol/pb/event_replay_added.pb.h>
#include <libcockatrice/protocol/pb/moderator_commands.pb.h>
#include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/utility/trice_limits.h>
#include <libcockatrice/utility/string_limits.h>
ShutdownDialog::ShutdownDialog(QWidget *parent) : QDialog(parent)
{

View file

@ -0,0 +1,293 @@
#include "tab_card_art_rules.h"
#include "libcockatrice/card/database/card_database_manager.h"
#include <QCompleter>
#include <QFormLayout>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <libcockatrice/network/client/abstract/abstract_client.h>
#include <libcockatrice/protocol/pb/moderator_commands.pb.h>
#include <libcockatrice/protocol/pb/response_card_art_rule_entry.pb.h>
#include <libcockatrice/protocol/pending_command.h>
CardArtRulesModel::CardArtRulesModel(AbstractClient *client, QObject *parent)
: QAbstractTableModel(parent), client(client)
{
}
int CardArtRulesModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid()) {
return 0;
}
return static_cast<int>(entries.size());
}
int CardArtRulesModel::columnCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return 4;
}
QVariant CardArtRulesModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid()) {
return {};
}
const auto &e = entries.at(index.row());
if (role == Qt::DisplayRole) {
switch (index.column()) {
case 0:
return e.cardName;
case 1:
return e.cardProviderId;
case 2:
return e.mode;
case 3:
return e.reason;
}
}
return {};
}
QVariant CardArtRulesModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if (orientation != Qt::Horizontal || role != Qt::DisplayRole) {
return {};
}
switch (section) {
case 0:
return tr("Card");
case 1:
return tr("ProviderId");
case 2:
return tr("Mode");
case 3:
return tr("Reason");
default:
return {};
}
}
void CardArtRulesModel::refresh()
{
Command_ListCardArtRules cmd;
PendingCommand *pend = client->prepareModeratorCommand(cmd);
connect(pend, &PendingCommand::finished, this, &CardArtRulesModel::onRefreshFinished);
client->sendCommand(pend);
}
void CardArtRulesModel::clear()
{
beginResetModel();
entries.clear();
endResetModel();
}
QString CardArtRulesModel::cardAt(int row) const
{
if (row < 0 || row >= static_cast<int>(entries.size())) {
return {};
}
return entries[row].cardName;
}
const CardArtRulesModel::Entry *CardArtRulesModel::entryAt(int row) const
{
if (row < 0 || row >= static_cast<int>(entries.size())) {
return nullptr;
}
return &entries[row];
}
void CardArtRulesModel::onRefreshFinished(const Response &r)
{
if (r.response_code() != Response::RespOk) {
return;
}
const auto &resp = r.GetExtension(Response_ListCardArtRules::ext);
beginResetModel();
entries.clear();
for (const auto &e : resp.entries()) {
entries.push_back({QString::fromStdString(e.card_name()), QString::fromStdString(e.card_provider_id()),
QString::fromStdString(e.mode()), QString::fromStdString(e.reason())});
}
endResetModel();
}
TabCardArtRules::TabCardArtRules(TabSupervisor *parent, AbstractClient *_client) : Tab(parent), client(_client)
{
setupUi();
refresh();
}
void TabCardArtRules::setupUi()
{
auto *central = new QWidget(this);
initSearchBar();
providerComboBox = new QComboBox;
modeBox = new QComboBox;
reasonEdit = new QLineEdit;
addBtn = new QPushButton;
removeBtn = new QPushButton;
refreshBtn = new QPushButton;
modeBox->addItems({"ALLOW", "DENY"});
tableModel = new CardArtRulesModel(client, this);
table = new QTableView;
table->setModel(tableModel);
table->setSelectionBehavior(QAbstractItemView::SelectRows);
table->setSelectionMode(QAbstractItemView::SingleSelection);
auto *form = new QFormLayout;
form->addRow(tr("Card:"), searchEdit);
form->addRow(tr("ProviderId:"), providerComboBox);
form->addRow(tr("Mode:"), modeBox);
form->addRow(tr("Reason:"), reasonEdit);
auto *buttons = new QHBoxLayout;
buttons->addWidget(addBtn);
buttons->addWidget(removeBtn);
buttons->addWidget(refreshBtn);
auto *layout = new QVBoxLayout;
layout->addLayout(form);
layout->addLayout(buttons);
layout->addWidget(table);
central->setLayout(layout);
setCentralWidget(central);
connect(addBtn, &QPushButton::clicked, this, &TabCardArtRules::addRule);
connect(removeBtn, &QPushButton::clicked, this, &TabCardArtRules::removeSelected);
connect(refreshBtn, &QPushButton::clicked, this, &TabCardArtRules::refresh);
retranslateUi();
}
void TabCardArtRules::initSearchBar()
{
searchEdit = new QLineEdit;
searchEdit->setPlaceholderText(tr("Type a card name..."));
cardDbModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this);
cardDbDisplayModel = new CardDatabaseDisplayModel(this);
cardDbDisplayModel->setSourceModel(cardDbModel);
cardSearchModel = new CardSearchModel(cardDbDisplayModel, this);
cardProxyModel = new CardCompleterProxyModel(this);
cardProxyModel->setSourceModel(cardSearchModel);
cardProxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
searchCompleter = new QCompleter(cardProxyModel, this);
searchCompleter->setCompletionRole(Qt::DisplayRole);
searchCompleter->setCompletionMode(QCompleter::PopupCompletion);
searchCompleter->setCaseSensitivity(Qt::CaseInsensitive);
searchCompleter->setFilterMode(Qt::MatchContains);
searchCompleter->setMaxVisibleItems(15);
searchEdit->setCompleter(searchCompleter);
connect(searchEdit, &QLineEdit::textEdited, cardSearchModel, &CardSearchModel::updateSearchResults);
connect(searchEdit, &QLineEdit::textEdited, this, [this](const QString &text) {
const QString pattern = ".*" + QRegularExpression::escape(text) + ".*";
cardProxyModel->setFilterRegularExpression(
QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption));
if (!text.isEmpty()) {
searchCompleter->complete();
}
});
connect(searchCompleter, static_cast<void (QCompleter::*)(const QString &)>(&QCompleter::activated), this,
[this](const QString &name) { searchEdit->setText(name); });
connect(searchEdit, &QLineEdit::editingFinished, this,
[this]() { populateProviderCombo(searchEdit->text().trimmed()); });
}
void TabCardArtRules::populateProviderCombo(const QString &cardName)
{
providerComboBox->clear();
auto card = CardDatabaseManager::query()->getCard({cardName});
const auto &sets = card.getInfo().getSets();
for (const auto &printings : sets) {
for (const auto &p : printings) {
QString setName = p.getSet()->getLongName();
QString collector = p.getProperty("num");
QString uuid = p.getUuid();
QString label = setName;
if (!collector.isEmpty()) {
label += " #" + collector;
}
providerComboBox->addItem(label, uuid);
}
}
}
void TabCardArtRules::retranslateUi()
{
addBtn->setText(tr("Add rule"));
removeBtn->setText(tr("Remove rule"));
refreshBtn->setText(tr("Refresh"));
}
void TabCardArtRules::refresh()
{
tableModel->refresh();
}
void TabCardArtRules::addRule()
{
Command_AddCardArtRule cmd;
cmd.set_card_name(searchEdit->text().toStdString());
cmd.set_card_provider_id(providerComboBox->currentData().toString().toStdString());
cmd.set_mode(modeBox->currentText().toStdString());
cmd.set_reason(reasonEdit->text().toStdString());
client->sendCommand(client->prepareModeratorCommand(cmd));
refresh();
}
void TabCardArtRules::removeSelected()
{
QModelIndex idx = table->currentIndex();
if (!idx.isValid()) {
return;
}
Command_RemoveCardArtRule cmd;
const auto e = tableModel->entryAt(idx.row());
cmd.set_card_name(e->cardName.toStdString());
cmd.set_card_provider_id(e->cardProviderId.toStdString());
client->sendCommand(client->prepareModeratorCommand(cmd));
refresh();
}

View file

@ -0,0 +1,93 @@
#ifndef COCKATRICE_DLG_CARD_ART_RULES_H
#define COCKATRICE_DLG_CARD_ART_RULES_H
#include "card/card_search_model.h"
#include "tab_supervisor.h"
#include <QAbstractTableModel>
#include <QComboBox>
#include <QLineEdit>
#include <QPushButton>
#include <QTableView>
class AbstractClient;
class CardArtRulesModel : public QAbstractTableModel
{
Q_OBJECT
public:
struct Entry
{
QString cardName;
QString cardProviderId;
QString mode;
QString reason;
};
explicit CardArtRulesModel(AbstractClient *client, QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
void refresh();
void clear();
QString cardAt(int row) const;
const Entry *entryAt(int row) const;
private slots:
void onRefreshFinished(const Response &r);
private:
AbstractClient *client;
std::vector<Entry> entries;
};
class TabCardArtRules : public Tab
{
Q_OBJECT
public:
TabCardArtRules(TabSupervisor *parent, AbstractClient *client);
QString getTabText() const override
{
return tr("Card Art Rules");
}
void retranslateUi() override;
private:
void setupUi();
private slots:
void addRule();
void removeSelected();
void refresh();
private:
AbstractClient *client;
QLineEdit *searchEdit;
void initSearchBar();
void populateProviderCombo(const QString &cardName);
QCompleter *searchCompleter;
CardDatabaseModel *cardDbModel;
CardDatabaseDisplayModel *cardDbDisplayModel;
CardSearchModel *cardSearchModel;
CardCompleterProxyModel *cardProxyModel;
QComboBox *providerComboBox;
QComboBox *modeBox;
QLineEdit *reasonEdit;
QPushButton *addBtn;
QPushButton *removeBtn;
QPushButton *refreshBtn;
QTableView *table;
CardArtRulesModel *tableModel;
};
#endif // COCKATRICE_DLG_CARD_ART_RULES_H

View file

@ -21,7 +21,6 @@
#include <libcockatrice/models/database/card_database_model.h>
#include <libcockatrice/network/client/abstract/abstract_client.h>
#include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/utility/trice_limits.h>
/**
* @brief Constructs a new TabDeckEditor object.
@ -120,16 +119,6 @@ void TabDeckEditor::refreshShortcuts()
aResetLayout->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aResetLayout"));
}
/**
* @brief Displays the printing selector dock with the current card.
*/
void TabDeckEditor::showPrintingSelector()
{
printingSelectorDockWidget->printingSelector->setCard(cardInfoDockWidget->cardInfo->getCard().getCardPtr());
printingSelectorDockWidget->printingSelector->updateDisplay();
printingSelectorDockWidget->setVisible(true);
}
/**
* @brief Loads deck editor layout from settings or resets to default.
*/

View file

@ -83,10 +83,6 @@ public:
/** @brief Creates menus for deck editing and view options. */
void createMenus() override;
public slots:
/** @brief Shows the printing selector dock and updates it with current card. */
void showPrintingSelector() override;
};
#endif

View file

@ -28,6 +28,7 @@
#include <libcockatrice/protocol/pb/response_deck_download.pb.h>
#include <libcockatrice/protocol/pb/response_deck_upload.pb.h>
#include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/utility/string_limits.h>
TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor,
AbstractClient *_client,

View file

@ -1,18 +1,21 @@
#include "tab_game.h"
#include "../../../client/settings/cache_settings.h"
#include "../game/board/arrow_item.h"
#include "../game/board/card_item.h"
#include "../game/deckview/deck_view_container.h"
#include "../game/deckview/tabbed_deck_view_container.h"
#include "../game/game.h"
#include "../game/game_scene.h"
#include "../game/game_view.h"
#include "../game/log/message_log_widget.h"
#include "../game/phases_toolbar.h"
#include "../game/player/player_list_widget.h"
#include "../game/player/player_logic.h"
#include "../game/replay.h"
#include "../game_graphics/board/arrow_item.h"
#include "../game_graphics/board/card_item.h"
#include "../game_graphics/deckview/deck_view_container.h"
#include "../game_graphics/deckview/tabbed_deck_view_container.h"
#include "../game_graphics/game_scene.h"
#include "../game_graphics/game_view.h"
#include "../game_graphics/log/message_log_widget.h"
#include "../game_graphics/phases_toolbar.h"
#include "../game_graphics/player/menu/card_menu.h"
#include "../game_graphics/player/menu/player_menu.h"
#include "../game_graphics/player/player_graphics_item.h"
#include "../game_graphics/player/player_list_widget.h"
#include "../interface/card_picture_loader/card_picture_loader.h"
#include "../interface/widgets/cards/card_info_frame_widget.h"
#include "../interface/widgets/dialogs/dlg_create_game.h"
@ -41,13 +44,13 @@
#include <libcockatrice/protocol/pb/game_replay.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_player.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
#include <libcockatrice/utility/trice_limits.h>
#include <libcockatrice/utility/string_limits.h>
TabGame::TabGame(TabSupervisor *_tabSupervisor, GameReplay *_replay)
: Tab(_tabSupervisor), sayLabel(nullptr), sayEdit(nullptr)
{
// THIS CTOR IS USED ON REPLAY
game = new Replay(this, _replay);
game = new Replay(this, _replay, tabSupervisor->getIsLocalGame());
createCardInfoDock(true);
createPlayerListDock(true);
@ -91,7 +94,7 @@ TabGame::TabGame(TabSupervisor *_tabSupervisor,
: Tab(_tabSupervisor), userListProxy(_tabSupervisor->getUserListManager())
{
// THIS CTOR IS USED ON GAMES
game = new Game(this, _clients, event, _roomGameTypes);
game = new Game(this, tabSupervisor->getIsLocalGame(), _clients, event, _roomGameTypes);
createCardInfoDock();
createPlayerListDock();
@ -363,11 +366,10 @@ void TabGame::retranslateUi()
cardInfoFrameWidget->retranslateUi();
QMapIterator<int, PlayerLogic *> i(game->getPlayerManager()->getPlayers());
while (i.hasNext()) {
i.next().value()->getGraphicsItem()->retranslateUi();
for (auto playerView : scene->getPlayers().values()) {
playerView->retranslateUi();
}
QMapIterator<int, TabbedDeckViewContainer *> j(deckViewContainers);
while (j.hasNext()) {
j.next().value()->playerDeckView->retranslateUi();
@ -608,7 +610,7 @@ void TabGame::actRemoveLocalArrows()
{
auto *local = game->getPlayerManager()->getActiveLocalPlayer(game->getGameState()->getActivePlayer());
if (local) {
scene->requestClearArrowsForPlayer(local->getPlayerInfo()->getId());
scene->clearArrowsForPlayer(local->getPlayerInfo()->getId());
}
}
@ -654,8 +656,12 @@ PlayerLogic *TabGame::addPlayer(PlayerLogic *newPlayer)
scene->addPlayer(newPlayer);
auto *view = scene->viewForPlayer(newPlayer->getPlayerInfo()->getId());
connect(newPlayer, &PlayerLogic::newCardAdded, this, &TabGame::newCardAdded);
connect(newPlayer->getPlayerMenu(), &PlayerMenu::cardMenuUpdated, this, &TabGame::setCardMenu);
connect(newPlayer, &PlayerLogic::openDeckEditor, this, &TabGame::openDeckEditor);
connect(view->getPlayerMenu(), &PlayerMenu::cardMenuUpdated, this, &TabGame::setCardMenu);
connect(view, &PlayerGraphicsItem::cardInfoRequested, this, &TabGame::viewCardInfo);
messageLog->connectToPlayerEventHandler(newPlayer->getPlayerEventHandler());
@ -668,7 +674,7 @@ PlayerLogic *TabGame::addPlayer(PlayerLogic *newPlayer)
addLocalPlayer(newPlayer, newPlayer->getPlayerInfo()->getId());
}
gameMenu->insertMenu(playersSeparator, newPlayer->getPlayerMenu()->getPlayerMenu());
gameMenu->insertMenu(playersSeparator, view->getPlayerMenu()->getPlayerMenu());
createZoneForPlayer(newPlayer, newPlayer->getPlayerInfo()->getId());
@ -678,7 +684,7 @@ PlayerLogic *TabGame::addPlayer(PlayerLogic *newPlayer)
void TabGame::addLocalPlayer(PlayerLogic *newPlayer, int playerId)
{
if (game->getGameState()->getClients().size() == 1) {
newPlayer->getPlayerMenu()->setShortcutsActive();
scene->viewForPlayer(playerId)->getPlayerMenu()->setShortcutsActive();
}
auto *deckView = new TabbedDeckViewContainer(playerId, this);
@ -698,27 +704,24 @@ void TabGame::addLocalPlayer(PlayerLogic *newPlayer, int playerId)
void TabGame::processPlayerLeave(PlayerLogic *leavingPlayer)
{
QString playerName = "@" + leavingPlayer->getPlayerInfo()->getName();
removePlayerFromAutoCompleteList(playerName);
scene->removePlayer(leavingPlayer);
removePlayerFromAutoCompleteList("@" + leavingPlayer->getPlayerInfo()->getName());
// When we inserted the playerMenu into the gameMenu earlier, Qt wrapped the playerMenu into a QAction*, which lives
// independently and does not get cleaned up when the source menu gets destroyed. We have to manually clean here.
if (leavingPlayer->getPlayerMenu()) {
QMenu *menu = leavingPlayer->getPlayerMenu()->getPlayerMenu();
if (menu) {
// Find and remove the QAction pointing to this menu
QList<QAction *> actions = gameMenu->actions();
for (QAction *act : actions) {
if (act->menu() == menu) {
gameMenu->removeAction(act);
delete act; // deletes the QAction wrapper around the submenu
break;
}
auto *view = scene->viewForPlayer(leavingPlayer->getPlayerInfo()->getId());
if (view) {
// Find and remove the QAction pointing to this menu
QMenu *menu = view->getPlayerMenu()->getPlayerMenu();
for (QAction *act : gameMenu->actions()) {
if (act->menu() == menu) {
gameMenu->removeAction(act);
delete act;
break;
}
}
}
scene->removePlayer(leavingPlayer);
}
void TabGame::processRemotePlayerDeckSelect(QString deckList, int playerId, QString playerName)
@ -869,12 +872,12 @@ PlayerLogic *TabGame::setActivePlayer(int id)
if (i.value() == player) {
i.value()->setActive(true);
if (game->getGameState()->getClients().size() > 1) {
i.value()->getPlayerMenu()->setShortcutsActive();
scene->viewForPlayer(i.value()->getPlayerInfo()->getId())->getPlayerMenu()->setShortcutsActive();
}
} else {
i.value()->setActive(false);
if (game->getGameState()->getClients().size() > 1) {
i.value()->getPlayerMenu()->setShortcutsInactive();
scene->viewForPlayer(i.value()->getPlayerInfo()->getId())->getPlayerMenu()->setShortcutsInactive();
}
}
}
@ -890,16 +893,16 @@ void TabGame::setActivePhase(int phase)
void TabGame::newCardAdded(AbstractCardItem *card)
{
connect(card, &AbstractCardItem::rightClicked, scene, &GameScene::onCardRightClicked);
connect(card, &AbstractCardItem::playSelected, scene, &GameScene::playSelected);
connect(card, &AbstractCardItem::playSelectedFaceDown, scene, &GameScene::playSelectedFaceDown);
connect(card, &AbstractCardItem::hideSelected, scene, &GameScene::hideSelected);
connect(card, &AbstractCardItem::hovered, cardInfoFrameWidget,
qOverload<AbstractCardItem *>(&CardInfoFrameWidget::setCard));
connect(card, &AbstractCardItem::selectionChanged, scene, &GameScene::onCardSelectionChanged);
connect(card, &AbstractCardItem::showCardInfoPopup, this, &TabGame::showCardInfoPopup);
connect(card, SIGNAL(deleteCardInfoPopup(QString)), this, SLOT(deleteCardInfoPopup(QString)));
connect(card, &AbstractCardItem::cardShiftClicked, this, &TabGame::linkCardToChat);
CardItem *cardItem = qobject_cast<CardItem *>(card);
if (cardItem) {
connect(cardItem->getState(), &CardState::zoneChanged, scene,
[this, cardItem]() { scene->onCardZoneChanged(cardItem, false); });
}
}
QString TabGame::getTabText() const
@ -940,7 +943,7 @@ QString TabGame::getTabText() const
/**
* @param menu The menu to set. Pass in nullptr to set the menu to empty.
*/
void TabGame::setCardMenu(QMenu *menu)
void TabGame::setCardMenu(CardMenu *menu)
{
if (!aCardMenu) {
return;
@ -1174,6 +1177,11 @@ void TabGame::createReplayDock(GameReplay *replay)
QDockWidget::DockWidgetMovable);
replayDock->setWidget(replayManager);
replayDock->setFloating(false);
connect(replayManager, &ReplayManager::eventReplayed, game->getGameEventHandler(),
[this](const auto &event, auto options) {
game->getGameEventHandler()->processGameEventContainer(event, nullptr, options);
});
}
void TabGame::createDeckViewContainerWidget(bool bReplay)

View file

@ -10,8 +10,8 @@
#define TAB_GAME_H
#include "../game/abstract_game.h"
#include "../game/log/message_log_widget.h"
#include "../game/player/player_logic.h"
#include "../game_graphics/log/message_log_widget.h"
#include "../interface/widgets/menus/tearoff_menu.h"
#include "../interface/widgets/replay/replay_manager.h"
#include "tab.h"
@ -20,6 +20,7 @@
#include <QLoggingCategory>
#include <QMap>
class CardMenu;
class ServerInfo_PlayerProperties;
class TabbedDeckViewContainer;
inline Q_LOGGING_CATEGORY(TabGameLog, "tab_game");
@ -141,7 +142,7 @@ signals:
private slots:
void adminLockChanged(bool lock);
void newCardAdded(AbstractCardItem *card);
void setCardMenu(QMenu *menu);
void setCardMenu(CardMenu *menu);
void actGameInfo();
void actConcede();

View file

@ -17,7 +17,7 @@
#include <libcockatrice/protocol/pb/moderator_commands.pb.h>
#include <libcockatrice/protocol/pb/response_viewlog_history.pb.h>
#include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/utility/trice_limits.h>
#include <libcockatrice/utility/string_limits.h>
TabLog::TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client) : Tab(_tabSupervisor), client(_client)
{

View file

@ -17,7 +17,7 @@
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
#include <libcockatrice/protocol/pb/session_commands.pb.h>
#include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/utility/trice_limits.h>
#include <libcockatrice/utility/string_limits.h>
TabMessage::TabMessage(TabSupervisor *_tabSupervisor,
AbstractClient *_client,

View file

@ -31,7 +31,7 @@
#include <libcockatrice/protocol/pb/room_commands.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_room.pb.h>
#include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/utility/trice_limits.h>
#include <libcockatrice/utility/string_limits.h>
TabRoom::TabRoom(TabSupervisor *_tabSupervisor,
AbstractClient *_client,
@ -49,10 +49,25 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor,
QMap<int, GameTypeMap> tempMap;
tempMap.insert(info.room_id(), gameTypes);
gameSelector = new GameSelector(client, tabSupervisor, this, QMap<int, QString>(), tempMap, true, true);
auto *tabs = new QTabWidget(this);
friendsList = new UserListWidget(tabSupervisor, client, UserListWidget::BuddyList);
friendsList->bind(tabSupervisor->getUserListManager());
userList = new UserListWidget(tabSupervisor, client, UserListWidget::RoomList);
userList->bind(tabSupervisor->getUserListManager());
ignoreList = new UserListWidget(tabSupervisor, client, UserListWidget::IgnoreList);
ignoreList->bind(tabSupervisor->getUserListManager());
connect(friendsList, SIGNAL(openMessageDialog(const QString &, bool)), this,
SIGNAL(openMessageDialog(const QString &, bool)));
connect(userList, SIGNAL(openMessageDialog(const QString &, bool)), this,
SIGNAL(openMessageDialog(const QString &, bool)));
tabs->addTab(friendsList, tr("Friends"));
tabs->addTab(userList, tr("Online"));
tabs->addTab(ignoreList, tr("Ignored"));
chatView = new ChatView(tabSupervisor, nullptr, true, this);
connect(chatView, &ChatView::showMentionPopup, this, &TabRoom::actShowMentionPopup);
connect(chatView, &ChatView::messageClickedSignal, this, &TabRoom::focusTab);
@ -101,7 +116,7 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor,
auto *hbox = new QHBoxLayout;
hbox->addWidget(splitter, 3);
hbox->addWidget(userList, 1);
hbox->addWidget(tabs, 1);
aLeaveRoom = new QAction(this);
connect(aLeaveRoom, &QAction::triggered, this, &TabRoom::closeRequest);
@ -112,10 +127,8 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor,
const int userListSize = info.user_list_size();
for (int i = 0; i < userListSize; ++i) {
userList->processUserInfo(info.user_list(i), true);
autocompleteUserList.append("@" + QString::fromStdString(info.user_list(i).name()));
}
userList->sortItems();
const int gameListSize = info.game_list_size();
for (int i = 0; i < gameListSize; ++i) {
@ -269,8 +282,6 @@ void TabRoom::processListGamesEvent(const Event_ListGames &event)
void TabRoom::processJoinRoomEvent(const Event_JoinRoom &event)
{
userList->processUserInfo(event.user_info(), true);
userList->sortItems();
if (!autocompleteUserList.contains("@" + QString::fromStdString(event.user_info().name()))) {
autocompleteUserList << "@" + QString::fromStdString(event.user_info().name());
sayEdit->setCompletionList(autocompleteUserList);
@ -279,7 +290,6 @@ void TabRoom::processJoinRoomEvent(const Event_JoinRoom &event)
void TabRoom::processLeaveRoomEvent(const Event_LeaveRoom &event)
{
userList->deleteUser(QString::fromStdString(event.name()));
autocompleteUserList.removeOne("@" + QString::fromStdString(event.name()));
sayEdit->setCompletionList(autocompleteUserList);
}

View file

@ -56,7 +56,9 @@ private:
QMap<int, QString> gameTypes;
GameSelector *gameSelector;
UserListWidget *friendsList;
UserListWidget *userList;
UserListWidget *ignoreList;
const UserListProxy *userListProxy;
ChatView *chatView;
QLabel *sayLabel;

View file

@ -9,6 +9,7 @@
#include "api/edhrec/tab_edhrec_main.h"
#include "tab_account.h"
#include "tab_admin.h"
#include "tab_card_art_rules.h"
#include "tab_deck_editor.h"
#include "tab_deck_storage.h"
#include "tab_game.h"
@ -157,6 +158,10 @@ TabSupervisor::TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget *
aTabAdmin->setCheckable(true);
connect(aTabAdmin, &QAction::triggered, this, &TabSupervisor::actTabAdmin);
aTabCardArtRules = new QAction(this);
aTabCardArtRules->setCheckable(true);
connect(aTabCardArtRules, &QAction::triggered, this, &TabSupervisor::actTabCardArtRules);
aTabLog = new QAction(this);
aTabLog->setCheckable(true);
connect(aTabLog, &QAction::triggered, this, &TabSupervisor::actTabLog);
@ -413,6 +418,7 @@ void TabSupervisor::start(const ServerInfo_User &_userInfo)
tabsMenu->addSeparator();
tabsMenu->addAction(aTabAdmin);
tabsMenu->addAction(aTabLog);
tabsMenu->addAction(aTabCardArtRules);
if (SettingsCache::instance().getTabAdminOpen()) {
openTabAdmin();
@ -420,6 +426,7 @@ void TabSupervisor::start(const ServerInfo_User &_userInfo)
if (SettingsCache::instance().getTabLogOpen()) {
openTabLog();
}
openTabCardArtRules();
}
retranslateUi();
@ -659,6 +666,30 @@ void TabSupervisor::openTabAdmin()
aTabAdmin->setChecked(true);
}
void TabSupervisor::actTabCardArtRules(bool checked)
{
if (checked && !tabCardArtRules) {
openTabCardArtRules();
setCurrentWidget(tabCardArtRules);
} else if (!checked && tabCardArtRules) {
tabCardArtRules->closeRequest();
}
}
void TabSupervisor::openTabCardArtRules()
{
tabCardArtRules = new TabCardArtRules(this, client);
myAddTab(tabCardArtRules, aTabCardArtRules);
connect(tabCardArtRules, &QObject::destroyed, this, [this] {
tabCardArtRules = nullptr;
aTabCardArtRules->setChecked(false);
});
aTabCardArtRules->setChecked(true);
}
void TabSupervisor::actTabLog(bool checked)
{
SettingsCache::instance().setTabLogOpen(checked);
@ -997,6 +1028,12 @@ void TabSupervisor::processUserMessageEvent(const Event_UserMessage &event)
!userLevel.testFlag(ServerInfo_User::IsRegistered)) {
// Flags are additive, so reg/mod/admin are all IsRegistered
return;
} else if (SettingsCache::instance().getIgnoreNonBuddyUserMessages() &&
!userListManager->isUserBuddy(senderName) && !userLevel.testFlag(ServerInfo_User::IsModerator) &&
!userLevel.testFlag(ServerInfo_User::IsAdmin)) {
// Ignore private messages from non-buddies
// Moderator/Admin messages are exempt to ensure warnings reach users
return;
}
}
tab = addMessageTab(QString::fromStdString(event.sender_name()), false);

View file

@ -23,6 +23,7 @@
#include <QMap>
#include <QTabWidget>
class TabCardArtRules;
inline Q_LOGGING_CATEGORY(TabSupervisorLog, "tab_supervisor");
class UserListManager;
@ -91,6 +92,7 @@ private:
TabDeckStorage *tabDeckStorage;
TabReplays *tabReplays;
TabAdmin *tabAdmin;
TabCardArtRules *tabCardArtRules;
TabLog *tabLog;
QMap<int, TabRoom *> roomTabs;
QMap<int, TabGame *> gameTabs;
@ -100,7 +102,8 @@ private:
bool isLocalGame;
QAction *aTabHome, *aTabDeckEditor, *aTabVisualDeckEditor, *aTabEdhRec, *aTabArchidekt, *aTabVisualDeckStorage,
*aTabVisualDatabaseDisplay, *aTabServer, *aTabAccount, *aTabDeckStorage, *aTabReplays, *aTabAdmin, *aTabLog;
*aTabVisualDatabaseDisplay, *aTabServer, *aTabAccount, *aTabDeckStorage, *aTabReplays, *aTabAdmin,
*aTabCardArtRules, *aTabLog;
int myAddTab(Tab *tab, QAction *manager = nullptr);
void addCloseButtonToTab(Tab *tab, int tabIndex, QAction *manager);
@ -133,7 +136,7 @@ public:
return userInfo;
}
[[nodiscard]] AbstractClient *getClient() const;
[[nodiscard]] const UserListManager *getUserListManager() const
[[nodiscard]] UserListManager *getUserListManager() const
{
return userListManager;
}
@ -185,6 +188,8 @@ private slots:
void openTabDeckStorage();
void openTabReplays();
void openTabAdmin();
void actTabCardArtRules(bool checked);
void openTabCardArtRules();
void openTabLog();
void updateCurrent(int index);

View file

@ -1,14 +1,19 @@
#include "tab_visual_database_display.h"
#include "tab_deck_editor.h"
#include "tab_supervisor.h"
#include <libcockatrice/card/database/card_database_manager.h>
TabVisualDatabaseDisplay::TabVisualDatabaseDisplay(TabSupervisor *_tabSupervisor) : Tab(_tabSupervisor)
{
deckEditor = new TabDeckEditor(_tabSupervisor);
deckEditor->setHidden(true);
visualDatabaseDisplayWidget = new VisualDatabaseDisplayWidget(
this, deckEditor, deckEditor->cardDatabaseDockWidget->databaseDisplayWidget->databaseModel,
deckEditor->cardDatabaseDockWidget->databaseDisplayWidget->databaseDisplayModel);
auto databaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), true, this);
databaseModel->setObjectName("databaseModel");
visualDatabaseDisplayWidget = new VisualDatabaseDisplayWidget(this, databaseModel);
connect(visualDatabaseDisplayWidget, &VisualDatabaseDisplayWidget::edhrecRequested, this,
&TabVisualDatabaseDisplay::openEdhrecTab);
setCentralWidget(visualDatabaseDisplayWidget);
@ -18,3 +23,8 @@ TabVisualDatabaseDisplay::TabVisualDatabaseDisplay(TabSupervisor *_tabSupervisor
void TabVisualDatabaseDisplay::retranslateUi()
{
}
void TabVisualDatabaseDisplay::openEdhrecTab(const CardInfoPtr &info, bool isCommander) const
{
getTabSupervisor()->addEdhrecTab(info, isCommander);
}

View file

@ -15,9 +15,11 @@ class TabVisualDatabaseDisplay : public Tab
Q_OBJECT
private:
TabDeckEditor *deckEditor;
VisualDatabaseDisplayWidget *visualDatabaseDisplayWidget;
private slots:
void openEdhrecTab(const CardInfoPtr &info, bool isCommander) const;
public:
TabVisualDatabaseDisplay(TabSupervisor *_tabSupervisor);
void retranslateUi() override;

View file

@ -1,6 +1,7 @@
#include "tab_deck_editor_visual.h"
#include "../../../../client/settings/cache_settings.h"
#include "../../cards/card_info_display_widget.h"
#include "../../deck_editor/deck_state_manager.h"
#include "../../filters/filter_builder.h"
#include "../../interface/pixel_map_generator.h"
@ -25,6 +26,7 @@
#include <QTimer>
#include <QTreeView>
#include <QVBoxLayout>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/models/deck_list/deck_list_model.h>
#include <libcockatrice/protocol/pb/command_deck_upload.pb.h>
#include <libcockatrice/protocol/pending_command.h>
@ -63,9 +65,10 @@ void TabDeckEditorVisual::createCentralFrame()
centralFrame = new QVBoxLayout;
centralWidget->setLayout(centralFrame);
tabContainer = new TabDeckEditorVisualTabWidget(
centralWidget, this, deckStateManager->getModel(), cardDatabaseDockWidget->databaseDisplayWidget->databaseModel,
cardDatabaseDockWidget->databaseDisplayWidget->databaseDisplayModel);
auto databaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), true, this);
databaseModel->setObjectName("databaseModel");
tabContainer = new TabDeckEditorVisualTabWidget(centralWidget, this, deckStateManager->getModel(), databaseModel);
connect(tabContainer, &TabDeckEditorVisualTabWidget::cardChanged, this,
&TabDeckEditorVisual::changeModelIndexAndCardInfo);
@ -74,7 +77,14 @@ void TabDeckEditorVisual::createCentralFrame()
connect(tabContainer, &TabDeckEditorVisualTabWidget::cardClicked, this,
&TabDeckEditorVisual::processMainboardCardClick);
connect(tabContainer, &TabDeckEditorVisualTabWidget::cardClickedDatabaseDisplay, this,
&TabDeckEditorVisual::processCardClickDatabaseDisplay);
&TabDeckEditorVisual::processDatabaseCardClick);
connect(tabContainer, &TabDeckEditorVisualTabWidget::cardAdded, this, &TabDeckEditorVisual::addCard);
connect(tabContainer, &TabDeckEditorVisualTabWidget::cardDecremented, this, &TabDeckEditorVisual::decrementCard);
connect(tabContainer, &TabDeckEditorVisualTabWidget::edhrecRequested, this, &TabDeckEditorVisual::openEdhrecTab);
connect(tabContainer, &TabDeckEditorVisualTabWidget::printingSelectorRequested, this,
&TabDeckEditorVisual::showPrintingSelector);
connect(tabContainer, &TabDeckEditorVisualTabWidget::cardInfoRequested, this, &TabDeckEditorVisual::updateCardInfo);
centralFrame->addWidget(tabContainer);
setCentralWidget(centralWidget);
@ -143,12 +153,10 @@ void TabDeckEditorVisual::changeModelIndexToCard(const ExactCard &activeCard)
}
}
void TabDeckEditorVisual::processMainboardCardClick(QMouseEvent *event,
CardInfoPictureWithTextOverlayWidget *instance,
void TabDeckEditorVisual::processMainboardCardClick(const QMouseEvent *event,
const ExactCard &card,
const QString &zoneName)
{
auto card = instance->getCard();
// Get the model index for the card
QModelIndex idx = deckStateManager->getModel()->findCard(card.getName(), zoneName);
if (!idx.isValid()) {
@ -168,22 +176,14 @@ void TabDeckEditorVisual::processMainboardCardClick(QMouseEvent *event,
// Alt + Right-click = decrement
if (event->button() == Qt::RightButton && event->modifiers().testFlag(Qt::AltModifier)) {
if (zoneName == DECK_ZONE_MAIN) {
actDecrementCard(card);
} else {
actDecrementCardFromSideboard(card);
}
decrementCard(card, zoneName);
// Keep selection intact.
return;
}
// Alt + Left click = increment
if (event->button() == Qt::LeftButton && event->modifiers().testFlag(Qt::AltModifier)) {
if (zoneName == DECK_ZONE_MAIN) {
actAddCard(card);
} else {
actAddCardToSideboard(card);
}
addCard(card, zoneName);
// Keep selection intact.
return;
}
@ -219,13 +219,16 @@ void TabDeckEditorVisual::processMainboardCardClick(QMouseEvent *event,
}
/** @brief Handles clicks on cards in the database display. */
void TabDeckEditorVisual::processCardClickDatabaseDisplay(QMouseEvent *event,
CardInfoPictureWithTextOverlayWidget *instance)
void TabDeckEditorVisual::processDatabaseCardClick(const QMouseEvent *event, const ExactCard &card)
{
if (event->button() == Qt::LeftButton) {
actAddCard(instance->getCard());
if (QApplication::keyboardModifiers() & Qt::ControlModifier) {
addCard(card, DECK_ZONE_SIDE);
} else {
addCard(card, DECK_ZONE_MAIN);
}
} else if (event->button() == Qt::RightButton) {
actDecrementCard(instance->getCard());
decrementCard(card, DECK_ZONE_MAIN);
} else if (event->button() == Qt::MiddleButton) {
deckDockWidget->actRemoveCard();
}
@ -240,14 +243,6 @@ bool TabDeckEditorVisual::actSaveDeckAs()
return result;
}
/** @brief Shows the printing selector dock and updates it with the current card. */
void TabDeckEditorVisual::showPrintingSelector()
{
printingSelectorDockWidget->printingSelector->setCard(cardInfoDockWidget->cardInfo->getCard().getCardPtr());
printingSelectorDockWidget->printingSelector->updateDisplay();
printingSelectorDockWidget->setVisible(true);
}
/** @brief Refreshes keyboard shortcuts for this tab from settings. */
void TabDeckEditorVisual::refreshShortcuts()
{

View file

@ -41,7 +41,7 @@
* - changeModelIndexAndCardInfo(const ExactCard &card) Updates deck model selection and card info.
* - changeModelIndexToCard(const ExactCard &card) Selects the card in the deck view.
* - processMainboardCardClick(QMouseEvent *event, ...) Handles clicks on mainboard cards.
* - processCardClickDatabaseDisplay(QMouseEvent *event, ...) Handles clicks on database cards.
* - processDatabaseCardClick(QMouseEvent *event, ...) Handles clicks on database cards.
* - actSaveDeckAs() Overrides save action with temporary UI adjustments.
* - showPrintingSelector() Opens the printing selector dock for the current card.
* - freeDocksSize() Frees constraints on dock widget sizes.
@ -144,27 +144,20 @@ public slots:
*/
void onDeckChanged() override;
/**
* @brief Show the printing selector dock for the currently active card.
*/
void showPrintingSelector() override;
/**
* @brief Handle card clicks in the mainboard visual deck.
* @param event Mouse event triggering the action.
* @param instance Widget representing the clicked card.
* @param card The clicked card.
* @param zoneName Deck zone of the card.
*/
void processMainboardCardClick(QMouseEvent *event,
CardInfoPictureWithTextOverlayWidget *instance,
const QString &zoneName);
void processMainboardCardClick(const QMouseEvent *event, const ExactCard &card, const QString &zoneName);
/**
* @brief Handle card clicks in the database visual display.
* @param event Mouse event triggering the action.
* @param instance Widget representing the clicked card.
* @param card The clicked card.
*/
void processCardClickDatabaseDisplay(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance);
void processDatabaseCardClick(const QMouseEvent *event, const ExactCard &card);
/**
* @brief Save the deck under a new name.

View file

@ -9,7 +9,6 @@
* @param _deckEditor Pointer to the associated deck editor.
* @param _deckModel Pointer to the deck list model.
* @param _cardDatabaseModel Pointer to the card database model.
* @param _cardDatabaseDisplayModel Pointer to the card database display model.
*
* Initializes all sub-widgets (visual deck view, database display, deck analytics,
* sample hand) and sets up the tab layout and signal connections.
@ -17,10 +16,8 @@
TabDeckEditorVisualTabWidget::TabDeckEditorVisualTabWidget(QWidget *parent,
AbstractTabDeckEditor *_deckEditor,
DeckListModel *_deckModel,
CardDatabaseModel *_cardDatabaseModel,
CardDatabaseDisplayModel *_cardDatabaseDisplayModel)
: QTabWidget(parent), deckEditor(_deckEditor), deckModel(_deckModel), cardDatabaseModel(_cardDatabaseModel),
cardDatabaseDisplayModel(_cardDatabaseDisplayModel)
CardDatabaseModel *_cardDatabaseModel)
: QTabWidget(parent), deckEditor(_deckEditor), deckModel(_deckModel), cardDatabaseModel(_cardDatabaseModel)
{
this->setTabsClosable(true); // Enable tab closing
connect(this, &QTabWidget::tabCloseRequested, this, &TabDeckEditorVisualTabWidget::handleTabClose);
@ -34,16 +31,25 @@ TabDeckEditorVisualTabWidget::TabDeckEditorVisualTabWidget(QWidget *parent,
&TabDeckEditorVisualTabWidget::onCardChanged);
connect(visualDeckView, &VisualDeckEditorWidget::cardClicked, this,
&TabDeckEditorVisualTabWidget::onCardClickedDeckEditor);
connect(visualDeckView, &VisualDeckEditorWidget::cardAdditionRequested, deckEditor,
&AbstractTabDeckEditor::actAddCard);
connect(visualDeckView, &VisualDeckEditorWidget::cardAdditionRequested, this,
&TabDeckEditorVisualTabWidget::actAddCard);
visualDatabaseDisplay =
new VisualDatabaseDisplayWidget(this, deckEditor, _cardDatabaseModel, _cardDatabaseDisplayModel);
visualDatabaseDisplay = new VisualDatabaseDisplayWidget(this, _cardDatabaseModel, deckModel);
visualDatabaseDisplay->setObjectName("visualDatabaseView");
connect(visualDatabaseDisplay, &VisualDatabaseDisplayWidget::cardHoveredDatabaseDisplay, this,
&TabDeckEditorVisualTabWidget::onCardChangedDatabaseDisplay);
connect(visualDatabaseDisplay, &VisualDatabaseDisplayWidget::cardClickedDatabaseDisplay, this,
&TabDeckEditorVisualTabWidget::onCardClickedDatabaseDisplay);
connect(visualDatabaseDisplay, &VisualDatabaseDisplayWidget::cardAdded, this,
&TabDeckEditorVisualTabWidget::cardAdded);
connect(visualDatabaseDisplay, &VisualDatabaseDisplayWidget::cardDecremented, this,
&TabDeckEditorVisualTabWidget::cardDecremented);
connect(visualDatabaseDisplay, &VisualDatabaseDisplayWidget::edhrecRequested, this,
&TabDeckEditorVisualTabWidget::edhrecRequested);
connect(visualDatabaseDisplay, &VisualDatabaseDisplayWidget::printingSelectorRequested, this,
&TabDeckEditorVisualTabWidget::printingSelectorRequested);
connect(visualDatabaseDisplay, &VisualDatabaseDisplayWidget::cardInfoRequested, this,
&TabDeckEditorVisualTabWidget::cardInfoRequested);
statsAnalyzer = new DeckListStatisticsAnalyzer(this, deckModel);
statsAnalyzer->analyze();
@ -82,25 +88,24 @@ void TabDeckEditorVisualTabWidget::onCardChangedDatabaseDisplay(const ExactCard
/**
* @brief Emits the cardClicked signal when a card is clicked in the visual deck view.
* @param event The mouse event.
* @param instance The widget instance of the clicked card.
* @param card The clicked card.
* @param zoneName The zone of the deck where the card is located.
*/
void TabDeckEditorVisualTabWidget::onCardClickedDeckEditor(QMouseEvent *event,
CardInfoPictureWithTextOverlayWidget *instance,
QString zoneName)
const ExactCard &card,
const QString &zoneName)
{
emit cardClicked(event, instance, zoneName);
emit cardClicked(event, card, zoneName);
}
/**
* @brief Emits the cardClickedDatabaseDisplay signal when a card is clicked in the database display.
* @param event The mouse event.
* @param instance The widget instance of the clicked card.
* @param card The clicked card.
*/
void TabDeckEditorVisualTabWidget::onCardClickedDatabaseDisplay(QMouseEvent *event,
CardInfoPictureWithTextOverlayWidget *instance)
void TabDeckEditorVisualTabWidget::onCardClickedDatabaseDisplay(QMouseEvent *event, const ExactCard &card)
{
emit cardClickedDatabaseDisplay(event, instance);
emit cardClickedDatabaseDisplay(event, card);
}
/**
@ -166,3 +171,15 @@ void TabDeckEditorVisualTabWidget::handleTabClose(int index)
this->removeTab(index);
delete tab;
}
void TabDeckEditorVisualTabWidget::actAddCard(const ExactCard &card)
{
QString zoneName;
if (QApplication::keyboardModifiers() & Qt::ControlModifier) {
zoneName = DECK_ZONE_SIDE;
} else {
zoneName = DECK_ZONE_MAIN;
}
deckEditor->addCard(card, zoneName);
}

View file

@ -55,13 +55,11 @@ public:
* @param _deckEditor Pointer to the deck editor instance.
* @param _deckModel Deck list model.
* @param _cardDatabaseModel Card database model.
* @param _cardDatabaseDisplayModel Database display model.
*/
explicit TabDeckEditorVisualTabWidget(QWidget *parent,
AbstractTabDeckEditor *_deckEditor,
DeckListModel *_deckModel,
CardDatabaseModel *_cardDatabaseModel,
CardDatabaseDisplayModel *_cardDatabaseDisplayModel);
CardDatabaseModel *_cardDatabaseModel);
/** @brief Add a new tab with a widget and title. */
void addNewTab(QWidget *widget, const QString &title);
@ -101,30 +99,35 @@ public slots:
/**
* @brief Emitted when a card is clicked in the deck view.
* @param event Mouse event.
* @param instance Widget representing the clicked card.
* @param card The clicked card.
* @param zoneName Deck zone of the card.
*/
void onCardClickedDeckEditor(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance, QString zoneName);
void onCardClickedDeckEditor(QMouseEvent *event, const ExactCard &card, const QString &zoneName);
/**
* @brief Emitted when a card is clicked in the database display.
* @param event Mouse event.
* @param instance Widget representing the clicked card.
* @param card The clicked card.
*/
void onCardClickedDatabaseDisplay(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance);
void onCardClickedDatabaseDisplay(QMouseEvent *event, const ExactCard &card);
signals:
void cardChanged(const ExactCard &activeCard);
void cardChangedDatabaseDisplay(const ExactCard &activeCard);
void cardClicked(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance, QString zoneName);
void cardClickedDatabaseDisplay(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance);
void cardClicked(QMouseEvent *event, const ExactCard &card, const QString &zoneName);
void cardClickedDatabaseDisplay(QMouseEvent *event, const ExactCard &card);
void cardAdded(const ExactCard &card, const QString &zoneName);
void cardDecremented(const ExactCard &card, const QString &zoneName);
void edhrecRequested(const CardInfoPtr &cardInfo, bool isCommander);
void printingSelectorRequested();
void cardInfoRequested(const ExactCard &cardName);
private:
QVBoxLayout *layout; ///< Layout for tabs and controls.
AbstractTabDeckEditor *deckEditor; ///< Reference to the deck editor.
DeckListModel *deckModel; ///< Deck list model.
CardDatabaseModel *cardDatabaseModel; ///< Card database model.
CardDatabaseDisplayModel *cardDatabaseDisplayModel; ///< Card database display model.
QVBoxLayout *layout; ///< Layout for tabs and controls.
AbstractTabDeckEditor *deckEditor; ///< Reference to the deck editor.
DeckListModel *deckModel; ///< Deck list model.
CardDatabaseModel *cardDatabaseModel; ///< Card database model.
private slots:
/**
@ -132,6 +135,12 @@ private slots:
* @param index Index of the tab to close.
*/
void handleTabClose(int index);
/**
* @brief Adds card to maindeck or side depending on whether ctrl is held
* @param card
*/
void actAddCard(const ExactCard &card);
};
#endif // TAB_DECK_EDITOR_VISUAL_TAB_WIDGET_H

View file

@ -9,7 +9,7 @@
#include <QLineEdit>
#include <QWidget>
#include <libcockatrice/utility/trice_limits.h>
#include <libcockatrice/utility/string_limits.h>
QString getTextWithMax(QWidget *parent,
const QString &title,

View file

@ -1,12 +1,14 @@
#include "visual_database_display_filter_toolbar_widget.h"
#include "../deck_editor/card_database_view.h"
#include "visual_database_display_widget.h"
#include <QGroupBox>
VisualDatabaseDisplayFilterToolbarWidget::VisualDatabaseDisplayFilterToolbarWidget(VisualDatabaseDisplayWidget *_parent)
VisualDatabaseDisplayFilterToolbarWidget::VisualDatabaseDisplayFilterToolbarWidget(VisualDatabaseDisplayWidget *_parent,
DeckListModel *deckListModel)
: FlowWidget(_parent, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAlwaysOff),
visualDatabaseDisplay(_parent)
visualDatabaseDisplay(_parent), deckListModel(deckListModel)
{
connect(this, &VisualDatabaseDisplayFilterToolbarWidget::searchModelChanged, visualDatabaseDisplay,
&VisualDatabaseDisplayWidget::onSearchModelChanged);
@ -97,8 +99,7 @@ void VisualDatabaseDisplayFilterToolbarWidget::initialize()
auto filterModel = visualDatabaseDisplay->getFilterModel();
saveLoadWidget = new VisualDatabaseDisplayFilterSaveLoadWidget(this, filterModel);
nameFilterWidget =
new VisualDatabaseDisplayNameFilterWidget(this, visualDatabaseDisplay->getDeckEditor(), filterModel);
nameFilterWidget = new VisualDatabaseDisplayNameFilterWidget(this, filterModel, deckListModel);
mainTypeFilterWidget = new VisualDatabaseDisplayMainTypeFilterWidget(this, filterModel);
formatLegalityWidget = new VisualDatabaseDisplayFormatLegalityFilterWidget(this, filterModel);
subTypeFilterWidget = new VisualDatabaseDisplaySubTypeFilterWidget(this, filterModel);

View file

@ -18,12 +18,14 @@ signals:
void searchModelChanged();
public:
explicit VisualDatabaseDisplayFilterToolbarWidget(VisualDatabaseDisplayWidget *parent);
explicit VisualDatabaseDisplayFilterToolbarWidget(VisualDatabaseDisplayWidget *parent,
DeckListModel *deckListModel = nullptr);
void initialize();
void retranslateUi();
private:
VisualDatabaseDisplayWidget *visualDatabaseDisplay;
DeckListModel *deckListModel;
QGroupBox *sortGroupBox;
QLabel *sortByLabel;

View file

@ -8,9 +8,9 @@
#include <QHBoxLayout>
VisualDatabaseDisplayNameFilterWidget::VisualDatabaseDisplayNameFilterWidget(QWidget *parent,
AbstractTabDeckEditor *_deckEditor,
FilterTreeModel *_filterModel)
: QWidget(parent), deckEditor(_deckEditor), filterModel(_filterModel)
FilterTreeModel *_filterModel,
DeckListModel *deckListModel)
: QWidget(parent), filterModel(_filterModel), deckListModel(deckListModel)
{
setMinimumWidth(300);
setMaximumHeight(300);
@ -62,8 +62,6 @@ void VisualDatabaseDisplayNameFilterWidget::retranslateUi()
void VisualDatabaseDisplayNameFilterWidget::actLoadFromDeck()
{
DeckListModel *deckListModel = deckEditor->deckStateManager->getModel();
if (!deckListModel) {
return;
}

View file

@ -21,8 +21,8 @@ class VisualDatabaseDisplayNameFilterWidget : public QWidget
Q_OBJECT
public:
explicit VisualDatabaseDisplayNameFilterWidget(QWidget *parent,
AbstractTabDeckEditor *deckEditor,
FilterTreeModel *filterModel);
FilterTreeModel *filterModel,
DeckListModel *deckListModel = nullptr);
void createNameFilter(const QString &name);
void removeNameFilter(const QString &name);
@ -34,8 +34,8 @@ public slots:
void retranslateUi();
private:
AbstractTabDeckEditor *deckEditor;
FilterTreeModel *filterModel;
DeckListModel *deckListModel;
QVBoxLayout *layout;
QLineEdit *searchBox;
FlowWidget *flowWidget;

View file

@ -5,7 +5,7 @@
#include "../../../filters/syntax_help.h"
#include "../../pixel_map_generator.h"
#include "../cards/card_info_picture_with_text_overlay_widget.h"
#include "../quick_settings/settings_button_widget.h"
#include "../deck_editor/card_database_view.h"
#include "../utility/custom_line_edit.h"
#include "visual_database_display_color_filter_widget.h"
#include "visual_database_display_filter_save_load_widget.h"
@ -23,17 +23,21 @@
#include <utility>
VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent,
AbstractTabDeckEditor *_deckEditor,
CardDatabaseModel *database_model,
CardDatabaseDisplayModel *database_display_model)
: QWidget(parent), deckEditor(_deckEditor), databaseModel(database_model),
databaseDisplayModel(database_display_model)
DeckListModel *deckListModel)
: QWidget(parent)
{
debounceTimer = new QTimer(this);
debounceTimer->setSingleShot(true); // Ensure it only fires once after the timeout
connect(debounceTimer, &QTimer::timeout, this, &VisualDatabaseDisplayWidget::onSearchModelChanged);
// Create display model
databaseDisplayModel = new CardDatabaseDisplayModel(this);
databaseDisplayModel->setObjectName("databaseDisplayModel");
databaseDisplayModel->setSourceModel(database_model);
databaseDisplayModel->setFilterKeyColumn(0);
cards = new QList<ExactCard>;
connect(databaseDisplayModel, &CardDatabaseDisplayModel::modelDirty, this,
&VisualDatabaseDisplayWidget::modelDirty);
@ -60,7 +64,6 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent,
searchEdit->addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition);
auto help = searchEdit->addAction(QPixmap("theme:icons/info"), QLineEdit::TrailingPosition);
connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(searchEdit); });
searchEdit->installEventFilter(&searchKeySignals);
setFocusProxy(searchEdit);
setFocusPolicy(Qt::ClickFocus);
@ -75,43 +78,29 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent,
filterModel = new FilterTreeModel();
filterModel->setObjectName("filterModel");
searchKeySignals.setObjectName("searchKeySignals");
connect(searchEdit, &SearchLineEdit::textChanged, this, &VisualDatabaseDisplayWidget::updateSearch);
connect(searchEdit, &SearchLineEdit::textChanged, databaseDisplayModel, &CardDatabaseDisplayModel::setStringFilter);
DeckEditorDatabaseDisplayWidget *databaseDisplayWidget = deckEditor->cardDatabaseDockWidget->databaseDisplayWidget;
connect(&searchKeySignals, &KeySignals::onEnter, databaseDisplayWidget,
&DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck);
connect(&searchKeySignals, &KeySignals::onCtrlAltEqual, databaseDisplayWidget,
&DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck);
connect(&searchKeySignals, &KeySignals::onCtrlAltRBracket, databaseDisplayWidget,
&DeckEditorDatabaseDisplayWidget::actAddCardToSideboard);
connect(&searchKeySignals, &KeySignals::onCtrlAltMinus, databaseDisplayWidget,
&DeckEditorDatabaseDisplayWidget::actDecrementCardFromMainDeck);
connect(&searchKeySignals, &KeySignals::onCtrlAltLBracket, databaseDisplayWidget,
&DeckEditorDatabaseDisplayWidget::actDecrementCardFromSideboard);
connect(&searchKeySignals, &KeySignals::onCtrlAltEnter, databaseDisplayWidget,
&DeckEditorDatabaseDisplayWidget::actAddCardToSideboard);
connect(&searchKeySignals, &KeySignals::onCtrlEnter, databaseDisplayWidget,
&DeckEditorDatabaseDisplayWidget::actAddCardToSideboard);
connect(&searchKeySignals, &KeySignals::onCtrlC, databaseDisplayWidget,
&DeckEditorDatabaseDisplayWidget::copyDatabaseCellContents);
connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(searchEdit); });
connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::addCardToMainDeck, this,
&VisualDatabaseDisplayWidget::highlightAllSearchEdit);
connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::addCardToSideboard, this,
&VisualDatabaseDisplayWidget::highlightAllSearchEdit);
databaseView = databaseDisplayWidget->getDatabaseView();
databaseView = new CardDatabaseView(this, databaseDisplayModel);
databaseView->setObjectName("databaseView");
databaseView->setFocusProxy(searchEdit);
databaseView->setItemDelegate(nullptr);
databaseView->setVisible(false);
searchEdit->setTreeView(databaseView);
searchEdit->installEventFilter(databaseView->getKeySignals());
connect(databaseView, &CardDatabaseView::cardChanged, this, &VisualDatabaseDisplayWidget::onSelectedCardChanged);
connect(databaseView, &CardDatabaseView::cardAdded, this, &VisualDatabaseDisplayWidget::actAddCard);
connect(databaseView, &CardDatabaseView::cardDecremented, this, &VisualDatabaseDisplayWidget::actDecrementCard);
connect(databaseView, &CardDatabaseView::edhrecClicked, this, &VisualDatabaseDisplayWidget::edhrecRequested);
connect(databaseView, &CardDatabaseView::selectPrintingClicked, this,
&VisualDatabaseDisplayWidget::printingSelectorRequested);
connect(databaseView, &CardDatabaseView::relatedCardClicked, this,
&VisualDatabaseDisplayWidget::onRelatedCardClicked);
colorFilterWidget = new VisualDatabaseDisplayColorFilterWidget(this, filterModel);
filterContainer = new VisualDatabaseDisplayFilterToolbarWidget(this);
filterContainer = new VisualDatabaseDisplayFilterToolbarWidget(this, deckListModel);
clearFilterWidget = new QToolButton();
clearFilterWidget->setFixedSize(32, 32);
@ -216,9 +205,9 @@ void VisualDatabaseDisplayWidget::onDisplayModeChanged(bool checked)
}
}
void VisualDatabaseDisplayWidget::onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance)
void VisualDatabaseDisplayWidget::onClick(QMouseEvent *event, const ExactCard &card)
{
emit cardClickedDatabaseDisplay(event, instance);
emit cardClickedDatabaseDisplay(event, card);
}
void VisualDatabaseDisplayWidget::onHover(const ExactCard &hoveredCard)
@ -226,28 +215,18 @@ void VisualDatabaseDisplayWidget::onHover(const ExactCard &hoveredCard)
emit cardHoveredDatabaseDisplay(hoveredCard);
}
void VisualDatabaseDisplayWidget::addCard(const ExactCard &cardToAdd)
void VisualDatabaseDisplayWidget::addCardToDisplay(const ExactCard &cardToAdd)
{
cards->append(cardToAdd);
auto *display = new CardInfoPictureWithTextOverlayWidget(flowWidget, false);
display->setScaleFactor(cardSizeWidget->getSlider()->value());
display->setCard(cardToAdd);
flowWidget->addWidget(display);
connect(display, &CardInfoPictureWithTextOverlayWidget::imageClicked, this, &VisualDatabaseDisplayWidget::onClick);
connect(display, &CardInfoPictureWithTextOverlayWidget::cardClicked, this, &VisualDatabaseDisplayWidget::onClick);
connect(display, &CardInfoPictureWithTextOverlayWidget::hoveredOnCard, this, &VisualDatabaseDisplayWidget::onHover);
connect(cardSizeWidget->getSlider(), &QSlider::valueChanged, display, &CardInfoPictureWidget::setScaleFactor);
}
void VisualDatabaseDisplayWidget::updateSearch(const QString &search) const
{
databaseDisplayModel->setStringFilter(search);
QModelIndexList sel = databaseView->selectionModel()->selectedRows();
if (sel.isEmpty() && databaseDisplayModel->rowCount()) {
databaseView->selectionModel()->setCurrentIndex(databaseDisplayModel->index(0, 0),
QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows);
}
}
bool VisualDatabaseDisplayWidget::isVisualDisplayMode() const
{
return !displayModeButton->isChecked();
@ -269,6 +248,30 @@ void VisualDatabaseDisplayWidget::onSearchModelChanged()
}
}
void VisualDatabaseDisplayWidget::onSelectedCardChanged(const QString &cardName)
{
emit cardHoveredDatabaseDisplay(CardDatabaseManager::query()->getPreferredCard(cardName));
}
void VisualDatabaseDisplayWidget::actAddCard(const QString &cardName, const QString &zoneName)
{
highlightAllSearchEdit();
ExactCard exactCard = CardDatabaseManager::query()->getPreferredCard(cardName);
emit cardAdded(exactCard, zoneName);
}
void VisualDatabaseDisplayWidget::actDecrementCard(const QString &cardName, const QString &zoneName)
{
ExactCard exactCard = CardDatabaseManager::query()->getPreferredCard(cardName);
emit cardDecremented(exactCard, zoneName);
}
void VisualDatabaseDisplayWidget::onRelatedCardClicked(const QString &relatedCard)
{
ExactCard exactCard = CardDatabaseManager::query()->guessCard({relatedCard});
emit cardInfoRequested(exactCard);
}
bool VisualDatabaseDisplayWidget::nearEndOfPage() const
{
if (!flowWidget->isVisible()) {
@ -335,12 +338,12 @@ void VisualDatabaseDisplayWidget::loadPage(int start, int end)
for (const CardFilter *setFilter : setFilters) {
if (setMap.contains(setFilter->term())) {
for (PrintingInfo printing : setMap[setFilter->term()]) {
addCard(ExactCard(info, printing));
addCardToDisplay(ExactCard(info, printing));
}
}
}
} else {
addCard(CardDatabaseManager::query()->getPreferredCard(info));
addCardToDisplay(CardDatabaseManager::query()->getPreferredCard(info));
}
} else {
qCDebug(VisualDatabaseDisplayLog) << "Card not found in database!";

View file

@ -34,9 +34,8 @@ class VisualDatabaseDisplayWidget : public QWidget
public:
explicit VisualDatabaseDisplayWidget(QWidget *parent,
AbstractTabDeckEditor *deckEditor,
CardDatabaseModel *database_model,
CardDatabaseDisplayModel *database_display_model);
DeckListModel *deckListModel = nullptr);
void retranslateUi();
void adjustCardsPerPage();
@ -47,17 +46,12 @@ public:
void sortCardList(const QStringList &properties, Qt::SortOrder order) const;
void setDeckList(const DeckList &new_deck_list_model);
AbstractTabDeckEditor *getDeckEditor()
{
return deckEditor;
}
CardDatabaseDisplayModel *getDatabaseDisplayModel()
{
return databaseDisplayModel;
}
QTreeView *getDatabaseView()
CardDatabaseView *getDatabaseView()
{
return databaseView;
}
@ -76,19 +70,29 @@ public slots:
void onSearchModelChanged();
signals:
void cardClickedDatabaseDisplay(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance);
void cardClickedDatabaseDisplay(QMouseEvent *event, const ExactCard &card);
void cardHoveredDatabaseDisplay(const ExactCard &hoveredCard);
void cardAdded(const ExactCard &card, const QString &zoneName);
void cardDecremented(const ExactCard &card, const QString &zoneName);
void edhrecRequested(const CardInfoPtr &cardInfo, bool isCommander);
void printingSelectorRequested();
void cardInfoRequested(const ExactCard &cardName);
protected slots:
void initialize();
void onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance);
void onClick(QMouseEvent *event, const ExactCard &card);
void onHover(const ExactCard &hoveredCard);
void addCard(const ExactCard &cardToAdd);
void addCardToDisplay(const ExactCard &cardToAdd);
void databaseDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight);
void modelDirty() const;
void updateSearch(const QString &search) const;
void onDisplayModeChanged(bool checked);
void onSelectedCardChanged(const QString &cardName);
void actAddCard(const QString &cardName, const QString &zoneName);
void actDecrementCard(const QString &cardName, const QString &zoneName);
void onRelatedCardClicked(const QString &relatedCard);
private:
FlowWidget *searchContainer;
SearchLineEdit *searchEdit;
@ -100,11 +104,8 @@ private:
QToolButton *clearFilterWidget;
VisualDatabaseDisplayFilterToolbarWidget *filterContainer;
KeySignals searchKeySignals;
AbstractTabDeckEditor *deckEditor;
CardDatabaseModel *databaseModel;
CardDatabaseDisplayModel *databaseDisplayModel;
QTreeView *databaseView;
CardDatabaseView *databaseView;
QList<ExactCard> *cards;
QVBoxLayout *mainLayout;
QScrollArea *scrollArea;

Some files were not shown because too many files have changed in this diff Show more