mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-25 11:56:11 -07:00
Merge branch 'master' into tooomm-qt5
This commit is contained in:
commit
3bc08ef94c
357 changed files with 23069 additions and 2916 deletions
|
|
@ -41,6 +41,11 @@ void ColorIdentityWidget::populateManaSymbolWidgets()
|
|||
// clear old layout
|
||||
QtUtils::clearLayoutRec(layout);
|
||||
|
||||
// The freshly created symbols haven't been sized yet, so force the next resize pass
|
||||
// to apply the symbol size again.
|
||||
lastIconSize = -1;
|
||||
lastWidth = -1;
|
||||
|
||||
// populate mana symbols
|
||||
if (SettingsCache::instance().visualDeckStorage().getVisualDeckStorageDrawUnusedColorIdentities()) {
|
||||
for (const QString symbol : fullColorIdentity) {
|
||||
|
|
@ -73,20 +78,33 @@ void ColorIdentityWidget::toggleUnusedVisibility()
|
|||
void ColorIdentityWidget::resizeEvent(QResizeEvent *event)
|
||||
{
|
||||
QWidget::resizeEvent(event);
|
||||
QList<ManaSymbolWidget *> manaSymbols = findChildren<ManaSymbolWidget *>();
|
||||
|
||||
if (!manaSymbols.isEmpty()) {
|
||||
int totalWidth = event->size().width();
|
||||
int totalHeight = totalWidth / 6; // Set height to 1/4 of the width
|
||||
setFixedHeight(totalHeight);
|
||||
const int totalWidth = event->size().width();
|
||||
if (totalWidth == lastWidth && lastIconSize != -1) {
|
||||
return;
|
||||
}
|
||||
lastWidth = totalWidth;
|
||||
|
||||
int spacing = layout->spacing();
|
||||
int count = manaSymbols.size();
|
||||
int availableWidth = totalWidth - (spacing * (count - 1));
|
||||
int iconSize = qMin(availableWidth / count, totalHeight); // Ensure icons fit within the new height
|
||||
const int totalHeight = totalWidth / 6; // Set height to 1/4 of the width
|
||||
setFixedHeight(totalHeight);
|
||||
|
||||
for (ManaSymbolWidget *manaSymbol : manaSymbols) {
|
||||
manaSymbol->setFixedSize(iconSize, iconSize);
|
||||
const int count = layout->count();
|
||||
if (count == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int spacing = layout->spacing();
|
||||
const int availableWidth = totalWidth - (spacing * (count - 1));
|
||||
const int iconSize = qMin(availableWidth / count, totalHeight); // Ensure icons fit within the new height
|
||||
|
||||
if (iconSize == lastIconSize) {
|
||||
return;
|
||||
}
|
||||
lastIconSize = iconSize;
|
||||
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (auto *w = qobject_cast<ManaSymbolWidget *>(layout->itemAt(i)->widget())) {
|
||||
w->setFixedSize(iconSize, iconSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ public slots:
|
|||
private:
|
||||
QString colorIdentity;
|
||||
QHBoxLayout *layout;
|
||||
int lastIconSize = -1; ///< The symbol size last applied, to skip redundant resize passes.
|
||||
int lastWidth = -1; ///< The width last processed, to skip redundant resize passes.
|
||||
};
|
||||
|
||||
#endif // COLOR_IDENTITY_WIDGET_H
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ class ManaCostWidget : public QWidget
|
|||
public:
|
||||
explicit ManaCostWidget(QWidget *parent, CardInfoPtr card);
|
||||
|
||||
QStringList parseManaCost(const QString &manaString);
|
||||
static QStringList parseManaCost(const QString &manaString);
|
||||
public slots:
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
#include "mana_symbol_widget.h"
|
||||
|
||||
#include "../../../../client/settings/cache_settings.h"
|
||||
#include "../../../pixel_map_generator.h"
|
||||
|
||||
#include <QResizeEvent>
|
||||
#include <libcockatrice/settings/visual_deck_storage_settings.h>
|
||||
|
||||
ManaSymbolWidget::ManaSymbolWidget(QWidget *parent, QString _symbol, bool _isActive, bool _mayBeToggled)
|
||||
: QLabel(parent), symbol(_symbol), isActive(_isActive), mayBeToggled(_mayBeToggled)
|
||||
: QLabel(parent), symbol(std::move(_symbol)), isActive(_isActive), mayBeToggled(_mayBeToggled)
|
||||
{
|
||||
loadManaIcon();
|
||||
setPixmap(manaIcon.scaled(50, 50, Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||||
setPixmap(ManaSymbolPixmapGenerator::generatePixmap(symbol, QSize(50, 50)));
|
||||
setMaximumWidth(50);
|
||||
|
||||
// Initialize opacity effect
|
||||
|
|
@ -64,16 +64,13 @@ void ManaSymbolWidget::mousePressEvent(QMouseEvent *event)
|
|||
void ManaSymbolWidget::resizeEvent(QResizeEvent *event)
|
||||
{
|
||||
QLabel::resizeEvent(event);
|
||||
setPixmap(manaIcon.scaled(event->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||||
}
|
||||
const QSize newSize = event->size();
|
||||
|
||||
void ManaSymbolWidget::loadManaIcon()
|
||||
{
|
||||
QString filename = "theme:icons/mana/";
|
||||
|
||||
if (symbol == "W" || symbol == "U" || symbol == "B" || symbol == "R" || symbol == "G") {
|
||||
filename += symbol;
|
||||
// Skip the rescale when the size didn't actually change: layout passes resize these
|
||||
// widgets repeatedly with identical sizes.
|
||||
if (newSize.isEmpty() || pixmap().size() == newSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
manaIcon = QPixmap(filename);
|
||||
setPixmap(ManaSymbolPixmapGenerator::generatePixmap(symbol, newSize));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,8 +33,6 @@ public:
|
|||
return symbol[0];
|
||||
}
|
||||
|
||||
void loadManaIcon();
|
||||
|
||||
public slots:
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
|
|
@ -44,7 +42,6 @@ signals:
|
|||
|
||||
private:
|
||||
QString symbol;
|
||||
QPixmap manaIcon;
|
||||
bool isActive;
|
||||
bool mayBeToggled;
|
||||
QGraphicsOpacityEffect *opacityEffect;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
#include "art_crop_attribution.h"
|
||||
|
||||
#include <QFontMetrics>
|
||||
#include <QObject>
|
||||
#include <QPainter>
|
||||
#include <libcockatrice/card/printing/exact_card.h>
|
||||
|
||||
QString buildArtAttribution(const ExactCard &card)
|
||||
{
|
||||
const QString artist = card.getPrinting().getArtist();
|
||||
if (artist.isEmpty()) {
|
||||
return QString();
|
||||
}
|
||||
return QObject::tr("Art: %1").arg(artist);
|
||||
}
|
||||
|
||||
QRectF paintArtAttribution(QPainter &painter,
|
||||
const QRectF &rect,
|
||||
const QString &attribution,
|
||||
Qt::Alignment anchor,
|
||||
qreal scale)
|
||||
{
|
||||
if (attribution.isEmpty()) {
|
||||
return QRectF();
|
||||
}
|
||||
|
||||
painter.save();
|
||||
|
||||
QFont font = painter.font();
|
||||
font.setPointSizeF(qMax(6.0, font.pointSizeF() * scale));
|
||||
painter.setFont(font);
|
||||
|
||||
const QFontMetrics fm(font);
|
||||
const qreal maxTextWidth = rect.width() * 0.45;
|
||||
const QString elided = fm.elidedText(attribution, Qt::ElideRight, qMax(qreal(80.0) * scale, maxTextWidth));
|
||||
|
||||
const qreal pad = 6.0 * scale;
|
||||
QRectF captionRect(QPointF(0, 0), QSizeF(fm.horizontalAdvance(elided) + pad * 2.0, fm.height() + pad * 2.0));
|
||||
const qreal margin = 4.0 * scale;
|
||||
if (anchor.testFlag(Qt::AlignLeft)) {
|
||||
captionRect.moveLeft(rect.left() + margin);
|
||||
} else {
|
||||
captionRect.moveRight(rect.right() - margin);
|
||||
}
|
||||
if (anchor.testFlag(Qt::AlignTop)) {
|
||||
captionRect.moveTop(rect.top() + margin);
|
||||
} else {
|
||||
captionRect.moveBottom(rect.bottom() - margin);
|
||||
}
|
||||
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setBrush(QColor(0, 0, 0, 120));
|
||||
painter.drawRoundedRect(captionRect, 4, 4);
|
||||
|
||||
painter.setPen(QColor(255, 255, 255, 220));
|
||||
painter.drawText(captionRect, Qt::AlignCenter, elided);
|
||||
|
||||
painter.restore();
|
||||
|
||||
return captionRect;
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
#ifndef COCKATRICE_ART_CROP_ATTRIBUTION_H
|
||||
#define COCKATRICE_ART_CROP_ATTRIBUTION_H
|
||||
|
||||
#include <QStringView>
|
||||
|
||||
class ExactCard;
|
||||
class QPainter;
|
||||
class QRectF;
|
||||
class QString;
|
||||
|
||||
/**
|
||||
* @brief Builds an attribution caption for a cropped card art display.
|
||||
*
|
||||
* When a card image's art region is shown cropped (an "art crop"), the artist
|
||||
* should be credited in the same interface. Returns an empty string when the
|
||||
* database has no artist data for the card.
|
||||
*
|
||||
* @param card The card whose art is being displayed.
|
||||
* @return Caption such as "Art: John Avon", or empty.
|
||||
*/
|
||||
QString buildArtAttribution(const ExactCard &card);
|
||||
|
||||
/**
|
||||
* @brief Paints an attribution caption in a corner of a rect.
|
||||
*
|
||||
* Draws a subtle semi-transparent pill containing the caption, elided to fit.
|
||||
*
|
||||
* @param painter Painter to draw with.
|
||||
* @param rect The area (e.g. the cropped art region) the caption belongs to.
|
||||
* @param attribution Caption text (see buildArtAttribution()).
|
||||
* @param anchor Corner of @p rect to pin the pill to (default bottom-right).
|
||||
* @param scale Size multiplier for the pill (e.g. 0.8 for a smaller pill).
|
||||
* @return The rect the pill was drawn in, or an empty rect if @p attribution is empty.
|
||||
*/
|
||||
QRectF paintArtAttribution(QPainter &painter,
|
||||
const QRectF &rect,
|
||||
const QString &attribution,
|
||||
Qt::Alignment anchor = Qt::AlignRight | Qt::AlignBottom,
|
||||
qreal scale = 1.0);
|
||||
|
||||
#endif // COCKATRICE_ART_CROP_ATTRIBUTION_H
|
||||
|
|
@ -52,12 +52,29 @@ void CardInfoPictureEnlargedWidget::loadPixmap(const QSize &size)
|
|||
* @param size The desired size for the pixmap.
|
||||
*
|
||||
* Sets the widget's pixmap to the card image and resizes the widget to match the specified size. Triggers a repaint.
|
||||
*
|
||||
* When the image is not yet cached, the pixmap is cleared (instead of showing a stale previous card) and the widget
|
||||
* refreshes automatically once the card image finishes loading.
|
||||
*/
|
||||
void CardInfoPictureEnlargedWidget::setCardPixmap(const ExactCard &_card, const QSize size)
|
||||
{
|
||||
if (card.getCardPtr()) {
|
||||
disconnect(card.getCardPtr().data(), nullptr, this, nullptr);
|
||||
}
|
||||
|
||||
card = _card;
|
||||
|
||||
// Clear any previous card's art so we never paint a stale pixmap while the new image loads
|
||||
enlargedPixmap = QPixmap();
|
||||
loadPixmap(size);
|
||||
|
||||
if (card.getCardPtr()) {
|
||||
connect(card.getCardPtr().data(), &CardInfo::pixmapUpdated, this, [this]() {
|
||||
loadPixmap(this->size());
|
||||
update();
|
||||
});
|
||||
}
|
||||
|
||||
setFixedSize(size); // Set the widget size to the enlarged size
|
||||
|
||||
update(); // Trigger a repaint
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../../../client/settings/shortcuts_settings.h"
|
||||
#include "../playmat/playmat_settings_dialog.h"
|
||||
#include "../settings_page/user_interface_settings_page.h"
|
||||
#include "../tabs/api/commander_spellbook/commander_bracket_widget.h"
|
||||
#include "deck_list_style_proxy.h"
|
||||
|
|
@ -11,10 +12,12 @@
|
|||
#include <QDockWidget>
|
||||
#include <QHeaderView>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QSplitter>
|
||||
#include <QTextEdit>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/settings/deck_editor_settings.h>
|
||||
#include <libcockatrice/settings/interface_settings.h>
|
||||
#include <libcockatrice/utility/macros.h>
|
||||
#include <libcockatrice/utility/string_limits.h>
|
||||
|
||||
|
|
@ -228,10 +231,18 @@ void DeckEditorDeckDockWidget::createDeckDock()
|
|||
upperLayout->addWidget(bannerCardLabel, 4, 0);
|
||||
upperLayout->addWidget(bannerCardComboBox, 4, 1);
|
||||
|
||||
upperLayout->addWidget(deckTagsDisplayWidget, 5, 1);
|
||||
playmatLabel = new QLabel();
|
||||
playmatLabel->setObjectName("playmatLabel");
|
||||
playmatLabel->setText(tr("Playmat"));
|
||||
playmatSettingsButton = new QPushButton(tr("Edit Playmat..."));
|
||||
connect(playmatSettingsButton, &QPushButton::clicked, this, &DeckEditorDeckDockWidget::openPlaymatSettings);
|
||||
upperLayout->addWidget(playmatLabel, 5, 0);
|
||||
upperLayout->addWidget(playmatSettingsButton, 5, 1);
|
||||
|
||||
upperLayout->addWidget(activeGroupCriteriaLabel, 6, 0);
|
||||
upperLayout->addWidget(activeGroupCriteriaComboBox, 6, 1);
|
||||
upperLayout->addWidget(deckTagsDisplayWidget, 6, 1);
|
||||
|
||||
upperLayout->addWidget(activeGroupCriteriaLabel, 7, 0);
|
||||
upperLayout->addWidget(activeGroupCriteriaComboBox, 7, 1);
|
||||
|
||||
hashLabel1 = new QLabel();
|
||||
hashLabel1->setObjectName("hashLabel1");
|
||||
|
|
@ -440,6 +451,35 @@ void DeckEditorDeckDockWidget::writeBannerCard(int index)
|
|||
deckStateManager->setBannerCard(bannerCard);
|
||||
}
|
||||
|
||||
void DeckEditorDeckDockWidget::openPlaymatSettings()
|
||||
{
|
||||
PlaymatInfo current = deckStateManager->getMetadata().playmat;
|
||||
|
||||
PlaymatSettingsDialog dialog(current.card, current.params, this);
|
||||
if (dialog.exec() == QDialog::Accepted) {
|
||||
CardRef newCard = dialog.card();
|
||||
PlaymatParams newParams = dialog.params();
|
||||
|
||||
if (newCard.isEmpty()) {
|
||||
deckStateManager->setPlaymat(PlaymatInfo{});
|
||||
} else {
|
||||
deckStateManager->setPlaymat({newCard, newParams});
|
||||
}
|
||||
|
||||
updatePlaymatLabel();
|
||||
}
|
||||
}
|
||||
|
||||
void DeckEditorDeckDockWidget::updatePlaymatLabel()
|
||||
{
|
||||
CardRef playmat = deckStateManager->getMetadata().playmat.card;
|
||||
if (playmat.isEmpty()) {
|
||||
playmatSettingsButton->setText(tr("Edit Playmat..."));
|
||||
} else {
|
||||
playmatSettingsButton->setText(tr("Edit Playmat (%1)").arg(playmat.name));
|
||||
}
|
||||
}
|
||||
|
||||
void DeckEditorDeckDockWidget::applyActiveGroupCriteria()
|
||||
{
|
||||
getModel()->setActiveGroupCriteria(
|
||||
|
|
@ -497,6 +537,7 @@ void DeckEditorDeckDockWidget::syncDisplayWidgetsToModel()
|
|||
syncBannerCardComboBoxSelectionWithDeck();
|
||||
updateBannerCardComboBox();
|
||||
bannerCardComboBox->blockSignals(false);
|
||||
updatePlaymatLabel();
|
||||
updateHash();
|
||||
|
||||
formatComboBox->blockSignals(true);
|
||||
|
|
|
|||
|
|
@ -15,11 +15,15 @@
|
|||
#include "deck_list_history_manager_widget.h"
|
||||
#include "deck_list_style_proxy.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QDockWidget>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QTextEdit>
|
||||
#include <QTreeView>
|
||||
#include <libcockatrice/card/card_info.h>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
|
||||
class CommanderBracketWidget;
|
||||
class DeckListModel;
|
||||
|
|
@ -33,6 +37,8 @@ public:
|
|||
DeckListStyleProxy *proxy;
|
||||
QTreeView *deckView;
|
||||
QComboBox *bannerCardComboBox;
|
||||
QLabel *playmatLabel;
|
||||
QPushButton *playmatSettingsButton;
|
||||
void createDeckDock();
|
||||
ExactCard getCurrentCard();
|
||||
void retranslateUi();
|
||||
|
|
@ -102,6 +108,8 @@ private slots:
|
|||
void writeName();
|
||||
void writeComments();
|
||||
void writeBannerCard(int);
|
||||
void openPlaymatSettings();
|
||||
void updatePlaymatLabel();
|
||||
void applyActiveGroupCriteria();
|
||||
void setSelectedIndex(const QModelIndex &newCardIndex, bool preserveWidgetFocus);
|
||||
void updateHash();
|
||||
|
|
|
|||
|
|
@ -142,6 +142,19 @@ void DeckStateManager::setBannerCard(const CardRef &bannerCard)
|
|||
doMetadataModified();
|
||||
}
|
||||
|
||||
void DeckStateManager::setPlaymat(const PlaymatInfo &playmat)
|
||||
{
|
||||
PlaymatInfo previous = deckList->getPlaymat();
|
||||
if (previous == playmat) {
|
||||
return;
|
||||
}
|
||||
|
||||
requestHistorySave(tr("Set playmat to %1").arg(playmat.card.name));
|
||||
deckList->setPlaymat(playmat);
|
||||
|
||||
doMetadataModified();
|
||||
}
|
||||
|
||||
void DeckStateManager::setTags(const QStringList &tags)
|
||||
{
|
||||
QStringList previous = deckList->getTags();
|
||||
|
|
|
|||
|
|
@ -171,6 +171,7 @@ public:
|
|||
void setName(const QString &name);
|
||||
void setComments(const QString &comments);
|
||||
void setBannerCard(const CardRef &bannerCard);
|
||||
void setPlaymat(const PlaymatInfo &playmat);
|
||||
void setTags(const QStringList &tags);
|
||||
void setFormat(const QString &format);
|
||||
///@}
|
||||
|
|
|
|||
|
|
@ -214,6 +214,7 @@ DlgCreateGame::DlgCreateGame(const ServerInfo_Game &gameInfo, const QMap<int, QS
|
|||
spectatorsNeedPasswordCheckBox->setChecked(gameInfo.spectators_need_password());
|
||||
spectatorsCanTalkCheckBox->setChecked(gameInfo.spectators_can_chat());
|
||||
spectatorsSeeEverythingCheckBox->setChecked(gameInfo.spectators_omniscient());
|
||||
shareDecklistsOnLoadCheckBox->setChecked(gameInfo.share_decklists_on_load());
|
||||
|
||||
QSet<int> types;
|
||||
for (int i = 0; i < gameInfo.game_types_size(); ++i) {
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ DlgFilterGames::DlgFilterGames(const QMap<int, QString> &_allGameTypes,
|
|||
hideFullGames = new QCheckBox(tr("Hide full games"));
|
||||
hideFullGames->setChecked(filters.hideFullGames);
|
||||
|
||||
hideGamesThatStarted = new QCheckBox(tr("Hide games that have started"));
|
||||
hideGamesThatStarted = new QCheckBox(tr("Hide started games"));
|
||||
hideGamesThatStarted->setChecked(filters.hideGamesThatStarted);
|
||||
|
||||
hidePasswordProtectedGames = new QCheckBox(tr("Hide password protected games"));
|
||||
|
|
@ -57,16 +57,16 @@ DlgFilterGames::DlgFilterGames(const QMap<int, QString> &_allGameTypes,
|
|||
gameNameFilterEdit->setText(filters.gameNameFilter);
|
||||
auto *gameNameFilterLabel = new QLabel(tr("Game &description:"));
|
||||
gameNameFilterLabel->setBuddy(gameNameFilterEdit);
|
||||
creatorNameFilterEdit = new QLineEdit;
|
||||
creatorNameFilterEdit->setText(filters.creatorNameFilters.join(", "));
|
||||
auto *creatorNameFilterLabel = new QLabel(tr("&Creator name:"));
|
||||
creatorNameFilterLabel->setBuddy(creatorNameFilterEdit);
|
||||
hostNameFilterEdit = new QLineEdit;
|
||||
hostNameFilterEdit->setText(filters.hostNameFilters.join(", "));
|
||||
auto *hostNameFilterLabel = new QLabel(tr("&Host name:"));
|
||||
hostNameFilterLabel->setBuddy(hostNameFilterEdit);
|
||||
|
||||
auto *generalGrid = new QGridLayout;
|
||||
generalGrid->addWidget(gameNameFilterLabel, 0, 0);
|
||||
generalGrid->addWidget(gameNameFilterEdit, 0, 1);
|
||||
generalGrid->addWidget(creatorNameFilterLabel, 1, 0);
|
||||
generalGrid->addWidget(creatorNameFilterEdit, 1, 1);
|
||||
generalGrid->addWidget(hostNameFilterLabel, 1, 0);
|
||||
generalGrid->addWidget(hostNameFilterEdit, 1, 1);
|
||||
generalGrid->addWidget(maxGameAgeLabel, 2, 0);
|
||||
generalGrid->addWidget(maxGameAgeComboBox, 2, 1);
|
||||
generalGroupBox = new QGroupBox(tr("General"));
|
||||
|
|
@ -193,7 +193,7 @@ GameFilterConfigs DlgFilterGames::getFilters() const
|
|||
hideNotBuddyCreatedGames->isChecked(),
|
||||
hideOpenDecklistGames->isChecked(),
|
||||
gameNameFilterEdit->text(),
|
||||
getCreatorNameFilters(),
|
||||
getHostNameFilters(),
|
||||
getGameTypeFilter(),
|
||||
maxPlayersFilterMinSpinBox->value(),
|
||||
maxPlayersFilterMaxSpinBox->value(),
|
||||
|
|
@ -216,9 +216,9 @@ void DlgFilterGames::toggleSpectatorCheckboxEnabledness(bool spectatorsEnabled)
|
|||
showOnlyIfSpectatorsCanSeeHands->setDisabled(!spectatorsEnabled);
|
||||
}
|
||||
|
||||
QStringList DlgFilterGames::getCreatorNameFilters() const
|
||||
QStringList DlgFilterGames::getHostNameFilters() const
|
||||
{
|
||||
return creatorNameFilterEdit->text().split(",", Qt::SkipEmptyParts);
|
||||
return hostNameFilterEdit->text().split(",", Qt::SkipEmptyParts);
|
||||
}
|
||||
|
||||
QSet<int> DlgFilterGames::getGameTypeFilter() const
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ private:
|
|||
QCheckBox *hideNotBuddyCreatedGames;
|
||||
QCheckBox *hideOpenDecklistGames;
|
||||
QLineEdit *gameNameFilterEdit;
|
||||
QLineEdit *creatorNameFilterEdit;
|
||||
QLineEdit *hostNameFilterEdit;
|
||||
QMap<int, QCheckBox *> gameTypeFilterCheckBoxes;
|
||||
QSpinBox *maxPlayersFilterMinSpinBox;
|
||||
QSpinBox *maxPlayersFilterMaxSpinBox;
|
||||
|
|
@ -50,7 +50,7 @@ private:
|
|||
const GamesProxyModel *gamesProxyModel;
|
||||
const QMap<QTime, QString> gameAgeMap;
|
||||
|
||||
[[nodiscard]] QStringList getCreatorNameFilters() const;
|
||||
[[nodiscard]] QStringList getHostNameFilters() const;
|
||||
[[nodiscard]] QSet<int> getGameTypeFilter() const;
|
||||
[[nodiscard]] QTime getMaxGameAge() const;
|
||||
[[nodiscard]] bool getShowSpectatorPasswordProtected() const;
|
||||
|
|
|
|||
112
cockatrice/src/interface/widgets/dialogs/dlg_invite_to_game.cpp
Normal file
112
cockatrice/src/interface/widgets/dialogs/dlg_invite_to_game.cpp
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
#include "dlg_invite_to_game.h"
|
||||
|
||||
#include "../server/user/user_list_manager.h"
|
||||
#include "../server/user/user_list_widget.h"
|
||||
#include "../tabs/tab_supervisor.h"
|
||||
|
||||
#include <QGuiApplication>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QScreen>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
DlgInviteToGame::DlgInviteToGame(TabSupervisor *_tabSupervisor,
|
||||
const QString &_inviteUrl,
|
||||
bool _onlyBuddies,
|
||||
const QStringList &_excludeUserNames,
|
||||
QWidget *parent)
|
||||
: QDialog(parent), tabSupervisor(_tabSupervisor), inviteUrl(_inviteUrl), onlyBuddies(_onlyBuddies),
|
||||
excludeUserNames(_excludeUserNames)
|
||||
{
|
||||
setModal(true);
|
||||
|
||||
searchEdit = new QLineEdit(this);
|
||||
searchEdit->setClearButtonEnabled(true);
|
||||
connect(searchEdit, &QLineEdit::textChanged, this, &DlgInviteToGame::searchTextChanged);
|
||||
|
||||
// The embedded list is the real room user list without the hover popup:
|
||||
// same manager, same delegate/painter, same sections, live via manager
|
||||
// signals while the modal loop runs.
|
||||
UserListManager *manager = tabSupervisor->getUserListManager();
|
||||
userList = new UserListWidget(tabSupervisor, tabSupervisor->getClient(), UserListWidget::RoomList, this,
|
||||
/*hasUserInfoPopup=*/false);
|
||||
userList->setUserFilter([this, manager](const QString &name, bool online) {
|
||||
return !excludeUserNames.contains(name) && online && !manager->isUserIgnored(name);
|
||||
});
|
||||
if (onlyBuddies) {
|
||||
userList->setSectioned({UserListWidget::Section::Buddy});
|
||||
} else {
|
||||
userList->setSectioned({UserListWidget::Section::Buddy, UserListWidget::Section::Online});
|
||||
}
|
||||
userList->bind(manager);
|
||||
userList->rebuild();
|
||||
|
||||
connect(userList, &UserListWidget::userActivated, this, &DlgInviteToGame::inviteCurrentUser);
|
||||
connect(userList, &UserListWidget::currentUserChanged, this, [this](const QString &userName) {
|
||||
currentUserName = userName;
|
||||
inviteButton->setEnabled(!userName.isEmpty());
|
||||
});
|
||||
|
||||
inviteButton = new QPushButton(this);
|
||||
inviteButton->setEnabled(false);
|
||||
inviteButton->setDefault(true);
|
||||
connect(inviteButton, &QPushButton::clicked, this, [this] { inviteCurrentUser(currentUserName); });
|
||||
|
||||
cancelButton = new QPushButton(this);
|
||||
connect(cancelButton, &QPushButton::clicked, this, &QDialog::reject);
|
||||
|
||||
auto *buttonRow = new QHBoxLayout;
|
||||
buttonRow->addStretch();
|
||||
buttonRow->addWidget(inviteButton);
|
||||
buttonRow->addWidget(cancelButton);
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->addWidget(searchEdit);
|
||||
layout->addWidget(userList, 1);
|
||||
layout->addLayout(buttonRow);
|
||||
|
||||
retranslateUi();
|
||||
|
||||
// Default to a comfortably tall dialog so the list has room to breathe,
|
||||
// capped by the available screen. No minimum is enforced: small screens
|
||||
// and manual resizing can go shorter than this.
|
||||
const QRect availableScreen = QGuiApplication::primaryScreen()->availableGeometry();
|
||||
resize(sizeHint().width(), qMin(sizeHint().height() * 3, availableScreen.height() * 4 / 5));
|
||||
}
|
||||
|
||||
void DlgInviteToGame::searchTextChanged(const QString &text)
|
||||
{
|
||||
userList->setFilterText(text);
|
||||
}
|
||||
|
||||
void DlgInviteToGame::inviteCurrentUser(const QString &userName)
|
||||
{
|
||||
if (userName.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
// The invite link carries the game's id and, when the game has one, its
|
||||
// description (makeGameJoinLink embeds both). Read them back so the prefix
|
||||
// names the game by description first, then its id — identical to the
|
||||
// context-menu invite so recipients see one consistent message style.
|
||||
const QUrl inviteUrlObj(inviteUrl);
|
||||
const QUrlQuery inviteQuery(inviteUrlObj);
|
||||
const int gameId = inviteQuery.queryItemValue("gameid").toInt();
|
||||
const QString gameDescription = inviteQuery.queryItemValue("game");
|
||||
const QString prefix = gameDescription.isEmpty()
|
||||
? tr("Join my game (#%1):").arg(gameId)
|
||||
: tr("Join my game \"%1\" (#%2):").arg(gameDescription).arg(gameId);
|
||||
tabSupervisor->sendInviteToUser(userName, prefix + " " + inviteUrl);
|
||||
accept();
|
||||
}
|
||||
|
||||
void DlgInviteToGame::retranslateUi()
|
||||
{
|
||||
setWindowTitle(tr("Invite to Game"));
|
||||
searchEdit->setPlaceholderText(tr("Search users..."));
|
||||
inviteButton->setText(tr("Invite"));
|
||||
cancelButton->setText(tr("Cancel"));
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
/**
|
||||
* @file dlg_invite_to_game.h
|
||||
* @ingroup RoomDialogs
|
||||
*/
|
||||
//! \todo Document this file.
|
||||
|
||||
#ifndef DLG_INVITE_TO_GAME_H
|
||||
#define DLG_INVITE_TO_GAME_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QStringList>
|
||||
|
||||
class QLineEdit;
|
||||
class QPushButton;
|
||||
class TabSupervisor;
|
||||
class UserListWidget;
|
||||
|
||||
class DlgInviteToGame : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
DlgInviteToGame(TabSupervisor *_tabSupervisor,
|
||||
const QString &_inviteUrl,
|
||||
bool _onlyBuddies,
|
||||
const QStringList &_excludeUserNames,
|
||||
QWidget *parent = nullptr);
|
||||
|
||||
private slots:
|
||||
void searchTextChanged(const QString &text);
|
||||
void inviteCurrentUser(const QString &userName);
|
||||
|
||||
private:
|
||||
TabSupervisor *tabSupervisor;
|
||||
QString inviteUrl;
|
||||
bool onlyBuddies;
|
||||
QStringList excludeUserNames;
|
||||
QString currentUserName;
|
||||
QLineEdit *searchEdit;
|
||||
UserListWidget *userList;
|
||||
QPushButton *inviteButton;
|
||||
QPushButton *cancelButton;
|
||||
|
||||
void retranslateUi();
|
||||
};
|
||||
|
||||
#endif
|
||||
297
cockatrice/src/interface/widgets/dialogs/dlg_my_reports.cpp
Normal file
297
cockatrice/src/interface/widgets/dialogs/dlg_my_reports.cpp
Normal file
|
|
@ -0,0 +1,297 @@
|
|||
#include "dlg_my_reports.h"
|
||||
|
||||
#include "../utility/report_utils.h"
|
||||
#include "abstract_client.h"
|
||||
|
||||
#include <QFontDatabase>
|
||||
#include <QGroupBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHeaderView>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QSplitter>
|
||||
#include <QTableWidget>
|
||||
#include <QTextEdit>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/protocol/pb/command_report_add_comment.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_report_details.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_report_my_list.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_report_details.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_report_my_list.pb.h>
|
||||
#include <libcockatrice/protocol/pending_command.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr int COL_ID = 0;
|
||||
constexpr int COL_TIME = 1;
|
||||
constexpr int COL_REPORTED = 2;
|
||||
constexpr int COL_CATEGORY = 3;
|
||||
constexpr int COL_GAMEID = 4;
|
||||
constexpr int COL_STATUS = 5;
|
||||
constexpr int COL_ASSIGNED = 6;
|
||||
constexpr int COL_COUNT = 7;
|
||||
} // namespace
|
||||
|
||||
DlgMyReports::DlgMyReports(AbstractClient *_client, QWidget *parent)
|
||||
: QDialog(parent), client(_client), selectedReportId(-1)
|
||||
{
|
||||
setWindowTitle(tr("My Reports"));
|
||||
setMinimumSize(800, 500);
|
||||
|
||||
table = new QTableWidget(0, COL_COUNT);
|
||||
table->setHorizontalHeaderLabels(
|
||||
{tr("#"), tr("Time"), tr("Reported User"), tr("Category"), tr("Game ID"), tr("Status"), tr("Assigned To")});
|
||||
table->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
table->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
table->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
table->setSortingEnabled(true);
|
||||
table->verticalHeader()->setVisible(false);
|
||||
table->setAlternatingRowColors(true);
|
||||
table->horizontalHeader()->setSectionResizeMode(COL_TIME, QHeaderView::ResizeToContents);
|
||||
table->horizontalHeader()->setSectionResizeMode(COL_REPORTED, QHeaderView::Stretch);
|
||||
connect(table, &QTableWidget::itemSelectionChanged, this, &DlgMyReports::onSelectionChanged);
|
||||
|
||||
auto *detailsGroup = new QGroupBox(tr("Report Details"));
|
||||
descriptionEdit = new QTextEdit;
|
||||
descriptionEdit->setReadOnly(true);
|
||||
descriptionEdit->setFixedHeight(80);
|
||||
|
||||
auto *chatGroup = new QGroupBox(tr("Chat Log Context"));
|
||||
chatLogEdit = new QTextEdit;
|
||||
chatLogEdit->setReadOnly(true);
|
||||
QFont monoFont("monospace");
|
||||
monoFont.setStyleHint(QFont::Monospace);
|
||||
const int systemPointSize = QFontDatabase::systemFont(QFontDatabase::GeneralFont).pointSize();
|
||||
if (systemPointSize > 0) {
|
||||
monoFont.setPointSize(systemPointSize);
|
||||
}
|
||||
chatLogEdit->setFont(monoFont);
|
||||
auto *chatLayout = new QVBoxLayout(chatGroup);
|
||||
chatLayout->setContentsMargins(4, 4, 4, 4);
|
||||
chatLayout->addWidget(chatLogEdit);
|
||||
|
||||
auto *commentsLabel = new QLabel(tr("Comments:"));
|
||||
commentsEdit = new QTextEdit;
|
||||
commentsEdit->setReadOnly(true);
|
||||
commentsEdit->setFixedHeight(120);
|
||||
|
||||
auto *addCommentLabel = new QLabel(tr("Add a comment:"));
|
||||
commentInput = new QLineEdit;
|
||||
commentInput->setPlaceholderText(tr("Type your comment here..."));
|
||||
commentButton = new QPushButton(tr("Send"));
|
||||
commentButton->setEnabled(false);
|
||||
connect(commentButton, &QPushButton::clicked, this, &DlgMyReports::addComment);
|
||||
connect(commentInput, &QLineEdit::returnPressed, this, &DlgMyReports::addComment);
|
||||
|
||||
auto *detailsLayout = new QVBoxLayout(detailsGroup);
|
||||
detailsLayout->setContentsMargins(4, 4, 4, 4);
|
||||
detailsLayout->addWidget(descriptionEdit);
|
||||
detailsLayout->addWidget(chatGroup);
|
||||
detailsLayout->addWidget(commentsLabel);
|
||||
detailsLayout->addWidget(commentsEdit);
|
||||
detailsLayout->addWidget(addCommentLabel);
|
||||
auto *commentRow = new QHBoxLayout;
|
||||
commentRow->addWidget(commentInput);
|
||||
commentRow->addWidget(commentButton);
|
||||
detailsLayout->addLayout(commentRow);
|
||||
|
||||
closeButton = new QPushButton(tr("Close"));
|
||||
connect(closeButton, &QPushButton::clicked, this, &QDialog::accept);
|
||||
|
||||
refreshButton = new QPushButton(tr("Refresh"));
|
||||
connect(refreshButton, &QPushButton::clicked, this, &DlgMyReports::refreshList);
|
||||
|
||||
statusLabel = new QLabel;
|
||||
|
||||
auto *bottomBar = new QHBoxLayout;
|
||||
bottomBar->addWidget(statusLabel);
|
||||
bottomBar->addStretch();
|
||||
bottomBar->addWidget(refreshButton);
|
||||
bottomBar->addWidget(closeButton);
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->addWidget(table, 1);
|
||||
layout->addWidget(detailsGroup);
|
||||
layout->addLayout(bottomBar);
|
||||
|
||||
setActionsEnabled(false);
|
||||
refreshList();
|
||||
}
|
||||
|
||||
void DlgMyReports::refreshList()
|
||||
{
|
||||
selectedReportIdBeforeRefresh = selectedReportId;
|
||||
commentDraftBeforeRefresh = commentInput->text();
|
||||
statusLabel->setText(tr("Loading..."));
|
||||
refreshButton->setEnabled(false);
|
||||
table->setRowCount(0);
|
||||
currentReports.clear();
|
||||
descriptionEdit->clear();
|
||||
chatLogEdit->clear();
|
||||
commentsEdit->clear();
|
||||
commentInput->clear();
|
||||
setActionsEnabled(false);
|
||||
selectedReportId = -1;
|
||||
|
||||
Command_ReportMyList cmd;
|
||||
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &DlgMyReports::reportListResponse);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void DlgMyReports::reportListResponse(const Response &response)
|
||||
{
|
||||
refreshButton->setEnabled(true);
|
||||
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
statusLabel->setText(tr("Failed to load reports."));
|
||||
return;
|
||||
}
|
||||
|
||||
const Response_ReportMyList &resp = response.GetExtension(Response_ReportMyList::ext);
|
||||
currentReports.clear();
|
||||
for (int i = 0; i < resp.reports_size(); ++i) {
|
||||
currentReports.append(resp.reports(i));
|
||||
}
|
||||
|
||||
table->setSortingEnabled(false);
|
||||
table->setRowCount(currentReports.size());
|
||||
|
||||
for (int row = 0; row < currentReports.size(); ++row) {
|
||||
const ServerInfo_Report &r = currentReports[row];
|
||||
|
||||
report_utils::fillReportTableRow(table, row, r, COL_ID, COL_TIME, COL_REPORTED, COL_CATEGORY, COL_GAMEID,
|
||||
COL_STATUS, COL_ASSIGNED);
|
||||
}
|
||||
|
||||
table->setSortingEnabled(true);
|
||||
table->resizeColumnsToContents();
|
||||
table->horizontalHeader()->setSectionResizeMode(COL_REPORTED, QHeaderView::Stretch);
|
||||
|
||||
if (selectedReportIdBeforeRefresh >= 0) {
|
||||
for (int row = 0; row < table->rowCount(); ++row) {
|
||||
if (table->item(row, COL_ID) &&
|
||||
table->item(row, COL_ID)->data(Qt::UserRole).toInt() == selectedReportIdBeforeRefresh) {
|
||||
table->setCurrentCell(row, 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (commentInput->text().isEmpty()) {
|
||||
commentInput->setText(commentDraftBeforeRefresh);
|
||||
}
|
||||
|
||||
statusLabel->setText(tr("%1 report(s)").arg(currentReports.size()));
|
||||
}
|
||||
|
||||
void DlgMyReports::onSelectionChanged()
|
||||
{
|
||||
const int row = table->currentRow();
|
||||
if (row < 0 || !table->item(row, COL_ID)) {
|
||||
descriptionEdit->clear();
|
||||
chatLogEdit->clear();
|
||||
commentsEdit->clear();
|
||||
commentInput->clear();
|
||||
commentButton->setEnabled(false);
|
||||
selectedReportId = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
const int reportId = table->item(row, COL_ID)->data(Qt::UserRole).toInt();
|
||||
selectedReportId = reportId;
|
||||
|
||||
for (const ServerInfo_Report &r : currentReports) {
|
||||
if (r.report_id() == reportId) {
|
||||
descriptionEdit->setPlainText(QString::fromStdString(r.description()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
chatLogEdit->setPlainText(tr("Loading..."));
|
||||
commentsEdit->setPlainText(tr("Loading..."));
|
||||
|
||||
Command_ReportDetails cmd;
|
||||
cmd.set_report_id(reportId);
|
||||
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &DlgMyReports::reportDetailsResponse);
|
||||
client->sendCommand(pend);
|
||||
|
||||
QString status = table->item(row, COL_STATUS)->text();
|
||||
bool canComment = (status == "open" || status == "assigned");
|
||||
commentButton->setEnabled(canComment);
|
||||
commentInput->setEnabled(canComment);
|
||||
if (!canComment) {
|
||||
commentInput->setPlaceholderText(tr("This report is closed."));
|
||||
} else {
|
||||
commentInput->setPlaceholderText(tr("Type your comment here..."));
|
||||
}
|
||||
}
|
||||
|
||||
void DlgMyReports::reportDetailsResponse(const Response &response)
|
||||
{
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
if (selectedReportId == -1) {
|
||||
return;
|
||||
}
|
||||
chatLogEdit->clear();
|
||||
commentsEdit->setPlainText(tr("Failed to load report details."));
|
||||
return;
|
||||
}
|
||||
|
||||
const Response_ReportDetails &resp = response.GetExtension(Response_ReportDetails::ext);
|
||||
const ServerInfo_Report &r = resp.report();
|
||||
|
||||
if (selectedReportId != r.report_id()) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadReportDetails(r);
|
||||
}
|
||||
|
||||
void DlgMyReports::loadReportDetails(const ServerInfo_Report &report)
|
||||
{
|
||||
report_utils::renderReportDetails(chatLogEdit, commentsEdit, report, tr("No comments yet."), tr("[Moderator]"),
|
||||
tr("[You]"));
|
||||
}
|
||||
|
||||
void DlgMyReports::addComment()
|
||||
{
|
||||
if (selectedReportId < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString text = commentInput->text().trimmed();
|
||||
if (text.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
commentButton->setEnabled(false);
|
||||
|
||||
Command_ReportAddComment cmd;
|
||||
cmd.set_report_id(selectedReportId);
|
||||
cmd.set_comment(text.toStdString());
|
||||
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &DlgMyReports::addCommentResponse);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void DlgMyReports::addCommentResponse(const Response &response)
|
||||
{
|
||||
if (response.response_code() == Response::RespOk) {
|
||||
commentInput->clear();
|
||||
refreshList();
|
||||
} else {
|
||||
commentButton->setEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
void DlgMyReports::setActionsEnabled(bool enabled)
|
||||
{
|
||||
commentButton->setEnabled(enabled);
|
||||
commentInput->setEnabled(enabled);
|
||||
}
|
||||
52
cockatrice/src/interface/widgets/dialogs/dlg_my_reports.h
Normal file
52
cockatrice/src/interface/widgets/dialogs/dlg_my_reports.h
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
#ifndef COCKATRICE_DLG_MY_REPORTS_H
|
||||
#define COCKATRICE_DLG_MY_REPORTS_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QList>
|
||||
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_report.pb.h>
|
||||
|
||||
class AbstractClient;
|
||||
class QTableWidget;
|
||||
class QTextEdit;
|
||||
class QLineEdit;
|
||||
class QPushButton;
|
||||
class QLabel;
|
||||
|
||||
class DlgMyReports : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit DlgMyReports(AbstractClient *_client, QWidget *parent = nullptr);
|
||||
|
||||
private slots:
|
||||
void refreshList();
|
||||
void reportListResponse(const Response &response);
|
||||
void onSelectionChanged();
|
||||
void reportDetailsResponse(const Response &response);
|
||||
void addComment();
|
||||
void addCommentResponse(const Response &response);
|
||||
|
||||
private:
|
||||
void loadReportDetails(const ServerInfo_Report &report);
|
||||
void setActionsEnabled(bool enabled);
|
||||
|
||||
AbstractClient *client;
|
||||
|
||||
QTableWidget *table;
|
||||
QTextEdit *descriptionEdit;
|
||||
QTextEdit *chatLogEdit;
|
||||
QTextEdit *commentsEdit;
|
||||
QLineEdit *commentInput;
|
||||
QPushButton *commentButton;
|
||||
QPushButton *refreshButton;
|
||||
QPushButton *closeButton;
|
||||
QLabel *statusLabel;
|
||||
|
||||
QList<ServerInfo_Report> currentReports;
|
||||
int selectedReportId;
|
||||
int selectedReportIdBeforeRefresh = -1;
|
||||
QString commentDraftBeforeRefresh;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_DLG_MY_REPORTS_H
|
||||
|
|
@ -1,18 +1,62 @@
|
|||
#include "dlg_register.h"
|
||||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../server/handle_public_servers.h"
|
||||
#include "../server/user/user_info_connection.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QGridLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QRadioButton>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/settings/servers_settings.h>
|
||||
#include <libcockatrice/utility/string_limits.h>
|
||||
|
||||
DlgRegister::DlgRegister(QWidget *parent) : QDialog(parent)
|
||||
{
|
||||
// ── Server picker ──────────────────────────────────────────────────
|
||||
previousHostButton = new QRadioButton(tr("Known Hosts"), this);
|
||||
previousHosts = new QComboBox(this);
|
||||
|
||||
btnDeleteServer = new QPushButton(this);
|
||||
btnDeleteServer->setIcon(QPixmap("theme:icons/remove_row"));
|
||||
btnDeleteServer->setToolTip(tr("Delete the currently selected saved server"));
|
||||
btnDeleteServer->setFixedWidth(30);
|
||||
|
||||
connect(btnDeleteServer, &QPushButton::clicked, this, &DlgRegister::actRemoveSavedServer);
|
||||
|
||||
hps = new HandlePublicServers(this);
|
||||
btnRefreshServers = new QPushButton(this);
|
||||
btnRefreshServers->setIcon(QPixmap("theme:icons/sync"));
|
||||
btnRefreshServers->setToolTip(tr("Refresh the server list with known public servers"));
|
||||
btnRefreshServers->setFixedWidth(30);
|
||||
|
||||
connect(hps, &HandlePublicServers::sigPublicServersDownloadedSuccessfully, this, [this] { rebuildComboBoxList(); });
|
||||
connect(hps, &HandlePublicServers::sigPublicServersDownloadedUnsuccessfully, this,
|
||||
&DlgRegister::rebuildComboBoxList);
|
||||
connect(btnRefreshServers, &QPushButton::released, this, &DlgRegister::downloadThePublicServers);
|
||||
|
||||
newHostButton = new QRadioButton(tr("New Host"), this);
|
||||
|
||||
auto *serverPickerRow = new QHBoxLayout;
|
||||
serverPickerRow->addWidget(previousHosts);
|
||||
serverPickerRow->addWidget(btnDeleteServer);
|
||||
serverPickerRow->addWidget(btnRefreshServers);
|
||||
|
||||
auto *serverGroupLayout = new QVBoxLayout;
|
||||
serverGroupLayout->addWidget(previousHostButton);
|
||||
serverGroupLayout->addLayout(serverPickerRow);
|
||||
serverGroupLayout->addWidget(newHostButton);
|
||||
|
||||
auto *serverGroupBox = new QGroupBox(tr("Server"));
|
||||
serverGroupBox->setLayout(serverGroupLayout);
|
||||
|
||||
// ── Registration fields ────────────────────────────────────────────
|
||||
ServersSettings &servers = SettingsCache::instance().servers();
|
||||
infoLabel = new QLabel(tr("Enter your information and the information of the server you'd like to register to.\n"
|
||||
"Your email will be used to verify your account."));
|
||||
|
|
@ -321,26 +365,28 @@ DlgRegister::DlgRegister(QWidget *parent) : QDialog(parent)
|
|||
realnameEdit->setMaxLength(MAX_NAME_LENGTH);
|
||||
realnameLabel->setBuddy(realnameEdit);
|
||||
|
||||
// ── Layout ─────────────────────────────────────────────────────────
|
||||
auto *grid = new QGridLayout;
|
||||
grid->addWidget(infoLabel, 0, 0, 1, 2);
|
||||
grid->addWidget(hostLabel, 1, 0);
|
||||
grid->addWidget(hostEdit, 1, 1);
|
||||
grid->addWidget(portLabel, 2, 0);
|
||||
grid->addWidget(portEdit, 2, 1);
|
||||
grid->addWidget(playernameLabel, 3, 0);
|
||||
grid->addWidget(playernameEdit, 3, 1);
|
||||
grid->addWidget(passwordLabel, 4, 0);
|
||||
grid->addWidget(passwordEdit, 4, 1);
|
||||
grid->addWidget(passwordConfirmationLabel, 5, 0);
|
||||
grid->addWidget(passwordConfirmationEdit, 5, 1);
|
||||
grid->addWidget(emailLabel, 6, 0);
|
||||
grid->addWidget(emailEdit, 6, 1);
|
||||
grid->addWidget(emailConfirmationLabel, 7, 0);
|
||||
grid->addWidget(emailConfirmationEdit, 7, 1);
|
||||
grid->addWidget(countryLabel, 9, 0);
|
||||
grid->addWidget(countryEdit, 9, 1);
|
||||
grid->addWidget(realnameLabel, 10, 0);
|
||||
grid->addWidget(realnameEdit, 10, 1);
|
||||
grid->addWidget(serverGroupBox, 0, 0, 1, 2);
|
||||
grid->addWidget(infoLabel, 1, 0, 1, 2);
|
||||
grid->addWidget(hostLabel, 2, 0);
|
||||
grid->addWidget(hostEdit, 2, 1);
|
||||
grid->addWidget(portLabel, 3, 0);
|
||||
grid->addWidget(portEdit, 3, 1);
|
||||
grid->addWidget(playernameLabel, 4, 0);
|
||||
grid->addWidget(playernameEdit, 4, 1);
|
||||
grid->addWidget(passwordLabel, 5, 0);
|
||||
grid->addWidget(passwordEdit, 5, 1);
|
||||
grid->addWidget(passwordConfirmationLabel, 6, 0);
|
||||
grid->addWidget(passwordConfirmationEdit, 6, 1);
|
||||
grid->addWidget(emailLabel, 7, 0);
|
||||
grid->addWidget(emailEdit, 7, 1);
|
||||
grid->addWidget(emailConfirmationLabel, 8, 0);
|
||||
grid->addWidget(emailConfirmationEdit, 8, 1);
|
||||
grid->addWidget(countryLabel, 10, 0);
|
||||
grid->addWidget(countryEdit, 10, 1);
|
||||
grid->addWidget(realnameLabel, 11, 0);
|
||||
grid->addWidget(realnameEdit, 11, 1);
|
||||
|
||||
auto *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
connect(buttonBox, &QDialogButtonBox::accepted, this, &DlgRegister::actOk);
|
||||
|
|
@ -352,13 +398,115 @@ DlgRegister::DlgRegister(QWidget *parent) : QDialog(parent)
|
|||
setLayout(mainLayout);
|
||||
|
||||
setWindowTitle(tr("Register to server"));
|
||||
setFixedHeight(sizeHint().height());
|
||||
setMinimumWidth(300);
|
||||
setMinimumWidth(360);
|
||||
|
||||
connect(previousHostButton, &QRadioButton::toggled, this, &DlgRegister::previousHostSelected);
|
||||
connect(newHostButton, &QRadioButton::toggled, this, &DlgRegister::newHostSelected);
|
||||
connect(previousHosts, &QComboBox::currentTextChanged, this, &DlgRegister::updateDisplayInfo);
|
||||
|
||||
previousHostButton->setChecked(true);
|
||||
|
||||
preRebuildComboBoxList();
|
||||
}
|
||||
|
||||
DlgRegister::~DlgRegister() = default;
|
||||
|
||||
void DlgRegister::downloadThePublicServers()
|
||||
{
|
||||
btnRefreshServers->setDisabled(true);
|
||||
previousHosts->clear();
|
||||
previousHosts->addItem(placeHolderText);
|
||||
hps->downloadPublicServers();
|
||||
}
|
||||
|
||||
void DlgRegister::preRebuildComboBoxList()
|
||||
{
|
||||
UserConnection_Information uci;
|
||||
savedHostList = uci.getServerInfo();
|
||||
|
||||
if (savedHostList.size() == 1) {
|
||||
downloadThePublicServers();
|
||||
} else {
|
||||
rebuildComboBoxList();
|
||||
}
|
||||
}
|
||||
|
||||
void DlgRegister::rebuildComboBoxList(int failure)
|
||||
{
|
||||
Q_UNUSED(failure);
|
||||
|
||||
previousHosts->clear();
|
||||
|
||||
UserConnection_Information uci;
|
||||
savedHostList = uci.getServerInfo();
|
||||
|
||||
auto &servers = SettingsCache::instance().servers();
|
||||
QString previousHostName = servers.getPrevioushostName();
|
||||
|
||||
for (const auto &pair : savedHostList) {
|
||||
const auto &tmp = pair.second;
|
||||
QString saveName = tmp.getSaveName();
|
||||
if (saveName.size()) {
|
||||
previousHosts->addItem(saveName);
|
||||
if (saveName.compare(previousHostName) == 0) {
|
||||
previousHosts->setCurrentIndex(previousHosts->count() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
btnRefreshServers->setDisabled(false);
|
||||
}
|
||||
|
||||
void DlgRegister::previousHostSelected(bool state)
|
||||
{
|
||||
if (state) {
|
||||
previousHosts->setDisabled(false);
|
||||
btnRefreshServers->setDisabled(false);
|
||||
hostEdit->setDisabled(true);
|
||||
portEdit->setDisabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
void DlgRegister::newHostSelected(bool state)
|
||||
{
|
||||
if (state) {
|
||||
previousHosts->setDisabled(true);
|
||||
btnRefreshServers->setDisabled(true);
|
||||
hostEdit->setDisabled(false);
|
||||
hostEdit->clear();
|
||||
hostEdit->setPlaceholderText(tr("Server URL"));
|
||||
portEdit->setDisabled(false);
|
||||
portEdit->clear();
|
||||
portEdit->setPlaceholderText(tr("Communication Port"));
|
||||
playernameEdit->setDisabled(false);
|
||||
playernameEdit->clear();
|
||||
} else {
|
||||
// Rebuild the list so the previously selected host's details are
|
||||
// repopulated (mirrors DlgConnect::newHostSelected).
|
||||
preRebuildComboBoxList();
|
||||
}
|
||||
}
|
||||
|
||||
void DlgRegister::updateDisplayInfo(const QString &saveName)
|
||||
{
|
||||
if (saveName.isEmpty() || saveName == placeHolderText) {
|
||||
return;
|
||||
}
|
||||
|
||||
UserConnection_Information uci;
|
||||
QStringList _data = uci.getServerInfo(saveName);
|
||||
|
||||
if (_data.size() < 7) {
|
||||
return;
|
||||
}
|
||||
|
||||
hostEdit->setText(_data.at(1));
|
||||
portEdit->setText(_data.at(2));
|
||||
playernameEdit->setText(_data.at(3));
|
||||
}
|
||||
|
||||
void DlgRegister::actOk()
|
||||
{
|
||||
//! \todo This stuff should be using QValidators.
|
||||
if (passwordEdit->text().length() < 8) {
|
||||
QMessageBox::critical(this, tr("Registration Warning"), tr("Your password is too short."));
|
||||
return;
|
||||
|
|
@ -375,5 +523,29 @@ void DlgRegister::actOk()
|
|||
return;
|
||||
}
|
||||
|
||||
ServersSettings &servers = SettingsCache::instance().servers();
|
||||
|
||||
if (newHostButton->isChecked()) {
|
||||
// Persist the new host so it shows up in the Connect dialog later.
|
||||
// The password is never stored: the account is not verified yet.
|
||||
const QString host = hostEdit->text().trimmed();
|
||||
if (!host.isEmpty()) {
|
||||
servers.addNewServer(host, host, portEdit->text().trimmed(), playernameEdit->text().trimmed(), QString(),
|
||||
false);
|
||||
servers.setPrevioushostName(host);
|
||||
}
|
||||
} else {
|
||||
const QString saveName = previousHosts->currentText();
|
||||
if (!saveName.isEmpty() && saveName != placeHolderText) {
|
||||
servers.setPrevioushostName(saveName);
|
||||
}
|
||||
}
|
||||
|
||||
accept();
|
||||
}
|
||||
|
||||
void DlgRegister::actRemoveSavedServer()
|
||||
{
|
||||
SettingsCache::instance().servers().removeServer(hostEdit->text());
|
||||
previousHosts->removeItem(previousHosts->currentIndex());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,24 @@
|
|||
/**
|
||||
* @file dlg_register.h
|
||||
* @ingroup AccountDialogs
|
||||
*/
|
||||
//! \todo Document this file.
|
||||
|
||||
#ifndef DLG_REGISTER_H
|
||||
#define DLG_REGISTER_H
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QDialog>
|
||||
#include <QLineEdit>
|
||||
#include <QMap>
|
||||
|
||||
class HandlePublicServers;
|
||||
class QLabel;
|
||||
class QPushButton;
|
||||
class QCheckBox;
|
||||
class QRadioButton;
|
||||
class UserConnection_Information;
|
||||
|
||||
class DlgRegister : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit DlgRegister(QWidget *parent = nullptr);
|
||||
~DlgRegister() override;
|
||||
|
||||
[[nodiscard]] QString getHost() const
|
||||
{
|
||||
return hostEdit->text();
|
||||
|
|
@ -48,15 +47,35 @@ public:
|
|||
{
|
||||
return realnameEdit->text();
|
||||
}
|
||||
|
||||
public slots:
|
||||
void downloadThePublicServers();
|
||||
|
||||
private slots:
|
||||
void actOk();
|
||||
void previousHostSelected(bool state);
|
||||
void newHostSelected(bool state);
|
||||
void updateDisplayInfo(const QString &saveName);
|
||||
void preRebuildComboBoxList();
|
||||
void rebuildComboBoxList(int failure = -1);
|
||||
void actRemoveSavedServer();
|
||||
|
||||
private:
|
||||
QRadioButton *newHostButton;
|
||||
QRadioButton *previousHostButton;
|
||||
QComboBox *previousHosts;
|
||||
QPushButton *btnDeleteServer;
|
||||
QPushButton *btnRefreshServers;
|
||||
HandlePublicServers *hps;
|
||||
|
||||
QLabel *infoLabel, *hostLabel, *portLabel, *playernameLabel, *passwordLabel, *passwordConfirmationLabel,
|
||||
*emailLabel, *emailConfirmationLabel, *countryLabel, *realnameLabel;
|
||||
QLineEdit *hostEdit, *portEdit, *playernameEdit, *passwordEdit, *passwordConfirmationEdit, *emailEdit,
|
||||
*emailConfirmationEdit, *realnameEdit;
|
||||
QComboBox *countryEdit;
|
||||
|
||||
QMap<QString, std::pair<QString, UserConnection_Information>> savedHostList;
|
||||
const QString placeHolderText = tr("Downloading...");
|
||||
};
|
||||
|
||||
#endif
|
||||
#endif // DLG_REGISTER_H
|
||||
|
|
|
|||
191
cockatrice/src/interface/widgets/dialogs/dlg_report_user.cpp
Normal file
191
cockatrice/src/interface/widgets/dialogs/dlg_report_user.cpp
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
#include "dlg_report_user.h"
|
||||
|
||||
#include "abstract_client.h"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFontDatabase>
|
||||
#include <QGridLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QTextEdit>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/protocol/pb/command_report.pb.h>
|
||||
#include <libcockatrice/protocol/pending_command.h>
|
||||
|
||||
DlgReportUser::DlgReportUser(AbstractClient *_client,
|
||||
const QString &_reportedUser,
|
||||
int _gameId,
|
||||
const QString &_autoChatLog,
|
||||
QWidget *parent)
|
||||
: QDialog(parent), client(_client), reportedUser(_reportedUser), gameId(_gameId)
|
||||
{
|
||||
setWindowTitle(tr("Report User"));
|
||||
setMinimumWidth(500);
|
||||
|
||||
auto *infoLabel =
|
||||
new QLabel(tr("Reports are reviewed by moderators. False reports may result in account penalties."));
|
||||
infoLabel->setWordWrap(true);
|
||||
infoLabel->setStyleSheet("color: palette(placeholderText); padding: 5px;");
|
||||
|
||||
auto *reportGroup = new QGroupBox(tr("Report Details"));
|
||||
auto *reportGrid = new QGridLayout(reportGroup);
|
||||
|
||||
reportGrid->addWidget(new QLabel(tr("Reported User:")), 0, 0);
|
||||
reportedUserLabel = new QLabel(reportedUser);
|
||||
reportedUserLabel->setStyleSheet("font-weight: bold;");
|
||||
reportGrid->addWidget(reportedUserLabel, 0, 1);
|
||||
|
||||
reportGrid->addWidget(new QLabel(tr("Game ID:")), 1, 0);
|
||||
if (gameId >= 0) {
|
||||
gameIdLabel = new QLabel(QString::number(gameId));
|
||||
gameIdLabel->setStyleSheet("font-weight: bold;");
|
||||
reportGrid->addWidget(gameIdLabel, 1, 1);
|
||||
} else {
|
||||
gameIdEdit = new QLineEdit;
|
||||
gameIdEdit->setPlaceholderText(tr("(Optional) Enter game ID if available"));
|
||||
gameIdEdit->setToolTip(tr("If the report is related to a specific game, enter its ID."));
|
||||
reportGrid->addWidget(gameIdEdit, 1, 1);
|
||||
gameIdLabel = nullptr;
|
||||
}
|
||||
|
||||
auto *categoryGroup = new QGroupBox(tr("Category"));
|
||||
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);
|
||||
|
||||
categoryGrid->addWidget(new QLabel(tr("Category:")), 0, 0);
|
||||
categoryGrid->addWidget(categoryBox, 0, 1);
|
||||
|
||||
auto *descGroup = new QGroupBox(tr("Description"));
|
||||
auto *descLayout = new QVBoxLayout(descGroup);
|
||||
|
||||
descriptionEdit = new QTextEdit;
|
||||
descriptionEdit->setPlaceholderText(
|
||||
tr("Please describe what happened. Include dates, game details, or any evidence if available."));
|
||||
descriptionEdit->setFixedHeight(120);
|
||||
descLayout->addWidget(descriptionEdit);
|
||||
|
||||
auto *chatGroup = new QGroupBox(tr("Chat Log Context"));
|
||||
auto *chatLayout = new QVBoxLayout(chatGroup);
|
||||
chatLogEdit = new QTextEdit;
|
||||
chatLogEdit->setReadOnly(true);
|
||||
QFont monoFont("monospace");
|
||||
monoFont.setStyleHint(QFont::Monospace);
|
||||
const int systemPointSize = QFontDatabase::systemFont(QFontDatabase::GeneralFont).pointSize();
|
||||
if (systemPointSize > 0) {
|
||||
monoFont.setPointSize(systemPointSize);
|
||||
}
|
||||
chatLogEdit->setFont(monoFont);
|
||||
if (!_autoChatLog.isEmpty()) {
|
||||
chatLogEdit->setPlainText(_autoChatLog);
|
||||
} else {
|
||||
chatLogEdit->setPlaceholderText(tr("No chat context available (not triggered from chat)."));
|
||||
}
|
||||
chatLogEdit->setFixedHeight(100);
|
||||
chatLayout->addWidget(chatLogEdit);
|
||||
|
||||
auto *chatNote = new QLabel(
|
||||
tr("This chat log is captured from your local chat window and may not reflect the full conversation."));
|
||||
chatNote->setWordWrap(true);
|
||||
chatNote->setStyleSheet("color: palette(placeholderText);");
|
||||
chatLayout->addWidget(chatNote);
|
||||
|
||||
buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Submit Report"));
|
||||
connect(buttonBox, &QDialogButtonBox::accepted, this, &DlgReportUser::actSubmit);
|
||||
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->addWidget(infoLabel);
|
||||
layout->addWidget(reportGroup);
|
||||
layout->addWidget(categoryGroup);
|
||||
layout->addWidget(descGroup);
|
||||
layout->addWidget(chatGroup);
|
||||
layout->addWidget(buttonBox);
|
||||
}
|
||||
|
||||
void DlgReportUser::actSubmit()
|
||||
{
|
||||
const QString description = descriptionEdit->toPlainText().trimmed();
|
||||
if (description.isEmpty()) {
|
||||
QMessageBox::warning(this, tr("Missing description"), tr("Please describe what happened before submitting."));
|
||||
return;
|
||||
}
|
||||
|
||||
QMessageBox::StandardButton reply =
|
||||
QMessageBox::question(this, tr("Confirm Report"),
|
||||
tr("Submit report against %1 for %2?").arg(reportedUser, categoryBox->currentText()),
|
||||
QMessageBox::Yes | QMessageBox::No);
|
||||
|
||||
if (reply != QMessageBox::Yes) {
|
||||
return;
|
||||
}
|
||||
|
||||
buttonBox->setEnabled(false);
|
||||
buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Submitting..."));
|
||||
|
||||
Command_Report cmd;
|
||||
cmd.set_reported_user(reportedUser.toStdString());
|
||||
cmd.set_category(categoryBox->currentData().toString().toStdString());
|
||||
cmd.set_description(description.toStdString());
|
||||
|
||||
if (gameId >= 0) {
|
||||
cmd.set_game_id(gameId);
|
||||
} else if (gameIdEdit && !gameIdEdit->text().trimmed().isEmpty()) {
|
||||
bool ok;
|
||||
int manualGameId = gameIdEdit->text().trimmed().toInt(&ok);
|
||||
if (ok && manualGameId > 0) {
|
||||
cmd.set_game_id(manualGameId);
|
||||
}
|
||||
}
|
||||
|
||||
const QString chatLog = chatLogEdit->toPlainText().trimmed();
|
||||
if (!chatLog.isEmpty()) {
|
||||
cmd.set_chat_log(chatLog.toStdString());
|
||||
}
|
||||
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &DlgReportUser::reportResponse);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void DlgReportUser::reportResponse(const Response &response)
|
||||
{
|
||||
buttonBox->setEnabled(true);
|
||||
buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Submit Report"));
|
||||
|
||||
if (response.response_code() == Response::RespOk) {
|
||||
QMessageBox::information(this, tr("Report Submitted"),
|
||||
tr("Your report has been submitted and will be reviewed by a moderator. Thank you."));
|
||||
accept();
|
||||
} else if (response.response_code() == Response::RespTooManyRequests) {
|
||||
QMessageBox::warning(this, tr("Submission Failed"),
|
||||
tr("You have reached the daily report limit. Please try again later."));
|
||||
} else if (response.response_code() == Response::RespNameNotFound) {
|
||||
QMessageBox::warning(
|
||||
this, tr("Submission Failed"),
|
||||
tr("The reported user could not be found. Guests (unregistered users) cannot be reported."));
|
||||
} else {
|
||||
QMessageBox::warning(this, tr("Submission Failed"), tr("Failed to submit report. Please try again."));
|
||||
}
|
||||
}
|
||||
42
cockatrice/src/interface/widgets/dialogs/dlg_report_user.h
Normal file
42
cockatrice/src/interface/widgets/dialogs/dlg_report_user.h
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
#ifndef COCKATRICE_DLG_REPORT_USER_H
|
||||
#define COCKATRICE_DLG_REPORT_USER_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||
|
||||
class AbstractClient;
|
||||
class QComboBox;
|
||||
class QDialogButtonBox;
|
||||
class QLineEdit;
|
||||
class QTextEdit;
|
||||
class QLabel;
|
||||
|
||||
class DlgReportUser : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
DlgReportUser(AbstractClient *_client,
|
||||
const QString &_reportedUser,
|
||||
int _gameId = -1,
|
||||
const QString &_autoChatLog = QString(),
|
||||
QWidget *parent = nullptr);
|
||||
|
||||
private slots:
|
||||
void actSubmit();
|
||||
void reportResponse(const Response &response);
|
||||
|
||||
private:
|
||||
AbstractClient *client;
|
||||
QString reportedUser;
|
||||
int gameId;
|
||||
|
||||
QLabel *reportedUserLabel;
|
||||
QLabel *gameIdLabel = nullptr;
|
||||
QLineEdit *gameIdEdit = nullptr;
|
||||
QComboBox *categoryBox;
|
||||
QTextEdit *descriptionEdit;
|
||||
QTextEdit *chatLogEdit;
|
||||
QDialogButtonBox *buttonBox;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_DLG_REPORT_USER_H
|
||||
|
|
@ -32,6 +32,7 @@ BannerWidget::BannerWidget(QWidget *parent, const QString &text, Qt::Orientation
|
|||
|
||||
// Set minimum height for the widget
|
||||
setMinimumHeight(50);
|
||||
setMaximumHeight(100);
|
||||
connect(this, &BannerWidget::buddyVisibilityChanged, this, &BannerWidget::toggleBuddyVisibility);
|
||||
|
||||
updateDropdownIconState();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
#ifndef COCKATRICE_HOME_TAB_BUTTON_COLOR_H
|
||||
#define COCKATRICE_HOME_TAB_BUTTON_COLOR_H
|
||||
|
||||
#include <QList>
|
||||
|
||||
namespace HomeTabButtonColor
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Where to get the colors for the home tab buttons from
|
||||
*/
|
||||
enum Source
|
||||
{
|
||||
Automatic, ///< Extract color from background, or use theme color if no background
|
||||
FromBackground, ///< Always extract color from background
|
||||
};
|
||||
|
||||
struct Entry
|
||||
{
|
||||
Source source;
|
||||
const char *trKey; ///< key for translation
|
||||
};
|
||||
|
||||
inline QList<Entry> all()
|
||||
{
|
||||
static QList<Entry> entries = {{Automatic, QT_TR_NOOP("Automatic")},
|
||||
{FromBackground, QT_TR_NOOP("Extract from background")}};
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely converts an int into the corresponding Source.
|
||||
*
|
||||
* @param value The int value
|
||||
* @return The Source. Returns Source::Automatic if the value is not within range
|
||||
*/
|
||||
inline Source intToSource(int value)
|
||||
{
|
||||
if (value > FromBackground) {
|
||||
return Automatic; // default
|
||||
}
|
||||
|
||||
return static_cast<Source>(value);
|
||||
}
|
||||
|
||||
} // namespace HomeTabButtonColor
|
||||
|
||||
#endif // COCKATRICE_HOME_TAB_BUTTON_COLOR_H
|
||||
|
|
@ -4,8 +4,10 @@
|
|||
#include "../../../interface/widgets/tabs/tab_supervisor.h"
|
||||
#include "../../theme_manager.h"
|
||||
#include "../../window_main.h"
|
||||
#include "../cards/art_crop_attribution.h"
|
||||
#include "background_sources.h"
|
||||
#include "home_styled_button.h"
|
||||
#include "home_tab_button_color.h"
|
||||
|
||||
#include <QGroupBox>
|
||||
#include <QPainter>
|
||||
|
|
@ -24,7 +26,7 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
|
|||
|
||||
backgroundSourceCard = new CardInfoPictureArtCropWidget(this);
|
||||
|
||||
gradientColors = extractDominantColors(background);
|
||||
gradientColors = determineButtonColor();
|
||||
|
||||
layout->addWidget(createButtons(), 1, 1, Qt::AlignVCenter | Qt::AlignHCenter);
|
||||
|
||||
|
|
@ -54,6 +56,8 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
|
|||
&HomeWidget::initializeBackgroundFromSource);
|
||||
connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this,
|
||||
&HomeWidget::updateButtonsToBackgroundColor);
|
||||
connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabButtonColorChanged, this,
|
||||
&HomeWidget::updateButtonsToBackgroundColor);
|
||||
}
|
||||
|
||||
void HomeWidget::initializeBackgroundFromSource()
|
||||
|
|
@ -96,6 +100,34 @@ void HomeWidget::loadBackgroundSourceDeck()
|
|||
backgroundSourceDeck = deckOpt.has_value() ? deckOpt.value().deckList : DeckList();
|
||||
}
|
||||
|
||||
static bool isDefaultBackgroundAndTheme()
|
||||
{
|
||||
QString sourceId = SettingsCache::instance().appearance().getHomeTabBackgroundSource();
|
||||
return themeManager->isBuiltInTheme() && BackgroundSources::fromId(sourceId) == BackgroundSources::Theme;
|
||||
}
|
||||
|
||||
QPair<QColor, QColor> HomeWidget::determineButtonColor() const
|
||||
{
|
||||
static QPair defaultColor = {QColor::fromRgb(20, 140, 60), QColor::fromRgb(120, 200, 80)};
|
||||
|
||||
auto colorSource =
|
||||
HomeTabButtonColor::intToSource(SettingsCache::instance().appearance().getHomeTabButtonColorSourceIndex());
|
||||
|
||||
switch (colorSource) {
|
||||
case HomeTabButtonColor::Automatic: {
|
||||
if (isDefaultBackgroundAndTheme()) {
|
||||
return defaultColor;
|
||||
} else {
|
||||
return extractDominantColors(background);
|
||||
}
|
||||
}
|
||||
case HomeTabButtonColor::FromBackground:
|
||||
return extractDominantColors(background);
|
||||
}
|
||||
|
||||
return defaultColor;
|
||||
}
|
||||
|
||||
void HomeWidget::setRandomCard(ExactCard &newCard)
|
||||
{
|
||||
static constexpr int ATTEMPTS = 10;
|
||||
|
|
@ -170,7 +202,7 @@ void HomeWidget::updateBackgroundProperties()
|
|||
|
||||
void HomeWidget::updateButtonsToBackgroundColor()
|
||||
{
|
||||
gradientColors = extractDominantColors(background);
|
||||
gradientColors = determineButtonColor();
|
||||
for (HomeStyledButton *button : findChildren<HomeStyledButton *>()) {
|
||||
button->updateStylesheet(gradientColors);
|
||||
button->update();
|
||||
|
|
@ -265,11 +297,6 @@ void HomeWidget::updateConnectButton(const ClientStatus status)
|
|||
|
||||
QPair<QColor, QColor> HomeWidget::extractDominantColors(const QPixmap &pixmap)
|
||||
{
|
||||
if (themeManager->isBuiltInTheme() && SettingsCache::instance().appearance().getHomeTabBackgroundSource() ==
|
||||
BackgroundSources::toId(BackgroundSources::Theme)) {
|
||||
return QPair<QColor, QColor>(QColor::fromRgb(20, 140, 60), QColor::fromRgb(120, 200, 80));
|
||||
}
|
||||
|
||||
// Step 1: Downscale image for performance
|
||||
QImage image = pixmap.toImage()
|
||||
.scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation)
|
||||
|
|
@ -341,8 +368,9 @@ void HomeWidget::paintEvent(QPaintEvent *event)
|
|||
QColor semiTransparentBlack(0, 0, 0, static_cast<int>(255 * 0.33));
|
||||
painter.fillPath(roundedRectPath, semiTransparentBlack);
|
||||
|
||||
// Card name overlay (bottom-right)
|
||||
// Card name overlay (above the attribution, bottom-right)
|
||||
QString cardName;
|
||||
QString attribution;
|
||||
ExactCard card = backgroundSourceCard->getCard();
|
||||
if (card) {
|
||||
cardName = card.getCardPtr()->getName();
|
||||
|
|
@ -350,8 +378,27 @@ void HomeWidget::paintEvent(QPaintEvent *event)
|
|||
cardName += " (" + card.getPrinting().getSet()->getCorrectedShortName() + ") " +
|
||||
card.getPrinting().getProperty("num");
|
||||
}
|
||||
attribution = buildArtAttribution(card);
|
||||
}
|
||||
|
||||
// Scryfall requires artist attribution wherever card art is shown cropped.
|
||||
// Pin it to the bottom-right corner, using the same font as the card name pill,
|
||||
// and align its right edge with the card name pill's right edge.
|
||||
constexpr int margin = 15;
|
||||
constexpr qreal attributionMargin = 4.0;
|
||||
|
||||
QFont attributionFont = painter.font();
|
||||
attributionFont.setPointSize(14);
|
||||
attributionFont.setBold(true);
|
||||
painter.setFont(attributionFont);
|
||||
|
||||
// paintArtAttribution insets the pill 4px from the given rect's right edge,
|
||||
// so nudge the rect's right edge to land exactly on the pill's right edge.
|
||||
QRectF attributionArea = rect();
|
||||
attributionArea.setRight(width() - margin + attributionMargin);
|
||||
const QRectF attributionRect = paintArtAttribution(painter, attributionArea, attribution);
|
||||
|
||||
// Card name bubble above the attribution (when enabled).
|
||||
if (!cardName.isEmpty() && SettingsCache::instance().appearance().getHomeTabDisplayCardName()) {
|
||||
QFont font = painter.font();
|
||||
font.setPointSize(14);
|
||||
|
|
@ -360,23 +407,26 @@ void HomeWidget::paintEvent(QPaintEvent *event)
|
|||
|
||||
QFontMetrics fm(font);
|
||||
constexpr int padding = 10;
|
||||
constexpr int margin = 15;
|
||||
|
||||
QRect textRect = fm.boundingRect(cardName);
|
||||
|
||||
QRect bgRect(width() - textRect.width() - padding * 2 - margin,
|
||||
height() - textRect.height() - padding * 2 - margin, textRect.width() + padding * 2,
|
||||
textRect.height() + padding * 2);
|
||||
int bubbleBottom = height() - margin;
|
||||
if (!attributionRect.isEmpty()) {
|
||||
bubbleBottom = attributionRect.top() - 6;
|
||||
}
|
||||
const QRect nameBubbleRect(width() - textRect.width() - padding * 2 - margin,
|
||||
bubbleBottom - textRect.height() - padding * 2, textRect.width() + padding * 2,
|
||||
textRect.height() + padding * 2);
|
||||
|
||||
// Background bubble
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setBrush(QColor(0, 0, 0, 160));
|
||||
painter.drawRoundedRect(bgRect, 8, 8);
|
||||
painter.drawRoundedRect(nameBubbleRect, 8, 8);
|
||||
|
||||
// Text
|
||||
painter.setPen(Qt::white);
|
||||
painter.drawText(bgRect.adjusted(padding, padding, -padding, -padding), Qt::AlignRight | Qt::AlignVCenter,
|
||||
cardName);
|
||||
painter.drawText(nameBubbleRect.adjusted(padding, padding, -padding, -padding),
|
||||
Qt::AlignRight | Qt::AlignVCenter, cardName);
|
||||
}
|
||||
|
||||
QWidget::paintEvent(event);
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ class HomeWidget : public QWidget
|
|||
public:
|
||||
HomeWidget(QWidget *parent, TabSupervisor *tabSupervisor);
|
||||
void updateRandomCard();
|
||||
QPair<QColor, QColor> extractDominantColors(const QPixmap &pixmap);
|
||||
static QPair<QColor, QColor> extractDominantColors(const QPixmap &pixmap);
|
||||
|
||||
public slots:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
|
|
@ -47,6 +47,7 @@ private:
|
|||
|
||||
void setRandomCard(ExactCard &newCard);
|
||||
void loadBackgroundSourceDeck();
|
||||
QPair<QColor, QColor> determineButtonColor() const;
|
||||
};
|
||||
|
||||
#endif // HOME_WIDGET_H
|
||||
|
|
|
|||
|
|
@ -0,0 +1,250 @@
|
|||
#ifndef BANNER_SHADER_CONFIG_H
|
||||
#define BANNER_SHADER_CONFIG_H
|
||||
|
||||
#include <QColor>
|
||||
#include <QObject>
|
||||
|
||||
/**
|
||||
* Uniform values fed to brand_banner.frag, exposed to QML as the
|
||||
* "bannerConfig" context property.
|
||||
*
|
||||
* Two independent "banks" (A/B) each carry their own mode/speed/seed so
|
||||
* BrandBanner.qml can render both simultaneously and crossfade between
|
||||
* them via opacity -- see frontIsA. The shared palette (colorA/colorB/
|
||||
* accent) and clock (time/aspect) apply to both banks identically, since
|
||||
* only the foreground motif changes between onboarding pages, never the
|
||||
* brand palette.
|
||||
*
|
||||
* Deliberately plain `property` (not `required property`) on the QML side
|
||||
* -- a required-property shadowing bug bit the home-screen particle
|
||||
* background before, and there's no reason to reintroduce that risk here.
|
||||
*/
|
||||
class BannerShaderConfig : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(qreal time READ time WRITE setTime NOTIFY timeChanged)
|
||||
Q_PROPERTY(qreal aspect READ aspect WRITE setAspect NOTIFY aspectChanged)
|
||||
|
||||
Q_PROPERTY(qreal modeA READ modeA WRITE setModeA NOTIFY modeAChanged)
|
||||
Q_PROPERTY(qreal speedA READ speedA WRITE setSpeedA NOTIFY speedAChanged)
|
||||
Q_PROPERTY(qreal seedA READ seedA WRITE setSeedA NOTIFY seedAChanged)
|
||||
|
||||
Q_PROPERTY(qreal modeB READ modeB WRITE setModeB NOTIFY modeBChanged)
|
||||
Q_PROPERTY(qreal speedB READ speedB WRITE setSpeedB NOTIFY speedBChanged)
|
||||
Q_PROPERTY(qreal seedB READ seedB WRITE setSeedB NOTIFY seedBChanged)
|
||||
|
||||
Q_PROPERTY(bool frontIsA READ frontIsA WRITE setFrontIsA NOTIFY frontIsAChanged)
|
||||
|
||||
Q_PROPERTY(QColor colorA READ colorA WRITE setColorA NOTIFY colorAChanged)
|
||||
Q_PROPERTY(QColor colorB READ colorB WRITE setColorB NOTIFY colorBChanged)
|
||||
Q_PROPERTY(QColor accent READ accent WRITE setAccent NOTIFY accentChanged)
|
||||
|
||||
Q_PROPERTY(bool logoVisible READ logoVisible WRITE setLogoVisible NOTIFY logoVisibleChanged)
|
||||
Q_PROPERTY(qreal logoGlow READ logoGlow WRITE setLogoGlow NOTIFY logoGlowChanged)
|
||||
|
||||
public:
|
||||
explicit BannerShaderConfig(QObject *parent = nullptr) : QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
qreal time() const
|
||||
{
|
||||
return m_time;
|
||||
}
|
||||
void setTime(qreal v)
|
||||
{
|
||||
if (v != m_time) {
|
||||
m_time = v;
|
||||
emit timeChanged();
|
||||
}
|
||||
}
|
||||
|
||||
qreal aspect() const
|
||||
{
|
||||
return m_aspect;
|
||||
}
|
||||
void setAspect(qreal v)
|
||||
{
|
||||
if (v != m_aspect) {
|
||||
m_aspect = v;
|
||||
emit aspectChanged();
|
||||
}
|
||||
}
|
||||
|
||||
qreal modeA() const
|
||||
{
|
||||
return m_modeA;
|
||||
}
|
||||
void setModeA(qreal v)
|
||||
{
|
||||
if (v != m_modeA) {
|
||||
m_modeA = v;
|
||||
emit modeAChanged();
|
||||
}
|
||||
}
|
||||
qreal speedA() const
|
||||
{
|
||||
return m_speedA;
|
||||
}
|
||||
void setSpeedA(qreal v)
|
||||
{
|
||||
if (v != m_speedA) {
|
||||
m_speedA = v;
|
||||
emit speedAChanged();
|
||||
}
|
||||
}
|
||||
qreal seedA() const
|
||||
{
|
||||
return m_seedA;
|
||||
}
|
||||
void setSeedA(qreal v)
|
||||
{
|
||||
if (v != m_seedA) {
|
||||
m_seedA = v;
|
||||
emit seedAChanged();
|
||||
}
|
||||
}
|
||||
|
||||
qreal modeB() const
|
||||
{
|
||||
return m_modeB;
|
||||
}
|
||||
void setModeB(qreal v)
|
||||
{
|
||||
if (v != m_modeB) {
|
||||
m_modeB = v;
|
||||
emit modeBChanged();
|
||||
}
|
||||
}
|
||||
qreal speedB() const
|
||||
{
|
||||
return m_speedB;
|
||||
}
|
||||
void setSpeedB(qreal v)
|
||||
{
|
||||
if (v != m_speedB) {
|
||||
m_speedB = v;
|
||||
emit speedBChanged();
|
||||
}
|
||||
}
|
||||
qreal seedB() const
|
||||
{
|
||||
return m_seedB;
|
||||
}
|
||||
void setSeedB(qreal v)
|
||||
{
|
||||
if (v != m_seedB) {
|
||||
m_seedB = v;
|
||||
emit seedBChanged();
|
||||
}
|
||||
}
|
||||
|
||||
bool frontIsA() const
|
||||
{
|
||||
return m_frontIsA;
|
||||
}
|
||||
void setFrontIsA(bool v)
|
||||
{
|
||||
if (v != m_frontIsA) {
|
||||
m_frontIsA = v;
|
||||
emit frontIsAChanged();
|
||||
}
|
||||
}
|
||||
|
||||
QColor colorA() const
|
||||
{
|
||||
return m_colorA;
|
||||
}
|
||||
void setColorA(const QColor &c)
|
||||
{
|
||||
if (c != m_colorA) {
|
||||
m_colorA = c;
|
||||
emit colorAChanged();
|
||||
}
|
||||
}
|
||||
QColor colorB() const
|
||||
{
|
||||
return m_colorB;
|
||||
}
|
||||
void setColorB(const QColor &c)
|
||||
{
|
||||
if (c != m_colorB) {
|
||||
m_colorB = c;
|
||||
emit colorBChanged();
|
||||
}
|
||||
}
|
||||
QColor accent() const
|
||||
{
|
||||
return m_accent;
|
||||
}
|
||||
void setAccent(const QColor &c)
|
||||
{
|
||||
if (c != m_accent) {
|
||||
m_accent = c;
|
||||
emit accentChanged();
|
||||
}
|
||||
}
|
||||
|
||||
bool logoVisible() const
|
||||
{
|
||||
return m_logoVisible;
|
||||
}
|
||||
void setLogoVisible(bool v)
|
||||
{
|
||||
if (v != m_logoVisible) {
|
||||
m_logoVisible = v;
|
||||
emit logoVisibleChanged();
|
||||
}
|
||||
}
|
||||
|
||||
qreal logoGlow() const
|
||||
{
|
||||
return m_logoGlow;
|
||||
}
|
||||
void setLogoGlow(qreal v)
|
||||
{
|
||||
if (v != m_logoGlow) {
|
||||
m_logoGlow = v;
|
||||
emit logoGlowChanged();
|
||||
}
|
||||
}
|
||||
|
||||
signals:
|
||||
void timeChanged();
|
||||
void aspectChanged();
|
||||
void modeAChanged();
|
||||
void speedAChanged();
|
||||
void seedAChanged();
|
||||
void modeBChanged();
|
||||
void speedBChanged();
|
||||
void seedBChanged();
|
||||
void frontIsAChanged();
|
||||
void colorAChanged();
|
||||
void colorBChanged();
|
||||
void accentChanged();
|
||||
void logoVisibleChanged();
|
||||
void logoGlowChanged();
|
||||
|
||||
private:
|
||||
qreal m_time = 0.0;
|
||||
qreal m_aspect = 16.0 / 9.0;
|
||||
|
||||
qreal m_modeA = 0.0;
|
||||
qreal m_speedA = 1.0;
|
||||
qreal m_seedA = 0.0;
|
||||
|
||||
qreal m_modeB = 0.0;
|
||||
qreal m_speedB = 1.0;
|
||||
qreal m_seedB = 0.0;
|
||||
|
||||
bool m_frontIsA = true;
|
||||
|
||||
QColor m_colorA{0x1A, 0x1A, 0x20};
|
||||
QColor m_colorB{0x0E, 0x0E, 0x12};
|
||||
QColor m_accent{0x8B, 0xDD, 0x6B};
|
||||
|
||||
bool m_logoVisible = false;
|
||||
qreal m_logoGlow = 1.0;
|
||||
};
|
||||
|
||||
#endif // BANNER_SHADER_CONFIG_H
|
||||
218
cockatrice/src/interface/widgets/onboarding/first_run_wizard.cpp
Normal file
218
cockatrice/src/interface/widgets/onboarding/first_run_wizard.cpp
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
#include "first_run_wizard.h"
|
||||
|
||||
#include "first_run_wizard_page.h"
|
||||
#include "pages/account_setup_page.h"
|
||||
#include "pages/card_database_setup_page.h"
|
||||
#include "pages/finish_page.h"
|
||||
#include "pages/preferences_setup_page.h"
|
||||
#include "pages/theme_setup_page.h"
|
||||
#include "pages/welcome_page.h"
|
||||
#include "shader_banner_widget.h"
|
||||
#include "step_indicator_widget.h"
|
||||
|
||||
#include <QCloseEvent>
|
||||
#include <QEvent>
|
||||
#include <QFont>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QStackedWidget>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
FirstRunWizard::FirstRunWizard(QWidget *parent) : QDialog(parent)
|
||||
{
|
||||
setWindowFlag(Qt::WindowContextHelpButtonHint, false);
|
||||
setMinimumSize(640, 490);
|
||||
resize(720, 550);
|
||||
|
||||
bannerHost = new BannerHost(this);
|
||||
|
||||
titleLabel = new QLabel(this);
|
||||
QFont titleFont = titleLabel->font();
|
||||
titleFont.setPointSizeF(titleFont.pointSizeF() * 1.4);
|
||||
titleFont.setBold(true);
|
||||
titleLabel->setFont(titleFont);
|
||||
|
||||
subtitleLabel = new QLabel(this);
|
||||
subtitleLabel->setWordWrap(true);
|
||||
|
||||
stack = new QStackedWidget(this);
|
||||
stepIndicator = new StepIndicatorWidget(this);
|
||||
|
||||
backButton = new QPushButton(this);
|
||||
skipButton = new QPushButton(this);
|
||||
nextButton = new QPushButton(this);
|
||||
nextButton->setDefault(true);
|
||||
|
||||
connect(backButton, &QPushButton::clicked, this, &FirstRunWizard::goBack);
|
||||
connect(skipButton, &QPushButton::clicked, this, &FirstRunWizard::skip);
|
||||
connect(nextButton, &QPushButton::clicked, this, &FirstRunWizard::goNext);
|
||||
|
||||
auto *headerLayout = new QVBoxLayout;
|
||||
headerLayout->setContentsMargins(0, 0, 0, 0);
|
||||
headerLayout->addWidget(bannerHost);
|
||||
headerLayout->addSpacing(12);
|
||||
headerLayout->addWidget(titleLabel);
|
||||
headerLayout->addWidget(subtitleLabel);
|
||||
|
||||
auto *navLayout = new QHBoxLayout;
|
||||
navLayout->addWidget(backButton);
|
||||
navLayout->addWidget(skipButton);
|
||||
navLayout->addStretch();
|
||||
navLayout->addWidget(stepIndicator);
|
||||
navLayout->addStretch();
|
||||
navLayout->addWidget(nextButton);
|
||||
|
||||
auto *root = new QVBoxLayout(this);
|
||||
root->addLayout(headerLayout);
|
||||
root->addSpacing(8);
|
||||
root->addWidget(stack, 1);
|
||||
root->addSpacing(8);
|
||||
root->addLayout(navLayout);
|
||||
|
||||
auto *welcome = new WelcomePage(this);
|
||||
auto *cardDb = new CardDatabaseSetupPage(this);
|
||||
auto *theme = new ThemeSetupPage(this);
|
||||
auto *account = new AccountSetupPage(this);
|
||||
auto *prefs = new PreferencesSetupPage(this);
|
||||
auto *finishPg = new FinishPage(this);
|
||||
|
||||
cardDatabasePage = cardDb;
|
||||
|
||||
connect(cardDb, &CardDatabaseSetupPage::updateRequested, this, &FirstRunWizard::cardDatabaseUpdateRequested);
|
||||
connect(cardDb, &CardDatabaseSetupPage::manualSetupRequested, this,
|
||||
&FirstRunWizard::manualCardDatabaseSetupRequested);
|
||||
connect(account, &AccountSetupPage::registerRequested, this, &FirstRunWizard::registerRequested);
|
||||
connect(account, &AccountSetupPage::connectRequested, this, &FirstRunWizard::connectRequested);
|
||||
|
||||
connect(cardDb, &CardDatabaseSetupPage::advanceRequested, this, [this] {
|
||||
if (stack->currentWidget() == cardDatabasePage) {
|
||||
showPage(currentIndex + 1);
|
||||
}
|
||||
});
|
||||
|
||||
addPage(welcome);
|
||||
addPage(cardDb);
|
||||
addPage(theme);
|
||||
addPage(account);
|
||||
addPage(prefs);
|
||||
addPage(finishPg);
|
||||
|
||||
stepIndicator->setStepCount(pages.count());
|
||||
retranslateUi();
|
||||
showPage(0);
|
||||
}
|
||||
|
||||
void FirstRunWizard::addPage(FirstRunWizardPage *page)
|
||||
{
|
||||
pages.append(page);
|
||||
stack->addWidget(page);
|
||||
connect(page, &FirstRunWizardPage::completeChanged, this, &FirstRunWizard::updateChrome);
|
||||
}
|
||||
|
||||
void FirstRunWizard::showPage(int index)
|
||||
{
|
||||
if (index < 0 || index >= pages.count()) {
|
||||
return;
|
||||
}
|
||||
currentIndex = index;
|
||||
stack->setCurrentIndex(index);
|
||||
pages[index]->initializePage();
|
||||
stepIndicator->setCurrentStep(index);
|
||||
static const QList<BannerHost::Motif> motifs = {
|
||||
BannerHost::Motif::Welcome, BannerHost::Motif::CardDatabase, BannerHost::Motif::Theming,
|
||||
BannerHost::Motif::Account, BannerHost::Motif::Preferences, BannerHost::Motif::Finish,
|
||||
};
|
||||
if (index < motifs.size()) {
|
||||
bannerHost->setMotif(motifs[index]);
|
||||
}
|
||||
titleLabel->setText(pages[index]->stepTitle());
|
||||
subtitleLabel->setText(pages[index]->stepSubtitle());
|
||||
subtitleLabel->setVisible(!pages[index]->stepSubtitle().isEmpty());
|
||||
updateChrome();
|
||||
}
|
||||
|
||||
void FirstRunWizard::updateChrome()
|
||||
{
|
||||
if (currentIndex < 0) {
|
||||
return;
|
||||
}
|
||||
FirstRunWizardPage *page = pages[currentIndex];
|
||||
const bool isLast = (currentIndex == pages.count() - 1);
|
||||
|
||||
backButton->setVisible(currentIndex > 0);
|
||||
skipButton->setVisible(page->isSkippable());
|
||||
nextButton->setEnabled(page->isComplete());
|
||||
|
||||
QString customText = page->nextButtonText();
|
||||
if (!customText.isEmpty()) {
|
||||
nextButton->setText(customText);
|
||||
} else {
|
||||
nextButton->setText(isLast ? tr("Finish") : tr("Next"));
|
||||
}
|
||||
}
|
||||
|
||||
void FirstRunWizard::goNext()
|
||||
{
|
||||
FirstRunWizardPage *page = pages[currentIndex];
|
||||
if (!page->validatePage() || !page->handleNextClick()) {
|
||||
return;
|
||||
}
|
||||
if (currentIndex == pages.count() - 1) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
showPage(currentIndex + 1);
|
||||
}
|
||||
|
||||
void FirstRunWizard::goBack()
|
||||
{
|
||||
showPage(currentIndex - 1);
|
||||
}
|
||||
|
||||
void FirstRunWizard::skip()
|
||||
{
|
||||
showPage(currentIndex + 1);
|
||||
}
|
||||
|
||||
void FirstRunWizard::onCardDatabaseUpdateFinished(bool success)
|
||||
{
|
||||
if (cardDatabasePage) {
|
||||
cardDatabasePage->onUpdateFinished(success);
|
||||
}
|
||||
}
|
||||
|
||||
void FirstRunWizard::finish()
|
||||
{
|
||||
accept();
|
||||
}
|
||||
|
||||
void FirstRunWizard::closeEvent(QCloseEvent *event)
|
||||
{
|
||||
// Every step persists its own choice as it's made, so closing early
|
||||
// isn't destructive -- treat it exactly like reaching the end.
|
||||
QDialog::closeEvent(event);
|
||||
}
|
||||
|
||||
void FirstRunWizard::changeEvent(QEvent *event)
|
||||
{
|
||||
if (event->type() == QEvent::LanguageChange) {
|
||||
retranslateUi();
|
||||
}
|
||||
QDialog::changeEvent(event);
|
||||
}
|
||||
|
||||
void FirstRunWizard::retranslateUi()
|
||||
{
|
||||
setWindowTitle(tr("Welcome to Cockatrice"));
|
||||
backButton->setText(tr("Back"));
|
||||
skipButton->setText(tr("Skip"));
|
||||
for (FirstRunWizardPage *page : std::as_const(pages)) {
|
||||
page->retranslateUi();
|
||||
}
|
||||
if (currentIndex >= 0) {
|
||||
titleLabel->setText(pages[currentIndex]->stepTitle());
|
||||
subtitleLabel->setText(pages[currentIndex]->stepSubtitle());
|
||||
}
|
||||
updateChrome();
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
#ifndef FIRST_RUN_WIZARD_H
|
||||
#define FIRST_RUN_WIZARD_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QList>
|
||||
|
||||
class BannerHost;
|
||||
class FirstRunWizardPage;
|
||||
class StepIndicatorWidget;
|
||||
class CardDatabaseSetupPage;
|
||||
class QLabel;
|
||||
class QPushButton;
|
||||
class QStackedWidget;
|
||||
|
||||
/** @brief Polished first-run onboarding flow: card database setup, theme
|
||||
* selection, server account setup, and a handful of key preferences.
|
||||
*
|
||||
* Deliberately ignorant of network/registration/download internals --
|
||||
* pages that need them emit request signals for MainWindow to fulfill.
|
||||
* Every choice is written to SettingsCache as it's made (via the pages
|
||||
* themselves, same as AppearanceSettingsPage does), so "Skip" or closing
|
||||
* the window never discards anything already confirmed. */
|
||||
class FirstRunWizard : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit FirstRunWizard(QWidget *parent = nullptr);
|
||||
|
||||
signals:
|
||||
void registerRequested();
|
||||
void connectRequested();
|
||||
void cardDatabaseUpdateRequested();
|
||||
void manualCardDatabaseSetupRequested();
|
||||
|
||||
public slots:
|
||||
/** @brief Forwarded from MainWindow once the background card database update process exits. */
|
||||
void onCardDatabaseUpdateFinished(bool success);
|
||||
|
||||
protected:
|
||||
void closeEvent(QCloseEvent *event) override;
|
||||
void changeEvent(QEvent *event) override;
|
||||
|
||||
private slots:
|
||||
void goNext();
|
||||
void goBack();
|
||||
void skip();
|
||||
void updateChrome();
|
||||
|
||||
private:
|
||||
void addPage(FirstRunWizardPage *page);
|
||||
void showPage(int index);
|
||||
void retranslateUi();
|
||||
void finish();
|
||||
|
||||
QStackedWidget *stack;
|
||||
StepIndicatorWidget *stepIndicator;
|
||||
BannerHost *bannerHost;
|
||||
QLabel *titleLabel;
|
||||
QLabel *subtitleLabel;
|
||||
QPushButton *backButton;
|
||||
QPushButton *skipButton;
|
||||
QPushButton *nextButton;
|
||||
|
||||
CardDatabaseSetupPage *cardDatabasePage = nullptr;
|
||||
|
||||
QList<FirstRunWizardPage *> pages;
|
||||
int currentIndex = -1;
|
||||
};
|
||||
|
||||
#endif // FIRST_RUN_WIZARD_H
|
||||
|
|
@ -0,0 +1 @@
|
|||
#include "first_run_wizard_page.h"
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
#ifndef FIRST_RUN_WIZARD_PAGE_H
|
||||
#define FIRST_RUN_WIZARD_PAGE_H
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
/** @brief Base class for a single step of FirstRunWizard.
|
||||
*
|
||||
* QWidget-based rather than QWizardPage-based: FirstRunWizard is a
|
||||
* QDialog + QStackedWidget shell (not a QWizard) so it can own the
|
||||
* banner/step-dot chrome that QWizard's native styles don't give us
|
||||
* consistent control over. Naming mirrors OracleWizardPage for
|
||||
* familiarity only -- the two hierarchies are unrelated. */
|
||||
class FirstRunWizardPage : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit FirstRunWizardPage(QWidget *parent = nullptr) : QWidget(parent)
|
||||
{
|
||||
}
|
||||
|
||||
/** @brief Called every time the page becomes visible, including navigating back to it. */
|
||||
virtual void initializePage()
|
||||
{
|
||||
}
|
||||
|
||||
/** @brief Called before advancing past this page. Return false to block navigation;
|
||||
the page itself is responsible for telling the user why. */
|
||||
virtual bool validatePage()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @brief Whether Next/Finish should currently be enabled. Pages doing async work
|
||||
can flip this mid-step; emit completeChanged() when they do. */
|
||||
virtual bool isComplete() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @brief Whether the wizard's "Skip" button should be offered on this page. */
|
||||
virtual bool isSkippable() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
virtual QString stepTitle() const = 0;
|
||||
virtual QString stepSubtitle() const
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
/** @brief Override to replace the "Next"/"Finish" button text on this page.
|
||||
Return an empty string to use the default label. */
|
||||
virtual QString nextButtonText() const
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
/** @brief Called when the user presses the Next button. Return true to allow
|
||||
advancing to the next page, false to stay on this page (e.g. to
|
||||
trigger an async action first). */
|
||||
virtual bool handleNextClick()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual void retranslateUi() = 0;
|
||||
|
||||
signals:
|
||||
void completeChanged();
|
||||
void advanceRequested();
|
||||
};
|
||||
|
||||
#endif // FIRST_RUN_WIZARD_PAGE_H
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
#include "account_setup_page.h"
|
||||
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
AccountSetupPage::AccountSetupPage(QWidget *parent) : FirstRunWizardPage(parent)
|
||||
{
|
||||
bodyLabel = new QLabel(this);
|
||||
bodyLabel->setWordWrap(true);
|
||||
bodyLabel->setAlignment(Qt::AlignCenter);
|
||||
|
||||
registerButton = new QPushButton(this);
|
||||
connectButton = new QPushButton(this);
|
||||
skipHintLabel = new QLabel(this);
|
||||
skipHintLabel->setWordWrap(true);
|
||||
skipHintLabel->setAlignment(Qt::AlignCenter);
|
||||
|
||||
connect(registerButton, &QPushButton::clicked, this, &AccountSetupPage::registerRequested);
|
||||
connect(connectButton, &QPushButton::clicked, this, &AccountSetupPage::connectRequested);
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->addStretch();
|
||||
layout->addWidget(bodyLabel);
|
||||
layout->addSpacing(16);
|
||||
layout->addWidget(registerButton, 0, Qt::AlignHCenter);
|
||||
layout->addWidget(connectButton, 0, Qt::AlignHCenter);
|
||||
layout->addSpacing(16);
|
||||
layout->addWidget(skipHintLabel);
|
||||
layout->addStretch();
|
||||
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
bool AccountSetupPage::isSkippable() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
QString AccountSetupPage::stepTitle() const
|
||||
{
|
||||
return tr("Join a Server");
|
||||
}
|
||||
|
||||
QString AccountSetupPage::stepSubtitle() const
|
||||
{
|
||||
return tr("Optional — you can always do this later from the menu.");
|
||||
}
|
||||
|
||||
void AccountSetupPage::retranslateUi()
|
||||
{
|
||||
bodyLabel->setText(tr("Playing online needs a server account."));
|
||||
registerButton->setText(tr("Register a new account…"));
|
||||
connectButton->setText(tr("I already have one — Connect…"));
|
||||
skipHintLabel->setText(tr("Just want to play locally? Skip this and connect whenever you're ready."));
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
#ifndef ACCOUNT_SETUP_PAGE_H
|
||||
#define ACCOUNT_SETUP_PAGE_H
|
||||
|
||||
#include "../first_run_wizard_page.h"
|
||||
|
||||
class QLabel;
|
||||
class QPushButton;
|
||||
|
||||
/** @brief First-run account step. Does NOT embed DlgRegister's fields: they exist
|
||||
* to be handed to ConnectionController's network registration flow, which
|
||||
* this wizard has no visibility into. Reimplementing the fields here
|
||||
* without that wiring would look functional and silently do nothing --
|
||||
* worse than reuse. So: a friendly landing spot that opens the *existing*
|
||||
* DlgRegister / connect flow via signals FirstRunWizard forwards. */
|
||||
class AccountSetupPage : public FirstRunWizardPage
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit AccountSetupPage(QWidget *parent = nullptr);
|
||||
|
||||
bool isSkippable() const override;
|
||||
QString stepTitle() const override;
|
||||
QString stepSubtitle() const override;
|
||||
void retranslateUi() override;
|
||||
|
||||
signals:
|
||||
void registerRequested();
|
||||
void connectRequested();
|
||||
|
||||
private:
|
||||
QLabel *bodyLabel;
|
||||
QPushButton *registerButton;
|
||||
QPushButton *connectButton;
|
||||
QLabel *skipHintLabel;
|
||||
};
|
||||
|
||||
#endif // ACCOUNT_SETUP_PAGE_H
|
||||
|
|
@ -0,0 +1,314 @@
|
|||
#include "card_database_setup_page.h"
|
||||
|
||||
#include "../../client/settings/cache_settings.h"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QGridLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QMessageBox>
|
||||
#include <QProgressBar>
|
||||
#include <QPushButton>
|
||||
#include <QSettings>
|
||||
#include <QSpinBox>
|
||||
#include <QTimer>
|
||||
#include <QUrl>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/settings/updates_settings.h>
|
||||
|
||||
CardDatabaseSetupPage::CardDatabaseSetupPage(QWidget *parent) : FirstRunWizardPage(parent)
|
||||
{
|
||||
statusLabel = new QLabel(this);
|
||||
statusLabel->setWordWrap(true);
|
||||
statusLabel->setAlignment(Qt::AlignCenter);
|
||||
|
||||
progressBar = new QProgressBar(this);
|
||||
progressBar->setRange(0, 0);
|
||||
progressBar->setTextVisible(false);
|
||||
progressBar->setFixedWidth(280);
|
||||
|
||||
retryButton = new QPushButton(this);
|
||||
manualButton = new QPushButton(this);
|
||||
|
||||
connect(retryButton, &QPushButton::clicked, this, [this] {
|
||||
setState(State::Running);
|
||||
emit updateRequested();
|
||||
});
|
||||
connect(manualButton, &QPushButton::clicked, this, &CardDatabaseSetupPage::manualSetupRequested);
|
||||
|
||||
// ── Advanced: custom download source ───────────────────────────────
|
||||
advancedToggleButton = new QPushButton(this);
|
||||
advancedToggleButton->setCheckable(true);
|
||||
advancedToggleButton->setChecked(false);
|
||||
advancedToggleButton->setFlat(true);
|
||||
advancedToggleButton->setStyleSheet("QPushButton { text-align: left; padding: 5px 12px; font-weight: bold; }"
|
||||
"QPushButton:checked { }");
|
||||
|
||||
advancedPanel = new QWidget(this);
|
||||
advancedPanel->setVisible(false);
|
||||
|
||||
urlLineEdit = new QLineEdit(advancedPanel);
|
||||
urlHintLabel = new QLabel(advancedPanel);
|
||||
urlHintLabel->setWordWrap(true);
|
||||
|
||||
restoreDefaultUrlButton = new QPushButton(advancedPanel);
|
||||
applyAndRetryButton = new QPushButton(advancedPanel);
|
||||
|
||||
connect(advancedToggleButton, &QPushButton::toggled, this, &CardDatabaseSetupPage::onToggleAdvanced);
|
||||
connect(restoreDefaultUrlButton, &QPushButton::clicked, this, &CardDatabaseSetupPage::onRestoreDefaultUrl);
|
||||
connect(applyAndRetryButton, &QPushButton::clicked, this, &CardDatabaseSetupPage::onApplyCustomUrl);
|
||||
|
||||
auto *advancedButtonRow = new QHBoxLayout;
|
||||
advancedButtonRow->addWidget(restoreDefaultUrlButton);
|
||||
advancedButtonRow->addStretch();
|
||||
advancedButtonRow->addWidget(applyAndRetryButton);
|
||||
|
||||
auto *advancedLayout = new QVBoxLayout(advancedPanel);
|
||||
advancedLayout->setContentsMargins(12, 4, 12, 4);
|
||||
advancedLayout->addWidget(urlLineEdit);
|
||||
advancedLayout->addWidget(urlHintLabel);
|
||||
advancedLayout->addLayout(advancedButtonRow);
|
||||
|
||||
// ── Startup card update check ───────────────────────────────────────
|
||||
auto &upd = SettingsCache::instance().updates();
|
||||
|
||||
const auto updateBehavior = [this] {
|
||||
auto &u = SettingsCache::instance().updates();
|
||||
int idx = startupBehaviorCombo->currentIndex();
|
||||
u.setStartupCardUpdateCheckPromptForUpdate(idx == 1);
|
||||
u.setStartupCardUpdateCheckAlwaysUpdate(idx == 2);
|
||||
};
|
||||
|
||||
startupBehaviorLabel = new QLabel(this);
|
||||
startupBehaviorCombo = new QComboBox(this);
|
||||
startupBehaviorCombo->addItem(QString()); // placeholder, filled in retranslateUi
|
||||
startupBehaviorCombo->addItem(QString());
|
||||
startupBehaviorCombo->addItem(QString());
|
||||
if (upd.getStartupCardUpdateCheckPromptForUpdate()) {
|
||||
startupBehaviorCombo->setCurrentIndex(1);
|
||||
} else if (upd.getStartupCardUpdateCheckAlwaysUpdate()) {
|
||||
startupBehaviorCombo->setCurrentIndex(2);
|
||||
} else {
|
||||
startupBehaviorCombo->setCurrentIndex(0);
|
||||
}
|
||||
connect(startupBehaviorCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, updateBehavior);
|
||||
|
||||
checkIntervalLabel = new QLabel(this);
|
||||
checkIntervalSpinBox = new QSpinBox(this);
|
||||
checkIntervalSpinBox->setMinimum(1);
|
||||
checkIntervalSpinBox->setMaximum(30);
|
||||
checkIntervalSpinBox->setValue(upd.getCardUpdateCheckInterval());
|
||||
connect(checkIntervalSpinBox, QOverload<int>::of(&QSpinBox::valueChanged), &upd,
|
||||
&UpdatesSettings::setCardUpdateCheckInterval);
|
||||
|
||||
auto *checkGrid = new QGridLayout;
|
||||
checkGrid->addWidget(startupBehaviorLabel, 0, 0);
|
||||
checkGrid->addWidget(startupBehaviorCombo, 0, 1);
|
||||
checkGrid->addWidget(checkIntervalLabel, 1, 0);
|
||||
checkGrid->addWidget(checkIntervalSpinBox, 1, 1);
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->addStretch();
|
||||
layout->addWidget(statusLabel);
|
||||
layout->addSpacing(12);
|
||||
layout->addWidget(progressBar, 0, Qt::AlignHCenter);
|
||||
layout->addSpacing(12);
|
||||
layout->addWidget(retryButton, 0, Qt::AlignHCenter);
|
||||
layout->addWidget(manualButton, 0, Qt::AlignHCenter);
|
||||
layout->addSpacing(16);
|
||||
layout->addWidget(advancedToggleButton);
|
||||
layout->addWidget(advancedPanel);
|
||||
layout->addSpacing(8);
|
||||
layout->addLayout(checkGrid);
|
||||
layout->addStretch();
|
||||
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
bool CardDatabaseSetupPage::alreadyHaveDatabase() const
|
||||
{
|
||||
return CardDatabaseManager::getInstance()->getCardList().count() > 0;
|
||||
}
|
||||
|
||||
QString CardDatabaseSetupPage::oracleSettingsFilePath() const
|
||||
{
|
||||
return SettingsCache::instance().getSettingsPath() + "oracle.ini";
|
||||
}
|
||||
|
||||
QString CardDatabaseSetupPage::readCustomUrl() const
|
||||
{
|
||||
QSettings oracleSettings(oracleSettingsFilePath(), QSettings::IniFormat);
|
||||
return oracleSettings.value("allsetsurl").toString();
|
||||
}
|
||||
|
||||
void CardDatabaseSetupPage::writeCustomUrl(const QString &url)
|
||||
{
|
||||
QSettings oracleSettings(oracleSettingsFilePath(), QSettings::IniFormat);
|
||||
if (url.isEmpty()) {
|
||||
oracleSettings.remove("allsetsurl");
|
||||
} else {
|
||||
oracleSettings.setValue("allsetsurl", url);
|
||||
}
|
||||
}
|
||||
|
||||
void CardDatabaseSetupPage::initializePage()
|
||||
{
|
||||
urlLineEdit->setText(readCustomUrl());
|
||||
|
||||
if (state != State::NotStarted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (alreadyHaveDatabase()) {
|
||||
setState(State::Succeeded);
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't auto-download — wait for the user to press "Download".
|
||||
setState(State::NotStarted);
|
||||
statusLabel->setText(tr("Press Download to fetch the card database, or Skip to do it later."));
|
||||
}
|
||||
|
||||
void CardDatabaseSetupPage::onUpdateFinished(bool success)
|
||||
{
|
||||
setState(success ? State::Succeeded : State::Failed);
|
||||
if (success) {
|
||||
emit advanceRequested();
|
||||
}
|
||||
}
|
||||
|
||||
QString CardDatabaseSetupPage::nextButtonText() const
|
||||
{
|
||||
return state == State::NotStarted ? tr("Download") : QString();
|
||||
}
|
||||
|
||||
bool CardDatabaseSetupPage::handleNextClick()
|
||||
{
|
||||
if (state == State::NotStarted) {
|
||||
setState(State::Running);
|
||||
emit updateRequested();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void CardDatabaseSetupPage::onToggleAdvanced(bool open)
|
||||
{
|
||||
advancedToggleButton->setText(open ? tr("▼ Advanced: custom download source")
|
||||
: tr("▶ Advanced: custom download source"));
|
||||
advancedPanel->setVisible(open);
|
||||
|
||||
QWidget *wizardWindow = window();
|
||||
if (!wizardWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (open) {
|
||||
windowSizeBeforeExpansion = wizardWindow->size();
|
||||
QTimer::singleShot(0, this, [wizardWindow] {
|
||||
wizardWindow->resize(wizardWindow->size().expandedTo(wizardWindow->sizeHint()));
|
||||
});
|
||||
} else {
|
||||
QTimer::singleShot(0, this, [this, wizardWindow] {
|
||||
wizardWindow->resize(wizardWindow->size().boundedTo(windowSizeBeforeExpansion));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void CardDatabaseSetupPage::onApplyCustomUrl()
|
||||
{
|
||||
const QString text = urlLineEdit->text().trimmed();
|
||||
|
||||
if (!text.isEmpty()) {
|
||||
const QUrl url = QUrl::fromUserInput(text);
|
||||
if (!url.isValid()) {
|
||||
QMessageBox::warning(this, tr("Invalid URL"),
|
||||
tr("That doesn't look like a valid URL. Double-check it and try again, "
|
||||
"or clear the field to use the default source."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
writeCustomUrl(text);
|
||||
setState(State::Running);
|
||||
emit updateRequested();
|
||||
}
|
||||
|
||||
void CardDatabaseSetupPage::onRestoreDefaultUrl()
|
||||
{
|
||||
urlLineEdit->clear();
|
||||
writeCustomUrl(QString());
|
||||
}
|
||||
|
||||
void CardDatabaseSetupPage::setState(State newState)
|
||||
{
|
||||
state = newState;
|
||||
|
||||
progressBar->setVisible(state == State::Running);
|
||||
retryButton->setVisible(state == State::Failed);
|
||||
manualButton->setVisible(state == State::Failed);
|
||||
applyAndRetryButton->setEnabled(state != State::Running);
|
||||
|
||||
switch (state) {
|
||||
case State::NotStarted:
|
||||
statusLabel->setText(tr("Press Download to fetch the card database, or Skip to do it later."));
|
||||
break;
|
||||
case State::Running:
|
||||
statusLabel->setText(tr("Downloading the latest card database…"));
|
||||
break;
|
||||
case State::Succeeded:
|
||||
statusLabel->setText(tr("Card database ready ✓"));
|
||||
break;
|
||||
case State::Failed:
|
||||
statusLabel->setText(
|
||||
tr("Couldn't download the card database automatically. Check your connection and retry, "
|
||||
"set it up manually, or skip this for now — you can do it later from the Card Database menu."));
|
||||
break;
|
||||
}
|
||||
|
||||
emit completeChanged();
|
||||
}
|
||||
|
||||
bool CardDatabaseSetupPage::isComplete() const
|
||||
{
|
||||
return state != State::Running;
|
||||
}
|
||||
|
||||
bool CardDatabaseSetupPage::isSkippable() const
|
||||
{
|
||||
return state != State::Succeeded;
|
||||
}
|
||||
|
||||
QString CardDatabaseSetupPage::stepTitle() const
|
||||
{
|
||||
return tr("Card Database");
|
||||
}
|
||||
|
||||
QString CardDatabaseSetupPage::stepSubtitle() const
|
||||
{
|
||||
return tr("Cockatrice needs card data to know what you're playing with.");
|
||||
}
|
||||
|
||||
void CardDatabaseSetupPage::retranslateUi()
|
||||
{
|
||||
retryButton->setText(tr("Retry"));
|
||||
manualButton->setText(tr("Set up manually…"));
|
||||
|
||||
onToggleAdvanced(advancedToggleButton->isChecked());
|
||||
urlLineEdit->setPlaceholderText(tr("Leave blank to use the default source"));
|
||||
urlHintLabel->setText(tr("Only change this if you know you need a mirror or a custom card data source."));
|
||||
restoreDefaultUrlButton->setText(tr("Restore default"));
|
||||
applyAndRetryButton->setText(tr("Apply && retry"));
|
||||
|
||||
startupBehaviorLabel->setText(tr("Check for card database updates on startup"));
|
||||
startupBehaviorCombo->setItemText(0, tr("Don't check"));
|
||||
startupBehaviorCombo->setItemText(1, tr("Prompt for update"));
|
||||
startupBehaviorCombo->setItemText(2, tr("Always update in the background"));
|
||||
|
||||
checkIntervalLabel->setText(tr("Check for card database updates every"));
|
||||
checkIntervalSpinBox->setSuffix(tr(" days"));
|
||||
|
||||
setState(state);
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
#ifndef CARD_DATABASE_SETUP_PAGE_H
|
||||
#define CARD_DATABASE_SETUP_PAGE_H
|
||||
|
||||
#include "../first_run_wizard_page.h"
|
||||
|
||||
#include <QSize>
|
||||
|
||||
class QComboBox;
|
||||
class QLabel;
|
||||
class QLineEdit;
|
||||
class QProgressBar;
|
||||
class QPushButton;
|
||||
class QSpinBox;
|
||||
class QWidget;
|
||||
|
||||
class CardDatabaseSetupPage : public FirstRunWizardPage
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit CardDatabaseSetupPage(QWidget *parent = nullptr);
|
||||
|
||||
void initializePage() override;
|
||||
bool isComplete() const override;
|
||||
bool isSkippable() const override;
|
||||
QString stepTitle() const override;
|
||||
QString stepSubtitle() const override;
|
||||
QString nextButtonText() const override;
|
||||
bool handleNextClick() override;
|
||||
void retranslateUi() override;
|
||||
|
||||
void onUpdateFinished(bool success);
|
||||
|
||||
signals:
|
||||
void updateRequested();
|
||||
void manualSetupRequested();
|
||||
|
||||
private:
|
||||
enum class State
|
||||
{
|
||||
NotStarted,
|
||||
Running,
|
||||
Succeeded,
|
||||
Failed,
|
||||
};
|
||||
|
||||
void setState(State newState);
|
||||
bool alreadyHaveDatabase() const;
|
||||
|
||||
QString oracleSettingsFilePath() const;
|
||||
QString readCustomUrl() const;
|
||||
void writeCustomUrl(const QString &url);
|
||||
|
||||
void onToggleAdvanced(bool open);
|
||||
void onApplyCustomUrl();
|
||||
void onRestoreDefaultUrl();
|
||||
|
||||
QLabel *statusLabel;
|
||||
QProgressBar *progressBar;
|
||||
QPushButton *retryButton;
|
||||
QPushButton *manualButton;
|
||||
|
||||
QPushButton *advancedToggleButton;
|
||||
QWidget *advancedPanel;
|
||||
QLineEdit *urlLineEdit;
|
||||
QLabel *urlHintLabel;
|
||||
QPushButton *restoreDefaultUrlButton;
|
||||
QPushButton *applyAndRetryButton;
|
||||
|
||||
QLabel *startupBehaviorLabel;
|
||||
QComboBox *startupBehaviorCombo;
|
||||
QLabel *checkIntervalLabel;
|
||||
QSpinBox *checkIntervalSpinBox;
|
||||
|
||||
State state = State::NotStarted;
|
||||
QSize windowSizeBeforeExpansion;
|
||||
};
|
||||
|
||||
#endif // CARD_DATABASE_SETUP_PAGE_H
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
#include "finish_page.h"
|
||||
|
||||
#include <QLabel>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
FinishPage::FinishPage(QWidget *parent) : FirstRunWizardPage(parent)
|
||||
{
|
||||
bodyLabel = new QLabel(this);
|
||||
bodyLabel->setWordWrap(true);
|
||||
bodyLabel->setAlignment(Qt::AlignCenter);
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->addStretch();
|
||||
layout->addWidget(bodyLabel);
|
||||
layout->addStretch();
|
||||
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
QString FinishPage::stepTitle() const
|
||||
{
|
||||
return tr("You're All Set");
|
||||
}
|
||||
|
||||
void FinishPage::retranslateUi()
|
||||
{
|
||||
bodyLabel->setText(
|
||||
tr("That's everything for now. Jump into Settings any time to change your mind about any of this.\n\n"
|
||||
"Have fun!"));
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
#ifndef FINISH_PAGE_H
|
||||
#define FINISH_PAGE_H
|
||||
|
||||
#include "../first_run_wizard_page.h"
|
||||
|
||||
class QLabel;
|
||||
|
||||
class FinishPage : public FirstRunWizardPage
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit FinishPage(QWidget *parent = nullptr);
|
||||
|
||||
QString stepTitle() const override;
|
||||
void retranslateUi() override;
|
||||
|
||||
private:
|
||||
QLabel *bodyLabel;
|
||||
};
|
||||
|
||||
#endif // FINISH_PAGE_H
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
#include "preferences_setup_page.h"
|
||||
|
||||
#include "../../client/settings/cache_settings.h"
|
||||
#include "../../client/sound_engine.h"
|
||||
#include "libcockatrice/settings/interface_settings.h"
|
||||
#include "libcockatrice/settings/sound_settings.h"
|
||||
#include "libcockatrice/settings/tabs_settings.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QFormLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QLabel>
|
||||
#include <QScrollArea>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace
|
||||
{
|
||||
// The server destinations are omitted: during first run their tabs are not
|
||||
// open yet, and the wizard offers no way to fill in the server/room details.
|
||||
QList<StartupTab> wizardStartupTabOrder()
|
||||
{
|
||||
return {StartupTabHome, StartupTabVisualDeckStorage, StartupTabDeckStorage,
|
||||
StartupTabReplays, StartupTabDeckEditor, StartupTabVisualDeckEditor};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
PreferencesSetupPage::PreferencesSetupPage(QWidget *parent) : FirstRunWizardPage(parent)
|
||||
{
|
||||
auto *content = new QWidget;
|
||||
auto *contentLayout = new QVBoxLayout(content);
|
||||
|
||||
gameplayGroup = new QGroupBox(content);
|
||||
auto *gameplayLayout = new QVBoxLayout(gameplayGroup);
|
||||
contentLayout->addWidget(gameplayGroup);
|
||||
doubleClickToPlayCheckBox = new QCheckBox(gameplayGroup);
|
||||
horizontalHandCheckBox = new QCheckBox(gameplayGroup);
|
||||
playToStackCheckBox = new QCheckBox(gameplayGroup);
|
||||
gameplayLayout->addWidget(doubleClickToPlayCheckBox);
|
||||
gameplayLayout->addWidget(horizontalHandCheckBox);
|
||||
gameplayLayout->addWidget(playToStackCheckBox);
|
||||
|
||||
notificationsGroup = new QGroupBox(content);
|
||||
auto *notificationsLayout = new QVBoxLayout(notificationsGroup);
|
||||
contentLayout->addWidget(notificationsGroup);
|
||||
notificationsEnabledCheckBox = new QCheckBox(notificationsGroup);
|
||||
soundEnabledCheckBox = new QCheckBox(notificationsGroup);
|
||||
notificationsLayout->addWidget(notificationsEnabledCheckBox);
|
||||
notificationsLayout->addWidget(soundEnabledCheckBox);
|
||||
|
||||
startupGroup = new QGroupBox(content);
|
||||
auto *startupForm = new QFormLayout(startupGroup);
|
||||
contentLayout->addWidget(startupGroup);
|
||||
startupTabLabel = new QLabel(startupGroup);
|
||||
startupTabSelector = new QComboBox(startupGroup);
|
||||
startupTabSelector->setSizeAdjustPolicy(QComboBox::AdjustToContents);
|
||||
for (StartupTab tab : wizardStartupTabOrder()) {
|
||||
startupTabSelector->addItem(QString(), tab); // texts set in retranslateUi
|
||||
}
|
||||
startupForm->addRow(startupTabLabel, startupTabSelector);
|
||||
|
||||
contentLayout->addStretch();
|
||||
|
||||
auto *scrollArea = new QScrollArea(this);
|
||||
scrollArea->setWidget(content);
|
||||
scrollArea->setWidgetResizable(true);
|
||||
scrollArea->setFrameShape(QFrame::NoFrame);
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->addWidget(scrollArea);
|
||||
|
||||
SettingsCache &settings = SettingsCache::instance();
|
||||
|
||||
connect(doubleClickToPlayCheckBox, &QCheckBox::toggled, &settings.userInterface(),
|
||||
&InterfaceSettings::setDoubleClickToPlay);
|
||||
connect(horizontalHandCheckBox, &QCheckBox::toggled, &settings.userInterface(),
|
||||
&InterfaceSettings::setHorizontalHand);
|
||||
connect(playToStackCheckBox, &QCheckBox::toggled, &settings.userInterface(), &InterfaceSettings::setPlayToStack);
|
||||
|
||||
connect(notificationsEnabledCheckBox, &QCheckBox::toggled, &settings.userInterface(),
|
||||
&InterfaceSettings::setNotificationsEnabled);
|
||||
connect(soundEnabledCheckBox, &QCheckBox::toggled, &settings.sound(), &SoundSettings::setSoundEnabled);
|
||||
connect(soundEnabledCheckBox, &QCheckBox::toggled, soundEngine, &SoundEngine::testSound);
|
||||
|
||||
connect(startupTabSelector, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int index) {
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
SettingsCache::instance().tabs().setStartupTabIndex(startupTabSelector->itemData(index).toInt());
|
||||
});
|
||||
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
void PreferencesSetupPage::initializePage()
|
||||
{
|
||||
SettingsCache &settings = SettingsCache::instance();
|
||||
|
||||
doubleClickToPlayCheckBox->setChecked(settings.userInterface().getDoubleClickToPlay());
|
||||
horizontalHandCheckBox->setChecked(settings.userInterface().getHorizontalHand());
|
||||
playToStackCheckBox->setChecked(settings.userInterface().getPlayToStack());
|
||||
|
||||
notificationsEnabledCheckBox->setChecked(settings.userInterface().getNotificationsEnabled());
|
||||
soundEnabledCheckBox->setChecked(settings.sound().getSoundEnabled());
|
||||
|
||||
startupTabSelector->setCurrentIndex(startupTabSelector->findData(settings.tabs().getStartupTabIndex()));
|
||||
}
|
||||
|
||||
bool PreferencesSetupPage::isSkippable() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
QString PreferencesSetupPage::stepTitle() const
|
||||
{
|
||||
return tr("A Few Preferences");
|
||||
}
|
||||
|
||||
QString PreferencesSetupPage::stepSubtitle() const
|
||||
{
|
||||
return tr("Defaults are fine — tweak these now or from Settings anytime.");
|
||||
}
|
||||
|
||||
void PreferencesSetupPage::retranslateUi()
|
||||
{
|
||||
gameplayGroup->setTitle(tr("Gameplay"));
|
||||
doubleClickToPlayCheckBox->setText(tr("Double-click cards to play them"));
|
||||
doubleClickToPlayCheckBox->setToolTip(tr("When disabled, a single click plays the selected card onto the table."));
|
||||
horizontalHandCheckBox->setText(tr("Display hand horizontally"));
|
||||
horizontalHandCheckBox->setToolTip(
|
||||
tr("Shows your hand as a row along the bottom of the table instead of a column beside it."));
|
||||
playToStackCheckBox->setText(tr("Play all nonlands onto the stack by default"));
|
||||
playToStackCheckBox->setToolTip(
|
||||
tr("Cards you play appear on the stack so other players can respond to them, as in a tabletop game."));
|
||||
|
||||
notificationsGroup->setTitle(tr("Notifications && Sound"));
|
||||
notificationsEnabledCheckBox->setText(tr("Show desktop notifications"));
|
||||
soundEnabledCheckBox->setText(tr("Play sound effects"));
|
||||
|
||||
startupGroup->setTitle(tr("Startup"));
|
||||
startupTabLabel->setText(tr("Startup tab:"));
|
||||
const QList<StartupTab> tabs = wizardStartupTabOrder();
|
||||
for (int i = 0; i < tabs.size(); ++i) {
|
||||
QString name;
|
||||
switch (tabs[i]) {
|
||||
case StartupTabHome:
|
||||
name = tr("Home");
|
||||
break;
|
||||
case StartupTabVisualDeckStorage:
|
||||
name = tr("Visual Deck Storage");
|
||||
break;
|
||||
case StartupTabDeckStorage:
|
||||
name = tr("Deck Storage");
|
||||
break;
|
||||
case StartupTabReplays:
|
||||
name = tr("Game Replays");
|
||||
break;
|
||||
case StartupTabDeckEditor:
|
||||
name = tr("Deck Editor");
|
||||
break;
|
||||
case StartupTabVisualDeckEditor:
|
||||
name = tr("Visual Deck Editor");
|
||||
break;
|
||||
case StartupTabServer:
|
||||
name = tr("Server");
|
||||
break;
|
||||
case StartupTabServerRoom:
|
||||
name = tr("Server Room");
|
||||
break;
|
||||
}
|
||||
startupTabSelector->setItemText(i, name);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
#ifndef PREFERENCES_SETUP_PAGE_H
|
||||
#define PREFERENCES_SETUP_PAGE_H
|
||||
|
||||
#include "../first_run_wizard_page.h"
|
||||
|
||||
class QCheckBox;
|
||||
class QComboBox;
|
||||
class QGroupBox;
|
||||
class QLabel;
|
||||
|
||||
/** @brief A curated subset of settings for the user to adjust.
|
||||
**/
|
||||
class PreferencesSetupPage : public FirstRunWizardPage
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit PreferencesSetupPage(QWidget *parent = nullptr);
|
||||
|
||||
void initializePage() override;
|
||||
bool isSkippable() const override;
|
||||
QString stepTitle() const override;
|
||||
QString stepSubtitle() const override;
|
||||
void retranslateUi() override;
|
||||
|
||||
private:
|
||||
QGroupBox *gameplayGroup;
|
||||
QCheckBox *doubleClickToPlayCheckBox;
|
||||
QCheckBox *horizontalHandCheckBox;
|
||||
QCheckBox *playToStackCheckBox;
|
||||
|
||||
QGroupBox *notificationsGroup;
|
||||
QCheckBox *notificationsEnabledCheckBox;
|
||||
QCheckBox *soundEnabledCheckBox;
|
||||
|
||||
QGroupBox *startupGroup;
|
||||
QLabel *startupTabLabel;
|
||||
QComboBox *startupTabSelector;
|
||||
};
|
||||
|
||||
#endif // PREFERENCES_SETUP_PAGE_H
|
||||
|
|
@ -0,0 +1,231 @@
|
|||
#include "theme_setup_page.h"
|
||||
|
||||
#include "../../client/settings/cache_settings.h"
|
||||
#include "../../interface/palette_editor/palette_generator.h"
|
||||
#include "../../interface/palette_editor/quick_setup_panel.h"
|
||||
#include "../../interface/theme_manager.h"
|
||||
#include "../../interface/widgets/general/background_sources.h"
|
||||
#include "libcockatrice/settings/appearance_settings.h"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFormLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/settings/paths_settings.h>
|
||||
#include <libcockatrice/settings/personal_settings.h>
|
||||
|
||||
ThemeSetupPage::ThemeSetupPage(QWidget *parent) : FirstRunWizardPage(parent)
|
||||
{
|
||||
themeCombo = new QComboBox(this);
|
||||
schemeCombo = new QComboBox(this);
|
||||
schemeCombo->addItem(tr("Light"), QStringLiteral("Light"));
|
||||
schemeCombo->addItem(tr("Dark"), QStringLiteral("Dark"));
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
|
||||
schemeCombo->addItem(tr("Match system"), QStringLiteral("System"));
|
||||
#endif
|
||||
|
||||
quickSetupPanel = new QuickSetupPanel(this);
|
||||
|
||||
connect(themeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ThemeSetupPage::onThemeChanged);
|
||||
connect(schemeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ThemeSetupPage::onSchemeChanged);
|
||||
connect(quickSetupPanel, &QuickSetupPanel::valueChanged, this, &ThemeSetupPage::onGenerateFromAccent);
|
||||
|
||||
homeTabBackgroundCombo = new QComboBox(this);
|
||||
for (const auto &entry : BackgroundSources::all()) {
|
||||
homeTabBackgroundCombo->addItem(QObject::tr(entry.trKey), QVariant::fromValue(entry.type));
|
||||
}
|
||||
connect(homeTabBackgroundCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
|
||||
&ThemeSetupPage::onHomeTabBackgroundChanged);
|
||||
|
||||
// Keep the scheme combo honest when the *theme* changes underneath it
|
||||
// (switching theme reloads that theme's own stored colorScheme), and
|
||||
// opportunistically seed a palette for themes that ship none at all.
|
||||
// Mirrors AppearanceSettingsPage's identical listener for the combo-sync
|
||||
// half of this.
|
||||
connect(themeManager, &ThemeManager::themeChanged, this, [this] {
|
||||
const QString newDir = themeManager->getAvailableThemes().value(SettingsCache::instance().getThemeName());
|
||||
const ThemeConfig cfg = ThemeConfig::fromThemeDir(newDir);
|
||||
const QString current = cfg.colorScheme;
|
||||
|
||||
schemeCombo->blockSignals(true);
|
||||
const int idx = schemeCombo->findData(current);
|
||||
schemeCombo->setCurrentIndex(idx >= 0 ? idx : 0);
|
||||
schemeCombo->blockSignals(false);
|
||||
|
||||
maybeAutoGeneratePalette();
|
||||
});
|
||||
|
||||
auto *form = new QFormLayout;
|
||||
form->addRow(tr("Theme:"), themeCombo);
|
||||
form->addRow(tr("Appearance:"), schemeCombo);
|
||||
form->addRow(tr("Home screen background:"), homeTabBackgroundCombo);
|
||||
|
||||
accentGroup = new QGroupBox(this);
|
||||
auto *accentLayout = new QVBoxLayout(accentGroup);
|
||||
accentLayout->addWidget(quickSetupPanel);
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->addLayout(form);
|
||||
layout->addWidget(accentGroup);
|
||||
layout->addStretch();
|
||||
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
void ThemeSetupPage::initializePage()
|
||||
{
|
||||
themeCombo->blockSignals(true);
|
||||
themeCombo->clear();
|
||||
const QString currentTheme = SettingsCache::instance().getThemeName();
|
||||
for (const QString &name : themeManager->getAvailableThemes().keys()) {
|
||||
themeCombo->addItem(name);
|
||||
}
|
||||
const int idx = themeCombo->findText(currentTheme);
|
||||
themeCombo->setCurrentIndex(idx >= 0 ? idx : 0);
|
||||
themeCombo->blockSignals(false);
|
||||
|
||||
const QString dirPath = themeManager->getAvailableThemes().value(SettingsCache::instance().getThemeName());
|
||||
const ThemeConfig cfg = ThemeConfig::fromThemeDir(dirPath);
|
||||
schemeCombo->blockSignals(true);
|
||||
const int schemeIdx = schemeCombo->findData(cfg.colorScheme);
|
||||
schemeCombo->setCurrentIndex(schemeIdx >= 0 ? schemeIdx : 0);
|
||||
schemeCombo->blockSignals(false);
|
||||
|
||||
homeTabBackgroundCombo->blockSignals(true);
|
||||
QString homeTabSource = SettingsCache::instance().appearance().getHomeTabBackgroundSource();
|
||||
int homeTabIdx = homeTabBackgroundCombo->findData(BackgroundSources::fromId(homeTabSource));
|
||||
homeTabBackgroundCombo->setCurrentIndex(homeTabIdx >= 0 ? homeTabIdx : 0);
|
||||
homeTabBackgroundCombo->blockSignals(false);
|
||||
|
||||
// Opening the page must not touch the running application's palette:
|
||||
// previews and auto-generation only happen in response to the user
|
||||
// actually changing a control, never on mere page visibility.
|
||||
paletteDirty = false;
|
||||
}
|
||||
|
||||
QString ThemeSetupPage::currentScheme() const
|
||||
{
|
||||
return schemeCombo->currentData().toString();
|
||||
}
|
||||
|
||||
QString ThemeSetupPage::resolvedScheme() const
|
||||
{
|
||||
const QString scheme = currentScheme();
|
||||
if (scheme.isEmpty() || scheme == QStringLiteral("System")) {
|
||||
return themeManager->isDarkMode(themeManager->getCurrentThemePath()) ? "Dark" : "Light";
|
||||
}
|
||||
return scheme;
|
||||
}
|
||||
|
||||
void ThemeSetupPage::onThemeChanged(int index)
|
||||
{
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
paletteDirty = false;
|
||||
SettingsCache::instance().setThemeName(themeCombo->itemText(index));
|
||||
// Scheme-combo sync and auto-generation both happen via the
|
||||
// ThemeManager::themeChanged listener above, triggered by setThemeName.
|
||||
}
|
||||
|
||||
void ThemeSetupPage::onSchemeChanged()
|
||||
{
|
||||
themeManager->setColorScheme(currentScheme());
|
||||
}
|
||||
|
||||
void ThemeSetupPage::onHomeTabBackgroundChanged(int index)
|
||||
{
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
auto type = homeTabBackgroundCombo->currentData().value<BackgroundSources::Type>();
|
||||
SettingsCache::instance().appearance().setHomeTabBackgroundSource(BackgroundSources::toId(type));
|
||||
}
|
||||
|
||||
void ThemeSetupPage::onGenerateFromAccent(const QColor &accent, int intensity)
|
||||
{
|
||||
PaletteConfig cfg = PaletteGenerator::fromAccent(accent, intensity, resolvedScheme());
|
||||
themeManager->previewPalette(cfg, resolvedScheme());
|
||||
paletteDirty = true;
|
||||
}
|
||||
|
||||
void ThemeSetupPage::maybeAutoGeneratePalette()
|
||||
{
|
||||
const QString dirPath = themeManager->getAvailableThemes().value(SettingsCache::instance().getThemeName());
|
||||
const QString scheme = resolvedScheme();
|
||||
|
||||
if (PaletteConfig::fromScheme(dirPath, scheme).hasPalette() ||
|
||||
PaletteConfig::fromDefault(dirPath, scheme).hasPalette()) {
|
||||
return; // theme already has something real to show -- leave it alone
|
||||
}
|
||||
|
||||
// The theme+scheme combination has nothing saved and nothing shipped, and
|
||||
// the user just switched to it. Rather than leaving a flat, unstyled look,
|
||||
// seed one from whatever accent QuickSetupPanel currently holds and mark
|
||||
// it dirty so it's written to disk if the user moves on. Only ever reached
|
||||
// through user interaction (theme/scheme change, accent drag) -- never on
|
||||
// page open.
|
||||
PaletteConfig generated =
|
||||
PaletteGenerator::fromAccent(quickSetupPanel->accentColor(), quickSetupPanel->intensity(), scheme);
|
||||
themeManager->previewPalette(generated, scheme);
|
||||
paletteDirty = true;
|
||||
}
|
||||
|
||||
bool ThemeSetupPage::validatePage()
|
||||
{
|
||||
if (paletteDirty) {
|
||||
const QString scheme = resolvedScheme();
|
||||
PaletteConfig cfg =
|
||||
PaletteGenerator::fromAccent(quickSetupPanel->accentColor(), quickSetupPanel->intensity(), scheme);
|
||||
if (!ThemeManager::commitPalette(writableThemeDir(), scheme, cfg)) {
|
||||
QMessageBox::warning(this, tr("Save failed"),
|
||||
tr("Could not write the theme palette to:\n%1").arg(writableThemeDir()));
|
||||
return false;
|
||||
}
|
||||
themeManager->reloadCurrentTheme();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
QString ThemeSetupPage::writableThemeDir() const
|
||||
{
|
||||
// Built-in themes resolve to the read-only system themes directory;
|
||||
// palette edits must go to the user themes directory instead, exactly
|
||||
// as PaletteEditorDialog does.
|
||||
const QString dirPath = themeManager->getAvailableThemes().value(SettingsCache::instance().getThemeName());
|
||||
if (!dirPath.isEmpty()) {
|
||||
const QString probe = QDir(dirPath).absoluteFilePath(".cockatrice_write_test");
|
||||
QFile f(probe);
|
||||
if (f.open(QIODevice::WriteOnly)) {
|
||||
f.close();
|
||||
f.remove();
|
||||
return dirPath;
|
||||
}
|
||||
}
|
||||
return QDir(SettingsCache::instance().paths().getThemesPath())
|
||||
.absoluteFilePath(SettingsCache::instance().getThemeName());
|
||||
}
|
||||
|
||||
bool ThemeSetupPage::isSkippable() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
QString ThemeSetupPage::stepTitle() const
|
||||
{
|
||||
return tr("Pick a Look");
|
||||
}
|
||||
|
||||
QString ThemeSetupPage::stepSubtitle() const
|
||||
{
|
||||
return tr("You can fine-tune every colour later from Settings → Appearance.");
|
||||
}
|
||||
|
||||
void ThemeSetupPage::retranslateUi()
|
||||
{
|
||||
accentGroup->setTitle(tr("Accent colour (optional)"));
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
#ifndef THEME_SETUP_PAGE_H
|
||||
#define THEME_SETUP_PAGE_H
|
||||
|
||||
#include "../first_run_wizard_page.h"
|
||||
|
||||
class QComboBox;
|
||||
class QGroupBox;
|
||||
class QuickSetupPanel;
|
||||
|
||||
/** @brief First-run theme step. Reuses the same building blocks as Appearance
|
||||
* settings and the Palette Editor (ThemeManager, PaletteConfig,
|
||||
* PaletteGenerator, and the QuickSetupPanel widget itself) rather than
|
||||
* reimplementing palette generation or preview here.
|
||||
*
|
||||
* Behavior specific to this page (deliberately not pushed down into
|
||||
* ThemeManager, to avoid changing app-wide behaviour for existing installs):
|
||||
* - Opening the page never changes the running palette; previews and
|
||||
* auto-generation only happen when the user actually changes a control.
|
||||
* - If a theme+scheme the user selects has no saved palette and no shipped
|
||||
* default, one is generated from the QuickSetupPanel's current accent so
|
||||
* the preview doesn't fall back to a flat, unstyled look. */
|
||||
class ThemeSetupPage : public FirstRunWizardPage
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ThemeSetupPage(QWidget *parent = nullptr);
|
||||
|
||||
void initializePage() override;
|
||||
bool validatePage() override;
|
||||
bool isSkippable() const override;
|
||||
QString stepTitle() const override;
|
||||
QString stepSubtitle() const override;
|
||||
void retranslateUi() override;
|
||||
|
||||
private slots:
|
||||
void onThemeChanged(int index);
|
||||
void onSchemeChanged();
|
||||
void onGenerateFromAccent(const QColor &accent, int intensity);
|
||||
void onHomeTabBackgroundChanged(int index);
|
||||
|
||||
private:
|
||||
QString currentScheme() const;
|
||||
QString resolvedScheme() const; // "System" -> actual Light/Dark
|
||||
void maybeAutoGeneratePalette();
|
||||
QString writableThemeDir() const;
|
||||
|
||||
QComboBox *themeCombo;
|
||||
QComboBox *schemeCombo;
|
||||
QGroupBox *accentGroup;
|
||||
QuickSetupPanel *quickSetupPanel;
|
||||
|
||||
QComboBox *homeTabBackgroundCombo;
|
||||
|
||||
bool paletteDirty = false;
|
||||
};
|
||||
|
||||
#endif // THEME_SETUP_PAGE_H
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
#include "welcome_page.h"
|
||||
|
||||
#include "../../../../main.h"
|
||||
#include "../../client/settings/cache_settings.h"
|
||||
#include "../../settings_page/general_settings_page.h"
|
||||
#include "libcockatrice/settings/personal_settings.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QComboBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLocale>
|
||||
#include <QTranslator>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
WelcomePage::WelcomePage(QWidget *parent) : FirstRunWizardPage(parent)
|
||||
{
|
||||
bodyLabel = new QLabel(this);
|
||||
bodyLabel->setWordWrap(true);
|
||||
bodyLabel->setAlignment(Qt::AlignCenter);
|
||||
|
||||
languageLabel = new QLabel(this);
|
||||
langCombo = new QComboBox(this);
|
||||
for (const QString &code : GeneralSettingsPage::findQmFiles()) {
|
||||
langCombo->addItem(GeneralSettingsPage::languageName(code), code);
|
||||
}
|
||||
|
||||
QString current = SettingsCache::instance().personal().getLang();
|
||||
if (current.isEmpty()) {
|
||||
current = QLocale::system().name();
|
||||
}
|
||||
int index = langCombo->findData(current);
|
||||
if (index < 0) {
|
||||
index = langCombo->findData(current.section('_', 0, 0));
|
||||
}
|
||||
if (index >= 0) {
|
||||
langCombo->setCurrentIndex(index);
|
||||
}
|
||||
|
||||
connect(langCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &WelcomePage::languageChanged);
|
||||
|
||||
auto *languageRow = new QHBoxLayout;
|
||||
languageRow->addStretch();
|
||||
languageRow->addWidget(languageLabel);
|
||||
languageRow->addWidget(langCombo);
|
||||
languageRow->addStretch();
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->addStretch();
|
||||
layout->addWidget(bodyLabel);
|
||||
layout->addStretch();
|
||||
layout->addLayout(languageRow);
|
||||
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
void WelcomePage::languageChanged(int index)
|
||||
{
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
SettingsCache::instance().personal().setLang(langCombo->itemData(index).toString());
|
||||
qApp->removeTranslator(translator); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast)
|
||||
installNewTranslator();
|
||||
}
|
||||
|
||||
QString WelcomePage::stepTitle() const
|
||||
{
|
||||
return tr("Welcome!");
|
||||
}
|
||||
|
||||
void WelcomePage::retranslateUi()
|
||||
{
|
||||
bodyLabel->setText(tr("Let's get you set up. This will only take a minute — "
|
||||
"we'll grab the card database, pick a look you like, "
|
||||
"and get you ready to connect to a server.\n\n"
|
||||
"You can change any of this later from Settings."));
|
||||
languageLabel->setText(tr("Language:"));
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
#ifndef WELCOME_PAGE_H
|
||||
#define WELCOME_PAGE_H
|
||||
|
||||
#include "../first_run_wizard_page.h"
|
||||
|
||||
class QComboBox;
|
||||
class QLabel;
|
||||
|
||||
class WelcomePage : public FirstRunWizardPage
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit WelcomePage(QWidget *parent = nullptr);
|
||||
|
||||
QString stepTitle() const override;
|
||||
void retranslateUi() override;
|
||||
|
||||
private slots:
|
||||
void languageChanged(int index);
|
||||
|
||||
private:
|
||||
QLabel *bodyLabel;
|
||||
QLabel *languageLabel;
|
||||
QComboBox *langCombo;
|
||||
};
|
||||
|
||||
#endif // WELCOME_PAGE_H
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
ShaderEffect {
|
||||
id: effectA
|
||||
anchors.fill: parent
|
||||
opacity: bannerConfig.frontIsA ? 1.0 : 0.0
|
||||
Behavior on opacity { NumberAnimation { duration: 450; easing.type: Easing.InOutCubic } }
|
||||
property real iTime: bannerConfig.time
|
||||
property real uAspect: bannerConfig.aspect
|
||||
property real uMode: bannerConfig.modeA
|
||||
property real uSpeed: bannerConfig.speedA
|
||||
property real uSeed: bannerConfig.seedA
|
||||
property vector4d uColorA: Qt.vector4d(bannerConfig.colorA.r, bannerConfig.colorA.g, bannerConfig.colorA.b, 1.0)
|
||||
property vector4d uColorB: Qt.vector4d(bannerConfig.colorB.r, bannerConfig.colorB.g, bannerConfig.colorB.b, 1.0)
|
||||
property vector4d uAccent: Qt.vector4d(bannerConfig.accent.r, bannerConfig.accent.g, bannerConfig.accent.b, 1.0)
|
||||
property real uLogoGlow: bannerConfig.logoGlow
|
||||
fragmentShader: "qrc:/onboarding/shaders/brand_banner.frag.qsb"
|
||||
}
|
||||
|
||||
ShaderEffect {
|
||||
id: effectB
|
||||
anchors.fill: parent
|
||||
opacity: bannerConfig.frontIsA ? 0.0 : 1.0
|
||||
Behavior on opacity { NumberAnimation { duration: 450; easing.type: Easing.InOutCubic } }
|
||||
property real iTime: bannerConfig.time
|
||||
property real uAspect: bannerConfig.aspect
|
||||
property real uMode: bannerConfig.modeB
|
||||
property real uSpeed: bannerConfig.speedB
|
||||
property real uSeed: bannerConfig.seedB
|
||||
property vector4d uColorA: Qt.vector4d(bannerConfig.colorA.r, bannerConfig.colorA.g, bannerConfig.colorA.b, 1.0)
|
||||
property vector4d uColorB: Qt.vector4d(bannerConfig.colorB.r, bannerConfig.colorB.g, bannerConfig.colorB.b, 1.0)
|
||||
property vector4d uAccent: Qt.vector4d(bannerConfig.accent.r, bannerConfig.accent.g, bannerConfig.accent.b, 1.0)
|
||||
property real uLogoGlow: bannerConfig.logoGlow
|
||||
fragmentShader: "qrc:/onboarding/shaders/brand_banner.frag.qsb"
|
||||
}
|
||||
|
||||
// The hero logo itself — breathes cleanly over a 0.5–1.0 opacity range
|
||||
Image {
|
||||
id: logo
|
||||
anchors.centerIn: parent
|
||||
visible: bannerConfig.logoVisible
|
||||
source: "qrc:/resources/cockatrice-logo-white.svg"
|
||||
width: root.height * 0.6
|
||||
height: width * (sourceSize.height > 0 ? sourceSize.height / Math.max(sourceSize.width, 1) : 1)
|
||||
fillMode: Image.PreserveAspectFit
|
||||
smooth: true
|
||||
opacity: 0.5 + 0.5 * bannerConfig.logoGlow
|
||||
sourceSize: Qt.size(256, 256)
|
||||
|
||||
Behavior on opacity { NumberAnimation { duration: 300; easing.type: Easing.InOutSine } }
|
||||
|
||||
transform: Scale {
|
||||
origin.x: logo.width / 2
|
||||
origin.y: logo.height / 2
|
||||
xScale: 0.94 + 0.06 * bannerConfig.logoGlow
|
||||
yScale: 0.94 + 0.06 * bannerConfig.logoGlow
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,195 @@
|
|||
#include "shader_banner_widget.h"
|
||||
|
||||
#include "banner_shader_config.h"
|
||||
|
||||
#include <QPainter>
|
||||
#include <QQmlContext>
|
||||
#include <QQmlEngine>
|
||||
#include <QQuickWidget>
|
||||
#include <QResizeEvent>
|
||||
#include <QStackedLayout>
|
||||
|
||||
namespace
|
||||
{
|
||||
// Near-black base palette -- the background is dark and quiet so the green
|
||||
// accent stands out.
|
||||
constexpr QRgb kColorA = 0x1A1A20;
|
||||
constexpr QRgb kColorB = 0x0E0E12;
|
||||
constexpr QRgb kAccent = 0x8BDD6B;
|
||||
} // namespace
|
||||
|
||||
class GradientFallbackWidget : public QWidget
|
||||
{
|
||||
public:
|
||||
using QWidget::QWidget;
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *) override
|
||||
{
|
||||
QPainter painter(this);
|
||||
QLinearGradient gradient(0, 0, width(), height());
|
||||
gradient.setColorAt(0.0, QColor(kColorA));
|
||||
gradient.setColorAt(1.0, QColor(kColorB));
|
||||
painter.fillRect(rect(), gradient);
|
||||
}
|
||||
};
|
||||
|
||||
BannerHost::BannerHost(QWidget *parent) : QWidget(parent)
|
||||
{
|
||||
setFixedHeight(150);
|
||||
|
||||
stack = new QStackedLayout(this);
|
||||
stack->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
fallback = new GradientFallbackWidget(this);
|
||||
stack->addWidget(fallback);
|
||||
|
||||
quickWidget = new QQuickWidget(this);
|
||||
quickWidget->setResizeMode(QQuickWidget::SizeRootObjectToView);
|
||||
|
||||
config = new BannerShaderConfig(quickWidget->engine());
|
||||
quickWidget->rootContext()->setContextProperty("bannerConfig", config);
|
||||
quickWidget->setSource(QUrl("qrc:/onboarding/qml/BrandBanner.qml"));
|
||||
|
||||
if (quickWidget->status() == QQuickWidget::Error) {
|
||||
activateFallback();
|
||||
} else {
|
||||
connect(quickWidget, &QQuickWidget::sceneGraphError, this, &BannerHost::onSceneGraphFailed);
|
||||
stack->addWidget(quickWidget);
|
||||
stack->setCurrentWidget(quickWidget);
|
||||
}
|
||||
|
||||
connect(&clock, &QTimer::timeout, this, &BannerHost::tick);
|
||||
clock.setInterval(16); // ~60fps; the shader itself is cheap, this is just a wall clock
|
||||
|
||||
applyMotifPreset(currentMotif);
|
||||
updateAspect();
|
||||
}
|
||||
|
||||
void BannerHost::activateFallback()
|
||||
{
|
||||
if (usingFallback) {
|
||||
return;
|
||||
}
|
||||
usingFallback = true;
|
||||
clock.stop();
|
||||
stack->setCurrentWidget(fallback);
|
||||
|
||||
if (quickWidget) {
|
||||
quickWidget->deleteLater(); // takes BannerShaderConfig (parented to its engine) with it
|
||||
quickWidget = nullptr;
|
||||
config = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void BannerHost::onSceneGraphFailed()
|
||||
{
|
||||
activateFallback();
|
||||
}
|
||||
|
||||
void BannerHost::setMotif(Motif motif)
|
||||
{
|
||||
currentMotif = motif;
|
||||
applyMotifPreset(motif);
|
||||
}
|
||||
|
||||
BannerHost::Preset BannerHost::presetFor(Motif motif)
|
||||
{
|
||||
// speed/seed tuned per motif so e.g. the network "pulse" (Account) reads
|
||||
// at a deliberately calmer cadence than the data "scan" lines
|
||||
// (Preferences), even though both come from the same shader.
|
||||
switch (motif) {
|
||||
case Motif::Welcome:
|
||||
return {0.0, 0.6, 0.15};
|
||||
case Motif::CardDatabase:
|
||||
return {1.0, 1.3, 0.42};
|
||||
case Motif::Theming:
|
||||
return {2.0, 1.2, 0.73};
|
||||
case Motif::Account:
|
||||
return {3.0, 0.8, 0.28};
|
||||
case Motif::Preferences:
|
||||
return {4.0, 1.0, 0.61};
|
||||
case Motif::Finish:
|
||||
return {5.0, 1.0, 0.91};
|
||||
}
|
||||
return {0.0, 0.6, 0.15};
|
||||
}
|
||||
|
||||
void BannerHost::applyMotifPreset(Motif motif)
|
||||
{
|
||||
if (usingFallback || !config) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Preset p = presetFor(motif);
|
||||
|
||||
config->setColorA(QColor(kColorA));
|
||||
config->setColorB(QColor(kColorB));
|
||||
config->setAccent(QColor(kAccent));
|
||||
config->setLogoVisible(motif == Motif::Welcome);
|
||||
|
||||
if (isFirstApply) {
|
||||
// Nothing on screen yet -- write straight into the front bank, no
|
||||
// crossfade needed for the very first paint.
|
||||
config->setModeA(p.mode);
|
||||
config->setSpeedA(p.speed);
|
||||
config->setSeedA(p.seed);
|
||||
config->setFrontIsA(true);
|
||||
isFirstApply = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Write the new preset into whichever bank is currently hidden, then
|
||||
// flip which one is front. QML's opacity Behavior does the actual
|
||||
// crossfade -- BannerHost never animates anything itself.
|
||||
if (config->frontIsA()) {
|
||||
config->setModeB(p.mode);
|
||||
config->setSpeedB(p.speed);
|
||||
config->setSeedB(p.seed);
|
||||
config->setFrontIsA(false);
|
||||
} else {
|
||||
config->setModeA(p.mode);
|
||||
config->setSpeedA(p.speed);
|
||||
config->setSeedA(p.seed);
|
||||
config->setFrontIsA(true);
|
||||
}
|
||||
}
|
||||
|
||||
void BannerHost::updateAspect()
|
||||
{
|
||||
if (config && height() > 0) {
|
||||
config->setAspect(qreal(width()) / qreal(height()));
|
||||
}
|
||||
}
|
||||
|
||||
void BannerHost::resizeEvent(QResizeEvent *event)
|
||||
{
|
||||
QWidget::resizeEvent(event);
|
||||
updateAspect();
|
||||
}
|
||||
|
||||
void BannerHost::showEvent(QShowEvent *event)
|
||||
{
|
||||
QWidget::showEvent(event);
|
||||
if (!usingFallback) {
|
||||
elapsed.restart();
|
||||
clock.start();
|
||||
}
|
||||
}
|
||||
|
||||
void BannerHost::hideEvent(QHideEvent *event)
|
||||
{
|
||||
QWidget::hideEvent(event);
|
||||
clock.stop();
|
||||
}
|
||||
|
||||
void BannerHost::tick()
|
||||
{
|
||||
if (config) {
|
||||
qreal t = elapsed.elapsed() / 1000.0;
|
||||
config->setTime(t);
|
||||
// Visible breathing for the logo: oscillates between 0.0 and 1.0
|
||||
qreal glow = 0.5 + 0.5 * qSin(t * 0.4);
|
||||
config->setLogoGlow(glow);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
#ifndef SHADER_BANNER_WIDGET_H
|
||||
#define SHADER_BANNER_WIDGET_H
|
||||
|
||||
#include <QElapsedTimer>
|
||||
#include <QTimer>
|
||||
#include <QWidget>
|
||||
|
||||
class BannerShaderConfig;
|
||||
class QQuickWidget;
|
||||
class GradientFallbackWidget;
|
||||
class QStackedLayout;
|
||||
|
||||
/** @brief Onboarding banner: a subtle, looping brand-shader animation, one of six
|
||||
* per-page "motifs" driving the same prebaked fragment shader
|
||||
* (onboarding/shaders/brand_banner.frag) with different uniform values, so
|
||||
* every page feels distinct but unmistakably part of the same family.
|
||||
*
|
||||
* Motif switches crossfade smoothly (see BrandBanner.qml's two stacked
|
||||
* ShaderEffect layers + Behavior on opacity) rather than cutting instantly
|
||||
* -- BannerHost just writes the new preset into whichever layer is
|
||||
* currently hidden and flips BannerShaderConfig::frontIsA; QML handles the
|
||||
* actual animation declaratively.
|
||||
*
|
||||
* Falls back to a static two-stop gradient (no shader, no QQuickWidget) if
|
||||
* the platform's Qt Quick scenegraph can't initialize -- e.g. software
|
||||
* rendering only, or a CI/VM environment with no GPU -- so onboarding
|
||||
* never blocks or blanks out over a graphics driver problem. The fallback
|
||||
* is permanent for the lifetime of this widget once triggered. */
|
||||
class BannerHost : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum class Motif
|
||||
{
|
||||
Welcome,
|
||||
CardDatabase,
|
||||
Theming,
|
||||
Account,
|
||||
Preferences,
|
||||
Finish,
|
||||
};
|
||||
|
||||
explicit BannerHost(QWidget *parent = nullptr);
|
||||
|
||||
void setMotif(Motif motif);
|
||||
|
||||
protected:
|
||||
void showEvent(QShowEvent *event) override;
|
||||
void hideEvent(QHideEvent *event) override;
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
|
||||
private slots:
|
||||
void tick();
|
||||
void onSceneGraphFailed();
|
||||
|
||||
private:
|
||||
struct Preset
|
||||
{
|
||||
qreal mode;
|
||||
qreal speed;
|
||||
qreal seed;
|
||||
};
|
||||
|
||||
static Preset presetFor(Motif motif);
|
||||
|
||||
void applyMotifPreset(Motif motif);
|
||||
void updateAspect();
|
||||
void activateFallback();
|
||||
|
||||
QStackedLayout *stack;
|
||||
QQuickWidget *quickWidget = nullptr;
|
||||
BannerShaderConfig *config = nullptr;
|
||||
GradientFallbackWidget *fallback = nullptr;
|
||||
|
||||
QTimer clock;
|
||||
QElapsedTimer elapsed;
|
||||
Motif currentMotif = Motif::Welcome;
|
||||
bool usingFallback = false;
|
||||
bool isFirstApply = true;
|
||||
};
|
||||
|
||||
#endif // SHADER_BANNER_WIDGET_H
|
||||
|
|
@ -0,0 +1,461 @@
|
|||
#version 440
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
// brand_banner.frag
|
||||
//
|
||||
// One shader, six motifs (uMode 0..5). All motifs composite over a shared
|
||||
// backgroundField() whose colour is flow-noise-modulated blend of uColorA
|
||||
// and uColorB. SDFs operate in aspect-corrected space (ac.x = uv.x *
|
||||
// uAspect) to preserve shape proportions on the wide banner.
|
||||
//
|
||||
// IMPORTANT: the uniform block below must list custom uniforms in EXACTLY
|
||||
// the order they're declared as properties on each ShaderEffect instance in
|
||||
// BrandBanner.qml (after the two Qt-supplied members, qt_Matrix/qt_Opacity).
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
|
||||
layout(location = 0) in vec2 qt_TexCoord0;
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
|
||||
layout(std140, binding = 0) uniform buf
|
||||
{
|
||||
mat4 qt_Matrix;
|
||||
float qt_Opacity;
|
||||
float iTime;
|
||||
float uAspect;
|
||||
float uMode;
|
||||
float uSpeed;
|
||||
float uSeed;
|
||||
vec4 uColorA;
|
||||
vec4 uColorB;
|
||||
vec4 uAccent;
|
||||
float uLogoGlow;
|
||||
};
|
||||
|
||||
// ── Primitives ──────────────────────────────────────────────────────────
|
||||
|
||||
float hash21(vec2 p)
|
||||
{
|
||||
p = fract(p * vec2(123.34, 456.21));
|
||||
p += dot(p, p + 45.32);
|
||||
return fract(p.x * p.y);
|
||||
}
|
||||
|
||||
float valueNoise(vec2 p)
|
||||
{
|
||||
vec2 i = floor(p);
|
||||
vec2 f = fract(p);
|
||||
float a = hash21(i);
|
||||
float b = hash21(i + vec2(1.0, 0.0));
|
||||
float c = hash21(i + vec2(0.0, 1.0));
|
||||
float d = hash21(i + vec2(1.0, 1.0));
|
||||
vec2 u = f * f * (3.0 - 2.0 * f);
|
||||
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
|
||||
}
|
||||
|
||||
float fbm(vec2 p)
|
||||
{
|
||||
float v = 0.0;
|
||||
float amp = 0.5;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
v += amp * valueNoise(p);
|
||||
p *= 2.03;
|
||||
amp *= 0.5;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
float flowNoise(vec2 p, float t)
|
||||
{
|
||||
vec2 warp1 = vec2(fbm(p + vec2(0.0, 0.0)), fbm(p + vec2(5.2, 1.3)));
|
||||
vec2 warp2 = vec2(fbm(p + 4.0 * warp1 + vec2(1.7, 9.2) + t * 0.6),
|
||||
fbm(p + 4.0 * warp1 + vec2(8.3, 2.8) - t * 0.5));
|
||||
return fbm(p + 4.0 * warp2 + t * 0.15);
|
||||
}
|
||||
|
||||
float bloom(float d, float coreRadius, float haloRadius)
|
||||
{
|
||||
float core = exp(-(d * d) / (coreRadius * coreRadius));
|
||||
float halo = exp(-d / haloRadius) * 0.35;
|
||||
return core + halo;
|
||||
}
|
||||
|
||||
float roundedBoxSDF(vec2 p, vec2 halfSize, float radius)
|
||||
{
|
||||
vec2 d = abs(p) - halfSize + radius;
|
||||
return length(max(d, 0.0)) - radius + min(max(d.x, d.y), 0.0);
|
||||
}
|
||||
|
||||
// Rotated box SDF -- applies 2D rotation to p before evaluating roundedBoxSDF.
|
||||
float rotatedBoxSDF(vec2 p, vec2 halfSize, float radius, float angle)
|
||||
{
|
||||
float c = cos(angle);
|
||||
float s = sin(angle);
|
||||
vec2 rp = vec2(p.x * c - p.y * s, p.x * s + p.y * c);
|
||||
return roundedBoxSDF(rp, halfSize, radius);
|
||||
}
|
||||
|
||||
float vignette(vec2 uv)
|
||||
{
|
||||
vec2 c = uv - 0.5;
|
||||
c.x *= max(uAspect, 0.0001);
|
||||
return smoothstep(1.0, 0.25, length(c));
|
||||
}
|
||||
|
||||
// ── Shared background ───────────────────────────────────────────────────
|
||||
|
||||
vec3 backgroundField(vec2 uv, float time)
|
||||
{
|
||||
// Diagonal luminance gradient from (0,0) to (1,1) used as blend factor
|
||||
// between uColorA and uColorB; modulated by flowNoise.
|
||||
float baseD = smoothstep(0.0, 1.0, uv.y * 0.5 + uv.x * 0.2);
|
||||
float painted = flowNoise(uv * 1.5, time * 0.04) - 0.5;
|
||||
baseD = clamp(baseD + painted * 0.12, 0.0, 1.0);
|
||||
|
||||
vec3 col = mix(uColorA.rgb, uColorB.rgb, baseD);
|
||||
|
||||
// Low-frequency fBM noise pushes local colour toward uColorB for depth
|
||||
float deep = fbm(uv * 1.0 + vec2(37.1, 12.4) + time * 0.015);
|
||||
col = mix(col, uColorB.rgb, (deep - 0.5) * 0.08);
|
||||
|
||||
// Accent-coloured fog layer: flowNoise peaks above 0.6 contribute accent
|
||||
float fog = flowNoise(uv * 0.8 + vec2(100.0, 50.0), time * 0.02);
|
||||
col += uAccent.rgb * max(fog - 0.6, 0.0) * 0.10;
|
||||
|
||||
return col;
|
||||
}
|
||||
|
||||
// ── Motifs ──────────────────────────────────────────────────────────────
|
||||
|
||||
// Centre bloom, flow-noise shimmer gated to centre, and 48 orbiting ember
|
||||
// particles that deflect into a tight ring near the centre.
|
||||
vec3 motifWelcome(vec2 uv, vec3 bg, float t)
|
||||
{
|
||||
vec3 col = bg;
|
||||
float asp = max(uAspect, 0.001);
|
||||
vec2 ac = vec2(uv.x * asp, uv.y);
|
||||
vec2 center = vec2(asp * 0.5, 0.5);
|
||||
float cDist = length(ac - center);
|
||||
|
||||
// Centre bloom at logo position; intensity scales with uLogoGlow
|
||||
float centreLight = bloom(cDist, 0.08 * asp, 0.40 * asp);
|
||||
col += centreLight * 0.20 * uLogoGlow;
|
||||
|
||||
// Flow-noise shimmer gated by Gaussian mask at centre
|
||||
float shimmer = flowNoise(ac * 0.8 + vec2(55.0, 33.0), t * 0.05) * 0.5 + 0.5;
|
||||
float shimmerMask = exp(-(cDist * cDist) / (0.18 * asp * 0.18 * asp));
|
||||
col += shimmer * shimmerMask * 0.04 * uLogoGlow;
|
||||
|
||||
// 48 ember particles: hash-seeded position, speed, size, brightness.
|
||||
// Embers within a distance threshold of centre are deflected into an
|
||||
// orbital ring via tangent displacement perpendicular to the centre vector.
|
||||
const int EMBERS = 48;
|
||||
for (int i = 0; i < EMBERS; i++) {
|
||||
float fi = float(i);
|
||||
|
||||
float baseX = hash21(vec2(fi * 7.31 + uSeed, fi * 3.17));
|
||||
float baseY = hash21(vec2(fi * 11.9 + uSeed * 1.4, fi * 5.53));
|
||||
|
||||
float riseSpeed = 0.025 + hash21(vec2(fi * 1.7, uSeed * 2.1)) * 0.035;
|
||||
float driftAmp = 0.04 + hash21(vec2(fi * 9.3, uSeed)) * 0.06;
|
||||
float driftFreq = 0.3 + hash21(vec2(fi * 4.1, uSeed * 3.3)) * 0.5;
|
||||
|
||||
float pX = baseX * asp + sin(t * driftFreq + fi * 1.7) * driftAmp * asp;
|
||||
float pY = fract(baseY + t * riseSpeed);
|
||||
|
||||
float size = 0.006 + hash21(vec2(fi * 2.9, uSeed * 4.7)) * 0.012;
|
||||
float bright = 0.15 + hash21(vec2(fi * 6.1, uSeed * 0.9)) * 0.30;
|
||||
|
||||
// Fade out near top/bottom edges
|
||||
float edgeFade = smoothstep(0.0, 0.12, pY) * smoothstep(1.0, 0.88, pY);
|
||||
float twinkle = 0.6 + 0.4 * sin(t * (1.2 + fi * 0.37) + fi * 2.9);
|
||||
|
||||
vec2 ePos = vec2(pX, pY);
|
||||
|
||||
// Embers near centre: deflect into orbital ring via tangent displacement
|
||||
vec2 toCenter = ePos - center;
|
||||
float distToCenter = length(toCenter);
|
||||
float ringWeight = smoothstep(0.38 * asp, 0.06 * asp, distToCenter);
|
||||
|
||||
float orbitPhase = t * (0.15 + fi * 0.020) + fi * 2.3;
|
||||
float orbitAmount = 0.020 + hash21(vec2(fi * 12.3, uSeed * 2.7)) * 0.020;
|
||||
vec2 tangent = vec2(-toCenter.y, toCenter.x);
|
||||
vec2 deflected = ePos + tangent * ringWeight * orbitAmount * asp * sin(orbitPhase);
|
||||
|
||||
float pushOut = ringWeight * (0.008 + hash21(vec2(fi * 6.7, uSeed * 1.1)) * 0.012) * asp;
|
||||
deflected += normalize(toCenter + 0.001) * pushOut;
|
||||
|
||||
float dist = length(ac - deflected);
|
||||
float intensity = bright * edgeFade * twinkle;
|
||||
col += uAccent.rgb * bloom(dist, size, size * 4.0) * intensity;
|
||||
}
|
||||
|
||||
return col;
|
||||
}
|
||||
|
||||
// 25 card-shaped box SDFs at parallax depths drifting horizontally across
|
||||
// the banner; each card has a semi-transparent fill, accent outline, and
|
||||
// card-back diamond pattern.
|
||||
vec3 motifCardDatabase(vec2 uv, vec3 bg, float t)
|
||||
{
|
||||
vec3 col = bg;
|
||||
float asp = max(uAspect, 0.001);
|
||||
vec2 ac = vec2(uv.x * asp, uv.y);
|
||||
|
||||
const int CARDS = 25;
|
||||
for (int i = 0; i < CARDS; i++) {
|
||||
float fi = float(i);
|
||||
|
||||
// Parallax depth via hash; used to scale size, speed, brightness
|
||||
float depth = hash21(vec2(fi * 1.37 + uSeed, fi * 0.91));
|
||||
|
||||
// Card dimensions in corrected space (portrait: height > width)
|
||||
float cardH = mix(0.055, 0.15, depth);
|
||||
cardH *= 0.85 + 0.30 * hash21(vec2(fi * 3.14, uSeed * 2.71));
|
||||
float cardW = cardH * 0.71; // 5:7 ratio
|
||||
|
||||
// Horizontal drift; nearer cards (higher depth) move faster
|
||||
float speed = mix(0.06, 0.18, depth);
|
||||
float xPhase = hash21(vec2(fi * 7.13, uSeed * 4.37));
|
||||
xPhase = fract(xPhase + t * speed);
|
||||
float x = mix(-1.5, asp + 1.5, xPhase);
|
||||
|
||||
// Vertical position: hash distribution with sinusoidal oscillation
|
||||
float yBase = hash21(vec2(fi * 2.91, uSeed * 1.63));
|
||||
float y = yBase + sin(t * 0.6 + fi * 1.9) * 0.035;
|
||||
y = clamp(y, cardH + 0.02, 1.0 - cardH - 0.02);
|
||||
|
||||
// Random rotation angle ±4 degrees
|
||||
float tilt = (hash21(vec2(fi * 5.71, uSeed * 8.29)) - 0.5) * 0.14;
|
||||
|
||||
vec2 p = ac - vec2(x, y);
|
||||
float d = rotatedBoxSDF(p, vec2(cardW, cardH), cardW * 0.14, tilt);
|
||||
|
||||
// Semi-transparent dark fill
|
||||
float fill = smoothstep(0.015, -0.005, d);
|
||||
col = mix(col, uColorB.rgb * 0.55, fill * 0.50);
|
||||
|
||||
// Accent outline
|
||||
float edge = smoothstep(0.035, 0.0, abs(d));
|
||||
col += uAccent.rgb * edge * mix(0.18, 0.50, 1.0 - depth);
|
||||
|
||||
// Card-back diamond: smaller rotated box inset from card edges
|
||||
float innerD = rotatedBoxSDF(p, vec2(cardW * 0.45, cardH * 0.55), cardW * 0.08, tilt);
|
||||
float innerEdge = smoothstep(0.012, 0.0, abs(innerD));
|
||||
col += uAccent.rgb * innerEdge * fill * 0.12 * (1.0 - depth);
|
||||
|
||||
// Centre dot
|
||||
float dotDist = length(p);
|
||||
col += uAccent.rgb * bloom(dotDist, 0.008, 0.02) * fill * 0.15 * (1.0 - depth);
|
||||
}
|
||||
return col;
|
||||
}
|
||||
|
||||
// 4 horizontal bands with multi-frequency sinusoidal warp and pulsing width.
|
||||
vec3 motifTheming(vec2 uv, vec3 bg, float t)
|
||||
{
|
||||
vec3 col = bg;
|
||||
|
||||
const int BANDS = 4;
|
||||
for (int i = 0; i < BANDS; i++) {
|
||||
float fi = float(i);
|
||||
float yCenter = 0.18 + fi * 0.22;
|
||||
|
||||
// Three summed sinusoids for horizontal undulation
|
||||
float wave = sin(uv.x * 3.2 + t * 0.5 + fi * 2.1) * 0.08;
|
||||
wave += sin(uv.x * 7.0 - t * 0.3 + fi * 1.3) * 0.035;
|
||||
wave += sin(uv.x * 1.6 + t * 0.18 + fi * 3.7) * 0.05;
|
||||
|
||||
float bandDist = abs(uv.y - yCenter - wave);
|
||||
float bandWidth = 0.04 + sin(t * 0.2 + fi * 0.8) * 0.012;
|
||||
float band = smoothstep(bandWidth, 0.0, bandDist);
|
||||
|
||||
// Upper bands have higher intensity
|
||||
float intensity = mix(0.15, 0.38, 1.0 - fi / float(BANDS));
|
||||
col += uAccent.rgb * band * intensity;
|
||||
}
|
||||
|
||||
return col;
|
||||
}
|
||||
|
||||
// 14 nodes at pseudo-random positions with sinusoidal pulse; edges drawn
|
||||
// between nodes within a threshold distance; central glow + periodic ring.
|
||||
vec3 motifAccount(vec2 uv, vec3 bg, float t)
|
||||
{
|
||||
vec3 col = bg;
|
||||
float asp = max(uAspect, 0.001);
|
||||
vec2 ac = vec2(uv.x * asp, uv.y);
|
||||
vec2 center = vec2(asp * 0.5, 0.5);
|
||||
|
||||
const int NODES = 14;
|
||||
vec2 nodePos[14];
|
||||
float nodePulse[14];
|
||||
|
||||
for (int i = 0; i < NODES; i++) {
|
||||
float fi = float(i);
|
||||
|
||||
// Hash-seeded position with gentle sinusoidal drift
|
||||
float nx = hash21(vec2(fi * 3.17 + uSeed, fi * 1.93)) * asp;
|
||||
float ny = hash21(vec2(fi * 5.41 + uSeed * 1.7, fi * 2.79));
|
||||
|
||||
float dx = sin(t * 0.12 + fi * 1.7) * 0.08;
|
||||
float dy = cos(t * 0.09 + fi * 2.3) * 0.04;
|
||||
vec2 pos = vec2(nx + dx, ny + dy);
|
||||
nodePos[i] = pos;
|
||||
|
||||
// Per-node pulse phase, normalised to [0, 1]
|
||||
float pulsePhase = hash21(vec2(fi * 4.31, uSeed * 6.17));
|
||||
float pulse = sin(t * 0.8 + pulsePhase * 6.283) * 0.5 + 0.5;
|
||||
nodePulse[i] = pulse;
|
||||
|
||||
// Node glow via bloom; intensity modulated by pulse
|
||||
float dist = length(ac - pos);
|
||||
col += uAccent.rgb * bloom(dist, 0.018, 0.08) * mix(0.20, 0.45, pulse);
|
||||
}
|
||||
|
||||
// Edges: connect nodes within a radius threshold
|
||||
float connectDist = asp * 0.22;
|
||||
for (int i = 0; i < NODES; i++) {
|
||||
for (int j = i + 1; j < NODES; j++) {
|
||||
float pairDist = length(nodePos[i] - nodePos[j]);
|
||||
if (pairDist < connectDist) {
|
||||
float strength = 1.0 - pairDist / connectDist;
|
||||
vec2 pa = ac - nodePos[i];
|
||||
vec2 ba = nodePos[j] - nodePos[i];
|
||||
float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
|
||||
float lineDist = length(pa - ba * h);
|
||||
col += uAccent.rgb * smoothstep(0.010, 0.0, lineDist) * strength * 0.10;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Central bloom at banner centre
|
||||
float cDist = length(ac - center);
|
||||
col += uAccent.rgb * bloom(cDist, 0.04, 0.25) * 0.12;
|
||||
|
||||
// Periodic expanding ring from centre
|
||||
float ripplePhase = t * 0.4;
|
||||
float rippleDist = abs(cDist - fract(ripplePhase) * asp * 0.7);
|
||||
col += uAccent.rgb * smoothstep(0.02, 0.0, rippleDist) * 0.10;
|
||||
|
||||
return col;
|
||||
}
|
||||
|
||||
// 18x5 toggle-grid of rounded boxes with hash-driven on/off per cell;
|
||||
// a scanning highlight sweeps L-to-R, brightening cells near the scan line.
|
||||
vec3 motifPreferences(vec2 uv, vec3 bg, float t)
|
||||
{
|
||||
vec3 col = bg;
|
||||
|
||||
float cols = 18.0;
|
||||
float rows = 5.0;
|
||||
vec2 gridUV = uv * vec2(cols, rows);
|
||||
vec2 cell = fract(gridUV) - 0.5;
|
||||
vec2 cellId = floor(gridUV);
|
||||
|
||||
// On/off state per cell, hash-seeded for pseudo-randomness
|
||||
float on = step(0.55, hash21(cellId + uSeed * 10.0));
|
||||
|
||||
float d = roundedBoxSDF(cell, vec2(0.28, 0.32), 0.06);
|
||||
|
||||
// Filled "on" cells
|
||||
float cellFill = smoothstep(0.04, -0.02, d);
|
||||
col += uAccent.rgb * cellFill * on * 0.18;
|
||||
|
||||
// Cell borders (drawn on all cells)
|
||||
float border = smoothstep(0.025, 0.0, abs(d));
|
||||
col += uAccent.rgb * border * 0.06;
|
||||
|
||||
// Scanning highlight: thin line + soft glow sweeping L-to-R
|
||||
float scanX = fract(t * 0.15);
|
||||
float scanDist = abs(uv.x - scanX);
|
||||
float scanLine = smoothstep(0.015, 0.0, scanDist);
|
||||
col += uAccent.rgb * scanLine * 0.40;
|
||||
|
||||
float scanGlow = smoothstep(0.08, 0.0, scanDist);
|
||||
col += uAccent.rgb * scanGlow * 0.08;
|
||||
|
||||
// "On" cells near the scan line get extra brightness
|
||||
float scanProximity = smoothstep(0.12, 0.0, scanDist);
|
||||
col += uAccent.rgb * cellFill * on * scanProximity * 0.15;
|
||||
|
||||
return col;
|
||||
}
|
||||
|
||||
// Centre radial bloom with sinusoidal pulse, 4 expanding ring halos with
|
||||
// outer glow falloff, and 35 rising particles.
|
||||
vec3 motifFinish(vec2 uv, vec3 bg, float t)
|
||||
{
|
||||
vec3 col = bg;
|
||||
float asp = max(uAspect, 0.001);
|
||||
vec2 ac = vec2(uv.x * asp, uv.y);
|
||||
vec2 center = vec2(asp * 0.5, 0.5);
|
||||
float cDist = length(ac - center);
|
||||
|
||||
// Centre bloom with sinusoidal pulse modulation
|
||||
float pulse = 0.65 + 0.35 * sin(t * 0.4);
|
||||
col += uAccent.rgb * bloom(cDist, 0.12, 0.55) * 0.10 * pulse;
|
||||
|
||||
// 4 expanding rings: radius increases via phase; ring width grows with
|
||||
// expansion; combined with exponential outer glow falloff
|
||||
for (int i = 0; i < 4; i++) {
|
||||
float fi = float(i);
|
||||
float phase = fract(t * 0.06 + fi * 0.25);
|
||||
float ringRadius = phase * asp * 0.7;
|
||||
float ringDist = abs(cDist - ringRadius);
|
||||
float ringWidth = 0.025 + phase * 0.025;
|
||||
float ring = smoothstep(ringWidth, 0.0, ringDist);
|
||||
float outerGlow = exp(-ringDist / (0.03 + phase * 0.02)) * 0.3;
|
||||
float combined = ring + outerGlow;
|
||||
float fade = 1.0 - phase * 0.5;
|
||||
col += uAccent.rgb * combined * fade * 0.15;
|
||||
}
|
||||
|
||||
// 35 particles rising vertically with sinusoidal horizontal drift;
|
||||
// each particle uses bloom with edge fade and twinkle animation
|
||||
const int PARTICLES = 35;
|
||||
for (int i = 0; i < PARTICLES; i++) {
|
||||
float fi = float(i);
|
||||
float baseX = hash21(vec2(fi * 13.7 + uSeed, fi * 7.31));
|
||||
float baseY = hash21(vec2(fi * 23.1 + uSeed * 1.9, fi * 11.3));
|
||||
|
||||
float riseSpeed = 0.04 + hash21(vec2(fi * 3.1, uSeed * 2.7)) * 0.06;
|
||||
float driftAmp = 0.03 + hash21(vec2(fi * 8.9, uSeed)) * 0.05;
|
||||
float driftFreq = 0.4 + hash21(vec2(fi * 5.3, uSeed * 4.1)) * 0.6;
|
||||
|
||||
float pX = baseX * asp + sin(t * driftFreq + fi * 2.3) * driftAmp * asp;
|
||||
float pY = fract(baseY + t * riseSpeed);
|
||||
|
||||
float size = 0.005 + hash21(vec2(fi * 4.7, uSeed * 3.9)) * 0.010;
|
||||
float bright = 0.12 + hash21(vec2(fi * 7.1, uSeed * 1.3)) * 0.25;
|
||||
|
||||
float edgeFade = smoothstep(0.0, 0.1, pY) * smoothstep(1.0, 0.9, pY);
|
||||
float twinkle = 0.5 + 0.5 * sin(t * (1.8 + fi * 0.43) + fi * 3.1);
|
||||
|
||||
vec2 pPos = vec2(pX, pY);
|
||||
float dist = length(ac - pPos);
|
||||
col += uAccent.rgb * bloom(dist, size, size * 3.5) * bright * edgeFade * twinkle;
|
||||
}
|
||||
|
||||
return col;
|
||||
}
|
||||
|
||||
// ── Main ────────────────────────────────────────────────────────────────
|
||||
|
||||
void main()
|
||||
{
|
||||
vec2 uv = qt_TexCoord0;
|
||||
float t = iTime * uSpeed;
|
||||
|
||||
vec3 bg = backgroundField(uv, iTime);
|
||||
|
||||
vec3 col;
|
||||
if (uMode < 0.5) col = motifWelcome(uv, bg, t);
|
||||
else if (uMode < 1.5) col = motifCardDatabase(uv, bg, t);
|
||||
else if (uMode < 2.5) col = motifTheming(uv, bg, t);
|
||||
else if (uMode < 3.5) col = motifAccount(uv, bg, t);
|
||||
else if (uMode < 4.5) col = motifPreferences(uv, bg, t);
|
||||
else col = motifFinish(uv, bg, t);
|
||||
|
||||
col *= mix(0.62, 1.0, vignette(uv));
|
||||
fragColor = vec4(col, 1.0) * qt_Opacity;
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
#include "step_indicator_widget.h"
|
||||
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
|
||||
StepIndicatorWidget::StepIndicatorWidget(QWidget *parent) : QWidget(parent)
|
||||
{
|
||||
setFixedHeight(kDotDiameter + 2 * kVerticalMargin);
|
||||
}
|
||||
|
||||
void StepIndicatorWidget::setStepCount(int count)
|
||||
{
|
||||
stepCount = qMax(0, count);
|
||||
currentStep = qBound(0, currentStep, qMax(0, stepCount - 1));
|
||||
updateGeometry();
|
||||
update();
|
||||
}
|
||||
|
||||
void StepIndicatorWidget::setCurrentStep(int index)
|
||||
{
|
||||
if (stepCount == 0) {
|
||||
return;
|
||||
}
|
||||
currentStep = qBound(0, index, stepCount - 1);
|
||||
update();
|
||||
}
|
||||
|
||||
QSize StepIndicatorWidget::sizeHint() const
|
||||
{
|
||||
return minimumSizeHint();
|
||||
}
|
||||
|
||||
QSize StepIndicatorWidget::minimumSizeHint() const
|
||||
{
|
||||
if (stepCount == 0) {
|
||||
return QSize(0, height());
|
||||
}
|
||||
int width = kActiveDotWidth + (stepCount - 1) * kDotDiameter + (stepCount - 1) * kDotSpacing;
|
||||
return QSize(width, height());
|
||||
}
|
||||
|
||||
void StepIndicatorWidget::paintEvent(QPaintEvent * /*event*/)
|
||||
{
|
||||
if (stepCount == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
const QColor activeColor = palette().color(QPalette::Highlight);
|
||||
|
||||
// QPalette::Mid alpha-blended against a dark Window background reads as
|
||||
// near-invisible (Mid is itself a dark grey in dark palettes -- see
|
||||
// PaletteGenerator's satShadeLo/Dark roles). WindowText is guaranteed to
|
||||
// contrast against Window in any theme by definition, so alpha-blending
|
||||
// *that* instead keeps the dots visibly dim-but-present in both light and
|
||||
// dark schemes. Same trick PaletteGenerator uses for placeholder text.
|
||||
QColor inactiveColor = palette().color(QPalette::WindowText);
|
||||
inactiveColor.setAlpha(100);
|
||||
|
||||
int totalWidth = 0;
|
||||
for (int i = 0; i < stepCount; ++i) {
|
||||
totalWidth += (i == currentStep) ? kActiveDotWidth : kDotDiameter;
|
||||
if (i > 0) {
|
||||
totalWidth += kDotSpacing;
|
||||
}
|
||||
}
|
||||
|
||||
int x = (width() - totalWidth) / 2;
|
||||
const int y = height() / 2;
|
||||
|
||||
for (int i = 0; i < stepCount; ++i) {
|
||||
const bool active = (i == currentStep);
|
||||
const int dotWidth = active ? kActiveDotWidth : kDotDiameter;
|
||||
|
||||
QPainterPath path;
|
||||
QRectF rect(x, y - kDotDiameter / 2.0, dotWidth, kDotDiameter);
|
||||
path.addRoundedRect(rect, kDotDiameter / 2.0, kDotDiameter / 2.0);
|
||||
painter.fillPath(path, active ? activeColor : inactiveColor);
|
||||
|
||||
x += dotWidth + kDotSpacing;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
#ifndef STEP_INDICATOR_WIDGET_H
|
||||
#define STEP_INDICATOR_WIDGET_H
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
/** @brief Row of dots showing progress through a fixed-length sequence of steps,
|
||||
* in the style of a mobile/OS setup flow. Purely presentational. */
|
||||
class StepIndicatorWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit StepIndicatorWidget(QWidget *parent = nullptr);
|
||||
|
||||
void setStepCount(int count);
|
||||
void setCurrentStep(int index);
|
||||
|
||||
QSize sizeHint() const override;
|
||||
QSize minimumSizeHint() const override;
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
|
||||
private:
|
||||
int stepCount = 0;
|
||||
int currentStep = 0;
|
||||
|
||||
static constexpr int kDotDiameter = 8;
|
||||
static constexpr int kActiveDotWidth = 22;
|
||||
static constexpr int kDotSpacing = 10;
|
||||
static constexpr int kVerticalMargin = 6;
|
||||
};
|
||||
|
||||
#endif // STEP_INDICATOR_WIDGET_H
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
#include "playmat_collection_dialog.h"
|
||||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "playmat_settings_dialog.h"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QListWidget>
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/settings/interface_settings.h>
|
||||
|
||||
PlaymatCollectionDialog::PlaymatCollectionDialog(QWidget *parent) : QDialog(parent)
|
||||
{
|
||||
setMinimumWidth(420);
|
||||
setupUi();
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
void PlaymatCollectionDialog::accept()
|
||||
{
|
||||
auto &interfaceSettings = SettingsCache::instance().userInterface();
|
||||
interfaceSettings.setPlaymatFallbackList(playmats);
|
||||
interfaceSettings.setPlaymatFallbackBehavior(modeCombo->currentData().toInt());
|
||||
QDialog::accept();
|
||||
}
|
||||
|
||||
int PlaymatCollectionDialog::currentRow() const
|
||||
{
|
||||
return playmatList->currentRow();
|
||||
}
|
||||
|
||||
void PlaymatCollectionDialog::setupUi()
|
||||
{
|
||||
auto &interfaceSettings = SettingsCache::instance().userInterface();
|
||||
playmats = interfaceSettings.getPlaymatFallbackList();
|
||||
|
||||
playmatList = new QListWidget;
|
||||
for (const PlaymatInfo &entry : playmats) {
|
||||
playmatList->addItem(entry.card.name);
|
||||
}
|
||||
connect(playmatList, &QListWidget::itemSelectionChanged, this, &PlaymatCollectionDialog::selectionChanged);
|
||||
connect(playmatList, &QListWidget::itemDoubleClicked, this, [this](QListWidgetItem *) { editPlaymat(); });
|
||||
|
||||
addButton = new QPushButton;
|
||||
editButton = new QPushButton;
|
||||
removeButton = new QPushButton;
|
||||
moveUpButton = new QPushButton;
|
||||
moveDownButton = new QPushButton;
|
||||
|
||||
connect(addButton, &QPushButton::clicked, this, &PlaymatCollectionDialog::addPlaymat);
|
||||
connect(editButton, &QPushButton::clicked, this, &PlaymatCollectionDialog::editPlaymat);
|
||||
connect(removeButton, &QPushButton::clicked, this, &PlaymatCollectionDialog::removePlaymat);
|
||||
connect(moveUpButton, &QPushButton::clicked, this, &PlaymatCollectionDialog::movePlaymatUp);
|
||||
connect(moveDownButton, &QPushButton::clicked, this, &PlaymatCollectionDialog::movePlaymatDown);
|
||||
|
||||
auto *listButtons = new QVBoxLayout;
|
||||
listButtons->addWidget(addButton);
|
||||
listButtons->addWidget(editButton);
|
||||
listButtons->addWidget(removeButton);
|
||||
listButtons->addWidget(moveUpButton);
|
||||
listButtons->addWidget(moveDownButton);
|
||||
listButtons->addStretch();
|
||||
|
||||
auto *listRow = new QHBoxLayout;
|
||||
listRow->addWidget(playmatList, 1);
|
||||
listRow->addLayout(listButtons);
|
||||
|
||||
modeCombo = new QComboBox;
|
||||
modeCombo->addItem(QString(), PlaymatFallbackModeFixed);
|
||||
modeCombo->addItem(QString(), PlaymatFallbackModeRoundRobin);
|
||||
modeCombo->addItem(QString(), PlaymatFallbackModeRandom);
|
||||
const int modeIndex = modeCombo->findData(interfaceSettings.getPlaymatFallbackBehavior());
|
||||
if (modeIndex >= 0) {
|
||||
modeCombo->setCurrentIndex(modeIndex);
|
||||
}
|
||||
|
||||
auto *modeRow = new QHBoxLayout;
|
||||
modeLabel = new QLabel;
|
||||
modeLabel->setBuddy(modeCombo);
|
||||
modeRow->addWidget(modeLabel);
|
||||
modeRow->addWidget(modeCombo, 1);
|
||||
|
||||
auto *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
connect(buttonBox, &QDialogButtonBox::accepted, this, &PlaymatCollectionDialog::accept);
|
||||
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
|
||||
auto *root = new QVBoxLayout;
|
||||
root->addLayout(listRow);
|
||||
root->addLayout(modeRow);
|
||||
root->addWidget(buttonBox);
|
||||
setLayout(root);
|
||||
|
||||
selectionChanged();
|
||||
}
|
||||
|
||||
void PlaymatCollectionDialog::selectionChanged()
|
||||
{
|
||||
const bool hasSelection = playmatList->currentRow() >= 0;
|
||||
editButton->setEnabled(hasSelection);
|
||||
removeButton->setEnabled(hasSelection);
|
||||
moveUpButton->setEnabled(hasSelection && playmatList->currentRow() > 0);
|
||||
moveDownButton->setEnabled(hasSelection && playmatList->currentRow() < playmatList->count() - 1);
|
||||
}
|
||||
|
||||
void PlaymatCollectionDialog::addPlaymat()
|
||||
{
|
||||
PlaymatSettingsDialog dialog(CardRef{}, PlaymatParams{}, this);
|
||||
if (dialog.exec() == QDialog::Accepted) {
|
||||
const CardRef card = dialog.card();
|
||||
if (!card.isEmpty()) {
|
||||
PlaymatInfo res = {card, dialog.params()};
|
||||
playmats.append(res);
|
||||
playmatList->addItem(res.card.name);
|
||||
playmatList->setCurrentRow(playmatList->count() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PlaymatCollectionDialog::editPlaymat()
|
||||
{
|
||||
const int row = currentRow();
|
||||
if (row < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const PlaymatInfo ¤t = playmats.at(row);
|
||||
PlaymatSettingsDialog dialog(current.card, current.params, this);
|
||||
if (dialog.exec() == QDialog::Accepted) {
|
||||
const CardRef card = dialog.card();
|
||||
if (card.isEmpty()) {
|
||||
return; // Removal is handled by the Remove button
|
||||
}
|
||||
playmats[row] = {card, dialog.params()};
|
||||
playmatList->item(row)->setText(card.name);
|
||||
}
|
||||
}
|
||||
|
||||
void PlaymatCollectionDialog::removePlaymat()
|
||||
{
|
||||
const int row = currentRow();
|
||||
if (row < 0) {
|
||||
return;
|
||||
}
|
||||
playmats.removeAt(row);
|
||||
delete playmatList->takeItem(row);
|
||||
selectionChanged();
|
||||
}
|
||||
|
||||
void PlaymatCollectionDialog::movePlaymatUp()
|
||||
{
|
||||
const int row = currentRow();
|
||||
if (row <= 0) {
|
||||
return;
|
||||
}
|
||||
playmats.swapItemsAt(row, row - 1);
|
||||
playmatList->insertItem(row - 1, playmatList->takeItem(row));
|
||||
playmatList->setCurrentRow(row - 1);
|
||||
selectionChanged();
|
||||
}
|
||||
|
||||
void PlaymatCollectionDialog::movePlaymatDown()
|
||||
{
|
||||
const int row = currentRow();
|
||||
if (row < 0 || row >= playmats.size() - 1) {
|
||||
return;
|
||||
}
|
||||
playmats.swapItemsAt(row, row + 1);
|
||||
playmatList->insertItem(row + 1, playmatList->takeItem(row));
|
||||
playmatList->setCurrentRow(row + 1);
|
||||
selectionChanged();
|
||||
}
|
||||
|
||||
void PlaymatCollectionDialog::retranslateUi()
|
||||
{
|
||||
setWindowTitle(tr("Default Playmats"));
|
||||
addButton->setText(tr("Add..."));
|
||||
editButton->setText(tr("Edit..."));
|
||||
removeButton->setText(tr("Remove"));
|
||||
moveUpButton->setText(tr("Move Up"));
|
||||
moveDownButton->setText(tr("Move Down"));
|
||||
modeLabel->setText(tr("List mode:"));
|
||||
modeCombo->setItemText(0, tr("Fixed (always the first entry)"));
|
||||
modeCombo->setItemText(1, tr("Round-robin (cycle through entries)"));
|
||||
modeCombo->setItemText(2, tr("Random (pick one per game)"));
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
#ifndef COCKATRICE_PLAYMAT_COLLECTION_DIALOG_H
|
||||
#define COCKATRICE_PLAYMAT_COLLECTION_DIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <libcockatrice/utility/playmat_params.h>
|
||||
|
||||
class QComboBox;
|
||||
class QLabel;
|
||||
class QListWidget;
|
||||
class QListWidgetItem;
|
||||
class QPushButton;
|
||||
|
||||
/**
|
||||
* @brief Dialog for editing the user-level playmat collection.
|
||||
*
|
||||
* The collection is the fallback used when a deck has no playmat of its own.
|
||||
* It supports multiple entries and a pick mode (always first / round-robin /
|
||||
* random). The dialog edits a working copy and writes it to the settings only
|
||||
* when accepted.
|
||||
*/
|
||||
class PlaymatCollectionDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit PlaymatCollectionDialog(QWidget *parent = nullptr);
|
||||
|
||||
void accept() override;
|
||||
|
||||
private slots:
|
||||
void addPlaymat();
|
||||
void editPlaymat();
|
||||
void removePlaymat();
|
||||
void movePlaymatUp();
|
||||
void movePlaymatDown();
|
||||
void selectionChanged();
|
||||
|
||||
private:
|
||||
void setupUi();
|
||||
void retranslateUi();
|
||||
int currentRow() const;
|
||||
|
||||
QList<PlaymatInfo> playmats; ///< Working copy edited by the dialog.
|
||||
QListWidget *playmatList;
|
||||
QComboBox *modeCombo;
|
||||
QLabel *modeLabel;
|
||||
QPushButton *addButton;
|
||||
QPushButton *editButton;
|
||||
QPushButton *removeButton;
|
||||
QPushButton *moveUpButton;
|
||||
QPushButton *moveDownButton;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_PLAYMAT_COLLECTION_DIALOG_H
|
||||
|
|
@ -0,0 +1,381 @@
|
|||
#include "playmat_preview_widget.h"
|
||||
|
||||
#include "../cards/art_crop_attribution.h"
|
||||
#include "playmat_utils.h"
|
||||
|
||||
#include <QKeyEvent>
|
||||
#include <QLinearGradient>
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QWheelEvent>
|
||||
#include <cmath>
|
||||
|
||||
namespace
|
||||
{
|
||||
// Mirrors the dialog/proto clamps so gestures can never produce an
|
||||
// out of range parameter. The zoom FLOOR is dynamic: see
|
||||
// playmatClampedZoom(), zooming out stops where the sampling window would
|
||||
// exceed the card itself, so there is no dead range at the bottom end.
|
||||
constexpr qreal MAX_MARGIN = 0.95;
|
||||
// Range of in game stack+table aspect ratios worth designing for, derived
|
||||
// from PlayerGraphicsItem::paint()'s combinedArea = stack ∪ table:
|
||||
// height = 10 + 30 + 3*102 + 2*30 = 406 (TableZone rows)
|
||||
// width = 1.5*72 + (20 + 5*72 + 15) = 503 (StackZone + TableZone
|
||||
// at MIN_WIDTH)
|
||||
// The area's shape depends on GAME CONTENT (played card columns widen the
|
||||
// table by ~107 px each), not on the window size. Fresh board ≈ 503/406,
|
||||
// a table grown to roughly double its minimum width ≈ 2.2.
|
||||
constexpr qreal MIN_TABLE_ASPECT = 503.0 / 406.0; // fresh board: most generous framing
|
||||
constexpr qreal MAX_TABLE_ASPECT = 2.2; // well developed, wide table
|
||||
// Keyboard nudge steps (viewport convention: Down looks further down).
|
||||
constexpr qreal KEY_PAN_MARGIN_STEP = 0.005;
|
||||
constexpr qreal KEY_PAN_OFFSET_STEP = 0.01;
|
||||
constexpr qreal KEY_ZOOM_STEP = 1.05;
|
||||
constexpr qreal WHEEL_ZOOM_BASE = 1.15; // zoom factor per wheel notch
|
||||
} // namespace
|
||||
|
||||
PlaymatPreviewWidget::PlaymatPreviewWidget(QWidget *parent) : QWidget(parent)
|
||||
{
|
||||
// The crop is square and drawn contain fit, so the height decides its
|
||||
// on screen size, keep it generous but let the dialog compress on small
|
||||
// or high DPI screens
|
||||
setMinimumSize(400, 180);
|
||||
QSizePolicy sp(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||||
setSizePolicy(sp);
|
||||
setFocusPolicy(Qt::StrongFocus);
|
||||
setCursor(Qt::OpenHandCursor);
|
||||
setAccessibleName(tr("Playmat crop"));
|
||||
setAccessibleDescription(tr("Zoom %1×").arg(QString::number(params.zoom, 'f', 2)));
|
||||
}
|
||||
|
||||
void PlaymatPreviewWidget::setPixmap(const QPixmap &pixmap)
|
||||
{
|
||||
sourcePixmap = pixmap;
|
||||
setCursor(sourcePixmap.isNull() ? Qt::ArrowCursor : Qt::OpenHandCursor);
|
||||
update();
|
||||
}
|
||||
|
||||
void PlaymatPreviewWidget::setParams(const PlaymatParams &p)
|
||||
{
|
||||
params = p;
|
||||
setAccessibleDescription(tr("Zoom %1×").arg(QString::number(params.zoom, 'f', 2)));
|
||||
update();
|
||||
}
|
||||
|
||||
void PlaymatPreviewWidget::setAttribution(const QString &attribution)
|
||||
{
|
||||
attributionText = attribution;
|
||||
update();
|
||||
}
|
||||
|
||||
QRectF PlaymatPreviewWidget::activePlayArea() const
|
||||
{
|
||||
// The viewport is a frame shaped like a fresh board's stack+table area
|
||||
// (kMinTableAspect): the most generous framing the game will produce.
|
||||
// Dimmed strips mark where a wider, developed table crops further.
|
||||
const QRectF cardRect = QRectF(rect()).adjusted(3, 2, -3, -2);
|
||||
return PlaymatUtils::aspectFitRect(cardRect.adjusted(6, 4, -4, -4), MIN_TABLE_ASPECT);
|
||||
}
|
||||
|
||||
qreal PlaymatPreviewWidget::samplingWindowSide() const
|
||||
{
|
||||
if (sourcePixmap.isNull()) {
|
||||
return 0.0;
|
||||
}
|
||||
// Same clamped window the render path uses, gestures and painting must
|
||||
// never disagree about geometry.
|
||||
return PlaymatUtils::playmatWindowSide(sourcePixmap.size(), params);
|
||||
}
|
||||
|
||||
qreal PlaymatPreviewWidget::widgetToSourceScale() const
|
||||
{
|
||||
const qreal cropSide = samplingWindowSide();
|
||||
const QRectF area = activePlayArea();
|
||||
if (cropSide <= 0.0 || area.isEmpty()) {
|
||||
return 0.0;
|
||||
}
|
||||
// Mirror coverFitRect(): the square crop into the (wider) viewport fills
|
||||
// its width.
|
||||
return area.width() / cropSide;
|
||||
}
|
||||
|
||||
void PlaymatPreviewWidget::applyCropDelta(qreal dMarginL, qreal dMarginR, qreal dOffset, qreal zoomFactor)
|
||||
{
|
||||
PlaymatParams next = params;
|
||||
if (dMarginL + dMarginR == 0.0) {
|
||||
// Pure horizontal pan rebalances the margins along their
|
||||
// sum constant segment. Individual bounds must not break that
|
||||
// invariant, otherwise repeated corner drags let one margin grow
|
||||
// without end, collapsing the viewing window and desynchronizing the
|
||||
// visual zoom from the readout.
|
||||
const qreal sum = params.marginPctL + params.marginPctR;
|
||||
const qreal lo = qMax(0.0, sum - MAX_MARGIN);
|
||||
const qreal hi = qMin(sum, MAX_MARGIN);
|
||||
next.marginPctL = qBound(lo, params.marginPctL + dMarginL, hi);
|
||||
next.marginPctR = sum - next.marginPctL;
|
||||
} else {
|
||||
next.marginPctL = qBound(0.0, params.marginPctL + dMarginL, MAX_MARGIN);
|
||||
next.marginPctR = qBound(0.0, params.marginPctR + dMarginR, MAX_MARGIN);
|
||||
}
|
||||
next.verticalOffset = qBound(0.0, params.verticalOffset + dOffset, 1.0);
|
||||
// Clamp through the shared helper so the floor tracks the new margins:
|
||||
// zooming out stops exactly where the window reaches the card bounds.
|
||||
next.zoom = params.zoom * zoomFactor;
|
||||
if (!sourcePixmap.isNull()) {
|
||||
next.zoom = PlaymatUtils::playmatClampedZoom(sourcePixmap.size(), next);
|
||||
}
|
||||
|
||||
if (sameCrop(next, params)) {
|
||||
return;
|
||||
}
|
||||
|
||||
params = next;
|
||||
setAccessibleDescription(tr("Zoom %1×").arg(QString::number(params.zoom, 'f', 2)));
|
||||
update();
|
||||
emit paramsEdited(params);
|
||||
}
|
||||
|
||||
bool PlaymatPreviewWidget::sameCrop(const PlaymatParams &a, const PlaymatParams &b) const
|
||||
{
|
||||
// Exact comparison on purpose: clamped assignments yield identical bits,
|
||||
// while qFuzzyCompare based equality misbehaves around zero
|
||||
return a.marginPctL == b.marginPctL && a.marginPctR == b.marginPctR && a.verticalOffset == b.verticalOffset &&
|
||||
a.zoom == b.zoom;
|
||||
}
|
||||
|
||||
void PlaymatPreviewWidget::restoreSnapshot()
|
||||
{
|
||||
// The snapshot only ever holds values that passed the gesture clamps,
|
||||
// so it is safe to restore verbatim
|
||||
if (sameCrop(paramsAtFocusIn, params)) {
|
||||
return;
|
||||
}
|
||||
params = paramsAtFocusIn;
|
||||
setAccessibleDescription(tr("Zoom %1×").arg(QString::number(params.zoom, 'f', 2)));
|
||||
update();
|
||||
emit paramsEdited(params);
|
||||
}
|
||||
|
||||
void PlaymatPreviewWidget::paintEvent(QPaintEvent *)
|
||||
{
|
||||
QPainter painter(this);
|
||||
painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::TextAntialiasing);
|
||||
|
||||
const QRect rect = this->rect();
|
||||
const QColor accentColor(100, 116, 139);
|
||||
|
||||
// Background
|
||||
const QRectF cardRect = QRectF(rect).adjusted(3, 2, -3, -2);
|
||||
QLinearGradient bg(cardRect.topLeft(), cardRect.topRight());
|
||||
bg.setColorAt(0, accentColor.darker(320));
|
||||
bg.setColorAt(1, QColor(18, 22, 30));
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setBrush(bg);
|
||||
painter.drawRoundedRect(cardRect, 6, 6);
|
||||
painter.setBrush(accentColor);
|
||||
painter.drawRoundedRect(QRectF(cardRect.left(), cardRect.top(), 3, cardRect.height()), 2, 2);
|
||||
|
||||
if (sourcePixmap.isNull()) {
|
||||
painter.setPen(QColor(150, 150, 150));
|
||||
painter.drawText(rect, Qt::AlignCenter, tr("No card selected"));
|
||||
return;
|
||||
}
|
||||
|
||||
const QRectF playArea = activePlayArea();
|
||||
|
||||
// Exactly the game's pipeline (player_graphics_item): cover fit the crop
|
||||
// into the table shaped viewport, centered, so the frame shows precisely
|
||||
// what a minimum aspect window shows, and dragging moves the art behind
|
||||
// the fixed frame.
|
||||
const QRectF srcRect = PlaymatUtils::computeArtSourceRect(sourcePixmap.size(), params);
|
||||
const QRectF dstRect = PlaymatUtils::coverFitRect(playArea, srcRect.size());
|
||||
|
||||
painter.setClipRect(playArea.toRect());
|
||||
painter.drawPixmap(dstRect, sourcePixmap, srcRect);
|
||||
|
||||
// Wider (developed) tables crop further: mark where a kMaxTableAspect
|
||||
// board stops. Palette driven so theme authors can recolor the markers.
|
||||
const qreal wideBandHeight = playArea.height() * (MIN_TABLE_ASPECT / MAX_TABLE_ASPECT);
|
||||
const qreal stripHeight = (playArea.height() - wideBandHeight) / 2.0;
|
||||
QColor stripColor = palette().color(QPalette::Window);
|
||||
stripColor.setAlpha(150);
|
||||
painter.fillRect(QRectF(playArea.left(), playArea.top(), playArea.width(), stripHeight), stripColor);
|
||||
painter.fillRect(QRectF(playArea.left(), playArea.bottom() - stripHeight, playArea.width(), stripHeight),
|
||||
stripColor);
|
||||
QColor hairlineColor = palette().color(QPalette::Highlight);
|
||||
hairlineColor.setAlpha(110);
|
||||
painter.setPen(QPen(hairlineColor, 1));
|
||||
painter.drawLine(QPointF(playArea.left(), playArea.top() + stripHeight),
|
||||
QPointF(playArea.right(), playArea.top() + stripHeight));
|
||||
painter.drawLine(QPointF(playArea.left(), playArea.bottom() - stripHeight),
|
||||
QPointF(playArea.right(), playArea.bottom() - stripHeight));
|
||||
|
||||
// Draw zone divider: stack is roughly the left portion
|
||||
const double stackWidthRatio = 0.18; // Stack is about 18% of total play area
|
||||
const double stackDividerX = playArea.left() + playArea.width() * stackWidthRatio;
|
||||
|
||||
// Subtle semi transparent overlays to distinguish zones
|
||||
// Stack zone overlay (slightly darker)
|
||||
QRectF stackOverlay(playArea.left(), playArea.top(), playArea.width() * stackWidthRatio, playArea.height());
|
||||
painter.fillRect(stackOverlay, QColor(0, 0, 0, 40));
|
||||
|
||||
// Table zone overlay (very subtle)
|
||||
QRectF tableOverlay(stackDividerX, playArea.top(), playArea.width() * (1.0 - stackWidthRatio), playArea.height());
|
||||
painter.fillRect(tableOverlay, QColor(0, 0, 0, 20));
|
||||
|
||||
// Zone divider line
|
||||
painter.setPen(QPen(QColor(255, 255, 255, 50), 1));
|
||||
painter.drawLine(QPointF(stackDividerX, playArea.top()), QPointF(stackDividerX, playArea.bottom()));
|
||||
|
||||
// Land divider line (about 60% down the table area)
|
||||
const double landDividerY = playArea.top() + playArea.height() * 0.65;
|
||||
painter.setPen(QPen(QColor(255, 255, 255, 30), 1));
|
||||
painter.drawLine(QPointF(stackDividerX, landDividerY), QPointF(playArea.right(), landDividerY));
|
||||
painter.setClipping(false);
|
||||
|
||||
// Border around the viewport = boundary of every plausible framing.
|
||||
painter.setPen(QPen(QColor(70, 80, 95, 120), 1));
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.drawRoundedRect(playArea.adjusted(0, 0, -1, -1), 3, 3);
|
||||
|
||||
// Visible keyboard focus per the focus cursor contract, Tab must show
|
||||
// where the keys land
|
||||
if (hasFocus()) {
|
||||
QPen focusPen(palette().color(QPalette::Highlight), 2);
|
||||
painter.setPen(focusPen);
|
||||
painter.drawRoundedRect(playArea.adjusted(-1, -1, 1, 1), 3, 3);
|
||||
}
|
||||
|
||||
paintArtAttribution(painter, playArea, attributionText, Qt::AlignRight | Qt::AlignBottom, 0.8);
|
||||
|
||||
// Zoom readout so the gesture has a visible, stable counterpart.
|
||||
QColor ink = palette().color(QPalette::WindowText);
|
||||
ink.setAlpha(160);
|
||||
painter.setPen(ink);
|
||||
painter.drawText(QPointF(playArea.left() + 8, playArea.bottom() - 8),
|
||||
tr("Zoom %1×").arg(QString::number(params.zoom, 'f', 2)));
|
||||
}
|
||||
|
||||
void PlaymatPreviewWidget::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
if (event->button() != Qt::LeftButton || sourcePixmap.isNull() || samplingWindowSide() <= 0.0) {
|
||||
QWidget::mousePressEvent(event);
|
||||
return;
|
||||
}
|
||||
lastDragPos = event->pos();
|
||||
setCursor(Qt::ClosedHandCursor);
|
||||
event->accept();
|
||||
}
|
||||
|
||||
void PlaymatPreviewWidget::mouseMoveEvent(QMouseEvent *event)
|
||||
{
|
||||
if (!(event->buttons() & Qt::LeftButton) || sourcePixmap.isNull()) {
|
||||
QWidget::mouseMoveEvent(event);
|
||||
return;
|
||||
}
|
||||
|
||||
const QPointF delta = QPointF(event->pos() - lastDragPos);
|
||||
lastDragPos = event->pos();
|
||||
|
||||
const qreal scale = widgetToSourceScale();
|
||||
// Vertical travel of the SAMPLING window: verticalOffset moves its top
|
||||
// edge by exactly this much per unit, identical to the render path.
|
||||
// Windows taller than the art (square/landscape sources zoomed out)
|
||||
// leave no travel, vertical drags are then boundary no ops.
|
||||
const qreal travel = static_cast<qreal>(sourcePixmap.height()) - samplingWindowSide();
|
||||
if (scale <= 0.0) {
|
||||
event->accept();
|
||||
return;
|
||||
}
|
||||
|
||||
// Dragging moves the ART with the cursor, so the viewing window slides the
|
||||
// other way. Horizontal panning rebalances the margins (their sum, hence
|
||||
// the window width, stays constant), vertical panning moves the window's
|
||||
// top edge within its available travel.
|
||||
const qreal sourceW = sourcePixmap.width();
|
||||
const qreal dMargin = -(delta.x() / scale) / sourceW;
|
||||
const qreal dOffset = travel > 0.5 ? -(delta.y() / scale) / travel : 0.0;
|
||||
|
||||
applyCropDelta(dMargin, -dMargin, dOffset, 1.0);
|
||||
event->accept();
|
||||
}
|
||||
|
||||
void PlaymatPreviewWidget::mouseReleaseEvent(QMouseEvent *event)
|
||||
{
|
||||
if (event->button() == Qt::LeftButton) {
|
||||
setCursor(sourcePixmap.isNull() ? Qt::ArrowCursor : Qt::OpenHandCursor);
|
||||
event->accept();
|
||||
return;
|
||||
}
|
||||
QWidget::mouseReleaseEvent(event);
|
||||
}
|
||||
|
||||
void PlaymatPreviewWidget::wheelEvent(QWheelEvent *event)
|
||||
{
|
||||
if (sourcePixmap.isNull() || samplingWindowSide() <= 0.0) {
|
||||
QWidget::wheelEvent(event);
|
||||
return;
|
||||
}
|
||||
const qreal notches = static_cast<qreal>(event->angleDelta().y()) / 120.0;
|
||||
if (notches == 0.0) {
|
||||
event->accept();
|
||||
return;
|
||||
}
|
||||
applyCropDelta(0.0, 0.0, 0.0, std::pow(WHEEL_ZOOM_BASE, notches));
|
||||
event->accept();
|
||||
}
|
||||
|
||||
void PlaymatPreviewWidget::keyPressEvent(QKeyEvent *event)
|
||||
{
|
||||
if (sourcePixmap.isNull()) {
|
||||
QWidget::keyPressEvent(event);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event->key()) {
|
||||
case Qt::Key_Escape:
|
||||
if (sameCrop(params, paramsAtFocusIn)) {
|
||||
// Nothing to undo on this surface, let the event reach the
|
||||
// dialog so Esc keeps its close meaning there
|
||||
QWidget::keyPressEvent(event);
|
||||
return;
|
||||
}
|
||||
restoreSnapshot();
|
||||
break;
|
||||
case Qt::Key_Backspace:
|
||||
restoreSnapshot();
|
||||
break;
|
||||
case Qt::Key_Left:
|
||||
applyCropDelta(-KEY_PAN_MARGIN_STEP, KEY_PAN_MARGIN_STEP, 0.0, 1.0);
|
||||
break;
|
||||
case Qt::Key_Right:
|
||||
applyCropDelta(KEY_PAN_MARGIN_STEP, -KEY_PAN_MARGIN_STEP, 0.0, 1.0);
|
||||
break;
|
||||
case Qt::Key_Up:
|
||||
applyCropDelta(0.0, 0.0, -KEY_PAN_OFFSET_STEP, 1.0);
|
||||
break;
|
||||
case Qt::Key_Down:
|
||||
applyCropDelta(0.0, 0.0, KEY_PAN_OFFSET_STEP, 1.0);
|
||||
break;
|
||||
case Qt::Key_Plus:
|
||||
case Qt::Key_Equal:
|
||||
applyCropDelta(0.0, 0.0, 0.0, KEY_ZOOM_STEP);
|
||||
break;
|
||||
case Qt::Key_Minus:
|
||||
applyCropDelta(0.0, 0.0, 0.0, 1.0 / KEY_ZOOM_STEP);
|
||||
break;
|
||||
default:
|
||||
QWidget::keyPressEvent(event);
|
||||
return;
|
||||
}
|
||||
event->accept();
|
||||
}
|
||||
|
||||
void PlaymatPreviewWidget::focusInEvent(QFocusEvent *event)
|
||||
{
|
||||
// Snapshot for the Esc or Backspace reset, restoring whatever the user
|
||||
// had when the surface took focus
|
||||
paramsAtFocusIn = params;
|
||||
QWidget::focusInEvent(event);
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
#ifndef COCKATRICE_PLAYMAT_PREVIEW_WIDGET_H
|
||||
#define COCKATRICE_PLAYMAT_PREVIEW_WIDGET_H
|
||||
|
||||
#include <QFocusEvent>
|
||||
#include <QPixmap>
|
||||
#include <QWidget>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
|
||||
/**
|
||||
* @brief Interactive crop surface showing how a playmat card art will appear
|
||||
* across the combined table + stack play area.
|
||||
*
|
||||
* Renders a fixed frame shaped like a fresh board's stack+table area (the
|
||||
* most generous framing the game produces): it shows the tallest slice of
|
||||
* the square crop in normal play, with dimmed strips marking where a wider,
|
||||
* developed table crops further, exactly the game's own render pipeline.
|
||||
* The widget doubles as the editor's primary crop control: dragging pans the
|
||||
* art behind the frame, the wheel zooms, and arrow keys nudge, mirroring
|
||||
* the stored parameters (margins pan horizontally, verticalOffset
|
||||
* vertically, zoom scales) so no separate numeric controls are needed.
|
||||
*/
|
||||
class PlaymatPreviewWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit PlaymatPreviewWidget(QWidget *parent = nullptr);
|
||||
|
||||
void setPixmap(const QPixmap &pixmap);
|
||||
void setParams(const PlaymatParams ¶ms);
|
||||
void setAttribution(const QString &attribution);
|
||||
|
||||
signals:
|
||||
/** @brief Emitted whenever direct manipulation (drag, wheel, keys) changes the crop parameters. */
|
||||
void paramsEdited(const PlaymatParams ¶ms);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
void mouseMoveEvent(QMouseEvent *event) override;
|
||||
void mouseReleaseEvent(QMouseEvent *event) override;
|
||||
void wheelEvent(QWheelEvent *event) override;
|
||||
void keyPressEvent(QKeyEvent *event) override;
|
||||
void focusInEvent(QFocusEvent *event) override;
|
||||
|
||||
private:
|
||||
QRectF activePlayArea() const; ///< destination rect used for rendering AND gesture math
|
||||
qreal samplingWindowSide() const; ///< clamped square window side, shared with the render path
|
||||
qreal widgetToSourceScale() const;
|
||||
void applyCropDelta(qreal dMarginL, qreal dMarginR, qreal dOffset, qreal zoomFactor);
|
||||
bool sameCrop(const PlaymatParams &a, const PlaymatParams &b) const;
|
||||
void restoreSnapshot();
|
||||
|
||||
QPixmap sourcePixmap;
|
||||
PlaymatParams params;
|
||||
PlaymatParams paramsAtFocusIn; ///< crop as of the latest focus gain, restored by Esc or Backspace
|
||||
QString attributionText;
|
||||
QPoint lastDragPos; ///< widget space position of the previous mouse move while panning
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_PLAYMAT_PREVIEW_WIDGET_H
|
||||
|
|
@ -0,0 +1,324 @@
|
|||
#include "playmat_settings_dialog.h"
|
||||
|
||||
#include "../../card_picture_loader/card_picture_loader.h"
|
||||
#include "../cards/art_crop_attribution.h"
|
||||
#include "../utility/completer_utils.h"
|
||||
#include "card_database_display_model.h"
|
||||
#include "card_database_model.h"
|
||||
#include "playmat_preview_widget.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QCompleter>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QDoubleSpinBox>
|
||||
#include <QFormLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
|
||||
PlaymatSettingsDialog::PlaymatSettingsDialog(const CardRef &initialCard,
|
||||
const PlaymatParams &initialParams,
|
||||
QWidget *parent)
|
||||
: QDialog(parent), currentCard(initialCard), currentParams(initialParams)
|
||||
{
|
||||
setMinimumWidth(500);
|
||||
setupUi();
|
||||
|
||||
// Seed UI from initial values
|
||||
if (!initialCard.name.isEmpty()) {
|
||||
searchBar->setText(initialCard.name);
|
||||
onCardNameChanged(initialCard.name);
|
||||
|
||||
// onCardNameChanged leaves the printing combo on the first printing in
|
||||
// the database, which would silently change the deck's stored playmat
|
||||
// card on accept. Restore the stored printing when it resolves locally.
|
||||
const int storedPrintingIndex = providerComboBox->findData(initialCard.providerId);
|
||||
if (storedPrintingIndex != -1) {
|
||||
providerComboBox->setCurrentIndex(storedPrintingIndex);
|
||||
} else {
|
||||
// Stored printing not in the local database: keep it rather than
|
||||
// silently substituting the first printing.
|
||||
currentCard.providerId = initialCard.providerId;
|
||||
reloadPreview();
|
||||
}
|
||||
}
|
||||
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
CardRef PlaymatSettingsDialog::card() const
|
||||
{
|
||||
return currentCard;
|
||||
}
|
||||
|
||||
PlaymatParams PlaymatSettingsDialog::params() const
|
||||
{
|
||||
return currentParams;
|
||||
}
|
||||
|
||||
QDoubleSpinBox *PlaymatSettingsDialog::makeSpinBox(double min, double max, double value, double step)
|
||||
{
|
||||
auto *spin = new QDoubleSpinBox;
|
||||
spin->setRange(min, max);
|
||||
spin->setSingleStep(step);
|
||||
spin->setDecimals(3);
|
||||
spin->setValue(value);
|
||||
return spin;
|
||||
}
|
||||
|
||||
void PlaymatSettingsDialog::initializeSearchBar()
|
||||
{
|
||||
searchBar = new QLineEdit;
|
||||
|
||||
cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this);
|
||||
cardDatabaseDisplayModel = new CardDatabaseDisplayModel(this);
|
||||
cardDatabaseDisplayModel->setSourceModel(cardDatabaseModel);
|
||||
|
||||
const CardCompleterSetup cardSetup = createCardCompleter(cardDatabaseDisplayModel, this, 15);
|
||||
searchModel = cardSetup.searchModel;
|
||||
proxyModel = cardSetup.proxyModel;
|
||||
completer = cardSetup.completer;
|
||||
searchBar->setCompleter(completer);
|
||||
|
||||
connectCardCompleterSearch(searchBar, cardSetup);
|
||||
|
||||
connect(completer, static_cast<void (QCompleter::*)(const QString &)>(&QCompleter::activated), this,
|
||||
[this](const QString &completion) {
|
||||
if (searchBar->text() != completion) {
|
||||
searchBar->setText(completion);
|
||||
searchBar->setCursorPosition(searchBar->text().length());
|
||||
}
|
||||
onCardNameChanged(completion);
|
||||
});
|
||||
|
||||
connect(searchBar, &QLineEdit::returnPressed, this, [this]() { onCardNameChanged(searchBar->text()); });
|
||||
}
|
||||
|
||||
void PlaymatSettingsDialog::setupUi()
|
||||
{
|
||||
initializeSearchBar();
|
||||
|
||||
providerComboBox = new QComboBox;
|
||||
connect(providerComboBox, &QComboBox::currentIndexChanged, this, [this]() {
|
||||
currentCard.providerId = providerComboBox->currentData().toString();
|
||||
reloadPreview();
|
||||
});
|
||||
|
||||
auto *form = new QFormLayout;
|
||||
controlsForm = form;
|
||||
cardNameLabel = new QLabel;
|
||||
printingLabel = new QLabel;
|
||||
form->addRow(cardNameLabel, searchBar);
|
||||
form->addRow(printingLabel, providerComboBox);
|
||||
|
||||
// Numerical editors expose the raw PlaymatParams for precise input. They
|
||||
// share the same form as the rows above so every field lines up on one
|
||||
// label column. They stay hidden until requested since the crop surface
|
||||
// is the primary control.
|
||||
marginLSpin = makeSpinBox(0.0, 0.95, currentParams.marginPctL, 0.01);
|
||||
marginRSpin = makeSpinBox(0.0, 0.95, currentParams.marginPctR, 0.01);
|
||||
verticalOffsetSpin = makeSpinBox(0.0, 1.0, currentParams.verticalOffset, 0.01);
|
||||
zoomSpin = makeSpinBox(0.1, 4.0, currentParams.zoom, 0.05);
|
||||
|
||||
leftMarginLabel = new QLabel;
|
||||
rightMarginLabel = new QLabel;
|
||||
verticalOffsetLabel = new QLabel;
|
||||
zoomLabel = new QLabel;
|
||||
|
||||
showNumericEditorsCheck = new QCheckBox;
|
||||
|
||||
form->addRow(showNumericEditorsCheck);
|
||||
form->addRow(leftMarginLabel, marginLSpin);
|
||||
form->addRow(rightMarginLabel, marginRSpin);
|
||||
form->addRow(verticalOffsetLabel, verticalOffsetSpin);
|
||||
form->addRow(zoomLabel, zoomSpin);
|
||||
|
||||
controlsGroup = new QGroupBox;
|
||||
controlsGroup->setLayout(form);
|
||||
|
||||
preview = new PlaymatPreviewWidget;
|
||||
preview->setParams(currentParams);
|
||||
|
||||
auto *previewLayout = new QVBoxLayout;
|
||||
previewLayout->addWidget(preview);
|
||||
previewCaptionLabel = new QLabel;
|
||||
previewCaptionLabel->setAlignment(Qt::AlignCenter);
|
||||
previewCaptionLabel->setWordWrap(true);
|
||||
previewLayout->addWidget(previewCaptionLabel);
|
||||
previewGroup = new QGroupBox;
|
||||
previewGroup->setLayout(previewLayout);
|
||||
|
||||
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
removeButton = new QPushButton;
|
||||
buttons->addButton(removeButton, QDialogButtonBox::ResetRole);
|
||||
|
||||
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
connect(removeButton, &QPushButton::clicked, this, [this]() {
|
||||
currentCard = CardRef{}; // empty signals removal
|
||||
accept();
|
||||
});
|
||||
|
||||
// The crop surface is the primary control: dragging pans, wheel/keys zoom,
|
||||
// editing exactly the same stored parameters the numeric fields do.
|
||||
connect(preview, &PlaymatPreviewWidget::paramsEdited, this, [this](const PlaymatParams &edited) {
|
||||
currentParams = edited;
|
||||
|
||||
QSignalBlocker blockMarginL(marginLSpin);
|
||||
QSignalBlocker blockMarginR(marginRSpin);
|
||||
QSignalBlocker blockOffset(verticalOffsetSpin);
|
||||
QSignalBlocker blockZoom(zoomSpin);
|
||||
marginLSpin->setValue(edited.marginPctL);
|
||||
marginRSpin->setValue(edited.marginPctR);
|
||||
verticalOffsetSpin->setValue(edited.verticalOffset);
|
||||
zoomSpin->setValue(edited.zoom);
|
||||
});
|
||||
|
||||
connect(showNumericEditorsCheck, &QCheckBox::toggled, this, &PlaymatSettingsDialog::setNumericEditorsVisible);
|
||||
setNumericEditorsVisible(false);
|
||||
|
||||
connect(marginLSpin, &QDoubleSpinBox::valueChanged, this, &PlaymatSettingsDialog::onParamChanged);
|
||||
connect(marginRSpin, &QDoubleSpinBox::valueChanged, this, &PlaymatSettingsDialog::onParamChanged);
|
||||
connect(verticalOffsetSpin, &QDoubleSpinBox::valueChanged, this, &PlaymatSettingsDialog::onParamChanged);
|
||||
connect(zoomSpin, &QDoubleSpinBox::valueChanged, this, &PlaymatSettingsDialog::onParamChanged);
|
||||
|
||||
// The crop surface leads visually, card selection supports it below.
|
||||
auto *root = new QVBoxLayout;
|
||||
root->addWidget(previewGroup);
|
||||
root->addWidget(controlsGroup);
|
||||
root->addWidget(buttons);
|
||||
setLayout(root);
|
||||
}
|
||||
|
||||
void PlaymatSettingsDialog::populateProviderCombo(const QString &cardName)
|
||||
{
|
||||
providerComboBox->clear();
|
||||
|
||||
auto card = CardDatabaseManager::query()->getCard({cardName});
|
||||
|
||||
const auto &sets = card.getInfo().getSets();
|
||||
|
||||
for (const auto &printings : sets) {
|
||||
for (const auto &p : printings) {
|
||||
QString setName = p.getSet()->getLongName();
|
||||
QString collector = p.getProperty("num");
|
||||
QString uuid = p.getUuid();
|
||||
|
||||
QString label = setName;
|
||||
if (!collector.isEmpty()) {
|
||||
label += " #" + collector;
|
||||
}
|
||||
|
||||
providerComboBox->addItem(label, uuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PlaymatSettingsDialog::onCardNameChanged(const QString &name)
|
||||
{
|
||||
if (name.isEmpty()) {
|
||||
currentPixmap = QPixmap();
|
||||
preview->setPixmap(currentPixmap);
|
||||
return;
|
||||
}
|
||||
|
||||
const ExactCard card = CardDatabaseManager::query()->getCard({name});
|
||||
if (!card) {
|
||||
currentPixmap = QPixmap();
|
||||
preview->setPixmap(currentPixmap);
|
||||
providerComboBox->clear();
|
||||
return;
|
||||
}
|
||||
|
||||
currentCard.name = name;
|
||||
|
||||
populateProviderCombo(name);
|
||||
|
||||
if (providerComboBox->count() == 0) {
|
||||
currentPixmap = QPixmap();
|
||||
preview->setPixmap(currentPixmap);
|
||||
currentCard.providerId.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
currentCard.providerId = providerComboBox->currentData().toString();
|
||||
reloadPreview();
|
||||
}
|
||||
|
||||
void PlaymatSettingsDialog::reloadPreview()
|
||||
{
|
||||
if (currentCard.name.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ExactCard card = CardDatabaseManager::query()->getCard({currentCard.name, currentCard.providerId});
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
|
||||
disconnect(pixmapUpdatedConnection);
|
||||
|
||||
QPixmap fullRes;
|
||||
CardPictureLoader::getPixmap(fullRes, card, QSize(745, 1040));
|
||||
|
||||
if (fullRes.isNull()) {
|
||||
CardInfo *cardInfo = card.getCardPtr().data();
|
||||
if (cardInfo) {
|
||||
pixmapUpdatedConnection = connect(cardInfo, &CardInfo::pixmapUpdated, this, [this]() { reloadPreview(); });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
currentPixmap = fullRes;
|
||||
preview->setPixmap(currentPixmap);
|
||||
preview->setParams(currentParams);
|
||||
preview->setAttribution(buildArtAttribution(card));
|
||||
}
|
||||
|
||||
void PlaymatSettingsDialog::onParamChanged()
|
||||
{
|
||||
currentParams.marginPctL = marginLSpin->value();
|
||||
currentParams.marginPctR = marginRSpin->value();
|
||||
currentParams.verticalOffset = verticalOffsetSpin->value();
|
||||
currentParams.zoom = zoomSpin->value();
|
||||
preview->setParams(currentParams);
|
||||
}
|
||||
|
||||
void PlaymatSettingsDialog::setNumericEditorsVisible(bool visible)
|
||||
{
|
||||
controlsForm->setRowVisible(leftMarginLabel, visible);
|
||||
controlsForm->setRowVisible(rightMarginLabel, visible);
|
||||
controlsForm->setRowVisible(verticalOffsetLabel, visible);
|
||||
controlsForm->setRowVisible(zoomLabel, visible);
|
||||
|
||||
// A QDialog never resizes itself when its content requirements change,
|
||||
// so revealing the editors would squeeze the crop group until the info
|
||||
// caption ran into the preview. Re-fit the dialog to the new size hint.
|
||||
adjustSize();
|
||||
}
|
||||
|
||||
void PlaymatSettingsDialog::retranslateUi()
|
||||
{
|
||||
setWindowTitle(tr("Playmat Settings"));
|
||||
searchBar->setPlaceholderText(tr("Type a card name..."));
|
||||
cardNameLabel->setText(tr("Card name:"));
|
||||
printingLabel->setText(tr("Printing:"));
|
||||
showNumericEditorsCheck->setText(tr("Show numerical editors"));
|
||||
leftMarginLabel->setText(tr("Left margin (%):"));
|
||||
rightMarginLabel->setText(tr("Right margin (%):"));
|
||||
verticalOffsetLabel->setText(tr("Vertical offset:"));
|
||||
zoomLabel->setText(tr("Zoom:"));
|
||||
controlsGroup->setTitle(tr("Card"));
|
||||
previewGroup->setTitle(tr("Crop"));
|
||||
previewCaptionLabel->setText(
|
||||
tr("Drag to pan, scroll to zoom, arrow keys nudge, plus and minus zoom, Backspace or Esc restores. "
|
||||
"Dimmed strips mark where a wider table crops further."));
|
||||
removeButton->setText(tr("Remove Playmat"));
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
#ifndef COCKATRICE_PLAYMAT_SETTINGS_DIALOG_H
|
||||
#define COCKATRICE_PLAYMAT_SETTINGS_DIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QPixmap>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
|
||||
class QCheckBox;
|
||||
class QComboBox;
|
||||
class QCompleter;
|
||||
class QDoubleSpinBox;
|
||||
class QFormLayout;
|
||||
class QGroupBox;
|
||||
class QLabel;
|
||||
class QLineEdit;
|
||||
class QPushButton;
|
||||
class QWidget;
|
||||
class CardDatabaseModel;
|
||||
class CardDatabaseDisplayModel;
|
||||
class CardSearchModel;
|
||||
class CardCompleterProxyModel;
|
||||
class PlaymatPreviewWidget;
|
||||
|
||||
/**
|
||||
* @brief Dialog for configuring the playmat card art for a deck.
|
||||
*
|
||||
* The crop surface is the primary control: drag to pan the visible art,
|
||||
* scroll (or +/- keys) to zoom, arrow keys to nudge. Card name and printing
|
||||
* are selected below. A checkbox reveals optional numerical editors for the
|
||||
* raw PlaymatParams. These controls edit the same stored PlaymatParams that
|
||||
* ship in deck files and player properties.
|
||||
*/
|
||||
class PlaymatSettingsDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit PlaymatSettingsDialog(const CardRef &initialCard = {},
|
||||
const PlaymatParams &initialParams = {},
|
||||
QWidget *parent = nullptr);
|
||||
|
||||
CardRef card() const;
|
||||
PlaymatParams params() const;
|
||||
|
||||
private slots:
|
||||
void onCardNameChanged(const QString &name);
|
||||
void onParamChanged();
|
||||
void reloadPreview();
|
||||
|
||||
private:
|
||||
void setupUi();
|
||||
void populateProviderCombo(const QString &cardName);
|
||||
void initializeSearchBar();
|
||||
void retranslateUi();
|
||||
void setNumericEditorsVisible(bool visible);
|
||||
QDoubleSpinBox *makeSpinBox(double min, double max, double value, double step);
|
||||
|
||||
QLineEdit *searchBar;
|
||||
QCompleter *completer;
|
||||
CardDatabaseModel *cardDatabaseModel;
|
||||
CardDatabaseDisplayModel *cardDatabaseDisplayModel;
|
||||
CardSearchModel *searchModel;
|
||||
CardCompleterProxyModel *proxyModel;
|
||||
|
||||
QComboBox *providerComboBox;
|
||||
|
||||
QMetaObject::Connection pixmapUpdatedConnection;
|
||||
|
||||
QLabel *cardNameLabel;
|
||||
QLabel *printingLabel;
|
||||
QLabel *previewCaptionLabel;
|
||||
QCheckBox *showNumericEditorsCheck;
|
||||
QFormLayout *controlsForm;
|
||||
QLabel *leftMarginLabel;
|
||||
QLabel *rightMarginLabel;
|
||||
QLabel *verticalOffsetLabel;
|
||||
QLabel *zoomLabel;
|
||||
QGroupBox *controlsGroup;
|
||||
QGroupBox *previewGroup;
|
||||
QPushButton *removeButton;
|
||||
|
||||
QDoubleSpinBox *marginLSpin;
|
||||
QDoubleSpinBox *marginRSpin;
|
||||
QDoubleSpinBox *verticalOffsetSpin;
|
||||
QDoubleSpinBox *zoomSpin;
|
||||
|
||||
PlaymatPreviewWidget *preview;
|
||||
|
||||
QPixmap currentPixmap;
|
||||
CardRef currentCard;
|
||||
PlaymatParams currentParams;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_PLAYMAT_SETTINGS_DIALOG_H
|
||||
140
cockatrice/src/interface/widgets/playmat/playmat_utils.h
Normal file
140
cockatrice/src/interface/widgets/playmat/playmat_utils.h
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
#ifndef COCKATRICE_PLAYMAT_UTILS_H
|
||||
#define COCKATRICE_PLAYMAT_UTILS_H
|
||||
|
||||
#include <QRectF>
|
||||
#include <QSize>
|
||||
#include <QSizeF>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
|
||||
namespace PlaymatUtils
|
||||
{
|
||||
|
||||
/** @brief Upper bound for zooming into the playmat art. */
|
||||
constexpr qreal MAX_ZOOM = 4.0;
|
||||
|
||||
/**
|
||||
* @brief Width of the outer viewing window: full card width trimmed by the
|
||||
* horizontal margins. Guarded against margins summing to >= 1.
|
||||
*/
|
||||
inline qreal playmatVisibleWidth(const QSize &fullCardSize, const PlaymatParams ¶ms)
|
||||
{
|
||||
const qreal srcW = fullCardSize.width();
|
||||
const qreal marginL = params.marginPctL * srcW;
|
||||
const qreal marginR = params.marginPctR * srcW;
|
||||
return qMax(0.0, srcW - marginL - marginR);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Zoom clamped to the range where every step renders differently.
|
||||
*
|
||||
* The square sampling window is visibleWidth / zoom, zooming out past
|
||||
* visibleWidth / min(card width, height) would sample beyond the card itself,
|
||||
* which both looks broken and makes whole ranges of the parameter dead. The
|
||||
* floor is therefore derived from the actual image instead of a static value,
|
||||
* and is shared verbatim by the render path and the editor's gesture math so
|
||||
* the two can never disagree.
|
||||
*/
|
||||
inline qreal playmatClampedZoom(const QSize &fullCardSize, const PlaymatParams ¶ms)
|
||||
{
|
||||
const qreal minDim = qMin<qreal>(fullCardSize.width(), fullCardSize.height());
|
||||
const qreal visibleW = playmatVisibleWidth(fullCardSize, params);
|
||||
const qreal zoomOutFloor = (minDim > 0.0 && visibleW > 0.0) ? visibleW / minDim : 1.0;
|
||||
// The floor deliberately bypasses MAX_ZOOM: when the art is much wider
|
||||
// than tall, keeping the square window inside it requires more than 4x
|
||||
// zoom-out, and honoring that larger floor keeps side within
|
||||
// min(card width, height). Zooming IN is still capped at MAX_ZOOM.
|
||||
return qMin(MAX_ZOOM, qMax(params.zoom, zoomOutFloor));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Side of the square sampling window actually rendered for these
|
||||
* parameters. Never exceeds either card dimension, so the source rect
|
||||
* always lies within the image (vertical travel remains for panning
|
||||
* whenever the art is taller than it is wide).
|
||||
*/
|
||||
inline qreal playmatWindowSide(const QSize &fullCardSize, const PlaymatParams ¶ms)
|
||||
{
|
||||
const qreal visibleW = playmatVisibleWidth(fullCardSize, params);
|
||||
if (visibleW <= 0.0) {
|
||||
return 0.0;
|
||||
}
|
||||
return visibleW / playmatClampedZoom(fullCardSize, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Computes the source region of the full resolution card image to use as a playmat.
|
||||
*
|
||||
* Parameters are relative to the full card image. horizontal margins trim the
|
||||
* card borders (shifting them pans the window), verticalOffset places the top
|
||||
* edge of the sampling window within its available travel, and zoom scales
|
||||
* into the trimmed span. The result always lies within the card image bounds.
|
||||
*
|
||||
* @param fullCardSize Size of the full card image.
|
||||
* @param params Positioning parameters.
|
||||
* @return Source rectangle in full card image pixel coordinates.
|
||||
*/
|
||||
inline QRectF computeArtSourceRect(const QSize &fullCardSize, const PlaymatParams ¶ms)
|
||||
{
|
||||
const qreal srcW = fullCardSize.width();
|
||||
const qreal srcH = fullCardSize.height();
|
||||
|
||||
// Square sampling window, keeps art unskewed, never exceeds the card on
|
||||
// either axis thanks to the zoom floor in playmatWindowSide().
|
||||
const qreal side = playmatWindowSide(fullCardSize, params);
|
||||
|
||||
// verticalOffset places the TOP edge of the sampling window itself within
|
||||
// its travel, so the full [0, 1] parameter range is live at every zoom and
|
||||
// the window can always reach the very top (0.0) and bottom (1.0) of the
|
||||
// art.
|
||||
const qreal offset = qBound(0.0, params.verticalOffset, 1.0);
|
||||
const qreal y = offset * qMax(0.0, srcH - side);
|
||||
|
||||
// Horizontally the sampling window sits centered inside the trimmed span
|
||||
// (margins pan it), zooming out can make it wider than that span, so it
|
||||
// is then kept within the image, an edge stop, never an invalid rect.
|
||||
const qreal outerW = playmatVisibleWidth(fullCardSize, params);
|
||||
const qreal x = qBound(0.0, params.marginPctL * srcW + (outerW - side) / 2.0, qMax(0.0, srcW - side));
|
||||
|
||||
return QRectF(x, y, side, side);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns the destination rectangle that fits a source of the given aspect
|
||||
* ratio into dstArea using "cover" semantics (no distortion, overflows cropped).
|
||||
*
|
||||
* @param dstArea Area to fill.
|
||||
* @param srcSize Size of the source, only its aspect ratio matters.
|
||||
* @return Destination rectangle centered in dstArea.
|
||||
*/
|
||||
inline QRectF coverFitRect(const QRectF &dstArea, const QSizeF &srcSize)
|
||||
{
|
||||
const qreal srcAspect = srcSize.width() / srcSize.height();
|
||||
const qreal dstAspect = dstArea.width() / dstArea.height();
|
||||
|
||||
if (srcAspect > dstAspect) {
|
||||
const qreal dstW = dstArea.height() * srcAspect;
|
||||
return QRectF(dstArea.left() + (dstArea.width() - dstW) / 2.0, dstArea.top(), dstW, dstArea.height());
|
||||
}
|
||||
|
||||
const qreal dstH = dstArea.width() / srcAspect;
|
||||
return QRectF(dstArea.left(), dstArea.top() + (dstArea.height() - dstH) / 2.0, dstArea.width(), dstH);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Fits a rectangle of the given aspect ratio into dstArea, centered,
|
||||
* touching the constraining dimension ("aspect fit" of the FRAME
|
||||
* itself, not of a source image).
|
||||
*/
|
||||
inline QRectF aspectFitRect(const QRectF &dstArea, qreal aspect)
|
||||
{
|
||||
if (aspect <= 0.0) {
|
||||
return dstArea;
|
||||
}
|
||||
qreal w = qMin(dstArea.width(), dstArea.height() * aspect);
|
||||
qreal h = w / aspect;
|
||||
return QRectF(dstArea.left() + (dstArea.width() - w) / 2.0, dstArea.top() + (dstArea.height() - h) / 2.0, w, h);
|
||||
}
|
||||
|
||||
} // namespace PlaymatUtils
|
||||
|
||||
#endif // COCKATRICE_PLAYMAT_UTILS_H
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
#include "printing_selector_card_overlay_widget.h"
|
||||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../cards/card_info_picture_widget.h"
|
||||
#include "printing_selector_card_display_widget.h"
|
||||
|
||||
#include <QImageReader>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@
|
|||
#include <QDesktopServices>
|
||||
#include <QMouseEvent>
|
||||
#include <QScrollBar>
|
||||
#include <QTimer>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/network/server/remote/user_level.h>
|
||||
#include <libcockatrice/settings/chat_settings.h>
|
||||
|
|
@ -48,9 +51,12 @@ ChatView::ChatView(TabSupervisor *_tabSupervisor, AbstractGame *_game, bool _sho
|
|||
|
||||
viewport()->setCursor(Qt::IBeamCursor);
|
||||
setReadOnly(true);
|
||||
setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::LinksAccessibleByMouse);
|
||||
setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::LinksAccessibleByMouse | Qt::LinksAccessibleByKeyboard);
|
||||
setOpenLinks(false);
|
||||
connect(this, &ChatView::anchorClicked, this, &ChatView::openLink);
|
||||
|
||||
connect(verticalScrollBar(), &QScrollBar::rangeChanged, this, &ChatView::onScrollBarRangeChanged);
|
||||
connect(verticalScrollBar(), &QScrollBar::valueChanged, this, &ChatView::onScrollBarValueChanged);
|
||||
}
|
||||
|
||||
void ChatView::adjustColorsToPalette()
|
||||
|
|
@ -151,7 +157,7 @@ void ChatView::appendHtml(const QString &html)
|
|||
bool atBottom = verticalScrollBar()->value() >= verticalScrollBar()->maximum();
|
||||
prepareBlock().insertHtml(html);
|
||||
if (atBottom) {
|
||||
verticalScrollBar()->setValue(verticalScrollBar()->maximum());
|
||||
scrollToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -169,7 +175,7 @@ void ChatView::appendHtmlServerMessage(const QString &html, bool optionalIsBold,
|
|||
|
||||
prepareBlock().insertHtml(htmlText);
|
||||
if (atBottom) {
|
||||
verticalScrollBar()->setValue(verticalScrollBar()->maximum());
|
||||
scrollToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -215,6 +221,46 @@ void ChatView::appendUrlTag(QTextCursor &cursor, QString url)
|
|||
cursor.setCharFormat(oldFormat);
|
||||
}
|
||||
|
||||
void ChatView::appendGameLinkTag(QTextCursor &cursor, const QString &url)
|
||||
{
|
||||
const QUrl gameUrl(url);
|
||||
const QUrlQuery query(gameUrl);
|
||||
const QString hostname = query.queryItemValue("hostname");
|
||||
// FullyDecoded undoes every %XX escape, so a description that itself
|
||||
// contains "%" cannot end up displayed as "%25" in the label.
|
||||
const QString description = query.queryItemValue("game", QUrl::FullyDecoded);
|
||||
const int gameId = query.queryItemValue("gameid").toInt();
|
||||
|
||||
QString label;
|
||||
if (gameId > 0 && !hostname.isEmpty()) {
|
||||
// Links built before the description was embedded stay readable: the
|
||||
// id + server fallback below is identical to the old anchor text.
|
||||
if (!description.isEmpty()) {
|
||||
// Multi-arg .arg() replaces all placeholders in a single pass, so a
|
||||
// description containing "%…" cannot corrupt later placeholders.
|
||||
label = tr("Join game \"%1\" (#%2) on %3").arg(description, QString::number(gameId), hostname);
|
||||
} else {
|
||||
label = tr("Join game #%1 on %2").arg(QString::number(gameId), hostname);
|
||||
}
|
||||
} else {
|
||||
label = tr("Join game");
|
||||
}
|
||||
|
||||
QTextCharFormat oldFormat = cursor.charFormat();
|
||||
QTextCharFormat gameLinkFormat = oldFormat;
|
||||
gameLinkFormat.setForeground(linkColor);
|
||||
gameLinkFormat.setFontWeight(QFont::Bold);
|
||||
gameLinkFormat.setAnchor(true);
|
||||
gameLinkFormat.setAnchorHref(url);
|
||||
QColor background = palette().highlight().color();
|
||||
background.setAlpha(40);
|
||||
gameLinkFormat.setBackground(background);
|
||||
|
||||
cursor.setCharFormat(gameLinkFormat);
|
||||
cursor.insertText(label);
|
||||
cursor.setCharFormat(oldFormat);
|
||||
}
|
||||
|
||||
void ChatView::appendMessage(QString message,
|
||||
RoomMessageTypeFlags messageType,
|
||||
const ServerInfo_User &userInfo,
|
||||
|
|
@ -227,6 +273,14 @@ void ChatView::appendMessage(QString message,
|
|||
// messageType should be Event_RoomSay::UserMessage though we don't actually check
|
||||
bool isUserMessage = !(userName.toLower() == "servatrice" || userName.isEmpty());
|
||||
bool sameSender = isUserMessage && userName == lastSender;
|
||||
|
||||
if (isUserMessage) {
|
||||
chatHistory.append({userName, message, QDateTime::currentDateTime()});
|
||||
while (chatHistory.size() > MAX_CHAT_HISTORY) {
|
||||
chatHistory.removeFirst();
|
||||
}
|
||||
}
|
||||
|
||||
QTextCursor cursor = prepareBlock(sameSender);
|
||||
lastSender = userName;
|
||||
|
||||
|
|
@ -338,11 +392,36 @@ void ChatView::appendMessage(QString message,
|
|||
}
|
||||
}
|
||||
|
||||
if (atBottom) {
|
||||
// ChatHistory messages are only ever sent once per room, right after joining, before the user can
|
||||
// interact with the view. Always scroll to the bottom so the whole history is visible on join.
|
||||
if (atBottom || messageType.testFlag(Event_RoomSay::ChatHistory)) {
|
||||
scrollToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
void ChatView::scrollToBottom()
|
||||
{
|
||||
// The document layout, and therefore the scrollbar range, may be updated asynchronously (e.g. while
|
||||
// the chat history is loaded into a view that has not been laid out yet). Setting the value once is
|
||||
// not enough: keep stickToBottom set so any later range change scrolls to the new maximum as well.
|
||||
stickToBottom = true;
|
||||
verticalScrollBar()->setValue(verticalScrollBar()->maximum());
|
||||
}
|
||||
|
||||
void ChatView::onScrollBarRangeChanged()
|
||||
{
|
||||
if (stickToBottom) {
|
||||
verticalScrollBar()->setValue(verticalScrollBar()->maximum());
|
||||
}
|
||||
}
|
||||
|
||||
void ChatView::onScrollBarValueChanged(int value)
|
||||
{
|
||||
if (value < verticalScrollBar()->maximum()) {
|
||||
stickToBottom = false;
|
||||
}
|
||||
}
|
||||
|
||||
void ChatView::checkTag(QTextCursor &cursor, QString &message)
|
||||
{
|
||||
if (message.startsWith("[card]")) {
|
||||
|
|
@ -474,6 +553,17 @@ void ChatView::checkWord(QTextCursor &cursor, QString &message)
|
|||
}
|
||||
}
|
||||
|
||||
if (fullWordUpToSpaceOrEnd.startsWith("cockatrice://", Qt::CaseInsensitive)) {
|
||||
// Only links to a game (cockatrice://joingame) become invite buttons;
|
||||
// any other cockatrice:// scheme falls through to plain text below.
|
||||
const QUrl gameLink(fullWordUpToSpaceOrEnd);
|
||||
if (gameLink.host().compare("joingame", Qt::CaseInsensitive) == 0) {
|
||||
appendGameLinkTag(cursor, fullWordUpToSpaceOrEnd);
|
||||
cursor.insertText(rest, defaultFormat);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// check word mentions
|
||||
for (const QString &word : highlightedWords) {
|
||||
if (fullWordUpToSpaceOrEnd.compare(word, Qt::CaseInsensitive) == 0) {
|
||||
|
|
@ -558,6 +648,19 @@ void ChatView::clearChat()
|
|||
document()->clear();
|
||||
lastSender = "";
|
||||
evenNumber = true;
|
||||
chatHistory.clear();
|
||||
}
|
||||
|
||||
QString ChatView::getRecentChatLog(int maxMessages) const
|
||||
{
|
||||
QStringList lines;
|
||||
int start = qMax(0, chatHistory.size() - maxMessages);
|
||||
for (int i = start; i < chatHistory.size(); ++i) {
|
||||
const ChatLogEntry &entry = chatHistory.at(i);
|
||||
lines.append(
|
||||
QString("[%1] %2: %3").arg(entry.timestamp.toString("hh:mm:ss")).arg(entry.userName).arg(entry.message));
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
void ChatView::redactMessages(const QString &userName, int amount)
|
||||
|
|
@ -685,6 +788,11 @@ void ChatView::mouseReleaseEvent(QMouseEvent *event)
|
|||
|
||||
void ChatView::openLink(const QUrl &link)
|
||||
{
|
||||
if (link.scheme() == "cockatrice") {
|
||||
emit cockatriceLinkActivated(link.toString(QUrl::FullyEncoded));
|
||||
return;
|
||||
}
|
||||
|
||||
if ((link.scheme() == "card") || (link.scheme() == "user")) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,13 @@ public:
|
|||
QTextBlock block;
|
||||
};
|
||||
|
||||
struct ChatLogEntry
|
||||
{
|
||||
QString userName;
|
||||
QString message;
|
||||
QDateTime timestamp;
|
||||
};
|
||||
|
||||
class ChatView : public QTextBrowser
|
||||
{
|
||||
Q_OBJECT
|
||||
|
|
@ -60,15 +67,20 @@ private:
|
|||
QStringList highlightedWords;
|
||||
bool evenNumber;
|
||||
bool showTimestamps;
|
||||
bool stickToBottom = false;
|
||||
HoveredItemType hoveredItemType;
|
||||
QString hoveredContent;
|
||||
QAction *messageClicked;
|
||||
QMap<QString, QVector<UserMessagePosition>> userMessagePositions;
|
||||
QList<ChatLogEntry> chatHistory;
|
||||
static constexpr int MAX_CHAT_HISTORY = 200;
|
||||
|
||||
[[nodiscard]] QTextFragment getFragmentUnderMouse(const QPoint &pos) const;
|
||||
QTextCursor prepareBlock(bool same = false);
|
||||
void scrollToBottom();
|
||||
void appendCardTag(QTextCursor &cursor, const QString &cardName);
|
||||
void appendUrlTag(QTextCursor &cursor, QString url);
|
||||
void appendGameLinkTag(QTextCursor &cursor, const QString &url);
|
||||
static QColor getCustomMentionColor();
|
||||
static QColor getCustomHighlightColor();
|
||||
void showSystemPopup(const QString &userName);
|
||||
|
|
@ -88,6 +100,8 @@ private slots:
|
|||
void actMessageClicked();
|
||||
void adjustColorsToPalette();
|
||||
void refreshBlockColors();
|
||||
void onScrollBarRangeChanged();
|
||||
void onScrollBarValueChanged(int value);
|
||||
|
||||
public:
|
||||
ChatView(TabSupervisor *_tabSupervisor, AbstractGame *_game, bool _showTimestamps, QWidget *parent = nullptr);
|
||||
|
|
@ -102,6 +116,7 @@ public:
|
|||
bool playerBold = false);
|
||||
void clearChat();
|
||||
void redactMessages(const QString &userName, int amount);
|
||||
QString getRecentChatLog(int maxMessages = 50) const;
|
||||
|
||||
protected:
|
||||
void enterEvent(QEnterEvent *event) override;
|
||||
|
|
@ -117,6 +132,7 @@ signals:
|
|||
void addMentionTag(QString mentionTag);
|
||||
void messageClickedSignal();
|
||||
void showMentionPopup(const QString &userName);
|
||||
void cockatriceLinkActivated(const QString &url);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ struct GameFilterConfigs
|
|||
bool hideNotBuddyCreatedGames = false;
|
||||
bool hideOpenDecklistGames = false;
|
||||
QString gameNameFilter = "";
|
||||
QStringList creatorNameFilters = {};
|
||||
QStringList hostNameFilters = {};
|
||||
QSet<int> gameTypeFilter = {};
|
||||
int maxPlayersFilterMin = DEFAULT_MAX_PLAYERS_MIN;
|
||||
int maxPlayersFilterMax = DEFAULT_MAX_PLAYERS_MAX;
|
||||
|
|
|
|||
23
cockatrice/src/interface/widgets/server/game_link.cpp
Normal file
23
cockatrice/src/interface/widgets/server/game_link.cpp
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
#include "game_link.h"
|
||||
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
|
||||
QString makeGameJoinLink(const QString &hostname, int port, int roomId, int gameId, const QString &description)
|
||||
{
|
||||
QUrl url;
|
||||
url.setScheme("cockatrice");
|
||||
url.setHost("joingame");
|
||||
QUrlQuery query;
|
||||
query.addQueryItem("hostname", hostname);
|
||||
query.addQueryItem("port", QString::number(port));
|
||||
query.addQueryItem("roomid", QString::number(roomId));
|
||||
query.addQueryItem("gameid", QString::number(gameId));
|
||||
if (!description.isEmpty()) {
|
||||
// addQueryItem percent-encodes, so arbitrary descriptions (quotes,
|
||||
// ampersands, non-ASCII…) survive the trip through chat.
|
||||
query.addQueryItem("game", description);
|
||||
}
|
||||
url.setQuery(query);
|
||||
return url.toString(QUrl::FullyEncoded);
|
||||
}
|
||||
41
cockatrice/src/interface/widgets/server/game_link.h
Normal file
41
cockatrice/src/interface/widgets/server/game_link.h
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
/**
|
||||
* @file game_link.h
|
||||
* @ingroup UI
|
||||
* @brief Builds cockatrice://joingame links that let another user join a server game.
|
||||
*/
|
||||
|
||||
#ifndef GAME_LINK_H
|
||||
#define GAME_LINK_H
|
||||
|
||||
#include <QString>
|
||||
|
||||
/**
|
||||
* Builds a cockatrice://joingame link for the given server game. The receiver's
|
||||
* client opens it through the intent chain (connect -> join room -> join game).
|
||||
* @p description, when non-empty, is embedded in the link as the URL-encoded
|
||||
* "game" query item so the receiving client can name the game in its confirm
|
||||
* prompt and chat anchor instead of only its numeric id. Links built without it
|
||||
* stay valid: the parser and chat renderer fall back to the id alone.
|
||||
*/
|
||||
QString
|
||||
makeGameJoinLink(const QString &hostname, int port, int roomId, int gameId, const QString &description = QString());
|
||||
|
||||
/**
|
||||
* One game the inviter is currently in and can invite another user to.
|
||||
* @p label is meant for display in menus, @p url is the ready-made invite link.
|
||||
* @p description is the raw game description for building tr()-wrapped invite
|
||||
* messages (the label already embeds it, but the send sites need the raw value).
|
||||
* @p onlyBuddies and @p creatorName mirror the server game's room settings so
|
||||
* callers can gate the invite to the creator's buddies.
|
||||
*/
|
||||
struct GameInviteOption
|
||||
{
|
||||
int gameId = 0;
|
||||
QString label;
|
||||
QString url;
|
||||
QString description;
|
||||
bool onlyBuddies = false;
|
||||
QString creatorName;
|
||||
};
|
||||
|
||||
#endif // GAME_LINK_H
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
#include "../interface/widgets/tabs/tab_room.h"
|
||||
#include "../interface/widgets/tabs/tab_supervisor.h"
|
||||
#include "../interface/widgets/utility/get_text_with_max.h"
|
||||
#include "game_link.h"
|
||||
#include "games_model.h"
|
||||
#include "user/user_list_manager.h"
|
||||
|
||||
|
|
@ -18,8 +19,6 @@
|
|||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QTreeView>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
||||
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||
#include <libcockatrice/protocol/pb/room_commands.pb.h>
|
||||
|
|
@ -320,19 +319,12 @@ void GameSelector::customContextMenu(const QPoint &point)
|
|||
dlg.exec();
|
||||
});
|
||||
|
||||
QAction copyLink(tr("Copy Game Link"));
|
||||
QAction copyLink(tr("Cop&y game link"));
|
||||
connect(©Link, &QAction::triggered, this, [=, this]() {
|
||||
const ServerInfo_Game &gameInfo = gameListModel->getGame(index.data(Qt::UserRole).toInt());
|
||||
QUrl url;
|
||||
url.setScheme("cockatrice");
|
||||
url.setHost("joingame");
|
||||
QUrlQuery query;
|
||||
query.addQueryItem("hostname", client->serverName());
|
||||
query.addQueryItem("port", QString::number(client->serverPort()));
|
||||
query.addQueryItem("roomid", QString::number(gameInfo.room_id()));
|
||||
query.addQueryItem("gameid", QString::number(gameInfo.game_id()));
|
||||
url.setQuery(query);
|
||||
QGuiApplication::clipboard()->setText(url.toString(QUrl::FullyEncoded));
|
||||
QGuiApplication::clipboard()->setText(makeGameJoinLink(client->serverName(), client->serverPort(),
|
||||
gameInfo.room_id(), gameInfo.game_id(),
|
||||
QString::fromStdString(gameInfo.description())));
|
||||
});
|
||||
|
||||
QMenu menu;
|
||||
|
|
@ -367,18 +359,40 @@ void GameSelector::joinGame(const bool asSpectator, const bool asJudge)
|
|||
return;
|
||||
}
|
||||
|
||||
const ServerInfo_Game &game = gameListModel->getGame(ind.data(Qt::UserRole).toInt());
|
||||
joinGame(gameListModel->getGame(ind.data(Qt::UserRole).toInt()), asSpectator, asJudge);
|
||||
}
|
||||
|
||||
void GameSelector::joinGame(const ServerInfo_Game &game, const bool asSpectator, const bool asJudge)
|
||||
{
|
||||
if (tabSupervisor->switchToGameTabIfAlreadyExists(game.game_id())) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool spectator = asSpectator || game.player_count() == game.max_players();
|
||||
|
||||
bool overrideRestrictions = !tabSupervisor->getAdminLocked();
|
||||
|
||||
// Joining a full game without override privileges silently becomes a
|
||||
// spectator join, so ask first instead of surprising the player.
|
||||
const bool gameFull = game.player_count() == game.max_players();
|
||||
if (gameFull && !asSpectator && !asJudge && !overrideRestrictions) {
|
||||
const QMessageBox::StandardButton answer =
|
||||
QMessageBox::question(this, tr("Join game"), tr("The game is full. Join as a spectator instead?"),
|
||||
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
|
||||
if (answer != QMessageBox::Yes) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool spectator = asSpectator || gameFull;
|
||||
|
||||
QString password;
|
||||
if (game.with_password() && !(spectator && !game.spectators_need_password()) && !overrideRestrictions) {
|
||||
bool ok;
|
||||
password = getTextWithMax(this, tr("Join game"), tr("Password:"), QLineEdit::Password, QString(), &ok);
|
||||
// Games without a description have no sensible label — fall back to the
|
||||
// game id so the prompt still tells the user which game they're entering.
|
||||
const QString gameLabel = QString::fromStdString(game.description());
|
||||
const QString prompt = gameLabel.isEmpty() ? tr("Password for game #%1:").arg(game.game_id())
|
||||
: tr("Password for \"%1\":").arg(gameLabel);
|
||||
password = getTextWithMax(this, tr("Join game"), prompt, QLineEdit::Password, QString(), &ok);
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -404,18 +418,16 @@ void GameSelector::joinGame(const bool asSpectator, const bool asJudge)
|
|||
disableButtons();
|
||||
}
|
||||
|
||||
bool GameSelector::joinGameById(int gameId)
|
||||
bool GameSelector::joinGameById(const int gameId, const bool asSpectator)
|
||||
{
|
||||
auto *model = gameListView->model();
|
||||
|
||||
for (int row = 0; row < model->rowCount(); ++row) {
|
||||
QModelIndex idx = model->index(row, 0);
|
||||
const ServerInfo_Game &game = gameListModel->getGame(idx.data(Qt::UserRole).toInt());
|
||||
if (game.game_id() == gameId) {
|
||||
gameListView->setCurrentIndex(idx);
|
||||
joinGame();
|
||||
return true;
|
||||
for (int row = 0; row < gameListModel->rowCount(); ++row) {
|
||||
const ServerInfo_Game &game = gameListModel->getGame(row);
|
||||
if (game.game_id() != gameId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
joinGame(game, asSpectator);
|
||||
return true;
|
||||
}
|
||||
|
||||
qWarning() << "Game" << gameId << "not found";
|
||||
|
|
|
|||
|
|
@ -171,6 +171,17 @@ private:
|
|||
*/
|
||||
void joinGame(bool asSpectator = false, bool asJudge = false);
|
||||
|
||||
/**
|
||||
* @brief Performs the join or spectate action for a specific game.
|
||||
* @param game The game to join.
|
||||
* @param asSpectator True to join as a spectator, false to join as a player.
|
||||
* @param asJudge True to join as a judge, false to join as a player.
|
||||
*
|
||||
* Unlike the selection-based overload, this does not depend on the game being
|
||||
* visible in the filtered game list.
|
||||
*/
|
||||
void joinGame(const ServerInfo_Game &game, bool asSpectator = false, bool asJudge = false);
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Constructs a GameSelector widget.
|
||||
|
|
@ -202,7 +213,16 @@ public:
|
|||
* @param info The ServerInfo_Game object containing information about the game to update.
|
||||
*/
|
||||
void processGameInfo(const ServerInfo_Game &info);
|
||||
bool joinGameById(int gameId);
|
||||
/**
|
||||
* @brief Finds a game by ID and joins or spectates it.
|
||||
* @param gameId The ID of the game to join.
|
||||
* @param asSpectator True to join as a spectator, false to join as a player.
|
||||
* @return True if the game was found and joined, false otherwise.
|
||||
*
|
||||
* Unlike the selection-based overload, this does not depend on the game
|
||||
* being visible in the filtered game list.
|
||||
*/
|
||||
bool joinGameById(int gameId, bool asSpectator = false);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -17,13 +17,27 @@ enum GameListColumn
|
|||
ROOM,
|
||||
CREATED,
|
||||
DESCRIPTION,
|
||||
CREATOR,
|
||||
HOST,
|
||||
GAME_TYPE,
|
||||
RESTRICTIONS,
|
||||
PLAYERS,
|
||||
SPECTATORS
|
||||
};
|
||||
|
||||
namespace
|
||||
{
|
||||
/**
|
||||
* @brief Returns the user info of the game's current host, falling back to the creator.
|
||||
*
|
||||
* The server only sends host_info once a host transfer has happened, so older
|
||||
* servers and freshly created games fall back to the original creator.
|
||||
*/
|
||||
const ServerInfo_User &getGameHost(const ServerInfo_Game &game)
|
||||
{
|
||||
return game.has_host_info() ? game.host_info() : game.creator_info();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
const QString GamesModel::getGameCreatedString(const int secs)
|
||||
{
|
||||
static const QTime zeroTime{0, 0};
|
||||
|
|
@ -110,16 +124,16 @@ QVariant GamesModel::data(const QModelIndex &index, int role) const
|
|||
default:
|
||||
return QVariant();
|
||||
}
|
||||
case CREATOR: {
|
||||
case HOST: {
|
||||
switch (role) {
|
||||
case SORT_ROLE:
|
||||
case Qt::DisplayRole:
|
||||
return QString::fromStdString(gameentry.creator_info().name());
|
||||
return QString::fromStdString(getGameHost(gameentry).name());
|
||||
case Qt::DecorationRole: {
|
||||
return UserLevelPixmapGenerator::generateIcon(
|
||||
13, UserLevelFlags(gameentry.creator_info().user_level()),
|
||||
gameentry.creator_info().pawn_colors(), false,
|
||||
QString::fromStdString(gameentry.creator_info().privlevel()));
|
||||
const ServerInfo_User &host = getGameHost(gameentry);
|
||||
return UserLevelPixmapGenerator::generateIcon(13, UserLevelFlags(host.user_level()),
|
||||
host.pawn_colors(), false,
|
||||
QString::fromStdString(host.privlevel()));
|
||||
}
|
||||
default:
|
||||
return QVariant();
|
||||
|
|
@ -233,8 +247,8 @@ QVariant GamesModel::headerData(int section, Qt::Orientation /*orientation*/, in
|
|||
}
|
||||
case DESCRIPTION:
|
||||
return tr("Description");
|
||||
case CREATOR:
|
||||
return tr("Creator");
|
||||
case HOST:
|
||||
return tr("Host");
|
||||
case GAME_TYPE:
|
||||
return tr("Type");
|
||||
case RESTRICTIONS:
|
||||
|
|
@ -271,6 +285,9 @@ void GamesModel::updateGameList(const ServerInfo_Game &game)
|
|||
gameList.removeAt(i);
|
||||
endRemoveRows();
|
||||
} else {
|
||||
// MergeFrom concatenates repeated fields instead of replacing them,
|
||||
// so clear game_types first to avoid duplicated entries.
|
||||
gameList[i].clear_game_types();
|
||||
gameList[i].MergeFrom(game);
|
||||
emit dataChanged(index(i, 0), index(i, NUM_COLS - 1));
|
||||
}
|
||||
|
|
@ -347,7 +364,7 @@ void GamesProxyModel::loadFilterParameters(const QMap<int, QString> &allGameType
|
|||
gameFilters.isHideFullGames(), gameFilters.isHideGamesThatStarted(),
|
||||
gameFilters.isHidePasswordProtectedGames(), gameFilters.isHideNotBuddyCreatedGames(),
|
||||
gameFilters.isHideOpenDecklistGames(), gameFilters.getGameNameFilter(),
|
||||
gameFilters.getCreatorNameFilters(), newGameTypeFilter, gameFilters.getMinPlayers(),
|
||||
gameFilters.getHostNameFilters(), newGameTypeFilter, gameFilters.getMinPlayers(),
|
||||
gameFilters.getMaxPlayers(), gameFilters.getMaxGameAge(),
|
||||
gameFilters.isShowOnlyIfSpectatorsCanWatch(), gameFilters.isShowSpectatorPasswordProtected(),
|
||||
gameFilters.isShowOnlyIfSpectatorsCanChat(), gameFilters.isShowOnlyIfSpectatorsCanSeeHands()});
|
||||
|
|
@ -364,7 +381,7 @@ void GamesProxyModel::saveFilterParameters(const QMap<int, QString> &allGameType
|
|||
gameFilters.setHideNotBuddyCreatedGames(filters.hideNotBuddyCreatedGames);
|
||||
gameFilters.setHideOpenDecklistGames(filters.hideOpenDecklistGames);
|
||||
gameFilters.setGameNameFilter(filters.gameNameFilter);
|
||||
gameFilters.setCreatorNameFilters(filters.creatorNameFilters);
|
||||
gameFilters.setHostNameFilters(filters.hostNameFilters);
|
||||
|
||||
QMapIterator<int, QString> gameTypeIterator(allGameTypes);
|
||||
while (gameTypeIterator.hasNext()) {
|
||||
|
|
@ -409,11 +426,11 @@ bool GamesProxyModel::filterAcceptsRow(int sourceRow) const
|
|||
return false;
|
||||
}
|
||||
if (filters.hideIgnoredUserGames &&
|
||||
userListProxy->isUserIgnored(QString::fromStdString(game.creator_info().name()))) {
|
||||
userListProxy->isUserIgnored(QString::fromStdString(getGameHost(game).name()))) {
|
||||
return false;
|
||||
}
|
||||
if (filters.hideNotBuddyCreatedGames &&
|
||||
!userListProxy->isUserBuddy(QString::fromStdString(game.creator_info().name()))) {
|
||||
!userListProxy->isUserBuddy(QString::fromStdString(getGameHost(game).name()))) {
|
||||
return false;
|
||||
}
|
||||
if (filters.hideFullGames && game.player_count() == game.max_players()) {
|
||||
|
|
@ -435,10 +452,10 @@ bool GamesProxyModel::filterAcceptsRow(int sourceRow) const
|
|||
return false;
|
||||
}
|
||||
}
|
||||
if (!filters.creatorNameFilters.isEmpty()) {
|
||||
if (!filters.hostNameFilters.isEmpty()) {
|
||||
bool found = false;
|
||||
for (const auto &createNameFilter : filters.creatorNameFilters) {
|
||||
if (QString::fromStdString(game.creator_info().name()).contains(createNameFilter, Qt::CaseInsensitive)) {
|
||||
for (const auto &hostNameFilter : filters.hostNameFilters) {
|
||||
if (QString::fromStdString(getGameHost(game).name()).contains(hostNameFilter, Qt::CaseInsensitive)) {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,10 @@ void UserCardArtProvider::requestCardArt(const QString &userName, const QString
|
|||
|
||||
const QString key = makeKey(userName, cardName, providerId);
|
||||
|
||||
if (cardArtCache.contains(key) || pending.contains(key)) {
|
||||
if (pending.contains(key)) {
|
||||
return;
|
||||
}
|
||||
if (cardArtCache.contains(key) && !cardArtCache.value(key).isNull()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -63,6 +66,10 @@ QPixmap UserCardArtProvider::cropCardArt(const QPixmap &fullRes)
|
|||
|
||||
void UserCardArtProvider::insertIntoCache(const QString &key, const QPixmap &pixmap)
|
||||
{
|
||||
if (pixmap.isNull()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!cardArtCache.contains(key)) {
|
||||
cacheInsertionOrder.append(key);
|
||||
while (cacheInsertionOrder.size() > MaxCacheEntries) {
|
||||
|
|
@ -129,8 +136,6 @@ void UserCardArtProvider::processQueue()
|
|||
|
||||
if (!fullRes.isNull()) {
|
||||
self->insertIntoCache(key, self->cropCardArt(fullRes));
|
||||
} else {
|
||||
self->insertIntoCache(key, QPixmap());
|
||||
}
|
||||
|
||||
self->pending.remove(key);
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ public slots:
|
|||
|
||||
private:
|
||||
bool dbReady = false;
|
||||
static constexpr int MaxCacheEntries = 300;
|
||||
static constexpr int MaxCacheEntries = 1024;
|
||||
QList<QString> cacheInsertionOrder; // FIFO eviction
|
||||
QMap<QString, QPixmap> cardArtCache;
|
||||
QSet<QString> pending;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
#include "user_card_settings_dialog.h"
|
||||
|
||||
#include "../../../card_picture_loader/card_picture_loader.h"
|
||||
#include "card/card_completer_proxy_model.h"
|
||||
#include "../../cards/art_crop_attribution.h"
|
||||
#include "../../utility/completer_utils.h"
|
||||
#include "card/card_search_model.h"
|
||||
#include "card_database_display_model.h"
|
||||
#include "card_database_model.h"
|
||||
|
|
@ -14,19 +15,46 @@
|
|||
#include <QFormLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QKeyEvent>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QPushButton>
|
||||
#include <QRegularExpression>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWheelEvent>
|
||||
#include <cmath>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
// Gesture clamps and step sizes for this direct manipulation surface. The
|
||||
// gesture zoom floor is 1.0: basescale already cover fits the art, so any
|
||||
// smaller scale would underfill the strip.
|
||||
constexpr qreal kMinGestureZoom = 1.0;
|
||||
constexpr qreal kMaxZoom = 4.0;
|
||||
constexpr qreal kKeyPanOffsetStep = 0.01;
|
||||
constexpr qreal kKeyZoomStep = 1.05;
|
||||
constexpr qreal kWheelZoomBase = 1.15; // zoom factor per wheel notch
|
||||
} // namespace
|
||||
|
||||
CardArtPreviewWidget::CardArtPreviewWidget(QWidget *parent) : QWidget(parent)
|
||||
{
|
||||
setMinimumSize(400, 72);
|
||||
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
setFocusPolicy(Qt::StrongFocus);
|
||||
setAccessibleName(tr("Banner preview"));
|
||||
setAccessibleDescription(tr("Zoom %1×").arg(QString::number(params.zoom, 'f', 2)));
|
||||
}
|
||||
|
||||
void CardArtPreviewWidget::focusInEvent(QFocusEvent *event)
|
||||
{
|
||||
// Snapshot for the Esc or Backspace reset, restoring whatever the user
|
||||
// had when the surface took focus
|
||||
paramsAtFocusIn = params;
|
||||
QWidget::focusInEvent(event);
|
||||
}
|
||||
|
||||
void CardArtPreviewWidget::setPixmap(const QPixmap &pixmap)
|
||||
|
|
@ -38,6 +66,13 @@ void CardArtPreviewWidget::setPixmap(const QPixmap &pixmap)
|
|||
void CardArtPreviewWidget::setParams(const CardArtParams &p)
|
||||
{
|
||||
params = p;
|
||||
setAccessibleDescription(tr("Zoom %1×").arg(QString::number(params.zoom, 'f', 2)));
|
||||
update();
|
||||
}
|
||||
|
||||
void CardArtPreviewWidget::setAttribution(const QString &attribution)
|
||||
{
|
||||
attributionText = attribution;
|
||||
update();
|
||||
}
|
||||
|
||||
|
|
@ -60,9 +95,22 @@ void CardArtPreviewWidget::paintEvent(QPaintEvent *)
|
|||
painter.setBrush(accentColor);
|
||||
painter.drawRoundedRect(QRectF(cardRect.left(), cardRect.top(), 3, cardRect.height()), 2, 2);
|
||||
|
||||
// Visible keyboard focus per the focus cursor contract, Tab must show
|
||||
// where the keys land, including in the empty state
|
||||
const auto paintFocusRing = [&painter, &cardRect, this]() {
|
||||
if (!hasFocus()) {
|
||||
return;
|
||||
}
|
||||
QPen focusPen(palette().color(QPalette::Highlight), 2);
|
||||
painter.setPen(focusPen);
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.drawRoundedRect(cardRect.adjusted(-1, -1, 1, 1), 6, 6);
|
||||
};
|
||||
|
||||
if (sourcePixmap.isNull()) {
|
||||
painter.setPen(QColor(150, 150, 150));
|
||||
painter.drawText(rect, Qt::AlignCenter, tr("No card selected"));
|
||||
paintFocusRing();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -73,7 +121,7 @@ void CardArtPreviewWidget::paintEvent(QPaintEvent *)
|
|||
&sourcePixmap // direct pixmap
|
||||
);
|
||||
|
||||
// Avatar placeholder so the left-margin interaction is visible
|
||||
// Avatar placeholder so the left margin interaction is visible
|
||||
const int avatarX = rect.left() + 14;
|
||||
const int avatarY = rect.top() + (rect.height() - 36) / 2;
|
||||
const QRect avatarRect(avatarX, avatarY, 36, 36);
|
||||
|
|
@ -90,24 +138,202 @@ void CardArtPreviewWidget::paintEvent(QPaintEvent *)
|
|||
painter.setPen(QPen(QColor(70, 80, 95), 2));
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.drawEllipse(avatarRect.adjusted(-1, -1, 1, 1));
|
||||
|
||||
paintArtAttribution(painter, cardRect, attributionText);
|
||||
|
||||
paintFocusRing();
|
||||
}
|
||||
|
||||
qreal CardArtPreviewWidget::bannerTravel() const
|
||||
{
|
||||
if (sourcePixmap.isNull()) {
|
||||
return 0.0;
|
||||
}
|
||||
// Mirror UserListPainter::drawCardArt() exactly: same strip metrics, the
|
||||
// copy is drawn 1:1, so output pixels equal widget pixels here.
|
||||
const int cardH = rect().height() - 4;
|
||||
const int totalW = (rect().right() - 4) - rect().left();
|
||||
const int marginL = qRound(totalW * params.marginPctL);
|
||||
const int marginR = qRound(totalW * params.marginPctR);
|
||||
const int drawW = totalW - marginL - marginR;
|
||||
const double basescale = qMax(double(drawW) / sourcePixmap.width(), double(cardH) / sourcePixmap.height());
|
||||
// qRound for literal parity with drawCardArt, which rounds the scaled
|
||||
// height before computing travel
|
||||
const double scaledH = qRound(sourcePixmap.height() * basescale * params.zoom);
|
||||
return scaledH - cardH;
|
||||
}
|
||||
|
||||
void CardArtPreviewWidget::applyCropDelta(qreal dOffset, qreal zoomFactor)
|
||||
{
|
||||
CardArtParams next = params;
|
||||
next.verticalOffset = qBound(0.0, params.verticalOffset + dOffset, 1.0);
|
||||
next.zoom = qBound(kMinGestureZoom, params.zoom * zoomFactor, kMaxZoom);
|
||||
|
||||
if (sameCrop(next, params)) {
|
||||
return;
|
||||
}
|
||||
|
||||
params = next;
|
||||
setAccessibleDescription(tr("Zoom %1×").arg(QString::number(params.zoom, 'f', 2)));
|
||||
update();
|
||||
emit paramsEdited(params);
|
||||
}
|
||||
|
||||
bool CardArtPreviewWidget::sameCrop(const CardArtParams &a, const CardArtParams &b) const
|
||||
{
|
||||
// Exact comparison on purpose: clamped assignments yield identical bits,
|
||||
// while qFuzzyCompare based equality misbehaves around zero
|
||||
return a.verticalOffset == b.verticalOffset && a.zoom == b.zoom;
|
||||
}
|
||||
|
||||
void CardArtPreviewWidget::restoreSnapshot()
|
||||
{
|
||||
// The snapshot only ever holds values that passed the gesture clamps,
|
||||
// so it is safe to restore verbatim
|
||||
if (sameCrop(paramsAtFocusIn, params)) {
|
||||
return;
|
||||
}
|
||||
params = paramsAtFocusIn;
|
||||
setAccessibleDescription(tr("Zoom %1×").arg(QString::number(params.zoom, 'f', 2)));
|
||||
update();
|
||||
emit paramsEdited(params);
|
||||
}
|
||||
|
||||
void CardArtPreviewWidget::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
if (event->button() != Qt::LeftButton || sourcePixmap.isNull() || bannerTravel() <= 0.5) {
|
||||
QWidget::mousePressEvent(event);
|
||||
return;
|
||||
}
|
||||
dragging = true;
|
||||
lastDragPos = event->pos();
|
||||
setCursor(Qt::ClosedHandCursor);
|
||||
event->accept();
|
||||
}
|
||||
|
||||
void CardArtPreviewWidget::mouseMoveEvent(QMouseEvent *event)
|
||||
{
|
||||
if (!dragging || sourcePixmap.isNull()) {
|
||||
QWidget::mouseMoveEvent(event);
|
||||
return;
|
||||
}
|
||||
|
||||
const qreal dy = event->pos().y() - lastDragPos.y();
|
||||
lastDragPos = event->pos();
|
||||
|
||||
const qreal travel = bannerTravel();
|
||||
if (travel <= 0.5) {
|
||||
event->accept();
|
||||
return;
|
||||
}
|
||||
|
||||
// Dragging moves the ART with the cursor, so the crop window slides the
|
||||
// other way through the available travel.
|
||||
applyCropDelta(-dy / travel, 1.0);
|
||||
event->accept();
|
||||
}
|
||||
|
||||
void CardArtPreviewWidget::mouseReleaseEvent(QMouseEvent *event)
|
||||
{
|
||||
if (event->button() == Qt::LeftButton) {
|
||||
dragging = false;
|
||||
unsetCursor();
|
||||
event->accept();
|
||||
return;
|
||||
}
|
||||
QWidget::mouseReleaseEvent(event);
|
||||
}
|
||||
|
||||
void CardArtPreviewWidget::wheelEvent(QWheelEvent *event)
|
||||
{
|
||||
if (sourcePixmap.isNull()) {
|
||||
QWidget::wheelEvent(event);
|
||||
return;
|
||||
}
|
||||
const qreal notches = static_cast<qreal>(event->angleDelta().y()) / 120.0;
|
||||
if (notches == 0.0) {
|
||||
event->accept();
|
||||
return;
|
||||
}
|
||||
applyCropDelta(0.0, std::pow(kWheelZoomBase, notches));
|
||||
event->accept();
|
||||
}
|
||||
|
||||
void CardArtPreviewWidget::keyPressEvent(QKeyEvent *event)
|
||||
{
|
||||
if (sourcePixmap.isNull()) {
|
||||
QWidget::keyPressEvent(event);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event->key()) {
|
||||
case Qt::Key_Escape:
|
||||
if (sameCrop(params, paramsAtFocusIn)) {
|
||||
// Nothing to undo on this surface, let the event reach the
|
||||
// dialog so Esc keeps its close meaning there
|
||||
QWidget::keyPressEvent(event);
|
||||
return;
|
||||
}
|
||||
restoreSnapshot();
|
||||
break;
|
||||
case Qt::Key_Backspace:
|
||||
restoreSnapshot();
|
||||
break;
|
||||
case Qt::Key_Up:
|
||||
applyCropDelta(-kKeyPanOffsetStep, 1.0);
|
||||
break;
|
||||
case Qt::Key_Down:
|
||||
applyCropDelta(kKeyPanOffsetStep, 1.0);
|
||||
break;
|
||||
case Qt::Key_Plus:
|
||||
case Qt::Key_Equal:
|
||||
applyCropDelta(0.0, kKeyZoomStep);
|
||||
break;
|
||||
case Qt::Key_Minus:
|
||||
applyCropDelta(0.0, 1.0 / kKeyZoomStep);
|
||||
break;
|
||||
default:
|
||||
QWidget::keyPressEvent(event);
|
||||
return;
|
||||
}
|
||||
event->accept();
|
||||
}
|
||||
|
||||
UserCardArtSettingsDialog::UserCardArtSettingsDialog(const CardArtParams &initial, QWidget *parent)
|
||||
: QDialog(parent), currentParams(initial)
|
||||
{
|
||||
setWindowTitle(tr("Card Art Settings"));
|
||||
// Legacy stored banners may carry zoom below the gesture floor or an out
|
||||
// of range offset. Normalize once on open so the preview renders filled
|
||||
// and Ok saves a state the gestures can reach again
|
||||
currentParams.zoom = qBound(kMinGestureZoom, currentParams.zoom, kMaxZoom);
|
||||
currentParams.verticalOffset = qBound(0.0, currentParams.verticalOffset, 1.0);
|
||||
|
||||
setMinimumWidth(500);
|
||||
setupUi();
|
||||
|
||||
// Seed UI from initial params
|
||||
if (!initial.cardName.isEmpty()) {
|
||||
searchBar->setText(initial.cardName);
|
||||
onCardNameChanged(initial.cardName);
|
||||
if (!currentParams.cardName.isEmpty()) {
|
||||
// onCardNameChanged overwrites cardProviderId with the first printing,
|
||||
// so remember the stored one before it runs
|
||||
const QString storedProviderId = currentParams.cardProviderId;
|
||||
searchBar->setText(currentParams.cardName);
|
||||
onCardNameChanged(currentParams.cardName);
|
||||
|
||||
// onCardNameChanged leaves the printing combo on the first printing in
|
||||
// the database, which would silently change the stored banner card on
|
||||
// accept. Restore the stored printing when it resolves locally.
|
||||
const int storedPrintingIndex = providerComboBox->findData(storedProviderId);
|
||||
if (storedPrintingIndex != -1) {
|
||||
providerComboBox->setCurrentIndex(storedPrintingIndex);
|
||||
} else if (!storedProviderId.isEmpty()) {
|
||||
// Stored printing not in the local database: keep it rather than
|
||||
// silently substituting the first printing.
|
||||
currentParams.cardProviderId = storedProviderId;
|
||||
reloadPreview();
|
||||
}
|
||||
}
|
||||
marginLSpin->setValue(initial.marginPctL);
|
||||
marginRSpin->setValue(initial.marginPctR);
|
||||
verticalOffsetSpin->setValue(initial.verticalOffset);
|
||||
zoomSpin->setValue(initial.zoom);
|
||||
marginLSpin->setValue(currentParams.marginPctL);
|
||||
marginRSpin->setValue(currentParams.marginPctR);
|
||||
}
|
||||
|
||||
CardArtParams UserCardArtSettingsDialog::params() const
|
||||
|
|
@ -128,34 +354,18 @@ QDoubleSpinBox *UserCardArtSettingsDialog::makeSpinBox(double min, double max, d
|
|||
void UserCardArtSettingsDialog::initializeSearchBar()
|
||||
{
|
||||
searchBar = new QLineEdit;
|
||||
searchBar->setPlaceholderText(tr("Type a card name..."));
|
||||
|
||||
cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this);
|
||||
cardDatabaseDisplayModel = new CardDatabaseDisplayModel(this);
|
||||
cardDatabaseDisplayModel->setSourceModel(cardDatabaseModel);
|
||||
searchModel = new CardSearchModel(cardDatabaseDisplayModel, this);
|
||||
|
||||
proxyModel = new CardCompleterProxyModel(this);
|
||||
proxyModel->setSourceModel(searchModel);
|
||||
proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
|
||||
proxyModel->setFilterRole(Qt::DisplayRole);
|
||||
|
||||
completer = new QCompleter(proxyModel, this);
|
||||
completer->setCompletionRole(Qt::DisplayRole);
|
||||
completer->setCompletionMode(QCompleter::PopupCompletion);
|
||||
completer->setCaseSensitivity(Qt::CaseInsensitive);
|
||||
completer->setFilterMode(Qt::MatchContains);
|
||||
completer->setMaxVisibleItems(15);
|
||||
const CardCompleterSetup cardSetup = createCardCompleter(cardDatabaseDisplayModel, this, 15);
|
||||
searchModel = cardSetup.searchModel;
|
||||
proxyModel = cardSetup.proxyModel;
|
||||
completer = cardSetup.completer;
|
||||
searchBar->setCompleter(completer);
|
||||
|
||||
connect(searchBar, &QLineEdit::textEdited, searchModel, &CardSearchModel::updateSearchResults);
|
||||
connect(searchBar, &QLineEdit::textEdited, this, [this](const QString &text) {
|
||||
const QString pattern = ".*" + QRegularExpression::escape(text) + ".*";
|
||||
proxyModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption));
|
||||
if (!text.isEmpty()) {
|
||||
completer->complete();
|
||||
}
|
||||
});
|
||||
connectCardCompleterSearch(searchBar, cardSetup);
|
||||
|
||||
connect(completer, static_cast<void (QCompleter::*)(const QString &)>(&QCompleter::activated), this,
|
||||
[this](const QString &completion) {
|
||||
|
|
@ -183,29 +393,33 @@ void UserCardArtSettingsDialog::setupUi()
|
|||
|
||||
marginLSpin = makeSpinBox(0.0, 0.95, currentParams.marginPctL, 0.01);
|
||||
marginRSpin = makeSpinBox(0.0, 0.95, currentParams.marginPctR, 0.01);
|
||||
verticalOffsetSpin = makeSpinBox(0.0, 1.0, currentParams.verticalOffset, 0.01);
|
||||
zoomSpin = makeSpinBox(0.1, 4.0, currentParams.zoom, 0.05);
|
||||
|
||||
auto *form = new QFormLayout;
|
||||
form->addRow(tr("Card name:"), searchBar);
|
||||
form->addRow(tr("Card ProviderId:"), providerComboBox);
|
||||
form->addRow(tr("Left margin (%):"), marginLSpin);
|
||||
form->addRow(tr("Right margin (%):"), marginRSpin);
|
||||
form->addRow(tr("Vertical offset:"), verticalOffsetSpin);
|
||||
form->addRow(tr("Zoom:"), zoomSpin);
|
||||
cardNameLabel = new QLabel;
|
||||
printingLabel = new QLabel;
|
||||
marginLLabel = new QLabel;
|
||||
marginRLabel = new QLabel;
|
||||
form->addRow(cardNameLabel, searchBar);
|
||||
form->addRow(printingLabel, providerComboBox);
|
||||
form->addRow(marginLLabel, marginLSpin);
|
||||
form->addRow(marginRLabel, marginRSpin);
|
||||
|
||||
auto *controlsGroup = new QGroupBox(tr("Parameters"));
|
||||
controlsGroup = new QGroupBox;
|
||||
controlsGroup->setLayout(form);
|
||||
|
||||
preview = new CardArtPreviewWidget;
|
||||
|
||||
auto *previewLayout = new QVBoxLayout;
|
||||
previewLayout->addWidget(preview);
|
||||
auto *previewGroup = new QGroupBox(tr("Preview"));
|
||||
previewCaptionLabel = new QLabel;
|
||||
previewCaptionLabel->setAlignment(Qt::AlignCenter);
|
||||
previewCaptionLabel->setWordWrap(true);
|
||||
previewLayout->addWidget(previewCaptionLabel);
|
||||
previewGroup = new QGroupBox;
|
||||
previewGroup->setLayout(previewLayout);
|
||||
|
||||
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
auto *removeBtn = new QPushButton(tr("Remove Banner Card"));
|
||||
removeBtn = new QPushButton;
|
||||
buttons->addButton(removeBtn, QDialogButtonBox::ResetRole);
|
||||
|
||||
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
|
|
@ -215,16 +429,38 @@ void UserCardArtSettingsDialog::setupUi()
|
|||
accept();
|
||||
});
|
||||
|
||||
// The banner leads visually, card selection and margins support it below.
|
||||
auto *root = new QVBoxLayout;
|
||||
root->addWidget(controlsGroup);
|
||||
root->addWidget(previewGroup);
|
||||
root->addWidget(controlsGroup);
|
||||
root->addWidget(buttons);
|
||||
setLayout(root);
|
||||
|
||||
connect(marginLSpin, &QDoubleSpinBox::valueChanged, this, &UserCardArtSettingsDialog::onParamChanged);
|
||||
connect(marginRSpin, &QDoubleSpinBox::valueChanged, this, &UserCardArtSettingsDialog::onParamChanged);
|
||||
connect(verticalOffsetSpin, &QDoubleSpinBox::valueChanged, this, &UserCardArtSettingsDialog::onParamChanged);
|
||||
connect(zoomSpin, &QDoubleSpinBox::valueChanged, this, &UserCardArtSettingsDialog::onParamChanged);
|
||||
|
||||
// Gestures are the only editors of offset and zoom on this surface.
|
||||
// Margins stay explicit numeric controls: they trim the strip's
|
||||
// sides and have no natural drag mapping.
|
||||
connect(preview, &CardArtPreviewWidget::paramsEdited, this,
|
||||
[this](const CardArtParams &edited) { currentParams = edited; });
|
||||
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
void UserCardArtSettingsDialog::retranslateUi()
|
||||
{
|
||||
setWindowTitle(tr("Card Art Settings"));
|
||||
searchBar->setPlaceholderText(tr("Type a card name..."));
|
||||
cardNameLabel->setText(tr("Card name:"));
|
||||
printingLabel->setText(tr("Printing:"));
|
||||
marginLLabel->setText(tr("Left margin (%):"));
|
||||
marginRLabel->setText(tr("Right margin (%):"));
|
||||
controlsGroup->setTitle(tr("Card"));
|
||||
previewCaptionLabel->setText(
|
||||
tr("Drag to pan, scroll to zoom, arrow keys nudge, plus and minus zoom, Backspace or Esc restores."));
|
||||
previewGroup->setTitle(tr("Banner"));
|
||||
removeBtn->setText(tr("Remove Banner Card"));
|
||||
}
|
||||
|
||||
void UserCardArtSettingsDialog::populateProviderCombo(const QString &cardName)
|
||||
|
|
@ -274,7 +510,7 @@ void UserCardArtSettingsDialog::onCardNameChanged(const QString &name)
|
|||
populateProviderCombo(name);
|
||||
|
||||
if (providerComboBox->count() == 0) {
|
||||
// No printings found for this card; nothing to preview.
|
||||
// No printings found for this card, nothing to preview.
|
||||
currentPixmap = QPixmap();
|
||||
preview->setPixmap(currentPixmap);
|
||||
currentParams.cardProviderId.clear();
|
||||
|
|
@ -304,7 +540,7 @@ void UserCardArtSettingsDialog::reloadPreview()
|
|||
// whichever CardInfo we just asked for, so the preview catches up once
|
||||
// the image actually arrives instead of staying on the placeholder.
|
||||
//
|
||||
// Disconnect any previous listener first -- otherwise switching cards
|
||||
// Disconnect any previous listener first, otherwise switching cards
|
||||
// repeatedly stacks up connections to old CardInfo objects, each of
|
||||
// which would still fire reloadPreview() (harmlessly, but wastefully)
|
||||
// whenever ITS art finishes loading later.
|
||||
|
|
@ -314,8 +550,8 @@ void UserCardArtSettingsDialog::reloadPreview()
|
|||
CardPictureLoader::getPixmap(fullRes, card, QSize(745, 1040));
|
||||
|
||||
if (fullRes.isNull()) {
|
||||
// Not loaded yet -- wait for the signal instead of giving up.
|
||||
// card.getCardPtr() is a CardInfoPtr (QSharedPointer<CardInfo>);
|
||||
// Not loaded yet, wait for the signal instead of giving up.
|
||||
// card.getCardPtr() is a CardInfoPtr (QSharedPointer<CardInfo>),
|
||||
// .data() gives the raw QObject* needed for connect().
|
||||
CardInfo *cardInfo = card.getCardPtr().data();
|
||||
if (cardInfo) {
|
||||
|
|
@ -327,13 +563,16 @@ void UserCardArtSettingsDialog::reloadPreview()
|
|||
currentPixmap = UserCardArtProvider::cropCardArt(fullRes);
|
||||
preview->setPixmap(currentPixmap);
|
||||
preview->setParams(currentParams);
|
||||
|
||||
// Only attribute the art once the new pixmap is actually displayed, so a
|
||||
// cache miss (which keeps the previous pixmap on screen) doesn't pair the
|
||||
// new card's attribution with the old card's art.
|
||||
preview->setAttribution(buildArtAttribution(card));
|
||||
}
|
||||
|
||||
void UserCardArtSettingsDialog::onParamChanged()
|
||||
{
|
||||
currentParams.marginPctL = marginLSpin->value();
|
||||
currentParams.marginPctR = marginRSpin->value();
|
||||
currentParams.verticalOffset = verticalOffsetSpin->value();
|
||||
currentParams.zoom = zoomSpin->value();
|
||||
preview->setParams(currentParams);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,13 +8,29 @@
|
|||
#include <QPixmap>
|
||||
|
||||
class QCompleter;
|
||||
class QFocusEvent;
|
||||
class QGroupBox;
|
||||
class QKeyEvent;
|
||||
class QMouseEvent;
|
||||
class QLineEdit;
|
||||
class QDoubleSpinBox;
|
||||
class QLabel;
|
||||
class QPushButton;
|
||||
class QWheelEvent;
|
||||
class CardDatabaseModel;
|
||||
class CardDatabaseDisplayModel;
|
||||
class CardSearchModel;
|
||||
class CardCompleterProxyModel;
|
||||
|
||||
/**
|
||||
* @brief Interactive preview of the user list banner art.
|
||||
*
|
||||
* Renders the banner strip with the given CardArtParams through the same
|
||||
* UserListPainter::drawCardArt() the live delegate uses, including the
|
||||
* avatar placeholder and fade masks. Dragging pans the art vertically at
|
||||
* output scale, the wheel zooms, arrow keys nudge, Backspace or Esc
|
||||
* restores the parameters as of focus gain.
|
||||
*/
|
||||
class CardArtPreviewWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
|
@ -24,13 +40,33 @@ public:
|
|||
|
||||
void setPixmap(const QPixmap &pixmap);
|
||||
void setParams(const CardArtParams ¶ms);
|
||||
void setAttribution(const QString &attribution);
|
||||
|
||||
signals:
|
||||
/** @brief Emitted whenever direct manipulation (drag, wheel, keys) changes the parameters. */
|
||||
void paramsEdited(const CardArtParams ¶ms);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
void mouseMoveEvent(QMouseEvent *event) override;
|
||||
void mouseReleaseEvent(QMouseEvent *event) override;
|
||||
void wheelEvent(QWheelEvent *event) override;
|
||||
void keyPressEvent(QKeyEvent *event) override;
|
||||
void focusInEvent(QFocusEvent *event) override;
|
||||
|
||||
private:
|
||||
qreal bannerTravel() const; ///< vertical travel of the art behind the strip, in output pixels
|
||||
void applyCropDelta(qreal dOffset, qreal zoomFactor);
|
||||
bool sameCrop(const CardArtParams &a, const CardArtParams &b) const;
|
||||
void restoreSnapshot();
|
||||
|
||||
QPixmap sourcePixmap;
|
||||
CardArtParams params;
|
||||
CardArtParams paramsAtFocusIn; ///< crop as of the latest focus gain, restored by Esc or Backspace
|
||||
QString attributionText;
|
||||
QPoint lastDragPos; ///< widget space position of the previous mouse move while panning
|
||||
bool dragging{false}; ///< true between an accepted press and its release, guards stale drag positions
|
||||
};
|
||||
|
||||
class UserCardArtSettingsDialog : public QDialog
|
||||
|
|
@ -49,6 +85,7 @@ private slots:
|
|||
|
||||
private:
|
||||
void setupUi();
|
||||
void retranslateUi();
|
||||
void populateProviderCombo(const QString &cardName);
|
||||
void initializeSearchBar();
|
||||
QDoubleSpinBox *makeSpinBox(double min, double max, double value, double step);
|
||||
|
|
@ -64,14 +101,20 @@ private:
|
|||
|
||||
QMetaObject::Connection pixmapUpdatedConnection;
|
||||
|
||||
QLabel *cardNameLabel;
|
||||
QLabel *printingLabel;
|
||||
QLabel *marginLLabel;
|
||||
QLabel *marginRLabel;
|
||||
QGroupBox *controlsGroup;
|
||||
QLabel *previewCaptionLabel;
|
||||
QGroupBox *previewGroup;
|
||||
QPushButton *removeBtn;
|
||||
QDoubleSpinBox *marginLSpin;
|
||||
QDoubleSpinBox *marginRSpin;
|
||||
QDoubleSpinBox *verticalOffsetSpin;
|
||||
QDoubleSpinBox *zoomSpin;
|
||||
CardArtPreviewWidget *preview;
|
||||
|
||||
QPixmap currentPixmap;
|
||||
CardArtParams currentParams;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_USER_CARD_ART_SETTINGS_DIALOG_H
|
||||
#endif // COCKATRICE_USER_CARD_ART_SETTINGS_DIALOG_H
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "user_context_menu.h"
|
||||
|
||||
#include "../../dialogs/dlg_report_user.h"
|
||||
#include "../../interface/widgets/tabs/tab_account.h"
|
||||
#include "../../interface/widgets/tabs/tab_game.h"
|
||||
#include "../../interface/widgets/tabs/tab_supervisor.h"
|
||||
|
|
@ -41,6 +42,7 @@ UserContextMenu::UserContextMenu(TabSupervisor *_tabSupervisor, QWidget *parent,
|
|||
aAddToIgnoreList = new QAction(QString(), this);
|
||||
aRemoveFromIgnoreList = new QAction(QString(), this);
|
||||
aKick = new QAction(QString(), this);
|
||||
aReport = new QAction(QString(), this);
|
||||
aWarnUser = new QAction(QString(), this);
|
||||
aWarnHistory = new QAction(QString(), this);
|
||||
aBan = new QAction(QString(), this);
|
||||
|
|
@ -50,6 +52,7 @@ UserContextMenu::UserContextMenu(TabSupervisor *_tabSupervisor, QWidget *parent,
|
|||
aPromoteToJudge = new QAction(QString(), this);
|
||||
aDemoteFromJudge = new QAction(QString(), this);
|
||||
aGetAdminNotes = new QAction(QString(), this);
|
||||
aInvestigateUser = new QAction(QString(), this);
|
||||
|
||||
retranslateUi();
|
||||
}
|
||||
|
|
@ -64,6 +67,7 @@ void UserContextMenu::retranslateUi()
|
|||
aAddToIgnoreList->setText(tr("Add to &ignore list"));
|
||||
aRemoveFromIgnoreList->setText(tr("Remove from &ignore list"));
|
||||
aKick->setText(tr("Kick from &game"));
|
||||
aReport->setText(tr("Report user"));
|
||||
aWarnUser->setText(tr("Warn user"));
|
||||
aWarnHistory->setText(tr("View user's war&n history"));
|
||||
aBan->setText(tr("Ban from &server"));
|
||||
|
|
@ -73,6 +77,7 @@ void UserContextMenu::retranslateUi()
|
|||
aPromoteToJudge->setText(tr("Promote user to &judge"));
|
||||
aDemoteFromJudge->setText(tr("Demote user from judge"));
|
||||
aGetAdminNotes->setText(tr("View admin notes"));
|
||||
aInvestigateUser->setText(tr("Investigate user"));
|
||||
}
|
||||
|
||||
void UserContextMenu::gamesOfUserReceived(const Response &resp, const CommandContainer &commandContainer)
|
||||
|
|
@ -144,7 +149,8 @@ void UserContextMenu::warnUser_processGetWarningsListResponse(const Response &r)
|
|||
|
||||
if (response.warning_size() > 0) {
|
||||
for (int i = 0; i < response.warning_size(); ++i) {
|
||||
dlg->addWarningOption(QString::fromStdString(response.warning(i)).simplified());
|
||||
int startingIl = i < response.warning_il_size() ? response.warning_il(i) : 1;
|
||||
dlg->addWarningOption(QString::fromStdString(response.warning(i)).simplified(), startingIl);
|
||||
}
|
||||
}
|
||||
dlg->show();
|
||||
|
|
@ -355,6 +361,7 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
|
|||
{
|
||||
QAction *aCopyToClipBoard = nullptr, *aRemoveMessages = nullptr;
|
||||
aUserName->setText(userName);
|
||||
const bool anotherUser = userName != userListProxy->getOwnUsername();
|
||||
|
||||
auto *menu = new QMenu(static_cast<QWidget *>(parent()));
|
||||
menu->addAction(aUserName);
|
||||
|
|
@ -366,6 +373,17 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
|
|||
menu->addAction(aDetails);
|
||||
menu->addAction(aShowGames);
|
||||
menu->addAction(aChat);
|
||||
const QList<GameInviteOption> inviteOptions = inviteOptionsForUser(userName);
|
||||
if (!inviteOptions.isEmpty()) {
|
||||
auto *inviteMenu = new QMenu(tr("&Invite to Game"), menu);
|
||||
for (const GameInviteOption &option : inviteOptions) {
|
||||
QAction *inviteAction = inviteMenu->addAction(option.label);
|
||||
inviteAction->setEnabled(anotherUser && online);
|
||||
connect(inviteAction, &QAction::triggered, this,
|
||||
[this, userName, option] { execInvite(userName, option); });
|
||||
}
|
||||
menu->addMenu(inviteMenu);
|
||||
}
|
||||
if (userLevel.testFlag(ServerInfo_User::IsRegistered) && userListProxy->isOwnUserRegistered()) {
|
||||
menu->addSeparator();
|
||||
if (userListProxy->isUserBuddy(userName)) {
|
||||
|
|
@ -383,6 +401,9 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
|
|||
aRemoveMessages = new QAction(tr("Remove this user's messages"), this);
|
||||
menu->addAction(aRemoveMessages);
|
||||
}
|
||||
if (userListProxy->isOwnUserRegistered()) {
|
||||
menu->addAction(aReport);
|
||||
}
|
||||
if (game && (game->isHost() || !tabSupervisor->getAdminLocked())) {
|
||||
menu->addSeparator();
|
||||
menu->addAction(aKick);
|
||||
|
|
@ -396,6 +417,7 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
|
|||
menu->addAction(aBanHistory);
|
||||
menu->addSeparator();
|
||||
menu->addAction(aGetAdminNotes);
|
||||
menu->addAction(aInvestigateUser);
|
||||
|
||||
menu->addSeparator();
|
||||
if (userLevel.testFlag(ServerInfo_User::IsModerator) &&
|
||||
|
|
@ -416,10 +438,10 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
|
|||
menu->addAction(aPromoteToJudge);
|
||||
}
|
||||
}
|
||||
bool anotherUser = userName != userListProxy->getOwnUsername();
|
||||
aDetails->setEnabled(true);
|
||||
aChat->setEnabled(anotherUser && online);
|
||||
aShowGames->setEnabled(online);
|
||||
aReport->setEnabled(anotherUser);
|
||||
aAddToBuddyList->setEnabled(anotherUser);
|
||||
aRemoveFromBuddyList->setEnabled(anotherUser);
|
||||
aAddToIgnoreList->setEnabled(anotherUser);
|
||||
|
|
@ -430,6 +452,7 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
|
|||
aBan->setEnabled(anotherUser);
|
||||
aBanHistory->setEnabled(anotherUser);
|
||||
aGetAdminNotes->setEnabled(anotherUser);
|
||||
aInvestigateUser->setEnabled(anotherUser);
|
||||
aPromoteToMod->setEnabled(anotherUser);
|
||||
aDemoteFromMod->setEnabled(anotherUser);
|
||||
|
||||
|
|
@ -451,6 +474,15 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
|
|||
execRemoveFromIgnore(userName);
|
||||
} else if (actionClicked == aKick) {
|
||||
execKick(playerId);
|
||||
} else if (actionClicked == aReport) {
|
||||
int gameId = game ? game->getGameMetaInfo()->gameId() : -1;
|
||||
QString autoChatLog;
|
||||
if (chatView) {
|
||||
autoChatLog = chatView->getRecentChatLog(50);
|
||||
}
|
||||
auto dlgReport = new DlgReportUser(client, userName, gameId, autoChatLog, static_cast<QWidget *>(parent()));
|
||||
dlgReport->setAttribute(Qt::WA_DeleteOnClose);
|
||||
dlgReport->exec();
|
||||
} else if (actionClicked == aBan) {
|
||||
execBan(userName);
|
||||
} else if (actionClicked == aPromoteToMod || actionClicked == aDemoteFromMod) {
|
||||
|
|
@ -465,6 +497,8 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
|
|||
execWarnHistory(userName);
|
||||
} else if (actionClicked == aGetAdminNotes) {
|
||||
execAdminNotes(userName);
|
||||
} else if (actionClicked == aInvestigateUser) {
|
||||
execInvestigateUser(userName);
|
||||
} else if (actionClicked == aCopyToClipBoard) {
|
||||
QClipboard *clipboard = QGuiApplication::clipboard();
|
||||
clipboard->setText(deckHash);
|
||||
|
|
@ -480,6 +514,60 @@ void UserContextMenu::execChat(const QString &userName)
|
|||
emit openMessageDialog(userName, true);
|
||||
}
|
||||
|
||||
QList<GameInviteOption> UserContextMenu::inviteOptionsForUser(const QString &userName) const
|
||||
{
|
||||
if (!gameInviteLinkProvider) {
|
||||
return {};
|
||||
}
|
||||
const QList<GameInviteOption> options = gameInviteLinkProvider();
|
||||
QList<GameInviteOption> result;
|
||||
for (const GameInviteOption &option : options) {
|
||||
// Buddy-only games accept invites only from their creator, and only to
|
||||
// users on the creator's buddy list.
|
||||
if (option.onlyBuddies &&
|
||||
(option.creatorName != userListProxy->getOwnUsername() || !userListProxy->isUserBuddy(userName))) {
|
||||
continue;
|
||||
}
|
||||
result.append(option);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void UserContextMenu::execInvite(const QString &userName)
|
||||
{
|
||||
const QList<GameInviteOption> options = inviteOptionsForUser(userName);
|
||||
if (options.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.size() == 1) {
|
||||
execInvite(userName, options.first());
|
||||
return;
|
||||
}
|
||||
|
||||
// More than one game in the room — let the user pick which one to invite to.
|
||||
auto *menu = new QMenu(static_cast<QWidget *>(parent()));
|
||||
for (const GameInviteOption &option : options) {
|
||||
QAction *action = menu->addAction(option.label);
|
||||
connect(action, &QAction::triggered, this, [this, userName, option] { execInvite(userName, option); });
|
||||
}
|
||||
menu->setAttribute(Qt::WA_DeleteOnClose);
|
||||
menu->popup(QCursor::pos());
|
||||
}
|
||||
|
||||
void UserContextMenu::execInvite(const QString &userName, const GameInviteOption &option)
|
||||
{
|
||||
// Name the game by description first, then its id — "Join my game 'Magic'
|
||||
// (#123)" — so a description-less fallback still identifies the game.
|
||||
// The multi-arg .arg() overloads replace in a single pass, so a description
|
||||
// containing "%…" cannot corrupt later placeholders.
|
||||
const QString prefix =
|
||||
option.description.isEmpty()
|
||||
? tr("Join my game (#%1):").arg(option.gameId)
|
||||
: tr("Join my game \"%1\" (#%2):").arg(option.description, QString::number(option.gameId));
|
||||
tabSupervisor->sendInviteToUser(userName, prefix + " " + option.url);
|
||||
}
|
||||
|
||||
void UserContextMenu::execDetails(const QString &userName)
|
||||
{
|
||||
auto *w = new UserInfoBox(client, false, static_cast<QWidget *>(parent()),
|
||||
|
|
@ -587,6 +675,11 @@ void UserContextMenu::execAdminNotes(const QString &userName)
|
|||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void UserContextMenu::execInvestigateUser(const QString &userName)
|
||||
{
|
||||
tabSupervisor->openTabModeration(userName);
|
||||
}
|
||||
|
||||
void UserContextMenu::execAdjustMod(const QString &userName, bool shouldBeMod)
|
||||
{
|
||||
Command_AdjustMod cmd;
|
||||
|
|
|
|||
|
|
@ -7,9 +7,12 @@
|
|||
#ifndef USER_CONTEXT_MENU_H
|
||||
#define USER_CONTEXT_MENU_H
|
||||
|
||||
#include <QObject>
|
||||
#include <libcockatrice/network/server/remote/user_level.h>
|
||||
#include "../../interface/widgets/server/game_link.h"
|
||||
|
||||
#include <QList>
|
||||
#include <QObject>
|
||||
#include <functional>
|
||||
#include <libcockatrice/network/server/remote/user_level.h>
|
||||
class AbstractGame;
|
||||
class UserListProxy;
|
||||
class AbstractClient;
|
||||
|
|
@ -38,11 +41,14 @@ private:
|
|||
QAction *aAddToBuddyList, *aRemoveFromBuddyList;
|
||||
QAction *aAddToIgnoreList, *aRemoveFromIgnoreList;
|
||||
QAction *aKick;
|
||||
QAction *aReport;
|
||||
QAction *aBan, *aBanHistory;
|
||||
QAction *aPromoteToMod, *aDemoteFromMod;
|
||||
QAction *aPromoteToJudge, *aDemoteFromJudge;
|
||||
QAction *aWarnUser, *aWarnHistory;
|
||||
QAction *aGetAdminNotes;
|
||||
std::function<QList<GameInviteOption>()> gameInviteLinkProvider;
|
||||
QAction *aInvestigateUser;
|
||||
signals:
|
||||
void openMessageDialog(const QString &userName, bool focus);
|
||||
private slots:
|
||||
|
|
@ -80,9 +86,28 @@ public:
|
|||
return userListProxy;
|
||||
}
|
||||
|
||||
void setGameInviteLinkProvider(std::function<QList<GameInviteOption>()> provider)
|
||||
{
|
||||
gameInviteLinkProvider = std::move(provider);
|
||||
}
|
||||
|
||||
/**
|
||||
* The games currently inviteable for @p userName, honoring the room's
|
||||
* buddy-only setting (the inviter must be the game's creator and the
|
||||
* target a buddy of theirs). Empty when there is no live provider.
|
||||
*/
|
||||
QList<GameInviteOption> inviteOptionsForUser(const QString &userName) const;
|
||||
|
||||
/** Whether at least one invite link is currently available for @p userName. */
|
||||
bool hasGameInviteLink(const QString &userName) const
|
||||
{
|
||||
return !inviteOptionsForUser(userName).isEmpty();
|
||||
}
|
||||
|
||||
// Individual action entry points — used by UserInfoPopup to trigger
|
||||
// actions without re-running the full context menu flow.
|
||||
void execChat(const QString &userName);
|
||||
void execInvite(const QString &userName);
|
||||
void execDetails(const QString &userName);
|
||||
void execShowGames(const QString &userName);
|
||||
void execAddToBuddy(const QString &userName);
|
||||
|
|
@ -95,8 +120,12 @@ public:
|
|||
void execBanHistory(const QString &userName);
|
||||
void execWarnHistory(const QString &userName);
|
||||
void execAdminNotes(const QString &userName);
|
||||
void execInvestigateUser(const QString &userName);
|
||||
void execAdjustMod(const QString &userName, bool shouldBeMod);
|
||||
void execAdjustJudge(const QString &userName, bool shouldBeJudge);
|
||||
|
||||
private:
|
||||
void execInvite(const QString &userName, const GameInviteOption &option);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -319,6 +319,7 @@ void UserInfoBox::actBannerCard()
|
|||
if (hasUserInfo && currentUserInfo.has_card_art_params()) {
|
||||
const auto &cap = currentUserInfo.card_art_params();
|
||||
initial.cardName = QString::fromStdString(cap.card_name());
|
||||
initial.cardProviderId = QString::fromStdString(cap.card_provider_id());
|
||||
initial.marginPctL = cap.margin_pct_l();
|
||||
initial.marginPctR = cap.margin_pct_r();
|
||||
initial.verticalOffset = cap.vertical_offset();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
#include "user_info_popup.h"
|
||||
|
||||
#include "../../cards/art_crop_attribution.h"
|
||||
#include "../../interface/pixel_map_generator.h"
|
||||
#include "../../interface/theme_manager.h"
|
||||
#include "../../interface/widgets/tabs/tab_supervisor.h"
|
||||
#include "user_list_painter.h"
|
||||
|
||||
|
|
@ -17,11 +19,48 @@
|
|||
#include <QStandardItem>
|
||||
#include <QStyledItemDelegate>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
||||
#include <libcockatrice/protocol/pb/commands.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_get_games_of_user.pb.h>
|
||||
#include <libcockatrice/protocol/pending_command.h>
|
||||
|
||||
/// Qt stylesheets accept #aarrggbb, which is QColor::name(QColor::HexArgb).
|
||||
static QString colorStr(const QColor &color)
|
||||
{
|
||||
return color.name(QColor::HexArgb);
|
||||
}
|
||||
|
||||
PopupTheme PopupTheme::fromPalette(const QPalette &palette, bool dark)
|
||||
{
|
||||
PopupTheme t;
|
||||
t.dark = dark;
|
||||
const QColor window = palette.color(QPalette::Window);
|
||||
const QColor base = palette.color(QPalette::Base);
|
||||
const QColor mid = palette.color(QPalette::Mid);
|
||||
const QColor text = palette.color(QPalette::Text);
|
||||
const QColor disabledText = palette.color(QPalette::Disabled, QPalette::Text);
|
||||
const QColor highlight = palette.color(QPalette::Highlight);
|
||||
|
||||
t.bg = window;
|
||||
t.border = mid;
|
||||
t.text = text;
|
||||
t.subText = disabledText;
|
||||
t.statusText = disabledText;
|
||||
t.buttonBg = base;
|
||||
t.buttonBorder = mid;
|
||||
t.buttonHover = UserListPainter::blend(base, highlight, dark ? 0.30 : 0.12);
|
||||
t.buttonPressed = UserListPainter::blend(base, highlight, dark ? 0.50 : 0.25);
|
||||
t.buttonDisabled = disabledText;
|
||||
t.closeBg = UserListPainter::blend(base, window, 0.5);
|
||||
t.closeHover = dark ? QColor(200, 50, 50) : UserListPainter::blend(QColor(200, 50, 50), base, 0.45);
|
||||
t.gamesRow = base;
|
||||
t.gamesSelected = UserListPainter::blend(base, highlight, dark ? 0.45 : 0.30);
|
||||
t.gamesSeparator = mid;
|
||||
t.gamesSeparator.setAlpha(90);
|
||||
return t;
|
||||
}
|
||||
|
||||
// ── Compact game row delegate ─────────────────────────────────────────────────
|
||||
|
||||
class PopupGameDelegate : public QStyledItemDelegate
|
||||
|
|
@ -48,8 +87,14 @@ public:
|
|||
const QRect rect = option.rect;
|
||||
const ServerInfo_Game game = var.value<ServerInfo_Game>();
|
||||
const bool selected = option.state & QStyle::State_Selected;
|
||||
const bool dark = themeManager && themeManager->isDarkModeActive();
|
||||
// The widget palette can be stale after a runtime theme change, so the
|
||||
// rows are styled from the application palette (always current).
|
||||
const QPalette pal = qApp->palette();
|
||||
const QColor base = pal.color(QPalette::Base);
|
||||
const QColor highlight = pal.color(QPalette::Highlight);
|
||||
|
||||
p->fillRect(rect, selected ? QColor(35, 45, 62) : QColor(14, 18, 26));
|
||||
p->fillRect(rect, selected ? UserListPainter::blend(base, highlight, dark ? 0.45 : 0.30) : base);
|
||||
|
||||
// State colour dot
|
||||
const QColor dot = game.started() ? QColor(239, 68, 68)
|
||||
|
|
@ -64,7 +109,7 @@ public:
|
|||
QFont tf = option.font;
|
||||
tf.setBold(true);
|
||||
p->setFont(tf);
|
||||
p->setPen(QColor(205, 215, 230));
|
||||
p->setPen(pal.color(QPalette::Text));
|
||||
const int textX = rect.left() + 26;
|
||||
const int countW = 52;
|
||||
const int titleW = rect.width() - textX - countW - 6;
|
||||
|
|
@ -74,13 +119,15 @@ public:
|
|||
// Player count
|
||||
const bool full = game.player_count() >= game.max_players();
|
||||
p->setFont(option.font);
|
||||
p->setPen(full ? QColor(249, 115, 22) : QColor(110, 128, 150));
|
||||
p->setPen(full ? QColor(249, 115, 22) : pal.color(QPalette::Disabled, QPalette::Text));
|
||||
p->drawText(QRect(rect.right() - countW - 4, rect.top(), countW, rect.height()),
|
||||
Qt::AlignVCenter | Qt::AlignRight,
|
||||
QStringLiteral("%1/%2").arg(game.player_count()).arg(game.max_players()));
|
||||
|
||||
// Row separator
|
||||
p->setPen(QColor(24, 32, 44));
|
||||
QColor separator = pal.color(QPalette::Mid);
|
||||
separator.setAlpha(90);
|
||||
p->setPen(separator);
|
||||
p->drawLine(rect.bottomLeft(), rect.bottomRight());
|
||||
|
||||
p->restore();
|
||||
|
|
@ -95,17 +142,27 @@ UserInfoHeaderWidget::UserInfoHeaderWidget(QWidget *parent) : QWidget(parent)
|
|||
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
}
|
||||
|
||||
void UserInfoHeaderWidget::setUserData(const ServerInfo_User &user,
|
||||
bool online,
|
||||
const QPixmap &avatar,
|
||||
const QPixmap &cardArt,
|
||||
const CardArtParams ¶ms)
|
||||
void UserInfoHeaderWidget::setUserData(const ServerInfo_User &_user,
|
||||
bool _online,
|
||||
const QPixmap &_avatar,
|
||||
const QPixmap &_cardArt,
|
||||
const CardArtParams &_params)
|
||||
{
|
||||
m_user = user;
|
||||
m_online = online;
|
||||
m_avatar = avatar;
|
||||
m_cardArt = cardArt;
|
||||
m_params = params;
|
||||
user = _user;
|
||||
online = _online;
|
||||
avatar = _avatar;
|
||||
cardArt = _cardArt;
|
||||
params = _params;
|
||||
|
||||
attribution.clear();
|
||||
if (user.has_card_art_params()) {
|
||||
const ExactCard card =
|
||||
CardDatabaseManager::query()->getCard({QString::fromStdString(user.card_art_params().card_name()),
|
||||
QString::fromStdString(user.card_art_params().card_provider_id())});
|
||||
if (card) {
|
||||
attribution = buildArtAttribution(card);
|
||||
}
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
|
|
@ -115,31 +172,48 @@ void UserInfoHeaderWidget::paintEvent(QPaintEvent *)
|
|||
p.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::TextAntialiasing);
|
||||
|
||||
const QRect rect = this->rect();
|
||||
const UserLevelFlags level(m_user.user_level());
|
||||
const QString userName = QString::fromStdString(m_user.name());
|
||||
const QString privLevel = QString::fromStdString(m_user.privlevel());
|
||||
const UserLevelFlags level(user.user_level());
|
||||
const QString userName = QString::fromStdString(user.name());
|
||||
const QString privLevel = QString::fromStdString(user.privlevel());
|
||||
|
||||
// Dark base
|
||||
p.fillRect(rect, QColor(14, 18, 26));
|
||||
const bool dark = themeManager && themeManager->isDarkModeActive();
|
||||
const UserListPainter::Style style = UserListPainter::resolveStyle(qApp->palette(), dark);
|
||||
|
||||
// Palette surface
|
||||
{
|
||||
QLinearGradient bg(0, 0, rect.width(), 0);
|
||||
bg.setColorAt(0, style.cardStart);
|
||||
bg.setColorAt(1, style.cardEnd);
|
||||
p.fillRect(rect, bg);
|
||||
}
|
||||
|
||||
// ── Card art background ───────────────────────────────────────────────────
|
||||
if (!m_cardArt.isNull()) {
|
||||
if (!cardArt.isNull()) {
|
||||
// Same DPR normalization as UserListPainter::drawCardArt: the cache
|
||||
// carries screen scaled pixmaps on HiDPI displays, the math below is
|
||||
// in raw pixels.
|
||||
QPixmap art = cardArt;
|
||||
art.setDevicePixelRatio(1.0);
|
||||
|
||||
const int w = rect.width();
|
||||
const int h = rect.height();
|
||||
const int mL = qRound(w * m_params.marginPctL);
|
||||
const int mR = qRound(w * m_params.marginPctR);
|
||||
const int mL = qRound(w * params.marginPctL);
|
||||
const int mR = qRound(w * params.marginPctR);
|
||||
const int dW = w - mL - mR;
|
||||
|
||||
const double base = qMax(double(dW) / m_cardArt.width(), double(h) / m_cardArt.height());
|
||||
const double scale = base * m_params.zoom;
|
||||
const int sW = qRound(m_cardArt.width() * scale);
|
||||
const int sH = qRound(m_cardArt.height() * scale);
|
||||
const double base = qMax(double(dW) / art.width(), double(h) / art.height());
|
||||
const double scale = base * params.zoom;
|
||||
const int sW = qRound(art.width() * scale);
|
||||
const int sH = qRound(art.height() * scale);
|
||||
|
||||
const QPixmap scaled = m_cardArt.scaled(sW, sH, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
||||
const int srcX = (sW - dW) / 2;
|
||||
const int srcY = qBound(0, qRound((sH - h) * m_params.verticalOffset), qMax(0, sH - h));
|
||||
const QPixmap scaled = art.scaled(sW, sH, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
||||
// Clamp against stored zoom < 1, which can push srcX negative and silently
|
||||
// underfill the strip with transparent padding
|
||||
const int safeSrcX = qBound(0, (sW - dW) / 2, qMax(0, sW - dW));
|
||||
const int safeSrcY = qBound(0, qRound((sH - h) * params.verticalOffset), qMax(0, sH - h));
|
||||
|
||||
QImage img = scaled.copy(srcX, srcY, dW, h).toImage().convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
||||
QImage img =
|
||||
scaled.copy(safeSrcX, safeSrcY, dW, h).toImage().convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
||||
{
|
||||
QPainter mask(&img);
|
||||
mask.setCompositionMode(QPainter::CompositionMode_DestinationIn);
|
||||
|
|
@ -155,12 +229,14 @@ void UserInfoHeaderWidget::paintEvent(QPaintEvent *)
|
|||
p.setOpacity(1.0);
|
||||
}
|
||||
|
||||
// Bottom gradient overlay so avatar and text are always legible
|
||||
// Bottom gradient overlay so avatar and text are always legible. The scrim
|
||||
// is the palette's Window color so it reads naturally in either scheme.
|
||||
{
|
||||
const QColor scrim = qApp->palette().color(QPalette::Window);
|
||||
QLinearGradient ov(0, 0, 0, rect.height());
|
||||
ov.setColorAt(0.0, QColor(14, 18, 26, 0));
|
||||
ov.setColorAt(0.55, QColor(14, 18, 26, 110));
|
||||
ov.setColorAt(1.0, QColor(14, 18, 26, 230));
|
||||
ov.setColorAt(0.0, QColor(scrim.red(), scrim.green(), scrim.blue(), 0));
|
||||
ov.setColorAt(0.55, QColor(scrim.red(), scrim.green(), scrim.blue(), 110));
|
||||
ov.setColorAt(1.0, QColor(scrim.red(), scrim.green(), scrim.blue(), 230));
|
||||
p.fillRect(rect, ov);
|
||||
}
|
||||
|
||||
|
|
@ -187,20 +263,20 @@ void UserInfoHeaderWidget::paintEvent(QPaintEvent *)
|
|||
p.save();
|
||||
p.setClipPath(clip);
|
||||
|
||||
if (!m_avatar.isNull()) {
|
||||
p.drawPixmap(ar, m_avatar.scaled(ar.size(), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation));
|
||||
if (!avatar.isNull()) {
|
||||
p.drawPixmap(ar, avatar.scaled(ar.size(), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation));
|
||||
} else {
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(accent.darker(200));
|
||||
p.setBrush(UserListPainter::blend(accent, style.base, dark ? 0.45 : 0.72));
|
||||
p.drawEllipse(ar);
|
||||
const QPixmap pawn =
|
||||
UserLevelPixmapGenerator::generatePixmap(AvatarPawnSize, level, m_user.pawn_colors(), false, privLevel);
|
||||
UserLevelPixmapGenerator::generatePixmap(AvatarPawnSize, level, user.pawn_colors(), false, privLevel);
|
||||
p.drawPixmap(ar.center().x() - AvatarPawnSize / 2, ar.center().y() - AvatarPawnSize / 2, pawn);
|
||||
}
|
||||
p.restore();
|
||||
|
||||
// Status ring
|
||||
p.setPen(QPen(m_online ? QColor(34, 197, 94) : QColor(70, 80, 95), 2.5));
|
||||
p.setPen(QPen(online ? QColor(34, 197, 94) : style.ringOffline, 2.5));
|
||||
p.setBrush(Qt::NoBrush);
|
||||
p.drawEllipse(QRectF(ar).adjusted(-1.25, -1.25, 1.25, 1.25));
|
||||
|
||||
|
|
@ -212,7 +288,7 @@ void UserInfoHeaderWidget::paintEvent(QPaintEvent *)
|
|||
nf.setBold(true);
|
||||
nf.setPointSizeF(nf.pointSizeF() * 1.12);
|
||||
p.setFont(nf);
|
||||
p.setPen(m_online ? QColor(220, 228, 240) : QColor(90, 100, 115));
|
||||
p.setPen(online ? style.textOnline : style.textOffline);
|
||||
p.drawText(QRect(tx, ay, tw, AvatarSize / 2 + 4), Qt::AlignBottom | Qt::AlignLeft,
|
||||
QFontMetrics(nf).elidedText(userName, Qt::ElideRight, tw));
|
||||
|
||||
|
|
@ -243,143 +319,184 @@ void UserInfoHeaderWidget::paintEvent(QPaintEvent *)
|
|||
const int bw = bfm.horizontalAdvance(badge.text) + 10;
|
||||
const QRect br(tx, ay + AvatarSize / 2 + 6, bw, 15);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(badge.color.darker(160));
|
||||
p.setBrush(UserListPainter::blend(badge.color, style.base, dark ? 0.55 : 0.78));
|
||||
p.drawRoundedRect(br, 3, 3);
|
||||
p.setPen(badge.color.lighter(150));
|
||||
p.setPen(dark ? UserListPainter::blend(badge.color, Qt::white, 0.5)
|
||||
: UserListPainter::blend(badge.color, Qt::black, 0.35));
|
||||
p.drawText(br, Qt::AlignCenter, badge.text);
|
||||
}
|
||||
|
||||
// The painter font at this point depends on whether a badge was drawn
|
||||
// (badge font vs username font), so pin an explicit font for the pill.
|
||||
p.setFont(font());
|
||||
|
||||
// Only show the attribution when there is actually art on screen: on a
|
||||
// cache miss cardArt is null and the pill would float over the plain
|
||||
// header with no art behind it.
|
||||
if (!cardArt.isNull()) {
|
||||
paintArtAttribution(p, rect, attribution, Qt::AlignRight | Qt::AlignBottom, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
// ── UserInfoPopup ─────────────────────────────────────────────────────────────
|
||||
|
||||
UserInfoPopup::UserInfoPopup(TabSupervisor *ts,
|
||||
AbstractClient *client,
|
||||
const QMap<QString, QPixmap> *avatarCache,
|
||||
const QMap<QString, QPixmap> *cardArtCache,
|
||||
const QMap<QString, CardArtParams> *cardArtParamsMap,
|
||||
UserInfoPopup::UserInfoPopup(TabSupervisor *_ts,
|
||||
AbstractClient *_client,
|
||||
const QMap<QString, QPixmap> *_avatarCache,
|
||||
const QMap<QString, QPixmap> *_cardArtCache,
|
||||
const QMap<QString, CardArtParams> *_cardArtParamsMap,
|
||||
QWidget *parent)
|
||||
: QFrame(parent, Qt::Tool | Qt::FramelessWindowHint), m_ts(ts), m_client(client), m_avatarCache(avatarCache),
|
||||
m_cardArtCache(cardArtCache), m_cardArtParamsMap(cardArtParamsMap)
|
||||
: QFrame(parent, Qt::Tool | Qt::FramelessWindowHint), ts(_ts), client(_client), avatarCache(_avatarCache),
|
||||
cardArtCache(_cardArtCache), cardArtParamsMap(_cardArtParamsMap)
|
||||
{
|
||||
setAttribute(Qt::WA_ShowWithoutActivating);
|
||||
setFixedWidth(PopupWidth);
|
||||
setFrameShape(QFrame::NoFrame);
|
||||
buildUi();
|
||||
|
||||
// Restyle the popup chrome when the theme or its color scheme changes.
|
||||
if (themeManager) {
|
||||
connect(themeManager, &ThemeManager::themeChanged, this, &UserInfoPopup::applyTheme);
|
||||
}
|
||||
}
|
||||
|
||||
void UserInfoPopup::buildUi()
|
||||
{
|
||||
setStyleSheet(QStringLiteral("UserInfoPopup {"
|
||||
" background:#0e1218;"
|
||||
" border:1px solid #1e2838;"
|
||||
" border-radius:8px;"
|
||||
"}"));
|
||||
|
||||
auto *root = new QVBoxLayout(this);
|
||||
root->setContentsMargins(0, 0, 0, 0);
|
||||
root->setSpacing(0);
|
||||
|
||||
// Header
|
||||
m_header = new UserInfoHeaderWidget(this);
|
||||
root->addWidget(m_header);
|
||||
header = new UserInfoHeaderWidget(this);
|
||||
root->addWidget(header);
|
||||
|
||||
// Action area — rebuilt per user
|
||||
m_actionArea = new QWidget(this);
|
||||
m_actionArea->setStyleSheet(QStringLiteral("background:#0e1218;"));
|
||||
root->addWidget(m_actionArea);
|
||||
// Action area, rebuilt per user
|
||||
actionArea = new QWidget(this);
|
||||
root->addWidget(actionArea);
|
||||
|
||||
// Thin separator
|
||||
auto *sep = new QFrame(this);
|
||||
sep->setFrameShape(QFrame::HLine);
|
||||
sep->setStyleSheet(QStringLiteral("color:#1a2434; margin: 0 8px;"));
|
||||
root->addWidget(sep);
|
||||
separator = new QFrame(this);
|
||||
separator->setFrameShape(QFrame::HLine);
|
||||
root->addWidget(separator);
|
||||
|
||||
// Games header row
|
||||
auto *gh = new QHBoxLayout;
|
||||
gh->setContentsMargins(10, 4, 8, 2);
|
||||
auto *gl = new QLabel(tr("Games"), this);
|
||||
gl->setStyleSheet(QStringLiteral("color:#6882a0; font-size:11px; font-weight:bold; background:transparent;"));
|
||||
gh->addWidget(gl);
|
||||
gamesLabel = new QLabel(tr("Games"), this);
|
||||
gh->addWidget(gamesLabel);
|
||||
gh->addStretch();
|
||||
m_refreshBtn = new QPushButton(QStringLiteral("↻"), this);
|
||||
m_refreshBtn->setFixedSize(20, 20);
|
||||
m_refreshBtn->setFlat(true);
|
||||
m_refreshBtn->setStyleSheet(
|
||||
QStringLiteral("QPushButton{color:#6882a0;border:none;font-size:14px;background:transparent;}"
|
||||
"QPushButton:hover{color:white;}"));
|
||||
connect(m_refreshBtn, &QPushButton::clicked, this, &UserInfoPopup::refreshGames);
|
||||
gh->addWidget(m_refreshBtn);
|
||||
refreshBtn = new QPushButton(QStringLiteral("↻"), this);
|
||||
refreshBtn->setFixedSize(20, 20);
|
||||
refreshBtn->setFlat(true);
|
||||
connect(refreshBtn, &QPushButton::clicked, this, &UserInfoPopup::refreshGames);
|
||||
gh->addWidget(refreshBtn);
|
||||
root->addLayout(gh);
|
||||
|
||||
// Status label
|
||||
m_gamesStatus = new QLabel(this);
|
||||
m_gamesStatus->setAlignment(Qt::AlignCenter);
|
||||
m_gamesStatus->setStyleSheet(
|
||||
QStringLiteral("color:#3a4a5e; font-size:11px; padding:10px; background:transparent;"));
|
||||
root->addWidget(m_gamesStatus);
|
||||
gamesStatus = new QLabel(this);
|
||||
gamesStatus->setAlignment(Qt::AlignCenter);
|
||||
root->addWidget(gamesStatus);
|
||||
|
||||
// Games list
|
||||
m_gamesModel = new QStandardItemModel(this);
|
||||
m_gamesView = new QListView(this);
|
||||
m_gamesView->setModel(m_gamesModel);
|
||||
m_gamesView->setItemDelegate(new PopupGameDelegate(m_gamesView));
|
||||
m_gamesView->setFrameShape(QFrame::NoFrame);
|
||||
m_gamesView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
m_gamesView->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
||||
m_gamesView->setMaximumHeight(220);
|
||||
m_gamesView->setStyleSheet(QStringLiteral("QListView{background:#0e1218;border:none;}"
|
||||
"QListView::item:selected{background:#232e42;}"));
|
||||
m_gamesView->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
connect(m_gamesView, &QListView::customContextMenuRequested, this, &UserInfoPopup::onGamesContextMenu);
|
||||
gamesModel = new QStandardItemModel(this);
|
||||
gamesView = new QListView(this);
|
||||
gamesView->setModel(gamesModel);
|
||||
gamesView->setItemDelegate(new PopupGameDelegate(gamesView));
|
||||
gamesView->setFrameShape(QFrame::NoFrame);
|
||||
gamesView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
gamesView->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
||||
gamesView->setMaximumHeight(220);
|
||||
gamesView->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
connect(gamesView, &QListView::customContextMenuRequested, this, &UserInfoPopup::onGamesContextMenu);
|
||||
|
||||
root->addWidget(m_gamesView);
|
||||
root->addWidget(gamesView);
|
||||
|
||||
// Close button — positioned absolutely in the top-right corner
|
||||
m_closeBtn = new QPushButton(QStringLiteral("✕"), this);
|
||||
m_closeBtn->setFixedSize(22, 22);
|
||||
m_closeBtn->setFlat(true);
|
||||
m_closeBtn->setStyleSheet(QStringLiteral("QPushButton{background:rgba(14,18,26,180);color:#607080;"
|
||||
"border:none;border-radius:11px;font-size:10px;}"
|
||||
"QPushButton:hover{color:white;background:rgba(200,50,50,200);}"));
|
||||
connect(m_closeBtn, &QPushButton::clicked, this, &UserInfoPopup::closeRequested);
|
||||
// Close button, positioned absolutely in the top right corner
|
||||
closeBtn = new QPushButton(QStringLiteral("✕"), this);
|
||||
closeBtn->setFixedSize(22, 22);
|
||||
closeBtn->setFlat(true);
|
||||
connect(closeBtn, &QPushButton::clicked, this, &UserInfoPopup::closeRequested);
|
||||
|
||||
applyTheme();
|
||||
}
|
||||
|
||||
void UserInfoPopup::applyTheme()
|
||||
{
|
||||
const bool dark = themeManager && themeManager->isDarkModeActive();
|
||||
theme = PopupTheme::fromPalette(qApp->palette(), dark);
|
||||
|
||||
setStyleSheet(QStringLiteral("UserInfoPopup {"
|
||||
" background:%1;"
|
||||
" border:1px solid %2;"
|
||||
" border-radius:8px;"
|
||||
"}")
|
||||
.arg(colorStr(theme.bg), colorStr(theme.border)));
|
||||
|
||||
actionArea->setStyleSheet(QStringLiteral("background:%1;").arg(colorStr(theme.bg)));
|
||||
|
||||
separator->setStyleSheet(QStringLiteral("color:%1; margin: 0 8px;").arg(colorStr(theme.border)));
|
||||
|
||||
gamesLabel->setStyleSheet(QStringLiteral("color:%1; font-size:11px; font-weight:bold; background:transparent;")
|
||||
.arg(colorStr(theme.subText)));
|
||||
|
||||
refreshBtn->setStyleSheet(QStringLiteral("QPushButton{color:%1;border:none;font-size:14px;background:transparent;}"
|
||||
"QPushButton:hover{color:%2;}")
|
||||
.arg(colorStr(theme.subText), colorStr(theme.text)));
|
||||
|
||||
gamesStatus->setStyleSheet(QStringLiteral("color:%1; font-size:11px; padding:10px; background:transparent;")
|
||||
.arg(colorStr(theme.statusText)));
|
||||
|
||||
gamesView->setStyleSheet(QStringLiteral("QListView{background:%1;border:none;}"
|
||||
"QListView::item:selected{background:%2;}")
|
||||
.arg(colorStr(theme.gamesRow), colorStr(theme.gamesSelected)));
|
||||
|
||||
closeBtn->setStyleSheet(
|
||||
QStringLiteral("QPushButton{background:%1;color:%2;"
|
||||
"border:none;border-radius:11px;font-size:10px;}"
|
||||
"QPushButton:hover{color:%3;background:%4;}")
|
||||
.arg(colorStr(theme.closeBg), colorStr(theme.subText), colorStr(theme.text), colorStr(theme.closeHover)));
|
||||
|
||||
header->update();
|
||||
}
|
||||
|
||||
// ── Action button factory ─────────────────────────────────────────────────────
|
||||
|
||||
static QPushButton *makeBtn(const QString &label, const QString &tip, QWidget *p)
|
||||
static QPushButton *makeBtn(const QString &label, const QString &tip, QWidget *p, const PopupTheme &t)
|
||||
{
|
||||
auto *b = new QPushButton(label, p);
|
||||
b->setToolTip(tip);
|
||||
b->setFixedHeight(26);
|
||||
b->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
b->setStyleSheet(QStringLiteral("QPushButton{"
|
||||
" background:#192030;color:#b8c8de;border:1px solid #263040;"
|
||||
" background:%1;color:%2;border:1px solid %3;"
|
||||
" border-radius:4px;font-size:11px;padding:0 4px;"
|
||||
"}"
|
||||
"QPushButton:hover{background:#223050;color:white;}"
|
||||
"QPushButton:pressed{background:#162030;}"
|
||||
"QPushButton:disabled{color:#384858;border-color:#192030;}"));
|
||||
"QPushButton:hover{background:%4;color:%5;}"
|
||||
"QPushButton:pressed{background:%6;}"
|
||||
"QPushButton:disabled{color:%7;border-color:%3;}")
|
||||
.arg(colorStr(t.buttonBg), colorStr(t.text), colorStr(t.buttonBorder), colorStr(t.buttonHover),
|
||||
colorStr(t.text), colorStr(t.buttonPressed), colorStr(t.buttonDisabled)));
|
||||
return b;
|
||||
}
|
||||
|
||||
void UserInfoPopup::rebuildActionButtons(const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored)
|
||||
{
|
||||
// Clear previous contents
|
||||
delete m_actionArea->layout();
|
||||
const auto old = m_actionArea->findChildren<QPushButton *>(QString{}, Qt::FindDirectChildrenOnly);
|
||||
delete actionArea->layout();
|
||||
const auto old = actionArea->findChildren<QPushButton *>(QString{}, Qt::FindDirectChildrenOnly);
|
||||
for (auto *w : old) {
|
||||
w->deleteLater();
|
||||
}
|
||||
|
||||
const QString name = QString::fromStdString(userInfo.name());
|
||||
const auto ownLevel = UserLevelFlags(m_ts->getUserInfo()->user_level());
|
||||
const bool isSelf = (name == QString::fromStdString(m_ts->getUserInfo()->name()));
|
||||
const auto ownLevel = UserLevelFlags(ts->getUserInfo()->user_level());
|
||||
const bool isSelf = (name == QString::fromStdString(ts->getUserInfo()->name()));
|
||||
const bool isMod = ownLevel.testFlag(ServerInfo_User::IsModerator);
|
||||
const bool isAdmin = ownLevel.testFlag(ServerInfo_User::IsAdmin);
|
||||
const auto their = UserLevelFlags(userInfo.user_level());
|
||||
const bool isReg = their.testFlag(ServerInfo_User::IsRegistered);
|
||||
|
||||
auto *grid = new QGridLayout(m_actionArea);
|
||||
auto *grid = new QGridLayout(actionArea);
|
||||
grid->setContentsMargins(8, 6, 8, 6);
|
||||
grid->setSpacing(4);
|
||||
|
||||
|
|
@ -394,16 +511,16 @@ void UserInfoPopup::rebuildActionButtons(const ServerInfo_User &userInfo, bool o
|
|||
};
|
||||
|
||||
// ── Always visible ────────────────────────────────────────────────────────
|
||||
auto *chat = makeBtn(tr("Chat"), tr("Open private chat"), m_actionArea);
|
||||
auto *chat = makeBtn(tr("Chat"), tr("Open private chat"), actionArea, theme);
|
||||
chat->setEnabled(!isSelf && online);
|
||||
connect(chat, &QPushButton::clicked, this, [this, name] { emit chatRequested(name); });
|
||||
add(chat);
|
||||
|
||||
auto *prof = makeBtn(tr("Profile"), tr("View user profile"), m_actionArea);
|
||||
auto *prof = makeBtn(tr("Profile"), tr("View user profile"), actionArea, theme);
|
||||
connect(prof, &QPushButton::clicked, this, [this, name] { emit detailsRequested(name); });
|
||||
add(prof);
|
||||
|
||||
auto *games = makeBtn(tr("Games"), tr("Show this user's games"), m_actionArea);
|
||||
auto *games = makeBtn(tr("Games"), tr("Show this user's games"), actionArea, theme);
|
||||
games->setEnabled(!isSelf && online);
|
||||
connect(games, &QPushButton::clicked, this, [this, name] { emit showGamesRequested(name); });
|
||||
add(games);
|
||||
|
|
@ -411,20 +528,20 @@ void UserInfoPopup::rebuildActionButtons(const ServerInfo_User &userInfo, bool o
|
|||
// ── Buddy / ignore (registered users only) ────────────────────────────────
|
||||
if (!isSelf && isReg) {
|
||||
if (isBuddy) {
|
||||
auto *b = makeBtn(tr("− Buddy"), tr("Remove from buddy list"), m_actionArea);
|
||||
auto *b = makeBtn(tr("− Buddy"), tr("Remove from buddy list"), actionArea, theme);
|
||||
connect(b, &QPushButton::clicked, this, [this, name] { emit removeBuddyRequested(name); });
|
||||
add(b);
|
||||
} else {
|
||||
auto *b = makeBtn(tr("+ Buddy"), tr("Add to buddy list"), m_actionArea);
|
||||
auto *b = makeBtn(tr("+ Buddy"), tr("Add to buddy list"), actionArea, theme);
|
||||
connect(b, &QPushButton::clicked, this, [this, name] { emit addBuddyRequested(name); });
|
||||
add(b);
|
||||
}
|
||||
if (isIgnored) {
|
||||
auto *b = makeBtn(tr("− Ignore"), tr("Remove from ignore list"), m_actionArea);
|
||||
auto *b = makeBtn(tr("− Ignore"), tr("Remove from ignore list"), actionArea, theme);
|
||||
connect(b, &QPushButton::clicked, this, [this, name] { emit removeIgnoreRequested(name); });
|
||||
add(b);
|
||||
} else {
|
||||
auto *b = makeBtn(tr("+ Ignore"), tr("Add to ignore list"), m_actionArea);
|
||||
auto *b = makeBtn(tr("+ Ignore"), tr("Add to ignore list"), actionArea, theme);
|
||||
connect(b, &QPushButton::clicked, this, [this, name] { emit addIgnoreRequested(name); });
|
||||
add(b);
|
||||
}
|
||||
|
|
@ -437,10 +554,10 @@ void UserInfoPopup::rebuildActionButtons(const ServerInfo_User &userInfo, bool o
|
|||
col = 0;
|
||||
} // start mod section on a fresh row
|
||||
|
||||
auto *ban = makeBtn(tr("Ban"), tr("Ban from server"), m_actionArea);
|
||||
auto *warn = makeBtn(tr("Warn"), tr("Warn user"), m_actionArea);
|
||||
auto *bLog = makeBtn(tr("Ban log"), tr("View ban history"), m_actionArea);
|
||||
auto *wLog = makeBtn(tr("Warn log"), tr("View warning history"), m_actionArea);
|
||||
auto *ban = makeBtn(tr("Ban"), tr("Ban from server"), actionArea, theme);
|
||||
auto *warn = makeBtn(tr("Warn"), tr("Warn user"), actionArea, theme);
|
||||
auto *bLog = makeBtn(tr("Ban log"), tr("View ban history"), actionArea, theme);
|
||||
auto *wLog = makeBtn(tr("Warn log"), tr("View warning history"), actionArea, theme);
|
||||
connect(ban, &QPushButton::clicked, this, [this, name] { emit banRequested(name); });
|
||||
connect(warn, &QPushButton::clicked, this, [this, name] { emit warnRequested(name); });
|
||||
connect(bLog, &QPushButton::clicked, this, [this, name] { emit banHistoryRequested(name); });
|
||||
|
|
@ -453,31 +570,31 @@ void UserInfoPopup::rebuildActionButtons(const ServerInfo_User &userInfo, bool o
|
|||
|
||||
// ── Admin actions ─────────────────────────────────────────────────────────
|
||||
if (!isSelf && isAdmin) {
|
||||
auto *notes = makeBtn(tr("Notes"), tr("View admin notes"), m_actionArea);
|
||||
auto *notes = makeBtn(tr("Notes"), tr("View admin notes"), actionArea, theme);
|
||||
connect(notes, &QPushButton::clicked, this, [this, name] { emit adminNotesRequested(name); });
|
||||
add(notes);
|
||||
|
||||
if (their.testFlag(ServerInfo_User::IsModerator)) {
|
||||
auto *b = makeBtn(tr("− Mod"), tr("Demote from moderator"), m_actionArea);
|
||||
auto *b = makeBtn(tr("− Mod"), tr("Demote from moderator"), actionArea, theme);
|
||||
connect(b, &QPushButton::clicked, this, [this, name] { emit demoteFromModRequested(name); });
|
||||
add(b);
|
||||
} else if (isReg) {
|
||||
auto *b = makeBtn(tr("+ Mod"), tr("Promote to moderator"), m_actionArea);
|
||||
auto *b = makeBtn(tr("+ Mod"), tr("Promote to moderator"), actionArea, theme);
|
||||
connect(b, &QPushButton::clicked, this, [this, name] { emit promoteToModRequested(name); });
|
||||
add(b);
|
||||
}
|
||||
if (their.testFlag(ServerInfo_User::IsJudge)) {
|
||||
auto *b = makeBtn(tr("− Judge"), tr("Demote from judge"), m_actionArea);
|
||||
auto *b = makeBtn(tr("− Judge"), tr("Demote from judge"), actionArea, theme);
|
||||
connect(b, &QPushButton::clicked, this, [this, name] { emit demoteFromJudgeRequested(name); });
|
||||
add(b);
|
||||
} else if (isReg) {
|
||||
auto *b = makeBtn(tr("+ Judge"), tr("Promote to judge"), m_actionArea);
|
||||
auto *b = makeBtn(tr("+ Judge"), tr("Promote to judge"), actionArea, theme);
|
||||
connect(b, &QPushButton::clicked, this, [this, name] { emit promoteToJudgeRequested(name); });
|
||||
add(b);
|
||||
}
|
||||
}
|
||||
|
||||
m_actionArea->adjustSize();
|
||||
actionArea->adjustSize();
|
||||
}
|
||||
|
||||
void UserInfoPopup::updateActionButtons(const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored)
|
||||
|
|
@ -488,7 +605,7 @@ void UserInfoPopup::updateActionButtons(const ServerInfo_User &userInfo, bool on
|
|||
|
||||
void UserInfoPopup::onGamesContextMenu(const QPoint &pos)
|
||||
{
|
||||
const QModelIndex idx = m_gamesView->indexAt(pos);
|
||||
const QModelIndex idx = gamesView->indexAt(pos);
|
||||
if (!idx.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -501,8 +618,9 @@ void UserInfoPopup::onGamesContextMenu(const QPoint &pos)
|
|||
|
||||
QMenu menu(this);
|
||||
menu.setStyleSheet(
|
||||
QStringLiteral("QMenu{background:#12182a;color:#c8d8ec;border:1px solid #1e2838;border-radius:4px;}"
|
||||
"QMenu::item:selected{background:#223050;}"));
|
||||
QStringLiteral("QMenu{background:%1;color:%2;border:1px solid %3;border-radius:4px;}"
|
||||
"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();
|
||||
QAction *join = menu.addAction(tr("Join game"));
|
||||
|
|
@ -513,7 +631,7 @@ void UserInfoPopup::onGamesContextMenu(const QPoint &pos)
|
|||
spec = menu.addAction(tr("Spectate"));
|
||||
}
|
||||
|
||||
const QAction *chosen = menu.exec(m_gamesView->viewport()->mapToGlobal(pos));
|
||||
const QAction *chosen = menu.exec(gamesView->viewport()->mapToGlobal(pos));
|
||||
if (!chosen) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -527,37 +645,46 @@ void UserInfoPopup::onGamesContextMenu(const QPoint &pos)
|
|||
|
||||
// ── showForUser ───────────────────────────────────────────────────────────────
|
||||
|
||||
void UserInfoPopup::refreshHeader()
|
||||
{
|
||||
if (currentUser.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QPixmap avatar = avatarCache ? avatarCache->value(currentUser) : QPixmap{};
|
||||
const CardArtParams params = (cardArtParamsMap && cardArtParamsMap->contains(currentUser))
|
||||
? cardArtParamsMap->value(currentUser)
|
||||
: CardArtParams{};
|
||||
const QString artKey = currentUser + u'|' + params.cardName + u'|' + params.cardProviderId;
|
||||
const QPixmap cardArt = (cardArtCache && !params.cardName.isEmpty()) ? cardArtCache->value(artKey) : QPixmap{};
|
||||
header->setUserData(currentUserInfo, currentOnline, avatar, cardArt, params);
|
||||
}
|
||||
|
||||
void UserInfoPopup::showForUser(const QString &userName,
|
||||
const ServerInfo_User &userInfo,
|
||||
bool online,
|
||||
bool isBuddy,
|
||||
bool isIgnored)
|
||||
{
|
||||
m_currentUser = userName;
|
||||
m_currentUserInfo = userInfo;
|
||||
m_currentOnline = online;
|
||||
currentUser = userName;
|
||||
currentUserInfo = userInfo;
|
||||
currentOnline = online;
|
||||
|
||||
// Header
|
||||
const QPixmap avatar = m_avatarCache ? m_avatarCache->value(userName) : QPixmap{};
|
||||
const CardArtParams params = (m_cardArtParamsMap && m_cardArtParamsMap->contains(userName))
|
||||
? m_cardArtParamsMap->value(userName)
|
||||
: CardArtParams{};
|
||||
const QString artKey = userName + u'|' + params.cardName + u'|' + params.cardProviderId;
|
||||
const QPixmap cardArt = (m_cardArtCache && !params.cardName.isEmpty()) ? m_cardArtCache->value(artKey) : QPixmap{};
|
||||
m_header->setUserData(userInfo, online, avatar, cardArt, params);
|
||||
refreshHeader();
|
||||
|
||||
// Actions
|
||||
rebuildActionButtons(userInfo, online, isBuddy, isIgnored);
|
||||
|
||||
// Games list reset
|
||||
m_gamesModel->clear();
|
||||
m_gamesView->hide();
|
||||
m_gamesStatus->setText(tr("Loading games…"));
|
||||
m_gamesStatus->show();
|
||||
gamesModel->clear();
|
||||
gamesView->hide();
|
||||
gamesStatus->setText(tr("Loading games…"));
|
||||
gamesStatus->show();
|
||||
|
||||
// Close button — top-right corner, above everything
|
||||
m_closeBtn->move(PopupWidth - m_closeBtn->width() - 6, 6);
|
||||
m_closeBtn->raise();
|
||||
// Close button, top right corner, above everything
|
||||
closeBtn->move(PopupWidth - closeBtn->width() - 6, 6);
|
||||
closeBtn->raise();
|
||||
|
||||
adjustSize();
|
||||
fetchGames();
|
||||
|
|
@ -567,40 +694,40 @@ void UserInfoPopup::showForUser(const QString &userName,
|
|||
|
||||
void UserInfoPopup::fetchGames()
|
||||
{
|
||||
if (!m_client || m_currentUser.isEmpty()) {
|
||||
if (!client || currentUser.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Command_GetGamesOfUser cmd;
|
||||
cmd.set_user_name(m_currentUser.toStdString());
|
||||
cmd.set_user_name(currentUser.toStdString());
|
||||
|
||||
const QString snapshot = m_currentUser;
|
||||
PendingCommand *pend = m_client->prepareSessionCommand(cmd);
|
||||
const QString snapshot = currentUser;
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this,
|
||||
[this, snapshot](const Response &r) { onGamesReceived(r, snapshot); });
|
||||
m_client->sendCommand(pend);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void UserInfoPopup::onGamesReceived(const Response &r, const QString &forUser)
|
||||
{
|
||||
if (forUser != m_currentUser) {
|
||||
return; // stale response — different user showing now
|
||||
if (forUser != currentUser) {
|
||||
return; // stale response, different user showing now
|
||||
}
|
||||
|
||||
m_gamesModel->clear();
|
||||
gamesModel->clear();
|
||||
|
||||
if (r.response_code() != Response::RespOk) {
|
||||
m_gamesStatus->setText(tr("Could not load games."));
|
||||
m_gamesStatus->show();
|
||||
m_gamesView->hide();
|
||||
gamesStatus->setText(tr("Could not load games."));
|
||||
gamesStatus->show();
|
||||
gamesView->hide();
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &resp = r.GetExtension(Response_GetGamesOfUser::ext);
|
||||
if (resp.game_list_size() == 0) {
|
||||
m_gamesStatus->setText(tr("No active games."));
|
||||
m_gamesStatus->show();
|
||||
m_gamesView->hide();
|
||||
gamesStatus->setText(tr("No active games."));
|
||||
gamesStatus->show();
|
||||
gamesView->hide();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -608,29 +735,29 @@ void UserInfoPopup::onGamesReceived(const Response &r, const QString &forUser)
|
|||
auto *item = new QStandardItem;
|
||||
item->setData(QVariant::fromValue(resp.game_list(i)), PopupRoles::GameData);
|
||||
item->setEditable(false);
|
||||
m_gamesModel->appendRow(item);
|
||||
gamesModel->appendRow(item);
|
||||
}
|
||||
|
||||
m_gamesStatus->hide();
|
||||
m_gamesView->show();
|
||||
gamesStatus->hide();
|
||||
gamesView->show();
|
||||
|
||||
// Fit exactly to the number of visible rows, scroll when more than 5
|
||||
constexpr int rowH = 38; // must match PopupGameDelegate::sizeHint
|
||||
constexpr int maxRows = 5;
|
||||
const int count = m_gamesModel->rowCount();
|
||||
const int count = gamesModel->rowCount();
|
||||
const int visible = qMin(count, maxRows);
|
||||
m_gamesView->setFixedHeight(visible * rowH + 2);
|
||||
m_gamesView->setVerticalScrollBarPolicy(count > maxRows ? Qt::ScrollBarAlwaysOn : Qt::ScrollBarAlwaysOff);
|
||||
gamesView->setFixedHeight(visible * rowH + 2);
|
||||
gamesView->setVerticalScrollBarPolicy(count > maxRows ? Qt::ScrollBarAlwaysOn : Qt::ScrollBarAlwaysOff);
|
||||
|
||||
adjustSize();
|
||||
}
|
||||
|
||||
void UserInfoPopup::refreshGames()
|
||||
{
|
||||
m_gamesModel->clear();
|
||||
m_gamesView->hide();
|
||||
m_gamesStatus->setText(tr("Loading games…"));
|
||||
m_gamesStatus->show();
|
||||
gamesModel->clear();
|
||||
gamesView->hide();
|
||||
gamesStatus->setText(tr("Loading games…"));
|
||||
gamesStatus->show();
|
||||
fetchGames();
|
||||
}
|
||||
|
||||
|
|
@ -646,4 +773,4 @@ void UserInfoPopup::leaveEvent(QEvent *e)
|
|||
{
|
||||
QFrame::leaveEvent(e);
|
||||
emit mouseLeftPopup();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,35 @@ namespace PopupRoles
|
|||
constexpr int GameData = Qt::UserRole + 10;
|
||||
}
|
||||
|
||||
// Popup theme
|
||||
|
||||
/**
|
||||
* Palette-derived colors for the popup chrome. Both color schemes read from
|
||||
* the active QPalette so custom palettes are respected. @c dark only tunes the
|
||||
* blend strengths.
|
||||
*/
|
||||
struct PopupTheme
|
||||
{
|
||||
bool dark = false;
|
||||
QColor bg;
|
||||
QColor border;
|
||||
QColor text;
|
||||
QColor subText;
|
||||
QColor buttonBg;
|
||||
QColor buttonBorder;
|
||||
QColor buttonHover;
|
||||
QColor buttonPressed;
|
||||
QColor buttonDisabled;
|
||||
QColor closeBg;
|
||||
QColor closeHover;
|
||||
QColor gamesRow;
|
||||
QColor gamesSelected;
|
||||
QColor gamesSeparator;
|
||||
QColor statusText;
|
||||
|
||||
static PopupTheme fromPalette(const QPalette &palette, bool dark);
|
||||
};
|
||||
|
||||
// ── Header widget ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
|
@ -51,21 +80,22 @@ class UserInfoHeaderWidget : public QWidget
|
|||
public:
|
||||
explicit UserInfoHeaderWidget(QWidget *parent = nullptr);
|
||||
|
||||
void setUserData(const ServerInfo_User &user,
|
||||
bool online,
|
||||
const QPixmap &avatar,
|
||||
const QPixmap &cardArt,
|
||||
const CardArtParams ¶ms);
|
||||
void setUserData(const ServerInfo_User &_user,
|
||||
bool _online,
|
||||
const QPixmap &_avatar,
|
||||
const QPixmap &_cardArt,
|
||||
const CardArtParams &_params);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
|
||||
private:
|
||||
ServerInfo_User m_user;
|
||||
bool m_online = false;
|
||||
QPixmap m_avatar;
|
||||
QPixmap m_cardArt;
|
||||
CardArtParams m_params;
|
||||
ServerInfo_User user;
|
||||
bool online = false;
|
||||
QPixmap avatar;
|
||||
QPixmap cardArt;
|
||||
CardArtParams params;
|
||||
QString attribution;
|
||||
};
|
||||
|
||||
// ── Main popup ────────────────────────────────────────────────────────────────
|
||||
|
|
@ -93,11 +123,11 @@ class UserInfoPopup : public QFrame
|
|||
static constexpr int PopupWidth = 316;
|
||||
|
||||
public:
|
||||
explicit UserInfoPopup(TabSupervisor *tabSupervisor,
|
||||
AbstractClient *client,
|
||||
const QMap<QString, QPixmap> *avatarCache,
|
||||
const QMap<QString, QPixmap> *cardArtCache,
|
||||
const QMap<QString, CardArtParams> *cardArtParamsMap,
|
||||
explicit UserInfoPopup(TabSupervisor *_ts,
|
||||
AbstractClient *_client,
|
||||
const QMap<QString, QPixmap> *_avatarCache,
|
||||
const QMap<QString, QPixmap> *_cardArtCache,
|
||||
const QMap<QString, CardArtParams> *_cardArtParamsMap,
|
||||
QWidget *parent);
|
||||
|
||||
/**
|
||||
|
|
@ -108,14 +138,17 @@ public:
|
|||
showForUser(const QString &userName, const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored);
|
||||
void fetchGames();
|
||||
|
||||
[[nodiscard]] QString currentUser() const
|
||||
[[nodiscard]] QString getCurrentUser() const
|
||||
{
|
||||
return m_currentUser;
|
||||
return currentUser;
|
||||
}
|
||||
|
||||
/** Called when buddy/ignore status changes externally while popup is open. */
|
||||
void updateActionButtons(const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored);
|
||||
|
||||
/** Re-pulls the avatar/card art for the currently shown user (e.g. after it loads). */
|
||||
void refreshHeader();
|
||||
|
||||
signals:
|
||||
void mouseEnteredPopup();
|
||||
void mouseLeftPopup();
|
||||
|
|
@ -153,25 +186,30 @@ private slots:
|
|||
|
||||
private:
|
||||
void buildUi();
|
||||
void applyTheme();
|
||||
void rebuildActionButtons(const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored);
|
||||
|
||||
TabSupervisor *m_ts;
|
||||
AbstractClient *m_client;
|
||||
const QMap<QString, QPixmap> *m_avatarCache;
|
||||
const QMap<QString, QPixmap> *m_cardArtCache;
|
||||
const QMap<QString, CardArtParams> *m_cardArtParamsMap;
|
||||
TabSupervisor *ts;
|
||||
AbstractClient *client;
|
||||
const QMap<QString, QPixmap> *avatarCache;
|
||||
const QMap<QString, QPixmap> *cardArtCache;
|
||||
const QMap<QString, CardArtParams> *cardArtParamsMap;
|
||||
|
||||
QString m_currentUser;
|
||||
ServerInfo_User m_currentUserInfo;
|
||||
bool m_currentOnline = false;
|
||||
PopupTheme theme;
|
||||
|
||||
UserInfoHeaderWidget *m_header;
|
||||
QWidget *m_actionArea; ///< rebuilt per user
|
||||
QListView *m_gamesView;
|
||||
QStandardItemModel *m_gamesModel;
|
||||
QLabel *m_gamesStatus;
|
||||
QPushButton *m_closeBtn;
|
||||
QPushButton *m_refreshBtn;
|
||||
QString currentUser;
|
||||
ServerInfo_User currentUserInfo;
|
||||
bool currentOnline = false;
|
||||
|
||||
UserInfoHeaderWidget *header;
|
||||
QWidget *actionArea; ///< rebuilt per user
|
||||
QLabel *gamesLabel;
|
||||
QFrame *separator;
|
||||
QListView *gamesView;
|
||||
QStandardItemModel *gamesModel;
|
||||
QLabel *gamesStatus;
|
||||
QPushButton *closeBtn;
|
||||
QPushButton *refreshBtn;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_USER_INFO_POPUP_H
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
#include "../../interface/pixel_map_generator.h"
|
||||
|
||||
#include <QAbstractScrollArea>
|
||||
#include <QApplication>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QScrollBar>
|
||||
|
|
@ -19,6 +20,29 @@ QSize UserListPainter::sizeHint()
|
|||
return QSize(0, RowHeight);
|
||||
}
|
||||
|
||||
UserListPainter::Style UserListPainter::resolveStyle(const QPalette &palette, bool dark)
|
||||
{
|
||||
Style style;
|
||||
style.dark = dark;
|
||||
const QColor base = palette.color(QPalette::Base);
|
||||
const QColor alt = palette.color(QPalette::AlternateBase);
|
||||
style.cardStart = base;
|
||||
style.cardEnd = (alt != base) ? alt : palette.color(QPalette::Midlight);
|
||||
style.base = base;
|
||||
style.textOnline = palette.color(QPalette::Text);
|
||||
style.textOffline = palette.color(QPalette::Disabled, QPalette::Text);
|
||||
style.ringOffline = palette.color(QPalette::Disabled, QPalette::Text);
|
||||
style.dropShadow = dark;
|
||||
return style;
|
||||
}
|
||||
|
||||
QColor UserListPainter::blend(const QColor &a, const QColor &b, qreal t)
|
||||
{
|
||||
const qreal u = 1.0 - t;
|
||||
return QColor(qRound(a.red() * u + b.red() * t), qRound(a.green() * u + b.green() * t),
|
||||
qRound(a.blue() * u + b.blue() * t), qRound(a.alpha() * u + b.alpha() * t));
|
||||
}
|
||||
|
||||
QColor UserListPainter::getAccentColor(const UserLevelFlags &userLevel, bool online)
|
||||
{
|
||||
QColor accentColor;
|
||||
|
|
@ -59,16 +83,37 @@ int UserListPainter::getCardRight(const QStyleOptionViewItem &option, const QRec
|
|||
void UserListPainter::drawBackground(QPainter *painter,
|
||||
const QRectF &cardRect,
|
||||
const QColor &accentColor,
|
||||
bool selected)
|
||||
bool selected,
|
||||
const Style &style,
|
||||
bool hasRole)
|
||||
{
|
||||
QLinearGradient bg(cardRect.topLeft(), cardRect.topRight());
|
||||
bg.setColorAt(0, selected ? accentColor.darker(130) : accentColor.darker(320));
|
||||
bg.setColorAt(1, selected ? QColor(40, 48, 60) : QColor(18, 22, 30));
|
||||
if (style.dark) {
|
||||
// Dark mode darkens the role color to fit the dark surface and fades
|
||||
// it into the deep navy surface on the right. The text drop shadow
|
||||
// keeps the username legible over the colored edge.
|
||||
bg.setColorAt(0, selected ? accentColor.darker(130) : accentColor.darker(320));
|
||||
bg.setColorAt(1, selected ? QColor(40, 48, 60) : QColor(18, 22, 30));
|
||||
} else if (hasRole) {
|
||||
// Light mode pegs the role color on the left at near full strength
|
||||
// and fades it into the white surface on the right. The tint stays
|
||||
// bright enough that the dark text remains legible without a shadow.
|
||||
bg.setColorAt(0, blend(style.cardStart, accentColor, selected ? 0.75 : 0.65));
|
||||
bg.setColorAt(1, blend(style.cardEnd, accentColor, selected ? 0.18 : 0.10));
|
||||
} else {
|
||||
// Regular users keep a scaled-down accent tint so the banner card art
|
||||
// stays legible over a colored backdrop (the pre-branch painter was
|
||||
// always dark-styled) while the role hierarchy still reads.
|
||||
bg.setColorAt(0, blend(style.cardStart, accentColor, (selected ? 0.75 : 0.65) * 0.7));
|
||||
bg.setColorAt(1, blend(style.cardEnd, accentColor, (selected ? 0.18 : 0.10) * 0.7));
|
||||
}
|
||||
|
||||
painter->setPen(Qt::NoPen);
|
||||
painter->setBrush(bg);
|
||||
painter->drawRoundedRect(cardRect, 6, 6);
|
||||
|
||||
// The 3px accent bar anchors every row so the banner card art reads as a
|
||||
// consistent strip in either scheme (pre-branch parity).
|
||||
painter->setBrush(accentColor);
|
||||
painter->drawRoundedRect(QRectF(cardRect.left(), cardRect.top(), 3, cardRect.height()), 2, 2);
|
||||
}
|
||||
|
|
@ -108,6 +153,12 @@ void UserListPainter::drawCardArt(QPainter *painter,
|
|||
return;
|
||||
}
|
||||
|
||||
// CardPictureLoader::getPixmap tags its output with the screen's
|
||||
// devicePixelRatio on HiDPI displays. Every calculation below is in raw
|
||||
// pixels, so normalize to 1.0 or the crop renders at 1/dpr scale anchored
|
||||
// to the top left corner of the row.
|
||||
art.setDevicePixelRatio(1.0);
|
||||
|
||||
const int cardH = rect.height() - 4;
|
||||
const int totalW = cardRight - rect.left();
|
||||
const int marginL = qRound(totalW * params.marginPctL);
|
||||
|
|
@ -125,11 +176,14 @@ void UserListPainter::drawCardArt(QPainter *painter,
|
|||
const int srcX = (scaledW - drawW) / 2;
|
||||
const int srcY = qRound((scaledH - cardH) * params.verticalOffset);
|
||||
|
||||
// Clamp srcY so we never copy outside the pixmap bounds
|
||||
// Clamp so we never copy outside the pixmap bounds. srcX can go negative
|
||||
// for stored zoom values below 1, which would silently underfill the
|
||||
// strip with transparent padding.
|
||||
const int safeSrcX = qBound(0, srcX, qMax(0, scaledW - drawW));
|
||||
const int safeSrcY = qBound(0, srcY, qMax(0, scaledH - cardH));
|
||||
|
||||
QImage img =
|
||||
scaled.copy(srcX, safeSrcY, drawW, cardH).toImage().convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
||||
scaled.copy(safeSrcX, safeSrcY, drawW, cardH).toImage().convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
||||
|
||||
{
|
||||
QPainter mask(&img);
|
||||
|
|
@ -163,7 +217,8 @@ void UserListPainter::drawAvatar(QPainter *painter,
|
|||
const UserLevelFlags &userLevel,
|
||||
const ServerInfo_User &userInfo,
|
||||
const QString &privLevel,
|
||||
const QMap<QString, QPixmap> *avatarCache)
|
||||
const QMap<QString, QPixmap> *avatarCache,
|
||||
const Style &style)
|
||||
{
|
||||
QPainterPath clipPath;
|
||||
clipPath.addEllipse(avatarRect);
|
||||
|
|
@ -183,7 +238,7 @@ void UserListPainter::drawAvatar(QPainter *painter,
|
|||
}
|
||||
|
||||
if (!drewAvatar) {
|
||||
painter->setBrush(accentColor.darker(200));
|
||||
painter->setBrush(blend(accentColor, style.base, style.dark ? 0.45 : 0.72));
|
||||
painter->setPen(Qt::NoPen);
|
||||
painter->drawEllipse(avatarRect);
|
||||
|
||||
|
|
@ -196,9 +251,9 @@ void UserListPainter::drawAvatar(QPainter *painter,
|
|||
painter->restore();
|
||||
}
|
||||
|
||||
void UserListPainter::drawStatusRing(QPainter *painter, const QRect &avatarRect, bool online)
|
||||
void UserListPainter::drawStatusRing(QPainter *painter, const QRect &avatarRect, bool online, const Style &style)
|
||||
{
|
||||
const QColor statusColor = online ? QColor(34, 197, 94) : QColor(70, 80, 95);
|
||||
const QColor statusColor = online ? QColor(34, 197, 94) : style.ringOffline;
|
||||
|
||||
painter->setPen(QPen(statusColor, 2));
|
||||
painter->setBrush(Qt::NoBrush);
|
||||
|
|
@ -212,7 +267,7 @@ void UserListPainter::drawUserName(QPainter *painter,
|
|||
int textX,
|
||||
const QString &userName,
|
||||
bool online,
|
||||
bool selected)
|
||||
const Style &style)
|
||||
{
|
||||
QFont nameFont = option.font;
|
||||
nameFont.setBold(true);
|
||||
|
|
@ -221,10 +276,12 @@ void UserListPainter::drawUserName(QPainter *painter,
|
|||
const QRect nameRect(textX, rect.top() + 8, cardRight - textX - 10, 20);
|
||||
const QString elidedName = QFontMetrics(nameFont).elidedText(userName, Qt::ElideRight, cardRight - textX - 10);
|
||||
|
||||
painter->setPen(QColor(0, 0, 0, 200));
|
||||
painter->drawText(nameRect.translated(1, 1), Qt::AlignVCenter | Qt::AlignLeft, elidedName);
|
||||
if (style.dropShadow) {
|
||||
painter->setPen(QColor(0, 0, 0, 200));
|
||||
painter->drawText(nameRect.translated(1, 1), Qt::AlignVCenter | Qt::AlignLeft, elidedName);
|
||||
}
|
||||
|
||||
painter->setPen(online ? (selected ? Qt::white : QColor(226, 232, 240)) : QColor(90, 100, 115));
|
||||
painter->setPen(online ? style.textOnline : style.textOffline);
|
||||
painter->drawText(nameRect, Qt::AlignVCenter | Qt::AlignLeft, elidedName);
|
||||
}
|
||||
|
||||
|
|
@ -262,7 +319,8 @@ void UserListPainter::drawBadges(QPainter *painter,
|
|||
const QRect &rect,
|
||||
int cardRight,
|
||||
const QList<Badge> &badges,
|
||||
bool online)
|
||||
bool online,
|
||||
const Style &style)
|
||||
{
|
||||
if (badges.isEmpty()) {
|
||||
return;
|
||||
|
|
@ -284,15 +342,17 @@ void UserListPainter::drawBadges(QPainter *painter,
|
|||
int bx = cardRight - 6 - totalBadgeW;
|
||||
|
||||
for (const Badge &b : badges) {
|
||||
const QColor col = online ? b.color : b.color.darker(180);
|
||||
const QColor col = online ? b.color : blend(b.color, style.base, 0.55);
|
||||
const QColor surface = blend(col, style.base, style.dark ? 0.55 : 0.78);
|
||||
const QColor text = style.dark ? blend(col, Qt::white, 0.5) : blend(col, Qt::black, 0.35);
|
||||
const int bw = fm.horizontalAdvance(b.text) + 8;
|
||||
const QRect br(bx, rect.top() + 44, bw, 13);
|
||||
|
||||
painter->setPen(Qt::NoPen);
|
||||
painter->setBrush(col.darker(online ? 160 : 220));
|
||||
painter->setBrush(surface);
|
||||
painter->drawRoundedRect(br, 3, 3);
|
||||
|
||||
painter->setPen(col.lighter(online ? 160 : 100));
|
||||
painter->setPen(text);
|
||||
painter->drawText(br, Qt::AlignCenter, b.text);
|
||||
|
||||
bx += bw + 4;
|
||||
|
|
@ -305,11 +365,19 @@ void UserListPainter::paint(QPainter *painter,
|
|||
const ServerInfo_User &userInfo,
|
||||
const QMap<QString, QPixmap> *avatarCache,
|
||||
const QMap<QString, QPixmap> *cardArtCache,
|
||||
const QMap<QString, CardArtParams> *cardArtParamsMap)
|
||||
const QMap<QString, CardArtParams> *cardArtParamsMap,
|
||||
bool dark)
|
||||
{
|
||||
painter->save();
|
||||
painter->setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::TextAntialiasing);
|
||||
|
||||
// The delegate supplies the application palette in option.palette, which
|
||||
// always reflects the active theme. The widget palette can be stale after
|
||||
// a runtime theme change, so it is only used as a defensive fallback.
|
||||
const QPalette pal =
|
||||
option.palette == QPalette() ? (option.widget ? option.widget->palette() : qApp->palette()) : option.palette;
|
||||
const Style style = resolveStyle(pal, dark);
|
||||
|
||||
const QRect rect = option.rect;
|
||||
const bool online = index.data(Qt::UserRole + 1).toBool();
|
||||
const bool selected = option.state & QStyle::State_Selected;
|
||||
|
|
@ -317,6 +385,9 @@ void UserListPainter::paint(QPainter *painter,
|
|||
const QString userName = QString::fromStdString(userInfo.name());
|
||||
const QString privLevel = QString::fromStdString(userInfo.privlevel());
|
||||
const QColor accentColor = getAccentColor(userLevel, online);
|
||||
const bool hasRole = userLevel.testFlag(ServerInfo_User::IsAdmin) ||
|
||||
userLevel.testFlag(ServerInfo_User::IsModerator) ||
|
||||
userLevel.testFlag(ServerInfo_User::IsJudge);
|
||||
const QRectF cardRect = QRectF(rect).adjusted(3, 2, -3, -2);
|
||||
const int cardRight = getCardRight(option, rect);
|
||||
|
||||
|
|
@ -324,19 +395,19 @@ void UserListPainter::paint(QPainter *painter,
|
|||
? cardArtParamsMap->value(userName)
|
||||
: CardArtParams{};
|
||||
|
||||
drawBackground(painter, cardRect, accentColor, selected);
|
||||
drawBackground(painter, cardRect, accentColor, selected, style, hasRole);
|
||||
drawCardArt(painter, rect, cardRight, userName, cardArtCache, params);
|
||||
|
||||
const QRect avatarRect = getAvatarRect(rect);
|
||||
drawAvatar(painter, avatarRect, userName, accentColor, userLevel, userInfo, privLevel, avatarCache);
|
||||
drawStatusRing(painter, avatarRect, online);
|
||||
drawAvatar(painter, avatarRect, userName, accentColor, userLevel, userInfo, privLevel, avatarCache, style);
|
||||
drawStatusRing(painter, avatarRect, online, style);
|
||||
|
||||
const int textX = avatarRect.right() + TextSpacing;
|
||||
drawUserName(painter, option, rect, cardRight, textX, userName, online, selected);
|
||||
drawUserName(painter, option, rect, cardRight, textX, userName, online, style);
|
||||
drawCountryFlag(painter, rect, textX, userInfo);
|
||||
|
||||
const QList<Badge> badges = buildBadges(userLevel, privLevel);
|
||||
drawBadges(painter, option, rect, cardRight, badges, online);
|
||||
drawBadges(painter, option, rect, cardRight, badges, online, style);
|
||||
|
||||
painter->restore();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
#include <QColor>
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
#include <QPalette>
|
||||
#include <QPixmap>
|
||||
#include <QRect>
|
||||
#include <QSize>
|
||||
|
|
@ -28,13 +29,36 @@ struct CardArtParams
|
|||
class UserListPainter
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* Palette-derived surface colors for the current color scheme. Both the
|
||||
* light and the dark scheme read from the active QPalette so custom
|
||||
* palettes are respected. @c dark only tunes the blend strengths (and
|
||||
* whether the name text keeps its drop shadow).
|
||||
*/
|
||||
struct Style
|
||||
{
|
||||
bool dark = true;
|
||||
QColor cardStart; ///< row fill, left edge (normal)
|
||||
QColor cardEnd; ///< row fill, right edge (normal)
|
||||
QColor base; ///< lightest surface, used for blending accent hues
|
||||
QColor textOnline;
|
||||
QColor textOffline;
|
||||
QColor ringOffline;
|
||||
bool dropShadow = false;
|
||||
};
|
||||
|
||||
static Style resolveStyle(const QPalette &palette, bool dark);
|
||||
/// Linear interpolation: @p t = 0 returns @p a, @p t = 1 returns @p b.
|
||||
static QColor blend(const QColor &a, const QColor &b, qreal t);
|
||||
|
||||
static void paint(QPainter *painter,
|
||||
const QStyleOptionViewItem &option,
|
||||
const QModelIndex &index,
|
||||
const ServerInfo_User &userInfo,
|
||||
const QMap<QString, QPixmap> *avatarCache,
|
||||
const QMap<QString, QPixmap> *cardArtCache,
|
||||
const QMap<QString, CardArtParams> *cardArtParamsMap);
|
||||
const QMap<QString, CardArtParams> *cardArtParamsMap,
|
||||
bool dark);
|
||||
|
||||
static QSize sizeHint();
|
||||
|
||||
|
|
@ -55,7 +79,12 @@ private:
|
|||
|
||||
static QColor getAccentColor(const UserLevelFlags &userLevel, bool online);
|
||||
static int getCardRight(const QStyleOptionViewItem &option, const QRect &rect);
|
||||
static void drawBackground(QPainter *painter, const QRectF &cardRect, const QColor &accentColor, bool selected);
|
||||
static void drawBackground(QPainter *painter,
|
||||
const QRectF &cardRect,
|
||||
const QColor &accentColor,
|
||||
bool selected,
|
||||
const Style &style,
|
||||
bool hasRole);
|
||||
static QRect getAvatarRect(const QRect &rect);
|
||||
static void drawAvatar(QPainter *painter,
|
||||
const QRect &avatarRect,
|
||||
|
|
@ -64,8 +93,9 @@ private:
|
|||
const UserLevelFlags &userLevel,
|
||||
const ServerInfo_User &userInfo,
|
||||
const QString &privLevel,
|
||||
const QMap<QString, QPixmap> *avatarCache);
|
||||
static void drawStatusRing(QPainter *painter, const QRect &avatarRect, bool online);
|
||||
const QMap<QString, QPixmap> *avatarCache,
|
||||
const Style &style);
|
||||
static void drawStatusRing(QPainter *painter, const QRect &avatarRect, bool online, const Style &style);
|
||||
static void drawUserName(QPainter *painter,
|
||||
const QStyleOptionViewItem &option,
|
||||
const QRect &rect,
|
||||
|
|
@ -73,7 +103,7 @@ private:
|
|||
int textX,
|
||||
const QString &userName,
|
||||
bool online,
|
||||
bool selected);
|
||||
const Style &style);
|
||||
static void drawCountryFlag(QPainter *painter, const QRect &rect, int textX, const ServerInfo_User &userInfo);
|
||||
static QList<Badge> buildBadges(const UserLevelFlags &userLevel, const QString &privLevel);
|
||||
static void drawBadges(QPainter *painter,
|
||||
|
|
@ -81,7 +111,8 @@ private:
|
|||
const QRect &rect,
|
||||
int cardRight,
|
||||
const QList<Badge> &badges,
|
||||
bool online);
|
||||
bool online,
|
||||
const Style &style);
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_USER_LIST_PAINTER_H
|
||||
#endif // COCKATRICE_USER_LIST_PAINTER_H
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
#include "user_list_panel_widget.h"
|
||||
|
||||
#include "../../../../client/settings/cache_settings.h"
|
||||
#include "user_list_manager.h"
|
||||
#include "user_list_widget.h"
|
||||
|
||||
#include <QLineEdit>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
||||
#include <libcockatrice/settings/interface_settings.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
// The persisted section keys are the serialization contract with the user's
|
||||
// settings file, so the values must stay stable across versions.
|
||||
QString sectionKey(UserListWidget::Section section)
|
||||
{
|
||||
switch (section) {
|
||||
case UserListWidget::Section::Buddy:
|
||||
return QStringLiteral("buddy");
|
||||
case UserListWidget::Section::Online:
|
||||
return QStringLiteral("online");
|
||||
case UserListWidget::Section::Ignore:
|
||||
return QStringLiteral("ignore");
|
||||
}
|
||||
return {};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
UserListPanelWidget::UserListPanelWidget(TabSupervisor *_tabSupervisor, AbstractClient *_client, QWidget *parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
auto *mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setContentsMargins(0, 0, 0, 0);
|
||||
mainLayout->setSpacing(2);
|
||||
|
||||
searchBar = new QLineEdit(this);
|
||||
searchBar->setClearButtonEnabled(true);
|
||||
mainLayout->addWidget(searchBar);
|
||||
|
||||
userList = new UserListWidget(_tabSupervisor, _client, UserListWidget::RoomList, this);
|
||||
userList->setSectioned(
|
||||
{UserListWidget::Section::Buddy, UserListWidget::Section::Online, UserListWidget::Section::Ignore});
|
||||
mainLayout->addWidget(userList, 1);
|
||||
|
||||
connect(searchBar, &QLineEdit::textChanged, userList, &UserListWidget::setFilterText);
|
||||
|
||||
connect(userList, &UserListWidget::sectionExpanded, this, &UserListPanelWidget::persistExpandedSections);
|
||||
connect(userList, &UserListWidget::openMessageDialog, this, &UserListPanelWidget::openMessageDialog);
|
||||
|
||||
// Restore the persisted expansion state, then apply it to the tree.
|
||||
const QStringList expandedSections = SettingsCache::instance().userInterface().getUserListExpandedSections();
|
||||
for (const UserListWidget::Section section : userList->getSectionIds()) {
|
||||
userList->setSectionExpanded(section, expandedSections.contains(sectionKey(section)));
|
||||
}
|
||||
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
void UserListPanelWidget::bind(UserListManager *manager)
|
||||
{
|
||||
userList->bind(manager);
|
||||
}
|
||||
|
||||
void UserListPanelWidget::persistExpandedSections(UserListWidget::Section section, bool expanded)
|
||||
{
|
||||
const QString key = sectionKey(section);
|
||||
QStringList expandedSections = SettingsCache::instance().userInterface().getUserListExpandedSections();
|
||||
if (expanded) {
|
||||
if (!expandedSections.contains(key)) {
|
||||
expandedSections.append(key);
|
||||
}
|
||||
} else {
|
||||
expandedSections.removeAll(key);
|
||||
}
|
||||
SettingsCache::instance().userInterface().setUserListExpandedSections(expandedSections);
|
||||
}
|
||||
|
||||
void UserListPanelWidget::retranslateUi()
|
||||
{
|
||||
searchBar->setPlaceholderText(tr("Search users..."));
|
||||
userList->retranslateUi();
|
||||
}
|
||||
|
||||
UserListWidget *UserListPanelWidget::getUserList() const
|
||||
{
|
||||
return userList;
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* @file user_list_panel_widget.h
|
||||
* @ingroup Lobby
|
||||
*/
|
||||
|
||||
#ifndef COCKATRICE_USER_LIST_PANEL_WIDGET_H
|
||||
#define COCKATRICE_USER_LIST_PANEL_WIDGET_H
|
||||
|
||||
#include "user_list_widget.h"
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
class AbstractClient;
|
||||
class QLineEdit;
|
||||
class TabSupervisor;
|
||||
class UserListManager;
|
||||
|
||||
/**
|
||||
* A unified user list: a search bar above a single tree whose section headers
|
||||
* (buddy, online, ignored) are inline dividers. The tree owns the scrolling.
|
||||
*/
|
||||
class UserListPanelWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit UserListPanelWidget(TabSupervisor *tabSupervisor, AbstractClient *client, QWidget *parent = nullptr);
|
||||
void bind(UserListManager *manager);
|
||||
void retranslateUi();
|
||||
|
||||
[[nodiscard]] UserListWidget *getUserList() const;
|
||||
|
||||
signals:
|
||||
void openMessageDialog(const QString &userName, bool focus);
|
||||
|
||||
private:
|
||||
void persistExpandedSections(UserListWidget::Section section, bool expanded);
|
||||
|
||||
QLineEdit *searchBar = nullptr;
|
||||
UserListWidget *userList = nullptr;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_USER_LIST_PANEL_WIDGET_H
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -8,6 +8,7 @@
|
|||
#define USERLIST_H
|
||||
|
||||
#include "../../cards/card_info_picture_art_crop_widget.h"
|
||||
#include "../../interface/widgets/server/game_link.h"
|
||||
#include "user_avatar_provider.h"
|
||||
#include "user_card_art_provider.h"
|
||||
#include "user_info_popup.h"
|
||||
|
|
@ -18,9 +19,11 @@
|
|||
#include <QDialog>
|
||||
#include <QGroupBox>
|
||||
#include <QQueue>
|
||||
#include <QSet>
|
||||
#include <QStyledItemDelegate>
|
||||
#include <QTextEdit>
|
||||
#include <QTreeWidgetItem>
|
||||
#include <functional>
|
||||
#include <libcockatrice/network/server/remote/user_level.h>
|
||||
#include <libcockatrice/protocol/pb/moderator_commands.pb.h>
|
||||
|
||||
|
|
@ -36,6 +39,8 @@ class QPlainTextEdit;
|
|||
class Response;
|
||||
class CommandContainer;
|
||||
class UserContextMenu;
|
||||
class UserListWidget;
|
||||
class QShowEvent;
|
||||
|
||||
class BanDialog : public QDialog
|
||||
{
|
||||
|
|
@ -80,7 +85,7 @@ public:
|
|||
[[nodiscard]] QString getWarnID() const;
|
||||
[[nodiscard]] QString getReason() const;
|
||||
[[nodiscard]] int getDeleteMessages() const;
|
||||
void addWarningOption(const QString warning);
|
||||
void addWarningOption(const QString warning, int startingIl = 1);
|
||||
};
|
||||
|
||||
class AdminNotesDialog : public QDialog
|
||||
|
|
@ -102,12 +107,15 @@ public:
|
|||
|
||||
class UserListItemDelegate : public QStyledItemDelegate
|
||||
{
|
||||
QTreeWidget *tree;
|
||||
UserListWidget *owner;
|
||||
const QMap<QString, QPixmap> *avatarCache;
|
||||
const QMap<QString, QPixmap> *cardArtCache;
|
||||
const QMap<QString, CardArtParams> *cardArtParamsMap;
|
||||
|
||||
public:
|
||||
explicit UserListItemDelegate(QObject *const parent,
|
||||
explicit UserListItemDelegate(UserListWidget *owner,
|
||||
QTreeWidget *tree,
|
||||
const QMap<QString, QPixmap> *avatarCache,
|
||||
const QMap<QString, QPixmap> *cardArtCache,
|
||||
const QMap<QString, CardArtParams> *cardArtParamsMap);
|
||||
|
|
@ -146,6 +154,12 @@ public:
|
|||
BuddyList,
|
||||
IgnoreList
|
||||
};
|
||||
enum class Section
|
||||
{
|
||||
Buddy,
|
||||
Online,
|
||||
Ignore
|
||||
};
|
||||
|
||||
private:
|
||||
UserListManager *manager = nullptr;
|
||||
|
|
@ -153,30 +167,75 @@ private:
|
|||
UserCardArtProvider *cardArtProvider = nullptr;
|
||||
QMap<QString, CardArtParams> cardArtParamsMap;
|
||||
// ── Hover popup ───────────────────────────────────────────────────────────
|
||||
UserInfoPopup *m_userInfoPopup = nullptr;
|
||||
QTimer *m_showPopupTimer = nullptr;
|
||||
QTimer *m_hidePopupTimer = nullptr;
|
||||
QString m_hoveredUser;
|
||||
bool m_popupPinned = false;
|
||||
UserInfoPopup *userInfoPopup = nullptr;
|
||||
QTimer *showPopupTimer = nullptr;
|
||||
QTimer *hidePopupTimer = nullptr;
|
||||
QString hoveredUser;
|
||||
bool popupPinned = false;
|
||||
bool bulkLoading = false;
|
||||
bool hasUserInfoPopup = true;
|
||||
std::function<bool(const QString &userName, bool online)> userFilter;
|
||||
|
||||
void showPopupForUser(const QString &userName);
|
||||
/**
|
||||
* Popup functions are anchored on the row, not the user name. In sectioned
|
||||
* mode a user can own several rows (online + buddy), and the popup must
|
||||
* follow the hovered/selected row rather than a lookup by name.
|
||||
*/
|
||||
void showPopupForUser(UserListTWI *item);
|
||||
void hidePopup(bool immediate = false);
|
||||
void positionPopup(const QString &userName);
|
||||
void positionPopup(UserListTWI *item);
|
||||
void connectPopupSignals();
|
||||
/** True when @p widget is the tree, the popup or an open menu. */
|
||||
bool isPressInsideListUi(const QWidget *widget) const;
|
||||
void clearSelectionAndClosePopup();
|
||||
bool isItemNearViewport(const UserListTWI *item) const;
|
||||
void requestVisibleItemResources();
|
||||
|
||||
// Sectioned mode (single tree with inline dividers)
|
||||
bool sectioned = false;
|
||||
QList<Section> sectionIds;
|
||||
QMap<Section, QTreeWidgetItem *> sectionItems;
|
||||
// One row per (section, user): a user that is online AND a buddy appears in
|
||||
// both the "Online" and the "Buddies" sections, so the same user can own
|
||||
// several rows, each hanging off its section's divider.
|
||||
QMap<Section, QMap<QString, UserListTWI *>> sectionUsers;
|
||||
QSet<Section> expandedSections;
|
||||
void createSectionItems();
|
||||
QTreeWidgetItem *createSectionItem(Section section);
|
||||
[[nodiscard]] QString sectionTitle(Section section) const;
|
||||
void updateSectionDivider(Section section);
|
||||
void handleSectionExpansion(QTreeWidgetItem *item, bool expanded);
|
||||
void setExpandedProgrammatically(QTreeWidgetItem *item, bool expanded);
|
||||
void handleOnlineChange(const ServerInfo_User &user);
|
||||
void handleOnlineChangeLeft(const QString &userName);
|
||||
void handleListAdd(Section section, const ServerInfo_User &user);
|
||||
void handleListRemove(Section section, const QString &userName);
|
||||
/** Creates or updates the row for @p user in @p section. */
|
||||
UserListTWI *ensureSectionMembership(Section section, const ServerInfo_User &user, bool online);
|
||||
/** Removes and deletes the row for @p userName in @p section. */
|
||||
bool dropSectionMembership(Section section, const QString &userName);
|
||||
/** Sorts, refilters and repaints after a sectioned mode mutation. */
|
||||
void finishSectionedMutation();
|
||||
void updateCardArtParams(const ServerInfo_User &user, const QString &userName);
|
||||
void processUserInfo(Section section, const ServerInfo_User &user, bool online);
|
||||
|
||||
QMap<QString, UserListTWI *> users;
|
||||
TabSupervisor *tabSupervisor;
|
||||
AbstractClient *client;
|
||||
UserListType type;
|
||||
QTreeWidget *userTree;
|
||||
QTreeWidget *userTree = nullptr;
|
||||
UserListItemDelegate *itemDelegate;
|
||||
UserContextMenu *userContextMenu;
|
||||
int onlineCount;
|
||||
QString titleStr;
|
||||
QString filterText;
|
||||
bool showTitle = true;
|
||||
void updateCount();
|
||||
void applyFilter();
|
||||
void refreshPopupButtons(const QString &userName);
|
||||
private slots:
|
||||
void userClicked(QTreeWidgetItem *item, int column);
|
||||
void refreshVisibleUserHeader(const QString &name);
|
||||
signals:
|
||||
void openMessageDialog(const QString &userName, bool focus);
|
||||
void addBuddy(const QString &userName);
|
||||
|
|
@ -184,29 +243,60 @@ signals:
|
|||
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);
|
||||
/** Dialog mode: the current row changed; empty string means no user row. */
|
||||
void currentUserChanged(const QString &userName);
|
||||
/** The set of visible rows changed (filter, search or a live mutation). */
|
||||
void userListChanged();
|
||||
|
||||
public:
|
||||
UserListWidget(TabSupervisor *_tabSupervisor,
|
||||
AbstractClient *_client,
|
||||
UserListType _type,
|
||||
QWidget *parent = nullptr);
|
||||
QWidget *parent = nullptr,
|
||||
bool hasUserInfoPopup = true);
|
||||
~UserListWidget() override;
|
||||
void bind(UserListManager *mgr);
|
||||
void applyDisplayMode();
|
||||
void beginBulkLoad();
|
||||
void endBulkLoad();
|
||||
bool eventFilter(QObject *obj, QEvent *event) override;
|
||||
void retranslateUi();
|
||||
void rebuild();
|
||||
void processUserInfo(const ServerInfo_User &user, bool online);
|
||||
bool deleteUser(const QString &userName);
|
||||
void setUserOnline(const QString &userName, bool online);
|
||||
void setFilterText(const QString &text);
|
||||
void setShowTitle(bool showTitle);
|
||||
void setSectioned(const QList<Section> &ids);
|
||||
void setSectionExpanded(Section section, bool expanded);
|
||||
/** Dialog mode: rows that fail the predicate are never shown. */
|
||||
void setUserFilter(std::function<bool(const QString &userName, bool online)> filter)
|
||||
{
|
||||
userFilter = std::move(filter);
|
||||
}
|
||||
[[nodiscard]] int visibleUserRowCount() const;
|
||||
[[nodiscard]] bool getHasUserInfoPopup() const
|
||||
{
|
||||
return hasUserInfoPopup;
|
||||
}
|
||||
[[nodiscard]] const QList<Section> &getSectionIds() const
|
||||
{
|
||||
return sectionIds;
|
||||
}
|
||||
[[nodiscard]] const QMap<QString, UserListTWI *> &getUsers() const
|
||||
{
|
||||
return users;
|
||||
}
|
||||
void showContextMenu(const QPoint &pos, const QModelIndex &index);
|
||||
void sortItems();
|
||||
void setGameInviteLinkProvider(std::function<QList<GameInviteOption>()> provider);
|
||||
|
||||
protected:
|
||||
void hideEvent(QHideEvent *e) override;
|
||||
void showEvent(QShowEvent *e) override;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -5,13 +5,17 @@
|
|||
#include "../../client/settings/card_counter_settings.h"
|
||||
#include "../../palette_editor/palette_editor_dialog.h"
|
||||
#include "../dialogs/override_printing_warning.h"
|
||||
#include "../general/home_tab_button_color.h"
|
||||
#include "../interface/theme_manager.h"
|
||||
#include "../interface/widgets/general/background_sources.h"
|
||||
#include "../playmat/playmat_collection_dialog.h"
|
||||
#include "../playmat/playmat_settings_dialog.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QColorDialog>
|
||||
#include <QDesktopServices>
|
||||
#include <QGridLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QMessageBox>
|
||||
#include <QStyleFactory>
|
||||
#include <QTimer>
|
||||
|
|
@ -128,6 +132,14 @@ AppearanceSettingsPage::AppearanceSettingsPage()
|
|||
connect(&homeTabDisplayCardNameCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.appearance(),
|
||||
&AppearanceSettings::setHomeTabDisplayCardName);
|
||||
|
||||
for (const auto &entry : HomeTabButtonColor::all()) {
|
||||
homeTabButtonColorSourceBox.addItem(QObject::tr(entry.trKey));
|
||||
}
|
||||
|
||||
homeTabButtonColorSourceBox.setCurrentIndex(settings.appearance().getHomeTabButtonColorSourceIndex());
|
||||
connect(&homeTabButtonColorSourceBox, QOverload<int>::of(&QComboBox::currentIndexChanged), &settings.appearance(),
|
||||
&AppearanceSettings::setHomeTabButtonColorSourceIndex);
|
||||
|
||||
updateHomeTabSettingsVisibility();
|
||||
|
||||
auto *homeTabGrid = new QGridLayout;
|
||||
|
|
@ -136,6 +148,8 @@ AppearanceSettingsPage::AppearanceSettingsPage()
|
|||
homeTabGrid->addWidget(&homeTabBackgroundShuffleFrequencyLabel, 1, 0);
|
||||
homeTabGrid->addWidget(&homeTabBackgroundShuffleFrequencySpinBox, 1, 1);
|
||||
homeTabGrid->addWidget(&homeTabDisplayCardNameCheckBox, 2, 0, 1, 2);
|
||||
homeTabGrid->addWidget(&homeTabButtonColorSourceLabel, 3, 0);
|
||||
homeTabGrid->addWidget(&homeTabButtonColorSourceBox, 3, 1);
|
||||
|
||||
homeTabGroupBox = new QGroupBox;
|
||||
homeTabGroupBox->setLayout(homeTabGrid);
|
||||
|
|
@ -325,10 +339,52 @@ AppearanceSettingsPage::AppearanceSettingsPage()
|
|||
tableGroupBox = new QGroupBox;
|
||||
tableGroupBox->setLayout(tableGrid);
|
||||
|
||||
// Playmat settings
|
||||
playmatVisibilityCombo.addItem(tr("Show all playmats"), PlaymatVisibilityAll);
|
||||
playmatVisibilityCombo.addItem(tr("Show own playmat only"), PlaymatVisibilityOwnOnly);
|
||||
playmatVisibilityCombo.addItem(tr("Don't use playmats"), PlaymatVisibilityNone);
|
||||
int visIdx = playmatVisibilityCombo.findData(settings.userInterface().getPlaymatVisibility());
|
||||
if (visIdx >= 0) {
|
||||
playmatVisibilityCombo.setCurrentIndex(visIdx);
|
||||
}
|
||||
connect(&playmatVisibilityCombo, qOverload<int>(&QComboBox::currentIndexChanged), this, [this](int index) {
|
||||
SettingsCache::instance().userInterface().setPlaymatVisibility(playmatVisibilityCombo.itemData(index).toInt());
|
||||
});
|
||||
playmatVisibilityLabel.setBuddy(&playmatVisibilityCombo);
|
||||
|
||||
// Playmat mode: Override / Fallback / Deck-only
|
||||
playmatModeCombo.addItem(tr("Override deck playmat"), PlaymatModeOverrideDeck);
|
||||
playmatModeCombo.addItem(tr("Fallback if deck has none"), PlaymatModeFallback);
|
||||
playmatModeCombo.addItem(tr("Deck only, ignore collection"), PlaymatModeDeckOnly);
|
||||
int modeIdx = playmatModeCombo.findData(settings.userInterface().getPlaymatMode());
|
||||
if (modeIdx >= 0) {
|
||||
playmatModeCombo.setCurrentIndex(modeIdx);
|
||||
}
|
||||
connect(&playmatModeCombo, qOverload<int>(&QComboBox::currentIndexChanged), this, [this](int index) {
|
||||
SettingsCache::instance().userInterface().setPlaymatMode(playmatModeCombo.itemData(index).toInt());
|
||||
});
|
||||
playmatModeLabel.setBuddy(&playmatModeCombo);
|
||||
|
||||
// User-level playmat settings: fallback collection.
|
||||
connect(&playmatDefaultEditButton, &QPushButton::clicked, this,
|
||||
&AppearanceSettingsPage::openPlaymatCollectionDialog);
|
||||
|
||||
auto *playmatGrid = new QGridLayout;
|
||||
playmatGrid->addWidget(&playmatVisibilityLabel, 0, 0, 1, 1);
|
||||
playmatGrid->addWidget(&playmatVisibilityCombo, 0, 1, 1, 1);
|
||||
playmatGrid->addWidget(&playmatModeLabel, 1, 0, 1, 1);
|
||||
playmatGrid->addWidget(&playmatModeCombo, 1, 1, 1, 1);
|
||||
playmatGrid->addWidget(&playmatDefaultLabel, 2, 0, 1, 1);
|
||||
playmatGrid->addWidget(&playmatDefaultEditButton, 2, 1, 1, 1);
|
||||
|
||||
playmatGroupBox = new QGroupBox;
|
||||
playmatGroupBox->setLayout(playmatGrid);
|
||||
|
||||
// putting it all together
|
||||
auto *mainLayout = new QVBoxLayout;
|
||||
mainLayout->addWidget(themeGroupBox);
|
||||
mainLayout->addWidget(homeTabGroupBox);
|
||||
mainLayout->addWidget(playmatGroupBox);
|
||||
mainLayout->addWidget(stylingGroupBox);
|
||||
mainLayout->addWidget(menuGroupBox);
|
||||
mainLayout->addWidget(printingsGroupBox);
|
||||
|
|
@ -375,8 +431,8 @@ void AppearanceSettingsPage::editPalette()
|
|||
|
||||
void AppearanceSettingsPage::updateHomeTabSettingsVisibility()
|
||||
{
|
||||
bool visible = SettingsCache::instance().appearance().getHomeTabBackgroundSource() !=
|
||||
BackgroundSources::toId(BackgroundSources::Theme);
|
||||
QString sourceId = SettingsCache::instance().appearance().getHomeTabBackgroundSource();
|
||||
bool visible = BackgroundSources::fromId(sourceId) != BackgroundSources::Theme;
|
||||
|
||||
homeTabBackgroundShuffleFrequencyLabel.setVisible(visible);
|
||||
homeTabBackgroundShuffleFrequencySpinBox.setVisible(visible);
|
||||
|
|
@ -431,6 +487,12 @@ void AppearanceSettingsPage::cardViewExpandedRowsMaxChanged(int value)
|
|||
}
|
||||
}
|
||||
|
||||
void AppearanceSettingsPage::openPlaymatCollectionDialog()
|
||||
{
|
||||
PlaymatCollectionDialog dialog(this);
|
||||
dialog.exec();
|
||||
}
|
||||
|
||||
void AppearanceSettingsPage::retranslateUi()
|
||||
{
|
||||
themeGroupBox->setTitle(tr("Theme settings"));
|
||||
|
|
@ -446,6 +508,9 @@ void AppearanceSettingsPage::retranslateUi()
|
|||
homeTabBackgroundShuffleFrequencyLabel.setText(tr("Home tab background shuffle frequency:"));
|
||||
homeTabBackgroundShuffleFrequencySpinBox.setSpecialValueText(tr("Disabled"));
|
||||
homeTabDisplayCardNameCheckBox.setText(tr("Display card name of background in bottom right"));
|
||||
homeTabButtonColorSourceLabel.setText(tr("Home tab button color:"));
|
||||
homeTabButtonColorSourceBox.setToolTip(
|
||||
tr("Automatic: extract from background if present, otherwise use theme default"));
|
||||
|
||||
stylingGroupBox->setTitle(tr("Styling settings"));
|
||||
styleUserListCheckBox.setText(tr("Style user list"));
|
||||
|
|
@ -489,4 +554,9 @@ void AppearanceSettingsPage::retranslateUi()
|
|||
tableGroupBox->setTitle(tr("Table grid layout"));
|
||||
invertVerticalCoordinateCheckBox.setText(tr("Invert vertical coordinate"));
|
||||
minPlayersForMultiColumnLayoutLabel.setText(tr("Minimum player count for multi-column layout:"));
|
||||
}
|
||||
playmatGroupBox->setTitle(tr("Playmat settings"));
|
||||
playmatVisibilityLabel.setText(tr("Playmat visibility:"));
|
||||
playmatModeLabel.setText(tr("Default collection behavior:"));
|
||||
playmatDefaultLabel.setText(tr("Default playmat collection:"));
|
||||
playmatDefaultEditButton.setText(tr("Edit..."));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ private slots:
|
|||
|
||||
void cardViewInitialRowsMaxChanged(int value);
|
||||
void cardViewExpandedRowsMaxChanged(int value);
|
||||
void openPlaymatCollectionDialog();
|
||||
|
||||
private:
|
||||
QLabel themeLabel;
|
||||
|
|
@ -34,11 +35,15 @@ private:
|
|||
QLabel styleComboLabel;
|
||||
QComboBox styleCombo;
|
||||
QPushButton editPaletteButton;
|
||||
|
||||
QLabel homeTabBackgroundSourceLabel;
|
||||
QComboBox homeTabBackgroundSourceBox;
|
||||
QLabel homeTabBackgroundShuffleFrequencyLabel;
|
||||
QSpinBox homeTabBackgroundShuffleFrequencySpinBox;
|
||||
QCheckBox homeTabDisplayCardNameCheckBox;
|
||||
QLabel homeTabButtonColorSourceLabel;
|
||||
QComboBox homeTabButtonColorSourceBox;
|
||||
|
||||
QCheckBox styleUserListCheckBox;
|
||||
QCheckBox showShortcutsCheckBox;
|
||||
QCheckBox showGameSelectorFilterToolbarCheckBox;
|
||||
|
|
@ -59,6 +64,12 @@ private:
|
|||
QCheckBox horizontalHandCheckBox;
|
||||
QCheckBox leftJustifiedHandCheckBox;
|
||||
QCheckBox invertVerticalCoordinateCheckBox;
|
||||
QLabel playmatVisibilityLabel;
|
||||
QComboBox playmatVisibilityCombo;
|
||||
QLabel playmatModeLabel;
|
||||
QComboBox playmatModeCombo;
|
||||
QLabel playmatDefaultLabel;
|
||||
QPushButton playmatDefaultEditButton;
|
||||
QGroupBox *themeGroupBox;
|
||||
QGroupBox *homeTabGroupBox;
|
||||
QGroupBox *stylingGroupBox;
|
||||
|
|
@ -67,6 +78,7 @@ private:
|
|||
QGroupBox *cardsGroupBox;
|
||||
QGroupBox *cardLayoutGroupBox;
|
||||
QGroupBox *handGroupBox;
|
||||
QGroupBox *playmatGroupBox;
|
||||
QGroupBox *tableGroupBox;
|
||||
QGroupBox *cardCountersGroupBox;
|
||||
QList<QLabel *> cardCounterNames;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../main.h"
|
||||
#include "../server/user/user_info_connection.h"
|
||||
#include "update/client/release_channel.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
|
|
@ -11,6 +12,7 @@
|
|||
#include <QTranslator>
|
||||
#include <libcockatrice/settings/paths_settings.h>
|
||||
#include <libcockatrice/settings/personal_settings.h>
|
||||
#include <libcockatrice/settings/tabs_settings.h>
|
||||
#include <libcockatrice/settings/updates_settings.h>
|
||||
#include <libcockatrice/utility/macros.h>
|
||||
|
||||
|
|
@ -121,8 +123,61 @@ GeneralSettingsPage::GeneralSettingsPage()
|
|||
|
||||
connect(&showTipsOnStartup, &QCheckBox::clicked, &settings.personal(), &PersonalSettings::setShowTipsOnStartup);
|
||||
|
||||
// startup destination
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
startupTabSelector.addItem(""); // texts set in retranslateUi
|
||||
}
|
||||
startupTabSelector.setCurrentIndex(settings.tabs().getStartupTabIndex());
|
||||
|
||||
connect(&startupTabSelector, qOverload<int>(&QComboBox::currentIndexChanged), &settings.tabs(),
|
||||
&TabsSettings::setStartupTabIndex);
|
||||
connect(&startupTabSelector, qOverload<int>(&QComboBox::currentIndexChanged), this,
|
||||
&GeneralSettingsPage::updateStartupServerControlsVisibility);
|
||||
|
||||
const QString savedHost = settings.tabs().getStartupServerHost();
|
||||
const QString savedPort = settings.tabs().getStartupServerPort();
|
||||
int startupServerIndex = -1;
|
||||
UserConnection_Information uci;
|
||||
for (const auto &savedServer : uci.getServerInfo()) {
|
||||
const UserConnection_Information &info = savedServer.second;
|
||||
const QString saveName = info.getSaveName();
|
||||
if (saveName.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
startupServerSelector.addItem(saveName, QVariantList{info.getServer(), info.getPort()});
|
||||
if (startupServerIndex == -1 && info.getServer() == savedHost && info.getPort() == savedPort) {
|
||||
startupServerIndex = startupServerSelector.count() - 1;
|
||||
}
|
||||
}
|
||||
startupServerSelector.setCurrentIndex(startupServerIndex);
|
||||
|
||||
connect(&startupServerSelector, qOverload<int>(&QComboBox::currentIndexChanged), this, [this](int index) {
|
||||
const QVariantList serverInfo = startupServerSelector.itemData(index).toList();
|
||||
if (serverInfo.size() != 2) {
|
||||
return;
|
||||
}
|
||||
TabsSettings &tabs = SettingsCache::instance().tabs();
|
||||
tabs.setStartupServerHost(serverInfo[0].toString());
|
||||
tabs.setStartupServerPort(serverInfo[1].toString());
|
||||
});
|
||||
|
||||
startupRoomNameEdit = new QLineEdit(settings.tabs().getStartupRoomName());
|
||||
// Default (Expanding) would stretch the whole controls column when this row becomes visible,
|
||||
// so size it like the combo boxes instead: fills the column, never widens it.
|
||||
startupRoomNameEdit->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
|
||||
connect(startupRoomNameEdit, &QLineEdit::editingFinished, this,
|
||||
[this] { SettingsCache::instance().tabs().setStartupRoomName(startupRoomNameEdit->text().trimmed()); });
|
||||
|
||||
auto *startupGrid = new QGridLayout;
|
||||
startupGrid->addWidget(&showTipsOnStartup, 0, 0, 1, 2);
|
||||
startupGrid->addWidget(&startupTabLabel, 1, 0);
|
||||
startupGrid->addWidget(&startupTabSelector, 1, 1);
|
||||
startupGrid->addWidget(&startupServerLabel, 2, 0);
|
||||
startupGrid->addWidget(&startupServerSelector, 2, 1);
|
||||
startupGrid->addWidget(&startupRoomLabel, 3, 0);
|
||||
startupGrid->addWidget(startupRoomNameEdit, 3, 1);
|
||||
|
||||
updateStartupServerControlsVisibility();
|
||||
|
||||
startupGroupBox = new QGroupBox;
|
||||
startupGroupBox->setLayout(startupGrid);
|
||||
|
|
@ -357,6 +412,17 @@ void GeneralSettingsPage::languageBoxChanged(int index)
|
|||
SettingsCache::instance().personal().setLang(languageBox.itemData(index).toString());
|
||||
}
|
||||
|
||||
void GeneralSettingsPage::updateStartupServerControlsVisibility()
|
||||
{
|
||||
const int index = startupTabSelector.currentIndex();
|
||||
const bool serverNeeded = index == StartupTab::StartupTabServer || index == StartupTab::StartupTabServerRoom;
|
||||
const bool roomNeeded = index == StartupTab::StartupTabServerRoom;
|
||||
startupServerLabel.setVisible(serverNeeded);
|
||||
startupServerSelector.setVisible(serverNeeded);
|
||||
startupRoomLabel.setVisible(roomNeeded);
|
||||
startupRoomNameEdit->setVisible(roomNeeded);
|
||||
}
|
||||
|
||||
void GeneralSettingsPage::retranslateUi()
|
||||
{
|
||||
languageGroupBox->setTitle(tr("Language settings"));
|
||||
|
|
@ -393,6 +459,20 @@ void GeneralSettingsPage::retranslateUi()
|
|||
updateNotificationCheckBox.setText(tr("Notify if a feature supported by the server is missing in my client"));
|
||||
newVersionOracleCheckBox.setText(tr("Automatically run Oracle when running a new version of Cockatrice"));
|
||||
showTipsOnStartup.setText(tr("Show tips on startup"));
|
||||
startupTabLabel.setText(tr("Startup tab:"));
|
||||
startupTabSelector.setItemText(StartupTab::StartupTabHome, tr("Home"));
|
||||
startupTabSelector.setItemText(StartupTab::StartupTabVisualDeckStorage, tr("Visual Deck Storage"));
|
||||
startupTabSelector.setItemText(StartupTab::StartupTabDeckStorage, tr("Deck Storage"));
|
||||
startupTabSelector.setItemText(StartupTab::StartupTabReplays, tr("Game Replays"));
|
||||
startupTabSelector.setItemText(StartupTab::StartupTabDeckEditor, tr("Deck Editor"));
|
||||
startupTabSelector.setItemText(StartupTab::StartupTabVisualDeckEditor, tr("Visual Deck Editor"));
|
||||
startupTabSelector.setItemText(StartupTab::StartupTabServer, tr("Server"));
|
||||
startupTabSelector.setItemText(StartupTab::StartupTabServerRoom, tr("Server Room"));
|
||||
startupTabSelector.setToolTip(
|
||||
tr("The tab shown when Cockatrice starts. If the chosen tab is not open yet, it is opened."));
|
||||
startupServerLabel.setText(tr("Server:"));
|
||||
startupRoomLabel.setText(tr("Room:"));
|
||||
startupRoomNameEdit->setPlaceholderText(tr("Room name"));
|
||||
resetAllPathsButton->setText(tr("Reset all paths"));
|
||||
|
||||
const auto &settings = SettingsCache::instance();
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ public:
|
|||
GeneralSettingsPage();
|
||||
void retranslateUi() override;
|
||||
|
||||
static QStringList findQmFiles();
|
||||
static QString languageName(const QString &lang);
|
||||
|
||||
private slots:
|
||||
void deckPathButtonClicked();
|
||||
void filtersPathButtonClicked();
|
||||
|
|
@ -30,11 +33,9 @@ private slots:
|
|||
void tokenDatabasePathButtonClicked();
|
||||
void resetAllPathsClicked();
|
||||
void languageBoxChanged(int index);
|
||||
void updateStartupServerControlsVisibility();
|
||||
|
||||
private:
|
||||
QStringList findQmFiles();
|
||||
QString languageName(const QString &lang);
|
||||
|
||||
QGroupBox *languageGroupBox;
|
||||
QGroupBox *versionGroupBox;
|
||||
QGroupBox *cardDatabaseGroupBox;
|
||||
|
|
@ -71,6 +72,12 @@ private:
|
|||
QLabel updateReleaseChannelLabel;
|
||||
QLabel advertiseTranslationPageLabel;
|
||||
QCheckBox showTipsOnStartup;
|
||||
QLabel startupTabLabel;
|
||||
QComboBox startupTabSelector;
|
||||
QLabel startupServerLabel;
|
||||
QComboBox startupServerSelector;
|
||||
QLabel startupRoomLabel;
|
||||
QLineEdit *startupRoomNameEdit;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_GENERAL_SETTINGS_PAGE_H
|
||||
|
|
|
|||
|
|
@ -116,8 +116,29 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage()
|
|||
connect(&tapAnimationCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().cardsDisplay(),
|
||||
&CardsDisplaySettings::setTapAnimation);
|
||||
|
||||
arrowDrawAnimationCheckBox.setChecked(SettingsCache::instance().cardsDisplay().getArrowDrawAnimation());
|
||||
connect(&arrowDrawAnimationCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().cardsDisplay(),
|
||||
&CardsDisplaySettings::setArrowDrawAnimation);
|
||||
|
||||
lifeCounterAnimationsCheckBox.setChecked(
|
||||
SettingsCache::instance().userInterface().getLifeCounterAnimationsEnabled());
|
||||
connect(&lifeCounterAnimationsCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(),
|
||||
&InterfaceSettings::setLifeCounterAnimationsEnabled);
|
||||
|
||||
battlefieldFlashCheckBox.setChecked(SettingsCache::instance().userInterface().getBattlefieldFlashEnabled());
|
||||
connect(&battlefieldFlashCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(),
|
||||
&InterfaceSettings::setBattlefieldFlashEnabled);
|
||||
|
||||
connect(&enableAllAnimationsButton, &QPushButton::clicked, this, &UserInterfaceSettingsPage::enableAllAnimations);
|
||||
connect(&disableAllAnimationsButton, &QPushButton::clicked, this, &UserInterfaceSettingsPage::disableAllAnimations);
|
||||
|
||||
auto *animationGrid = new QGridLayout;
|
||||
animationGrid->addWidget(&tapAnimationCheckBox, 0, 0);
|
||||
animationGrid->addWidget(&enableAllAnimationsButton, 0, 0);
|
||||
animationGrid->addWidget(&disableAllAnimationsButton, 0, 1);
|
||||
animationGrid->addWidget(&tapAnimationCheckBox, 1, 0);
|
||||
animationGrid->addWidget(&arrowDrawAnimationCheckBox, 2, 0);
|
||||
animationGrid->addWidget(&lifeCounterAnimationsCheckBox, 3, 0);
|
||||
animationGrid->addWidget(&battlefieldFlashCheckBox, 4, 0);
|
||||
|
||||
animationGroupBox = new QGroupBox;
|
||||
animationGroupBox->setLayout(animationGrid);
|
||||
|
|
@ -162,6 +183,13 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage()
|
|||
connect(&defaultDeckEditorTypeSelector, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
&SettingsCache::instance().deckEditor(), &DeckEditorSettings::setDefaultDeckEditorType);
|
||||
|
||||
vdeStartupTabSelector.addItem(""); // these will be set in retranslateUI
|
||||
vdeStartupTabSelector.addItem("");
|
||||
vdeStartupTabSelector.addItem("");
|
||||
vdeStartupTabSelector.setCurrentIndex(SettingsCache::instance().deckEditor().getVdeStartupTab());
|
||||
connect(&vdeStartupTabSelector, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
&SettingsCache::instance().deckEditor(), &DeckEditorSettings::setVdeStartupTab);
|
||||
|
||||
commanderSpellbookIntegrationUseOfficialBracketNamesExplainer.setText("?");
|
||||
commanderSpellbookIntegrationUseOfficialBracketNamesExplainer.setAutoRaise(true);
|
||||
commanderSpellbookIntegrationUseOfficialBracketNamesExplainer.setEnabled(false);
|
||||
|
|
@ -221,10 +249,12 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage()
|
|||
deckEditorGrid->addWidget(&visualDeckStoragePromptForConversionSelector, 3, 1);
|
||||
deckEditorGrid->addWidget(&defaultDeckEditorTypeLabel, 4, 0);
|
||||
deckEditorGrid->addWidget(&defaultDeckEditorTypeSelector, 4, 1);
|
||||
deckEditorGrid->addWidget(&commanderSpellbookIntegrationEnabledLabel, 5, 0);
|
||||
deckEditorGrid->addWidget(&commanderSpellbookIntegrationEnabledSelector, 5, 1);
|
||||
deckEditorGrid->addWidget(labelWidget, 6, 0);
|
||||
deckEditorGrid->addWidget(&commanderSpellbookIntegrationBracketNamingSelector, 6, 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);
|
||||
|
||||
deckEditorGroupBox = new QGroupBox;
|
||||
deckEditorGroupBox->setLayout(deckEditorGrid);
|
||||
|
|
@ -268,6 +298,22 @@ void UserInterfaceSettingsPage::setNotificationEnabled(QT_STATE_CHANGED_T i)
|
|||
}
|
||||
}
|
||||
|
||||
void UserInterfaceSettingsPage::enableAllAnimations()
|
||||
{
|
||||
tapAnimationCheckBox.setChecked(true);
|
||||
arrowDrawAnimationCheckBox.setChecked(true);
|
||||
lifeCounterAnimationsCheckBox.setChecked(true);
|
||||
battlefieldFlashCheckBox.setChecked(true);
|
||||
}
|
||||
|
||||
void UserInterfaceSettingsPage::disableAllAnimations()
|
||||
{
|
||||
tapAnimationCheckBox.setChecked(false);
|
||||
arrowDrawAnimationCheckBox.setChecked(false);
|
||||
lifeCounterAnimationsCheckBox.setChecked(false);
|
||||
battlefieldFlashCheckBox.setChecked(false);
|
||||
}
|
||||
|
||||
void UserInterfaceSettingsPage::updateCommanderSpellbookUiState()
|
||||
{
|
||||
const int mode = SettingsCache::instance().deckEditor().getCommanderSpellbookIntegrationEnabled();
|
||||
|
|
@ -310,7 +356,12 @@ void UserInterfaceSettingsPage::retranslateUi()
|
|||
specNotificationsEnabledCheckBox.setText(tr("Notify in the taskbar for game events while you are spectating"));
|
||||
buddyConnectNotificationsEnabledCheckBox.setText(tr("Notify in the taskbar when users in your buddy list connect"));
|
||||
animationGroupBox->setTitle(tr("Animation settings"));
|
||||
enableAllAnimationsButton.setText(tr("&Enable all animations"));
|
||||
disableAllAnimationsButton.setText(tr("&Disable all animations"));
|
||||
tapAnimationCheckBox.setText(tr("&Tap/untap animation"));
|
||||
arrowDrawAnimationCheckBox.setText(tr("&Arrow draw animation"));
|
||||
lifeCounterAnimationsCheckBox.setText(tr("Life counter flash"));
|
||||
battlefieldFlashCheckBox.setText(tr("Battlefield flash on damage"));
|
||||
deckEditorGroupBox->setTitle(tr("Deck editor/storage settings"));
|
||||
openDeckInNewTabCheckBox.setText(tr("Open deck in new tab by default"));
|
||||
visualDeckStorageInGameCheckBox.setText(tr("Use visual deck storage in game lobby"));
|
||||
|
|
@ -326,6 +377,12 @@ void UserInterfaceSettingsPage::retranslateUi()
|
|||
defaultDeckEditorTypeLabel.setText(tr("Default deck editor type"));
|
||||
defaultDeckEditorTypeSelector.setItemText(TabSupervisor::ClassicDeckEditor, tr("Classic Deck Editor"));
|
||||
defaultDeckEditorTypeSelector.setItemText(TabSupervisor::VisualDeckEditor, tr("Visual Deck Editor"));
|
||||
vdeStartupTabLabel.setText(tr("Visual deck editor startup tab"));
|
||||
vdeStartupTabSelector.setItemText(VdeStartupTabContext, tr("Context"));
|
||||
vdeStartupTabSelector.setItemText(VdeStartupTabDeckDisplay, tr("Deck display"));
|
||||
vdeStartupTabSelector.setItemText(VdeStartupTabDatabaseDisplay, tr("Database display"));
|
||||
vdeStartupTabSelector.setToolTip(
|
||||
tr("Context mode: New decks open on the database display, existing decks open on the deck view."));
|
||||
|
||||
commanderSpellbookIntegrationEnabledLabel.setText(
|
||||
tr("CommanderSpellbook integration to estimate commander bracket"));
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
#include <QComboBox>
|
||||
#include <QGroupBox>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QSpinBox>
|
||||
#include <QToolButton>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
|
|
@ -17,6 +18,8 @@ class UserInterfaceSettingsPage : public AbstractSettingsPage
|
|||
Q_OBJECT
|
||||
private slots:
|
||||
void setNotificationEnabled(QT_STATE_CHANGED_T);
|
||||
void enableAllAnimations();
|
||||
void disableAllAnimations();
|
||||
void updateCommanderSpellbookUiState();
|
||||
|
||||
private:
|
||||
|
|
@ -34,7 +37,12 @@ private:
|
|||
QCheckBox showTotalSelectionCountCheckBox;
|
||||
QCheckBox useTearOffMenusCheckBox;
|
||||
QCheckBox keepGameChatFocusCheckBox;
|
||||
QPushButton enableAllAnimationsButton;
|
||||
QPushButton disableAllAnimationsButton;
|
||||
QCheckBox tapAnimationCheckBox;
|
||||
QCheckBox arrowDrawAnimationCheckBox;
|
||||
QCheckBox lifeCounterAnimationsCheckBox;
|
||||
QCheckBox battlefieldFlashCheckBox;
|
||||
QCheckBox openDeckInNewTabCheckBox;
|
||||
QLabel visualDeckStoragePromptForConversionLabel;
|
||||
QComboBox visualDeckStoragePromptForConversionSelector;
|
||||
|
|
@ -42,6 +50,8 @@ private:
|
|||
QCheckBox visualDeckStorageSelectionAnimationCheckBox;
|
||||
QLabel defaultDeckEditorTypeLabel;
|
||||
QComboBox defaultDeckEditorTypeSelector;
|
||||
QLabel vdeStartupTabLabel;
|
||||
QComboBox vdeStartupTabSelector;
|
||||
QLabel commanderSpellbookIntegrationEnabledLabel;
|
||||
QComboBox commanderSpellbookIntegrationEnabledSelector;
|
||||
QLabel commanderSpellbookIntegrationUseOfficialBracketNamesLabel;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
#ifndef TAB_GENERIC_DECK_EDITOR_H
|
||||
#define TAB_GENERIC_DECK_EDITOR_H
|
||||
|
||||
#include "../../deck_loader/deck_loader.h"
|
||||
#include "../interface/widgets/deck_editor/deck_editor_card_database_dock_widget.h"
|
||||
#include "../interface/widgets/deck_editor/deck_editor_card_info_dock_widget.h"
|
||||
#include "../interface/widgets/deck_editor/deck_editor_database_display_widget.h"
|
||||
|
|
@ -263,12 +264,12 @@ protected slots:
|
|||
/** @brief Handles dock close events. */
|
||||
void closeEvent(QCloseEvent *event) override;
|
||||
|
||||
private:
|
||||
/** @brief Sets the deck for this tab.
|
||||
* @param _deck The deck object.
|
||||
*/
|
||||
virtual void setDeck(const LoadedDeck &_deck);
|
||||
|
||||
private:
|
||||
/** @brief Helper for editing decks from the clipboard. */
|
||||
void editDeckInClipboard(bool annotated);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@
|
|||
|
||||
#include "../../../../../client/settings/cache_settings.h"
|
||||
#include "../../../cards/additional_info/mana_symbol_widget.h"
|
||||
#include "../../../utility/completer_utils.h"
|
||||
#include "../../tab_supervisor.h"
|
||||
#include "api_response/archidekt_deck_listing_api_response.h"
|
||||
#include "display/archidekt_api_response_deck_display_widget.h"
|
||||
#include "display/archidekt_api_response_deck_listings_display_widget.h"
|
||||
|
||||
#include <QCompleter>
|
||||
#include <QDebug>
|
||||
#include <QFormLayout>
|
||||
#include <QGridLayout>
|
||||
|
|
@ -19,7 +19,6 @@
|
|||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QPushButton>
|
||||
#include <QRegularExpression>
|
||||
#include <QResizeEvent>
|
||||
#include <QScrollArea>
|
||||
#include <QScrollBar>
|
||||
|
|
@ -279,41 +278,14 @@ void TabArchidekt::setupFilterWidgets()
|
|||
auto cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this);
|
||||
auto displayModel = new CardDatabaseDisplayModel(this);
|
||||
displayModel->setSourceModel(cardDatabaseModel);
|
||||
auto *searchModel = new CardSearchModel(displayModel, this);
|
||||
|
||||
auto *proxyModel = new CardCompleterProxyModel(this);
|
||||
proxyModel->setSourceModel(searchModel);
|
||||
proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
|
||||
proxyModel->setFilterRole(Qt::DisplayRole);
|
||||
|
||||
auto *completer = new QCompleter(proxyModel, this);
|
||||
completer->setCompletionRole(Qt::DisplayRole);
|
||||
completer->setCompletionMode(QCompleter::PopupCompletion);
|
||||
completer->setCaseSensitivity(Qt::CaseInsensitive);
|
||||
completer->setFilterMode(Qt::MatchContains);
|
||||
completer->setMaxVisibleItems(10);
|
||||
|
||||
cardsField->setCompleter(completer);
|
||||
commandersField->setCompleter(completer);
|
||||
const CardCompleterSetup cardSetup = createCardCompleter(displayModel, this);
|
||||
cardsField->setCompleter(cardSetup.completer);
|
||||
commandersField->setCompleter(cardSetup.completer);
|
||||
|
||||
// Keep autocomplete working for both fields
|
||||
connect(cardsField, &QLineEdit::textChanged, this, [=](const QString &text) {
|
||||
searchModel->updateSearchResults(text);
|
||||
QString pattern = ".*" + QRegularExpression::escape(text) + ".*";
|
||||
proxyModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption));
|
||||
if (!text.isEmpty()) {
|
||||
completer->complete();
|
||||
}
|
||||
});
|
||||
|
||||
connect(commandersField, &QLineEdit::textChanged, this, [=](const QString &text) {
|
||||
searchModel->updateSearchResults(text);
|
||||
QString pattern = ".*" + QRegularExpression::escape(text) + ".*";
|
||||
proxyModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption));
|
||||
if (!text.isEmpty()) {
|
||||
completer->complete();
|
||||
}
|
||||
});
|
||||
connectCardCompleterSearch(cardsField, cardSetup);
|
||||
connectCardCompleterSearch(commandersField, cardSetup);
|
||||
|
||||
// Assemble secondary toolbar
|
||||
secondaryToolbarLayout->addWidget(bracketLabel);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "tab_edhrec_main.h"
|
||||
|
||||
#include "../../../../../client/settings/cache_settings.h"
|
||||
#include "../../../utility/completer_utils.h"
|
||||
#include "../../tab_supervisor.h"
|
||||
#include "api_response/average_deck/edhrec_average_deck_api_response.h"
|
||||
#include "api_response/commander/edhrec_commander_api_response.h"
|
||||
|
|
@ -12,7 +13,6 @@
|
|||
#include "display/top_commander/edhrec_top_commanders_api_response_display_widget.h"
|
||||
#include "display/top_tags/edhrec_top_tags_api_response_display_widget.h"
|
||||
|
||||
#include <QCompleter>
|
||||
#include <QDebug>
|
||||
#include <QHBoxLayout>
|
||||
#include <QJsonArray>
|
||||
|
|
@ -63,32 +63,12 @@ TabEdhRecMain::TabEdhRecMain(TabSupervisor *_tabSupervisor) : Tab(_tabSupervisor
|
|||
auto cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this);
|
||||
auto displayModel = new CardDatabaseDisplayModel(this);
|
||||
displayModel->setSourceModel(cardDatabaseModel);
|
||||
auto *searchModel = new CardSearchModel(displayModel, this);
|
||||
|
||||
auto *proxyModel = new CardCompleterProxyModel(this);
|
||||
proxyModel->setSourceModel(searchModel);
|
||||
proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
|
||||
proxyModel->setFilterRole(Qt::DisplayRole);
|
||||
|
||||
auto *completer = new QCompleter(proxyModel, this);
|
||||
completer->setCompletionRole(Qt::DisplayRole);
|
||||
completer->setCompletionMode(QCompleter::PopupCompletion);
|
||||
completer->setCaseSensitivity(Qt::CaseInsensitive);
|
||||
completer->setFilterMode(Qt::MatchContains);
|
||||
completer->setMaxVisibleItems(10);
|
||||
searchBar->setCompleter(completer);
|
||||
const CardCompleterSetup cardSetup = createCardCompleter(displayModel, this);
|
||||
searchBar->setCompleter(cardSetup.completer);
|
||||
|
||||
// Update suggestions dynamically
|
||||
connect(searchBar, &QLineEdit::textChanged, searchModel, &CardSearchModel::updateSearchResults);
|
||||
connect(searchBar, &QLineEdit::textChanged, this, [=](const QString &text) {
|
||||
// Ensure substring matching
|
||||
QString pattern = ".*" + QRegularExpression::escape(text) + ".*";
|
||||
proxyModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption));
|
||||
|
||||
if (!text.isEmpty()) {
|
||||
completer->complete(); // Force the dropdown to appear
|
||||
}
|
||||
});
|
||||
connectCardCompleterSearch(searchBar, cardSetup);
|
||||
|
||||
searchPushButton = new QPushButton(navigationContainer);
|
||||
connect(searchPushButton, &QPushButton::clicked, this, [=, this]() { doSearch(); });
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ class Tab : public QMainWindow
|
|||
signals:
|
||||
void userEvent(bool globalEvent = true);
|
||||
void tabTextChanged(Tab *tab, const QString &newTabText);
|
||||
void cockatriceLinkActivated(const QString &url);
|
||||
|
||||
protected:
|
||||
TabSupervisor *tabSupervisor;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "tab_account.h"
|
||||
|
||||
#include "../client/sound_engine.h"
|
||||
#include "../interface/widgets/dialogs/dlg_my_reports.h"
|
||||
#include "../interface/widgets/server/user/user_info_box.h"
|
||||
#include "../interface/widgets/server/user/user_list_manager.h"
|
||||
#include "../interface/widgets/server/user/user_list_widget.h"
|
||||
|
|
@ -49,6 +50,11 @@ TabAccount::TabAccount(TabSupervisor *_tabSupervisor, AbstractClient *_client, c
|
|||
|
||||
auto *vbox = new QVBoxLayout;
|
||||
vbox->addWidget(userInfoBox);
|
||||
|
||||
myReportsButton = new QPushButton(tr("My Reports"));
|
||||
connect(myReportsButton, &QPushButton::clicked, this, &TabAccount::openMyReports);
|
||||
vbox->addWidget(myReportsButton);
|
||||
|
||||
vbox->addWidget(allUsersList);
|
||||
|
||||
auto *addToBuddyList = new QHBoxLayout;
|
||||
|
|
@ -126,6 +132,7 @@ void TabAccount::addToList(const std::string &listName, const QString &userName)
|
|||
|
||||
void TabAccount::retranslateUi()
|
||||
{
|
||||
myReportsButton->setText(tr("My Reports"));
|
||||
allUsersList->retranslateUi();
|
||||
buddyList->retranslateUi();
|
||||
ignoreList->retranslateUi();
|
||||
|
|
@ -135,6 +142,7 @@ void TabAccount::retranslateUi()
|
|||
void TabAccount::processListUsersResponse(const Response &response)
|
||||
{
|
||||
const Response_ListUsers &resp = response.GetExtension(Response_ListUsers::ext);
|
||||
allUsersList->beginBulkLoad();
|
||||
for (int i = 0; i < resp.user_list_size(); ++i) {
|
||||
const ServerInfo_User &info = resp.user_list(i);
|
||||
const QString &userName = QString::fromStdString(info.name());
|
||||
|
|
@ -142,8 +150,8 @@ void TabAccount::processListUsersResponse(const Response &response)
|
|||
ignoreList->setUserOnline(userName, true);
|
||||
buddyList->setUserOnline(userName, true);
|
||||
}
|
||||
allUsersList->endBulkLoad();
|
||||
|
||||
allUsersList->sortItems();
|
||||
ignoreList->sortItems();
|
||||
buddyList->sortItems();
|
||||
}
|
||||
|
|
@ -188,18 +196,20 @@ void TabAccount::processUserLeftEvent(const Event_UserLeft &event)
|
|||
|
||||
void TabAccount::buddyListReceived(const QList<ServerInfo_User> &_buddyList)
|
||||
{
|
||||
buddyList->beginBulkLoad();
|
||||
for (const auto &user : _buddyList) {
|
||||
buddyList->processUserInfo(user, false);
|
||||
}
|
||||
buddyList->sortItems();
|
||||
buddyList->endBulkLoad();
|
||||
}
|
||||
|
||||
void TabAccount::ignoreListReceived(const QList<ServerInfo_User> &_ignoreList)
|
||||
{
|
||||
ignoreList->beginBulkLoad();
|
||||
for (const auto &user : _ignoreList) {
|
||||
ignoreList->processUserInfo(user, false);
|
||||
}
|
||||
ignoreList->sortItems();
|
||||
ignoreList->endBulkLoad();
|
||||
}
|
||||
|
||||
void TabAccount::processAddToListEvent(const Event_AddToList &event)
|
||||
|
|
@ -237,3 +247,10 @@ void TabAccount::processRemoveFromListEvent(const Event_RemoveFromList &event)
|
|||
|
||||
userList->deleteUser(user);
|
||||
}
|
||||
|
||||
void TabAccount::openMyReports()
|
||||
{
|
||||
auto *dlg = new DlgMyReports(client, this);
|
||||
dlg->setAttribute(Qt::WA_DeleteOnClose);
|
||||
dlg->exec();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ class Event_RemoveFromList;
|
|||
class Event_UserJoined;
|
||||
class Event_UserLeft;
|
||||
class LineEditUnfocusable;
|
||||
class QPushButton;
|
||||
class Response;
|
||||
class ServerInfo_User;
|
||||
class UserInfoBox;
|
||||
|
|
@ -41,6 +42,7 @@ private slots:
|
|||
void processRemoveFromListEvent(const Event_RemoveFromList &event);
|
||||
void addToIgnoreList();
|
||||
void addToBuddyList();
|
||||
void openMyReports();
|
||||
|
||||
private:
|
||||
AbstractClient *client;
|
||||
|
|
@ -50,6 +52,7 @@ private:
|
|||
UserInfoBox *userInfoBox;
|
||||
LineEditUnfocusable *addBuddyEdit;
|
||||
LineEditUnfocusable *addIgnoreEdit;
|
||||
QPushButton *myReportsButton;
|
||||
void addToList(const std::string &listName, const QString &userName);
|
||||
|
||||
public:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "tab_card_art_rules.h"
|
||||
|
||||
#include "../utility/completer_utils.h"
|
||||
#include "libcockatrice/card/database/card_database_manager.h"
|
||||
|
||||
#include <QCompleter>
|
||||
|
|
@ -194,29 +195,14 @@ void TabCardArtRules::initSearchBar()
|
|||
cardDbModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this);
|
||||
cardDbDisplayModel = new CardDatabaseDisplayModel(this);
|
||||
cardDbDisplayModel->setSourceModel(cardDbModel);
|
||||
cardSearchModel = new CardSearchModel(cardDbDisplayModel, this);
|
||||
|
||||
cardProxyModel = new CardCompleterProxyModel(this);
|
||||
cardProxyModel->setSourceModel(cardSearchModel);
|
||||
cardProxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
|
||||
|
||||
searchCompleter = new QCompleter(cardProxyModel, this);
|
||||
searchCompleter->setCompletionRole(Qt::DisplayRole);
|
||||
searchCompleter->setCompletionMode(QCompleter::PopupCompletion);
|
||||
searchCompleter->setCaseSensitivity(Qt::CaseInsensitive);
|
||||
searchCompleter->setFilterMode(Qt::MatchContains);
|
||||
searchCompleter->setMaxVisibleItems(15);
|
||||
const CardCompleterSetup cardSetup = createCardCompleter(cardDbDisplayModel, this, 15);
|
||||
cardSearchModel = cardSetup.searchModel;
|
||||
cardProxyModel = cardSetup.proxyModel;
|
||||
searchCompleter = cardSetup.completer;
|
||||
searchEdit->setCompleter(searchCompleter);
|
||||
|
||||
connect(searchEdit, &QLineEdit::textEdited, cardSearchModel, &CardSearchModel::updateSearchResults);
|
||||
connect(searchEdit, &QLineEdit::textEdited, this, [this](const QString &text) {
|
||||
const QString pattern = ".*" + QRegularExpression::escape(text) + ".*";
|
||||
cardProxyModel->setFilterRegularExpression(
|
||||
QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption));
|
||||
if (!text.isEmpty()) {
|
||||
searchCompleter->complete();
|
||||
}
|
||||
});
|
||||
connectCardCompleterSearch(searchEdit, cardSetup);
|
||||
connect(searchCompleter, static_cast<void (QCompleter::*)(const QString &)>(&QCompleter::activated), this,
|
||||
[this](const QString &name) { searchEdit->setText(name); });
|
||||
connect(searchEdit, &QLineEdit::editingFinished, this,
|
||||
|
|
|
|||
|
|
@ -20,14 +20,23 @@
|
|||
#include "../interface/card_picture_loader/card_picture_loader.h"
|
||||
#include "../interface/widgets/cards/card_info_frame_widget.h"
|
||||
#include "../interface/widgets/dialogs/dlg_create_game.h"
|
||||
#include "../interface/widgets/dialogs/dlg_invite_to_game.h"
|
||||
#include "../interface/widgets/server/game_link.h"
|
||||
#include "../interface/widgets/server/user/user_list_manager.h"
|
||||
#include "../interface/widgets/utility/completer_utils.h"
|
||||
#include "../interface/widgets/utility/line_edit_completer.h"
|
||||
#include "../interface/window_main.h"
|
||||
#include "../main.h"
|
||||
#include "../utility/visibility_change_listener.h"
|
||||
#include "card/card_completer_proxy_model.h"
|
||||
#include "card/card_search_model.h"
|
||||
#include "card_database_display_model.h"
|
||||
#include "card_database_model.h"
|
||||
#include "tab_supervisor.h"
|
||||
|
||||
#include <QAction>
|
||||
#include <QApplication>
|
||||
#include <QClipboard>
|
||||
#include <QCompleter>
|
||||
#include <QDebug>
|
||||
#include <QDockWidget>
|
||||
|
|
@ -35,8 +44,12 @@
|
|||
#include <QLabel>
|
||||
#include <QMenu>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QRegularExpression>
|
||||
#include <QStackedWidget>
|
||||
#include <QStringListModel>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
#include <libcockatrice/card/database/card_database.h>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
|
|
@ -286,6 +299,9 @@ void TabGame::retranslateUi()
|
|||
QString tabText = " | " + type + " #" + QString::number(game->getGameMetaInfo()->gameId());
|
||||
|
||||
updatePlayerListDockTitle();
|
||||
if (inviteButton) {
|
||||
inviteButton->setText(tr("Invite"));
|
||||
}
|
||||
cardInfoDock->setWindowTitle(tr("Card Info") + (cardInfoDock->isWindow() ? tabText : QString()));
|
||||
messageLayoutDock->setWindowTitle(tr("Messages") + (messageLayoutDock->isWindow() ? tabText : QString()));
|
||||
if (replayDock) {
|
||||
|
|
@ -324,6 +340,12 @@ void TabGame::retranslateUi()
|
|||
if (aGameInfo) {
|
||||
aGameInfo->setText(tr("Game &information"));
|
||||
}
|
||||
if (aCopyGameLink) {
|
||||
aCopyGameLink->setText(tr("Cop&y game link"));
|
||||
}
|
||||
if (aInviteToGame) {
|
||||
aInviteToGame->setText(tr("Invite to Game..."));
|
||||
}
|
||||
if (aConcede) {
|
||||
if (game->getPlayerManager()->isMainPlayerConceded()) {
|
||||
aConcede->setText(tr("Un&concede"));
|
||||
|
|
@ -491,6 +513,56 @@ void TabGame::actGameInfo()
|
|||
dlg.exec();
|
||||
}
|
||||
|
||||
void TabGame::actCopyGameLink()
|
||||
{
|
||||
const QString link =
|
||||
makeGameJoinLink(tabSupervisor->getClient()->serverName(), tabSupervisor->getClient()->serverPort(),
|
||||
game->getGameMetaInfo()->proto().room_id(), game->getGameMetaInfo()->gameId(),
|
||||
QString::fromStdString(game->getGameMetaInfo()->proto().description()));
|
||||
QApplication::clipboard()->setText(link);
|
||||
}
|
||||
|
||||
void TabGame::updateInviteButtonState()
|
||||
{
|
||||
// The dock button stays conservative (pre-start, not full); the menu action
|
||||
// additionally covers started/full games, which are legitimate spectate
|
||||
// invites, so it only needs the server-linked + not-closed conditions.
|
||||
const bool canInvite = !tabSupervisor->getIsLocalGame() && !game->getGameState()->isGameClosed() &&
|
||||
!game->getGameMetaInfo()->started() &&
|
||||
game->getPlayerManager()->getPlayerCount() < game->getGameMetaInfo()->maxPlayers();
|
||||
if (inviteButton) {
|
||||
inviteButton->setVisible(canInvite);
|
||||
}
|
||||
if (aInviteToGame) {
|
||||
aInviteToGame->setEnabled(!tabSupervisor->getIsLocalGame() && !game->getGameState()->isGameClosed());
|
||||
}
|
||||
}
|
||||
|
||||
void TabGame::actInviteToGame()
|
||||
{
|
||||
if (!tabSupervisor || tabSupervisor->getIsLocalGame()) {
|
||||
return;
|
||||
}
|
||||
|
||||
GameMetaInfo *metaInfo = game->getGameMetaInfo();
|
||||
const QString inviteUrl = makeGameJoinLink(
|
||||
tabSupervisor->getClient()->serverName(), tabSupervisor->getClient()->serverPort(), metaInfo->proto().room_id(),
|
||||
metaInfo->gameId(), QString::fromStdString(metaInfo->proto().description()));
|
||||
|
||||
QStringList excludeUserNames;
|
||||
excludeUserNames << tabSupervisor->getUserListManager()->getOwnUsername();
|
||||
for (auto player : game->getPlayerManager()->getPlayers()) {
|
||||
excludeUserNames << player->getPlayerInfo()->getName();
|
||||
}
|
||||
for (auto it = game->getPlayerManager()->getSpectators().cbegin();
|
||||
it != game->getPlayerManager()->getSpectators().cend(); ++it) {
|
||||
excludeUserNames << QString::fromStdString(it.value().name());
|
||||
}
|
||||
|
||||
DlgInviteToGame dlg(tabSupervisor, inviteUrl, metaInfo->proto().only_buddies(), excludeUserNames, this);
|
||||
dlg.exec();
|
||||
}
|
||||
|
||||
void TabGame::actConcede()
|
||||
{
|
||||
PlayerLogic *player = game->getPlayerManager()->getActiveLocalPlayer(game->getGameState()->getActivePlayer());
|
||||
|
|
@ -538,7 +610,8 @@ bool TabGame::leaveGame()
|
|||
|
||||
void TabGame::actSay()
|
||||
{
|
||||
if (completer->popup()->isVisible()) {
|
||||
if (sayEdit->hasVisibleCompleterPopup()) {
|
||||
sayEdit->hideCompleterPopups();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -558,14 +631,14 @@ void TabGame::addPlayerToAutoCompleteList(QString playerName)
|
|||
{
|
||||
if (sayEdit && !autocompleteUserList.contains(playerName)) {
|
||||
autocompleteUserList << playerName;
|
||||
sayEdit->setCompletionList(autocompleteUserList);
|
||||
mentionModel->setStringList(autocompleteUserList);
|
||||
}
|
||||
}
|
||||
|
||||
void TabGame::removePlayerFromAutoCompleteList(QString playerName)
|
||||
{
|
||||
if (sayEdit && autocompleteUserList.removeOne(playerName)) {
|
||||
sayEdit->setCompletionList(autocompleteUserList);
|
||||
mentionModel->setStringList(autocompleteUserList);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -628,8 +701,8 @@ void TabGame::actRotateViewCCW()
|
|||
|
||||
void TabGame::actCompleterChanged()
|
||||
{
|
||||
SettingsCache::instance().chat().getChatMentionCompleter() ? completer->setCompletionRole(2)
|
||||
: completer->setCompletionRole(1);
|
||||
SettingsCache::instance().chat().getChatMentionCompleter() ? mentionCompleter->setCompletionRole(2)
|
||||
: mentionCompleter->setCompletionRole(1);
|
||||
}
|
||||
|
||||
void TabGame::notifyPlayerJoin(QString playerName)
|
||||
|
|
@ -838,6 +911,7 @@ void TabGame::stopGame()
|
|||
QMapIterator<int, TabbedDeckViewContainer *> i(deckViewContainers);
|
||||
while (i.hasNext()) {
|
||||
i.next();
|
||||
i.value()->playerDeckView->advancePlaymatRotation();
|
||||
i.value()->show();
|
||||
}
|
||||
|
||||
|
|
@ -978,6 +1052,11 @@ void TabGame::createMenuItems()
|
|||
connect(aRotateViewCCW, &QAction::triggered, this, &TabGame::actRotateViewCCW);
|
||||
aGameInfo = new QAction(this);
|
||||
connect(aGameInfo, &QAction::triggered, this, &TabGame::actGameInfo);
|
||||
aCopyGameLink = new QAction(this);
|
||||
aCopyGameLink->setEnabled(!tabSupervisor->getIsLocalGame() && !tabSupervisor->getClient()->serverName().isEmpty());
|
||||
connect(aCopyGameLink, &QAction::triggered, this, &TabGame::actCopyGameLink);
|
||||
aInviteToGame = new QAction(this);
|
||||
connect(aInviteToGame, &QAction::triggered, this, &TabGame::actInviteToGame);
|
||||
aConcede = new QAction(this);
|
||||
connect(aConcede, &QAction::triggered, this, &TabGame::actConcede);
|
||||
if (!game->getGameMetaInfo()->started()) {
|
||||
|
|
@ -1016,6 +1095,8 @@ void TabGame::createMenuItems()
|
|||
gameMenu->addAction(aRotateViewCCW);
|
||||
gameMenu->addSeparator();
|
||||
gameMenu->addAction(aGameInfo);
|
||||
gameMenu->addAction(aCopyGameLink);
|
||||
gameMenu->addAction(aInviteToGame);
|
||||
gameMenu->addAction(aConcede);
|
||||
gameMenu->addAction(aFocusChat);
|
||||
gameMenu->addAction(aLeaveGame);
|
||||
|
|
@ -1024,6 +1105,9 @@ void TabGame::createMenuItems()
|
|||
|
||||
aCardMenu = gameMenu->addMenu(new QMenu(this));
|
||||
|
||||
// Sync the new action with the same state the dock button already shows.
|
||||
updateInviteButtonState();
|
||||
|
||||
addTabMenu(gameMenu);
|
||||
}
|
||||
|
||||
|
|
@ -1038,6 +1122,8 @@ void TabGame::createReplayMenuItems()
|
|||
aRotateViewCCW = nullptr;
|
||||
aResetLayout = nullptr;
|
||||
aGameInfo = nullptr;
|
||||
aCopyGameLink = nullptr;
|
||||
aInviteToGame = nullptr;
|
||||
aConcede = nullptr;
|
||||
aFocusChat = nullptr;
|
||||
aLeaveGame = new QAction(this);
|
||||
|
|
@ -1236,11 +1322,29 @@ void TabGame::createPlayerListDock(bool bReplay)
|
|||
}
|
||||
playerListWidget->setFocusPolicy(Qt::NoFocus);
|
||||
|
||||
auto *playerListBox = new QWidget(this);
|
||||
auto *vbox = new QVBoxLayout(playerListBox);
|
||||
vbox->setContentsMargins(0, 0, 0, 0);
|
||||
vbox->setSpacing(0);
|
||||
|
||||
vbox->addWidget(playerListWidget);
|
||||
|
||||
if (!bReplay) {
|
||||
inviteButton = new QPushButton(tr("Invite"), playerListBox);
|
||||
inviteButton->setVisible(false);
|
||||
connect(inviteButton, &QPushButton::clicked, this, &TabGame::actInviteToGame);
|
||||
vbox->addWidget(inviteButton);
|
||||
|
||||
connect(game->getGameMetaInfo(), &GameMetaInfo::startedChanged, this, &TabGame::updateInviteButtonState);
|
||||
connect(game->getPlayerManager(), &PlayerManager::playerCountChanged, this, &TabGame::updateInviteButtonState);
|
||||
updateInviteButtonState();
|
||||
}
|
||||
|
||||
playerListDock = new QDockWidget(this);
|
||||
playerListDock->setObjectName("playerListDock");
|
||||
playerListDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetFloatable |
|
||||
QDockWidget::DockWidgetMovable);
|
||||
playerListDock->setWidget(playerListWidget);
|
||||
playerListDock->setWidget(playerListBox);
|
||||
playerListDock->setFloating(false);
|
||||
}
|
||||
|
||||
|
|
@ -1264,6 +1368,7 @@ void TabGame::createMessageDock(bool bReplay)
|
|||
qOverload<const QString &>(&CardInfoFrameWidget::setCard));
|
||||
connect(messageLog, &MessageLogWidget::showCardInfoPopup, this, &TabGame::showCardInfoPopup);
|
||||
connect(messageLog, &MessageLogWidget::deleteCardInfoPopup, this, &TabGame::deleteCardInfoPopup);
|
||||
connect(messageLog, &MessageLogWidget::cockatriceLinkActivated, this, &TabGame::cockatriceLinkActivated);
|
||||
|
||||
if (!bReplay) {
|
||||
connect(messageLog, &MessageLogWidget::openMessageDialog, this, &TabGame::openMessageDialog);
|
||||
|
|
@ -1281,12 +1386,25 @@ void TabGame::createMessageDock(bool bReplay)
|
|||
sayEdit->setMaxLength(MAX_TEXT_LENGTH);
|
||||
sayLabel->setBuddy(sayEdit);
|
||||
connect(this, &TabGame::chatMessageSent, game->getGameEventHandler(), &GameEventHandler::handleChatMessageSent);
|
||||
completer = new QCompleter(autocompleteUserList, sayEdit);
|
||||
completer->setCaseSensitivity(Qt::CaseInsensitive);
|
||||
completer->setMaxVisibleItems(5);
|
||||
completer->setFilterMode(Qt::MatchStartsWith);
|
||||
mentionModel = new QStringListModel(autocompleteUserList, sayEdit);
|
||||
mentionCompleter = createMentionCompleter(mentionModel, sayEdit);
|
||||
sayEdit->addCompleter(mentionCompleter, CompleterTrigger::Mention);
|
||||
|
||||
auto *cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, sayEdit);
|
||||
auto *displayModel = new CardDatabaseDisplayModel(sayEdit);
|
||||
displayModel->setSourceModel(cardDatabaseModel);
|
||||
const CardCompleterSetup cardSetup = createCardCompleter(displayModel, sayEdit);
|
||||
sayEdit->addCompleter(cardSetup.completer, CompleterTrigger::Card);
|
||||
|
||||
connect(sayEdit, &LineEditCompleter::cardPartialChanged, this, [this, cardSetup](const QString &text) {
|
||||
cardSetup.searchModel->updateSearchResults(text);
|
||||
cardSetup.proxyModel->setFilterRegularExpression(
|
||||
QRegularExpression(QRegularExpression::escape(text), QRegularExpression::CaseInsensitiveOption));
|
||||
if (sayEdit->hasFocus()) {
|
||||
cardSetup.completer->complete();
|
||||
}
|
||||
});
|
||||
|
||||
sayEdit->setCompleter(completer);
|
||||
actCompleterChanged();
|
||||
|
||||
if (game->getPlayerManager()->isSpectator()) {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
#include <QCompleter>
|
||||
#include <QLoggingCategory>
|
||||
#include <QMap>
|
||||
#include <QStringListModel>
|
||||
|
||||
class CardMenu;
|
||||
class ServerInfo_PlayerProperties;
|
||||
|
|
@ -36,6 +37,7 @@ class CardInfoFrameWidget;
|
|||
class QTimer;
|
||||
class QSplitter;
|
||||
class QLabel;
|
||||
class QPushButton;
|
||||
class QToolButton;
|
||||
class QMenu;
|
||||
class ZoneViewLayout;
|
||||
|
|
@ -61,12 +63,14 @@ private:
|
|||
const UserListProxy *userListProxy;
|
||||
ReplayWidget *replayWidget = nullptr;
|
||||
QStringList gameTypes;
|
||||
QCompleter *completer;
|
||||
QCompleter *mentionCompleter;
|
||||
QStringListModel *mentionModel;
|
||||
QStringList autocompleteUserList;
|
||||
QStackedWidget *mainWidget;
|
||||
|
||||
CardInfoFrameWidget *cardInfoFrameWidget;
|
||||
PlayerListWidget *playerListWidget;
|
||||
QPushButton *inviteButton = nullptr;
|
||||
QLabel *timeElapsedLabel;
|
||||
MessageLogWidget *messageLog;
|
||||
QLabel *sayLabel;
|
||||
|
|
@ -81,9 +85,10 @@ private:
|
|||
QAction *playersSeparator;
|
||||
QMenu *gameMenu, *viewMenu;
|
||||
TearOffMenu *phasesMenu;
|
||||
QAction *aGameInfo, *aConcede, *aLeaveGame, *aNextPhase, *aNextPhaseAction, *aNextTurn, *aReverseTurn,
|
||||
*aRemoveLocalArrows, *aRotateViewCW, *aRotateViewCCW, *aResetLayout, *aResetReplayLayout;
|
||||
QAction *aGameInfo, *aConcede, *aCopyGameLink, *aLeaveGame, *aNextPhase, *aNextPhaseAction, *aNextTurn,
|
||||
*aReverseTurn, *aRemoveLocalArrows, *aRotateViewCW, *aRotateViewCCW, *aResetLayout, *aResetReplayLayout;
|
||||
QAction *aFocusChat;
|
||||
QAction *aInviteToGame = nullptr;
|
||||
QList<QAction *> phaseActions;
|
||||
QAction *aCardMenu;
|
||||
|
||||
|
|
@ -126,6 +131,7 @@ private:
|
|||
void createPlayAreaWidget(bool bReplay = false);
|
||||
void createDeckViewContainerWidget(bool bReplay = false);
|
||||
void createReplayDock(GameReplay *replay);
|
||||
void updateInviteButtonState();
|
||||
signals:
|
||||
void gameClosing(TabGame *tab);
|
||||
void containerProcessingStarted(const GameEventContext &context);
|
||||
|
|
@ -145,7 +151,9 @@ private slots:
|
|||
void setCardMenu(CardMenu *menu);
|
||||
|
||||
void actGameInfo();
|
||||
void actInviteToGame();
|
||||
void actConcede();
|
||||
void actCopyGameLink();
|
||||
void actRemoveLocalArrows();
|
||||
void actRotateViewCW();
|
||||
void actRotateViewCCW();
|
||||
|
|
|
|||
|
|
@ -23,14 +23,16 @@
|
|||
TabMessage::TabMessage(TabSupervisor *_tabSupervisor,
|
||||
AbstractClient *_client,
|
||||
const ServerInfo_User &_ownUserInfo,
|
||||
const ServerInfo_User &_otherUserInfo)
|
||||
const ServerInfo_User &_otherUserInfo,
|
||||
bool _userOnline)
|
||||
: Tab(_tabSupervisor), client(_client), ownUserInfo(new ServerInfo_User(_ownUserInfo)),
|
||||
otherUserInfo(new ServerInfo_User(_otherUserInfo)), userOnline(true)
|
||||
otherUserInfo(new ServerInfo_User(_otherUserInfo)), userOnline(_userOnline)
|
||||
{
|
||||
chatView = new ChatView(tabSupervisor, 0, true);
|
||||
connect(chatView, &ChatView::showCardInfoPopup, this, &TabMessage::showCardInfoPopup);
|
||||
connect(chatView, &ChatView::deleteCardInfoPopup, this, &TabMessage::deleteCardInfoPopup);
|
||||
connect(chatView, &ChatView::addMentionTag, this, &TabMessage::addMentionTag);
|
||||
connect(chatView, &ChatView::cockatriceLinkActivated, this, &TabMessage::cockatriceLinkActivated);
|
||||
sayEdit = new LineEditUnfocusable;
|
||||
sayEdit->setMaxLength(MAX_TEXT_LENGTH);
|
||||
connect(sayEdit, &LineEditUnfocusable::returnPressed, this, &TabMessage::sendMessage);
|
||||
|
|
@ -94,28 +96,58 @@ void TabMessage::closeEvent(QCloseEvent *event)
|
|||
event->accept();
|
||||
}
|
||||
|
||||
void TabMessage::sendPrivateMessage(const QString &text)
|
||||
{
|
||||
Command_Message cmd;
|
||||
cmd.set_user_name(otherUserInfo->name());
|
||||
cmd.set_message(text.toStdString());
|
||||
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
pend->setExtraData(text);
|
||||
connect(pend, &PendingCommand::finished, this, &TabMessage::messageSent);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void TabMessage::sendMessage()
|
||||
{
|
||||
if (sayEdit->text().isEmpty() || !userOnline) {
|
||||
if (sayEdit->text().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Command_Message cmd;
|
||||
cmd.set_user_name(otherUserInfo->name());
|
||||
cmd.set_message(sayEdit->text().toStdString());
|
||||
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &TabMessage::messageSent);
|
||||
client->sendCommand(pend);
|
||||
if (!userOnline) {
|
||||
notifyUserOffline();
|
||||
return;
|
||||
}
|
||||
|
||||
sendPrivateMessage(sayEdit->text());
|
||||
sayEdit->clear();
|
||||
}
|
||||
|
||||
void TabMessage::messageSent(const Response &response)
|
||||
bool TabMessage::isUserOnline() const
|
||||
{
|
||||
return userOnline;
|
||||
}
|
||||
|
||||
void TabMessage::sendInviteMessage(const QString &text)
|
||||
{
|
||||
sayEdit->setText(text);
|
||||
sendMessage();
|
||||
}
|
||||
|
||||
void TabMessage::messageSent(const Response &response,
|
||||
const CommandContainer & /*commandContainer*/,
|
||||
const QVariant &extraData)
|
||||
{
|
||||
if (response.response_code() == Response::RespInIgnoreList) {
|
||||
chatView->appendMessage(tr(
|
||||
"This user is ignoring you, they cannot see your messages in main chat and you cannot join their games."));
|
||||
} else if (response.response_code() == Response::RespNameNotFound) {
|
||||
// The recipient went offline before the command reached the server: restore the draft.
|
||||
userOnline = false;
|
||||
if (sayEdit->text().isEmpty()) {
|
||||
sayEdit->setText(extraData.toString());
|
||||
}
|
||||
notifyUserOffline();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -175,3 +207,8 @@ void TabMessage::processUserJoined(const ServerInfo_User &_userInfo)
|
|||
userOnline = true;
|
||||
*otherUserInfo = _userInfo;
|
||||
}
|
||||
|
||||
void TabMessage::notifyUserOffline()
|
||||
{
|
||||
chatView->appendMessage(tr("Message not sent — %1 is offline.").arg(QString::fromStdString(otherUserInfo->name())));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ class LineEditUnfocusable;
|
|||
class Event_UserMessage;
|
||||
class Response;
|
||||
class ServerInfo_User;
|
||||
class CommandContainer;
|
||||
|
||||
class TabMessage : public Tab
|
||||
{
|
||||
|
|
@ -39,7 +40,7 @@ signals:
|
|||
void maximizeClient();
|
||||
private slots:
|
||||
void sendMessage();
|
||||
void messageSent(const Response &response);
|
||||
void messageSent(const Response &response, const CommandContainer &commandContainer, const QVariant &extraData);
|
||||
void addMentionTag(QString mentionTag);
|
||||
void messageClicked();
|
||||
|
||||
|
|
@ -50,7 +51,8 @@ public:
|
|||
TabMessage(TabSupervisor *_tabSupervisor,
|
||||
AbstractClient *_client,
|
||||
const ServerInfo_User &_ownUserInfo,
|
||||
const ServerInfo_User &_otherUserInfo);
|
||||
const ServerInfo_User &_otherUserInfo,
|
||||
bool _userOnline);
|
||||
~TabMessage() override;
|
||||
void retranslateUi() override;
|
||||
void tabActivated() override;
|
||||
|
|
@ -62,9 +64,14 @@ public:
|
|||
void processUserLeft();
|
||||
void processUserJoined(const ServerInfo_User &_userInfo);
|
||||
|
||||
[[nodiscard]] bool isUserOnline() const;
|
||||
void sendPrivateMessage(const QString &text);
|
||||
void sendInviteMessage(const QString &text);
|
||||
|
||||
private:
|
||||
bool shouldShowSystemPopup(const Event_UserMessage &event);
|
||||
void showSystemPopup(const Event_UserMessage &event);
|
||||
void notifyUserOffline();
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
|
|||
462
cockatrice/src/interface/widgets/tabs/tab_moderation.cpp
Normal file
462
cockatrice/src/interface/widgets/tabs/tab_moderation.cpp
Normal file
|
|
@ -0,0 +1,462 @@
|
|||
#include "tab_moderation.h"
|
||||
|
||||
#include "abstract_client.h"
|
||||
#include "tab_supervisor.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QGridLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHeaderView>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QSplitter>
|
||||
#include <QTableWidget>
|
||||
#include <QTextEdit>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/protocol/pb/command_report_user_info.pb.h>
|
||||
#include <libcockatrice/protocol/pb/moderator_commands.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_moderator_last_logins.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_remove_user_avatar.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_report_user_info.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_reset_user_password.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_user_alts.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_user_sessions.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
|
||||
#include <libcockatrice/protocol/pending_command.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr int COL_ALTS_USER = 0;
|
||||
constexpr int COL_ALTS_EMAIL = 1;
|
||||
constexpr int COL_ALTS_CLIENTID = 2;
|
||||
constexpr int COL_ALTS_REGISTERED = 3;
|
||||
constexpr int COL_ALTS_LAST_LOGIN = 4;
|
||||
constexpr int COL_ALTS_WARNS = 5;
|
||||
constexpr int COL_ALTS_BANS = 6;
|
||||
constexpr int COL_ALTS_ACTIVE = 7;
|
||||
constexpr int COL_ALTS_COUNT = 8;
|
||||
|
||||
constexpr int COL_SESSIONS_IP = 0;
|
||||
constexpr int COL_SESSIONS_CLIENTID = 1;
|
||||
constexpr int COL_SESSIONS_START = 2;
|
||||
constexpr int COL_SESSIONS_END = 3;
|
||||
constexpr int COL_SESSIONS_TYPE = 4;
|
||||
constexpr int COL_SESSIONS_COUNT = 5;
|
||||
|
||||
constexpr int COL_STAFF_USER = 0;
|
||||
constexpr int COL_STAFF_LEVEL = 1;
|
||||
constexpr int COL_STAFF_LAST_LOGIN = 2;
|
||||
constexpr int COL_STAFF_COUNT = 3;
|
||||
} // namespace
|
||||
|
||||
TabModeration::TabModeration(TabSupervisor *_tabSupervisor, AbstractClient *_client, const QString &initialUser)
|
||||
: Tab(_tabSupervisor), client(_client)
|
||||
{
|
||||
auto *centralWidget = new QWidget(this);
|
||||
setCentralWidget(centralWidget);
|
||||
|
||||
searchEdit = new QLineEdit;
|
||||
searchEdit->setClearButtonEnabled(true);
|
||||
connect(searchEdit, &QLineEdit::returnPressed, this, &TabModeration::investigateUser);
|
||||
|
||||
investigateButton = new QPushButton;
|
||||
connect(investigateButton, &QPushButton::clicked, this, &TabModeration::investigateUser);
|
||||
|
||||
resetPasswordButton = new QPushButton;
|
||||
connect(resetPasswordButton, &QPushButton::clicked, this, &TabModeration::resetPassword);
|
||||
|
||||
removeAvatarButton = new QPushButton;
|
||||
connect(removeAvatarButton, &QPushButton::clicked, this, &TabModeration::removeAvatar);
|
||||
|
||||
auto *topBar = new QHBoxLayout;
|
||||
topBar->addWidget(searchEdit);
|
||||
topBar->addWidget(investigateButton);
|
||||
topBar->addStretch();
|
||||
topBar->addWidget(resetPasswordButton);
|
||||
topBar->addWidget(removeAvatarButton);
|
||||
|
||||
userInfoGroup = new QGroupBox;
|
||||
userInfoNameLabel = new QLabel;
|
||||
userInfoNameValue = new QLabel;
|
||||
userInfoRegisteredLabel = new QLabel;
|
||||
userInfoRegisteredValue = new QLabel;
|
||||
userInfoLastLoginLabel = new QLabel;
|
||||
userInfoLastLoginValue = new QLabel;
|
||||
userInfoStatusLabel = new QLabel;
|
||||
userInfoStatusValue = new QLabel;
|
||||
userInfoCountsLabel = new QLabel;
|
||||
userInfoCountsValue = new QLabel;
|
||||
userInfoNotesLabel = new QLabel;
|
||||
userInfoNotesEdit = new QTextEdit;
|
||||
userInfoNotesEdit->setReadOnly(true);
|
||||
|
||||
auto *infoGrid = new QGridLayout;
|
||||
infoGrid->addWidget(userInfoNameLabel, 0, 0);
|
||||
infoGrid->addWidget(userInfoNameValue, 0, 1);
|
||||
infoGrid->addWidget(userInfoRegisteredLabel, 0, 2);
|
||||
infoGrid->addWidget(userInfoRegisteredValue, 0, 3);
|
||||
infoGrid->addWidget(userInfoLastLoginLabel, 1, 0);
|
||||
infoGrid->addWidget(userInfoLastLoginValue, 1, 1);
|
||||
infoGrid->addWidget(userInfoStatusLabel, 1, 2);
|
||||
infoGrid->addWidget(userInfoStatusValue, 1, 3);
|
||||
infoGrid->addWidget(userInfoCountsLabel, 2, 0);
|
||||
infoGrid->addWidget(userInfoCountsValue, 2, 1, 1, 3);
|
||||
infoGrid->addWidget(userInfoNotesLabel, 3, 0, Qt::AlignTop);
|
||||
infoGrid->addWidget(userInfoNotesEdit, 3, 1, 1, 3);
|
||||
infoGrid->setColumnStretch(1, 1);
|
||||
infoGrid->setColumnStretch(3, 1);
|
||||
|
||||
auto *infoLayout = new QVBoxLayout(userInfoGroup);
|
||||
infoLayout->addLayout(infoGrid);
|
||||
|
||||
auto configureTable = [](QTableWidget *table) {
|
||||
table->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
table->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
table->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
table->verticalHeader()->setVisible(false);
|
||||
table->setAlternatingRowColors(true);
|
||||
table->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
|
||||
table->horizontalHeader()->setStretchLastSection(true);
|
||||
};
|
||||
|
||||
altsGroup = new QGroupBox;
|
||||
altsTable = new QTableWidget(0, COL_ALTS_COUNT);
|
||||
configureTable(altsTable);
|
||||
auto *altsLayout = new QVBoxLayout(altsGroup);
|
||||
altsLayout->addWidget(altsTable);
|
||||
|
||||
sessionsGroup = new QGroupBox;
|
||||
sessionsTable = new QTableWidget(0, COL_SESSIONS_COUNT);
|
||||
configureTable(sessionsTable);
|
||||
auto *sessionsLayout = new QVBoxLayout(sessionsGroup);
|
||||
sessionsLayout->addWidget(sessionsTable);
|
||||
|
||||
staffGroup = new QGroupBox;
|
||||
staffTable = new QTableWidget(0, COL_STAFF_COUNT);
|
||||
configureTable(staffTable);
|
||||
refreshStaffButton = new QPushButton;
|
||||
connect(refreshStaffButton, &QPushButton::clicked, this, &TabModeration::requestModeratorLogins);
|
||||
auto *staffHeader = new QHBoxLayout;
|
||||
staffHeader->addStretch();
|
||||
staffHeader->addWidget(refreshStaffButton);
|
||||
auto *staffLayout = new QVBoxLayout(staffGroup);
|
||||
staffLayout->addWidget(staffTable);
|
||||
staffLayout->addLayout(staffHeader);
|
||||
|
||||
auto *splitter = new QSplitter(Qt::Vertical);
|
||||
splitter->addWidget(userInfoGroup);
|
||||
splitter->addWidget(altsGroup);
|
||||
splitter->addWidget(sessionsGroup);
|
||||
splitter->addWidget(staffGroup);
|
||||
splitter->setStretchFactor(0, 1);
|
||||
splitter->setStretchFactor(1, 2);
|
||||
splitter->setStretchFactor(2, 2);
|
||||
splitter->setStretchFactor(3, 1);
|
||||
|
||||
auto *mainLayout = new QVBoxLayout(centralWidget);
|
||||
mainLayout->addLayout(topBar);
|
||||
mainLayout->addWidget(splitter);
|
||||
|
||||
retranslateUi();
|
||||
clearUserData();
|
||||
requestModeratorLogins();
|
||||
investigate(initialUser);
|
||||
}
|
||||
|
||||
void TabModeration::retranslateUi()
|
||||
{
|
||||
searchEdit->setPlaceholderText(tr("User name"));
|
||||
investigateButton->setText(tr("Investigate"));
|
||||
resetPasswordButton->setText(tr("Reset Password"));
|
||||
removeAvatarButton->setText(tr("Remove Avatar"));
|
||||
refreshStaffButton->setText(tr("Refresh"));
|
||||
|
||||
userInfoGroup->setTitle(tr("User Info"));
|
||||
userInfoNameLabel->setText(tr("Name:"));
|
||||
userInfoRegisteredLabel->setText(tr("Registered:"));
|
||||
userInfoLastLoginLabel->setText(tr("Last login:"));
|
||||
userInfoStatusLabel->setText(tr("Status:"));
|
||||
userInfoCountsLabel->setText(tr("Counts:"));
|
||||
userInfoNotesLabel->setText(tr("Admin notes:"));
|
||||
|
||||
altsGroup->setTitle(tr("Alts"));
|
||||
sessionsGroup->setTitle(tr("Sessions"));
|
||||
staffGroup->setTitle(tr("Staff Last Logins"));
|
||||
|
||||
altsTable->setHorizontalHeaderLabels({tr("User"), tr("eMail"), tr("Client ID"), tr("Registered"), tr("Last login"),
|
||||
tr("Warns"), tr("Bans"), tr("Active")});
|
||||
sessionsTable->setHorizontalHeaderLabels({tr("IP"), tr("Client ID"), tr("Start"), tr("End"), tr("Type")});
|
||||
staffTable->setHorizontalHeaderLabels({tr("User"), tr("Level"), tr("Last login")});
|
||||
}
|
||||
|
||||
QString TabModeration::formatEpoch(quint64 ts) const
|
||||
{
|
||||
if (ts == 0) {
|
||||
return tr("Unknown");
|
||||
}
|
||||
return QDateTime::fromSecsSinceEpoch(ts).toLocalTime().toString("yyyy-MM-dd HH:mm");
|
||||
}
|
||||
|
||||
void TabModeration::clearUserData()
|
||||
{
|
||||
currentUser.clear();
|
||||
userInfoNameValue->clear();
|
||||
userInfoRegisteredValue->clear();
|
||||
userInfoLastLoginValue->clear();
|
||||
userInfoStatusValue->clear();
|
||||
userInfoCountsValue->clear();
|
||||
userInfoNotesEdit->clear();
|
||||
altsTable->setRowCount(0);
|
||||
sessionsTable->setRowCount(0);
|
||||
resetPasswordButton->setEnabled(false);
|
||||
removeAvatarButton->setEnabled(false);
|
||||
}
|
||||
|
||||
void TabModeration::investigate(const QString &userName)
|
||||
{
|
||||
if (userName.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
searchEdit->setText(userName);
|
||||
investigateUser();
|
||||
}
|
||||
|
||||
void TabModeration::investigateUser()
|
||||
{
|
||||
const QString userName = searchEdit->text().simplified();
|
||||
if (userName.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
currentUser = userName;
|
||||
resetPasswordButton->setEnabled(true);
|
||||
removeAvatarButton->setEnabled(true);
|
||||
altsTable->setRowCount(0);
|
||||
sessionsTable->setRowCount(0);
|
||||
requestUserInfo(userName);
|
||||
requestSessions(userName);
|
||||
requestAlts(userName);
|
||||
}
|
||||
|
||||
void TabModeration::requestUserInfo(const QString &userName)
|
||||
{
|
||||
userInfoNameValue->setText(userName);
|
||||
userInfoRegisteredValue->setText(tr("Loading..."));
|
||||
userInfoLastLoginValue->setText(tr("Loading..."));
|
||||
userInfoStatusValue->clear();
|
||||
userInfoCountsValue->clear();
|
||||
userInfoNotesEdit->clear();
|
||||
|
||||
Command_ReportUserInfo cmd;
|
||||
cmd.set_user_name(userName.toStdString());
|
||||
PendingCommand *pend = client->prepareModeratorCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &TabModeration::userInfoResponse);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void TabModeration::userInfoResponse(const Response &response)
|
||||
{
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
userInfoRegisteredValue->clear();
|
||||
userInfoLastLoginValue->clear();
|
||||
userInfoStatusValue->setText(tr("Error loading user info."));
|
||||
return;
|
||||
}
|
||||
|
||||
const Response_ReportUserInfo &resp = response.GetExtension(Response_ReportUserInfo::ext);
|
||||
if (resp.has_user_name() && QString::fromStdString(resp.user_name()) != currentUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
userInfoRegisteredValue->setText(formatEpoch(resp.registration_time()));
|
||||
userInfoLastLoginValue->setText(formatEpoch(resp.last_login()));
|
||||
|
||||
QStringList statusParts;
|
||||
statusParts << (resp.is_active() ? tr("active") : tr("inactive"));
|
||||
if (resp.has_is_admin() && resp.is_admin()) {
|
||||
statusParts << tr("admin");
|
||||
}
|
||||
userInfoStatusValue->setText(statusParts.join(", "));
|
||||
|
||||
userInfoCountsValue->setText(tr("Reports: %1 Bans: %2 Warnings: %3")
|
||||
.arg(resp.total_reports())
|
||||
.arg(resp.total_bans())
|
||||
.arg(resp.total_warns()));
|
||||
|
||||
if (resp.has_admin_notes() && !resp.admin_notes().empty()) {
|
||||
userInfoNotesEdit->setPlainText(QString::fromStdString(resp.admin_notes()));
|
||||
} else {
|
||||
userInfoNotesEdit->setPlainText(tr("(none)"));
|
||||
}
|
||||
}
|
||||
|
||||
void TabModeration::requestSessions(const QString &userName)
|
||||
{
|
||||
Command_GetUserSessions cmd;
|
||||
cmd.set_user_name(userName.toStdString());
|
||||
PendingCommand *pend = client->prepareModeratorCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &TabModeration::sessionsResponse);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void TabModeration::sessionsResponse(const Response &response)
|
||||
{
|
||||
sessionsTable->setRowCount(0);
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Response_UserSessions &resp = response.GetExtension(Response_UserSessions::ext);
|
||||
sessionsTable->setRowCount(resp.sessions_size());
|
||||
for (int i = 0; i < resp.sessions_size(); ++i) {
|
||||
const ServerInfo_UserSession &session = resp.sessions(i);
|
||||
sessionsTable->setItem(i, COL_SESSIONS_IP, new QTableWidgetItem(QString::fromStdString(session.ip_address())));
|
||||
sessionsTable->setItem(i, COL_SESSIONS_CLIENTID,
|
||||
new QTableWidgetItem(QString::fromStdString(session.clientid())));
|
||||
sessionsTable->setItem(i, COL_SESSIONS_START, new QTableWidgetItem(formatEpoch(session.start_time())));
|
||||
sessionsTable->setItem(
|
||||
i, COL_SESSIONS_END,
|
||||
new QTableWidgetItem(session.end_time() == 0 ? tr("Active") : formatEpoch(session.end_time())));
|
||||
sessionsTable->setItem(i, COL_SESSIONS_TYPE,
|
||||
new QTableWidgetItem(QString::fromStdString(session.connection_type())));
|
||||
}
|
||||
}
|
||||
|
||||
void TabModeration::requestAlts(const QString &userName)
|
||||
{
|
||||
Command_GetUserAlts cmd;
|
||||
cmd.set_user_name(userName.toStdString());
|
||||
PendingCommand *pend = client->prepareModeratorCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &TabModeration::altsResponse);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void TabModeration::altsResponse(const Response &response)
|
||||
{
|
||||
altsTable->setRowCount(0);
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Response_UserAlts &resp = response.GetExtension(Response_UserAlts::ext);
|
||||
altsTable->setRowCount(resp.alts_size());
|
||||
for (int i = 0; i < resp.alts_size(); ++i) {
|
||||
const ServerInfo_UserAlt &alt = resp.alts(i);
|
||||
altsTable->setItem(i, COL_ALTS_USER, new QTableWidgetItem(QString::fromStdString(alt.user_name())));
|
||||
altsTable->setItem(i, COL_ALTS_EMAIL, new QTableWidgetItem(QString::fromStdString(alt.email())));
|
||||
altsTable->setItem(i, COL_ALTS_CLIENTID, new QTableWidgetItem(QString::fromStdString(alt.clientid())));
|
||||
altsTable->setItem(i, COL_ALTS_REGISTERED, new QTableWidgetItem(formatEpoch(alt.registration_time())));
|
||||
altsTable->setItem(i, COL_ALTS_LAST_LOGIN, new QTableWidgetItem(formatEpoch(alt.last_login())));
|
||||
altsTable->setItem(i, COL_ALTS_WARNS, new QTableWidgetItem(QString::number(alt.warn_count())));
|
||||
altsTable->setItem(i, COL_ALTS_BANS, new QTableWidgetItem(QString::number(alt.ban_count())));
|
||||
altsTable->setItem(i, COL_ALTS_ACTIVE, new QTableWidgetItem(alt.is_active() ? tr("yes") : tr("no")));
|
||||
}
|
||||
}
|
||||
|
||||
void TabModeration::requestModeratorLogins()
|
||||
{
|
||||
Command_GetModeratorLastLogins cmd;
|
||||
PendingCommand *pend = client->prepareModeratorCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &TabModeration::moderatorLoginsResponse);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void TabModeration::moderatorLoginsResponse(const Response &response)
|
||||
{
|
||||
staffTable->setRowCount(0);
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Response_ModeratorLastLogins &resp = response.GetExtension(Response_ModeratorLastLogins::ext);
|
||||
staffTable->setRowCount(resp.logins_size());
|
||||
for (int i = 0; i < resp.logins_size(); ++i) {
|
||||
const ServerInfo_ModeratorLogin &login = resp.logins(i);
|
||||
staffTable->setItem(i, COL_STAFF_USER, new QTableWidgetItem(QString::fromStdString(login.user_name())));
|
||||
staffTable->setItem(i, COL_STAFF_LAST_LOGIN, new QTableWidgetItem(formatEpoch(login.last_login())));
|
||||
|
||||
QStringList levels;
|
||||
if (login.user_level() & ServerInfo_User::IsAdmin) {
|
||||
levels << tr("Admin");
|
||||
}
|
||||
if (login.user_level() & ServerInfo_User::IsModerator) {
|
||||
levels << tr("Moderator");
|
||||
}
|
||||
if (login.user_level() & ServerInfo_User::IsJudge) {
|
||||
levels << tr("Judge");
|
||||
}
|
||||
staffTable->setItem(i, COL_STAFF_LEVEL, new QTableWidgetItem(levels.join(" / ")));
|
||||
}
|
||||
}
|
||||
|
||||
void TabModeration::resetPassword()
|
||||
{
|
||||
if (currentUser.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QMessageBox::StandardButton choice =
|
||||
QMessageBox::warning(this, tr("Reset Password"),
|
||||
tr("Reset the password of %1? A temporary password will be generated and shown to you. "
|
||||
"The user must change it on their first login.")
|
||||
.arg(currentUser),
|
||||
QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Cancel);
|
||||
if (choice != QMessageBox::Ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
Command_ResetUserPassword cmd;
|
||||
cmd.set_user_name(currentUser.toStdString());
|
||||
PendingCommand *pend = client->prepareModeratorCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &TabModeration::resetPasswordResponse);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void TabModeration::resetPasswordResponse(const Response &response)
|
||||
{
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
QMessageBox::critical(this, tr("Error"), tr("Password reset failed."));
|
||||
return;
|
||||
}
|
||||
|
||||
const Response_ResetUserPassword &resp = response.GetExtension(Response_ResetUserPassword::ext);
|
||||
QMessageBox::information(this, tr("Password reset"),
|
||||
tr("Temporary password for %1:\n\n%2\n\nPass it to the user through a secure channel.")
|
||||
.arg(QString::fromStdString(resp.user_name()))
|
||||
.arg(QString::fromStdString(resp.temporary_password())));
|
||||
}
|
||||
|
||||
void TabModeration::removeAvatar()
|
||||
{
|
||||
if (currentUser.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QMessageBox::StandardButton choice =
|
||||
QMessageBox::warning(this, tr("Remove Avatar"),
|
||||
tr("Remove the avatar of %1? The user will have to upload a new one.").arg(currentUser),
|
||||
QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Cancel);
|
||||
if (choice != QMessageBox::Ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
Command_RemoveUserAvatar cmd;
|
||||
cmd.set_user_name(currentUser.toStdString());
|
||||
PendingCommand *pend = client->prepareModeratorCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &TabModeration::removeAvatarResponse);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void TabModeration::removeAvatarResponse(const Response &response)
|
||||
{
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
QMessageBox::critical(this, tr("Error"), tr("Could not remove the avatar."));
|
||||
return;
|
||||
}
|
||||
|
||||
const Response_RemoveUserAvatar &resp = response.GetExtension(Response_RemoveUserAvatar::ext);
|
||||
QMessageBox::information(this, tr("Avatar removed"),
|
||||
tr("The avatar of %1 has been removed.").arg(QString::fromStdString(resp.user_name())));
|
||||
}
|
||||
83
cockatrice/src/interface/widgets/tabs/tab_moderation.h
Normal file
83
cockatrice/src/interface/widgets/tabs/tab_moderation.h
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
#ifndef TAB_MODERATION_H
|
||||
#define TAB_MODERATION_H
|
||||
|
||||
#include "tab.h"
|
||||
|
||||
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||
|
||||
class AbstractClient;
|
||||
class QGroupBox;
|
||||
class QLabel;
|
||||
class QLineEdit;
|
||||
class QPushButton;
|
||||
class QTableWidget;
|
||||
class QTextEdit;
|
||||
|
||||
/**
|
||||
* Staff investigation tool. Lets moderators look up a user's account data,
|
||||
* alternate accounts, login sessions, and staff login activity, and offers
|
||||
* the password-reset and remove-avatar actions.
|
||||
*/
|
||||
class TabModeration : public Tab
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit TabModeration(TabSupervisor *_tabSupervisor, AbstractClient *_client, const QString &initialUser = {});
|
||||
void retranslateUi() override;
|
||||
[[nodiscard]] QString getTabText() const override
|
||||
{
|
||||
return tr("Moderation");
|
||||
}
|
||||
void investigate(const QString &userName);
|
||||
|
||||
private slots:
|
||||
void investigateUser();
|
||||
void userInfoResponse(const Response &response);
|
||||
void sessionsResponse(const Response &response);
|
||||
void altsResponse(const Response &response);
|
||||
void moderatorLoginsResponse(const Response &response);
|
||||
void resetPassword();
|
||||
void resetPasswordResponse(const Response &response);
|
||||
void removeAvatar();
|
||||
void removeAvatarResponse(const Response &response);
|
||||
|
||||
private:
|
||||
void requestUserInfo(const QString &userName);
|
||||
void requestSessions(const QString &userName);
|
||||
void requestAlts(const QString &userName);
|
||||
void requestModeratorLogins();
|
||||
void clearUserData();
|
||||
[[nodiscard]] QString formatEpoch(quint64 ts) const;
|
||||
|
||||
AbstractClient *client;
|
||||
QString currentUser;
|
||||
|
||||
QLineEdit *searchEdit;
|
||||
QPushButton *investigateButton;
|
||||
QPushButton *resetPasswordButton;
|
||||
QPushButton *removeAvatarButton;
|
||||
|
||||
QGroupBox *userInfoGroup;
|
||||
QLabel *userInfoNameLabel;
|
||||
QLabel *userInfoNameValue;
|
||||
QLabel *userInfoRegisteredLabel;
|
||||
QLabel *userInfoRegisteredValue;
|
||||
QLabel *userInfoLastLoginLabel;
|
||||
QLabel *userInfoLastLoginValue;
|
||||
QLabel *userInfoStatusLabel;
|
||||
QLabel *userInfoStatusValue;
|
||||
QLabel *userInfoCountsLabel;
|
||||
QLabel *userInfoCountsValue;
|
||||
QLabel *userInfoNotesLabel;
|
||||
QTextEdit *userInfoNotesEdit;
|
||||
|
||||
QGroupBox *altsGroup;
|
||||
QTableWidget *altsTable;
|
||||
QGroupBox *sessionsGroup;
|
||||
QTableWidget *sessionsTable;
|
||||
QGroupBox *staffGroup;
|
||||
QTableWidget *staffTable;
|
||||
QPushButton *refreshStaffButton;
|
||||
};
|
||||
|
||||
#endif // TAB_MODERATION_H
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue