mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-06 01:53:25 -08:00
Merge branch 'master' into tooomm-qt5
This commit is contained in:
commit
94b85b8940
266 changed files with 11392 additions and 930 deletions
|
|
@ -85,7 +85,7 @@ void ColorIdentityWidget::resizeEvent(QResizeEvent *event)
|
|||
}
|
||||
lastWidth = totalWidth;
|
||||
|
||||
const int totalHeight = totalWidth / 6; // Set height to 1/4 of the width
|
||||
const int totalHeight = qMax(0, totalWidth / 6); // Set height to 1/4 of the width
|
||||
setFixedHeight(totalHeight);
|
||||
|
||||
const int count = layout->count();
|
||||
|
|
@ -97,6 +97,10 @@ void ColorIdentityWidget::resizeEvent(QResizeEvent *event)
|
|||
const int availableWidth = totalWidth - (spacing * (count - 1));
|
||||
const int iconSize = qMin(availableWidth / count, totalHeight); // Ensure icons fit within the new height
|
||||
|
||||
if (iconSize <= 0) {
|
||||
lastIconSize = iconSize;
|
||||
return;
|
||||
}
|
||||
if (iconSize == lastIconSize) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
#include "deck_color_identity.h"
|
||||
|
||||
#include <QSet>
|
||||
#include <libcockatrice/card/database/card_database_querier.h>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
#include <libcockatrice/deck_list/tree/inner_deck_list_node.h>
|
||||
|
||||
QString getDeckColorIdentity(const DeckList &deck, const CardDatabaseQuerier *db)
|
||||
{
|
||||
const QStringList cardList = deck.getCardList({DECK_ZONE_MAIN, DECK_ZONE_SIDE});
|
||||
if (cardList.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QSet<QChar> colorSet; // A set to collect unique color symbols (e.g., W, U, B, R, G)
|
||||
|
||||
for (const QString &cardName : cardList) {
|
||||
CardInfoPtr currentCard = db->getCardInfo(cardName);
|
||||
if (currentCard) {
|
||||
const QString colors = currentCard->getColors(); // returns something like "WUB"
|
||||
for (const QChar &color : colors) {
|
||||
colorSet.insert(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the color identity is in WUBRG order
|
||||
QString colorIdentity;
|
||||
const QString wubrgOrder = "WUBRG";
|
||||
for (const QChar &color : wubrgOrder) {
|
||||
if (colorSet.contains(color)) {
|
||||
colorIdentity.append(color);
|
||||
}
|
||||
}
|
||||
|
||||
return colorIdentity;
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
#ifndef COCKATRICE_DECK_COLOR_IDENTITY_H
|
||||
#define COCKATRICE_DECK_COLOR_IDENTITY_H
|
||||
|
||||
#include <QString>
|
||||
|
||||
class CardDatabaseQuerier;
|
||||
class DeckList;
|
||||
|
||||
/**
|
||||
* @brief Computes the color identity of a deck (e.g. "WUBRG") from the color
|
||||
* symbols of all cards in the main deck and sideboard, ordered WUBRG.
|
||||
*
|
||||
* Shared as a free function so the deck storage previews and the deck share
|
||||
* dialog compute identities identically.
|
||||
*
|
||||
* @param db Card database used to look up card color symbols.
|
||||
*/
|
||||
QString getDeckColorIdentity(const DeckList &deck, const CardDatabaseQuerier *db);
|
||||
|
||||
#endif // COCKATRICE_DECK_COLOR_IDENTITY_H
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
#include "card_group_display_widget.h"
|
||||
|
||||
#include "../../../../client/settings/cache_settings.h"
|
||||
#include "../card_info_picture_with_text_overlay_widget.h"
|
||||
|
||||
#include <QResizeEvent>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/models/deck_list/deck_list_model.h>
|
||||
#include <libcockatrice/models/deck_list/deck_list_sort_filter_proxy_model.h>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
|
||||
CardGroupDisplayWidget::CardGroupDisplayWidget(QWidget *parent,
|
||||
DeckListModel *_deckListModel,
|
||||
|
|
@ -30,6 +32,7 @@ CardGroupDisplayWidget::CardGroupDisplayWidget(QWidget *parent,
|
|||
layout->addWidget(banner);
|
||||
|
||||
CardGroupDisplayWidget::updateCardDisplays();
|
||||
updateCardCount();
|
||||
|
||||
connect(deckListModel, &QAbstractItemModel::rowsInserted, this, &CardGroupDisplayWidget::onCardAddition);
|
||||
if (selectionModel) {
|
||||
|
|
@ -38,6 +41,13 @@ CardGroupDisplayWidget::CardGroupDisplayWidget(QWidget *parent,
|
|||
}
|
||||
connect(deckListModel, &QAbstractItemModel::rowsRemoved, this, &CardGroupDisplayWidget::onCardRemoval);
|
||||
connect(deckListModel, &QAbstractItemModel::dataChanged, this, &CardGroupDisplayWidget::onDataChanged);
|
||||
connect(deckListModel, &QAbstractItemModel::rowsInserted, this, &CardGroupDisplayWidget::updateCardCount);
|
||||
connect(deckListModel, &QAbstractItemModel::rowsRemoved, this, &CardGroupDisplayWidget::updateCardCount);
|
||||
connect(deckListModel, &QAbstractItemModel::dataChanged, this, &CardGroupDisplayWidget::updateCardCount);
|
||||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::visualDeckEditorShowCardCountsChanged,
|
||||
this, &CardGroupDisplayWidget::updateCardCount);
|
||||
|
||||
cardSizeWidget->enableCtrlScrollResize(this);
|
||||
}
|
||||
|
||||
// Just here so it can get overwritten in subclasses.
|
||||
|
|
@ -348,4 +358,22 @@ void CardGroupDisplayWidget::onActiveSortCriteriaChanged(QStringList _activeSort
|
|||
|
||||
clearAllDisplayWidgets();
|
||||
updateCardDisplays();
|
||||
}
|
||||
}
|
||||
|
||||
void CardGroupDisplayWidget::updateCardCount()
|
||||
{
|
||||
if (!banner || !deckListModel || !trackedIndex.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString text = cardGroupCategory;
|
||||
if (SettingsCache::instance().cardsDisplay().getVisualDeckEditorShowCardCounts()) {
|
||||
int total = 0;
|
||||
for (int i = 0; i < deckListModel->rowCount(trackedIndex); ++i) {
|
||||
total +=
|
||||
deckListModel->index(i, DeckListModelColumns::CARD_AMOUNT, trackedIndex).data(Qt::EditRole).toInt();
|
||||
}
|
||||
text += QStringLiteral(" (%1)").arg(total);
|
||||
}
|
||||
banner->setText(text);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ public slots:
|
|||
virtual void onCardRemoval(const QModelIndex &parent, int first, int last);
|
||||
void onDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles);
|
||||
void onActiveSortCriteriaChanged(QStringList activeSortCriteria);
|
||||
void updateCardCount();
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
|
||||
signals:
|
||||
|
|
|
|||
|
|
@ -133,12 +133,14 @@ void CardInfoPictureWithTextOverlayWidget::paintEvent(QPaintEvent *event)
|
|||
path.addRoundedRect(glowRect, radius, radius);
|
||||
|
||||
// Soft outer glow
|
||||
QColor glowColor(0, 150, 255, 80); // subtle blu
|
||||
QColor glowColor = palette().color(QPalette::Highlight);
|
||||
glowColor.setAlpha(80);
|
||||
painter.setPen(QPen(glowColor, 6));
|
||||
painter.drawPath(path);
|
||||
|
||||
// Thin inner border for crispness
|
||||
QColor borderColor(0, 150, 255, 200);
|
||||
QColor borderColor = palette().color(QPalette::Highlight);
|
||||
borderColor.setAlpha(200);
|
||||
painter.setPen(QPen(borderColor, 2));
|
||||
painter.drawRoundedRect(pixmapRect, radius, radius);
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@
|
|||
#include "../printing_selector/printing_selector.h"
|
||||
#include "../visual_deck_storage/visual_deck_storage_widget.h"
|
||||
|
||||
#include <QScrollArea>
|
||||
#include <QWheelEvent>
|
||||
|
||||
/**
|
||||
* @class CardSizeWidget
|
||||
* @brief A widget for adjusting card sizes using a slider.
|
||||
|
|
@ -58,3 +61,39 @@ QSlider *CardSizeWidget::getSlider() const
|
|||
{
|
||||
return cardSizeSlider;
|
||||
}
|
||||
|
||||
void CardSizeWidget::enableCtrlScrollResize(QWidget *host)
|
||||
{
|
||||
host->installEventFilter(this);
|
||||
|
||||
const auto scrollAreas = host->findChildren<QScrollArea *>();
|
||||
for (QScrollArea *scrollArea : scrollAreas) {
|
||||
if (QWidget *content = scrollArea->widget()) {
|
||||
content->installEventFilter(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool CardSizeWidget::eventFilter(QObject *watched, QEvent *event)
|
||||
{
|
||||
if (event->type() == QEvent::Wheel && adjustSliderForWheel(static_cast<QWheelEvent *>(event))) {
|
||||
return true;
|
||||
}
|
||||
return QWidget::eventFilter(watched, event);
|
||||
}
|
||||
|
||||
bool CardSizeWidget::adjustSliderForWheel(QWheelEvent *event)
|
||||
{
|
||||
if (!(event->modifiers() & Qt::ControlModifier)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const int angleDelta = event->angleDelta().y();
|
||||
if (angleDelta == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const int wheelSteps = angleDelta > 0 ? qMax(1, angleDelta / 120) : qMin(-1, angleDelta / 120);
|
||||
cardSizeSlider->setValue(cardSizeSlider->value() + wheelSteps * CARD_SIZE_WHEEL_STEP);
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@
|
|||
#include <QTimer>
|
||||
#include <QWidget>
|
||||
|
||||
class QWheelEvent;
|
||||
|
||||
class CardSizeWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
|
@ -25,6 +27,18 @@ public:
|
|||
explicit CardSizeWidget(QWidget *parent, FlowWidget *flowWidget = nullptr, int defaultValue = 100);
|
||||
[[nodiscard]] QSlider *getSlider() const;
|
||||
|
||||
/**
|
||||
* @brief Resizes the cards when the user Ctrl + scrolls over @p host or any of its descendants.
|
||||
*
|
||||
* Installs this widget as an event filter on the given host widget. If the host contains a
|
||||
* scroll area the filter is also installed on the scroll area's content widget, so the resize
|
||||
* intercepts the wheel event before the scroll area would scroll the view.
|
||||
*/
|
||||
void enableCtrlScrollResize(QWidget *host);
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||
|
||||
private slots:
|
||||
void updateCardSizeSetting(int newValue);
|
||||
|
||||
|
|
@ -44,6 +58,10 @@ private:
|
|||
QSlider *cardSizeSlider;
|
||||
QTimer debounceTimer; // Debounce timer
|
||||
int pendingValue; // Stores the latest slider value
|
||||
|
||||
bool adjustSliderForWheel(QWheelEvent *event);
|
||||
|
||||
static constexpr int CARD_SIZE_WHEEL_STEP = 10; ///< Slider step applied per Ctrl + scroll notch.
|
||||
};
|
||||
|
||||
#endif // CARD_SIZE_WIDGET_H
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
#include "deck_card_zone_display_widget.h"
|
||||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "card_group_display_widgets/flat_card_group_display_widget.h"
|
||||
#include "card_group_display_widgets/overlapped_card_group_display_widget.h"
|
||||
#include "libcockatrice/card/database/card_database_manager.h"
|
||||
#include "libcockatrice/settings/cards_display_settings.h"
|
||||
|
||||
#include <QResizeEvent>
|
||||
#include <algorithm>
|
||||
|
|
@ -39,6 +41,7 @@ DeckCardZoneDisplayWidget::DeckCardZoneDisplayWidget(QWidget *parent,
|
|||
banner->setBuddy(cardGroupContainer);
|
||||
|
||||
displayCards();
|
||||
updateZoneCardCount();
|
||||
|
||||
connect(deckListModel, &QAbstractItemModel::rowsInserted, this, &DeckCardZoneDisplayWidget::onCategoryAddition);
|
||||
if (selectionModel) {
|
||||
|
|
@ -46,6 +49,11 @@ DeckCardZoneDisplayWidget::DeckCardZoneDisplayWidget(QWidget *parent,
|
|||
&DeckCardZoneDisplayWidget::onSelectionChanged);
|
||||
}
|
||||
connect(deckListModel, &QAbstractItemModel::rowsRemoved, this, &DeckCardZoneDisplayWidget::onCategoryRemoval);
|
||||
connect(deckListModel, &QAbstractItemModel::rowsInserted, this, &DeckCardZoneDisplayWidget::updateZoneCardCount);
|
||||
connect(deckListModel, &QAbstractItemModel::rowsRemoved, this, &DeckCardZoneDisplayWidget::updateZoneCardCount);
|
||||
connect(deckListModel, &QAbstractItemModel::dataChanged, this, &DeckCardZoneDisplayWidget::updateZoneCardCount);
|
||||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::visualDeckEditorShowCardCountsChanged,
|
||||
this, &DeckCardZoneDisplayWidget::updateZoneCardCount);
|
||||
}
|
||||
|
||||
// =====================================================================================================================
|
||||
|
|
@ -245,3 +253,21 @@ QList<QString> DeckCardZoneDisplayWidget::getGroupCriteriaValueList()
|
|||
|
||||
return groupCriteriaValues;
|
||||
}
|
||||
|
||||
void DeckCardZoneDisplayWidget::updateZoneCardCount()
|
||||
{
|
||||
if (!banner || !deckListModel) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString text = zoneName;
|
||||
if (SettingsCache::instance().cardsDisplay().getVisualDeckEditorShowCardCounts()) {
|
||||
int total = 0;
|
||||
const auto cardNodes = deckListModel->getCardNodesForZone(zoneName);
|
||||
for (const auto *node : cardNodes) {
|
||||
total += node->getNumber();
|
||||
}
|
||||
text += QStringLiteral(" (%1)").arg(total);
|
||||
}
|
||||
banner->setText(text);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ public slots:
|
|||
QList<QString> getGroupCriteriaValueList();
|
||||
void onCategoryAddition(const QModelIndex &parent, int first, int last);
|
||||
void onCategoryRemoval(const QModelIndex &parent, int first, int last);
|
||||
void updateZoneCardCount();
|
||||
|
||||
signals:
|
||||
void cardClicked(QMouseEvent *event, const ExactCard &card, const QString &zoneName);
|
||||
|
|
|
|||
|
|
@ -27,18 +27,23 @@ DeckPreviewCardPictureWidget::DeckPreviewCardPictureWidget(QWidget *parent,
|
|||
const QColor &textColor,
|
||||
const QColor &outlineColor,
|
||||
const int fontSize,
|
||||
const Qt::Alignment alignment)
|
||||
const Qt::Alignment alignment,
|
||||
const bool _emitClickImmediately)
|
||||
: CardInfoPictureWithTextOverlayWidget(parent,
|
||||
hoverToZoomEnabled,
|
||||
raiseOnEnter,
|
||||
textColor,
|
||||
outlineColor,
|
||||
fontSize,
|
||||
alignment)
|
||||
alignment),
|
||||
emitClickImmediately(_emitClickImmediately)
|
||||
{
|
||||
singleClickTimer = new QTimer(this);
|
||||
singleClickTimer->setSingleShot(true);
|
||||
connect(singleClickTimer, &QTimer::timeout, this, [this]() { emit imageClicked(lastMouseEvent, this); });
|
||||
connect(singleClickTimer, &QTimer::timeout, this, [this]() {
|
||||
emit imageClicked(lastMouseEvent, this);
|
||||
emit imageSingleClicked();
|
||||
});
|
||||
connect(&SettingsCache::instance().visualDeckStorage(),
|
||||
&VisualDeckStorageSettings::visualDeckStorageSelectionAnimationChanged, this,
|
||||
&CardInfoPictureWidget::setRaiseOnEnterEnabled);
|
||||
|
|
@ -47,8 +52,13 @@ DeckPreviewCardPictureWidget::DeckPreviewCardPictureWidget(QWidget *parent,
|
|||
void DeckPreviewCardPictureWidget::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
if (event->button() == Qt::LeftButton) {
|
||||
lastMouseEvent = event;
|
||||
singleClickTimer->start(QApplication::doubleClickInterval());
|
||||
if (emitClickImmediately) {
|
||||
emit imageClicked(event, this);
|
||||
emit imageSingleClicked();
|
||||
} else {
|
||||
lastMouseEvent = event;
|
||||
singleClickTimer->start(QApplication::doubleClickInterval());
|
||||
}
|
||||
} else {
|
||||
emit imageClicked(event, this);
|
||||
event->accept();
|
||||
|
|
@ -58,7 +68,14 @@ void DeckPreviewCardPictureWidget::mousePressEvent(QMouseEvent *event)
|
|||
void DeckPreviewCardPictureWidget::mouseDoubleClickEvent(QMouseEvent *event)
|
||||
{
|
||||
if (event->button() == Qt::LeftButton) {
|
||||
singleClickTimer->stop(); // Prevent single-click logic
|
||||
emit imageDoubleClicked(lastMouseEvent, this);
|
||||
if (emitClickImmediately) {
|
||||
// Do not report a second single click for the second press of the
|
||||
// double-click; the consumer maps the double-click to select+open.
|
||||
lastMouseEvent = event;
|
||||
emit imageDoubleClicked(event, this);
|
||||
} else {
|
||||
singleClickTimer->stop(); // Prevent single-click logic
|
||||
emit imageDoubleClicked(lastMouseEvent, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,21 +20,38 @@ class DeckPreviewCardPictureWidget final : public CardInfoPictureWithTextOverlay
|
|||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Constructs a DeckPreviewCardPictureWidget.
|
||||
* @param parent The parent widget.
|
||||
* @param hoverToZoomEnabled If this widget will spawn a larger widget when hovered over.
|
||||
* @param raiseOnEnter If the widget raises its border when the mouse enters.
|
||||
* @param textColor The color of the overlay text.
|
||||
* @param outlineColor The color of the outline around the text.
|
||||
* @param fontSize The font size of the overlay text.
|
||||
* @param alignment The alignment of the text within the overlay.
|
||||
* @param emitClickImmediately If true, a left click is reported immediately on click
|
||||
* instead of after the double-click interval. Use this for selection surfaces
|
||||
* where reacting to a double-click (select-and-open) would needlessly delay the
|
||||
* single-click feedback. The double-click signal is still emitted.
|
||||
*/
|
||||
explicit DeckPreviewCardPictureWidget(QWidget *parent,
|
||||
bool hoverToZoomEnabled = false,
|
||||
bool raiseOnEnter = false,
|
||||
const QColor &textColor = Qt::white,
|
||||
const QColor &outlineColor = Qt::black,
|
||||
int fontSize = 12,
|
||||
Qt::Alignment alignment = Qt::AlignCenter);
|
||||
Qt::Alignment alignment = Qt::AlignCenter,
|
||||
bool _emitClickImmediately = false);
|
||||
|
||||
signals:
|
||||
void imageClicked(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
|
||||
void imageSingleClicked();
|
||||
void imageDoubleClicked(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
|
||||
|
||||
private:
|
||||
QTimer *singleClickTimer;
|
||||
QMouseEvent *lastMouseEvent = nullptr; // Store the last mouse event
|
||||
bool emitClickImmediately;
|
||||
|
||||
protected:
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
|
|
|
|||
|
|
@ -11,8 +11,10 @@
|
|||
#include <QHeaderView>
|
||||
#include <QToolButton>
|
||||
#include <QTreeView>
|
||||
#include <libcockatrice/card/card_localization.h>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/card/relation/card_relation.h>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
|
||||
DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(QWidget *parent, CardDatabaseModel *databaseModel)
|
||||
: QWidget(parent)
|
||||
|
|
@ -40,6 +42,17 @@ DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(QWidget *parent
|
|||
databaseDisplayModel->setSourceModel(databaseModel);
|
||||
databaseDisplayModel->setFilterKeyColumn(0);
|
||||
|
||||
const auto applyCardSearchLanguage = [this]() {
|
||||
const CardsDisplaySettings &cardsDisplay = SettingsCache::instance().cardsDisplay();
|
||||
databaseDisplayModel->setSearchLanguage(CardSearchLanguage{
|
||||
cardsDisplay.getCardLang(), static_cast<SearchLanguageMode>(cardsDisplay.getCardSearchLanguage())});
|
||||
};
|
||||
applyCardSearchLanguage();
|
||||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this,
|
||||
applyCardSearchLanguage);
|
||||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardSearchLanguageChanged, this,
|
||||
applyCardSearchLanguage);
|
||||
|
||||
databaseView = new CardDatabaseView(this, databaseDisplayModel);
|
||||
databaseView->setObjectName("databaseView");
|
||||
databaseView->setFocusProxy(searchEdit);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
#include "deck_share_utils.h"
|
||||
|
||||
#include <QClipboard>
|
||||
#include <QGuiApplication>
|
||||
#include <QLocale>
|
||||
#include <QTimeZone>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
||||
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_deck_share_create.pb.h>
|
||||
|
||||
namespace DeckShareUtils
|
||||
{
|
||||
|
||||
QString buildShareLink(const AbstractClient *client, const QString &token)
|
||||
{
|
||||
QUrl url;
|
||||
url.setScheme(QStringLiteral("cockatrice"));
|
||||
url.setHost(QStringLiteral("opendeck"));
|
||||
|
||||
QUrlQuery query;
|
||||
query.addQueryItem(QStringLiteral("share"), token);
|
||||
query.addQueryItem(QStringLiteral("hostname"), client->serverName());
|
||||
query.addQueryItem(QStringLiteral("port"), QString::number(client->serverPort()));
|
||||
url.setQuery(query);
|
||||
|
||||
return url.toString(QUrl::FullyEncoded);
|
||||
}
|
||||
|
||||
QString copyShareLinkToClipboard(const QString &link)
|
||||
{
|
||||
QGuiApplication::clipboard()->setText(link);
|
||||
return link;
|
||||
}
|
||||
|
||||
QString formatShareExpiry(const QDateTime &expiry)
|
||||
{
|
||||
return QLocale().toString(expiry.toLocalTime(), QLocale::ShortFormat);
|
||||
}
|
||||
|
||||
ShareResponse handleShareResponse(const AbstractClient *client, const Response &response)
|
||||
{
|
||||
const Response_DeckShareCreate &resp = response.GetExtension(Response_DeckShareCreate::ext);
|
||||
const QString token = QString::fromStdString(resp.token());
|
||||
|
||||
const QString link = buildShareLink(client, token);
|
||||
copyShareLinkToClipboard(link);
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
|
||||
const QDateTime expiry = QDateTime::fromSecsSinceEpoch(resp.expires_at(), QTimeZone::UTC);
|
||||
#else
|
||||
const QDateTime expiry = QDateTime::fromSecsSinceEpoch(resp.expires_at(), Qt::UTC);
|
||||
#endif
|
||||
|
||||
return {link, expiry};
|
||||
}
|
||||
|
||||
} // namespace DeckShareUtils
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
* @file deck_share_utils.h
|
||||
* @ingroup DeckShareWidgets
|
||||
*/
|
||||
//! \todo Document this file.
|
||||
|
||||
#ifndef DECK_SHARE_UTILS_H
|
||||
#define DECK_SHARE_UTILS_H
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QString>
|
||||
|
||||
class AbstractClient;
|
||||
class Response;
|
||||
|
||||
/**
|
||||
* @brief Shared helpers for creating temporary deck shares.
|
||||
*/
|
||||
namespace DeckShareUtils
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief The outcome of a successful share-create response.
|
||||
*/
|
||||
struct ShareResponse
|
||||
{
|
||||
QString link; ///< The share link that was copied to the clipboard.
|
||||
QDateTime expiry; ///< When the share expires (UTC).
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Builds the cockatrice:// link for a freshly created deck share.
|
||||
* @param client Used to embed the target server's hostname and port.
|
||||
* @param token The share token from Response_DeckShareCreate.
|
||||
*/
|
||||
QString buildShareLink(const AbstractClient *client, const QString &token);
|
||||
|
||||
/**
|
||||
* @brief Copies the share link to the clipboard.
|
||||
* @return The link that was copied.
|
||||
*/
|
||||
QString copyShareLinkToClipboard(const QString &link);
|
||||
|
||||
/**
|
||||
* @brief Formats the expiration timestamp for a share.
|
||||
*/
|
||||
QString formatShareExpiry(const QDateTime &expiry);
|
||||
|
||||
/**
|
||||
* @brief Handles a successful Response_DeckShareCreate: builds the share link,
|
||||
* copies it to the clipboard, and derives the share expiry.
|
||||
* @param client Used to embed the target server's hostname and port.
|
||||
* @param response The successful response carrying the share token and expiry.
|
||||
*/
|
||||
ShareResponse handleShareResponse(const AbstractClient *client, const Response &response);
|
||||
|
||||
} // namespace DeckShareUtils
|
||||
|
||||
#endif // DECK_SHARE_UTILS_H
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
#include "share_bar_widget.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
|
||||
ShareBarWidget::ShareBarWidget(QWidget *parent) : QWidget(parent)
|
||||
{
|
||||
auto *layout = new QHBoxLayout(this);
|
||||
layout->setContentsMargins(12, 10, 12, 10);
|
||||
layout->setSpacing(8);
|
||||
|
||||
hintLabel = new QLabel(this);
|
||||
hintLabel->setWordWrap(true);
|
||||
|
||||
nameEdit = new QLineEdit(this);
|
||||
nameEdit->setMaximumWidth(260);
|
||||
|
||||
countLabel = new QLabel(this);
|
||||
|
||||
cancelButton = new QPushButton(this);
|
||||
connect(cancelButton, &QPushButton::clicked, this, &ShareBarWidget::cancelRequested);
|
||||
|
||||
createButton = new QPushButton(this);
|
||||
createButton->setDefault(true);
|
||||
connect(createButton, &QPushButton::clicked, this, &ShareBarWidget::createRequested);
|
||||
|
||||
layout->addWidget(hintLabel, 1);
|
||||
layout->addWidget(nameEdit);
|
||||
layout->addWidget(countLabel);
|
||||
layout->addStretch();
|
||||
layout->addWidget(cancelButton);
|
||||
layout->addWidget(createButton);
|
||||
|
||||
setLayout(layout);
|
||||
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
void ShareBarWidget::retranslateUi()
|
||||
{
|
||||
nameEdit->setPlaceholderText(tr("Share name"));
|
||||
cancelButton->setText(tr("Cancel"));
|
||||
createButton->setText(tr("Create share link"));
|
||||
}
|
||||
|
||||
QString ShareBarWidget::name() const
|
||||
{
|
||||
return nameEdit->text().trimmed();
|
||||
}
|
||||
|
||||
void ShareBarWidget::setName(const QString &value)
|
||||
{
|
||||
nameEdit->setText(value);
|
||||
}
|
||||
|
||||
void ShareBarWidget::setCountText(const QString &text)
|
||||
{
|
||||
countLabel->setText(text);
|
||||
}
|
||||
|
||||
void ShareBarWidget::setHintText(const QString &text, bool visible)
|
||||
{
|
||||
hintLabel->setText(text);
|
||||
hintLabel->setVisible(visible);
|
||||
}
|
||||
|
||||
void ShareBarWidget::setCreateEnabled(bool enabled)
|
||||
{
|
||||
createButton->setEnabled(enabled);
|
||||
}
|
||||
|
||||
void ShareBarWidget::focusName()
|
||||
{
|
||||
nameEdit->setFocus();
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
/**
|
||||
* @file share_bar_widget.h
|
||||
* @ingroup DeckShareWidgets
|
||||
*/
|
||||
//! \todo Document this file.
|
||||
|
||||
#ifndef SHARE_BAR_WIDGET_H
|
||||
#define SHARE_BAR_WIDGET_H
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
class QLabel;
|
||||
class QLineEdit;
|
||||
class QPushButton;
|
||||
|
||||
/**
|
||||
* @brief The activated toolbar used to create a temporary deck share.
|
||||
*
|
||||
* A single reusable component shared by the local visual deck storage and the
|
||||
* remote server deck storage tabs, so the share workflow renders identically in
|
||||
* both places. It owns its own widgets, strings, and layout; the owning tab only
|
||||
* sets the count/hint text and reacts to the create/cancel signals.
|
||||
*/
|
||||
class ShareBarWidget final : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ShareBarWidget(QWidget *parent = nullptr);
|
||||
|
||||
void retranslateUi();
|
||||
|
||||
/** @return The trimmed name entered by the user. */
|
||||
[[nodiscard]] QString name() const;
|
||||
|
||||
/** @brief Resets the name field to the given default. */
|
||||
void setName(const QString &name);
|
||||
|
||||
/** @brief Sets the selected-count summary label text. */
|
||||
void setCountText(const QString &text);
|
||||
|
||||
/** @brief Sets the explainer hint text, showing it when @p visible is true. */
|
||||
void setHintText(const QString &text, bool visible);
|
||||
|
||||
/** @brief Enables or disables the create-share-link button (guards double submission). */
|
||||
void setCreateEnabled(bool enabled);
|
||||
|
||||
/** @brief Moves keyboard focus to the name field. */
|
||||
void focusName();
|
||||
|
||||
signals:
|
||||
void createRequested();
|
||||
void cancelRequested();
|
||||
|
||||
private:
|
||||
QLabel *hintLabel;
|
||||
QLineEdit *nameEdit;
|
||||
QLabel *countLabel;
|
||||
QPushButton *cancelButton;
|
||||
QPushButton *createButton;
|
||||
};
|
||||
|
||||
#endif // SHARE_BAR_WIDGET_H
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
#include "shared_deck_preview_widget.h"
|
||||
|
||||
#include "../cards/additional_info/color_identity_widget.h"
|
||||
#include "../cards/deck_preview_card_picture_widget.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QFrame>
|
||||
#include <QHBoxLayout>
|
||||
#include <QKeyEvent>
|
||||
#include <QLabel>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/card/database/card_database_querier.h>
|
||||
|
||||
SharedDeckPreviewWidget::SharedDeckPreviewWidget(QWidget *parent,
|
||||
const CardDatabaseQuerier *querier,
|
||||
const QString &deckName,
|
||||
const QString &bannerCardName,
|
||||
const QString &colorIdentity,
|
||||
const QString &gameFormat,
|
||||
const QString &deckToolTip)
|
||||
: QWidget(parent)
|
||||
{
|
||||
bannerCardDisplayWidget =
|
||||
new DeckPreviewCardPictureWidget(this, false, false, Qt::white, Qt::black, 12, Qt::AlignCenter, true);
|
||||
bannerCardDisplayWidget->setScaleFactor(100);
|
||||
const ExactCard bannerCard = bannerCardName.isEmpty() ? ExactCard() : querier->getCard(CardRef{bannerCardName, {}});
|
||||
bannerCardDisplayWidget->setCard(bannerCard);
|
||||
bannerCardDisplayWidget->setOverlayText(deckName);
|
||||
setToolTip(deckToolTip.isEmpty() ? deckName : deckToolTip);
|
||||
setFocusPolicy(Qt::StrongFocus);
|
||||
setBaseAccessibleName(deckName);
|
||||
|
||||
colorIdentityWidget = new ColorIdentityWidget(this, colorIdentity);
|
||||
colorIdentityWidget->setVisible(!colorIdentity.isEmpty());
|
||||
|
||||
// gameFormat is server-supplied and the QLabel renders AutoText, so escape it.
|
||||
gameFormatLabel = new QLabel(gameFormat.toHtmlEscaped(), this);
|
||||
gameFormatLabel->setAlignment(Qt::AlignCenter);
|
||||
gameFormatLabel->setVisible(!gameFormat.isEmpty());
|
||||
|
||||
selectionCheckBox = new QCheckBox(this);
|
||||
selectionCheckBox->setToolTip(tr("Select this deck"));
|
||||
// The tile itself is focusable (Space/Enter toggles); keep the checkbox
|
||||
// from creating a second tab stop per tile.
|
||||
selectionCheckBox->setFocusPolicy(Qt::NoFocus);
|
||||
|
||||
// Selection frame reused from the deck-preview selection covenant: a
|
||||
// palette(highlight) border around the banner card, shown while selected.
|
||||
selectionFrame = new QFrame(bannerCardDisplayWidget);
|
||||
selectionFrame->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
selectionFrame->setStyleSheet(QStringLiteral(
|
||||
"QFrame { border: 2px solid palette(highlight); border-radius: 4px; background: transparent; }"));
|
||||
selectionFrame->setVisible(false);
|
||||
|
||||
auto *selectionRow = new QHBoxLayout;
|
||||
selectionRow->addWidget(selectionCheckBox);
|
||||
selectionRow->addStretch(1);
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->addLayout(selectionRow);
|
||||
layout->addWidget(bannerCardDisplayWidget, 0, Qt::AlignHCenter);
|
||||
layout->addWidget(colorIdentityWidget, 0, Qt::AlignHCenter);
|
||||
layout->addWidget(gameFormatLabel, 0, Qt::AlignHCenter);
|
||||
setLayout(layout);
|
||||
|
||||
connect(selectionCheckBox, &QCheckBox::toggled, this, [this](bool checked) {
|
||||
updateSelectionVisual(checked);
|
||||
emit selectionToggled(checked);
|
||||
});
|
||||
connect(bannerCardDisplayWidget, &DeckPreviewCardPictureWidget::imageClicked, this,
|
||||
&SharedDeckPreviewWidget::toggleSelection);
|
||||
connect(bannerCardDisplayWidget, &DeckPreviewCardPictureWidget::imageDoubleClicked, this,
|
||||
&SharedDeckPreviewWidget::activate);
|
||||
}
|
||||
|
||||
bool SharedDeckPreviewWidget::isSelected() const
|
||||
{
|
||||
return selectionCheckBox->isChecked();
|
||||
}
|
||||
|
||||
void SharedDeckPreviewWidget::setSelected(bool selected)
|
||||
{
|
||||
if (isSelected() == selected) {
|
||||
return;
|
||||
}
|
||||
selectionCheckBox->setChecked(selected);
|
||||
}
|
||||
|
||||
void SharedDeckPreviewWidget::updateSelectionVisual(bool selected)
|
||||
{
|
||||
selectionFrame->setVisible(selected);
|
||||
selectionFrame->raise();
|
||||
if (selected) {
|
||||
setAccessibleName(baseAccessibleName + tr(" (selected)"));
|
||||
} else {
|
||||
setAccessibleName(baseAccessibleName);
|
||||
}
|
||||
}
|
||||
|
||||
void SharedDeckPreviewWidget::setBaseAccessibleName(const QString &name)
|
||||
{
|
||||
baseAccessibleName = name;
|
||||
setAccessibleName(name);
|
||||
}
|
||||
|
||||
void SharedDeckPreviewWidget::toggleSelection()
|
||||
{
|
||||
setSelected(!isSelected());
|
||||
}
|
||||
|
||||
void SharedDeckPreviewWidget::activate()
|
||||
{
|
||||
setSelected(true);
|
||||
emit activated();
|
||||
}
|
||||
|
||||
void SharedDeckPreviewWidget::resizeEvent(QResizeEvent *event)
|
||||
{
|
||||
QWidget::resizeEvent(event);
|
||||
updateSelectionFrameGeometry();
|
||||
}
|
||||
|
||||
void SharedDeckPreviewWidget::updateSelectionFrameGeometry()
|
||||
{
|
||||
if (selectionFrame == nullptr || bannerCardDisplayWidget == nullptr) {
|
||||
return;
|
||||
}
|
||||
selectionFrame->setGeometry(bannerCardDisplayWidget->rect().adjusted(1, 1, -1, -1));
|
||||
}
|
||||
|
||||
void SharedDeckPreviewWidget::keyPressEvent(QKeyEvent *event)
|
||||
{
|
||||
if (event->key() == Qt::Key_Space || event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) {
|
||||
toggleSelection();
|
||||
event->accept();
|
||||
return;
|
||||
}
|
||||
QWidget::keyPressEvent(event);
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
/**
|
||||
* @file shared_deck_preview_widget.h
|
||||
* @ingroup DeckShareWidgets
|
||||
*/
|
||||
//! \todo Document this file.
|
||||
|
||||
#ifndef SHARED_DECK_PREVIEW_WIDGET_H
|
||||
#define SHARED_DECK_PREVIEW_WIDGET_H
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
class ColorIdentityWidget;
|
||||
class DeckPreviewCardPictureWidget;
|
||||
class QCheckBox;
|
||||
class QFrame;
|
||||
class QKeyEvent;
|
||||
class QLabel;
|
||||
class QResizeEvent;
|
||||
class CardDatabaseQuerier;
|
||||
|
||||
/**
|
||||
* @brief A selectable preview tile for a deck that has no local file.
|
||||
*
|
||||
* Renders a banner card picture (looked up by name in the card database), the
|
||||
* deck name, color identity and game format. Used to preview decks shared via a
|
||||
* cockatrice:// link (metadata from Command_DeckShareList) and the deck
|
||||
* currently open in the deck editor.
|
||||
*
|
||||
* Selection follows the deck-preview covenant: the tile reports its click
|
||||
* immediately (no double-click interval delay), a palette(highlight) frame
|
||||
* marks the selected tile, and Space/Enter toggles selection from the keyboard.
|
||||
* A double click selects the tile and emits activated() so the caller can open
|
||||
* just that deck.
|
||||
*/
|
||||
class SharedDeckPreviewWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SharedDeckPreviewWidget(QWidget *parent,
|
||||
const CardDatabaseQuerier *querier,
|
||||
const QString &deckName,
|
||||
const QString &bannerCardName,
|
||||
const QString &colorIdentity,
|
||||
const QString &gameFormat = QString(),
|
||||
const QString &deckToolTip = QString());
|
||||
|
||||
[[nodiscard]] bool isSelected() const;
|
||||
void setSelected(bool selected);
|
||||
|
||||
void setBaseAccessibleName(const QString &name);
|
||||
|
||||
signals:
|
||||
void selectionToggled(bool selected);
|
||||
void activated();
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
void keyPressEvent(QKeyEvent *event) override;
|
||||
|
||||
private slots:
|
||||
void toggleSelection();
|
||||
void activate();
|
||||
|
||||
private:
|
||||
void updateSelectionVisual(bool selected);
|
||||
void updateSelectionFrameGeometry();
|
||||
|
||||
DeckPreviewCardPictureWidget *bannerCardDisplayWidget;
|
||||
ColorIdentityWidget *colorIdentityWidget;
|
||||
QLabel *gameFormatLabel;
|
||||
QCheckBox *selectionCheckBox;
|
||||
QFrame *selectionFrame;
|
||||
QString baseAccessibleName;
|
||||
};
|
||||
|
||||
#endif // SHARED_DECK_PREVIEW_WIDGET_H
|
||||
|
|
@ -19,6 +19,9 @@ DlgEditAvatar::DlgEditAvatar(QWidget *parent) : QDialog(parent), image()
|
|||
|
||||
textLabel = new QLabel(tr("To change your avatar, choose a new image.\nTo remove your current avatar, confirm "
|
||||
"without choosing a new image."));
|
||||
hintLabel = new QLabel(
|
||||
tr("Images are automatically downscaled to at most %1x%1 pixels.").arg(QString::number(MAX_AVATAR_DIMENSION)));
|
||||
|
||||
browseButton = new QPushButton(tr("Browse..."));
|
||||
connect(browseButton, &QPushButton::clicked, this, &DlgEditAvatar::actBrowse);
|
||||
|
||||
|
|
@ -26,6 +29,7 @@ DlgEditAvatar::DlgEditAvatar(QWidget *parent) : QDialog(parent), image()
|
|||
grid->addWidget(imageLabel, 0, 0, 1, 2);
|
||||
grid->addWidget(textLabel, 1, 0);
|
||||
grid->addWidget(browseButton, 1, 1);
|
||||
grid->addWidget(hintLabel, 2, 0, 1, 2);
|
||||
|
||||
auto *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
connect(buttonBox, &QDialogButtonBox::accepted, this, &DlgEditAvatar::actOk);
|
||||
|
|
@ -72,6 +76,10 @@ QByteArray DlgEditAvatar::getImage()
|
|||
return QByteArray();
|
||||
}
|
||||
|
||||
if (image.width() > MAX_AVATAR_DIMENSION || image.height() > MAX_AVATAR_DIMENSION) {
|
||||
image = image.scaled(MAX_AVATAR_DIMENSION, MAX_AVATAR_DIMENSION, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
QByteArray ba;
|
||||
QBuffer buffer(&ba);
|
||||
|
|
|
|||
|
|
@ -29,8 +29,9 @@ private slots:
|
|||
void actBrowse();
|
||||
|
||||
private:
|
||||
static constexpr int MAX_AVATAR_DIMENSION = 1024;
|
||||
QImage image;
|
||||
QLabel *textLabel, *imageLabel;
|
||||
QLabel *textLabel, *imageLabel, *hintLabel;
|
||||
QPushButton *browseButton;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
#include "dlg_login_prompt.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFormLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
DlgLoginPrompt::DlgLoginPrompt(const QString &serverText, QWidget *parent) : QDialog(parent)
|
||||
{
|
||||
setWindowTitle(tr("Sign in"));
|
||||
|
||||
auto *mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->addWidget(
|
||||
new QLabel(tr("This link requires you to be signed in.\nSign in to %1:").arg(serverText), this));
|
||||
|
||||
auto *formLayout = new QFormLayout;
|
||||
usernameEdit = new QLineEdit(this);
|
||||
passwordEdit = new QLineEdit(this);
|
||||
passwordEdit->setEchoMode(QLineEdit::Password);
|
||||
formLayout->addRow(tr("Username:"), usernameEdit);
|
||||
formLayout->addRow(tr("Password:"), passwordEdit);
|
||||
mainLayout->addLayout(formLayout);
|
||||
|
||||
savePasswordCheckBox = new QCheckBox(tr("Save password for this server"), this);
|
||||
mainLayout->addWidget(savePasswordCheckBox);
|
||||
|
||||
auto *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||||
connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
mainLayout->addWidget(buttonBox);
|
||||
|
||||
usernameEdit->setFocus();
|
||||
}
|
||||
|
||||
QString DlgLoginPrompt::username() const
|
||||
{
|
||||
return usernameEdit->text().trimmed();
|
||||
}
|
||||
|
||||
QString DlgLoginPrompt::password() const
|
||||
{
|
||||
return passwordEdit->text();
|
||||
}
|
||||
|
||||
bool DlgLoginPrompt::savePassword() const
|
||||
{
|
||||
return savePasswordCheckBox->isChecked();
|
||||
}
|
||||
40
cockatrice/src/interface/widgets/dialogs/dlg_login_prompt.h
Normal file
40
cockatrice/src/interface/widgets/dialogs/dlg_login_prompt.h
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* @file dlg_login_prompt.h
|
||||
* @ingroup ConnectionDialogs
|
||||
*/
|
||||
//! \todo Document this file.
|
||||
|
||||
#ifndef DLG_LOGIN_PROMPT_H
|
||||
#define DLG_LOGIN_PROMPT_H
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
class QCheckBox;
|
||||
class QLineEdit;
|
||||
|
||||
/**
|
||||
* @brief Small sign-in dialog used when a cockatrice:// link needs credentials
|
||||
* that are not saved for the target server.
|
||||
*
|
||||
* The entered name and password are handed to the intent chain; when the user
|
||||
* opts to save them, they are stored in the server settings so that later links
|
||||
* to the same server connect seamlessly.
|
||||
*/
|
||||
class DlgLoginPrompt : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit DlgLoginPrompt(const QString &serverText, QWidget *parent = nullptr);
|
||||
|
||||
[[nodiscard]] QString username() const;
|
||||
[[nodiscard]] QString password() const;
|
||||
[[nodiscard]] bool savePassword() const;
|
||||
|
||||
private:
|
||||
QLineEdit *usernameEdit;
|
||||
QLineEdit *passwordEdit;
|
||||
QCheckBox *savePasswordCheckBox;
|
||||
};
|
||||
|
||||
#endif // DLG_LOGIN_PROMPT_H
|
||||
|
|
@ -15,6 +15,7 @@
|
|||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/protocol/pb/command_report.pb.h>
|
||||
#include <libcockatrice/protocol/pending_command.h>
|
||||
#include <libcockatrice/utility/report_categories.h>
|
||||
|
||||
DlgReportUser::DlgReportUser(AbstractClient *_client,
|
||||
const QString &_reportedUser,
|
||||
|
|
@ -56,22 +57,14 @@ DlgReportUser::DlgReportUser(AbstractClient *_client,
|
|||
auto *categoryGrid = new QGridLayout(categoryGroup);
|
||||
|
||||
categoryBox = new QComboBox;
|
||||
categoryBox->addItem(tr("Cheating / Unsporting behavior"), "cheating");
|
||||
categoryBox->setItemData(categoryBox->count() - 1,
|
||||
tr("Using external tools, card marked manipulation, or exploiting game bugs"),
|
||||
Qt::ToolTipRole);
|
||||
categoryBox->addItem(tr("Harassment / Abuse"), "harassment");
|
||||
categoryBox->setItemData(categoryBox->count() - 1, tr("Threatening, bullying, or persistent unwanted contact"),
|
||||
Qt::ToolTipRole);
|
||||
categoryBox->addItem(tr("Hate speech"), "hate_speech");
|
||||
categoryBox->setItemData(categoryBox->count() - 1,
|
||||
tr("Discriminatory language targeting race, gender, religion, etc."), Qt::ToolTipRole);
|
||||
categoryBox->addItem(tr("Spam"), "spam");
|
||||
categoryBox->setItemData(categoryBox->count() - 1, tr("Repeated unwanted messages or advertisements"),
|
||||
Qt::ToolTipRole);
|
||||
categoryBox->addItem(tr("Other"), "other");
|
||||
categoryBox->setItemData(categoryBox->count() - 1, tr("Any behavior not covered by the above categories"),
|
||||
Qt::ToolTipRole);
|
||||
for (const QString &key : ReportCategories::keys()) {
|
||||
const QString label = categoryLabel(key);
|
||||
if (label.isEmpty()) {
|
||||
continue; // skip keys without a dialog label
|
||||
}
|
||||
categoryBox->addItem(label, key);
|
||||
categoryBox->setItemData(categoryBox->count() - 1, categoryToolTip(key), Qt::ToolTipRole);
|
||||
}
|
||||
|
||||
categoryGrid->addWidget(new QLabel(tr("Category:")), 0, 0);
|
||||
categoryGrid->addWidget(categoryBox, 0, 1);
|
||||
|
|
@ -189,3 +182,55 @@ void DlgReportUser::reportResponse(const Response &response)
|
|||
QMessageBox::warning(this, tr("Submission Failed"), tr("Failed to submit report. Please try again."));
|
||||
}
|
||||
}
|
||||
|
||||
QString DlgReportUser::categoryLabel(const QString &key)
|
||||
{
|
||||
if (key == QLatin1String("cheating")) {
|
||||
return tr("Cheating / Unsporting behavior");
|
||||
}
|
||||
if (key == QLatin1String("bug_abuse")) {
|
||||
return tr("Bug Abuse");
|
||||
}
|
||||
if (key == QLatin1String("harassment")) {
|
||||
return tr("Harassment / Abuse");
|
||||
}
|
||||
if (key == QLatin1String("verbal_abuse")) {
|
||||
return tr("Verbal Abuse");
|
||||
}
|
||||
if (key == QLatin1String("hate_speech")) {
|
||||
return tr("Hate speech");
|
||||
}
|
||||
if (key == QLatin1String("spam")) {
|
||||
return tr("Spam");
|
||||
}
|
||||
if (key == QLatin1String("other")) {
|
||||
return tr("Other");
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString DlgReportUser::categoryToolTip(const QString &key)
|
||||
{
|
||||
if (key == QLatin1String("cheating")) {
|
||||
return tr("Using external tools, cheat programs, or exploiting game bugs");
|
||||
}
|
||||
if (key == QLatin1String("bug_abuse")) {
|
||||
return tr("Exploiting a bug or glitch to gain an unfair advantage");
|
||||
}
|
||||
if (key == QLatin1String("harassment")) {
|
||||
return tr("Threatening, bullying, or persistent unwanted contact");
|
||||
}
|
||||
if (key == QLatin1String("verbal_abuse")) {
|
||||
return tr("Abusive or offensive language directed at another player");
|
||||
}
|
||||
if (key == QLatin1String("hate_speech")) {
|
||||
return tr("Discriminatory language targeting race, gender, religion, etc.");
|
||||
}
|
||||
if (key == QLatin1String("spam")) {
|
||||
return tr("Repeated unwanted messages or advertisements");
|
||||
}
|
||||
if (key == QLatin1String("other")) {
|
||||
return tr("Any behavior not covered by the above categories");
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ private slots:
|
|||
void reportResponse(const Response &response);
|
||||
|
||||
private:
|
||||
static QString categoryLabel(const QString &key);
|
||||
static QString categoryToolTip(const QString &key);
|
||||
|
||||
AbstractClient *client;
|
||||
QString reportedUser;
|
||||
int gameId;
|
||||
|
|
|
|||
96
cockatrice/src/interface/widgets/dialogs/dlg_share_deck.cpp
Normal file
96
cockatrice/src/interface/widgets/dialogs/dlg_share_deck.cpp
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
#include "dlg_share_deck.h"
|
||||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../cards/additional_info/deck_color_identity.h"
|
||||
#include "../deck_share/deck_share_utils.h"
|
||||
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFormLayout>
|
||||
#include <QLineEdit>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
||||
#include <libcockatrice/protocol/pb/command_deck_share_create.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_deck_share_create.pb.h>
|
||||
#include <libcockatrice/protocol/pending_command.h>
|
||||
#include <libcockatrice/settings/network_settings.h>
|
||||
|
||||
DlgShareDeck::DlgShareDeck(AbstractClient *_client, const QSharedPointer<DeckList> &_deck, QWidget *_parent)
|
||||
: QDialog(_parent), client(_client), deck(_deck), shareTimeoutTimer(new QTimer(this))
|
||||
{
|
||||
setWindowTitle(tr("Share deck"));
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
|
||||
nameEdit = new QLineEdit(this);
|
||||
nameEdit->setText(tr("Shared deck"));
|
||||
|
||||
auto *form = new QFormLayout;
|
||||
form->addRow(tr("Share name:"), nameEdit);
|
||||
layout->addLayout(form);
|
||||
|
||||
auto *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||||
buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Create share link"));
|
||||
buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel"));
|
||||
connect(buttonBox, &QDialogButtonBox::accepted, this, &DlgShareDeck::actShare);
|
||||
connect(buttonBox, &QDialogButtonBox::rejected, this, &DlgShareDeck::reject);
|
||||
this->buttonBox = buttonBox;
|
||||
layout->addWidget(buttonBox);
|
||||
|
||||
shareTimeoutTimer->setSingleShot(true);
|
||||
shareTimeoutTimer->setInterval(
|
||||
static_cast<int>((static_cast<qint64>(SettingsCache::instance().network().getTimeOut()) + 1) *
|
||||
SettingsCache::instance().network().getKeepAlive() * 1000));
|
||||
connect(shareTimeoutTimer, &QTimer::timeout, this, &DlgShareDeck::onShareTimeout);
|
||||
}
|
||||
|
||||
void DlgShareDeck::actShare()
|
||||
{
|
||||
buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
|
||||
|
||||
Command_DeckShareCreate cmd;
|
||||
cmd.set_name(nameEdit->text().trimmed().toStdString());
|
||||
if (cmd.name().empty()) {
|
||||
cmd.set_name(tr("Shared deck").toStdString());
|
||||
}
|
||||
|
||||
DeckShareItem *item = cmd.add_items();
|
||||
item->set_deck_list(deck->writeToString_Native().toStdString());
|
||||
item->set_color_identity(getDeckColorIdentity(*deck, CardDatabaseManager::query()).toStdString());
|
||||
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &DlgShareDeck::shareFinished);
|
||||
client->sendCommand(pend);
|
||||
shareTimeoutTimer->start();
|
||||
}
|
||||
|
||||
void DlgShareDeck::shareFinished(const Response &response, const CommandContainer & /*commandContainer*/)
|
||||
{
|
||||
shareTimeoutTimer->stop();
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true);
|
||||
QMessageBox::critical(this, tr("Share deck"),
|
||||
tr("Failed to create the share link (server response code %1).")
|
||||
.arg(QString::number(static_cast<int>(response.response_code()))));
|
||||
return;
|
||||
}
|
||||
|
||||
const DeckShareUtils::ShareResponse share = DeckShareUtils::handleShareResponse(client, response);
|
||||
|
||||
QMessageBox::information(this, tr("Share deck"),
|
||||
tr("Share link created and copied to the clipboard:\n\n%1\n\n"
|
||||
"The share expires on %2.")
|
||||
.arg(share.link, DeckShareUtils::formatShareExpiry(share.expiry)));
|
||||
accept();
|
||||
}
|
||||
|
||||
void DlgShareDeck::onShareTimeout()
|
||||
{
|
||||
buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true);
|
||||
QMessageBox::warning(this, tr("Share deck"), tr("The server did not respond in time. Try again."));
|
||||
}
|
||||
46
cockatrice/src/interface/widgets/dialogs/dlg_share_deck.h
Normal file
46
cockatrice/src/interface/widgets/dialogs/dlg_share_deck.h
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/**
|
||||
* @file dlg_share_deck.h
|
||||
* @ingroup Dialogs
|
||||
*/
|
||||
//! \todo Document this file.
|
||||
|
||||
#ifndef DLG_SHARE_DECK_H
|
||||
#define DLG_SHARE_DECK_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QSharedPointer>
|
||||
|
||||
class AbstractClient;
|
||||
class CommandContainer;
|
||||
class DeckList;
|
||||
class QDialogButtonBox;
|
||||
class QLineEdit;
|
||||
class QTimer;
|
||||
class Response;
|
||||
|
||||
/**
|
||||
* @brief Slim dialog to create a temporary share for the deck open in the editor.
|
||||
*
|
||||
* Asks for a share name, sends Command_DeckShareCreate for the single inline
|
||||
* deck, and copies the resulting link to the clipboard.
|
||||
*/
|
||||
class DlgShareDeck : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
DlgShareDeck(AbstractClient *_client, const QSharedPointer<DeckList> &_deck, QWidget *parent = nullptr);
|
||||
|
||||
private slots:
|
||||
void actShare();
|
||||
void shareFinished(const Response &response, const CommandContainer &commandContainer);
|
||||
void onShareTimeout();
|
||||
|
||||
private:
|
||||
AbstractClient *client;
|
||||
QSharedPointer<DeckList> deck;
|
||||
QLineEdit *nameEdit;
|
||||
QDialogButtonBox *buttonBox;
|
||||
QTimer *shareTimeoutTimer;
|
||||
};
|
||||
|
||||
#endif // DLG_SHARE_DECK_H
|
||||
|
|
@ -0,0 +1,181 @@
|
|||
#include "dlg_shared_decks_preview.h"
|
||||
|
||||
#include "../deck_share/shared_deck_preview_widget.h"
|
||||
#include "../general/layout_containers/flow_widget.h"
|
||||
|
||||
#include <QCloseEvent>
|
||||
#include <QDateTime>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/card/database/card_database_querier.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_deck_share_item.pb.h>
|
||||
|
||||
DlgSharedDecksPreview::DlgSharedDecksPreview(QWidget *parent,
|
||||
const CardDatabaseQuerier *querier,
|
||||
const QString &shareName,
|
||||
qint64 expiresAt,
|
||||
const QString &serverText,
|
||||
const QList<ServerInfo_DeckShareItem> &items)
|
||||
: QDialog(parent)
|
||||
{
|
||||
setWindowTitle(tr("Open shared decks"));
|
||||
resize(700, 500);
|
||||
|
||||
auto *mainLayout = new QVBoxLayout(this);
|
||||
|
||||
// shareName and serverText come from the share server, so escape them: the
|
||||
// QLabels render AutoText and markup would otherwise be shown as rich text.
|
||||
auto *titleLabel =
|
||||
new QLabel(tr("Share: %1").arg((shareName.isEmpty() ? tr("Untitled") : shareName).toHtmlEscaped()), this);
|
||||
QFont titleFont = titleLabel->font();
|
||||
titleFont.setBold(true);
|
||||
titleFont.setPointSize(titleFont.pointSize() + 2);
|
||||
titleLabel->setFont(titleFont);
|
||||
mainLayout->addWidget(titleLabel);
|
||||
|
||||
if (!serverText.isEmpty()) {
|
||||
mainLayout->addWidget(new QLabel(tr("From %1").arg(serverText.toHtmlEscaped()), this));
|
||||
}
|
||||
|
||||
if (expiresAt > 0) {
|
||||
const QString expiryText = QDateTime::fromSecsSinceEpoch(expiresAt).toLocalTime().toString(Qt::TextDate);
|
||||
mainLayout->addWidget(new QLabel(tr("This share link expires on %1").arg(expiryText), this));
|
||||
}
|
||||
|
||||
downloadStatusLabel = new QLabel(this);
|
||||
downloadStatusLabel->setVisible(false);
|
||||
mainLayout->addWidget(downloadStatusLabel);
|
||||
|
||||
flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded);
|
||||
mainLayout->addWidget(flowWidget, 1);
|
||||
|
||||
for (const ServerInfo_DeckShareItem &item : items) {
|
||||
QStringList tags;
|
||||
for (const auto &tag : item.tags()) {
|
||||
tags.append(QString::fromStdString(tag));
|
||||
}
|
||||
|
||||
auto *tile = new SharedDeckPreviewWidget(
|
||||
this, querier, QString::fromStdString(item.name()), QString::fromStdString(item.banner_card()),
|
||||
QString::fromStdString(item.color_identity()), QString::fromStdString(item.game_format()), tags.join(", "));
|
||||
flowWidget->addNavigableWidget(tile);
|
||||
tiles.append(tile);
|
||||
itemIds.append(item.id());
|
||||
}
|
||||
|
||||
if (tiles.size() == 1) {
|
||||
tiles.first()->setSelected(true);
|
||||
}
|
||||
|
||||
auto *buttonBox = new QDialogButtonBox(this);
|
||||
openSelectedButton = buttonBox->addButton(tr("Open selected"), QDialogButtonBox::AcceptRole);
|
||||
openAllButton = buttonBox->addButton(tr("Open all"), QDialogButtonBox::ActionRole);
|
||||
buttonBox->addButton(tr("Cancel"), QDialogButtonBox::RejectRole);
|
||||
mainLayout->addWidget(buttonBox);
|
||||
|
||||
connect(buttonBox, &QDialogButtonBox::rejected, this, [this]() {
|
||||
onCancel();
|
||||
close();
|
||||
});
|
||||
|
||||
// Esc calls QDialog::reject() directly (which hides the dialog without a
|
||||
// close event), so route it through the same guarded cancel as the button.
|
||||
connect(this, &QDialog::rejected, this, [this]() {
|
||||
onCancel();
|
||||
close();
|
||||
});
|
||||
|
||||
connect(openSelectedButton, &QPushButton::clicked, this, &DlgSharedDecksPreview::openSelected);
|
||||
connect(buttonBox, &QDialogButtonBox::clicked, this, [this, buttonBox](QAbstractButton *button) {
|
||||
if (buttonBox->buttonRole(button) == QDialogButtonBox::ActionRole) {
|
||||
openAll();
|
||||
}
|
||||
});
|
||||
|
||||
for (SharedDeckPreviewWidget *tile : tiles) {
|
||||
connect(tile, &SharedDeckPreviewWidget::selectionToggled, this,
|
||||
&DlgSharedDecksPreview::updateOpenSelectedEnabled);
|
||||
}
|
||||
for (int i = 0; i < tiles.size(); ++i) {
|
||||
const int itemId = itemIds.at(i);
|
||||
// Double-clicking a tile selects it and opens just that deck.
|
||||
connect(tiles.at(i), &SharedDeckPreviewWidget::activated, this, [this, itemId]() {
|
||||
resultEmitted = true;
|
||||
setDownloading(true);
|
||||
emit openRequested(QList<int>{itemId});
|
||||
});
|
||||
}
|
||||
updateOpenSelectedEnabled();
|
||||
}
|
||||
|
||||
QList<int> DlgSharedDecksPreview::selectedItemIds() const
|
||||
{
|
||||
QList<int> selectedIds;
|
||||
for (int i = 0; i < tiles.size(); ++i) {
|
||||
if (tiles.at(i)->isSelected()) {
|
||||
selectedIds.append(itemIds.at(i));
|
||||
}
|
||||
}
|
||||
return selectedIds;
|
||||
}
|
||||
|
||||
void DlgSharedDecksPreview::openSelected()
|
||||
{
|
||||
const QList<int> selectedIds = selectedItemIds();
|
||||
if (selectedIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
resultEmitted = true;
|
||||
setDownloading(true);
|
||||
emit openRequested(selectedIds);
|
||||
}
|
||||
|
||||
void DlgSharedDecksPreview::openAll()
|
||||
{
|
||||
resultEmitted = true;
|
||||
setDownloading(true);
|
||||
emit openRequested(itemIds);
|
||||
}
|
||||
|
||||
void DlgSharedDecksPreview::setDownloading(bool downloading)
|
||||
{
|
||||
if (downloadInProgress == downloading) {
|
||||
return;
|
||||
}
|
||||
downloadInProgress = downloading;
|
||||
downloadStatusLabel->setVisible(downloading);
|
||||
for (SharedDeckPreviewWidget *tile : tiles) {
|
||||
tile->setEnabled(!downloading);
|
||||
}
|
||||
openSelectedButton->setEnabled(!downloading);
|
||||
openAllButton->setEnabled(!downloading);
|
||||
}
|
||||
|
||||
void DlgSharedDecksPreview::setDownloadProgress(int done, int total, const QString ¤tDeckName)
|
||||
{
|
||||
if (!downloadInProgress) {
|
||||
return;
|
||||
}
|
||||
downloadStatusLabel->setText(tr("Downloading deck %1 of %2: %3").arg(done).arg(total).arg(currentDeckName));
|
||||
}
|
||||
|
||||
void DlgSharedDecksPreview::updateOpenSelectedEnabled()
|
||||
{
|
||||
openSelectedButton->setEnabled(!selectedItemIds().isEmpty());
|
||||
}
|
||||
|
||||
void DlgSharedDecksPreview::onCancel()
|
||||
{
|
||||
if (!resultEmitted || downloadInProgress) {
|
||||
resultEmitted = true;
|
||||
emit cancelled();
|
||||
}
|
||||
}
|
||||
|
||||
void DlgSharedDecksPreview::closeEvent(QCloseEvent *event)
|
||||
{
|
||||
onCancel();
|
||||
QDialog::closeEvent(event);
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
#ifndef COCKATRICE_DLG_SHARED_DECKS_PREVIEW_H
|
||||
#define COCKATRICE_DLG_SHARED_DECKS_PREVIEW_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QList>
|
||||
|
||||
class FlowWidget;
|
||||
class QCloseEvent;
|
||||
class QLabel;
|
||||
class QPushButton;
|
||||
class ServerInfo_DeckShareItem;
|
||||
class SharedDeckPreviewWidget;
|
||||
class CardDatabaseQuerier;
|
||||
|
||||
/**
|
||||
* @brief Non-modal preview of the decks contained in a shared-deck link.
|
||||
*
|
||||
* Lets the user pick which of the shared decks to open before anything is
|
||||
* downloaded. Emits openRequested with the ids of the chosen decks, or
|
||||
* cancelled when the user closes the dialog without choosing. Once the user
|
||||
* picks, the dialog switches into a "downloading" state: the tiles and open
|
||||
* buttons are disabled, a progress label shows the current download and Cancel
|
||||
* stays functional so the download can be aborted.
|
||||
*/
|
||||
class DlgSharedDecksPreview : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit DlgSharedDecksPreview(QWidget *parent,
|
||||
const CardDatabaseQuerier *querier,
|
||||
const QString &shareName,
|
||||
qint64 expiresAt,
|
||||
const QString &serverText,
|
||||
const QList<ServerInfo_DeckShareItem> &items);
|
||||
|
||||
void setDownloadProgress(int done, int total, const QString ¤tDeckName);
|
||||
|
||||
public slots:
|
||||
void setDownloading(bool downloading);
|
||||
|
||||
signals:
|
||||
void openRequested(const QList<int> &itemIds);
|
||||
void cancelled();
|
||||
|
||||
protected:
|
||||
void closeEvent(QCloseEvent *event) override;
|
||||
|
||||
private slots:
|
||||
void openSelected();
|
||||
void openAll();
|
||||
void updateOpenSelectedEnabled();
|
||||
void onCancel();
|
||||
|
||||
private:
|
||||
QList<int> selectedItemIds() const;
|
||||
|
||||
FlowWidget *flowWidget;
|
||||
QList<SharedDeckPreviewWidget *> tiles;
|
||||
QList<int> itemIds;
|
||||
QPushButton *openSelectedButton;
|
||||
QPushButton *openAllButton;
|
||||
QLabel *downloadStatusLabel;
|
||||
bool resultEmitted = false;
|
||||
bool downloadInProgress = false;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_DLG_SHARED_DECKS_PREVIEW_H
|
||||
|
|
@ -5,15 +5,25 @@
|
|||
#include "../client/network/update/client/release_channel.h"
|
||||
#include "../interface/window_main.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDesktopServices>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QProgressBar>
|
||||
#include <QPushButton>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
#include <QtNetwork>
|
||||
#include <version_string.h>
|
||||
|
||||
// Executable that, when it sits next to the downloaded update installer, is installed instead of
|
||||
// it. A packager shipping a custom build - or someone testing one - can drop the file there and
|
||||
// have Cockatrice run it rather than the official installer, without the release channel having to
|
||||
// host an installer for that build. It is a drop-in and is run with the same arguments.
|
||||
static const QString UPDATE_INSTALLER_OVERRIDE = "Cockatrice-Update-Override.exe";
|
||||
|
||||
DlgUpdate::DlgUpdate(QWidget *parent) : QDialog(parent)
|
||||
{
|
||||
|
||||
|
|
@ -203,6 +213,24 @@ void DlgUpdate::setLabel(const QString &newText)
|
|||
statusLabel->setText(newText);
|
||||
}
|
||||
|
||||
void DlgUpdate::warnInstallerIsWaiting()
|
||||
{
|
||||
// Modeless on purpose: the installer asks the application to close exactly once (see
|
||||
// CloseMatchingApps in NSIS.template.in) and only polls afterwards, while Qt drops spontaneous
|
||||
// close events - a WM_CLOSE from the installer - for windows that are blocked by a modal
|
||||
// widget (QGuiApplicationPrivate::processCloseEvent). An application modal message box here
|
||||
// would therefore swallow the installer's one and only request for the rest of its wait.
|
||||
auto *notification = new QMessageBox(QMessageBox::Warning, tr("Update"),
|
||||
tr("The update installer is already running and waits about a "
|
||||
"minute for Cockatrice to close. Cockatrice is still busy, so "
|
||||
"save your work and close it before then. Otherwise the "
|
||||
"installer gives up and the update is cancelled."),
|
||||
QMessageBox::Ok, parentWidget());
|
||||
notification->setWindowModality(Qt::NonModal);
|
||||
notification->setAttribute(Qt::WA_DeleteOnClose);
|
||||
notification->show();
|
||||
}
|
||||
|
||||
void DlgUpdate::updateCheckError(const QString &errorString)
|
||||
{
|
||||
setLabel(tr("Error"));
|
||||
|
|
@ -224,6 +252,12 @@ void DlgUpdate::downloadSuccessful(const QUrl &filepath)
|
|||
|
||||
QString installerPath = filepath.toLocalFile();
|
||||
|
||||
const QString overridePath = QDir(QFileInfo(installerPath).absolutePath()).filePath(UPDATE_INSTALLER_OVERRIDE);
|
||||
if (QFileInfo::exists(overridePath)) {
|
||||
qCInfo(DlgUpdateLog) << "Installing the update installer override instead of the download:" << overridePath;
|
||||
installerPath = overridePath;
|
||||
}
|
||||
|
||||
QString appDir = QDir::toNativeSeparators(QCoreApplication::applicationDirPath());
|
||||
QProcess process;
|
||||
process.setProgram(installerPath);
|
||||
|
|
@ -231,8 +265,16 @@ void DlgUpdate::downloadSuccessful(const QUrl &filepath)
|
|||
// 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.
|
||||
//
|
||||
// /PID= tells the installer which process to wait for before it replaces anything. We launch the installer first
|
||||
// and only quit afterwards, so it starts while this process is still alive and still has every Qt runtime DLL
|
||||
// mapped, and a mapped DLL cannot be deleted. Without the handle the installer can only poll for an image name and
|
||||
// guess, and a wrong guess is what leaves the update writing new executables next to runtime files of the previous
|
||||
// version, which fails on the next start with "The procedure entry point ... could not be located in the dynamic
|
||||
// link library
|
||||
// ...Qt6QuickWidgets.dll". /D= has to stay the last parameter, which is why it is last here too.
|
||||
#ifdef Q_OS_WIN
|
||||
process.setNativeArguments(QString("/R /D=%1").arg(appDir));
|
||||
process.setNativeArguments(QString("/R /PID=%1 /D=%2").arg(QCoreApplication::applicationPid()).arg(appDir));
|
||||
#else
|
||||
// Linux/macOS: normal argument passing (not relevant since they update differently.)
|
||||
process.setArguments({"/R", QString("/D=%1").arg(appDir)});
|
||||
|
|
@ -240,8 +282,31 @@ void DlgUpdate::downloadSuccessful(const QUrl &filepath)
|
|||
|
||||
// Try to open the installer. If it opens, quit Cockatrice
|
||||
if (process.startDetached()) {
|
||||
QMetaObject::invokeMethod(static_cast<MainWindow *>(parent()), "close", Qt::QueuedConnection);
|
||||
qCInfo(DlgUpdateLog) << "Opened downloaded update file successfully - closing Cockatrice";
|
||||
qCInfo(DlgUpdateLog) << "Opened update installer successfully - closing Cockatrice";
|
||||
// Close the main window synchronously so file locks are released before the NSIS installer
|
||||
// (already launched) starts replacing files. This also flushes settings and shuts down the
|
||||
// tabs, but only when the close is actually accepted: MainWindow may veto it for a running
|
||||
// card DB update, an open game, or an unsaved deck, and closeForUpdate() also reports a
|
||||
// close already in progress (reached from a nested event loop while a shutdown prompt is
|
||||
// up). Only quit when the shutdown really ran - otherwise keep running so the user can
|
||||
// resolve the blocker, and warn them that the installer only waits about a minute
|
||||
// before it gives up and cancels the update.
|
||||
if (auto *window = qobject_cast<MainWindow *>(parent())) {
|
||||
if (window->closeForUpdate()) {
|
||||
QTimer::singleShot(0, qApp, [] { QCoreApplication::exit(0); });
|
||||
} else {
|
||||
warnInstallerIsWaiting();
|
||||
}
|
||||
} else {
|
||||
// Not a MainWindow, so no faithful close can be requested - but the parent still gets
|
||||
// its close() call, which is what the code before this change did and lets it flush
|
||||
// settings and shut down its tabs. Leaving is then unconditional: the installer is
|
||||
// already running against a process that still holds locks on the files it replaces.
|
||||
if (auto *widget = parentWidget()) {
|
||||
widget->close();
|
||||
}
|
||||
QTimer::singleShot(0, qApp, [] { QCoreApplication::exit(0); });
|
||||
}
|
||||
close();
|
||||
} else {
|
||||
setLabel(tr("Error"));
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ private:
|
|||
void addStopDownloadAndRemoveOthers(bool enable);
|
||||
void beginUpdateCheck();
|
||||
void setLabel(const QString &text);
|
||||
void warnInstallerIsWaiting();
|
||||
QLabel *statusLabel, *descriptionLabel;
|
||||
QProgressBar *progress;
|
||||
QPushButton *manualDownload, *gotoDownload, *ok, *stopDownload;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
#include "flow_widget.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QKeyEvent>
|
||||
#include <QResizeEvent>
|
||||
#include <QScrollArea>
|
||||
#include <QSizePolicy>
|
||||
|
|
@ -80,13 +81,35 @@ FlowWidget::FlowWidget(QWidget *parent,
|
|||
/**
|
||||
* @brief Adds a widget to the flow layout within the FlowWidget.
|
||||
*
|
||||
* Plain widgets are not filtered for arrow keys: intercepting them would steal
|
||||
* Up/Down/Left/Right from controls that use them (combo boxes, spin boxes
|
||||
* etc.). Widgets that want keyboard navigation between flow items must be
|
||||
* added via addNavigableWidget instead.
|
||||
*
|
||||
* @param widget_to_add The widget to add to the flow layout.
|
||||
*/
|
||||
void FlowWidget::addWidget(QWidget *widget_to_add) const
|
||||
void FlowWidget::addWidget(QWidget *widget_to_add)
|
||||
{
|
||||
flowLayout->addWidget(widget_to_add);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Adds a widget and routes its arrow keys to FlowWidget focus navigation.
|
||||
*
|
||||
* The widget is filtered for arrow-key events so keyboard navigation between
|
||||
* the flow items keeps working even when the flow sits inside a QScrollArea,
|
||||
* which swallows arrow keys before they can reach FlowWidget::keyPressEvent.
|
||||
* Only widgets added through this method are affected; anything that needs its
|
||||
* own arrow keys should use plain addWidget.
|
||||
*
|
||||
* @param widget_to_add The widget to add to the flow layout.
|
||||
*/
|
||||
void FlowWidget::addNavigableWidget(QWidget *widget_to_add)
|
||||
{
|
||||
widget_to_add->installEventFilter(this);
|
||||
flowLayout->addWidget(widget_to_add);
|
||||
}
|
||||
|
||||
void FlowWidget::insertWidgetAtIndex(QWidget *toInsert, int index)
|
||||
{
|
||||
flowLayout->insertWidgetAtIndex(toInsert, index);
|
||||
|
|
@ -177,6 +200,66 @@ QLayoutItem *FlowWidget::itemAt(int index) const
|
|||
return flowLayout->itemAt(index);
|
||||
}
|
||||
|
||||
void FlowWidget::keyPressEvent(QKeyEvent *event)
|
||||
{
|
||||
if (moveFocus(event)) {
|
||||
event->accept();
|
||||
return;
|
||||
}
|
||||
QWidget::keyPressEvent(event);
|
||||
}
|
||||
|
||||
bool FlowWidget::eventFilter(QObject *watched, QEvent *event)
|
||||
{
|
||||
if (event->type() == QEvent::KeyPress && moveFocus(static_cast<QKeyEvent *>(event))) {
|
||||
return true;
|
||||
}
|
||||
return QWidget::eventFilter(watched, event);
|
||||
}
|
||||
|
||||
bool FlowWidget::moveFocus(QKeyEvent *event)
|
||||
{
|
||||
// Keyboard navigation between the flow items: arrow keys move focus just
|
||||
// like clicking the sibling tiles would. Only items that can take keyboard
|
||||
// focus (e.g. the deck-preview tiles in shared-deck links) are visited.
|
||||
const bool moveForward = event->key() == Qt::Key_Right || event->key() == Qt::Key_Down;
|
||||
const bool moveBackward = event->key() == Qt::Key_Left || event->key() == Qt::Key_Up;
|
||||
if (!moveForward && !moveBackward) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QList<QWidget *> focusableItems;
|
||||
for (int i = 0; i < flowLayout->count(); ++i) {
|
||||
QWidget *item = flowLayout->itemAt(i)->widget();
|
||||
if (item != nullptr && (item->focusPolicy() & Qt::TabFocus)) {
|
||||
focusableItems.append(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (focusableItems.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int currentIndex = -1;
|
||||
for (int i = 0; i < focusableItems.size(); ++i) {
|
||||
if (focusableItems.at(i)->hasFocus()) {
|
||||
currentIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const int delta = moveForward ? 1 : -1;
|
||||
int nextIndex;
|
||||
if (currentIndex < 0) {
|
||||
nextIndex = moveForward ? 0 : focusableItems.size() - 1;
|
||||
} else {
|
||||
nextIndex = (currentIndex + delta + focusableItems.size()) % focusableItems.size();
|
||||
}
|
||||
focusableItems.value(nextIndex)->setFocus();
|
||||
event->accept();
|
||||
return true;
|
||||
}
|
||||
|
||||
int FlowWidget::count() const
|
||||
{
|
||||
return flowLayout->count();
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
#include "../../../layouts/flow_layout.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QKeyEvent>
|
||||
#include <QLoggingCategory>
|
||||
#include <QScrollArea>
|
||||
#include <QWidget>
|
||||
|
|
@ -28,7 +29,8 @@ public:
|
|||
Qt::ScrollBarPolicy horizontalPolicy,
|
||||
Qt::ScrollBarPolicy verticalPolicy);
|
||||
|
||||
void addWidget(QWidget *widget_to_add) const;
|
||||
void addWidget(QWidget *widget_to_add);
|
||||
void addNavigableWidget(QWidget *widget_to_add);
|
||||
void insertWidgetAtIndex(QWidget *toInsert, int index);
|
||||
void removeWidget(QWidget *widgetToRemove) const;
|
||||
void clearLayout();
|
||||
|
|
@ -43,9 +45,15 @@ public slots:
|
|||
void setSpacing(int hSpacing, int vSpacing);
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
void keyPressEvent(QKeyEvent *event) override;
|
||||
|
||||
private:
|
||||
/// @brief Moves keyboard focus to an adjacent flow item for an arrow-key event.
|
||||
/// @return True when the event was an arrow key and was handled.
|
||||
bool moveFocus(QKeyEvent *event);
|
||||
|
||||
Qt::Orientation flowDirection;
|
||||
QHBoxLayout *mainLayout;
|
||||
FlowLayout *flowLayout;
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@ DeckEditorMenu::DeckEditorMenu(AbstractTabDeckEditor *parent) : QMenu(parent), d
|
|||
aSaveDeckAs = new QAction(QString(), this);
|
||||
connect(aSaveDeckAs, &QAction::triggered, deckEditor, &AbstractTabDeckEditor::actSaveDeckAs);
|
||||
|
||||
aShareDeck = new QAction(QString(), this);
|
||||
connect(aShareDeck, &QAction::triggered, deckEditor, &AbstractTabDeckEditor::actShareDeck);
|
||||
|
||||
aLoadDeckFromClipboard = new QAction(QString(), this);
|
||||
connect(aLoadDeckFromClipboard, &QAction::triggered, deckEditor, &AbstractTabDeckEditor::actLoadDeckFromClipboard);
|
||||
|
||||
|
|
@ -96,6 +99,7 @@ DeckEditorMenu::DeckEditorMenu(AbstractTabDeckEditor *parent) : QMenu(parent), d
|
|||
addMenu(loadRecentDeckMenu);
|
||||
addAction(aSaveDeck);
|
||||
addAction(aSaveDeckAs);
|
||||
addAction(aShareDeck);
|
||||
addSeparator();
|
||||
addAction(aLoadDeckFromClipboard);
|
||||
addMenu(editDeckInClipboardMenu);
|
||||
|
|
@ -120,6 +124,7 @@ void DeckEditorMenu::setSaveStatus(bool newStatus)
|
|||
{
|
||||
aSaveDeck->setEnabled(newStatus);
|
||||
aSaveDeckAs->setEnabled(newStatus);
|
||||
aShareDeck->setEnabled(newStatus);
|
||||
aSaveDeckToClipboard->setEnabled(newStatus);
|
||||
aSaveDeckToClipboardNoSetInfo->setEnabled(newStatus);
|
||||
aSaveDeckToClipboardRaw->setEnabled(newStatus);
|
||||
|
|
@ -157,6 +162,7 @@ void DeckEditorMenu::retranslateUi()
|
|||
aClearRecents->setText(tr("Clear"));
|
||||
aSaveDeck->setText(tr("&Save deck"));
|
||||
aSaveDeckAs->setText(tr("Save deck &as..."));
|
||||
aShareDeck->setText(tr("Share deck..."));
|
||||
|
||||
aLoadDeckFromClipboard->setText(tr("Load deck from cl&ipboard..."));
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@ public:
|
|||
QAction *aNewDeck, *aLoadDeck, *aClearRecents, *aSaveDeck, *aSaveDeckAs, *aLoadDeckFromClipboard,
|
||||
*aEditDeckInClipboard, *aEditDeckInClipboardRaw, *aSaveDeckToClipboard, *aSaveDeckToClipboardNoSetInfo,
|
||||
*aSaveDeckToClipboardRaw, *aSaveDeckToClipboardRawNoSetInfo, *aPrintDeck, *aLoadDeckFromWebsite,
|
||||
*aExportDeckDecklist, *aExportDeckDecklistXyz, *aAnalyzeDeckDeckstats, *aAnalyzeDeckTappedout, *aClose;
|
||||
*aExportDeckDecklist, *aExportDeckDecklistXyz, *aAnalyzeDeckDeckstats, *aAnalyzeDeckTappedout, *aShareDeck,
|
||||
*aClose;
|
||||
QMenu *loadRecentDeckMenu, *analyzeDeckMenu, *editDeckInClipboardMenu, *saveDeckToClipboardMenu;
|
||||
|
||||
void setSaveStatus(bool newStatus);
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ PrintingSelector::PrintingSelector(QWidget *parent, AbstractTabDeckEditor *_deck
|
|||
SettingsCache::instance().cardsDisplay().getPrintingSelectorCardSize());
|
||||
connect(cardSizeWidget, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance().cardsDisplay(),
|
||||
&CardsDisplaySettings::setPrintingSelectorCardSize);
|
||||
cardSizeWidget->enableCtrlScrollResize(flowWidget);
|
||||
|
||||
displayOptionsWidget->addSettingsWidget(sortToolBar);
|
||||
displayOptionsWidget->addSettingsWidget(navigationCheckBox);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include "../../../../client/settings/cache_settings.h"
|
||||
#include "../../client/sound_engine.h"
|
||||
#include "../../interface/card_localization.h"
|
||||
#include "../../interface/pixel_map_generator.h"
|
||||
#include "../../interface/widgets/tabs/tab_account.h"
|
||||
#include "../user/user_context_menu.h"
|
||||
|
|
@ -167,7 +168,7 @@ void ChatView::appendHtmlServerMessage(const QString &html, bool optionalIsBold,
|
|||
|
||||
QString htmlText =
|
||||
"<font color=" + ((optionalFontColor.size() > 0) ? optionalFontColor : serverMessageColor.name()) + ">" +
|
||||
QDateTime::currentDateTime().toString("[hh:mm:ss] ") + html + "</font>";
|
||||
getCurrentTime() + html + "</font>";
|
||||
|
||||
if (optionalIsBold) {
|
||||
htmlText = "<b>" + htmlText + "</b>";
|
||||
|
|
@ -179,13 +180,19 @@ void ChatView::appendHtmlServerMessage(const QString &html, bool optionalIsBold,
|
|||
}
|
||||
}
|
||||
|
||||
QString ChatView::getCurrentTime() const
|
||||
{
|
||||
return QDateTime::currentDateTime().toString("[hh:mm:ss] ");
|
||||
}
|
||||
|
||||
void ChatView::appendCardTag(QTextCursor &cursor, const QString &cardName)
|
||||
{
|
||||
QTextCharFormat oldFormat = cursor.charFormat();
|
||||
QTextCharFormat cardFormat = oldFormat;
|
||||
cardFormat.setFontItalic(true);
|
||||
|
||||
if (!CardDatabaseManager::query()->lookupCardByName(cardName)) {
|
||||
const CardInfoPtr card = CardDatabaseManager::query()->lookupCardByName(cardName);
|
||||
if (card.isNull()) {
|
||||
cardFormat.setForeground(unresolvedCardTagColor);
|
||||
cursor.setCharFormat(cardFormat);
|
||||
cursor.insertText(cardName);
|
||||
|
|
@ -195,10 +202,12 @@ void ChatView::appendCardTag(QTextCursor &cursor, const QString &cardName)
|
|||
|
||||
cardFormat.setForeground(linkColor);
|
||||
cardFormat.setAnchor(true);
|
||||
// The href keeps the canonical name so the card popup can resolve it, while the text shows the name
|
||||
// in the configured card language.
|
||||
cardFormat.setAnchorHref("card://" + cardName);
|
||||
|
||||
cursor.setCharFormat(cardFormat);
|
||||
cursor.insertText(cardName);
|
||||
cursor.insertText(CardLocalization::displayName(card));
|
||||
cursor.setCharFormat(oldFormat);
|
||||
}
|
||||
|
||||
|
|
@ -290,7 +299,7 @@ void ChatView::appendMessage(QString message,
|
|||
timeFormat.setForeground(serverMessageColor);
|
||||
timeFormat.setFontWeight(QFont::Bold);
|
||||
cursor.setCharFormat(timeFormat);
|
||||
cursor.insertText(QDateTime::currentDateTime().toString("[hh:mm:ss] "));
|
||||
cursor.insertText(getCurrentTime());
|
||||
}
|
||||
|
||||
// nickname
|
||||
|
|
@ -340,16 +349,27 @@ void ChatView::appendMessage(QString message,
|
|||
pos.relativePosition = match.captured(0).length(); // set message start
|
||||
auto before = match.captured(1);
|
||||
auto sentBy = match.captured(2);
|
||||
|
||||
// The user level is not carried in the room chat history, so history
|
||||
// entries used to render as fixed-level user tags. Resolve online users
|
||||
// against the user list to turn their history entries into full user
|
||||
// tags (correct level, name casing and moderation context menu).
|
||||
QString displayName = sentBy;
|
||||
// Offline users have no known level; render them as zero-level tags.
|
||||
QString levelMarker = "0";
|
||||
if (const ServerInfo_User *onlineUser = userListProxy->getOnlineUser(sentBy)) {
|
||||
displayName = QString::fromStdString(onlineUser->name());
|
||||
levelMarker = QString::number(onlineUser->user_level());
|
||||
}
|
||||
|
||||
cursor.insertText(before); // add message timestamp
|
||||
QTextCharFormat senderFormat(defaultFormat);
|
||||
senderFormat.setAnchor(true);
|
||||
// this underscore is important, it is used to add the user level, but in this case the level is
|
||||
// unknown, if the name contains an underscore it would split up the name
|
||||
senderFormat.setAnchorHref("user://_" + sentBy);
|
||||
senderFormat.setAnchorHref("user://" + levelMarker + "_" + displayName);
|
||||
cursor.setCharFormat(senderFormat);
|
||||
cursor.insertText(sentBy); // add username with href so it shows the menu
|
||||
userMessagePositions[sentBy].append(pos); // save message position
|
||||
message.remove(0, pos.relativePosition - 2); // do not remove semicolon
|
||||
cursor.insertText(displayName); // add username with href so it shows the menu
|
||||
userMessagePositions[displayName].append(pos); // save message position
|
||||
message.remove(0, pos.relativePosition - 2); // do not remove semicolon
|
||||
}
|
||||
} else {
|
||||
//! \todo Remove hardcoded color.
|
||||
|
|
|
|||
|
|
@ -107,9 +107,8 @@ public:
|
|||
ChatView(TabSupervisor *_tabSupervisor, AbstractGame *_game, bool _showTimestamps, QWidget *parent = nullptr);
|
||||
void retranslateUi();
|
||||
void appendHtml(const QString &html);
|
||||
void virtual appendHtmlServerMessage(const QString &html,
|
||||
bool optionalIsBold = false,
|
||||
QString optionalFontColor = QString());
|
||||
virtual void
|
||||
appendHtmlServerMessage(const QString &html, bool optionalIsBold = false, QString optionalFontColor = QString());
|
||||
void appendMessage(QString message,
|
||||
RoomMessageTypeFlags messageType = {},
|
||||
const ServerInfo_User &userInfo = {},
|
||||
|
|
@ -119,6 +118,7 @@ public:
|
|||
QString getRecentChatLog(int maxMessages = 50) const;
|
||||
|
||||
protected:
|
||||
[[nodiscard]] virtual QString getCurrentTime() const;
|
||||
void enterEvent(QEnterEvent *event) override;
|
||||
void leaveEvent(QEvent *event) override;
|
||||
void mouseMoveEvent(QMouseEvent *event) override;
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ int RemoteDeckList_TreeModel::rowCount(const QModelIndex &parent) const
|
|||
|
||||
int RemoteDeckList_TreeModel::columnCount(const QModelIndex & /*parent*/) const
|
||||
{
|
||||
return 3;
|
||||
return 4;
|
||||
}
|
||||
|
||||
QVariant RemoteDeckList_TreeModel::data(const QModelIndex &index, int role) const
|
||||
|
|
@ -121,7 +121,7 @@ QVariant RemoteDeckList_TreeModel::data(const QModelIndex &index, int role) cons
|
|||
if (!index.isValid()) {
|
||||
return QVariant();
|
||||
}
|
||||
if (index.column() >= 3) {
|
||||
if (index.column() >= 4) {
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
|
|
@ -134,12 +134,29 @@ QVariant RemoteDeckList_TreeModel::data(const QModelIndex &index, int role) cons
|
|||
switch (index.column()) {
|
||||
case 0:
|
||||
return node->getName();
|
||||
case 3:
|
||||
// Report the node's own bit, not the inherited effective
|
||||
// state, so it stays in step with what publishing toggles.
|
||||
if (node->isPublic()) {
|
||||
return tr("Public");
|
||||
}
|
||||
return isEffectivelyPublic(node) ? tr("Public (inherited)") : tr("Private");
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
case Qt::DecorationRole:
|
||||
return index.column() == 0 ? dirIcon : QVariant();
|
||||
case Qt::ToolTipRole:
|
||||
if (index.column() == 3) {
|
||||
if (node->isPublic()) {
|
||||
return tr("This folder is visible to other users");
|
||||
}
|
||||
return isEffectivelyPublic(node)
|
||||
? tr("This folder is private, but a parent folder is public (inherited).")
|
||||
: tr("This folder is only visible to you");
|
||||
}
|
||||
return QVariant();
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
|
|
@ -153,6 +170,13 @@ QVariant RemoteDeckList_TreeModel::data(const QModelIndex &index, int role) cons
|
|||
return file->getId();
|
||||
case 2:
|
||||
return file->getUploadTime();
|
||||
case 3:
|
||||
// Report the node's own bit, not the inherited effective
|
||||
// state, so it stays in step with what publishing toggles.
|
||||
if (file->isPublic()) {
|
||||
return tr("Public");
|
||||
}
|
||||
return isEffectivelyPublic(file) ? tr("Public (inherited)") : tr("Private");
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
|
|
@ -161,6 +185,16 @@ QVariant RemoteDeckList_TreeModel::data(const QModelIndex &index, int role) cons
|
|||
return index.column() == 0 ? fileIcon : QVariant();
|
||||
case Qt::TextAlignmentRole:
|
||||
return index.column() == 1 ? Qt::AlignRight : Qt::AlignLeft;
|
||||
case Qt::ToolTipRole:
|
||||
if (index.column() == 3) {
|
||||
if (file->isPublic()) {
|
||||
return tr("This deck is visible to other users");
|
||||
}
|
||||
return isEffectivelyPublic(file)
|
||||
? tr("This deck is private, but a parent folder is public (inherited).")
|
||||
: tr("This deck is only visible to you");
|
||||
}
|
||||
return QVariant();
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
|
|
@ -183,6 +217,8 @@ QVariant RemoteDeckList_TreeModel::headerData(int section, Qt::Orientation orien
|
|||
return tr("ID");
|
||||
case 2:
|
||||
return tr("Upload time");
|
||||
case 3:
|
||||
return tr("Visibility");
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
|
|
@ -239,13 +275,14 @@ void RemoteDeckList_TreeModel::addFileToTree(const ServerInfo_DeckStorage_TreeIt
|
|||
time.setSecsSinceEpoch(fileInfo.creation_time());
|
||||
|
||||
beginInsertRows(nodeToIndex(parent), parent->size(), parent->size());
|
||||
parent->append(new FileNode(QString::fromStdString(file.name()), file.id(), time, parent));
|
||||
parent->append(new FileNode(QString::fromStdString(file.name()), file.id(), time, parent, fileInfo.is_public()));
|
||||
endInsertRows();
|
||||
}
|
||||
|
||||
void RemoteDeckList_TreeModel::addFolderToTree(const ServerInfo_DeckStorage_TreeItem &folder, DirectoryNode *parent)
|
||||
{
|
||||
DirectoryNode *newItem = addNamedFolderToTree(QString::fromStdString(folder.name()), parent);
|
||||
newItem->setIsPublic(folder.folder().is_public());
|
||||
const ServerInfo_DeckStorage_Folder &folderInfo = folder.folder();
|
||||
const int folderItemsSize = folderInfo.items_size();
|
||||
for (int i = 0; i < folderItemsSize; ++i) {
|
||||
|
|
@ -285,6 +322,21 @@ void RemoteDeckList_TreeModel::refreshTree()
|
|||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
bool RemoteDeckList_TreeModel::isEffectivelyPublic(const Node *node) const
|
||||
{
|
||||
if (node == nullptr || node == root) {
|
||||
return false;
|
||||
}
|
||||
const Node *current = node;
|
||||
while (current != nullptr) {
|
||||
if (current->isPublic()) {
|
||||
return true;
|
||||
}
|
||||
current = current->getParent();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void RemoteDeckList_TreeModel::clearTree()
|
||||
{
|
||||
beginResetModel();
|
||||
|
|
|
|||
|
|
@ -27,9 +27,11 @@ public:
|
|||
protected:
|
||||
DirectoryNode *parent;
|
||||
QString name;
|
||||
bool publicFlag;
|
||||
|
||||
public:
|
||||
explicit Node(const QString &_name, DirectoryNode *_parent = nullptr) : parent(_parent), name(_name)
|
||||
explicit Node(const QString &_name, DirectoryNode *_parent = nullptr)
|
||||
: parent(_parent), name(_name), publicFlag(false)
|
||||
{
|
||||
}
|
||||
virtual ~Node() = default;
|
||||
|
|
@ -41,6 +43,14 @@ public:
|
|||
{
|
||||
return name;
|
||||
}
|
||||
[[nodiscard]] bool isPublic() const
|
||||
{
|
||||
return publicFlag;
|
||||
}
|
||||
void setIsPublic(bool _public)
|
||||
{
|
||||
publicFlag = _public;
|
||||
}
|
||||
};
|
||||
class DirectoryNode : public Node, public QList<Node *>
|
||||
{
|
||||
|
|
@ -59,9 +69,14 @@ public:
|
|||
QDateTime uploadTime;
|
||||
|
||||
public:
|
||||
FileNode(const QString &_name, int _id, const QDateTime &_uploadTime, DirectoryNode *_parent = nullptr)
|
||||
FileNode(const QString &_name,
|
||||
int _id,
|
||||
const QDateTime &_uploadTime,
|
||||
DirectoryNode *_parent = nullptr,
|
||||
bool _isPublic = false)
|
||||
: Node(_name, _parent), id(_id), uploadTime(_uploadTime)
|
||||
{
|
||||
setIsPublic(_isPublic);
|
||||
}
|
||||
[[nodiscard]] int getId() const
|
||||
{
|
||||
|
|
@ -109,6 +124,11 @@ public:
|
|||
{
|
||||
return root;
|
||||
}
|
||||
/**
|
||||
* @brief Whether a node is visible to other users (own flag or inherited
|
||||
* from any ancestor folder).
|
||||
*/
|
||||
[[nodiscard]] bool isEffectivelyPublic(const Node *node) const;
|
||||
void addFileToTree(const ServerInfo_DeckStorage_TreeItem &file, DirectoryNode *parent);
|
||||
void addFolderToTree(const ServerInfo_DeckStorage_TreeItem &folder, DirectoryNode *parent);
|
||||
DirectoryNode *addNamedFolderToTree(const QString &name, DirectoryNode *parent);
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ UserContextMenu::UserContextMenu(TabSupervisor *_tabSupervisor, QWidget *parent,
|
|||
aDetails = new QAction(QString(), this);
|
||||
aChat = new QAction(QString(), this);
|
||||
aShowGames = new QAction(QString(), this);
|
||||
aViewPublicDecks = new QAction(QString(), this);
|
||||
aAddToBuddyList = new QAction(QString(), this);
|
||||
aRemoveFromBuddyList = new QAction(QString(), this);
|
||||
aAddToIgnoreList = new QAction(QString(), this);
|
||||
|
|
@ -64,6 +65,7 @@ void UserContextMenu::retranslateUi()
|
|||
aDetails->setText(tr("User &details"));
|
||||
aChat->setText(tr("Private &chat"));
|
||||
aShowGames->setText(tr("Show this user's &games"));
|
||||
aViewPublicDecks->setText(tr("View this user's &public decks"));
|
||||
aAddToBuddyList->setText(tr("Add to &buddy list"));
|
||||
aRemoveFromBuddyList->setText(tr("Remove from &buddy list"));
|
||||
aAddToIgnoreList->setText(tr("Add to &ignore list"));
|
||||
|
|
@ -376,6 +378,9 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
|
|||
}
|
||||
menu->addAction(aDetails);
|
||||
menu->addAction(aShowGames);
|
||||
if (userLevel.testFlag(ServerInfo_User::IsRegistered)) {
|
||||
menu->addAction(aViewPublicDecks);
|
||||
}
|
||||
menu->addAction(aChat);
|
||||
const QList<GameInviteOption> inviteOptions = inviteOptionsForUser(userName);
|
||||
if (!inviteOptions.isEmpty()) {
|
||||
|
|
@ -455,6 +460,7 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
|
|||
aChat->setEnabled(anotherUser && online && !userListProxy->isUserIgnored(userName));
|
||||
aShowGames->setEnabled(online);
|
||||
aReport->setEnabled(anotherUser);
|
||||
aViewPublicDecks->setEnabled(anotherUser);
|
||||
aAddToBuddyList->setEnabled(anotherUser);
|
||||
aRemoveFromBuddyList->setEnabled(anotherUser);
|
||||
aAddToIgnoreList->setEnabled(anotherUser);
|
||||
|
|
@ -481,6 +487,8 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
|
|||
execChat(userName);
|
||||
} else if (actionClicked == aShowGames) {
|
||||
execShowGames(userName);
|
||||
} else if (actionClicked == aViewPublicDecks) {
|
||||
execViewPublicDecks(userName);
|
||||
} else if (actionClicked == aAddToBuddyList) {
|
||||
execAddToBuddy(userName);
|
||||
} else if (actionClicked == aRemoveFromBuddyList) {
|
||||
|
|
@ -604,6 +612,11 @@ void UserContextMenu::execShowGames(const QString &userName)
|
|||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void UserContextMenu::execViewPublicDecks(const QString &userName)
|
||||
{
|
||||
tabSupervisor->openTabPublicDecks(userName);
|
||||
}
|
||||
|
||||
void UserContextMenu::execAddToBuddy(const QString &userName)
|
||||
{
|
||||
Command_AddToList cmd;
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ private:
|
|||
QAction *aUserName;
|
||||
QAction *aDetails;
|
||||
QAction *aShowGames;
|
||||
QAction *aViewPublicDecks;
|
||||
QAction *aChat;
|
||||
QAction *aAddToBuddyList, *aRemoveFromBuddyList;
|
||||
QAction *aAddToIgnoreList, *aRemoveFromIgnoreList;
|
||||
|
|
@ -111,6 +112,7 @@ public:
|
|||
void execInvite(const QString &userName);
|
||||
void execDetails(const QString &userName);
|
||||
void execShowGames(const QString &userName);
|
||||
void execViewPublicDecks(const QString &userName);
|
||||
void execAddToBuddy(const QString &userName);
|
||||
void execRemoveFromBuddy(const QString &userName);
|
||||
void execAddToIgnore(const QString &userName);
|
||||
|
|
|
|||
|
|
@ -634,9 +634,10 @@ void UserInfoPopup::onGamesContextMenu(const QPoint &pos)
|
|||
"QMenu::item:selected{background:%4;}")
|
||||
.arg(colorStr(theme.bg), colorStr(theme.text), colorStr(theme.border), colorStr(theme.buttonHover)));
|
||||
|
||||
const bool canJoin = !game.started() && game.player_count() < game.max_players();
|
||||
// Never grey the join action out: GameSelector::joinGame() asks whether to
|
||||
// fall back to spectating when the game is full or already running, so a
|
||||
// disabled entry would just read as a dead button.
|
||||
QAction *join = menu.addAction(tr("Join game"));
|
||||
join->setEnabled(canJoin);
|
||||
|
||||
QAction *spec = nullptr;
|
||||
if (game.spectators_allowed()) {
|
||||
|
|
|
|||
|
|
@ -218,9 +218,10 @@ void UserListTWI::setOnline(bool online)
|
|||
* 2) Admins, judge/vip/donator status ignored
|
||||
* 3) Moderators, judge/vip/donator status ignored
|
||||
* 4) Judges
|
||||
* 5) VIPs
|
||||
* 6) Donators
|
||||
* 7) Everyone else
|
||||
* 5) Developers
|
||||
* 6) VIPs
|
||||
* 7) Donators
|
||||
* 8) Everyone else
|
||||
* @param other RHS to compare to
|
||||
* @return Left is less than the Right
|
||||
*/
|
||||
|
|
@ -234,11 +235,10 @@ bool UserListTWI::operator<(const QTreeWidgetItem &other) const
|
|||
const auto &lhsUserLevelFlags = UserLevelFlags(data(0, Qt::UserRole).toInt());
|
||||
const auto &rhsUserLevelFlags = UserLevelFlags(other.data(0, Qt::UserRole).toInt());
|
||||
|
||||
// Admins, Developers & Mods need no additional comparison checks, just to see if they're an admin, a developer
|
||||
// Admins & Moderators need no additional comparison checks, just to see if they're an admin
|
||||
// or a moderator
|
||||
static const QList<ServerInfo_User_UserLevelFlag> userLevelWithNoOtherPrefOrder = {
|
||||
ServerInfo_User_UserLevelFlag_IsAdmin, ServerInfo_User_UserLevelFlag_IsDeveloper,
|
||||
ServerInfo_User_UserLevelFlag_IsModerator};
|
||||
ServerInfo_User_UserLevelFlag_IsAdmin, ServerInfo_User_UserLevelFlag_IsModerator};
|
||||
for (const auto &userLevelEntry : userLevelWithNoOtherPrefOrder) {
|
||||
if (lhsUserLevelFlags.testFlag(userLevelEntry) &&
|
||||
lhsUserLevelFlags.testFlag(userLevelEntry) == rhsUserLevelFlags.testFlag(userLevelEntry)) {
|
||||
|
|
@ -249,10 +249,10 @@ bool UserListTWI::operator<(const QTreeWidgetItem &other) const
|
|||
}
|
||||
}
|
||||
|
||||
// Judges can be sorted by their additional ranks
|
||||
static const QList<ServerInfo_User_UserLevelFlag> userLevelOrder = {ServerInfo_User_UserLevelFlag_IsJudge,
|
||||
ServerInfo_User_UserLevelFlag_IsRegistered,
|
||||
ServerInfo_User_UserLevelFlag_IsUser};
|
||||
// Judges and developers can be sorted by their additional ranks
|
||||
static const QList<ServerInfo_User_UserLevelFlag> userLevelOrder = {
|
||||
ServerInfo_User_UserLevelFlag_IsJudge, ServerInfo_User_UserLevelFlag_IsDeveloper,
|
||||
ServerInfo_User_UserLevelFlag_IsRegistered, ServerInfo_User_UserLevelFlag_IsUser};
|
||||
for (const auto &userLevelEntry : userLevelOrder) {
|
||||
if (lhsUserLevelFlags.testFlag(userLevelEntry) != rhsUserLevelFlags.testFlag(userLevelEntry)) {
|
||||
return lhsUserLevelFlags.testFlag(userLevelEntry) > rhsUserLevelFlags.testFlag(userLevelEntry);
|
||||
|
|
@ -430,9 +430,6 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
|
|||
hidePopup(true);
|
||||
requestVisibleItemResources();
|
||||
});
|
||||
|
||||
// Forward join requests from popup upward
|
||||
connect(userInfoPopup, &UserInfoPopup::joinGameRequested, this, &UserListWidget::joinGameRequested);
|
||||
} else {
|
||||
// Dialog mode: keyboard selection drives the Invite button.
|
||||
connect(userTree, &QTreeWidget::currentItemChanged, this, [this](QTreeWidgetItem *current, QTreeWidgetItem *) {
|
||||
|
|
@ -692,6 +689,17 @@ void UserListWidget::connectPopupSignals()
|
|||
[this](const QString &n) { userContextMenu->execAdjustJudge(n, true); });
|
||||
connect(userInfoPopup, &UserInfoPopup::demoteFromJudgeRequested, this,
|
||||
[this](const QString &n) { userContextMenu->execAdjustJudge(n, false); });
|
||||
|
||||
// Joining is a tab-level action, not a per-user one: the popup's games list
|
||||
// spans every room the user is in, so the target room may not be open yet.
|
||||
// Hand it to the supervisor, which owns the intent that joins the room if
|
||||
// needed. The popup closes here because that intent switches tabs, which
|
||||
// would otherwise strand it on a list that is no longer on screen.
|
||||
connect(userInfoPopup, &UserInfoPopup::joinGameRequested, this,
|
||||
[this](const int gameId, const int roomId, const bool asSpectator) {
|
||||
hidePopup(true);
|
||||
tabSupervisor->joinGameFromUserCard(gameId, roomId, asSpectator);
|
||||
});
|
||||
}
|
||||
|
||||
bool UserListWidget::eventFilter(QObject *obj, QEvent *event)
|
||||
|
|
|
|||
|
|
@ -172,7 +172,6 @@ signals:
|
|||
void removeBuddy(const QString &userName);
|
||||
void addIgnore(const QString &userName);
|
||||
void removeIgnore(const QString &userName);
|
||||
void joinGameRequested(int gameId, int roomId, bool asSpectator);
|
||||
void sectionExpanded(Section section, bool expanded);
|
||||
/** Dialog mode: the user activated (Enter/double-click) the given row. */
|
||||
void userActivated(const QString &userName);
|
||||
|
|
|
|||
|
|
@ -62,6 +62,12 @@ AppearanceSettingsPage::AppearanceSettingsPage()
|
|||
// Qt widget style; "System" lets the application decide
|
||||
styleCombo.addItem(tr("System"), QStringLiteral("System"));
|
||||
for (const QString &key : QStyleFactory::keys()) {
|
||||
// The Windows 11 native style is broken (board rendering glitches when
|
||||
// moving cards), so never offer it; it is already sanitized at apply
|
||||
// time in ThemeManager.
|
||||
if (key.compare("windows11", Qt::CaseInsensitive) == 0) {
|
||||
continue;
|
||||
}
|
||||
styleCombo.addItem(key, key);
|
||||
}
|
||||
|
||||
|
|
@ -505,7 +511,7 @@ void AppearanceSettingsPage::retranslateUi()
|
|||
openThemeButton.setText(tr("Open themes folder"));
|
||||
schemeComboLabel.setText(tr("Active theme palette:"));
|
||||
styleComboLabel.setText(tr("Active theme style:"));
|
||||
styleCombo.setToolTip(tr("Qt widget style saved to this theme (\"Default\" lets the application decide)"));
|
||||
styleCombo.setToolTip(tr("Qt widget style saved to this theme (\"System\" lets the application decide)"));
|
||||
editPaletteButton.setText(tr("Edit theme palette"));
|
||||
|
||||
homeTabGroupBox->setTitle(tr("Home tab settings"));
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@
|
|||
#include <QInputDialog>
|
||||
#include <QLineEdit>
|
||||
#include <QMessageBox>
|
||||
#include <QSet>
|
||||
#include <QToolBar>
|
||||
#include <QUrl>
|
||||
#include <libcockatrice/settings/download_settings.h>
|
||||
#include <libcockatrice/settings/paths_settings.h>
|
||||
#include <libcockatrice/settings/personal_settings.h>
|
||||
|
|
@ -51,7 +53,9 @@ DeckEditorSettingsPage::DeckEditorSettingsPage()
|
|||
urlList->setDragDropMode(QAbstractItemView::InternalMove);
|
||||
connect(urlList->model(), &QAbstractItemModel::rowsMoved, this, &DeckEditorSettingsPage::urlListChanged);
|
||||
|
||||
urlList->addItems(SettingsCache::instance().downloads().getAllURLs());
|
||||
for (const QString &url : SettingsCache::instance().downloads().getAllURLs()) {
|
||||
addUrlItem(url);
|
||||
}
|
||||
|
||||
aAdd = new QAction(this);
|
||||
aAdd->setIcon(themePixmap(QStringLiteral("icons/increment")));
|
||||
|
|
@ -65,11 +69,16 @@ DeckEditorSettingsPage::DeckEditorSettingsPage()
|
|||
aRemove->setIcon(themePixmap(QStringLiteral("icons/decrement")));
|
||||
connect(aRemove, &QAction::triggered, this, &DeckEditorSettingsPage::actRemoveURL);
|
||||
|
||||
aRateLimit = new QAction(this);
|
||||
aRateLimit->setIcon(themePixmap(QStringLiteral("icons/cogwheel")));
|
||||
connect(aRateLimit, &QAction::triggered, this, &DeckEditorSettingsPage::actAdjustRateLimit);
|
||||
|
||||
auto *urlToolBar = new QToolBar;
|
||||
urlToolBar->setOrientation(Qt::Vertical);
|
||||
urlToolBar->addAction(aAdd);
|
||||
urlToolBar->addAction(aRemove);
|
||||
urlToolBar->addAction(aEdit);
|
||||
urlToolBar->addAction(aRateLimit);
|
||||
urlToolBar->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding);
|
||||
|
||||
auto *urlListLayout = new QHBoxLayout;
|
||||
|
|
@ -117,7 +126,9 @@ void DeckEditorSettingsPage::resetDownloadedURLsButtonClicked()
|
|||
{
|
||||
SettingsCache::instance().downloads().resetToDefaultURLs();
|
||||
urlList->clear();
|
||||
urlList->addItems(SettingsCache::instance().downloads().getAllURLs());
|
||||
for (const QString &url : SettingsCache::instance().downloads().getAllURLs()) {
|
||||
addUrlItem(url);
|
||||
}
|
||||
QMessageBox::information(this, tr("Success"), tr("Download URLs have been reset."));
|
||||
}
|
||||
|
||||
|
|
@ -126,7 +137,7 @@ void DeckEditorSettingsPage::actAddURL()
|
|||
bool ok;
|
||||
QString msg = QInputDialog::getText(this, tr("Add URL"), tr("URL:"), QLineEdit::Normal, QString(), &ok);
|
||||
if (ok) {
|
||||
urlList->addItem(msg);
|
||||
addUrlItem(msg);
|
||||
storeSettings();
|
||||
}
|
||||
}
|
||||
|
|
@ -141,12 +152,14 @@ void DeckEditorSettingsPage::actRemoveURL()
|
|||
|
||||
void DeckEditorSettingsPage::actEditURL()
|
||||
{
|
||||
if (urlList->currentItem()) {
|
||||
QString oldText = urlList->currentItem()->text();
|
||||
QListWidgetItem *item = urlList->currentItem();
|
||||
if (item) {
|
||||
const QString oldText = urlForItem(item);
|
||||
bool ok;
|
||||
QString msg = QInputDialog::getText(this, tr("Edit URL"), tr("URL:"), QLineEdit::Normal, oldText, &ok);
|
||||
if (ok) {
|
||||
urlList->currentItem()->setText(msg);
|
||||
item->setData(Qt::UserRole, msg);
|
||||
item->setText(urlLabel(msg));
|
||||
storeSettings();
|
||||
}
|
||||
}
|
||||
|
|
@ -158,10 +171,133 @@ void DeckEditorSettingsPage::storeSettings()
|
|||
|
||||
QStringList downloadUrls;
|
||||
for (int i = 0; i < urlList->count(); i++) {
|
||||
qInfo() << "Priority" << i << ":" << urlList->item(i)->text();
|
||||
downloadUrls << urlList->item(i)->text();
|
||||
const QString url = urlForItem(urlList->item(i));
|
||||
qInfo() << "Priority" << i << ":" << url;
|
||||
downloadUrls << url;
|
||||
}
|
||||
SettingsCache::instance().downloads().setDownloadUrls(downloadUrls);
|
||||
|
||||
// Drop per-host limits whose host is no longer referenced by any configured URL, so removing
|
||||
// a URL doesn't leave a stale throttle behind that reactivates if the host is re-added.
|
||||
QSet<QString> usedHosts;
|
||||
for (const QString &url : downloadUrls) {
|
||||
const QString host = QUrl(url).host();
|
||||
if (!host.isEmpty()) {
|
||||
usedHosts.insert(host);
|
||||
}
|
||||
}
|
||||
QHash<QString, int> limits = SettingsCache::instance().downloads().getHostRequestLimits();
|
||||
bool limitsChanged = false;
|
||||
for (auto it = limits.begin(); it != limits.end();) {
|
||||
// Prune only limits for hosts that are neither referenced by a configured URL nor carry a
|
||||
// developer cap. Capped hosts are often redirect targets (e.g. api.scryfall.com redirects
|
||||
// to cards.scryfall.io) that never appear in the URL list, yet they are exactly the hosts
|
||||
// the throttle applies to, so dropping them when a URL is removed would silently re-enable
|
||||
// free-running traffic to a rate-sensitive server.
|
||||
if (!usedHosts.contains(it.key()) && !DownloadSettings::getDeveloperHostCaps().contains(it.key())) {
|
||||
it = limits.erase(it);
|
||||
limitsChanged = true;
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
if (limitsChanged) {
|
||||
SettingsCache::instance().downloads().setHostRequestLimits(limits);
|
||||
}
|
||||
|
||||
refreshUrlItems();
|
||||
}
|
||||
|
||||
QListWidgetItem *DeckEditorSettingsPage::addUrlItem(const QString &url)
|
||||
{
|
||||
auto *item = new QListWidgetItem(urlLabel(url));
|
||||
item->setData(Qt::UserRole, url);
|
||||
urlList->addItem(item);
|
||||
return item;
|
||||
}
|
||||
|
||||
QString DeckEditorSettingsPage::urlForItem(const QListWidgetItem *item) const
|
||||
{
|
||||
return item->data(Qt::UserRole).toString();
|
||||
}
|
||||
|
||||
QString DeckEditorSettingsPage::urlLabel(const QString &url) const
|
||||
{
|
||||
const QString host = QUrl(url).host();
|
||||
if (host.isEmpty()) {
|
||||
return url;
|
||||
}
|
||||
|
||||
const QHash<QString, int> limits = SettingsCache::instance().downloads().getHostRequestLimits();
|
||||
const int devCap =
|
||||
DownloadSettings::getDeveloperHostCaps().value(host, DownloadSettings::DEFAULT_HOST_REQUEST_LIMIT);
|
||||
if (devCap == DownloadSettings::UNLIMITED_HOST_QUOTA && !limits.contains(host)) {
|
||||
return tr("%1 (unlimited)").arg(url);
|
||||
}
|
||||
|
||||
const int requested = limits.value(
|
||||
host, devCap == DownloadSettings::UNLIMITED_HOST_QUOTA ? DownloadSettings::DEFAULT_HOST_REQUEST_LIMIT : devCap);
|
||||
const int effective = SettingsCache::instance().downloads().clampHostRequestLimit(host, requested);
|
||||
return tr("%1 (%2/s)").arg(url).arg(effective);
|
||||
}
|
||||
|
||||
void DeckEditorSettingsPage::refreshUrlItems()
|
||||
{
|
||||
for (int i = 0; i < urlList->count(); ++i) {
|
||||
QListWidgetItem *item = urlList->item(i);
|
||||
item->setText(urlLabel(urlForItem(item)));
|
||||
}
|
||||
}
|
||||
|
||||
void DeckEditorSettingsPage::actAdjustRateLimit()
|
||||
{
|
||||
if (urlList->currentItem() == nullptr) {
|
||||
QMessageBox::information(this, tr("Adjust Rate Limit"), tr("Select a URL in the list first."));
|
||||
return;
|
||||
}
|
||||
|
||||
const QString host = QUrl(urlForItem(urlList->currentItem())).host();
|
||||
if (host.isEmpty()) {
|
||||
QMessageBox::information(this, tr("Adjust Rate Limit"), tr("The selected URL does not have a valid host."));
|
||||
return;
|
||||
}
|
||||
|
||||
const QHash<QString, int> &devCaps = DownloadSettings::getDeveloperHostCaps();
|
||||
const QHash<QString, int> currentLimits = SettingsCache::instance().downloads().getHostRequestLimits();
|
||||
const int devCap = devCaps.value(host, DownloadSettings::DEFAULT_HOST_REQUEST_LIMIT);
|
||||
const bool unlocked = devCap == DownloadSettings::UNLIMITED_HOST_QUOTA;
|
||||
|
||||
bool ok = false;
|
||||
int minimum;
|
||||
int maximum;
|
||||
int defaultValue;
|
||||
QString prompt;
|
||||
if (unlocked) {
|
||||
minimum = 0; // 0 means "unlimited"
|
||||
maximum = DownloadSettings::UNLOCKED_HOST_LIMIT_MAX;
|
||||
defaultValue = currentLimits.value(host, 0);
|
||||
prompt = tr("Requests per second (0 = unlimited, fastest; up to %1):").arg(maximum);
|
||||
} else {
|
||||
minimum = DownloadSettings::MIN_HOST_REQUEST_LIMIT;
|
||||
maximum = devCap;
|
||||
defaultValue = currentLimits.value(host, devCap);
|
||||
prompt = tr("Requests per second (developer maximum is %1):").arg(maximum);
|
||||
}
|
||||
|
||||
const int value = QInputDialog::getInt(this, tr("Adjust Rate Limit for %1").arg(host), prompt, defaultValue,
|
||||
minimum, maximum, 1, &ok);
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
QHash<QString, int> limits = currentLimits;
|
||||
if (unlocked ? value == 0 : value == devCap) {
|
||||
limits.remove(host);
|
||||
} else {
|
||||
limits.insert(host, value);
|
||||
}
|
||||
SettingsCache::instance().downloads().setHostRequestLimits(limits);
|
||||
refreshUrlItems();
|
||||
}
|
||||
|
||||
void DeckEditorSettingsPage::urlListChanged(const QModelIndex &, int, int, const QModelIndex &, int)
|
||||
|
|
@ -244,4 +380,8 @@ void DeckEditorSettingsPage::retranslateUi()
|
|||
aAdd->setText(tr("Add New URL"));
|
||||
aEdit->setText(tr("Edit URL"));
|
||||
aRemove->setText(tr("Remove URL"));
|
||||
}
|
||||
aRateLimit->setText(tr("Adjust Rate Limit"));
|
||||
|
||||
// The per-URL rate limit suffixes are translated, so refresh them when the language changes.
|
||||
refreshUrlItems();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ private slots:
|
|||
void actAddURL();
|
||||
void actRemoveURL();
|
||||
void actEditURL();
|
||||
void actAdjustRateLimit();
|
||||
void resetDownloadedURLsButtonClicked();
|
||||
|
||||
private:
|
||||
|
|
@ -34,7 +35,7 @@ private:
|
|||
QLabel urlLinkLabel;
|
||||
QCheckBox picDownloadCheckBox;
|
||||
QListWidget *urlList;
|
||||
QAction *aAdd, *aEdit, *aRemove;
|
||||
QAction *aAdd, *aEdit, *aRemove, *aRateLimit;
|
||||
QCheckBox mcDownloadSpoilersCheckBox;
|
||||
QLabel msDownloadSpoilersLabel;
|
||||
QGroupBox *mpGeneralGroupBox;
|
||||
|
|
@ -46,6 +47,18 @@ private:
|
|||
QLabel infoOnSpoilersLabel;
|
||||
QPushButton *mpSpoilerPathButton;
|
||||
QPushButton *updateNowButton;
|
||||
|
||||
/** @brief Adds a list item for the given URL, storing the raw URL alongside its displayed label. */
|
||||
QListWidgetItem *addUrlItem(const QString &url);
|
||||
|
||||
/** @brief Returns the raw URL stored on a list item. */
|
||||
[[nodiscard]] QString urlForItem(const QListWidgetItem *item) const;
|
||||
|
||||
/** @brief Returns the display label for a URL, including its current effective rate limit. */
|
||||
[[nodiscard]] QString urlLabel(const QString &url) const;
|
||||
|
||||
/** @brief Refreshes the displayed label of every URL item after limits or settings change. */
|
||||
void refreshUrlItems();
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_DECK_EDITOR_SETTINGS_PAGE_H
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
#include <libcockatrice/settings/tabs_settings.h>
|
||||
#include <libcockatrice/settings/updates_settings.h>
|
||||
#include <libcockatrice/utility/macros.h>
|
||||
#include <libcockatrice/utility/translation_loader.h>
|
||||
|
||||
enum startupCardUpdateCheckBehaviorIndex
|
||||
{
|
||||
|
|
@ -63,13 +64,26 @@ GeneralSettingsPage::GeneralSettingsPage()
|
|||
connect(&cardLanguageBox, qOverload<int>(&QComboBox::currentIndexChanged), this,
|
||||
&GeneralSettingsPage::cardLanguageBoxChanged);
|
||||
|
||||
// card search language, independent of the card display language
|
||||
cardSearchLanguageBox.addItem(""); // texts set in retranslateUi
|
||||
cardSearchLanguageBox.addItem("");
|
||||
cardSearchLanguageBox.addItem("");
|
||||
const int cardSearchLanguageIndex = SettingsCache::instance().cardsDisplay().getCardSearchLanguage();
|
||||
cardSearchLanguageBox.setCurrentIndex(cardSearchLanguageIndex < 0 ? static_cast<int>(SearchLanguageMode::English)
|
||||
: cardSearchLanguageIndex);
|
||||
|
||||
connect(&cardSearchLanguageBox, qOverload<int>(&QComboBox::currentIndexChanged), this,
|
||||
&GeneralSettingsPage::cardSearchLanguageBoxChanged);
|
||||
|
||||
auto *languageGrid = new QGridLayout;
|
||||
languageGrid->addWidget(&languageLabel, 0, 0);
|
||||
languageGrid->addWidget(&languageBox, 0, 1);
|
||||
languageGrid->addWidget(&cardLanguageLabel, 1, 0);
|
||||
languageGrid->addWidget(&cardLanguageBox, 1, 1);
|
||||
languageGrid->addWidget(&cardLanguageNoteLabel, 2, 1);
|
||||
languageGrid->addWidget(&advertiseTranslationPageLabel, 3, 1, Qt::AlignRight);
|
||||
languageGrid->addWidget(&cardSearchLanguageLabel, 3, 0);
|
||||
languageGrid->addWidget(&cardSearchLanguageBox, 3, 1);
|
||||
languageGrid->addWidget(&advertiseTranslationPageLabel, 4, 1, Qt::AlignRight);
|
||||
|
||||
cardLanguageNoteLabel.setWordWrap(true);
|
||||
cardLanguageNoteLabel.setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
||||
|
|
@ -320,22 +334,18 @@ GeneralSettingsPage::GeneralSettingsPage()
|
|||
|
||||
QStringList GeneralSettingsPage::findQmFiles()
|
||||
{
|
||||
QDir dir(translationPath);
|
||||
QStringList fileNames = dir.entryList(QStringList(translationPrefix + "_*.qm"), QDir::Files, QDir::Name);
|
||||
fileNames.replaceInStrings(QRegularExpression(translationPrefix + "_(.*)\\.qm"), "\\1");
|
||||
return fileNames;
|
||||
return TranslationLoader::availableLanguages(translationPrefix,
|
||||
TranslationLoader::applicationTranslationPaths(translationPath));
|
||||
}
|
||||
|
||||
QString GeneralSettingsPage::languageName(const QString &lang)
|
||||
{
|
||||
QTranslator qTranslator;
|
||||
|
||||
QString appNameHint = translationPrefix + "_" + lang;
|
||||
bool appTranslationLoaded = qTranslator.load(appNameHint, translationPath);
|
||||
if (!appTranslationLoaded) {
|
||||
qCWarning(GeneralSettingsPageLog)
|
||||
<< "Unable to load" << translationPrefix << "translation" << appNameHint << "at" << translationPath;
|
||||
}
|
||||
// Only the language name is needed here, so the file is loaded without logging a line per
|
||||
// language: findQmFiles() asked for languages whose files exist in the first place.
|
||||
TranslationLoader::loadFrom(qTranslator, translationPrefix + "_" + lang,
|
||||
TranslationLoader::applicationTranslationPaths(translationPath));
|
||||
|
||||
return qTranslator.translate("i18n", DEFAULT_LANG_NAME);
|
||||
}
|
||||
|
|
@ -481,6 +491,11 @@ void GeneralSettingsPage::cardLanguageBoxChanged(int index)
|
|||
}
|
||||
}
|
||||
|
||||
void GeneralSettingsPage::cardSearchLanguageBoxChanged(int index)
|
||||
{
|
||||
SettingsCache::instance().cardsDisplay().setCardSearchLanguage(index);
|
||||
}
|
||||
|
||||
void GeneralSettingsPage::updateStartupServerControlsVisibility()
|
||||
{
|
||||
const int index = startupTabSelector.currentIndex();
|
||||
|
|
@ -502,6 +517,12 @@ void GeneralSettingsPage::retranslateUi()
|
|||
cardLanguageLabel.setText(tr("Card text & images language:"));
|
||||
cardLanguageNoteLabel.setText(
|
||||
tr("Foreign card names, text and art apply after you update the card database (Oracle)."));
|
||||
cardSearchLanguageLabel.setText(tr("Language used in card search:"));
|
||||
cardSearchLanguageBox.setItemText(static_cast<int>(SearchLanguageMode::English), tr("English"));
|
||||
cardSearchLanguageBox.setItemText(static_cast<int>(SearchLanguageMode::Selected),
|
||||
tr("Selected card language (untranslated cards still match in English)"));
|
||||
cardSearchLanguageBox.setItemText(static_cast<int>(SearchLanguageMode::Both),
|
||||
tr("English and selected card language"));
|
||||
advertiseTranslationPageLabel.setText(
|
||||
QString("<a href='%1'>%2</a>").arg(WIKI_TRANSLATION_FAQ).arg(tr("How to help with translations")));
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ private slots:
|
|||
void resetAllPathsClicked();
|
||||
void languageBoxChanged(int index);
|
||||
void cardLanguageBoxChanged(int index);
|
||||
void cardSearchLanguageBoxChanged(int index);
|
||||
void updateStartupServerControlsVisibility();
|
||||
|
||||
private:
|
||||
|
|
@ -55,6 +56,9 @@ private:
|
|||
QComboBox cardLanguageBox;
|
||||
QLabel cardLanguageNoteLabel;
|
||||
|
||||
QLabel cardSearchLanguageLabel;
|
||||
QComboBox cardSearchLanguageBox;
|
||||
|
||||
QLabel updateReleaseChannelLabel;
|
||||
QComboBox updateReleaseChannelBox;
|
||||
QCheckBox startupUpdateCheckCheckBox;
|
||||
|
|
|
|||
|
|
@ -64,6 +64,10 @@ MessagesSettingsPage::MessagesSettingsPage()
|
|||
connect(&ignoreAllPrivateMessagesCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().chat(),
|
||||
&ChatSettings::setIgnoreAllPrivateMessages);
|
||||
|
||||
useGameTimeCheckBox.setChecked(SettingsCache::instance().chat().getUseGameTime());
|
||||
connect(&useGameTimeCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().chat(),
|
||||
&ChatSettings::setUseGameTime);
|
||||
|
||||
customAlertString = new QLineEdit();
|
||||
customAlertString->setText(SettingsCache::instance().chat().getHighlightWords());
|
||||
connect(customAlertString, &QLineEdit::textChanged, &SettingsCache::instance().chat(),
|
||||
|
|
@ -82,6 +86,7 @@ MessagesSettingsPage::MessagesSettingsPage()
|
|||
chatGrid->addWidget(&mentionPopups, 6, 0);
|
||||
chatGrid->addWidget(&roomHistory, 7, 0);
|
||||
chatGrid->addWidget(&ignoreAllPrivateMessagesCheckBox, 8, 0);
|
||||
chatGrid->addWidget(&useGameTimeCheckBox, 9, 0);
|
||||
chatGroupBox = new QGroupBox;
|
||||
chatGroupBox->setLayout(chatGrid);
|
||||
|
||||
|
|
@ -253,6 +258,7 @@ void MessagesSettingsPage::retranslateUi()
|
|||
mentionPopups.setText(tr("Enable desktop notification for mentions"));
|
||||
roomHistory.setText(tr("Enable room message history on join"));
|
||||
ignoreAllPrivateMessagesCheckBox.setText(tr("Ignore all private messages"));
|
||||
useGameTimeCheckBox.setText(tr("Use game time instead of local time in game logs"));
|
||||
hexLabel.setText(tr("(Color is hexadecimal)"));
|
||||
hexHighlightLabel.setText(tr("(Color is hexadecimal)"));
|
||||
customAlertStringLabel.setText(tr("Separate words with a space, alphanumeric characters only"));
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ private:
|
|||
QCheckBox mentionPopups;
|
||||
QCheckBox roomHistory;
|
||||
QCheckBox ignoreAllPrivateMessagesCheckBox;
|
||||
QCheckBox useGameTimeCheckBox;
|
||||
QGroupBox *chatGroupBox;
|
||||
QGroupBox *highlightGroupBox;
|
||||
QGroupBox *messageGroupBox;
|
||||
|
|
|
|||
|
|
@ -181,9 +181,14 @@ StorageSettingsPage::StorageSettingsPage()
|
|||
|
||||
void StorageSettingsPage::clearDownloadedPicsButtonClicked()
|
||||
{
|
||||
CardPictureLoader::clearNetworkCache();
|
||||
// The network cache is cleared asynchronously on the worker thread, so wait for the completion
|
||||
// signal before confirming; the in-memory pixmap cache is cleared synchronously right away.
|
||||
connect(
|
||||
&CardPictureLoader::getInstance(), &CardPictureLoader::networkCacheCleared, this,
|
||||
[this] { QMessageBox::information(this, tr("Success"), tr("Cached card pictures have been reset.")); },
|
||||
Qt::SingleShotConnection);
|
||||
CardPictureLoader::clearPixmapCache();
|
||||
QMessageBox::information(this, tr("Success"), tr("Cached card pictures have been reset."));
|
||||
CardPictureLoader::clearNetworkCache();
|
||||
}
|
||||
|
||||
void StorageSettingsPage::clearImageBackupsButtonClicked()
|
||||
|
|
|
|||
|
|
@ -149,6 +149,11 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage()
|
|||
connect(&openDeckInNewTabCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().deckEditor(),
|
||||
&DeckEditorSettings::setOpenDeckInNewTab);
|
||||
|
||||
visualDeckEditorShowCardCountsCheckBox.setChecked(
|
||||
SettingsCache::instance().cardsDisplay().getVisualDeckEditorShowCardCounts());
|
||||
connect(&visualDeckEditorShowCardCountsCheckBox, &QCheckBox::QT_STATE_CHANGED,
|
||||
&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::setVisualDeckEditorShowCardCounts);
|
||||
|
||||
visualDeckStorageInGameCheckBox.setChecked(
|
||||
SettingsCache::instance().visualDeckStorage().getVisualDeckStorageInGame());
|
||||
connect(&visualDeckStorageInGameCheckBox, &QCheckBox::QT_STATE_CHANGED,
|
||||
|
|
@ -244,18 +249,19 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage()
|
|||
|
||||
auto *deckEditorGrid = new QGridLayout;
|
||||
deckEditorGrid->addWidget(&openDeckInNewTabCheckBox, 0, 0);
|
||||
deckEditorGrid->addWidget(&visualDeckStorageInGameCheckBox, 1, 0);
|
||||
deckEditorGrid->addWidget(&visualDeckStorageSelectionAnimationCheckBox, 2, 0);
|
||||
deckEditorGrid->addWidget(&visualDeckStoragePromptForConversionLabel, 3, 0);
|
||||
deckEditorGrid->addWidget(&visualDeckStoragePromptForConversionSelector, 3, 1);
|
||||
deckEditorGrid->addWidget(&defaultDeckEditorTypeLabel, 4, 0);
|
||||
deckEditorGrid->addWidget(&defaultDeckEditorTypeSelector, 4, 1);
|
||||
deckEditorGrid->addWidget(&vdeStartupTabLabel, 5, 0);
|
||||
deckEditorGrid->addWidget(&vdeStartupTabSelector, 5, 1);
|
||||
deckEditorGrid->addWidget(&commanderSpellbookIntegrationEnabledLabel, 6, 0);
|
||||
deckEditorGrid->addWidget(&commanderSpellbookIntegrationEnabledSelector, 6, 1);
|
||||
deckEditorGrid->addWidget(labelWidget, 7, 0);
|
||||
deckEditorGrid->addWidget(&commanderSpellbookIntegrationBracketNamingSelector, 7, 1);
|
||||
deckEditorGrid->addWidget(&visualDeckEditorShowCardCountsCheckBox, 1, 0);
|
||||
deckEditorGrid->addWidget(&visualDeckStorageInGameCheckBox, 2, 0);
|
||||
deckEditorGrid->addWidget(&visualDeckStorageSelectionAnimationCheckBox, 3, 0);
|
||||
deckEditorGrid->addWidget(&visualDeckStoragePromptForConversionLabel, 4, 0);
|
||||
deckEditorGrid->addWidget(&visualDeckStoragePromptForConversionSelector, 4, 1);
|
||||
deckEditorGrid->addWidget(&defaultDeckEditorTypeLabel, 5, 0);
|
||||
deckEditorGrid->addWidget(&defaultDeckEditorTypeSelector, 5, 1);
|
||||
deckEditorGrid->addWidget(&vdeStartupTabLabel, 6, 0);
|
||||
deckEditorGrid->addWidget(&vdeStartupTabSelector, 6, 1);
|
||||
deckEditorGrid->addWidget(&commanderSpellbookIntegrationEnabledLabel, 7, 0);
|
||||
deckEditorGrid->addWidget(&commanderSpellbookIntegrationEnabledSelector, 7, 1);
|
||||
deckEditorGrid->addWidget(labelWidget, 8, 0);
|
||||
deckEditorGrid->addWidget(&commanderSpellbookIntegrationBracketNamingSelector, 8, 1);
|
||||
|
||||
deckEditorGroupBox = new QGroupBox;
|
||||
deckEditorGroupBox->setLayout(deckEditorGrid);
|
||||
|
|
@ -367,6 +373,7 @@ void UserInterfaceSettingsPage::retranslateUi()
|
|||
|
||||
deckEditorGroupBox->setTitle(tr("Deck editor/storage settings"));
|
||||
openDeckInNewTabCheckBox.setText(tr("Open deck in new tab by default"));
|
||||
visualDeckEditorShowCardCountsCheckBox.setText(tr("Show card counts in Visual Deck Editor"));
|
||||
visualDeckStorageInGameCheckBox.setText(tr("Use visual deck storage in game lobby"));
|
||||
visualDeckStorageSelectionAnimationCheckBox.setText(tr("Use selection animation for Visual Deck Storage"));
|
||||
visualDeckStoragePromptForConversionLabel.setText(
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ private:
|
|||
QComboBox visualDeckStoragePromptForConversionSelector;
|
||||
QCheckBox visualDeckStorageInGameCheckBox;
|
||||
QCheckBox visualDeckStorageSelectionAnimationCheckBox;
|
||||
QCheckBox visualDeckEditorShowCardCountsCheckBox;
|
||||
QLabel defaultDeckEditorTypeLabel;
|
||||
QComboBox defaultDeckEditorTypeSelector;
|
||||
QLabel vdeStartupTabLabel;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../../../client/settings/shortcuts_settings.h"
|
||||
#include "../cards/additional_info/deck_color_identity.h"
|
||||
#include "../client/network/interfaces/deck_stats_interface.h"
|
||||
#include "../client/network/interfaces/tapped_out_interface.h"
|
||||
#include "../deck_editor/deck_state_manager.h"
|
||||
|
|
@ -19,6 +20,7 @@
|
|||
#include "../interface/widgets/dialogs/dlg_load_deck.h"
|
||||
#include "../interface/widgets/dialogs/dlg_load_deck_from_clipboard.h"
|
||||
#include "../interface/widgets/dialogs/dlg_load_deck_from_website.h"
|
||||
#include "../interface/widgets/dialogs/dlg_share_deck.h"
|
||||
#include "../utility/visibility_change_listener.h"
|
||||
#include "tab_supervisor.h"
|
||||
|
||||
|
|
@ -323,6 +325,7 @@ bool AbstractTabDeckEditor::actSaveDeck()
|
|||
Command_DeckUpload cmd;
|
||||
cmd.set_deck_id(static_cast<google::protobuf::uint32>(loadedDeck.lastLoadInfo.remoteDeckId));
|
||||
cmd.set_deck_list(deckString.toStdString());
|
||||
cmd.set_color_identity(getDeckColorIdentity(loadedDeck.deckList, CardDatabaseManager::query()).toStdString());
|
||||
|
||||
PendingCommand *pend = AbstractClient::prepareSessionCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &AbstractTabDeckEditor::saveDeckRemoteFinished);
|
||||
|
|
@ -382,6 +385,27 @@ bool AbstractTabDeckEditor::actSaveDeckAs()
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Opens the deck share dialog with the current deck preselected.
|
||||
*/
|
||||
void AbstractTabDeckEditor::actShareDeck()
|
||||
{
|
||||
AbstractClient *client = tabSupervisor->getServerClient();
|
||||
if (client->getStatus() != StatusLoggedIn) {
|
||||
QMessageBox::information(this, tr("Share deck"), tr("You must be connected to the server to share a deck."));
|
||||
return;
|
||||
}
|
||||
|
||||
const QSharedPointer<DeckList> deck = deckStateManager->getDeckListShared();
|
||||
if (deck->isBlankDeck()) {
|
||||
QMessageBox::information(this, tr("Share deck"), tr("The deck is empty. Add cards before sharing it."));
|
||||
return;
|
||||
}
|
||||
|
||||
DlgShareDeck shareDialog(client, deck, this);
|
||||
shareDialog.exec();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Callback for remote deck save completion.
|
||||
* @param response Server response.
|
||||
|
|
|
|||
|
|
@ -214,6 +214,9 @@ protected slots:
|
|||
/** @brief Saves the current deck under a new name. */
|
||||
virtual bool actSaveDeckAs();
|
||||
|
||||
/** @brief Opens the deck share dialog for the current deck. */
|
||||
void actShareDeck();
|
||||
|
||||
/** @brief Loads a deck from the clipboard. */
|
||||
virtual void actLoadDeckFromClipboard();
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ ArchidektApiResponseDeckListingsDisplayWidget::ArchidektApiResponseDeckListingsD
|
|||
|
||||
flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded);
|
||||
|
||||
cardSizeSlider->enableCtrlScrollResize(flowWidget);
|
||||
|
||||
imageNetworkManager = new QNetworkAccessManager(this);
|
||||
imageNetworkManager->setTransferTimeout(); // Use Qt's default timeout
|
||||
imageNetworkManager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy);
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ EdhrecApiResponseCardDetailsDisplayWidget::EdhrecApiResponseCardDetailsDisplayWi
|
|||
&CardInfoPictureWidget::setScaleFactor);
|
||||
connect(this, &EdhrecApiResponseCardDetailsDisplayWidget::requestUrl, parentTab,
|
||||
&TabEdhRecMain::actNavigatePage);
|
||||
parentTab->getCardSizeSlider()->enableCtrlScrollResize(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ EdhrecCommanderResponseCommanderDetailsDisplayWidget::EdhrecCommanderResponseCom
|
|||
connect(parentTab->getCardSizeSlider()->getSlider(), &QSlider::valueChanged, commanderPicture,
|
||||
&CardInfoPictureWidget::setScaleFactor);
|
||||
commanderPicture->setScaleFactor(parentTab->getCardSizeSlider()->getSlider()->value());
|
||||
parentTab->getCardSizeSlider()->enableCtrlScrollResize(this);
|
||||
}
|
||||
|
||||
commanderDetails.debugPrint();
|
||||
|
|
|
|||
|
|
@ -3,35 +3,61 @@
|
|||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../../deck_loader/deck_loader.h"
|
||||
#include "../../pixel_map_generator.h"
|
||||
#include "../cards/additional_info/deck_color_identity.h"
|
||||
#include "../deck_share/deck_share_utils.h"
|
||||
#include "../deck_share/share_bar_widget.h"
|
||||
#include "../interface/widgets/server/remote/remote_decklist_tree_widget.h"
|
||||
#include "../interface/widgets/utility/get_text_with_max.h"
|
||||
|
||||
#include <QAction>
|
||||
#include <QApplication>
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
#include <QDesktopServices>
|
||||
#include <QDir>
|
||||
#include <QFileSystemModel>
|
||||
#include <QGroupBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHeaderView>
|
||||
#include <QInputDialog>
|
||||
#include <QLineEdit>
|
||||
#include <QMessageBox>
|
||||
#include <QTimer>
|
||||
#include <QToolBar>
|
||||
#include <QTreeView>
|
||||
#include <QUrl>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
#include <libcockatrice/protocol/pb/command_deck_del.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_deck_del_dir.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_deck_download.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_deck_new_dir.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_deck_set_visibility.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_deck_share_create.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_deck_upload.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_deck_download.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_deck_share_create.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_deck_upload.pb.h>
|
||||
#include <libcockatrice/protocol/pending_command.h>
|
||||
#include <libcockatrice/settings/network_settings.h>
|
||||
#include <libcockatrice/settings/paths_settings.h>
|
||||
#include <libcockatrice/utility/string_limits.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
// How long to wait after the last visibility change before reading back the
|
||||
// Public/Private column, in milliseconds.
|
||||
constexpr int VISIBILITY_REFRESH_DELAY = 500;
|
||||
|
||||
// Whether the file's name matches one of the deck formats Cockatrice can load.
|
||||
bool isSupportedDeckFile(const QString &filePath)
|
||||
{
|
||||
return QDir::match(DeckLoader::ACCEPTED_FILE_EXTENSIONS, QFileInfo(filePath).fileName());
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor,
|
||||
AbstractClient *_client,
|
||||
const ServerInfo_User *currentUserInfo)
|
||||
|
|
@ -39,6 +65,8 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor,
|
|||
{
|
||||
localDirModel = new QFileSystemModel(this);
|
||||
localDirModel->setRootPath(SettingsCache::instance().paths().getDeckPath());
|
||||
localDirModel->setNameFilters(DeckLoader::ACCEPTED_FILE_EXTENSIONS);
|
||||
localDirModel->setNameFilterDisables(false);
|
||||
localDirModel->sort(0, Qt::AscendingOrder);
|
||||
|
||||
localDirView = new QTreeView;
|
||||
|
|
@ -92,8 +120,34 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor,
|
|||
serverDirView = new RemoteDeckList_TreeWidget(client);
|
||||
|
||||
connect(serverDirView, &QTreeView::doubleClicked, this, &TabDeckStorage::actRemoteDoubleClick);
|
||||
connect(serverDirView->selectionModel(), &QItemSelectionModel::selectionChanged, this,
|
||||
[this] { onServerSelectionChanged(); });
|
||||
|
||||
// Share bar for creating a share link from the selected server decks/folders.
|
||||
shareBar = new ShareBarWidget(this);
|
||||
connect(shareBar, &ShareBarWidget::createRequested, this, &TabDeckStorage::actShareSelection);
|
||||
connect(shareBar, &ShareBarWidget::cancelRequested, this, &TabDeckStorage::cancelShareDecks);
|
||||
shareBar->setVisible(false);
|
||||
|
||||
shareTimeoutTimer = new QTimer(this);
|
||||
shareTimeoutTimer->setSingleShot(true);
|
||||
shareTimeoutTimer->setInterval(
|
||||
static_cast<int>((static_cast<qint64>(SettingsCache::instance().network().getTimeOut()) + 1) *
|
||||
SettingsCache::instance().network().getKeepAlive() * 1000));
|
||||
connect(shareTimeoutTimer, &QTimer::timeout, this, &TabDeckStorage::onShareFromTreeTimeout);
|
||||
|
||||
// Restartable single-shot refresh for the Public/Private column. It is
|
||||
// armed with the full network timeout when a publish is sent (so a dropped
|
||||
// reply still drains once) and re-armed with the short delay every time a
|
||||
// reply lands, so the drain cannot fire while a slow round trip is still in
|
||||
// flight. Either way the tree is re-read once things quiet down.
|
||||
visibilityRefreshTimer = new QTimer(this);
|
||||
visibilityRefreshTimer->setSingleShot(true);
|
||||
visibilityRefreshTimer->setInterval(VISIBILITY_REFRESH_DELAY);
|
||||
connect(visibilityRefreshTimer, &QTimer::timeout, this, &TabDeckStorage::onVisibilityRefreshTimeout);
|
||||
|
||||
QVBoxLayout *rightVbox = new QVBoxLayout;
|
||||
rightVbox->addWidget(shareBar);
|
||||
rightVbox->addWidget(serverDirView);
|
||||
rightVbox->addLayout(rightToolBarLayout);
|
||||
rightGroupBox = new QGroupBox;
|
||||
|
|
@ -139,6 +193,14 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor,
|
|||
aDeleteRemoteDeck->setIcon(themePixmap(QStringLiteral("icons/remove_row")));
|
||||
connect(aDeleteRemoteDeck, &QAction::triggered, this, &TabDeckStorage::actDeleteRemoteDeck);
|
||||
|
||||
aShareDecks = new QAction(this);
|
||||
aShareDecks->setIcon(themePixmap(QStringLiteral("icons/share")));
|
||||
connect(aShareDecks, &QAction::triggered, this, &TabDeckStorage::actShareDecks);
|
||||
|
||||
aPublishDeck = new QAction(this);
|
||||
aPublishDeck->setIcon(QPixmap("theme:icons/lock"));
|
||||
connect(aPublishDeck, &QAction::triggered, this, &TabDeckStorage::actPublishDeck);
|
||||
|
||||
// Add actions to toolbars
|
||||
leftToolBar->addAction(aOpenLocalDeck);
|
||||
leftToolBar->addAction(aRenameLocal);
|
||||
|
|
@ -150,6 +212,8 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor,
|
|||
|
||||
rightToolBar->addAction(aOpenRemoteDeck);
|
||||
rightToolBar->addAction(aDownload);
|
||||
rightToolBar->addAction(aShareDecks);
|
||||
rightToolBar->addAction(aPublishDeck);
|
||||
rightToolBar->addAction(aNewFolder);
|
||||
rightToolBar->addAction(aDeleteRemoteDeck);
|
||||
|
||||
|
|
@ -178,7 +242,13 @@ void TabDeckStorage::retranslateUi()
|
|||
aNewFolder->setText(tr("New folder"));
|
||||
aDeleteLocalDeck->setText(tr("Delete"));
|
||||
aDeleteRemoteDeck->setText(tr("Delete"));
|
||||
aShareDecks->setText(tr("Share decks"));
|
||||
aPublishDeck->setText(tr("Publish/unpublish deck"));
|
||||
aOpenDecksFolder->setText(tr("Open decks folder"));
|
||||
shareBar->retranslateUi();
|
||||
if (shareBar->isVisible()) {
|
||||
onServerSelectionChanged();
|
||||
}
|
||||
}
|
||||
|
||||
QString TabDeckStorage::getTargetPath() const
|
||||
|
|
@ -211,6 +281,8 @@ void TabDeckStorage::handleConnected(const ServerInfo_User &userInfo)
|
|||
void TabDeckStorage::handleConnectionChanged(ClientStatus status)
|
||||
{
|
||||
if (status == StatusDisconnected) {
|
||||
visibilityRefreshTimer->stop();
|
||||
visibilityRefreshStarted = false;
|
||||
setRemoteEnabled(false);
|
||||
}
|
||||
}
|
||||
|
|
@ -220,12 +292,15 @@ void TabDeckStorage::setRemoteEnabled(bool enabled)
|
|||
aUpload->setEnabled(enabled);
|
||||
aOpenRemoteDeck->setEnabled(enabled);
|
||||
aDownload->setEnabled(enabled);
|
||||
aShareDecks->setEnabled(enabled);
|
||||
aPublishDeck->setEnabled(enabled);
|
||||
aNewFolder->setEnabled(enabled);
|
||||
aDeleteRemoteDeck->setEnabled(enabled);
|
||||
|
||||
if (enabled) {
|
||||
serverDirView->refreshTree();
|
||||
} else {
|
||||
setShareModeEnabled(false);
|
||||
serverDirView->clearTree();
|
||||
}
|
||||
}
|
||||
|
|
@ -246,6 +321,10 @@ void TabDeckStorage::actOpenLocalDeck()
|
|||
}
|
||||
QString filePath = localDirModel->filePath(curLeft);
|
||||
|
||||
if (!isSupportedDeckFile(filePath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::optional<LoadedDeck> deckOpt = DeckLoader::loadFromFile(filePath, DeckFileFormat::Cockatrice, true);
|
||||
if (!deckOpt) {
|
||||
continue;
|
||||
|
|
@ -310,6 +389,11 @@ void TabDeckStorage::actUpload()
|
|||
|
||||
void TabDeckStorage::uploadDeck(const QString &filePath, const QString &targetPath)
|
||||
{
|
||||
if (!isSupportedDeckFile(filePath)) {
|
||||
QMessageBox::critical(this, tr("Error"), tr("Invalid deck file"));
|
||||
return;
|
||||
}
|
||||
|
||||
QFile deckFile(filePath);
|
||||
QFileInfo deckFileInfo(deckFile);
|
||||
|
||||
|
|
@ -347,6 +431,8 @@ void TabDeckStorage::uploadDeck(const QString &filePath, const QString &targetPa
|
|||
cmd.set_path(targetPath.toStdString());
|
||||
cmd.set_deck_list(deckString.toStdString());
|
||||
|
||||
cmd.set_color_identity(getDeckColorIdentity(deck, CardDatabaseManager::query()).toStdString());
|
||||
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &TabDeckStorage::uploadFinished);
|
||||
client->sendCommand(pend);
|
||||
|
|
@ -626,3 +712,241 @@ void TabDeckStorage::deleteFolderFinished(const Response &response, const Comman
|
|||
serverDirView->removeNode(toDelete);
|
||||
}
|
||||
}
|
||||
|
||||
void TabDeckStorage::actShareDecks()
|
||||
{
|
||||
setShareModeEnabled(true);
|
||||
}
|
||||
|
||||
void TabDeckStorage::cancelShareDecks()
|
||||
{
|
||||
setShareModeEnabled(false);
|
||||
}
|
||||
|
||||
void TabDeckStorage::setShareModeEnabled(bool enabled)
|
||||
{
|
||||
shareBar->setVisible(enabled);
|
||||
if (enabled) {
|
||||
shareBar->setCreateEnabled(true);
|
||||
shareBar->setName(tr("Shared decks"));
|
||||
onServerSelectionChanged();
|
||||
shareBar->focusName();
|
||||
} else {
|
||||
// Abandon any in-flight request: otherwise the timer keeps running and a late
|
||||
// response reports the share as created after the user already backed out.
|
||||
shareTimeoutTimer->stop();
|
||||
shareInFlightSeq = 0;
|
||||
serverDirView->clearSelection();
|
||||
}
|
||||
}
|
||||
|
||||
void TabDeckStorage::onServerSelectionChanged()
|
||||
{
|
||||
if (!shareBar->isVisible()) {
|
||||
return;
|
||||
}
|
||||
const auto selection = serverDirView->getCurrentSelection();
|
||||
int folders = 0;
|
||||
int files = 0;
|
||||
for (const auto *node : selection) {
|
||||
if (dynamic_cast<const RemoteDeckList_TreeModel::DirectoryNode *>(node)) {
|
||||
++folders;
|
||||
} else {
|
||||
++files;
|
||||
}
|
||||
}
|
||||
|
||||
QString hint;
|
||||
if (folders > 1) {
|
||||
hint = tr("Only one folder can be shared at a time.");
|
||||
} else if (folders > 0 && files > 0) {
|
||||
hint = tr("Share either a folder or decks, not both.");
|
||||
} else if (folders == 0 && files == 0) {
|
||||
hint = tr("Select folders or decks in the tree to share.");
|
||||
}
|
||||
shareBar->setHintText(hint, !hint.isEmpty());
|
||||
|
||||
QStringList parts;
|
||||
if (folders > 0) {
|
||||
parts << tr("%n folder(s)", "", folders);
|
||||
}
|
||||
if (files > 0) {
|
||||
parts << tr("%n deck(s)", "", files);
|
||||
}
|
||||
shareBar->setCountText(parts.isEmpty() ? tr("No decks selected")
|
||||
: tr("Selected: %1").arg(parts.join(QStringLiteral(", "))));
|
||||
}
|
||||
|
||||
void TabDeckStorage::actShareSelection()
|
||||
{
|
||||
const auto selection = serverDirView->getCurrentSelection();
|
||||
QString sharedFolder;
|
||||
bool hasFile = false;
|
||||
bool hasFolder = false;
|
||||
for (const auto *node : selection) {
|
||||
if (const auto *dirNode = dynamic_cast<const RemoteDeckList_TreeModel::DirectoryNode *>(node)) {
|
||||
hasFolder = true;
|
||||
if (!sharedFolder.isEmpty()) {
|
||||
showShareNotice(tr("Only one folder can be shared at a time."), true);
|
||||
return;
|
||||
}
|
||||
sharedFolder = dirNode->getPath();
|
||||
} else {
|
||||
hasFile = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasFile && hasFolder) {
|
||||
showShareNotice(tr("Share either a folder or decks, not both."), true);
|
||||
return;
|
||||
}
|
||||
if (hasFolder && sharedFolder.isEmpty()) {
|
||||
showShareNotice(tr("The root folder cannot be shared."), true);
|
||||
return;
|
||||
}
|
||||
|
||||
Command_DeckShareCreate cmd;
|
||||
cmd.set_name(shareBar->name().toStdString());
|
||||
if (cmd.name().empty()) {
|
||||
cmd.set_name(tr("Shared decks").toStdString());
|
||||
}
|
||||
|
||||
if (!sharedFolder.isEmpty()) {
|
||||
cmd.set_folder_path(sharedFolder.toStdString());
|
||||
} else {
|
||||
for (const auto *node : selection) {
|
||||
if (const auto *fileNode = dynamic_cast<const RemoteDeckList_TreeModel::FileNode *>(node)) {
|
||||
DeckShareItem *item = cmd.add_items();
|
||||
item->set_deck_id(fileNode->getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cmd.items_size() == 0 && cmd.folder_path().empty()) {
|
||||
showShareNotice(tr("Select decks to share."), true);
|
||||
return;
|
||||
}
|
||||
|
||||
shareBar->setCreateEnabled(false);
|
||||
const int seq = ++shareRequestSeq;
|
||||
shareInFlightSeq = seq;
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this,
|
||||
[this, seq](const Response &response, const CommandContainer &commandContainer) {
|
||||
if (shareInFlightSeq != seq) {
|
||||
return; // the user cancelled or a newer request superseded this one
|
||||
}
|
||||
shareInFlightSeq = 0;
|
||||
shareFromTreeFinished(response, commandContainer);
|
||||
});
|
||||
client->sendCommand(pend);
|
||||
shareTimeoutTimer->start();
|
||||
}
|
||||
|
||||
void TabDeckStorage::shareFromTreeFinished(const Response &response, const CommandContainer & /*commandContainer*/)
|
||||
{
|
||||
shareTimeoutTimer->stop();
|
||||
shareBar->setCreateEnabled(true);
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
qWarning() << "failed to create deck share:" << response.response_code();
|
||||
showShareNotice(tr("Failed to create the share link (server response code %1).")
|
||||
.arg(QString::number(static_cast<int>(response.response_code()))),
|
||||
true);
|
||||
return;
|
||||
}
|
||||
const DeckShareUtils::ShareResponse share = DeckShareUtils::handleShareResponse(client, response);
|
||||
|
||||
showShareNotice(
|
||||
tr("Share link copied to the clipboard.\nExpires on %1.").arg(DeckShareUtils::formatShareExpiry(share.expiry)));
|
||||
setShareModeEnabled(false);
|
||||
}
|
||||
|
||||
void TabDeckStorage::showShareNotice(const QString &message, bool warning)
|
||||
{
|
||||
QMessageBox box(warning ? QMessageBox::Warning : QMessageBox::Information, tr("Share link"), message,
|
||||
QMessageBox::Ok, this);
|
||||
box.exec();
|
||||
}
|
||||
|
||||
void TabDeckStorage::onShareFromTreeTimeout()
|
||||
{
|
||||
if (shareInFlightSeq == 0) {
|
||||
return; // share mode was left while the request was still outstanding
|
||||
}
|
||||
shareInFlightSeq = 0;
|
||||
shareBar->setCreateEnabled(true);
|
||||
showShareNotice(tr("The server did not respond in time. Try again."), true);
|
||||
}
|
||||
|
||||
void TabDeckStorage::actPublishDeck()
|
||||
{
|
||||
visibilityFailures.clear();
|
||||
// Arm the drain with the full network timeout so a lost reply still costs
|
||||
// one refresh instead of a dead column; each reply shrinks it to the short
|
||||
// delay below, so a slow round trip is never drained before it lands.
|
||||
const int visibilityFailSafeDelay =
|
||||
static_cast<int>((static_cast<qint64>(SettingsCache::instance().network().getTimeOut()) + 1) *
|
||||
SettingsCache::instance().network().getKeepAlive() * 1000);
|
||||
|
||||
const auto selection = serverDirView->getCurrentSelection();
|
||||
for (const auto *node : selection) {
|
||||
Command_DeckSetVisibility cmd;
|
||||
if (const auto *fileNode = dynamic_cast<const RemoteDeckList_TreeModel::FileNode *>(node)) {
|
||||
cmd.set_deck_id(fileNode->getId());
|
||||
} else if (const auto *dirNode = dynamic_cast<const RemoteDeckList_TreeModel::DirectoryNode *>(node)) {
|
||||
const QString path = dirNode->getPath();
|
||||
if (path.isEmpty()) {
|
||||
continue; // the root folder cannot be published
|
||||
}
|
||||
cmd.set_folder_path(path.toStdString());
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
// Toggle the node's own visibility bit (what the server persists); the
|
||||
// effective visibility shown by the column may additionally be inherited
|
||||
// from a parent folder.
|
||||
cmd.set_is_public(!node->isPublic());
|
||||
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &TabDeckStorage::setVisibilityFinished);
|
||||
visibilityRefreshStarted = true;
|
||||
visibilityRefreshTimer->setInterval(visibilityFailSafeDelay);
|
||||
visibilityRefreshTimer->start();
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
}
|
||||
|
||||
void TabDeckStorage::setVisibilityFinished(const Response &r, const CommandContainer & /*commandContainer*/)
|
||||
{
|
||||
if (r.response_code() == Response::RespOk) {
|
||||
if (visibilityRefreshStarted) {
|
||||
visibilityRefreshTimer->setInterval(VISIBILITY_REFRESH_DELAY);
|
||||
visibilityRefreshTimer->start();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect batch failures and surface them once, when publishing quiets
|
||||
// down, instead of stacking one modal dialog per rejected node.
|
||||
const QString message = tr("Failed to change deck visibility on server (response code %1).")
|
||||
.arg(QString::number(static_cast<int>(r.response_code())));
|
||||
if (visibilityRefreshStarted) {
|
||||
visibilityFailures.append(message);
|
||||
visibilityRefreshTimer->setInterval(VISIBILITY_REFRESH_DELAY);
|
||||
visibilityRefreshTimer->start();
|
||||
} else {
|
||||
QMessageBox::critical(this, tr("Error"), message);
|
||||
}
|
||||
}
|
||||
|
||||
void TabDeckStorage::onVisibilityRefreshTimeout()
|
||||
{
|
||||
visibilityRefreshStarted = false;
|
||||
if (!visibilityFailures.isEmpty()) {
|
||||
QMessageBox::critical(
|
||||
this, tr("Error"),
|
||||
tr("Failed to change the visibility of %n selected deck(s).", "", visibilityFailures.size()));
|
||||
visibilityFailures.clear();
|
||||
}
|
||||
serverDirView->refreshTree();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
#include "../interface/widgets/server/remote/remote_decklist_tree_widget.h"
|
||||
#include "tab.h"
|
||||
|
||||
#include <QStringList>
|
||||
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
||||
|
||||
struct LoadedDeck;
|
||||
|
|
@ -22,8 +23,10 @@ class QToolBar;
|
|||
class QTreeWidget;
|
||||
class QTreeWidgetItem;
|
||||
class QGroupBox;
|
||||
class QTimer;
|
||||
class CommandContainer;
|
||||
class Response;
|
||||
class ShareBarWidget;
|
||||
|
||||
class TabDeckStorage : public Tab
|
||||
{
|
||||
|
|
@ -35,14 +38,25 @@ private:
|
|||
QToolBar *leftToolBar, *rightToolBar;
|
||||
RemoteDeckList_TreeWidget *serverDirView;
|
||||
QGroupBox *leftGroupBox, *rightGroupBox;
|
||||
ShareBarWidget *shareBar;
|
||||
QTimer *shareTimeoutTimer;
|
||||
int shareRequestSeq = 0;
|
||||
int shareInFlightSeq = 0;
|
||||
|
||||
QAction *aOpenLocalDeck, *aRenameLocal, *aUpload, *aNewLocalFolder, *aDeleteLocalDeck;
|
||||
QAction *aOpenDecksFolder;
|
||||
QAction *aOpenRemoteDeck, *aDownload, *aNewFolder, *aDeleteRemoteDeck;
|
||||
QAction *aOpenRemoteDeck, *aDownload, *aShareDecks, *aPublishDeck, *aNewFolder, *aDeleteRemoteDeck;
|
||||
bool visibilityRefreshStarted = false;
|
||||
QTimer *visibilityRefreshTimer;
|
||||
QStringList visibilityFailures;
|
||||
QString getTargetPath() const;
|
||||
|
||||
void setRemoteEnabled(bool enabled);
|
||||
|
||||
void showShareNotice(const QString &message, bool warning = false);
|
||||
|
||||
void setShareModeEnabled(bool enabled);
|
||||
|
||||
void uploadDeck(const QString &filePath, const QString &targetPath);
|
||||
void deleteRemoteDeck(const RemoteDeckList_TreeModel::Node *node);
|
||||
|
||||
|
|
@ -75,6 +89,17 @@ private slots:
|
|||
void actNewFolder();
|
||||
void newFolderFinished(const Response &response, const CommandContainer &commandContainer);
|
||||
|
||||
void actShareDecks();
|
||||
void actShareSelection();
|
||||
void cancelShareDecks();
|
||||
void onServerSelectionChanged();
|
||||
void shareFromTreeFinished(const Response &r, const CommandContainer &commandContainer);
|
||||
void onShareFromTreeTimeout();
|
||||
|
||||
void actPublishDeck();
|
||||
void setVisibilityFinished(const Response &r, const CommandContainer &commandContainer);
|
||||
void onVisibilityRefreshTimeout();
|
||||
|
||||
void actDeleteRemoteDeck();
|
||||
void deleteFolderFinished(const Response &response, const CommandContainer &commandContainer);
|
||||
void deleteDeckFinished(const Response &response, const CommandContainer &commandContainer);
|
||||
|
|
|
|||
|
|
@ -259,7 +259,7 @@ void TabGame::linkCardToChat(const QString &cardName)
|
|||
sayEdit->setFocus();
|
||||
}
|
||||
|
||||
void TabGame::resetChatAndPhase()
|
||||
void TabGame::resetForRewind()
|
||||
{
|
||||
// reset chat log
|
||||
messageLog->clearChat();
|
||||
|
|
@ -267,6 +267,9 @@ void TabGame::resetChatAndPhase()
|
|||
// reset phase markers
|
||||
game->getGameState()->setCurrentPhase(-1);
|
||||
|
||||
// reset view zones
|
||||
static_cast<GameScene *>(gameView->scene())->emptyActiveViews();
|
||||
|
||||
// reset spectator state so the replay can rebuild it from the start
|
||||
game->getPlayerManager()->clearSpectators();
|
||||
playerListWidget->clearSpectators();
|
||||
|
|
@ -1270,7 +1273,7 @@ void TabGame::createReplayDock(GameReplay *replay)
|
|||
replayDock->setWidget(replayWidget);
|
||||
replayDock->setFloating(false);
|
||||
|
||||
connect(replayWidget, &ReplayWidget::rewound, this, &TabGame::resetChatAndPhase);
|
||||
connect(replayWidget, &ReplayWidget::rewound, this, &TabGame::resetForRewind);
|
||||
connect(replayWidget, &ReplayWidget::eventReplayed, game->getGameEventHandler(),
|
||||
[this](const auto &event, auto options) {
|
||||
game->getGameEventHandler()->processGameEventContainer(event, nullptr, options);
|
||||
|
|
|
|||
|
|
@ -204,7 +204,7 @@ public:
|
|||
|
||||
public slots:
|
||||
void viewCardInfo(const CardRef &cardRef = {}) const;
|
||||
void resetChatAndPhase();
|
||||
void resetForRewind();
|
||||
void updateTimeElapsedLabel(QString newTime);
|
||||
void addPlayerToAutoCompleteList(QString playerName);
|
||||
void removePlayerFromAutoCompleteList(QString playerName);
|
||||
|
|
|
|||
253
cockatrice/src/interface/widgets/tabs/tab_public_decks.cpp
Normal file
253
cockatrice/src/interface/widgets/tabs/tab_public_decks.cpp
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
#include "tab_public_decks.h"
|
||||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../../deck_loader/deck_loader.h"
|
||||
#include "../cards/card_size_widget.h"
|
||||
#include "../general/layout_containers/flow_widget.h"
|
||||
#include "../visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.h"
|
||||
#include "../visual_deck_storage/deck_preview/public_deck_preview_widget.h"
|
||||
#include "../visual_deck_storage/remote_public_decks_model.h"
|
||||
#include "../visual_deck_storage/visual_deck_storage_quick_settings_widget.h"
|
||||
#include "../visual_deck_storage/visual_deck_storage_search_widget.h"
|
||||
#include "../visual_deck_storage/visual_deck_storage_tag_filter_widget.h"
|
||||
#include "tab_supervisor.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QPixmap>
|
||||
#include <QStringList>
|
||||
#include <QToolButton>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
||||
#include <libcockatrice/protocol/pb/command_deck_download_public.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_deck_download.pb.h>
|
||||
#include <libcockatrice/protocol/pending_command.h>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
#include <optional>
|
||||
|
||||
TabPublicDecks::TabPublicDecks(TabSupervisor *_tabSupervisor, AbstractClient *_client, const QString &_userName)
|
||||
: Tab(_tabSupervisor), client(_client), userName(_userName)
|
||||
{
|
||||
model = new RemotePublicDecksModel(client, this);
|
||||
cardSize = SettingsCache::instance().cardsDisplay().getVisualDeckStorageCardSize();
|
||||
|
||||
titleLabel = new QLabel(tr("Public decks of %1").arg(userName.toHtmlEscaped()), this);
|
||||
QFont titleFont = titleLabel->font();
|
||||
titleFont.setBold(true);
|
||||
titleLabel->setFont(titleFont);
|
||||
|
||||
auto *headerLayout = new QHBoxLayout;
|
||||
headerLayout->addWidget(titleLabel);
|
||||
headerLayout->addStretch(1);
|
||||
|
||||
// Filter/toolbar row, matching the Visual Deck Storage: color identity filter
|
||||
// first, the search bar stretching in the middle, and the quick settings
|
||||
// cogwheel at the end. The card size slider lives inside the cogwheel popup.
|
||||
emptyLabel = new QLabel(tr("This user has not published any decks."), this);
|
||||
emptyLabel->setAlignment(Qt::AlignCenter);
|
||||
emptyLabel->setVisible(false);
|
||||
|
||||
statusLabel = new QLabel(this);
|
||||
statusLabel->setAlignment(Qt::AlignCenter);
|
||||
statusLabel->setVisible(false);
|
||||
|
||||
flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded);
|
||||
flowWidget->setSpacing(8, 8);
|
||||
|
||||
colorIdentityFilter = new DeckPreviewColorIdentityFilterWidget(this);
|
||||
searchWidget = new VisualDeckStorageSearchWidget(this);
|
||||
refreshButton = new QToolButton(this);
|
||||
refreshButton->setIcon(QPixmap("theme:icons/reload"));
|
||||
refreshButton->setFixedSize(32, 32);
|
||||
quickSettingsWidget = new VisualDeckStorageQuickSettingsWidget(this);
|
||||
quickSettingsWidget->setPublicDecksMode(true);
|
||||
|
||||
auto *filterLayout = new QHBoxLayout;
|
||||
filterLayout->addWidget(colorIdentityFilter);
|
||||
filterLayout->addWidget(searchWidget, 1);
|
||||
filterLayout->addWidget(refreshButton);
|
||||
filterLayout->addWidget(quickSettingsWidget);
|
||||
|
||||
tagFilterWidget = new VisualDeckStorageTagFilterWidget(this);
|
||||
tagFilterWidget->setAllTagsProvider([this] { return model->allTags(); });
|
||||
updateTagsVisibility(quickSettingsWidget->getShowTagFilter());
|
||||
|
||||
auto *layout = new QVBoxLayout;
|
||||
layout->addLayout(headerLayout);
|
||||
layout->addLayout(filterLayout);
|
||||
layout->addWidget(tagFilterWidget);
|
||||
layout->addWidget(statusLabel);
|
||||
layout->addWidget(emptyLabel);
|
||||
layout->addWidget(flowWidget, 1);
|
||||
|
||||
auto *mainWidget = new QWidget(this);
|
||||
mainWidget->setLayout(layout);
|
||||
setCentralWidget(mainWidget);
|
||||
|
||||
connect(refreshButton, &QToolButton::clicked, this, [this] { model->refresh(userName); });
|
||||
connect(model, &QAbstractItemModel::modelReset, this, &TabPublicDecks::rebuildGrid);
|
||||
connect(model, &RemotePublicDecksModel::loadingChanged, this, &TabPublicDecks::updateLoadingState);
|
||||
connect(model, &RemotePublicDecksModel::loadFailed, this, [this](const QString &message) {
|
||||
lastFailureMessage = message;
|
||||
statusLabel->setText(message);
|
||||
statusLabel->setVisible(true);
|
||||
flowWidget->setVisible(false);
|
||||
emptyLabel->setVisible(false);
|
||||
});
|
||||
connect(searchWidget, &VisualDeckStorageSearchWidget::searchTextChanged, this,
|
||||
[this](const QString &text) { model->setSearchText(text); });
|
||||
connect(colorIdentityFilter, &DeckPreviewColorIdentityFilterWidget::activeColorsChanged, this,
|
||||
&TabPublicDecks::updateColorFilter);
|
||||
connect(colorIdentityFilter, &DeckPreviewColorIdentityFilterWidget::filterModeChanged, this,
|
||||
&TabPublicDecks::updateColorFilter);
|
||||
connect(tagFilterWidget, &VisualDeckStorageTagFilterWidget::filterChanged, this, &TabPublicDecks::updateTagFilter);
|
||||
connect(quickSettingsWidget, &VisualDeckStorageQuickSettingsWidget::cardSizeChanged, this,
|
||||
&TabPublicDecks::updateCardSize);
|
||||
connect(quickSettingsWidget, &VisualDeckStorageQuickSettingsWidget::showTagFilterChanged, this,
|
||||
&TabPublicDecks::updateTagsVisibility);
|
||||
quickSettingsWidget->getCardSizeWidget()->enableCtrlScrollResize(flowWidget);
|
||||
|
||||
retranslateUi();
|
||||
model->refresh(userName);
|
||||
}
|
||||
|
||||
QString TabPublicDecks::getTabText() const
|
||||
{
|
||||
return tr("Public decks of %1").arg(userName);
|
||||
}
|
||||
|
||||
void TabPublicDecks::retranslateUi()
|
||||
{
|
||||
// The username is another user's data, so escape it for the AutoText QLabel.
|
||||
titleLabel->setText(tr("Public decks of %1").arg(userName.toHtmlEscaped()));
|
||||
// The same choice rebuildGrid makes, so a language change does not swap
|
||||
// the "no match" variant for the "nothing published" one.
|
||||
emptyLabel->setText(model->totalCount() > 0 ? tr("No decks match your filters.")
|
||||
: tr("This user has not published any decks."));
|
||||
refreshButton->setToolTip(tr("Refresh"));
|
||||
refreshButton->setAccessibleName(tr("Refresh"));
|
||||
quickSettingsWidget->setToolTip(tr("Public Decks Settings"));
|
||||
// Re-show whatever the status label is showing so a language change picks up
|
||||
// the new language or, for a failure message, at least does not hide it.
|
||||
if (model->isLoading()) {
|
||||
updateLoadingState(true);
|
||||
} else if (!lastFailureMessage.isEmpty()) {
|
||||
statusLabel->setText(lastFailureMessage);
|
||||
statusLabel->setVisible(true);
|
||||
flowWidget->setVisible(false);
|
||||
emptyLabel->setVisible(false);
|
||||
} else {
|
||||
updateLoadingState(false);
|
||||
}
|
||||
emit tabTextChanged(this, getTabText());
|
||||
}
|
||||
|
||||
bool TabPublicDecks::closeRequest()
|
||||
{
|
||||
emit closing(this);
|
||||
return Tab::closeRequest();
|
||||
}
|
||||
|
||||
void TabPublicDecks::rebuildGrid()
|
||||
{
|
||||
flowWidget->clearLayout();
|
||||
|
||||
const int count = model->rowCount();
|
||||
if (count == 0) {
|
||||
emptyLabel->setText(model->totalCount() > 0 ? tr("No decks match your filters.")
|
||||
: tr("This user has not published any decks."));
|
||||
}
|
||||
emptyLabel->setVisible(count == 0);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
auto *tile = new PublicDeckPreviewWidget(flowWidget, model->entryAt(i));
|
||||
tile->setScaleFactor(cardSize);
|
||||
connect(tile, &PublicDeckPreviewWidget::openDeckRequested, this, &TabPublicDecks::openDeck);
|
||||
flowWidget->addWidget(tile);
|
||||
}
|
||||
|
||||
// The deck set changed, so the tag filter chips are re-gathered from it.
|
||||
tagFilterWidget->refreshTags();
|
||||
}
|
||||
|
||||
void TabPublicDecks::updateColorFilter()
|
||||
{
|
||||
model->setColorFilter(colorIdentityFilter->getFilterMode(), colorIdentityFilter->getActiveColors());
|
||||
}
|
||||
|
||||
void TabPublicDecks::updateTagFilter()
|
||||
{
|
||||
const QStringList selectedTags = tagFilterWidget->selectedTags();
|
||||
const QStringList excludedTags = tagFilterWidget->excludedTags();
|
||||
model->setTagFilter(QSet<QString>(selectedTags.cbegin(), selectedTags.cend()),
|
||||
QSet<QString>(excludedTags.cbegin(), excludedTags.cend()));
|
||||
tagFilterWidget->refreshTags();
|
||||
}
|
||||
|
||||
void TabPublicDecks::updateTagsVisibility(bool visible)
|
||||
{
|
||||
tagFilterWidget->setVisible(visible);
|
||||
}
|
||||
|
||||
void TabPublicDecks::updateLoadingState(bool loading)
|
||||
{
|
||||
if (loading) {
|
||||
// A new attempt is under way, so the previously shown failure, if any,
|
||||
// no longer describes the current state.
|
||||
lastFailureMessage.clear();
|
||||
statusLabel->setText(tr("Loading public decks…"));
|
||||
statusLabel->setVisible(true);
|
||||
flowWidget->setVisible(false);
|
||||
emptyLabel->setVisible(false);
|
||||
} else {
|
||||
statusLabel->setVisible(false);
|
||||
flowWidget->setVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
void TabPublicDecks::updateCardSize(int scale)
|
||||
{
|
||||
cardSize = scale;
|
||||
applyCardSize(scale);
|
||||
}
|
||||
|
||||
void TabPublicDecks::applyCardSize(int scale)
|
||||
{
|
||||
const auto tiles = flowWidget->findChildren<PublicDeckPreviewWidget *>();
|
||||
for (PublicDeckPreviewWidget *tile : tiles) {
|
||||
tile->setScaleFactor(scale);
|
||||
}
|
||||
flowWidget->setMinimumSizeToMaxSizeHint();
|
||||
}
|
||||
|
||||
void TabPublicDecks::openDeck(int deckId)
|
||||
{
|
||||
Command_DeckDownloadPublic cmd;
|
||||
cmd.set_deck_id(deckId);
|
||||
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &TabPublicDecks::openDeckFinished);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void TabPublicDecks::openDeckFinished(const Response &response, const CommandContainer & /*commandContainer*/)
|
||||
{
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
QMessageBox::warning(this, tr("Open public deck"),
|
||||
tr("Failed to open the public deck (server response code %1).")
|
||||
.arg(QString::number(static_cast<int>(response.response_code()))));
|
||||
return;
|
||||
}
|
||||
|
||||
const Response_DeckDownload &resp = response.GetExtension(Response_DeckDownload::ext);
|
||||
std::optional<LoadedDeck> deckOpt =
|
||||
DeckLoader::loadFromRemote(QString::fromStdString(resp.deck()), LoadedDeck::LoadInfo::NON_REMOTE_ID);
|
||||
if (!deckOpt) {
|
||||
QMessageBox::warning(this, tr("Open public deck"), tr("The public deck could not be parsed."));
|
||||
return;
|
||||
}
|
||||
|
||||
tabSupervisor->openDeckInNewTab(deckOpt.value());
|
||||
}
|
||||
80
cockatrice/src/interface/widgets/tabs/tab_public_decks.h
Normal file
80
cockatrice/src/interface/widgets/tabs/tab_public_decks.h
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
/**
|
||||
* @file tab_public_decks.h
|
||||
* @ingroup Tabs
|
||||
*/
|
||||
|
||||
#ifndef TAB_PUBLIC_DECKS_H
|
||||
#define TAB_PUBLIC_DECKS_H
|
||||
|
||||
#include "tab.h"
|
||||
|
||||
class AbstractClient;
|
||||
class CommandContainer;
|
||||
class DeckPreviewColorIdentityFilterWidget;
|
||||
class FlowWidget;
|
||||
class PublicDeckPreviewWidget;
|
||||
class QLabel;
|
||||
class QToolButton;
|
||||
class RemotePublicDecksModel;
|
||||
class Response;
|
||||
class VisualDeckStorageQuickSettingsWidget;
|
||||
class VisualDeckStorageSearchWidget;
|
||||
class VisualDeckStorageTagFilterWidget;
|
||||
|
||||
/**
|
||||
* @brief A visual grid of the public decks published by another user.
|
||||
*
|
||||
* The grid is rendered from the preview metadata the server stores for the
|
||||
* decks, so browsing costs no downloads; the deck list is fetched via
|
||||
* Command_DeckDownloadPublic only when the user opens a deck. Multiple users
|
||||
* can be browsed simultaneously; each gets its own tab.
|
||||
*/
|
||||
class TabPublicDecks final : public Tab
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
TabPublicDecks(TabSupervisor *tabSupervisor, AbstractClient *client, const QString &userName);
|
||||
|
||||
[[nodiscard]] QString getTabText() const override;
|
||||
void retranslateUi() override;
|
||||
bool closeRequest() override;
|
||||
|
||||
[[nodiscard]] QString getUserName() const
|
||||
{
|
||||
return userName;
|
||||
}
|
||||
|
||||
signals:
|
||||
void closing(TabPublicDecks *tab);
|
||||
|
||||
private slots:
|
||||
void openDeck(int deckId);
|
||||
void openDeckFinished(const Response &response, const CommandContainer &commandContainer);
|
||||
void updateColorFilter();
|
||||
void updateTagFilter();
|
||||
void updateCardSize(int scale);
|
||||
void updateTagsVisibility(bool visible);
|
||||
void updateLoadingState(bool loading);
|
||||
|
||||
private:
|
||||
void rebuildGrid();
|
||||
void applyCardSize(int scale);
|
||||
|
||||
AbstractClient *client;
|
||||
QString userName;
|
||||
RemotePublicDecksModel *model;
|
||||
FlowWidget *flowWidget;
|
||||
VisualDeckStorageSearchWidget *searchWidget;
|
||||
DeckPreviewColorIdentityFilterWidget *colorIdentityFilter;
|
||||
VisualDeckStorageTagFilterWidget *tagFilterWidget;
|
||||
QToolButton *refreshButton;
|
||||
VisualDeckStorageQuickSettingsWidget *quickSettingsWidget;
|
||||
QLabel *titleLabel;
|
||||
QLabel *statusLabel;
|
||||
QLabel *emptyLabel;
|
||||
QString lastFailureMessage; ///< Last load-failure text, re-shown on retranslate.
|
||||
int cardSize = 100;
|
||||
};
|
||||
|
||||
#endif // TAB_PUBLIC_DECKS_H
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
#include <QApplication>
|
||||
#include <QClipboard>
|
||||
#include <QDesktopServices>
|
||||
#include <QDir>
|
||||
#include <QFileSystemModel>
|
||||
#include <QGroupBox>
|
||||
#include <QHBoxLayout>
|
||||
|
|
@ -34,6 +35,12 @@
|
|||
|
||||
inline Q_LOGGING_CATEGORY(TabReplaysLog, "replays_tab");
|
||||
|
||||
namespace
|
||||
{
|
||||
// File name filters for the local replay files Cockatrice can load.
|
||||
const QStringList REPLAY_FILE_NAME_FILTERS = {"*.cor"};
|
||||
} // namespace
|
||||
|
||||
TabReplays::TabReplays(TabSupervisor *_tabSupervisor, AbstractClient *_client, const ServerInfo_User *currentUserInfo)
|
||||
: Tab(_tabSupervisor), client(_client)
|
||||
{
|
||||
|
|
@ -62,6 +69,8 @@ QGroupBox *TabReplays::createLeftLayout()
|
|||
{
|
||||
localDirModel = new QFileSystemModel(this);
|
||||
localDirModel->setRootPath(SettingsCache::instance().paths().getReplaysPath());
|
||||
localDirModel->setNameFilters(REPLAY_FILE_NAME_FILTERS);
|
||||
localDirModel->setNameFilterDisables(false);
|
||||
localDirModel->sort(0, Qt::AscendingOrder);
|
||||
|
||||
localDirView = new QTreeView;
|
||||
|
|
@ -263,6 +272,10 @@ void TabReplays::actOpenLocalReplay()
|
|||
}
|
||||
QString filePath = localDirModel->filePath(curLeft);
|
||||
|
||||
if (!QDir::match(REPLAY_FILE_NAME_FILTERS, QFileInfo(filePath).fileName())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QFile f(filePath);
|
||||
if (!f.open(QIODevice::ReadOnly)) {
|
||||
continue;
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
#include "tab_logs.h"
|
||||
#include "tab_message.h"
|
||||
#include "tab_moderation.h"
|
||||
#include "tab_public_decks.h"
|
||||
#include "tab_replays.h"
|
||||
#include "tab_report.h"
|
||||
#include "tab_room.h"
|
||||
|
|
@ -274,6 +275,10 @@ void TabSupervisor::retranslateUi()
|
|||
while (gameIterator.hasNext()) {
|
||||
tabs.append(gameIterator.next().value());
|
||||
}
|
||||
QMapIterator<QString, TabPublicDecks *> publicDecksIterator(publicDecksTabs);
|
||||
while (publicDecksIterator.hasNext()) {
|
||||
tabs.append(publicDecksIterator.next().value());
|
||||
}
|
||||
QListIterator<TabGame *> replayIterator(replayTabs);
|
||||
while (replayIterator.hasNext()) {
|
||||
tabs.append(replayIterator.next());
|
||||
|
|
@ -626,6 +631,10 @@ void TabSupervisor::stop()
|
|||
tabsToDelete << i.value();
|
||||
}
|
||||
|
||||
for (auto i = publicDecksTabs.cbegin(), end = publicDecksTabs.cend(); i != end; ++i) {
|
||||
tabsToDelete << i.value();
|
||||
}
|
||||
|
||||
for (const auto tab : tabsToDelete) {
|
||||
tab->close();
|
||||
}
|
||||
|
|
@ -672,7 +681,7 @@ void TabSupervisor::actTabVisualDeckStorage(bool checked)
|
|||
|
||||
void TabSupervisor::openTabVisualDeckStorage()
|
||||
{
|
||||
tabVisualDeckStorage = new TabDeckStorageVisual(this);
|
||||
tabVisualDeckStorage = new TabDeckStorageVisual(this, client);
|
||||
myAddTab(tabVisualDeckStorage, aTabVisualDeckStorage);
|
||||
connect(tabVisualDeckStorage, &QObject::destroyed, this, [this] {
|
||||
tabVisualDeckStorage = nullptr;
|
||||
|
|
@ -1037,6 +1046,30 @@ void TabSupervisor::roomLeft(TabRoom *tab)
|
|||
removeTab(indexOf(tab));
|
||||
}
|
||||
|
||||
void TabSupervisor::openTabPublicDecks(const QString &userName)
|
||||
{
|
||||
if (auto *existing = publicDecksTabs.value(userName, nullptr)) {
|
||||
setCurrentWidget(existing);
|
||||
return;
|
||||
}
|
||||
|
||||
auto *tab = new TabPublicDecks(this, client, userName);
|
||||
connect(tab, &TabPublicDecks::closing, this, &TabSupervisor::publicDecksClosed);
|
||||
myAddTab(tab);
|
||||
publicDecksTabs.insert(userName, tab);
|
||||
setCurrentWidget(tab);
|
||||
}
|
||||
|
||||
void TabSupervisor::publicDecksClosed(TabPublicDecks *tab)
|
||||
{
|
||||
if (tab == currentWidget()) {
|
||||
emit setMenu();
|
||||
}
|
||||
|
||||
publicDecksTabs.remove(tab->getUserName());
|
||||
removeTab(indexOf(tab));
|
||||
}
|
||||
|
||||
void TabSupervisor::switchToFirstAvailableNetworkTab()
|
||||
{
|
||||
if (!roomTabs.isEmpty()) {
|
||||
|
|
@ -1067,10 +1100,27 @@ void TabSupervisor::replayLeft(TabGame *tab)
|
|||
}
|
||||
|
||||
void TabSupervisor::joinReportGame(const int gameId, const int roomId)
|
||||
{
|
||||
joinGameByIntent(gameId, roomId, true);
|
||||
}
|
||||
|
||||
void TabSupervisor::joinGameFromUserCard(const int gameId, const int roomId, const bool asSpectator)
|
||||
{
|
||||
joinGameByIntent(gameId, roomId, asSpectator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the join/spectate intent for @p gameId in @p roomId.
|
||||
*
|
||||
* The intent owns the whole flow: if the room tab is not open yet it joins the
|
||||
* room first, then retries the game, so callers do not have to care whether the
|
||||
* room happens to be on screen.
|
||||
*/
|
||||
void TabSupervisor::joinGameByIntent(const int gameId, const int roomId, const bool asSpectator)
|
||||
{
|
||||
auto *remoteClient = qobject_cast<RemoteClient *>(client);
|
||||
if (!remoteClient) {
|
||||
actShowPopup(tr("Report joins are only available on a remote server."));
|
||||
actShowPopup(tr("Game joins are only available on a remote server."));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1079,7 +1129,7 @@ void TabSupervisor::joinReportGame(const int gameId, const int roomId)
|
|||
ctx->roomContext.serverContext.port = QString::number(remoteClient->peerPort());
|
||||
ctx->roomContext.roomId = roomId;
|
||||
ctx->gameId = gameId;
|
||||
ctx->asSpectator = true;
|
||||
ctx->asSpectator = asSpectator;
|
||||
|
||||
auto *joinGameIntent = new IntentJoinServerGame(this, remoteClient, std::move(ctx));
|
||||
joinGameIntent->setParent(this);
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ class TabAccount;
|
|||
class TabDeckEditor;
|
||||
class TabDeveloper;
|
||||
class TabLog;
|
||||
class TabPublicDecks;
|
||||
class RoomEvent;
|
||||
class GameEventContainer;
|
||||
class Event_GameJoined;
|
||||
|
|
@ -114,6 +115,7 @@ private:
|
|||
QMap<int, TabGame *> gameTabs;
|
||||
QList<TabGame *> replayTabs;
|
||||
QMap<QString, TabMessage *> messageTabs;
|
||||
QMap<QString, TabPublicDecks *> publicDecksTabs;
|
||||
QList<AbstractTabDeckEditor *> deckEditorTabs;
|
||||
bool isLocalGame;
|
||||
|
||||
|
|
@ -126,6 +128,8 @@ private:
|
|||
static QString sanitizeTabName(QString dirty);
|
||||
static QString sanitizeHtml(QString dirty);
|
||||
void resetTabsMenu();
|
||||
/** Shared join/spectate driver behind the report tab and the profile hover card. */
|
||||
void joinGameByIntent(int gameId, int roomId, bool asSpectator);
|
||||
|
||||
public:
|
||||
explicit TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget *parent = nullptr);
|
||||
|
|
@ -152,6 +156,10 @@ public:
|
|||
return userInfo;
|
||||
}
|
||||
[[nodiscard]] AbstractClient *getClient() const;
|
||||
[[nodiscard]] AbstractClient *getServerClient() const
|
||||
{
|
||||
return client;
|
||||
}
|
||||
[[nodiscard]] UserListManager *getUserListManager() const
|
||||
{
|
||||
return userListManager;
|
||||
|
|
@ -192,6 +200,8 @@ public slots:
|
|||
TabEdhRec *addEdhrecTab(const CardInfoPtr &cardToQuery, bool isCommander = false);
|
||||
void openReplay(GameReplay *replay);
|
||||
void joinReportGame(int gameId, int roomId);
|
||||
/** Joins or spectates a game listed on a user's profile hover card. */
|
||||
void joinGameFromUserCard(int gameId, int roomId, bool asSpectator);
|
||||
void openTabModeration(const QString &userName = {});
|
||||
void switchToFirstAvailableNetworkTab();
|
||||
void maximizeMainWindow();
|
||||
|
|
@ -199,6 +209,7 @@ public slots:
|
|||
void actTabReplays(bool checked);
|
||||
void openTabServer();
|
||||
void addRoomTab(const ServerInfo_Room &info, bool setCurrent);
|
||||
void openTabPublicDecks(const QString &userName);
|
||||
private slots:
|
||||
void refreshShortcuts();
|
||||
|
||||
|
|
@ -231,6 +242,7 @@ private slots:
|
|||
void localGameJoined(const Event_GameJoined &event);
|
||||
void gameLeft(TabGame *tab);
|
||||
void roomLeft(TabRoom *tab);
|
||||
void publicDecksClosed(TabPublicDecks *tab);
|
||||
TabMessage *addMessageTab(const QString &userName, bool focus);
|
||||
void replayLeft(TabGame *tab);
|
||||
void processUserLeft(const QString &userName);
|
||||
|
|
|
|||
|
|
@ -1,25 +1,78 @@
|
|||
#include "tab_deck_storage_visual.h"
|
||||
|
||||
#include "../../../../client/settings/cache_settings.h"
|
||||
#include "../../../deck_loader/deck_loader.h"
|
||||
#include "../../cards/additional_info/deck_color_identity.h"
|
||||
#include "../../deck_share/deck_share_utils.h"
|
||||
#include "../../interface/widgets/visual_deck_storage/visual_deck_storage_widget.h"
|
||||
#include "../tab_supervisor.h"
|
||||
|
||||
#include <QMessageBox>
|
||||
#include <libcockatrice/protocol/pb/command_deck_del.pb.h>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
||||
#include <libcockatrice/protocol/pb/command_deck_share_create.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_deck_share_create.pb.h>
|
||||
#include <libcockatrice/protocol/pending_command.h>
|
||||
#include <libcockatrice/settings/network_settings.h>
|
||||
|
||||
TabDeckStorageVisual::TabDeckStorageVisual(TabSupervisor *_tabSupervisor)
|
||||
: Tab(_tabSupervisor), visualDeckStorageWidget(new VisualDeckStorageWidget(this))
|
||||
TabDeckStorageVisual::TabDeckStorageVisual(TabSupervisor *_tabSupervisor, AbstractClient *_client)
|
||||
: Tab(_tabSupervisor), visualDeckStorageWidget(new VisualDeckStorageWidget(this)), client(_client),
|
||||
shareTimeoutTimer(new QTimer(this))
|
||||
{
|
||||
connect(this, &TabDeckStorageVisual::openDeckEditor, tabSupervisor, &TabSupervisor::openDeckInNewTab);
|
||||
connect(visualDeckStorageWidget, &VisualDeckStorageWidget::deckLoadRequested, this,
|
||||
&TabDeckStorageVisual::actOpenLocalDeck);
|
||||
connect(visualDeckStorageWidget, &VisualDeckStorageWidget::openDeckEditor, this,
|
||||
&TabDeckStorageVisual::openDeckEditor);
|
||||
connect(visualDeckStorageWidget, &VisualDeckStorageWidget::shareDeckRequested, this,
|
||||
&TabDeckStorageVisual::actShareDeck);
|
||||
connect(visualDeckStorageWidget, &VisualDeckStorageWidget::shareSelectionChanged, this,
|
||||
&TabDeckStorageVisual::onShareSelectionChanged);
|
||||
connect(visualDeckStorageWidget, &VisualDeckStorageWidget::shareRequested, this, [this] {
|
||||
if (shareDeckAvailable) {
|
||||
enterShareMode();
|
||||
}
|
||||
});
|
||||
|
||||
auto *widget = new QWidget(this);
|
||||
auto *layout = new QVBoxLayout(widget);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setSpacing(0);
|
||||
widget->setLayout(layout);
|
||||
this->setCentralWidget(widget);
|
||||
layout->addWidget(visualDeckStorageWidget);
|
||||
|
||||
shareBar = new ShareBarWidget(this);
|
||||
connect(shareBar, &ShareBarWidget::createRequested, this, &TabDeckStorageVisual::actShareSelected);
|
||||
connect(shareBar, &ShareBarWidget::cancelRequested, this, &TabDeckStorageVisual::exitShareMode);
|
||||
|
||||
layout->insertWidget(0, shareBar);
|
||||
shareBar->setVisible(false);
|
||||
|
||||
connect(client, &AbstractClient::statusChanged, this, &TabDeckStorageVisual::handleConnectionChanged);
|
||||
shareDeckAvailable = (client->getStatus() == StatusLoggedIn);
|
||||
visualDeckStorageWidget->setShareAvailable(shareDeckAvailable);
|
||||
|
||||
shareTimeoutTimer->setSingleShot(true);
|
||||
shareTimeoutTimer->setInterval(
|
||||
static_cast<int>((static_cast<qint64>(SettingsCache::instance().network().getTimeOut()) + 1) *
|
||||
SettingsCache::instance().network().getKeepAlive() * 1000));
|
||||
connect(shareTimeoutTimer, &QTimer::timeout, this, &TabDeckStorageVisual::onShareTimeout);
|
||||
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
void TabDeckStorageVisual::retranslateUi()
|
||||
{
|
||||
visualDeckStorageWidget->retranslateUi();
|
||||
shareBar->retranslateUi();
|
||||
if (shareBar->isVisible()) {
|
||||
updateShareHint();
|
||||
}
|
||||
}
|
||||
|
||||
void TabDeckStorageVisual::actOpenLocalDeck(const QString &filePath)
|
||||
|
|
@ -33,3 +86,144 @@ void TabDeckStorageVisual::actOpenLocalDeck(const QString &filePath)
|
|||
|
||||
emit openDeckEditor(deckOpt.value());
|
||||
}
|
||||
|
||||
void TabDeckStorageVisual::enterShareMode(const QStringList &preselectFiles)
|
||||
{
|
||||
if (!shareDeckAvailable) {
|
||||
return; // sharing is gated on being logged in
|
||||
}
|
||||
shareBar->setCreateEnabled(true);
|
||||
visualDeckStorageWidget->setShareSelectable(true);
|
||||
visualDeckStorageWidget->setShareSelectedFiles(preselectFiles);
|
||||
shareBar->setName(tr("Shared decks"));
|
||||
shareBar->setVisible(true);
|
||||
updateShareHint();
|
||||
shareBar->focusName();
|
||||
}
|
||||
|
||||
void TabDeckStorageVisual::exitShareMode()
|
||||
{
|
||||
// Abandon any in-flight request: otherwise the timer keeps running and a late
|
||||
// response reports the share as created after the user already backed out.
|
||||
shareTimeoutTimer->stop();
|
||||
shareInFlightSeq = 0;
|
||||
visualDeckStorageWidget->setShareSelectable(false);
|
||||
visualDeckStorageWidget->clearShareSelection();
|
||||
shareBar->setVisible(false);
|
||||
}
|
||||
|
||||
void TabDeckStorageVisual::actShareDeck(const QString &filePath)
|
||||
{
|
||||
if (!shareDeckAvailable) {
|
||||
QMessageBox::information(this, tr("Share deck"), tr("You must be connected to the server to share a deck."));
|
||||
return;
|
||||
}
|
||||
enterShareMode({filePath});
|
||||
}
|
||||
|
||||
void TabDeckStorageVisual::onShareSelectionChanged()
|
||||
{
|
||||
if (shareBar->isVisible()) {
|
||||
updateShareHint();
|
||||
}
|
||||
}
|
||||
|
||||
void TabDeckStorageVisual::updateShareHint()
|
||||
{
|
||||
const int count = visualDeckStorageWidget->selectedFilePaths().size();
|
||||
shareBar->setCountText(tr("%n deck(s)", "", count));
|
||||
if (count == 0) {
|
||||
shareBar->setHintText(tr("Click deck tiles to select the decks you want to share."), true);
|
||||
} else if (count == 1) {
|
||||
shareBar->setHintText(tr("One deck selected. Create the link to share it with other players."), true);
|
||||
} else {
|
||||
shareBar->setHintText(tr("%n decks selected. Create the link to share them with other players.", "", count),
|
||||
true);
|
||||
}
|
||||
}
|
||||
|
||||
void TabDeckStorageVisual::actShareSelected()
|
||||
{
|
||||
const QStringList filePaths = visualDeckStorageWidget->selectedFilePaths();
|
||||
if (filePaths.isEmpty()) {
|
||||
QMessageBox::warning(this, tr("Share decks"), tr("Select at least one deck to share."));
|
||||
return;
|
||||
}
|
||||
|
||||
Command_DeckShareCreate cmd;
|
||||
cmd.set_name(shareBar->name().toStdString());
|
||||
if (cmd.name().empty()) {
|
||||
cmd.set_name(tr("Shared decks").toStdString());
|
||||
}
|
||||
|
||||
for (const QString &filePath : filePaths) {
|
||||
std::optional<LoadedDeck> deckOpt =
|
||||
DeckLoader::loadFromFile(filePath, DeckFileFormat::getFormatFromName(filePath), true);
|
||||
if (!deckOpt) {
|
||||
QMessageBox::warning(this, tr("Share decks"), tr("Unable to load deck file %1").arg(filePath));
|
||||
return;
|
||||
}
|
||||
DeckShareItem *item = cmd.add_items();
|
||||
item->set_deck_list(deckOpt->deckList.writeToString_Native().toStdString());
|
||||
item->set_color_identity(getDeckColorIdentity(deckOpt->deckList, CardDatabaseManager::query()).toStdString());
|
||||
}
|
||||
|
||||
shareBar->setCreateEnabled(false);
|
||||
const int seq = ++shareRequestSeq;
|
||||
shareInFlightSeq = seq;
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this,
|
||||
[this, seq](const Response &response, const CommandContainer &commandContainer) {
|
||||
if (shareInFlightSeq != seq) {
|
||||
return; // the user cancelled or a newer request superseded this one
|
||||
}
|
||||
shareInFlightSeq = 0;
|
||||
shareFinished(response, commandContainer);
|
||||
});
|
||||
client->sendCommand(pend);
|
||||
shareTimeoutTimer->start();
|
||||
}
|
||||
|
||||
void TabDeckStorageVisual::shareFinished(const Response &response, const CommandContainer & /*commandContainer*/)
|
||||
{
|
||||
shareTimeoutTimer->stop();
|
||||
shareBar->setCreateEnabled(true);
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
showShareNotice(tr("Failed to create the share link (server response code %1).")
|
||||
.arg(QString::number(static_cast<int>(response.response_code()))),
|
||||
true);
|
||||
return;
|
||||
}
|
||||
|
||||
const DeckShareUtils::ShareResponse share = DeckShareUtils::handleShareResponse(client, response);
|
||||
|
||||
showShareNotice(
|
||||
tr("Share link copied to the clipboard.\nExpires on %1.").arg(DeckShareUtils::formatShareExpiry(share.expiry)));
|
||||
exitShareMode();
|
||||
}
|
||||
|
||||
void TabDeckStorageVisual::handleConnectionChanged(ClientStatus status)
|
||||
{
|
||||
shareDeckAvailable = (status == StatusLoggedIn);
|
||||
visualDeckStorageWidget->setShareAvailable(shareDeckAvailable);
|
||||
if (!shareDeckAvailable && shareBar->isVisible()) {
|
||||
exitShareMode();
|
||||
}
|
||||
}
|
||||
|
||||
void TabDeckStorageVisual::showShareNotice(const QString &message, bool warning)
|
||||
{
|
||||
QMessageBox box(warning ? QMessageBox::Warning : QMessageBox::Information, tr("Share link"), message,
|
||||
QMessageBox::Ok, this);
|
||||
box.exec();
|
||||
}
|
||||
|
||||
void TabDeckStorageVisual::onShareTimeout()
|
||||
{
|
||||
if (shareInFlightSeq == 0) {
|
||||
return; // share mode was left while the request was still outstanding
|
||||
}
|
||||
shareInFlightSeq = 0;
|
||||
shareBar->setCreateEnabled(true);
|
||||
showShareNotice(tr("The server did not respond in time. Try again."), true);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,14 +7,19 @@
|
|||
#ifndef TAB_DECK_STORAGE_VISUAL_H
|
||||
#define TAB_DECK_STORAGE_VISUAL_H
|
||||
|
||||
#include "../../deck_share/share_bar_widget.h"
|
||||
#include "../tab.h"
|
||||
|
||||
#include <QStringList>
|
||||
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
||||
|
||||
struct LoadedDeck;
|
||||
class AbstractClient;
|
||||
class CommandContainer;
|
||||
class DeckPreviewWidget;
|
||||
class QFileSystemModel;
|
||||
class QGroupBox;
|
||||
class QTimer;
|
||||
class QToolBar;
|
||||
class QTreeView;
|
||||
class QTreeWidget;
|
||||
|
|
@ -26,23 +31,55 @@ class TabDeckStorageVisual final : public Tab
|
|||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit TabDeckStorageVisual(TabSupervisor *_tabSupervisor);
|
||||
void retranslateUi() override
|
||||
{
|
||||
}
|
||||
explicit TabDeckStorageVisual(TabSupervisor *_tabSupervisor, AbstractClient *_client);
|
||||
void retranslateUi() override;
|
||||
|
||||
[[nodiscard]] QString getTabText() const override
|
||||
{
|
||||
return tr("Visual Deck Storage");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Enters share-selection mode, optionally preselecting the given deck files.
|
||||
*/
|
||||
void enterShareMode(const QStringList &preselectFiles = {});
|
||||
|
||||
/**
|
||||
* @brief Leaves share-selection mode and clears the selection.
|
||||
*/
|
||||
void exitShareMode();
|
||||
|
||||
[[nodiscard]] bool isShareModeActive() const
|
||||
{
|
||||
return shareBar->isVisible();
|
||||
}
|
||||
|
||||
public slots:
|
||||
void actOpenLocalDeck(const QString &filePath);
|
||||
void actShareDeck(const QString &filePath);
|
||||
|
||||
signals:
|
||||
void openDeckEditor(const LoadedDeck &deck);
|
||||
|
||||
private slots:
|
||||
void actShareSelected();
|
||||
void shareFinished(const Response &response, const CommandContainer &commandContainer);
|
||||
void onShareTimeout();
|
||||
void onShareSelectionChanged();
|
||||
void handleConnectionChanged(ClientStatus status);
|
||||
|
||||
private:
|
||||
void showShareNotice(const QString &message, bool warning = false);
|
||||
void updateShareHint();
|
||||
|
||||
VisualDeckStorageWidget *visualDeckStorageWidget;
|
||||
|
||||
ShareBarWidget *shareBar = nullptr;
|
||||
AbstractClient *client;
|
||||
QTimer *shareTimeoutTimer;
|
||||
int shareRequestSeq = 0;
|
||||
int shareInFlightSeq = 0;
|
||||
bool shareDeckAvailable = false;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "completer_utils.h"
|
||||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "card_completer_styler.h"
|
||||
|
||||
#include <QCompleter>
|
||||
|
|
@ -7,13 +8,26 @@
|
|||
#include <QObject>
|
||||
#include <QRegularExpression>
|
||||
#include <QStringListModel>
|
||||
#include <libcockatrice/card/card_localization.h>
|
||||
#include <libcockatrice/models/database/card/card_completer_proxy_model.h>
|
||||
#include <libcockatrice/models/database/card/card_search_model.h>
|
||||
#include <libcockatrice/models/database/card_database_display_model.h>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
void applyCardSearchLanguage(CardSearchModel *searchModel)
|
||||
{
|
||||
const CardsDisplaySettings &cardsDisplay = SettingsCache::instance().cardsDisplay();
|
||||
searchModel->setSearchLanguage(CardSearchLanguage{
|
||||
cardsDisplay.getCardLang(), static_cast<SearchLanguageMode>(cardsDisplay.getCardSearchLanguage())});
|
||||
}
|
||||
} // namespace
|
||||
|
||||
CardCompleterSetup createCardCompleter(CardDatabaseDisplayModel *displayModel, QObject *parent, int maxVisibleItems)
|
||||
{
|
||||
auto *searchModel = new CardSearchModel(displayModel, parent);
|
||||
applyCardSearchLanguage(searchModel);
|
||||
|
||||
auto *proxyModel = new CardCompleterProxyModel(parent);
|
||||
proxyModel->setSourceModel(searchModel);
|
||||
|
|
@ -27,6 +41,12 @@ CardCompleterSetup createCardCompleter(CardDatabaseDisplayModel *displayModel, Q
|
|||
completer->setMaxVisibleItems(maxVisibleItems);
|
||||
CardCompleterStyler::apply(completer);
|
||||
|
||||
auto *cardsDisplay = &SettingsCache::instance().cardsDisplay();
|
||||
QObject::connect(cardsDisplay, &CardsDisplaySettings::cardLangChanged, searchModel,
|
||||
[searchModel] { applyCardSearchLanguage(searchModel); });
|
||||
QObject::connect(cardsDisplay, &CardsDisplaySettings::cardSearchLanguageChanged, searchModel,
|
||||
[searchModel] { applyCardSearchLanguage(searchModel); });
|
||||
|
||||
return {searchModel, proxyModel, completer};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,17 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent,
|
|||
databaseDisplayModel->setSourceModel(database_model);
|
||||
databaseDisplayModel->setFilterKeyColumn(0);
|
||||
|
||||
const auto applyCardSearchLanguage = [this]() {
|
||||
const CardsDisplaySettings &cardsDisplay = SettingsCache::instance().cardsDisplay();
|
||||
databaseDisplayModel->setSearchLanguage(CardSearchLanguage{
|
||||
cardsDisplay.getCardLang(), static_cast<SearchLanguageMode>(cardsDisplay.getCardSearchLanguage())});
|
||||
};
|
||||
applyCardSearchLanguage();
|
||||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this,
|
||||
applyCardSearchLanguage);
|
||||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardSearchLanguageChanged, this,
|
||||
applyCardSearchLanguage);
|
||||
|
||||
cards = new QList<ExactCard>;
|
||||
connect(databaseDisplayModel, &CardDatabaseDisplayModel::modelDirty, this,
|
||||
&VisualDatabaseDisplayWidget::modelDirty);
|
||||
|
|
@ -58,6 +69,7 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent,
|
|||
SettingsCache::instance().cardsDisplay().getVisualDatabaseDisplayCardSize());
|
||||
connect(cardSizeWidget, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance().cardsDisplay(),
|
||||
&CardsDisplaySettings::setVisualDatabaseDisplayCardSize);
|
||||
cardSizeWidget->enableCtrlScrollResize(flowWidget);
|
||||
|
||||
searchContainer = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAlwaysOff);
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ VisualDeckEditorSampleHandWidget::VisualDeckEditorSampleHandWidget(QWidget *pare
|
|||
upperLayout->addWidget(flowWidget);
|
||||
|
||||
cardSizeWidget = new CardSizeWidget(this, flowWidget);
|
||||
cardSizeWidget->enableCtrlScrollResize(flowWidget);
|
||||
upperLayout->addWidget(cardSizeWidget);
|
||||
|
||||
auto upperLayoutWidget = new QWidget(this);
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QResizeEvent>
|
||||
#include <QTimer>
|
||||
#include <libcockatrice/card/card_info_comparator.h>
|
||||
#include <libcockatrice/card/database/card_database.h>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
|
|
@ -82,13 +83,8 @@ void VisualDeckEditorWidget::initializeSearchBarAndCompleter()
|
|||
searchBar = new QLineEdit(searchContainer);
|
||||
searchContainer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
|
||||
connect(searchBar, &QLineEdit::returnPressed, this, [=, this]() {
|
||||
if (!searchBar->hasFocus()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ExactCard card = CardDatabaseManager::query()->getCard({searchBar->text()});
|
||||
if (card) {
|
||||
emit cardAdditionRequested(card);
|
||||
if (searchBar->hasFocus()) {
|
||||
addCardFromSearch();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -133,16 +129,24 @@ void VisualDeckEditorWidget::initializeSearchBarAndCompleter()
|
|||
// Search button functionality
|
||||
searchPushButton = new CompactPushButton(searchContainer);
|
||||
searchPushButton->setButtonIcon(themePixmap(QStringLiteral("icons/search")));
|
||||
connect(searchPushButton, &QPushButton::clicked, this, [=, this]() {
|
||||
ExactCard card = CardDatabaseManager::query()->getCard({searchBar->text()});
|
||||
if (card) {
|
||||
emit cardAdditionRequested(card);
|
||||
}
|
||||
});
|
||||
connect(searchPushButton, &QPushButton::clicked, this, [this] { addCardFromSearch(); });
|
||||
|
||||
searchLayout->addWidget(searchPushButton);
|
||||
}
|
||||
|
||||
void VisualDeckEditorWidget::addCardFromSearch()
|
||||
{
|
||||
ExactCard card = CardDatabaseManager::query()->getCard({searchBar->text()});
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit cardAdditionRequested(card);
|
||||
|
||||
searchBar->setFocus();
|
||||
QTimer::singleShot(0, searchBar, [this] { searchBar->setSelection(0, searchBar->text().length()); });
|
||||
}
|
||||
|
||||
void VisualDeckEditorWidget::initializeDisplayOptionsWidget()
|
||||
{
|
||||
displayOptionsWidget = new VisualDeckDisplayOptionsWidget(this);
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ signals:
|
|||
|
||||
protected:
|
||||
void initializeSearchBarAndCompleter();
|
||||
void addCardFromSearch();
|
||||
void initializeDisplayOptionsWidget();
|
||||
void initializeDisplayOptionsAndSearchWidget();
|
||||
void initializeScrollAreaAndZoneContainer();
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
#include "deck_preview_color_identity_filter_widget.h"
|
||||
|
||||
#include "../../cards/additional_info/mana_symbol_widget.h"
|
||||
#include "../visual_deck_storage_widget.h"
|
||||
|
||||
#include <QSet>
|
||||
|
||||
DeckPreviewColorIdentityFilterWidget::DeckPreviewColorIdentityFilterWidget(VisualDeckStorageWidget *parent)
|
||||
DeckPreviewColorIdentityFilterWidget::DeckPreviewColorIdentityFilterWidget(QWidget *parent)
|
||||
: QWidget(parent), layout(new QHBoxLayout(this))
|
||||
{
|
||||
setLayout(layout);
|
||||
|
|
|
|||
|
|
@ -14,14 +14,12 @@
|
|||
#include <QSet>
|
||||
#include <QWidget>
|
||||
|
||||
class VisualDeckStorageWidget;
|
||||
|
||||
class DeckPreviewColorIdentityFilterWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit DeckPreviewColorIdentityFilterWidget(VisualDeckStorageWidget *parent);
|
||||
explicit DeckPreviewColorIdentityFilterWidget(QWidget *parent = nullptr);
|
||||
void retranslateUi();
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ QSize DeckPreviewTagDisplayWidget::sizeHint() const
|
|||
|
||||
void DeckPreviewTagDisplayWidget::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
const TagState previousState = state;
|
||||
switch (event->button()) {
|
||||
case Qt::LeftButton:
|
||||
setState(state != TagState::Selected ? TagState::Selected : TagState::NotSelected);
|
||||
|
|
@ -62,7 +63,12 @@ void DeckPreviewTagDisplayWidget::mousePressEvent(QMouseEvent *event)
|
|||
break;
|
||||
}
|
||||
|
||||
emit tagClicked();
|
||||
// Only announce a change when the state was actually toggled, so a click that falls
|
||||
// through the switch (e.g. a button the widget does not react to) does not drive a
|
||||
// full tag-filter update and layout pass for nothing.
|
||||
if (state != previousState) {
|
||||
emit tagClicked();
|
||||
}
|
||||
QWidget::mousePressEvent(event);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,10 @@ public:
|
|||
|
||||
signals:
|
||||
/**
|
||||
* @brief Emitted when the tag is clicked.
|
||||
* @brief Emitted when a click toggles the chip's selection/exclusion state.
|
||||
*
|
||||
* Not emitted for clicks that leave the state unchanged. Connected handlers use
|
||||
* this as the trigger to update filters built from selectedTags()/excludedTags().
|
||||
*/
|
||||
void tagClicked();
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
#include "deck_preview_deck_tags_display_widget.h"
|
||||
|
||||
#include <QFileInfo>
|
||||
#include <QFrame>
|
||||
#include <QInputDialog>
|
||||
#include <QLabel>
|
||||
#include <QMenu>
|
||||
|
|
@ -27,7 +28,7 @@ DeckPreviewWidget::DeckPreviewWidget(QWidget *_parent,
|
|||
VisualDeckStorageWidget *_visualDeckStorageWidget,
|
||||
VisualDeckStorageModel *_model,
|
||||
const QString &_filePath)
|
||||
: QWidget(_parent), visualDeckStorageWidget(_visualDeckStorageWidget), model(_model), filePath(_filePath)
|
||||
: QWidget(_parent), filePath(_filePath), visualDeckStorageWidget(_visualDeckStorageWidget), model(_model)
|
||||
{
|
||||
layout = new QVBoxLayout(this);
|
||||
setLayout(layout);
|
||||
|
|
@ -36,6 +37,8 @@ DeckPreviewWidget::DeckPreviewWidget(QWidget *_parent,
|
|||
new DeckPreviewCardPictureWidget(this, false, visualDeckStorageWidget->deckPreviewSelectionAnimationEnabled);
|
||||
pictureWidget->setFontSize(24);
|
||||
connect(pictureWidget, &DeckPreviewCardPictureWidget::imageClicked, this, &DeckPreviewWidget::imageClickedEvent);
|
||||
connect(pictureWidget, &DeckPreviewCardPictureWidget::imageSingleClicked, this,
|
||||
&DeckPreviewWidget::imageSingleClicked);
|
||||
connect(pictureWidget, &DeckPreviewCardPictureWidget::imageDoubleClicked, this,
|
||||
&DeckPreviewWidget::imageDoubleClickedEvent);
|
||||
bannerCardDisplayWidget = pictureWidget;
|
||||
|
|
@ -99,6 +102,15 @@ DeckPreviewWidget::DeckPreviewWidget(QWidget *_parent,
|
|||
// to keep the resize handler from searching the widget tree on every layout pass.
|
||||
fixedWidthChildren = {bannerCardDisplayWidget, colorIdentityWidget, deckTagsDisplayWidget, bannerCardLabel,
|
||||
bannerCardComboBox};
|
||||
|
||||
// Child of the banner widget so the frame tracks the banner's selection animation
|
||||
// (which animates the banner's position) instead of staying at a stale static offset.
|
||||
selectionFrame = new QFrame(bannerCardDisplayWidget);
|
||||
selectionFrame->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
selectionFrame->setStyleSheet(QStringLiteral(
|
||||
"QFrame { border: 2px solid palette(highlight); border-radius: 4px; background: transparent; }"));
|
||||
selectionFrame->setVisible(false);
|
||||
bannerCardDisplayWidget->installEventFilter(this);
|
||||
}
|
||||
|
||||
void DeckPreviewWidget::retranslateUi()
|
||||
|
|
@ -122,6 +134,63 @@ void DeckPreviewWidget::resizeEvent(QResizeEvent *event)
|
|||
for (QWidget *widget : fixedWidthChildren) {
|
||||
widget->setMaximumWidth(width);
|
||||
}
|
||||
updateSelectionFrameGeometry();
|
||||
}
|
||||
|
||||
bool DeckPreviewWidget::eventFilter(QObject *watched, QEvent *event)
|
||||
{
|
||||
if (watched == bannerCardDisplayWidget && (event->type() == QEvent::Resize || event->type() == QEvent::Move)) {
|
||||
updateSelectionFrameGeometry();
|
||||
}
|
||||
return QWidget::eventFilter(watched, event);
|
||||
}
|
||||
|
||||
void DeckPreviewWidget::setShareSelectable(bool selectable)
|
||||
{
|
||||
shareSelectable = selectable;
|
||||
if (!selectable) {
|
||||
setShareSelected(false);
|
||||
}
|
||||
updateSelectionStyle();
|
||||
}
|
||||
|
||||
void DeckPreviewWidget::setShareSelected(bool selected)
|
||||
{
|
||||
if (shareSelected == selected) {
|
||||
return;
|
||||
}
|
||||
shareSelected = selected;
|
||||
updateSelectionStyle();
|
||||
emit shareSelectionToggled(selected);
|
||||
}
|
||||
|
||||
bool DeckPreviewWidget::isShareSelected() const
|
||||
{
|
||||
return shareSelected;
|
||||
}
|
||||
|
||||
bool DeckPreviewWidget::isShareSelectable() const
|
||||
{
|
||||
return shareSelectable;
|
||||
}
|
||||
|
||||
void DeckPreviewWidget::updateSelectionFrameGeometry()
|
||||
{
|
||||
if (selectionFrame == nullptr || bannerCardDisplayWidget == nullptr) {
|
||||
return;
|
||||
}
|
||||
// Frame is a child of the banner, so it is positioned in banner coordinates and
|
||||
// tracks the banner's selection animation automatically. A small inset keeps the
|
||||
// highlight visible around the card art without occluding it.
|
||||
selectionFrame->setGeometry(bannerCardDisplayWidget->rect().adjusted(1, 1, -1, -1));
|
||||
selectionFrame->raise();
|
||||
}
|
||||
|
||||
void DeckPreviewWidget::updateSelectionStyle()
|
||||
{
|
||||
if (selectionFrame != nullptr) {
|
||||
selectionFrame->setVisible(shareSelectable && isShareSelected());
|
||||
}
|
||||
}
|
||||
|
||||
void DeckPreviewWidget::enterEvent(QEnterEvent *event)
|
||||
|
|
@ -337,10 +406,20 @@ void DeckPreviewWidget::imageClickedEvent(QMouseEvent *event, DeckPreviewCardPic
|
|||
}
|
||||
}
|
||||
|
||||
void DeckPreviewWidget::imageSingleClicked()
|
||||
{
|
||||
if (isShareSelectable()) {
|
||||
setShareSelected(!isShareSelected());
|
||||
}
|
||||
}
|
||||
|
||||
void DeckPreviewWidget::imageDoubleClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance)
|
||||
{
|
||||
Q_UNUSED(event);
|
||||
Q_UNUSED(instance);
|
||||
if (isShareSelectable()) {
|
||||
return; // in share mode a double click would just toggle a single selection
|
||||
}
|
||||
emit deckLoadRequested(filePath);
|
||||
}
|
||||
|
||||
|
|
@ -365,6 +444,9 @@ QMenu *DeckPreviewWidget::createRightClickMenu()
|
|||
}
|
||||
});
|
||||
|
||||
connect(menu->addAction(tr("Share deck...")), &QAction::triggered, this,
|
||||
[this] { emit shareDeckRequested(filePath); });
|
||||
|
||||
connect(menu->addAction(tr("Edit Tags")), &QAction::triggered, deckTagsDisplayWidget,
|
||||
&DeckPreviewDeckTagsDisplayWidget::openTagEditDlg);
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
#include <QWidget>
|
||||
|
||||
class QEnterEvent;
|
||||
class QFrame;
|
||||
class QLabel;
|
||||
class QMenu;
|
||||
class QMouseEvent;
|
||||
|
|
@ -41,9 +42,19 @@ public:
|
|||
*/
|
||||
DeckPreviewCardPictureWidget *bannerCardDisplayWidget;
|
||||
|
||||
/** @brief The path of the deck file backing this preview. */
|
||||
QString filePath;
|
||||
|
||||
void setShareSelectable(bool selectable);
|
||||
void setShareSelected(bool selected);
|
||||
[[nodiscard]] bool isShareSelected() const;
|
||||
[[nodiscard]] bool isShareSelectable() const;
|
||||
|
||||
signals:
|
||||
void deckLoadRequested(const QString &filePath);
|
||||
void openDeckEditor(const LoadedDeck &deck);
|
||||
void shareDeckRequested(const QString &filePath);
|
||||
void shareSelectionToggled(bool selected);
|
||||
|
||||
public slots:
|
||||
/**
|
||||
|
|
@ -66,6 +77,7 @@ public slots:
|
|||
protected:
|
||||
void enterEvent(QEnterEvent *event) override;
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||
|
||||
private:
|
||||
[[nodiscard]] int row() const;
|
||||
|
|
@ -76,6 +88,7 @@ private:
|
|||
QMenu *createRightClickMenu();
|
||||
void addSetBannerCardMenu(QMenu *menu);
|
||||
void imageClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
|
||||
void imageSingleClicked();
|
||||
void imageDoubleClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
|
||||
|
||||
void actRenameDeck();
|
||||
|
|
@ -84,7 +97,6 @@ private:
|
|||
|
||||
VisualDeckStorageWidget *visualDeckStorageWidget;
|
||||
VisualDeckStorageModel *model;
|
||||
QString filePath;
|
||||
QVBoxLayout *layout;
|
||||
ColorIdentityWidget *colorIdentityWidget;
|
||||
DeckPreviewDeckTagsDisplayWidget *deckTagsDisplayWidget;
|
||||
|
|
@ -92,6 +104,12 @@ private:
|
|||
QComboBox *bannerCardComboBox;
|
||||
QList<QWidget *> fixedWidthChildren; ///< Children clamped to the picture width on resize.
|
||||
int lastKnownBannerWidth = -1; ///< The picture width last applied to the children.
|
||||
QFrame *selectionFrame = nullptr;
|
||||
bool shareSelectable = false;
|
||||
bool shareSelected = false;
|
||||
|
||||
void updateSelectionStyle();
|
||||
void updateSelectionFrameGeometry();
|
||||
};
|
||||
|
||||
class NoScrollFilter : public QObject
|
||||
|
|
|
|||
|
|
@ -0,0 +1,167 @@
|
|||
#include "public_deck_preview_widget.h"
|
||||
|
||||
#include "../../../../client/settings/cache_settings.h"
|
||||
#include "../../cards/additional_info/color_identity_widget.h"
|
||||
#include "../../cards/deck_preview_card_picture_widget.h"
|
||||
#include "../../general/layout_containers/flow_widget.h"
|
||||
#include "deck_preview_tag_display_widget.h"
|
||||
|
||||
#include <QKeyEvent>
|
||||
#include <QLabel>
|
||||
#include <QMouseEvent>
|
||||
#include <QResizeEvent>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/settings/visual_deck_storage_settings.h>
|
||||
|
||||
PublicDeckPreviewWidget::PublicDeckPreviewWidget(QWidget *parent, const RemotePublicDecksModel::DeckEntry &entry)
|
||||
: QWidget(parent)
|
||||
{
|
||||
bannerCardDisplayWidget = new DeckPreviewCardPictureWidget(this);
|
||||
bannerCardDisplayWidget->setFontSize(24);
|
||||
|
||||
// The whole tile is a single focusable, keyboard-operable control: Tab lands
|
||||
// on it and Space/Enter opens the deck, mirroring the shared-deck preview tile.
|
||||
setFocusPolicy(Qt::StrongFocus);
|
||||
|
||||
uploadTimeLabel = new QLabel(this);
|
||||
uploadTimeLabel->setAlignment(Qt::AlignHCenter);
|
||||
|
||||
colorIdentityWidget = new ColorIdentityWidget(this);
|
||||
|
||||
tagsFlowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded);
|
||||
tagsFlowWidget->setSpacing(3, 3);
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->addWidget(bannerCardDisplayWidget);
|
||||
layout->addWidget(uploadTimeLabel);
|
||||
layout->addWidget(colorIdentityWidget);
|
||||
layout->addWidget(tagsFlowWidget);
|
||||
setLayout(layout);
|
||||
|
||||
connect(&SettingsCache::instance().visualDeckStorage(),
|
||||
&VisualDeckStorageSettings::visualDeckStorageShowColorIdentityChanged, this,
|
||||
&PublicDeckPreviewWidget::updateColorIdentityVisibility);
|
||||
connect(&SettingsCache::instance().visualDeckStorage(),
|
||||
&VisualDeckStorageSettings::visualDeckStorageShowTagsOnDeckPreviewsChanged, this,
|
||||
&PublicDeckPreviewWidget::updateTagsVisibility);
|
||||
connect(&SettingsCache::instance().visualDeckStorage(),
|
||||
&VisualDeckStorageSettings::visualDeckStorageShowUploadTimeChanged, this,
|
||||
&PublicDeckPreviewWidget::updateUploadTimeVisibility);
|
||||
|
||||
setEntry(entry);
|
||||
|
||||
connect(bannerCardDisplayWidget, &DeckPreviewCardPictureWidget::imageClicked, this,
|
||||
&PublicDeckPreviewWidget::imageClickedEvent);
|
||||
connect(bannerCardDisplayWidget, &DeckPreviewCardPictureWidget::imageDoubleClicked, this,
|
||||
&PublicDeckPreviewWidget::imageDoubleClickedEvent);
|
||||
|
||||
// resizeEvent clamps every child to the banner picture's width, so collect them
|
||||
// once here to keep the resize handler from searching the widget tree on every pass.
|
||||
fixedWidthChildren = {bannerCardDisplayWidget, uploadTimeLabel, colorIdentityWidget, tagsFlowWidget};
|
||||
}
|
||||
|
||||
void PublicDeckPreviewWidget::resizeEvent(QResizeEvent *event)
|
||||
{
|
||||
QWidget::resizeEvent(event);
|
||||
if (bannerCardDisplayWidget == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int width = bannerCardDisplayWidget->width();
|
||||
if (width == lastKnownBannerWidth) {
|
||||
return;
|
||||
}
|
||||
lastKnownBannerWidth = width;
|
||||
|
||||
for (QWidget *widget : fixedWidthChildren) {
|
||||
widget->setMaximumWidth(width);
|
||||
}
|
||||
}
|
||||
|
||||
void PublicDeckPreviewWidget::setEntry(const RemotePublicDecksModel::DeckEntry &entry)
|
||||
{
|
||||
deckId = entry.id;
|
||||
|
||||
hasColorIdentity = !entry.colorIdentity.isEmpty();
|
||||
colorIdentityWidget->setColorIdentity(entry.colorIdentity);
|
||||
updateColorIdentityVisibility();
|
||||
|
||||
const ExactCard bannerCard =
|
||||
entry.bannerCardName.isEmpty()
|
||||
? ExactCard()
|
||||
: CardDatabaseManager::query()->getCard(CardRef{entry.bannerCardName, entry.bannerCardProvider});
|
||||
bannerCardDisplayWidget->setCard(bannerCard);
|
||||
|
||||
// The deck name is the overlay text on the banner, like the local preview.
|
||||
bannerCardDisplayWidget->setOverlayText(entry.name);
|
||||
// The deck name comes from another user's record, and Qt tooltips are
|
||||
// rendered as AutoText, so escape and bound it to keep it readable text
|
||||
// (the overlay painted onto the banner is already a plain painter draw).
|
||||
setToolTip(entry.name.left(200).toHtmlEscaped());
|
||||
setBaseAccessibleName(entry.name);
|
||||
|
||||
tagsFlowWidget->clearLayout();
|
||||
for (const QString &tag : entry.tags) {
|
||||
auto *chip = new DeckPreviewTagDisplayWidget(tagsFlowWidget, tag);
|
||||
chip->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
tagsFlowWidget->addWidget(chip);
|
||||
}
|
||||
hasTags = !entry.tags.isEmpty();
|
||||
updateTagsVisibility();
|
||||
|
||||
uploadTimeLabel->setText(tr("Uploaded %1").arg(entry.uploadTime.toString(Qt::TextDate)));
|
||||
hasUploadTime = !entry.uploadTime.isNull();
|
||||
updateUploadTimeVisibility();
|
||||
}
|
||||
|
||||
void PublicDeckPreviewWidget::updateColorIdentityVisibility()
|
||||
{
|
||||
colorIdentityWidget->setVisible(
|
||||
hasColorIdentity && SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowColorIdentity());
|
||||
}
|
||||
|
||||
void PublicDeckPreviewWidget::updateTagsVisibility()
|
||||
{
|
||||
tagsFlowWidget->setVisible(
|
||||
hasTags && SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowTagsOnDeckPreviews());
|
||||
}
|
||||
|
||||
void PublicDeckPreviewWidget::updateUploadTimeVisibility()
|
||||
{
|
||||
uploadTimeLabel->setVisible(hasUploadTime &&
|
||||
SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowUploadTime());
|
||||
}
|
||||
|
||||
void PublicDeckPreviewWidget::keyPressEvent(QKeyEvent *event)
|
||||
{
|
||||
if (event->key() == Qt::Key_Space || event->key() == Qt::Key_Enter || event->key() == Qt::Key_Return) {
|
||||
event->accept();
|
||||
emit openDeckRequested(deckId);
|
||||
return;
|
||||
}
|
||||
QWidget::keyPressEvent(event);
|
||||
}
|
||||
|
||||
void PublicDeckPreviewWidget::setBaseAccessibleName(const QString &name)
|
||||
{
|
||||
baseAccessibleName = name;
|
||||
setAccessibleName(name);
|
||||
}
|
||||
|
||||
void PublicDeckPreviewWidget::setScaleFactor(int scale)
|
||||
{
|
||||
bannerCardDisplayWidget->setScaleFactor(scale);
|
||||
}
|
||||
|
||||
void PublicDeckPreviewWidget::imageClickedEvent(QMouseEvent * /*event*/, DeckPreviewCardPictureWidget * /*instance*/)
|
||||
{
|
||||
// Reserved: clicking could show a card popup for the banner card.
|
||||
}
|
||||
|
||||
void PublicDeckPreviewWidget::imageDoubleClickedEvent(QMouseEvent * /*event*/,
|
||||
DeckPreviewCardPictureWidget * /*instance*/)
|
||||
{
|
||||
emit openDeckRequested(deckId);
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
/**
|
||||
* @file public_deck_preview_widget.h
|
||||
* @ingroup VisualDeckPreviewWidgets
|
||||
*/
|
||||
|
||||
#ifndef PUBLIC_DECK_PREVIEW_WIDGET_H
|
||||
#define PUBLIC_DECK_PREVIEW_WIDGET_H
|
||||
|
||||
#include "../remote_public_decks_model.h"
|
||||
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QWidget>
|
||||
|
||||
class ColorIdentityWidget;
|
||||
class DeckPreviewCardPictureWidget;
|
||||
class FlowWidget;
|
||||
class QKeyEvent;
|
||||
class QLabel;
|
||||
class QMouseEvent;
|
||||
class QResizeEvent;
|
||||
|
||||
/**
|
||||
* @brief A preview tile for a public deck published by another user.
|
||||
*
|
||||
* Renders the banner card picture (looked up by name/provider in the card
|
||||
* database) with the deck name overlaid, the color identity, the deck's tags
|
||||
* (read-only) and its upload time, all from the metadata the server stores for
|
||||
* the deck, so no deck list is downloaded until the user actually opens the
|
||||
* deck. Double-clicking the banner requests opening it.
|
||||
*/
|
||||
class PublicDeckPreviewWidget final : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit PublicDeckPreviewWidget(QWidget *parent, const RemotePublicDecksModel::DeckEntry &entry);
|
||||
|
||||
void setEntry(const RemotePublicDecksModel::DeckEntry &entry);
|
||||
|
||||
/** @brief Sets the accessible name announced to assistive technologies. */
|
||||
void setBaseAccessibleName(const QString &name);
|
||||
|
||||
/** @brief Scales the banner card picture, mirroring the Visual Deck Storage. */
|
||||
void setScaleFactor(int scale);
|
||||
|
||||
signals:
|
||||
void openDeckRequested(int deckId);
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
void keyPressEvent(QKeyEvent *event) override;
|
||||
|
||||
private slots:
|
||||
void imageClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
|
||||
void imageDoubleClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
|
||||
void updateColorIdentityVisibility();
|
||||
void updateTagsVisibility();
|
||||
void updateUploadTimeVisibility();
|
||||
|
||||
private:
|
||||
int deckId = 0;
|
||||
QString baseAccessibleName;
|
||||
bool hasColorIdentity = false;
|
||||
bool hasTags = false;
|
||||
bool hasUploadTime = false;
|
||||
int lastKnownBannerWidth = 0;
|
||||
QList<QWidget *> fixedWidthChildren;
|
||||
DeckPreviewCardPictureWidget *bannerCardDisplayWidget;
|
||||
ColorIdentityWidget *colorIdentityWidget;
|
||||
FlowWidget *tagsFlowWidget;
|
||||
QLabel *uploadTimeLabel;
|
||||
};
|
||||
|
||||
#endif // PUBLIC_DECK_PREVIEW_WIDGET_H
|
||||
|
|
@ -0,0 +1,220 @@
|
|||
#include "remote_public_decks_model.h"
|
||||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
|
||||
#include <QTimer>
|
||||
#include <algorithm>
|
||||
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
||||
#include <libcockatrice/protocol/pb/command_deck_list_other_user.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_deck_list.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_deckstorage.pb.h>
|
||||
#include <libcockatrice/protocol/pending_command.h>
|
||||
#include <libcockatrice/settings/network_settings.h>
|
||||
|
||||
RemotePublicDecksModel::RemotePublicDecksModel(AbstractClient *_client, QObject *parent)
|
||||
: QAbstractListModel(parent), client(_client)
|
||||
{
|
||||
// The ping sweep can drop a pending command without ever emitting finished,
|
||||
// so loading must not be a latch: time it out and clear it when the client
|
||||
// goes away, or the tab is stuck on the loading state for the session.
|
||||
loadingTimeoutTimer = new QTimer(this);
|
||||
loadingTimeoutTimer->setSingleShot(true);
|
||||
loadingTimeoutTimer->setInterval(
|
||||
static_cast<int>((static_cast<qint64>(SettingsCache::instance().network().getTimeOut()) + 1) *
|
||||
SettingsCache::instance().network().getKeepAlive() * 1000));
|
||||
connect(loadingTimeoutTimer, &QTimer::timeout, this, &RemotePublicDecksModel::onLoadingTimeout);
|
||||
connect(client, &AbstractClient::statusChanged, this, [this](ClientStatus status) {
|
||||
if (status == StatusDisconnected) {
|
||||
loadingTimeoutTimer->stop();
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
int RemotePublicDecksModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
return parent.isValid() ? 0 : visibleIndices.size();
|
||||
}
|
||||
|
||||
QVariant RemotePublicDecksModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid() || index.row() < 0 || index.row() >= visibleIndices.size()) {
|
||||
return QVariant();
|
||||
}
|
||||
if (role == Qt::DisplayRole || role == Qt::ToolTipRole) {
|
||||
return decks.at(visibleIndices.at(index.row())).name;
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
RemotePublicDecksModel::DeckEntry RemotePublicDecksModel::entryAt(int row) const
|
||||
{
|
||||
if (row < 0 || row >= visibleIndices.size()) {
|
||||
return DeckEntry{};
|
||||
}
|
||||
return decks.at(visibleIndices.at(row));
|
||||
}
|
||||
|
||||
void RemotePublicDecksModel::setSearchText(const QString &text)
|
||||
{
|
||||
searchText = text.trimmed();
|
||||
rebuildVisibleIndices();
|
||||
}
|
||||
|
||||
void RemotePublicDecksModel::setColorFilter(VisualDeckStorageSortFilterProxyModel::FilterMode mode,
|
||||
const QSet<QChar> &colors)
|
||||
{
|
||||
colorFilterMode = mode;
|
||||
activeColors = colors;
|
||||
rebuildVisibleIndices();
|
||||
}
|
||||
|
||||
void RemotePublicDecksModel::setTagFilter(const QSet<QString> &selected, const QSet<QString> &excluded)
|
||||
{
|
||||
includedTags = selected;
|
||||
excludedTags = excluded;
|
||||
rebuildVisibleIndices();
|
||||
}
|
||||
|
||||
QSet<QString> RemotePublicDecksModel::allTags() const
|
||||
{
|
||||
QSet<QString> all;
|
||||
for (const DeckEntry &entry : decks) {
|
||||
all.unite(QSet<QString>(entry.tags.cbegin(), entry.tags.cend()));
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
void RemotePublicDecksModel::rebuildVisibleIndices()
|
||||
{
|
||||
QList<int> newIndices;
|
||||
newIndices.reserve(decks.size());
|
||||
for (int row = 0; row < decks.size(); ++row) {
|
||||
const DeckEntry &entry = decks.at(row);
|
||||
|
||||
if (!searchText.isEmpty() && !entry.name.contains(searchText, Qt::CaseInsensitive)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!activeColors.isEmpty()) {
|
||||
const QString &identity = entry.colorIdentity;
|
||||
if (!colorIdentityMatches(colorFilterMode, activeColors, identity)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!includedTags.isEmpty()) {
|
||||
const QSet<QString> entryTags(entry.tags.cbegin(), entry.tags.cend());
|
||||
bool hasAll = std::all_of(includedTags.begin(), includedTags.end(),
|
||||
[&entryTags](const QString &tag) { return entryTags.contains(tag); });
|
||||
if (!hasAll) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!excludedTags.isEmpty() && std::any_of(excludedTags.begin(), excludedTags.end(),
|
||||
[&entry](const QString &tag) { return entry.tags.contains(tag); })) {
|
||||
continue;
|
||||
}
|
||||
|
||||
newIndices.append(row);
|
||||
}
|
||||
|
||||
beginResetModel();
|
||||
visibleIndices = newIndices;
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
void RemotePublicDecksModel::refresh(const QString &userName)
|
||||
{
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
// Every refresh captures its own request id so a reply that lands after its
|
||||
// loading timeout (the reverse of the ping sweep dropping the command) is
|
||||
// recognised as stale: it must not stop the newer request's timer or paint
|
||||
// the grid with out-of-date data.
|
||||
const int seq = ++requestSequence;
|
||||
setLoading(true);
|
||||
loadingTimeoutTimer->start();
|
||||
Command_DeckListOtherUser cmd;
|
||||
cmd.set_user_name(userName.toStdString());
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this,
|
||||
[this, seq](const Response &response, const CommandContainer &commandContainer) {
|
||||
if (seq != requestSequence) {
|
||||
return; // a newer refresh superseded this one
|
||||
}
|
||||
decksReceived(response, commandContainer);
|
||||
});
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void RemotePublicDecksModel::onLoadingTimeout()
|
||||
{
|
||||
setLoading(false);
|
||||
emit loadFailed(tr("The server did not respond in time. Try again."));
|
||||
}
|
||||
|
||||
void RemotePublicDecksModel::clear()
|
||||
{
|
||||
decks.clear();
|
||||
rebuildVisibleIndices();
|
||||
}
|
||||
|
||||
void RemotePublicDecksModel::setLoading(bool value)
|
||||
{
|
||||
if (loading == value) {
|
||||
return;
|
||||
}
|
||||
loading = value;
|
||||
emit loadingChanged(loading);
|
||||
}
|
||||
|
||||
void RemotePublicDecksModel::decksReceived(const Response &response, const CommandContainer & /*commandContainer*/)
|
||||
{
|
||||
setLoading(false);
|
||||
loadingTimeoutTimer->stop();
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
emit loadFailed(tr("Failed to load the user's public decks (server response code %1).")
|
||||
.arg(QString::number(static_cast<int>(response.response_code()))));
|
||||
return;
|
||||
}
|
||||
|
||||
const Response_DeckList &resp = response.GetExtension(Response_DeckList::ext);
|
||||
decks.clear();
|
||||
addFolder(resp.root());
|
||||
rebuildVisibleIndices();
|
||||
}
|
||||
|
||||
void RemotePublicDecksModel::addFolder(const ServerInfo_DeckStorage_Folder &folder)
|
||||
{
|
||||
const int itemCount = folder.items_size();
|
||||
for (int i = 0; i < itemCount; ++i) {
|
||||
addTreeItem(folder.items(i));
|
||||
}
|
||||
}
|
||||
|
||||
void RemotePublicDecksModel::addTreeItem(const ServerInfo_DeckStorage_TreeItem &item)
|
||||
{
|
||||
if (item.has_folder()) {
|
||||
addFolder(item.folder());
|
||||
return;
|
||||
}
|
||||
|
||||
const ServerInfo_DeckStorage_File &file = item.file();
|
||||
DeckEntry entry;
|
||||
entry.id = item.id();
|
||||
entry.name = QString::fromStdString(item.name());
|
||||
entry.uploadTime = QDateTime::fromSecsSinceEpoch(file.creation_time());
|
||||
entry.bannerCardName = QString::fromStdString(file.banner_card_name());
|
||||
entry.bannerCardProvider = QString::fromStdString(file.banner_card_provider());
|
||||
entry.colorIdentity = QString::fromStdString(file.color_identity());
|
||||
QStringList tags;
|
||||
for (const auto &tag : file.tags()) {
|
||||
tags.append(QString::fromStdString(tag));
|
||||
}
|
||||
entry.tags = tags;
|
||||
decks.append(entry);
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
/**
|
||||
* @file remote_public_decks_model.h
|
||||
* @ingroup DeckStorageWidgets
|
||||
*/
|
||||
|
||||
#ifndef REMOTE_PUBLIC_DECKS_MODEL_H
|
||||
#define REMOTE_PUBLIC_DECKS_MODEL_H
|
||||
|
||||
#include "visual_deck_storage_sort_filter_proxy_model.h"
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QDateTime>
|
||||
#include <QList>
|
||||
#include <QSet>
|
||||
#include <QStringList>
|
||||
|
||||
class AbstractClient;
|
||||
class CommandContainer;
|
||||
class QTimer;
|
||||
class Response;
|
||||
class ServerInfo_DeckStorage_Folder;
|
||||
class ServerInfo_DeckStorage_TreeItem;
|
||||
|
||||
/**
|
||||
* @brief Flat, read-only list of the public decks published by another user.
|
||||
*
|
||||
* Fetches the target user's public decks via Command_DeckListOtherUser and
|
||||
* flattens the response tree into entries carrying the preview metadata stored
|
||||
* on the server (banner card name/provider and color identity). No deck list is
|
||||
* downloaded until the user actually opens a deck.
|
||||
*
|
||||
* Name and color-identity filtering is applied against this metadata, mirroring
|
||||
* the Visual Deck Storage's filter semantics, so the grid can be narrowed like
|
||||
* the local deck storage.
|
||||
*/
|
||||
class RemotePublicDecksModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
struct DeckEntry
|
||||
{
|
||||
int id = 0;
|
||||
QString name;
|
||||
QDateTime uploadTime;
|
||||
QString bannerCardName;
|
||||
QString bannerCardProvider;
|
||||
QString colorIdentity;
|
||||
QStringList tags;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief The color identity filter mode, shared with the Visual Deck Storage.
|
||||
*/
|
||||
using FilterMode = VisualDeckStorageSortFilterProxyModel::FilterMode;
|
||||
|
||||
explicit RemotePublicDecksModel(AbstractClient *client, QObject *parent = nullptr);
|
||||
|
||||
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
/** @brief Fetches the public decks of another user, replacing the current contents. */
|
||||
void refresh(const QString &userName);
|
||||
void clear();
|
||||
|
||||
/** @brief Sets a case-insensitive substring filter on the deck name. */
|
||||
void setSearchText(const QString &text);
|
||||
|
||||
/** @brief Sets the active color identity filter and mode. */
|
||||
void setColorFilter(FilterMode mode, const QSet<QChar> &colors);
|
||||
|
||||
/** @brief Filters decks by required (`selected`) and forbidden (`excluded`) tags. */
|
||||
void setTagFilter(const QSet<QString> &selected, const QSet<QString> &excluded);
|
||||
|
||||
/** @brief All tags present across all loaded decks, for building filter chips. */
|
||||
[[nodiscard]] QSet<QString> allTags() const;
|
||||
|
||||
/** @brief The number of decks after filtering. */
|
||||
[[nodiscard]] int filteredCount() const
|
||||
{
|
||||
return visibleIndices.size();
|
||||
}
|
||||
|
||||
/** @brief The number of decks before filtering. */
|
||||
[[nodiscard]] int totalCount() const
|
||||
{
|
||||
return decks.size();
|
||||
}
|
||||
|
||||
/** @brief True while a refresh request is in flight and the grid has no data yet. */
|
||||
[[nodiscard]] bool isLoading() const
|
||||
{
|
||||
return loading;
|
||||
}
|
||||
|
||||
[[nodiscard]] DeckEntry entryAt(int row) const;
|
||||
|
||||
signals:
|
||||
/** @brief Emitted when a refresh starts, completes, or fails (see loading()). */
|
||||
void loadingChanged(bool loading);
|
||||
|
||||
/** @brief Emitted when the last refresh failed; contains a user-facing message. */
|
||||
void loadFailed(const QString &message);
|
||||
|
||||
private slots:
|
||||
void decksReceived(const Response &response, const CommandContainer &commandContainer);
|
||||
void onLoadingTimeout();
|
||||
|
||||
private:
|
||||
void addFolder(const ServerInfo_DeckStorage_Folder &folder);
|
||||
void addTreeItem(const ServerInfo_DeckStorage_TreeItem &item);
|
||||
void rebuildVisibleIndices();
|
||||
void setLoading(bool value);
|
||||
|
||||
AbstractClient *client;
|
||||
QTimer *loadingTimeoutTimer;
|
||||
QList<DeckEntry> decks;
|
||||
QList<int> visibleIndices; ///< Row indices into `decks` that pass the current filters.
|
||||
bool loading = false;
|
||||
int requestSequence = 0; ///< Monotonically increases per refresh; only the newest request may update the grid.
|
||||
|
||||
QString searchText;
|
||||
VisualDeckStorageSortFilterProxyModel::FilterMode colorFilterMode = VisualDeckStorageSortFilterProxyModel::Includes;
|
||||
QSet<QChar> activeColors;
|
||||
QSet<QString> includedTags;
|
||||
QSet<QString> excludedTags;
|
||||
};
|
||||
|
||||
#endif // REMOTE_PUBLIC_DECKS_MODEL_H
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
#include "visual_deck_storage_folder_display_widget.h"
|
||||
|
||||
#include "../cards/card_info_picture_widget.h"
|
||||
#include "../cards/card_size_widget.h"
|
||||
#include "../general/display/banner_widget.h"
|
||||
#include "../general/layout_containers/flow_widget.h"
|
||||
#include "deck_preview/deck_preview_widget.h"
|
||||
|
|
@ -45,6 +46,8 @@ VisualDeckStorageFolderDisplayWidget::VisualDeckStorageFolderDisplayWidget(
|
|||
flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAlwaysOff);
|
||||
containerLayout->addWidget(flowWidget);
|
||||
|
||||
visualDeckStorageWidget->settings()->getCardSizeWidget()->enableCtrlScrollResize(this);
|
||||
|
||||
auto *proxy = visualDeckStorageWidget->proxyModel();
|
||||
// A burst of proxy changes (one dataChanged per finished deck load, plus the filter
|
||||
// invalidations) coalesces into a single reconcile, so a scan of many decks doesn't
|
||||
|
|
@ -126,6 +129,11 @@ void VisualDeckStorageFolderDisplayWidget::continueDeckPass()
|
|||
|
||||
const bool matches = index.data(VisualDeckStorageRoles::FilterMatchRole).toBool();
|
||||
deckPreviewWidget->setVisible(matches);
|
||||
if (!matches) {
|
||||
// A deck that no longer matches the filters is dropped from the selection so its
|
||||
// highlight cannot linger on an invisible preview or be counted in the share.
|
||||
deckPreviewWidget->setShareSelected(false);
|
||||
}
|
||||
if (matches) {
|
||||
++visibleDeckCount;
|
||||
}
|
||||
|
|
@ -209,6 +217,11 @@ DeckPreviewWidget *VisualDeckStorageFolderDisplayWidget::createDeckPreviewWidget
|
|||
&VisualDeckStorageWidget::deckLoadRequested);
|
||||
connect(deckPreviewWidget, &DeckPreviewWidget::openDeckEditor, visualDeckStorageWidget,
|
||||
&VisualDeckStorageWidget::openDeckEditor);
|
||||
connect(deckPreviewWidget, &DeckPreviewWidget::shareDeckRequested, visualDeckStorageWidget,
|
||||
&VisualDeckStorageWidget::shareDeckRequested);
|
||||
connect(deckPreviewWidget, &DeckPreviewWidget::shareSelectionToggled, visualDeckStorageWidget,
|
||||
&VisualDeckStorageWidget::shareSelectionChanged);
|
||||
deckPreviewWidget->setShareSelectable(visualDeckStorageWidget->isShareSelectable());
|
||||
connect(visualDeckStorageWidget->settings(), &VisualDeckStorageQuickSettingsWidget::cardSizeChanged,
|
||||
deckPreviewWidget->bannerCardDisplayWidget, &CardInfoPictureWidget::setScaleFactor);
|
||||
deckPreviewWidget->bannerCardDisplayWidget->setScaleFactor(visualDeckStorageWidget->settings()->getCardSize());
|
||||
|
|
@ -216,6 +229,18 @@ DeckPreviewWidget *VisualDeckStorageFolderDisplayWidget::createDeckPreviewWidget
|
|||
return deckPreviewWidget;
|
||||
}
|
||||
|
||||
void VisualDeckStorageFolderDisplayWidget::setShareSelectable(bool selectable)
|
||||
{
|
||||
const auto previews = flowWidget->findChildren<DeckPreviewWidget *>();
|
||||
for (DeckPreviewWidget *preview : previews) {
|
||||
preview->setShareSelectable(selectable);
|
||||
}
|
||||
const auto subFolders = findChildren<VisualDeckStorageFolderDisplayWidget *>();
|
||||
for (VisualDeckStorageFolderDisplayWidget *subFolder : subFolders) {
|
||||
subFolder->setShareSelectable(selectable);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Creates, removes and keeps in sync the subfolder widgets of this folder.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ public slots:
|
|||
*/
|
||||
void scheduleReconcile();
|
||||
void updateShowFolders(bool enabled);
|
||||
void setShareSelectable(bool selectable);
|
||||
|
||||
signals:
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "visual_deck_storage_model.h"
|
||||
|
||||
#include "../../deck_loader/deck_loader.h"
|
||||
#include "../cards/additional_info/deck_color_identity.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QDirIterator>
|
||||
|
|
@ -119,8 +120,6 @@ DeckScanResult scanDeckDirectory(const QString &deckPath)
|
|||
}
|
||||
} // namespace
|
||||
|
||||
static QString computeColorIdentity(const LoadedDeck &deck);
|
||||
|
||||
VisualDeckStorageModel::VisualDeckStorageModel(QObject *parent) : QAbstractListModel(parent)
|
||||
{
|
||||
}
|
||||
|
|
@ -306,7 +305,7 @@ void VisualDeckStorageModel::beginLoad(int row)
|
|||
}
|
||||
// Color identity walks every card through the database, so compute it here to
|
||||
// keep the completion handler on the UI thread cheap.
|
||||
const QString colorIdentity = computeColorIdentity(*deck);
|
||||
const QString colorIdentity = getDeckColorIdentity(deck->deckList, CardDatabaseManager::query());
|
||||
return DeckLoadResult{std::move(*deck), QFileInfo(filePath).lastModified(), colorIdentity};
|
||||
}));
|
||||
}
|
||||
|
|
@ -367,40 +366,6 @@ void VisualDeckStorageModel::drainPendingLoads()
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Computes the color identity of a deck in WUBRG order.
|
||||
*/
|
||||
static QString computeColorIdentity(const LoadedDeck &deck)
|
||||
{
|
||||
QStringList cardList = deck.deckList.getCardList({DECK_ZONE_MAIN, DECK_ZONE_SIDE});
|
||||
if (cardList.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QSet<QChar> colorSet; // A set to collect unique color symbols (e.g., W, U, B, R, G)
|
||||
|
||||
for (const QString &cardName : cardList) {
|
||||
CardInfoPtr currentCard = CardDatabaseManager::query()->getCardInfo(cardName);
|
||||
if (currentCard) {
|
||||
const QString colors = currentCard->getColors(); // Something like "WUB"
|
||||
for (const QChar &color : colors) {
|
||||
colorSet.insert(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the color identity is in WUBRG order
|
||||
QString colorIdentity;
|
||||
const QString wubrgOrder = "WUBRG";
|
||||
for (const QChar &color : wubrgOrder) {
|
||||
if (colorSet.contains(color)) {
|
||||
colorIdentity.append(color);
|
||||
}
|
||||
}
|
||||
|
||||
return colorIdentity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Recomputes all derived metadata of a row from its loaded deck.
|
||||
*/
|
||||
|
|
@ -414,7 +379,7 @@ void VisualDeckStorageModel::recomputeDeckMetadata(DeckPreviewData &data, bool r
|
|||
data.lastLoaded = QDateTime::fromString(deckList.getLastLoadedTimestamp());
|
||||
data.bannerCard = deckList.getBannerCard();
|
||||
if (recomputeColorIdentity) {
|
||||
data.colorIdentity = computeColorIdentity(data.deck);
|
||||
data.colorIdentity = getDeckColorIdentity(data.deck.deckList, CardDatabaseManager::query());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,15 @@ VisualDeckStorageQuickSettingsWidget::VisualDeckStorageQuickSettingsWidget(QWidg
|
|||
&SettingsCache::instance().visualDeckStorage(),
|
||||
&VisualDeckStorageSettings::setVisualDeckStorageShowTagsOnDeckPreviews);
|
||||
|
||||
// show upload time on DeckPreviewWidget checkbox
|
||||
showUploadTimeCheckBox = new QCheckBox(this);
|
||||
showUploadTimeCheckBox->setChecked(
|
||||
SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowUploadTime());
|
||||
connect(showUploadTimeCheckBox, &QCheckBox::QT_STATE_CHANGED, this,
|
||||
&VisualDeckStorageQuickSettingsWidget::showUploadTimeChanged);
|
||||
connect(showUploadTimeCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().visualDeckStorage(),
|
||||
&VisualDeckStorageSettings::setVisualDeckStorageShowUploadTime);
|
||||
|
||||
// show banner card selector checkbox
|
||||
showBannerCardComboBoxCheckBox = new QCheckBox(this);
|
||||
showBannerCardComboBoxCheckBox->setChecked(
|
||||
|
|
@ -94,7 +103,7 @@ VisualDeckStorageQuickSettingsWidget::VisualDeckStorageQuickSettingsWidget(QWidg
|
|||
unusedColorIdentityOpacityLayout->addWidget(unusedColorIdentitiesOpacitySpinBox);
|
||||
|
||||
// tooltip selector
|
||||
auto deckPreviewTooltipWidget = new QWidget(this);
|
||||
deckPreviewTooltipWidget = new QWidget(this);
|
||||
|
||||
deckPreviewTooltipLabel = new QLabel(deckPreviewTooltipWidget);
|
||||
deckPreviewTooltipComboBox = new QComboBox(deckPreviewTooltipWidget);
|
||||
|
|
@ -128,6 +137,7 @@ VisualDeckStorageQuickSettingsWidget::VisualDeckStorageQuickSettingsWidget(QWidg
|
|||
this->addSettingsWidget(showTagFilterCheckBox);
|
||||
this->addSettingsWidget(showColorIdentityCheckBox);
|
||||
this->addSettingsWidget(showTagsOnDeckPreviewsCheckBox);
|
||||
this->addSettingsWidget(showUploadTimeCheckBox);
|
||||
this->addSettingsWidget(showBannerCardComboBoxCheckBox);
|
||||
this->addSettingsWidget(drawUnusedColorIdentitiesCheckBox);
|
||||
this->addSettingsWidget(unusedColorIdentityOpacityWidget);
|
||||
|
|
@ -145,6 +155,7 @@ void VisualDeckStorageQuickSettingsWidget::retranslateUi()
|
|||
showTagFilterCheckBox->setText(tr("Show Tag Filter"));
|
||||
showColorIdentityCheckBox->setText(tr("Show Color Identity"));
|
||||
showTagsOnDeckPreviewsCheckBox->setText(tr("Show Tags On Deck Previews"));
|
||||
showUploadTimeCheckBox->setText(tr("Show Upload Time"));
|
||||
showBannerCardComboBoxCheckBox->setText(tr("Show Banner Card Selection Option"));
|
||||
drawUnusedColorIdentitiesCheckBox->setText(tr("Draw unused Color Identities"));
|
||||
unusedColorIdentitiesOpacityLabel->setText(tr("Unused Color Identities Opacity"));
|
||||
|
|
@ -155,6 +166,14 @@ void VisualDeckStorageQuickSettingsWidget::retranslateUi()
|
|||
deckPreviewTooltipComboBox->setItemText(1, tr("Filepath"));
|
||||
}
|
||||
|
||||
void VisualDeckStorageQuickSettingsWidget::setPublicDecksMode(bool enabled)
|
||||
{
|
||||
const bool hidden = enabled;
|
||||
showFoldersCheckBox->setVisible(!hidden);
|
||||
showBannerCardComboBoxCheckBox->setVisible(!hidden);
|
||||
deckPreviewTooltipWidget->setVisible(!hidden);
|
||||
}
|
||||
|
||||
bool VisualDeckStorageQuickSettingsWidget::getShowFolders() const
|
||||
{
|
||||
return showFoldersCheckBox->isChecked();
|
||||
|
|
@ -185,6 +204,11 @@ bool VisualDeckStorageQuickSettingsWidget::getShowTagsOnDeckPreviews() const
|
|||
return showTagsOnDeckPreviewsCheckBox->isChecked();
|
||||
}
|
||||
|
||||
bool VisualDeckStorageQuickSettingsWidget::getShowUploadTime() const
|
||||
{
|
||||
return showUploadTimeCheckBox->isChecked();
|
||||
}
|
||||
|
||||
int VisualDeckStorageQuickSettingsWidget::getUnusedColorIdentitiesOpacity() const
|
||||
{
|
||||
return unusedColorIdentitiesOpacitySpinBox->value();
|
||||
|
|
|
|||
|
|
@ -27,10 +27,12 @@ class VisualDeckStorageQuickSettingsWidget : public SettingsButtonWidget
|
|||
QCheckBox *showBannerCardComboBoxCheckBox;
|
||||
QCheckBox *showTagFilterCheckBox;
|
||||
QCheckBox *showTagsOnDeckPreviewsCheckBox;
|
||||
QCheckBox *showUploadTimeCheckBox;
|
||||
QLabel *unusedColorIdentitiesOpacityLabel;
|
||||
QSpinBox *unusedColorIdentitiesOpacitySpinBox;
|
||||
QLabel *deckPreviewTooltipLabel;
|
||||
QComboBox *deckPreviewTooltipComboBox;
|
||||
QWidget *deckPreviewTooltipWidget;
|
||||
CardSizeWidget *cardSizeWidget;
|
||||
|
||||
public:
|
||||
|
|
@ -46,6 +48,15 @@ public:
|
|||
|
||||
explicit VisualDeckStorageQuickSettingsWidget(QWidget *parent = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Hides the controls that do not apply to the public decks tab.
|
||||
*
|
||||
* The public decks tab reuses this widget for its quick settings menu but
|
||||
* has no folders, banner selection or per-deck tooltip, so those controls
|
||||
* are hidden while every shared key keeps syncing with SettingsCache.
|
||||
*/
|
||||
void setPublicDecksMode(bool enabled);
|
||||
|
||||
void retranslateUi();
|
||||
|
||||
[[nodiscard]] bool getShowFolders() const;
|
||||
|
|
@ -54,10 +65,20 @@ public:
|
|||
[[nodiscard]] bool getShowBannerCardComboBox() const;
|
||||
[[nodiscard]] bool getShowTagFilter() const;
|
||||
[[nodiscard]] bool getShowTagsOnDeckPreviews() const;
|
||||
[[nodiscard]] bool getShowUploadTime() const;
|
||||
[[nodiscard]] int getUnusedColorIdentitiesOpacity() const;
|
||||
[[nodiscard]] TooltipType getDeckPreviewTooltip() const;
|
||||
[[nodiscard]] int getCardSize() const;
|
||||
|
||||
/**
|
||||
* @return The card size widget, so card display hosts can resize the cards on
|
||||
* Ctrl + scroll even though the slider itself lives inside this menu.
|
||||
*/
|
||||
[[nodiscard]] CardSizeWidget *getCardSizeWidget() const
|
||||
{
|
||||
return cardSizeWidget;
|
||||
}
|
||||
|
||||
signals:
|
||||
void showFoldersChanged(bool enabled);
|
||||
void drawUnusedColorIdentitiesChanged(bool enabled);
|
||||
|
|
@ -65,6 +86,7 @@ signals:
|
|||
void showBannerCardComboBoxChanged(bool enabled);
|
||||
void showTagFilterChanged(bool enabled);
|
||||
void showTagsOnDeckPreviewsChanged(bool enabled);
|
||||
void showUploadTimeChanged(bool enabled);
|
||||
void unusedColorIdentitiesOpacityChanged(int opacity);
|
||||
void deckPreviewTooltipChanged(TooltipType tooltip);
|
||||
void cardSizeChanged(int scale);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
#include "visual_deck_storage_sort_filter_proxy_model.h"
|
||||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../../filters/deck_filter_string.h"
|
||||
|
||||
#include <QFileInfo>
|
||||
#include <algorithm>
|
||||
#include <libcockatrice/card/card_localization.h>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
|
||||
VisualDeckStorageSortFilterProxyModel::VisualDeckStorageSortFilterProxyModel(QObject *parent)
|
||||
: QSortFilterProxyModel(parent)
|
||||
|
|
@ -11,6 +14,34 @@ VisualDeckStorageSortFilterProxyModel::VisualDeckStorageSortFilterProxyModel(QOb
|
|||
setDynamicSortFilter(false);
|
||||
}
|
||||
|
||||
bool colorIdentityMatches(VisualDeckStorageSortFilterProxyModel::FilterMode mode,
|
||||
const QSet<QChar> &colors,
|
||||
const QString &identity)
|
||||
{
|
||||
switch (mode) {
|
||||
case VisualDeckStorageSortFilterProxyModel::ExactMatch: {
|
||||
QSet<QChar> activeColorSet;
|
||||
for (const QChar &color : colors) {
|
||||
activeColorSet.insert(color.toUpper());
|
||||
}
|
||||
|
||||
QSet<QChar> colorIdentitySet;
|
||||
for (const QChar &color : identity) {
|
||||
colorIdentitySet.insert(color.toUpper());
|
||||
}
|
||||
|
||||
return activeColorSet == colorIdentitySet;
|
||||
}
|
||||
case VisualDeckStorageSortFilterProxyModel::Includes:
|
||||
return std::all_of(colors.begin(), colors.end(),
|
||||
[&identity](const QChar &color) { return identity.contains(color); });
|
||||
case VisualDeckStorageSortFilterProxyModel::Excludes:
|
||||
return std::none_of(colors.begin(), colors.end(),
|
||||
[&identity](const QChar &color) { return identity.contains(color); });
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void VisualDeckStorageSortFilterProxyModel::setSourceModel(QAbstractItemModel *model)
|
||||
{
|
||||
if (QAbstractItemModel *oldModel = sourceModel()) {
|
||||
|
|
@ -187,7 +218,10 @@ void VisualDeckStorageSortFilterProxyModel::updateSearchMatches()
|
|||
return;
|
||||
}
|
||||
|
||||
DeckFilterString filterString(searchText);
|
||||
const auto &cardsDisplay = SettingsCache::instance().cardsDisplay();
|
||||
DeckFilterString filterString(
|
||||
searchText, CardSearchLanguage{cardsDisplay.getCardLang(),
|
||||
static_cast<SearchLanguageMode>(cardsDisplay.getCardSearchLanguage())});
|
||||
for (int row = 0; row < count; ++row) {
|
||||
const DeckPreviewData &data = source->dataForRow(row);
|
||||
|
||||
|
|
@ -255,34 +289,7 @@ void VisualDeckStorageSortFilterProxyModel::updateColorMatches()
|
|||
|
||||
for (int row = 0; row < count; ++row) {
|
||||
const QString colorIdentity = source->dataForRow(row).colorIdentity;
|
||||
|
||||
bool matches = true;
|
||||
switch (colorFilterMode) {
|
||||
case ExactMatch: {
|
||||
QSet<QChar> activeColorSet;
|
||||
for (const QChar &color : activeColors) {
|
||||
activeColorSet.insert(color.toUpper());
|
||||
}
|
||||
|
||||
QSet<QChar> colorIdentitySet;
|
||||
for (const QChar &color : colorIdentity) {
|
||||
colorIdentitySet.insert(color.toUpper());
|
||||
}
|
||||
|
||||
matches = activeColorSet == colorIdentitySet;
|
||||
break;
|
||||
}
|
||||
case Includes:
|
||||
matches = std::all_of(activeColors.begin(), activeColors.end(),
|
||||
[&colorIdentity](const QChar &color) { return colorIdentity.contains(color); });
|
||||
break;
|
||||
case Excludes:
|
||||
matches = std::none_of(activeColors.begin(), activeColors.end(),
|
||||
[&colorIdentity](const QChar &color) { return colorIdentity.contains(color); });
|
||||
break;
|
||||
}
|
||||
|
||||
colorMatches[row] = matches;
|
||||
colorMatches[row] = colorIdentityMatches(colorFilterMode, activeColors, colorIdentity);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -102,4 +102,14 @@ private:
|
|||
QList<bool> colorMatches; ///< Per-row color identity match.
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Whether an identity string matches the active color-identity filter.
|
||||
*
|
||||
* The single source of truth for the color identity matching rule, shared by
|
||||
* the Visual Deck Storage proxy and the remote public decks model.
|
||||
*/
|
||||
[[nodiscard]] bool colorIdentityMatches(VisualDeckStorageSortFilterProxyModel::FilterMode mode,
|
||||
const QSet<QChar> &colors,
|
||||
const QString &identity);
|
||||
|
||||
#endif // VISUAL_DECK_STORAGE_SORT_FILTER_PROXY_MODEL_H
|
||||
|
|
|
|||
|
|
@ -2,14 +2,10 @@
|
|||
|
||||
#include "../general/layout_containers/flow_widget.h"
|
||||
#include "deck_preview/deck_preview_tag_display_widget.h"
|
||||
#include "visual_deck_storage_model.h"
|
||||
#include "visual_deck_storage_sort_filter_proxy_model.h"
|
||||
#include "visual_deck_storage_widget.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
|
||||
VisualDeckStorageTagFilterWidget::VisualDeckStorageTagFilterWidget(VisualDeckStorageWidget *_parent)
|
||||
: QWidget(_parent), parent(_parent)
|
||||
VisualDeckStorageTagFilterWidget::VisualDeckStorageTagFilterWidget(QWidget *parent) : QWidget(parent)
|
||||
{
|
||||
|
||||
setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
|
||||
|
|
@ -25,99 +21,72 @@ VisualDeckStorageTagFilterWidget::VisualDeckStorageTagFilterWidget(VisualDeckSto
|
|||
layout->addWidget(flowWidget);
|
||||
}
|
||||
|
||||
void VisualDeckStorageTagFilterWidget::setAllTagsProvider(const std::function<QSet<QString>()> &provider)
|
||||
{
|
||||
allTagsProvider = provider;
|
||||
}
|
||||
|
||||
void VisualDeckStorageTagFilterWidget::showEvent(QShowEvent *event)
|
||||
{
|
||||
QWidget::showEvent(event);
|
||||
refreshTags();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The tags of all decks currently accepted by the proxy model.
|
||||
*/
|
||||
QSet<QString> VisualDeckStorageTagFilterWidget::gatherAllTags() const
|
||||
{
|
||||
QSet<QString> allTags;
|
||||
auto *proxy = parent->proxyModel();
|
||||
|
||||
for (int proxyRow = 0; proxyRow < proxy->rowCount(); ++proxyRow) {
|
||||
const QModelIndex index = proxy->index(proxyRow, 0);
|
||||
if (!index.data(VisualDeckStorageRoles::FilterMatchRole).toBool()) {
|
||||
continue;
|
||||
}
|
||||
const QStringList deckTags = index.data(VisualDeckStorageRoles::TagsRole).toStringList();
|
||||
for (const QString &tag : deckTags) {
|
||||
allTags.insert(tag);
|
||||
}
|
||||
}
|
||||
|
||||
return allTags;
|
||||
}
|
||||
|
||||
void VisualDeckStorageTagFilterWidget::refreshTags()
|
||||
{
|
||||
QSet<QString> allTags = gatherAllTags();
|
||||
removeTagsNotInList(allTags);
|
||||
addTagsIfNotPresent(allTags);
|
||||
sortTags();
|
||||
}
|
||||
const QSet<QString> allTags = allTagsProvider ? allTagsProvider() : QSet<QString>();
|
||||
|
||||
void VisualDeckStorageTagFilterWidget::removeTagsNotInList(const QSet<QString> &tags)
|
||||
{
|
||||
// Existing chips survive if their tag is still part of the deck set, or if the chip
|
||||
// is currently selected/excluded. Everything else is dropped. Dropped chips must NOT
|
||||
// be re-added to the layout afterwards: they are reparented to nullptr and scheduled
|
||||
// for a deferred delete (QWidget::setParent(nullptr) also hides them), and the flow
|
||||
// layout would keep a dangling reference to them once the deletion runs on the next
|
||||
// event-loop cycle.
|
||||
QList<DeckPreviewTagDisplayWidget *> chips;
|
||||
for (DeckPreviewTagDisplayWidget *tagWidget : findChildren<DeckPreviewTagDisplayWidget *>()) {
|
||||
const QString &tagName = tagWidget->getTagName();
|
||||
|
||||
// Keep the tag widget if it is either selected or excluded
|
||||
if (!tags.contains(tagName) && tagWidget->getState() == TagState::NotSelected) {
|
||||
if (tagWidget->getState() != TagState::NotSelected || allTags.contains(tagWidget->getTagName())) {
|
||||
chips.append(tagWidget);
|
||||
} else {
|
||||
flowWidget->removeWidget(tagWidget);
|
||||
tagWidget->setParent(nullptr);
|
||||
tagWidget->deleteLater();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VisualDeckStorageTagFilterWidget::addTagsIfNotPresent(const QSet<QString> &tags)
|
||||
{
|
||||
for (const QString &tag : tags) {
|
||||
addTagIfNotPresent(tag);
|
||||
// Add chips for tags that are not shown yet.
|
||||
QSet<QString> existingTags;
|
||||
for (DeckPreviewTagDisplayWidget *tagWidget : chips) {
|
||||
existingTags.insert(tagWidget->getTagName());
|
||||
}
|
||||
}
|
||||
|
||||
void VisualDeckStorageTagFilterWidget::addTagIfNotPresent(const QString &tag)
|
||||
{
|
||||
// Check if the tag already exists in the flow widget
|
||||
bool tagExists = false;
|
||||
for (DeckPreviewTagDisplayWidget *tagWidget : findChildren<DeckPreviewTagDisplayWidget *>()) {
|
||||
if (tagWidget->getTagName() == tag) {
|
||||
tagExists = true;
|
||||
break;
|
||||
for (const QString &tag : allTags) {
|
||||
if (!existingTags.contains(tag)) {
|
||||
auto *newTagWidget = new DeckPreviewTagDisplayWidget(this, tag);
|
||||
connect(newTagWidget, &DeckPreviewTagDisplayWidget::tagClicked, this,
|
||||
&VisualDeckStorageTagFilterWidget::filterChanged);
|
||||
flowWidget->addWidget(newTagWidget);
|
||||
chips.append(newTagWidget);
|
||||
}
|
||||
}
|
||||
|
||||
// If the tag doesn't exist, add a new DeckPreviewTagDisplayWidget
|
||||
if (!tagExists) {
|
||||
auto *newTagWidget = new DeckPreviewTagDisplayWidget(this, tag);
|
||||
connect(newTagWidget, &DeckPreviewTagDisplayWidget::tagClicked, parent,
|
||||
&VisualDeckStorageWidget::updateTagFilter);
|
||||
flowWidget->addWidget(newTagWidget);
|
||||
}
|
||||
}
|
||||
|
||||
void VisualDeckStorageTagFilterWidget::sortTags()
|
||||
{
|
||||
// Get all tag widgets
|
||||
QList<DeckPreviewTagDisplayWidget *> tagWidgets = findChildren<DeckPreviewTagDisplayWidget *>();
|
||||
|
||||
// Sort widgets by tag name
|
||||
std::sort(tagWidgets.begin(), tagWidgets.end(), [](DeckPreviewTagDisplayWidget *a, DeckPreviewTagDisplayWidget *b) {
|
||||
return a->getTagName().toLower() < b->getTagName().toLower();
|
||||
// Sort, but skip the full remove/re-add when the order already matches the layout.
|
||||
// FlowWidget inherits QLayout::removeWidget's linear scan, so rebuilding an unchanged
|
||||
// order would be quadratic plus a full relayout on every chip click and load batch.
|
||||
std::sort(chips.begin(), chips.end(), [](DeckPreviewTagDisplayWidget *a, DeckPreviewTagDisplayWidget *b) {
|
||||
const QString aName = a->getTagName();
|
||||
const QString bName = b->getTagName();
|
||||
const int compared = aName.compare(bName, Qt::CaseInsensitive);
|
||||
return compared != 0 ? compared < 0 : aName < bName;
|
||||
});
|
||||
|
||||
// Clear and re-add widgets in sorted order
|
||||
for (DeckPreviewTagDisplayWidget *tagWidget : tagWidgets) {
|
||||
if (chips == currentChipOrder) {
|
||||
return;
|
||||
}
|
||||
for (DeckPreviewTagDisplayWidget *tagWidget : chips) {
|
||||
flowWidget->removeWidget(tagWidget);
|
||||
}
|
||||
for (DeckPreviewTagDisplayWidget *tagWidget : tagWidgets) {
|
||||
for (DeckPreviewTagDisplayWidget *tagWidget : chips) {
|
||||
flowWidget->addWidget(tagWidget);
|
||||
}
|
||||
currentChipOrder = chips;
|
||||
}
|
||||
|
||||
QStringList VisualDeckStorageTagFilterWidget::selectedTags() const
|
||||
|
|
|
|||
|
|
@ -9,26 +9,28 @@
|
|||
#include <QSet>
|
||||
#include <QStringList>
|
||||
#include <QWidget>
|
||||
#include <functional>
|
||||
|
||||
class DeckPreviewTagDisplayWidget;
|
||||
class FlowWidget;
|
||||
class VisualDeckStorageWidget;
|
||||
|
||||
class VisualDeckStorageTagFilterWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
VisualDeckStorageWidget *parent;
|
||||
FlowWidget *flowWidget;
|
||||
|
||||
[[nodiscard]] QSet<QString> gatherAllTags() const;
|
||||
void removeTagsNotInList(const QSet<QString> &tags);
|
||||
void addTagsIfNotPresent(const QSet<QString> &tags);
|
||||
void addTagIfNotPresent(const QString &tag);
|
||||
void sortTags();
|
||||
std::function<QSet<QString>()> allTagsProvider;
|
||||
QList<DeckPreviewTagDisplayWidget *> currentChipOrder;
|
||||
|
||||
public:
|
||||
explicit VisualDeckStorageTagFilterWidget(VisualDeckStorageWidget *_parent);
|
||||
explicit VisualDeckStorageTagFilterWidget(QWidget *parent = nullptr);
|
||||
[[nodiscard]] QStringList getAllKnownTags() const;
|
||||
|
||||
/**
|
||||
* @brief Sets a provider for the full set of tags to draw chips from.
|
||||
*/
|
||||
void setAllTagsProvider(const std::function<QSet<QString>()> &provider);
|
||||
|
||||
/**
|
||||
* @brief The tags currently in "selected" state.
|
||||
*/
|
||||
|
|
@ -39,9 +41,18 @@ public:
|
|||
*/
|
||||
[[nodiscard]] QStringList excludedTags() const;
|
||||
|
||||
signals:
|
||||
/**
|
||||
* Emitted when a chip's selection or exclusion state changes.
|
||||
*
|
||||
* The chip only emits when its state actually changed, so this fires once per
|
||||
* effective toggle rather than on every click.
|
||||
*/
|
||||
void filterChanged();
|
||||
|
||||
public slots:
|
||||
/**
|
||||
* @brief Rebuilds the tag chips from the tags of the currently visible decks.
|
||||
* @brief Rebuilds the tag chips from the currently available tags.
|
||||
*/
|
||||
void refreshTags();
|
||||
void showEvent(QShowEvent *event) override;
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
#include <libcockatrice/settings/paths_settings.h>
|
||||
#include <libcockatrice/settings/visual_deck_storage_settings.h>
|
||||
|
||||
|
|
@ -48,6 +49,12 @@ VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(pare
|
|||
refreshButton->setFixedSize(32, 32);
|
||||
connect(refreshButton, &QPushButton::clicked, this, &VisualDeckStorageWidget::refreshIfPossible);
|
||||
|
||||
shareButton = new QToolButton(this);
|
||||
shareButton->setIcon(themePixmap(QStringLiteral("icons/share")));
|
||||
shareButton->setFixedSize(32, 32);
|
||||
shareButton->setVisible(false);
|
||||
connect(shareButton, &QPushButton::clicked, this, &VisualDeckStorageWidget::shareRequested);
|
||||
|
||||
quickSettingsWidget = new VisualDeckStorageQuickSettingsWidget(this);
|
||||
connect(quickSettingsWidget, &VisualDeckStorageQuickSettingsWidget::showFoldersChanged, this,
|
||||
&VisualDeckStorageWidget::updateShowFolders);
|
||||
|
|
@ -58,10 +65,14 @@ VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(pare
|
|||
searchAndSortLayout->addWidget(sortWidget);
|
||||
searchAndSortLayout->addWidget(searchWidget);
|
||||
searchAndSortLayout->addWidget(refreshButton);
|
||||
searchAndSortLayout->addWidget(shareButton);
|
||||
searchAndSortLayout->addWidget(quickSettingsWidget);
|
||||
|
||||
// tag filter box
|
||||
tagFilterWidget = new VisualDeckStorageTagFilterWidget(this);
|
||||
tagFilterWidget->setAllTagsProvider([this] { return gatherVisibleTags(); });
|
||||
connect(tagFilterWidget, &VisualDeckStorageTagFilterWidget::filterChanged, this,
|
||||
&VisualDeckStorageWidget::updateTagFilter);
|
||||
updateTagsVisibility(SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowTagFilter());
|
||||
|
||||
deckPreviewSelectionAnimationEnabled =
|
||||
|
|
@ -108,6 +119,13 @@ VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(pare
|
|||
connect(searchWidget, &VisualDeckStorageSearchWidget::searchTextChanged, this,
|
||||
&VisualDeckStorageWidget::updateSearchFilter);
|
||||
|
||||
// The deck content search matches card names in the configured card language;
|
||||
// re-run it whenever that setting changes so active searches follow immediately.
|
||||
CardsDisplaySettings *cardsDisplay = &SettingsCache::instance().cardsDisplay();
|
||||
const auto reapplySearchForLanguage = [this] { storageProxyModel->reapplyFilters(); };
|
||||
connect(cardsDisplay, &CardsDisplaySettings::cardLangChanged, this, reapplySearchForLanguage);
|
||||
connect(cardsDisplay, &CardsDisplaySettings::cardSearchLanguageChanged, this, reapplySearchForLanguage);
|
||||
|
||||
connect(CardDatabaseManager::getInstance(), &CardDatabase::cardDatabaseLoadingFinished, this,
|
||||
&VisualDeckStorageWidget::createRootFolderWidget);
|
||||
|
||||
|
|
@ -155,11 +173,71 @@ void VisualDeckStorageWidget::retranslateUi()
|
|||
databaseLoadIndicator->setText(tr("Loading database ..."));
|
||||
|
||||
refreshButton->setToolTip(tr("Refresh loaded files"));
|
||||
shareButton->setToolTip(tr("Select decks to share"));
|
||||
quickSettingsWidget->setToolTip(tr("Visual Deck Storage Settings"));
|
||||
|
||||
sortWidget->retranslateUi();
|
||||
}
|
||||
|
||||
void VisualDeckStorageWidget::setShareSelectable(bool selectable)
|
||||
{
|
||||
if (shareSelectable == selectable) {
|
||||
return;
|
||||
}
|
||||
shareSelectable = selectable;
|
||||
if (folderWidget != nullptr) {
|
||||
folderWidget->setShareSelectable(selectable);
|
||||
}
|
||||
emit shareSelectionChanged();
|
||||
}
|
||||
|
||||
bool VisualDeckStorageWidget::isShareSelectable() const
|
||||
{
|
||||
return shareSelectable;
|
||||
}
|
||||
|
||||
QStringList VisualDeckStorageWidget::selectedFilePaths() const
|
||||
{
|
||||
QStringList selectedPaths;
|
||||
if (folderWidget != nullptr) {
|
||||
const auto previews = folderWidget->findChildren<DeckPreviewWidget *>();
|
||||
for (DeckPreviewWidget *preview : previews) {
|
||||
// Filtered-out previews stay alive hidden in their sorted place, so only decks the
|
||||
// user can actually see are part of the share.
|
||||
if (preview->isVisible() && preview->isShareSelected()) {
|
||||
selectedPaths.append(preview->filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
return selectedPaths;
|
||||
}
|
||||
|
||||
void VisualDeckStorageWidget::clearShareSelection()
|
||||
{
|
||||
if (folderWidget != nullptr) {
|
||||
const auto previews = folderWidget->findChildren<DeckPreviewWidget *>();
|
||||
for (DeckPreviewWidget *preview : previews) {
|
||||
preview->setShareSelected(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VisualDeckStorageWidget::setShareAvailable(bool available)
|
||||
{
|
||||
shareButton->setVisible(available);
|
||||
shareButton->setEnabled(available);
|
||||
}
|
||||
|
||||
void VisualDeckStorageWidget::setShareSelectedFiles(const QStringList &paths)
|
||||
{
|
||||
if (folderWidget != nullptr) {
|
||||
const auto previews = folderWidget->findChildren<DeckPreviewWidget *>();
|
||||
for (DeckPreviewWidget *preview : previews) {
|
||||
preview->setShareSelected(paths.contains(preview->filePath));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a const pointer to the quick settings so that the values can be accessed.
|
||||
*/
|
||||
|
|
@ -217,6 +295,25 @@ void VisualDeckStorageWidget::updateTagFilter()
|
|||
tagFilterWidget->refreshTags();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The tags of all decks currently accepted by the proxy model.
|
||||
*/
|
||||
QSet<QString> VisualDeckStorageWidget::gatherVisibleTags() const
|
||||
{
|
||||
QSet<QString> allTags;
|
||||
for (int proxyRow = 0; proxyRow < storageProxyModel->rowCount(); ++proxyRow) {
|
||||
const QModelIndex index = storageProxyModel->index(proxyRow, 0);
|
||||
if (!index.data(VisualDeckStorageRoles::FilterMatchRole).toBool()) {
|
||||
continue;
|
||||
}
|
||||
const QStringList deckTags = index.data(VisualDeckStorageRoles::TagsRole).toStringList();
|
||||
for (const QString &tag : deckTags) {
|
||||
allTags.insert(tag);
|
||||
}
|
||||
}
|
||||
return allTags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes the color identity filter widget's state into the proxy model.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -33,6 +33,12 @@ public:
|
|||
explicit VisualDeckStorageWidget(QWidget *parent);
|
||||
void refreshIfPossible();
|
||||
void retranslateUi();
|
||||
void setShareSelectable(bool selectable);
|
||||
[[nodiscard]] bool isShareSelectable() const;
|
||||
[[nodiscard]] QStringList selectedFilePaths() const;
|
||||
void setShareSelectedFiles(const QStringList &paths);
|
||||
void clearShareSelection();
|
||||
void setShareAvailable(bool available);
|
||||
|
||||
VisualDeckStorageTagFilterWidget *tagFilterWidget;
|
||||
bool deckPreviewSelectionAnimationEnabled;
|
||||
|
|
@ -63,6 +69,9 @@ public slots:
|
|||
signals:
|
||||
void deckLoadRequested(const QString &filePath);
|
||||
void openDeckEditor(const LoadedDeck &deck);
|
||||
void shareDeckRequested(const QString &filePath);
|
||||
void shareSelectionChanged();
|
||||
void shareRequested();
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
|
|
@ -70,6 +79,7 @@ protected:
|
|||
|
||||
private:
|
||||
void reapplySortAndFilters();
|
||||
[[nodiscard]] QSet<QString> gatherVisibleTags() const;
|
||||
|
||||
private:
|
||||
QVBoxLayout *layout;
|
||||
|
|
@ -80,12 +90,14 @@ private:
|
|||
VisualDeckStorageSearchWidget *searchWidget;
|
||||
DeckPreviewColorIdentityFilterWidget *deckPreviewColorIdentityFilterWidget;
|
||||
QToolButton *refreshButton;
|
||||
QToolButton *shareButton;
|
||||
VisualDeckStorageQuickSettingsWidget *quickSettingsWidget;
|
||||
QScrollArea *scrollArea;
|
||||
VisualDeckStorageFolderDisplayWidget *folderWidget = nullptr;
|
||||
VisualDeckStorageModel *storageModel = nullptr;
|
||||
VisualDeckStorageSortFilterProxyModel *storageProxyModel = nullptr;
|
||||
QTimer *refreshTimer = nullptr; ///< Coalesces the re-apply/refresh burst following a batch of deck loads.
|
||||
bool shareSelectable = false;
|
||||
};
|
||||
|
||||
#endif // VISUAL_DECK_STORAGE_WIDGET_H
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue