Merge branch 'master' into master

This commit is contained in:
Jurriaan 2026-11-02 16:43:51 +01:00 • committed by GitHub
commit ba22736def
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
114 changed files with 4835 additions and 1044 deletions

View file

@ -238,6 +238,7 @@ void CardPictureLoader::imageLoaded(const ExactCard &card, const QImage &image)
});
card.emitPixmapUpdated();
emit getInstance().pixmapLoaded(card);
}
void CardPictureLoader::deleteAllLocalOverrides(const ExactCard &card)

View file

@ -146,6 +146,14 @@ signals:
/** @brief Emitted after the worker has finished clearing the network and redirect caches. */
void networkCacheCleared();
/**
* @brief Emitted once an image has been loaded and put into the pixmap cache.
*
* Views that paint cards from the loader without owning a widget per card can
* listen to this to repaint the rows whose picture just became available.
*/
void pixmapLoaded(const ExactCard &card);
private slots:
/**
* @brief Triggered when the user changes the picture download settings.

View file

@ -27,9 +27,9 @@ void IntentGetLoginCredentials::onPreconditionSatisfied()
ServersSettings &servers = SettingsCache::instance().servers();
const int index = servers.findServerIndex(context->hostname, context->port);
if (index >= 0) {
context->username = servers.getValue(QString("username%1").arg(index), "server", "server_details").toString();
context->password = servers.getValue(QString("password%1").arg(index), "server", "server_details").toString();
if (const auto server = servers.getServer(index)) {
context->username = server->username;
context->password = server->password;
emitFinished();
} else {
emitFailed(tr("No saved credentials for this server"));
@ -69,9 +69,8 @@ void IntentGetLoginCredentials::onPreconditionNotSatisfied()
// it with the raw hostname when addNewServer updates the entry in place.
QString saveName = context->hostname;
const int existingIndex = servers.findServerIndex(context->hostname, context->port);
if (existingIndex >= 0) {
saveName =
servers.getValue(QString("saveName%1").arg(existingIndex), "server", "server_details").toString();
if (const auto existing = servers.getServer(existingIndex)) {
saveName = existing->saveName;
if (saveName.isEmpty()) {
saveName = context->hostname;
}

View file

@ -418,12 +418,10 @@ void IntentUrlParser::restoreToPreviousServer(const PendingIntentChain &chain)
ServersSettings &servers = SettingsCache::instance().servers();
const int index = servers.findServerIndex(chain.previousServerHost, chain.previousServerPort);
if (index >= 0 && servers.hasLoginData(chain.previousServerHost, chain.previousServerPort)) {
const QString username =
servers.getValue(QString("username%1").arg(index), "server", "server_details").toString();
const QString password =
servers.getValue(QString("password%1").arg(index), "server", "server_details").toString();
client->connectToServer(chain.previousServerHost, chain.previousServerPort.toUInt(), username, password);
const auto server = servers.getServer(index);
if (server && servers.hasLoginData(chain.previousServerHost, chain.previousServerPort)) {
client->connectToServer(chain.previousServerHost, chain.previousServerPort.toUInt(), server->username,
server->password);
return;
}
client->disconnectFromServer();
@ -438,11 +436,9 @@ void IntentUrlParser::restoreToPreviousServer(const PendingIntentChain &chain)
// when credentials are saved, otherwise stay offline.
ServersSettings &servers = SettingsCache::instance().servers();
const int index = servers.findServerIndex(chain.previousServerHost, chain.previousServerPort);
if (index >= 0 && servers.hasLoginData(chain.previousServerHost, chain.previousServerPort)) {
const QString username =
servers.getValue(QString("username%1").arg(index), "server", "server_details").toString();
const QString password =
servers.getValue(QString("password%1").arg(index), "server", "server_details").toString();
client->connectToServer(chain.previousServerHost, chain.previousServerPort.toUInt(), username, password);
const auto server = servers.getServer(index);
if (server && servers.hasLoginData(chain.previousServerHost, chain.previousServerPort)) {
client->connectToServer(chain.previousServerHost, chain.previousServerPort.toUInt(), server->username,
server->password);
}
}

View file

@ -43,11 +43,6 @@
#include <qlogging.h>
#include <qnamespace.h>
static constexpr qreal MTG_CARD_ASPECT_RATIO = 1.396;
// static constexpr qreal YUGIOH_CARD_ASPECT_RATIO = 1.457;
static constexpr qreal ASPECT_RATIO = MTG_CARD_ASPECT_RATIO;
static constexpr int BASE_WIDTH = 200;
static constexpr int BASE_HEIGHT = 200;
static constexpr int ENLARGED_PIXMAP_OFFSET = 10;
@ -162,12 +157,11 @@ void CardInfoPictureWidget::resizeEvent(QResizeEvent *event)
*/
void CardInfoPictureWidget::setScaleFactor(const int scale)
{
const int newWidth = BASE_WIDTH * scale / 100;
const int newHeight = static_cast<int>(newWidth * ASPECT_RATIO);
const QSize newSize = sizeForScaleFactor(scale);
scaleFactor = scale;
setFixedSize(newWidth, newHeight);
setFixedSize(newSize);
updatePixmap();
emit cardScaleFactorChanged(scale);
@ -266,8 +260,13 @@ void CardInfoPictureWidget::paintEvent(QPaintEvent *event)
*/
QSize CardInfoPictureWidget::sizeHint() const
{
return {static_cast<int>(BASE_WIDTH * scaleFactor / 100.0),
static_cast<int>(BASE_WIDTH * scaleFactor / 100.0 * ASPECT_RATIO)};
return sizeForScaleFactor(scaleFactor);
}
QSize CardInfoPictureWidget::sizeForScaleFactor(int scaleFactor)
{
const int width = BASE_WIDTH * scaleFactor / 100;
return {width, static_cast<int>(width * ASPECT_RATIO)};
}
/**

View file

@ -28,6 +28,11 @@ class CardInfoPictureWidget : public QWidget
Q_OBJECT
public:
/// Width of a card picture at 100 % scale, in pixels.
static constexpr int BASE_WIDTH = 200;
/// Height / width of a card picture.
static constexpr qreal ASPECT_RATIO = 1.396;
explicit CardInfoPictureWidget(QWidget *parent = nullptr,
bool hoverToZoomEnabled = false,
bool raiseOnEnter = false);
@ -37,6 +42,12 @@ public:
}
[[nodiscard]] QSize sizeHint() const override;
/**
* @brief The size a card picture has at @p scaleFactor percent.
* @param scaleFactor Percentage of BASE_WIDTH, as set by the card size slider.
*/
[[nodiscard]] static QSize sizeForScaleFactor(int scaleFactor);
public slots:
void setCard(const ExactCard &card);
void setScaleFactor(int scale); // New slot for scaling

View file

@ -0,0 +1,83 @@
#include "card_picture_delegate.h"
#include "../../../client/settings/cache_settings.h"
#include "../../card_picture_loader/card_picture_loader.h"
#include "card_art_utils.h"
#include "card_database_visual_display_model.h"
#include "libcockatrice/card/printing/exact_card.h"
#include "libcockatrice/settings/cards_display_settings.h"
#include <QPainter>
#include <QPainterPath>
#include <QPixmap>
#include <QRect>
#include <QStyleOptionViewItem>
#include <libcockatrice/card/printing/printing_info.h>
CardPictureDelegate::CardPictureDelegate(CardDatabaseVisualDisplayModel *_model, QObject *parent)
: QStyledItemDelegate(parent), model(_model)
{
}
void CardPictureDelegate::setCardSize(const QSize &size)
{
if (cardSize == size) {
return;
}
cardSize = size;
}
void CardPictureDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
if (model == nullptr) {
return;
}
const ExactCard card = model->cardAt(index.row());
if (!card) {
return;
}
// The grid cell is the card plus SPACING; give the picture the rest.
const int inset = SPACING / 2;
const QRect cardArea = option.rect.adjusted(inset, inset, -inset, -inset);
QPixmap pixmap;
CardPictureLoader::getCardBackLoadingInProgressPixmap(pixmap, cardArea.size());
CardPictureLoader::getPixmap(pixmap, card, cardArea.size());
if (pixmap.isNull()) {
return;
}
if (SettingsCache::instance().cardsDisplay().getAutoRotateSidewaysLayoutCards()) {
pixmap = CardArtUtils::rotateSidewaysLayoutArt(pixmap, card);
}
// Fit the picture into the cell and center it, like CardInfoPictureWidget does.
const QSize pictureSize = pixmap.deviceIndependentSize().toSize().scaled(cardArea.size(), Qt::KeepAspectRatio);
const QRect pictureRect(cardArea.center().x() - pictureSize.width() / 2,
cardArea.center().y() - pictureSize.height() / 2, pictureSize.width(),
pictureSize.height());
const qreal radius = SettingsCache::instance().cardsDisplay().getRoundCardCorners()
? 0.05 * static_cast<qreal>(pictureRect.width())
: 0.0;
painter->save();
painter->setRenderHint(QPainter::Antialiasing);
painter->setRenderHint(QPainter::SmoothPixmapTransform);
QPainterPath shape;
shape.addRoundedRect(pictureRect, radius, radius);
painter->setClipPath(shape);
// Source rect is in device pixels, target in logical ones: drawPixmap scales
// between them without the device pixel ratio being applied twice.
painter->drawPixmap(pictureRect, pixmap, QRectF(pixmap.rect()));
painter->restore();
}
QSize CardPictureDelegate::sizeHint(const QStyleOptionViewItem & /*option*/, const QModelIndex & /*index*/) const
{
return cardSize + QSize(SPACING, SPACING);
}

View file

@ -0,0 +1,45 @@
/**
* @file card_picture_delegate.h
* @ingroup CardWidgets
* @brief Paints a card picture for a visual card grid item.
*/
#ifndef CARD_PICTURE_DELEGATE_H
#define CARD_PICTURE_DELEGATE_H
#include <QSize>
#include <QStyledItemDelegate>
#include <qtmetamacros.h>
class CardDatabaseVisualDisplayModel;
/**
* @brief Draws the card of a CardDatabaseVisualDisplayModel row as a picture.
*
* The delegate only paints rows the view asks for, so a grid of the whole card
* database stays cheap: no widget per card and no pixmap loaded for items that
* are off screen. Card images come from CardPictureLoader, which caches them and
* loads missing ones asynchronously.
*/
class CardPictureDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
/// Gap left around a card picture inside its grid cell, in pixels.
static constexpr int SPACING = 4;
explicit CardPictureDelegate(CardDatabaseVisualDisplayModel *_model, QObject *parent = nullptr);
/// The picture size inside a grid cell. The cell is this size plus SPACING.
void setCardSize(const QSize &size);
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
[[nodiscard]] QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
private:
CardDatabaseVisualDisplayModel *model;
QSize cardSize;
};
#endif // CARD_PICTURE_DELEGATE_H

View file

@ -2,6 +2,7 @@
#include "../general/layout_containers/flow_widget.h"
#include <QAbstractScrollArea>
#include <QEvent>
#include <QFlags>
#include <QHBoxLayout>
@ -33,8 +34,8 @@ CardSizeWidget::CardSizeWidget(QWidget *parent, FlowWidget *_flowWidget, int def
cardSizeLabel->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
cardSizeSlider = new QSlider(Qt::Horizontal, this);
cardSizeSlider->setRange(50, 250); ///< Slider range for card size adjustment.
cardSizeSlider->setValue(defaultValue); ///< Initial slider value.
cardSizeSlider->setRange(MIN_CARD_SIZE, MAX_CARD_SIZE); ///< Slider range for card size adjustment.
cardSizeSlider->setValue(defaultValue); ///< Initial slider value.
cardSizeLayout->addWidget(cardSizeLabel);
cardSizeLayout->addWidget(cardSizeSlider);
@ -73,13 +74,26 @@ QSlider *CardSizeWidget::getSlider() const
void CardSizeWidget::enableCtrlScrollResize(QWidget *host)
{
host->installEventFilter(this);
const auto scrollAreas = host->findChildren<QScrollArea *>();
for (QScrollArea *scrollArea : scrollAreas) {
if (QWidget *content = scrollArea->widget()) {
content->installEventFilter(this);
const auto watchScrollArea = [this](QAbstractScrollArea *scrollArea) {
if (scrollArea == nullptr) {
return;
}
if (QWidget *viewport = scrollArea->viewport()) {
viewport->installEventFilter(this);
}
if (auto *contentScrollArea = qobject_cast<QScrollArea *>(scrollArea)) {
if (QWidget *content = contentScrollArea->widget()) {
content->installEventFilter(this);
}
}
};
host->installEventFilter(this);
watchScrollArea(qobject_cast<QAbstractScrollArea *>(host));
const auto scrollAreas = host->findChildren<QAbstractScrollArea *>();
for (QAbstractScrollArea *scrollArea : scrollAreas) {
watchScrollArea(scrollArea);
}
}

View file

@ -24,6 +24,10 @@ class CardSizeWidget : public QWidget
Q_OBJECT
public:
/// Range of the card size slider, as a percentage of the base card width.
static constexpr int MIN_CARD_SIZE = 50;
static constexpr int MAX_CARD_SIZE = 250;
explicit CardSizeWidget(QWidget *parent, FlowWidget *flowWidget = nullptr, int defaultValue = 100);
[[nodiscard]] QSlider *getSlider() const;
@ -31,8 +35,9 @@ public:
* @brief Resizes the cards when the user Ctrl + scrolls over @p host or any of its descendants.
*
* Installs this widget as an event filter on the given host widget. If the host contains a
* scroll area the filter is also installed on the scroll area's content widget, so the resize
* intercepts the wheel event before the scroll area would scroll the view.
* scroll area the filter is also installed on the scroll area's content widget, and on the
* viewport of every scroll area base (QListView, QTableView, ...), so the resize intercepts
* the wheel event before the scroll area would scroll the view.
*/
void enableCtrlScrollResize(QWidget *host);

View file

@ -32,7 +32,6 @@
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
#include <libcockatrice/protocol/pb/session_commands.pb.h>
#include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/utility/passwordhasher.h>
#include <libcockatrice/utility/qt_utils.h>
#include <string>
#include <sys/types.h>
@ -299,17 +298,7 @@ void UserInfoBox::actPassword()
void UserInfoBox::changePassword(const QString &oldPassword, const QString &newPassword)
{
Command_AccountPassword cmd;
cmd.set_old_password(oldPassword.toStdString());
if (client->getServerSupportsChallengeResponse()) {
cmd.set_hashed_new_password(PasswordHasher::generatePasswordVerifier(newPassword).toStdString());
} else if (client->getServerSupportsPasswordHash()) {
auto passwordSalt = PasswordHasher::generateRandomSalt();
QString hashedPassword = PasswordHasher::computeHash(newPassword, passwordSalt);
cmd.set_hashed_new_password(hashedPassword.toStdString());
} else {
cmd.set_new_password(newPassword.toStdString());
}
const Command_AccountPassword cmd = client->makeAccountPasswordCommand(oldPassword, newPassword);
PendingCommand *pend = client->prepareSessionCommand(cmd);
connect(pend, &PendingCommand::finished, this, &UserInfoBox::processPasswordResponse);

View file

@ -29,19 +29,10 @@ QMap<QString, std::pair<QString, UserConnection_Information>> UserConnection_Inf
ServersSettings &servers = SettingsCache::instance().servers();
int size = servers.getValue("totalServers", "server", "server_details").toInt() + 1;
for (int i = 0; i < size; i++) {
QString _saveName = servers.getValue(QString("saveName%1").arg(i), "server", "server_details").toString();
QString serverName = servers.getValue(QString("server%1").arg(i), "server", "server_details").toString();
QString portNum = servers.getValue(QString("port%1").arg(i), "server", "server_details").toString();
QString userName = servers.getValue(QString("username%1").arg(i), "server", "server_details").toString();
QString pass = servers.getValue(QString("password%1").arg(i), "server", "server_details").toString();
bool savePass = servers.getValue(QString("savePassword%1").arg(i), "server", "server_details").toBool();
QString _site = servers.getValue(QString("site%1").arg(i), "server", "server_details").toString();
UserConnection_Information userInfo(_saveName, serverName, portNum, userName, pass, savePass, _site);
serverList.insert(_saveName, std::make_pair(serverName, userInfo));
for (const auto &entry : servers.getServers()) {
UserConnection_Information userInfo(entry.saveName, entry.host, entry.port, entry.username, entry.password,
entry.savePassword, entry.site);
serverList.insert(entry.saveName, std::make_pair(entry.host, userInfo));
}
return serverList;
@ -53,28 +44,18 @@ QStringList UserConnection_Information::getServerInfo(const QString &find)
ServersSettings &servers = SettingsCache::instance().servers();
int size = servers.getValue("totalServers", "server", "server_details").toInt() + 1;
for (int i = 0; i < size; i++) {
QString _saveName = servers.getValue(QString("saveName%1").arg(i), "server", "server_details").toString();
if (find != _saveName) {
for (const auto &entry : servers.getServers()) {
if (find != entry.saveName) {
continue;
}
QString serverName = servers.getValue(QString("server%1").arg(i), "server", "server_details").toString();
QString portNum = servers.getValue(QString("port%1").arg(i), "server", "server_details").toString();
QString userName = servers.getValue(QString("username%1").arg(i), "server", "server_details").toString();
QString pass = servers.getValue(QString("password%1").arg(i), "server", "server_details").toString();
bool savePass = servers.getValue(QString("savePassword%1").arg(i), "server", "server_details").toBool();
QString _site = servers.getValue(QString("site%1").arg(i), "server", "server_details").toString();
_server.append(_saveName);
_server.append(serverName);
_server.append(portNum);
_server.append(userName);
_server.append(pass);
_server.append(savePass ? "1" : "0");
_server.append(_site);
_server.append(entry.saveName);
_server.append(entry.host);
_server.append(entry.port);
_server.append(entry.username);
_server.append(entry.password);
_server.append(entry.savePassword ? "1" : "0");
_server.append(entry.site);
break;
}

View file

@ -13,6 +13,7 @@
#include <QMessageBox>
#include <QPushButton>
#include <QRadioButton>
#include <QScrollBar>
#include <QSharedPointer>
#include <QSizePolicy>
#include <QSpinBox>
@ -21,6 +22,7 @@
#include <QTableWidgetItem>
#include <QVBoxLayout>
#include <QWidget>
#include <QtGlobal>
#include <libcockatrice/network/client/abstract/abstract_client.h>
#include <libcockatrice/protocol/pb/moderator_commands.pb.h>
#include <libcockatrice/protocol/pb/response_viewlog_history.pb.h>
@ -31,6 +33,17 @@
class TabSupervisor;
namespace
{
/** @brief Identifies the newest row of a page well enough to recognise a repeated page. */
QString pageStartKey(const Response_ViewLogHistory &response)
{
const ServerInfo_ChatMessage &first = response.log_message(0);
return QString::fromStdString(first.time()) + QChar(0x1f) + QString::fromStdString(first.sender_name()) +
QChar(0x1f) + QString::fromStdString(first.message());
}
} // namespace
TabLog::TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client, bool _canUseDeveloperCommands)
: Tab(_tabSupervisor), client(_client), canUseDeveloperCommands(_canUseDeveloperCommands)
{
@ -52,12 +65,20 @@ TabLog::TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client, bool _can
chatTable->setHorizontalHeaderLabels(
QString(tr("Time;SenderName;SenderIP;Message;TargetID;TargetName")).split(";"));
QTabWidget *tabManager = new QTabWidget();
tabManager = new QTabWidget();
tabManager->addTab(roomTable, tr("Room Logs"));
tabManager->addTab(gameTable, tr("Game Logs"));
tabManager->addTab(chatTable, tr("Chat Logs"));
setCentralWidget(tabManager);
connect(roomTable->verticalScrollBar(), &QScrollBar::valueChanged, this, &TabLog::checkFetchMore);
connect(gameTable->verticalScrollBar(), &QScrollBar::valueChanged, this, &TabLog::checkFetchMore);
connect(chatTable->verticalScrollBar(), &QScrollBar::valueChanged, this, &TabLog::checkFetchMore);
// A tab that gains only a few rows has nothing to scroll, so its scrollbar never fires:
// switching to it would leave its next page unfetched until the moderator scrolled
// another tab. Re-check on every tab switch so the visible table can continue the browse.
connect(tabManager, &QTabWidget::currentChanged, this, &TabLog::checkFetchMore);
createDock();
restartLayout();
clearClicked();
@ -74,12 +95,9 @@ void TabLog::retranslateUi()
void TabLog::getClicked()
{
if (findUsername->text().isEmpty() && findIPAddress->text().isEmpty() && findGameName->text().isEmpty() &&
findGameID->text().isEmpty() && findMessage->text().isEmpty()) {
QMessageBox::critical(this, tr("Error"), tr("You must select at least one filter."));
return;
}
// Empty filters are a valid browse: room/game history going back from
// today, paginated. Only require a date range + location, both of which
// get sensible defaults below so a bare "Get" just works.
if (!lastHour->isChecked() && !today->isChecked() && !pastDays->isChecked()) {
pastDays->setChecked(true);
pastXDays->setValue(20);
@ -99,39 +117,71 @@ void TabLog::getClicked()
}
if (maximumResults->value() == 0) {
maximumResults->setValue(1000);
maximumResults->setValue(200);
}
pageSize = qBound(1, maximumResults->value(), 1000);
int dateRange = 0;
if (lastHour->isChecked()) {
dateRange = 1;
}
if (today->isChecked()) {
dateRange = 24;
}
if (pastDays->isChecked()) {
dateRange = pastXDays->value() * 24;
}
Command_ViewLogHistory cmd;
cmd.set_user_name(findUsername->text().toStdString());
cmd.set_ip_address(findIPAddress->text().toStdString());
cmd.set_game_name(findGameName->text().toStdString());
cmd.set_game_id(findGameID->text().toStdString());
cmd.set_message(findMessage->text().toStdString());
// Build the command once, here, and let every page copy it: rebuilding it from the
// widgets per page would run page 2+ against filters edited after Get was clicked,
// appending rows selected by a different query under the first page's results.
currentQuery.Clear();
currentQuery.set_user_name(findUsername->text().toStdString());
currentQuery.set_ip_address(findIPAddress->text().toStdString());
currentQuery.set_game_name(findGameName->text().toStdString());
currentQuery.set_game_id(findGameID->text().toStdString());
currentQuery.set_message(findMessage->text().toStdString());
if (mainRoom->isChecked()) {
cmd.add_log_location("room");
};
currentQuery.add_log_location("room");
}
if (gameRoom->isChecked()) {
cmd.add_log_location("game");
};
currentQuery.add_log_location("game");
}
if (privateChat->isChecked()) {
cmd.add_log_location("chat");
};
cmd.set_date_range(dateRange);
cmd.set_maximum_results(maximumResults->value());
currentQuery.add_log_location("chat");
}
currentQuery.set_date_range(dateRangeForSelection());
currentQuery.set_maximum_results(pageSize);
hasCurrentQuery = true;
snapshotBefore.clear();
lastPageStartKey.clear();
// Fresh search: reset pagination state and clear previous pages.
currentOffset = 0;
roomCounter = 0;
gameCounter = 0;
chatCounter = 0;
roomTable->setRowCount(0);
gameTable->setRowCount(0);
chatTable->setRowCount(0);
fetchPage(0);
}
int TabLog::dateRangeForSelection() const
{
if (lastHour->isChecked()) {
return 1;
}
if (today->isChecked()) {
return 24;
}
if (pastDays->isChecked()) {
return pastXDays->value() * 24;
}
return 0;
}
void TabLog::fetchPage(int offset)
{
if (isFetching || !hasCurrentQuery) {
return;
}
Command_ViewLogHistory cmd = currentQuery;
cmd.set_offset(offset);
if (!snapshotBefore.isEmpty()) {
cmd.set_before_time(snapshotBefore.toStdString());
}
PendingCommand *pend;
if (canUseDeveloperCommands) {
@ -141,10 +191,42 @@ void TabLog::getClicked()
pend = client->prepareModeratorCommand(cmd);
}
isFetching = true;
getButton->setEnabled(false);
connect(pend, &PendingCommand::finished, this, &TabLog::viewLogHistory_processResponse);
client->sendCommand(pend);
}
void TabLog::checkFetchMore()
{
if (isFetching || !hasMoreResults) {
return;
}
// Only the table the moderator is looking at may pull the next page. Checking all
// three would make an empty or hidden table nearBottom by definition (maximum() and
// value() are both zero), so a room-only browse chained page after page without any
// scrolling - pulling the whole range with a growing OFFSET scan per page, and
// looping forever against a server that ignores `offset`.
auto *table = qobject_cast<QTableWidget *>(tabManager->currentWidget());
if (table == nullptr || table->rowCount() == 0) {
return;
}
// ScrollPerItem (the default) counts rows, not pixels: `value` is the top visible row
// and `maximum` the last one that can be scrolled to. Prefetch half a page ahead so
// scrolling never visibly stalls, without the fixed 300-row threshold firing the next
// page the moment a page loads.
const QScrollBar *bar = table->verticalScrollBar();
const int prefetchRows = qMax(1, pageSize / 2);
if (bar->maximum() - bar->value() > prefetchRows) {
return;
}
fetchPage(currentOffset);
}
void TabLog::clearClicked()
{
findUsername->clear();
@ -166,6 +248,10 @@ void TabLog::clearClicked()
pastDays->setAutoExclusive(true);
today->setAutoExclusive(true);
lastHour->setAutoExclusive(true);
currentOffset = 0;
hasMoreResults = false;
hasCurrentQuery = false;
snapshotBefore.clear();
}
void TabLog::createDock()
@ -209,14 +295,15 @@ void TabLog::createDock()
pastXDays = new QSpinBox;
pastXDays->setMaximum(20);
labelMaximum = new QLabel(tr("Maximum Results: "));
labelMaximum = new QLabel(tr("Results per page: "));
maximumResults = new QSpinBox;
maximumResults->setMaximum(1000);
maximumResults->setToolTip(tr("Page size for smooth scrolling: more rows load automatically as you scroll."));
labelDescription = new QLabel(tr(
"At least one filter is required.\nThe more information you put in, the more specific your results will be."));
labelDescription = new QLabel(tr("Leave filters empty to browse recent room and game messages.\n"
"More rows load automatically as you scroll."));
getButton = new QPushButton(tr("Get User Logs"));
getButton = new QPushButton(tr("Get Logs"));
getButton->setAutoDefault(true);
connect(getButton, &QPushButton::clicked, this, &TabLog::getClicked);
@ -262,7 +349,7 @@ void TabLog::createDock()
maxResultsGrid->addWidget(labelMaximum, 0, 0);
maxResultsGrid->addWidget(maximumResults, 0, 1);
maxResultsGroupBox = new QGroupBox(tr("Maximum Results"));
maxResultsGroupBox = new QGroupBox(tr("Results per page"));
maxResultsGroupBox->setLayout(maxResultsGrid);
descriptionGrid = new QGridLayout;
@ -295,84 +382,134 @@ void TabLog::createDock()
searchDock->setWidget(searchDockContents);
}
void TabLog::appendChatMessage(QTableWidget *table,
int &counter,
const QString &time,
const QString &senderName,
const QString &senderIp,
const QString &messageText,
const QString &targetId,
const QString &targetName,
const QString &channel)
{
table->insertRow(counter);
table->setItem(counter, 0, new QTableWidgetItem(time));
table->setItem(counter, 1, new QTableWidgetItem(senderName));
table->setItem(counter, 2, new QTableWidgetItem(senderIp));
table->setItem(counter, 3, new QTableWidgetItem(messageText));
table->setItem(counter, 4, new QTableWidgetItem(targetId));
table->setItem(counter, 5, new QTableWidgetItem(targetName));
if (table->columnCount() > 6) {
table->setItem(counter, 6, new QTableWidgetItem(channel));
}
++counter;
}
void TabLog::viewLogHistory_processResponse(const Response &resp)
{
const Response_ViewLogHistory &response = resp.GetExtension(Response_ViewLogHistory::ext);
const bool isFirstPage = (currentOffset == 0);
isFetching = false;
getButton->setEnabled(true);
if (resp.response_code() != Response::RespOk) {
QMessageBox::critical(static_cast<QWidget *>(parent()), tr("Message History"),
tr("Failed to collect message history information."));
return;
}
if (response.log_message_size() == 0) {
if (isFirstPage && response.log_message_size() == 0) {
QMessageBox::information(static_cast<QWidget *>(parent()), tr("Message History"),
tr("There are no messages for the selected filters."));
hasMoreResults = false;
return;
}
int roomCounter = 0, gameCounter = 0, chatCounter = 0;
roomTable->setRowCount(roomCounter);
gameTable->setRowCount(gameCounter);
chatTable->setRowCount(chatCounter);
const QString thisPageStart = response.log_message_size() > 0 ? pageStartKey(response) : QString();
if (isFirstPage) {
// Rows come newest first, so the first one fixes the snapshot every later page is
// bounded by. Without it, each message logged between two page fetches pushes the
// window down by one and repeats a row the moderator already has.
snapshotBefore = QString::fromStdString(response.log_message(0).time());
} else if (!response.has_has_more() && !thisPageStart.isEmpty() && thisPageStart == lastPageStartKey) {
// A server that predates `offset` ignores it and returns the same page again. It
// also predates `has_more`, so the full-page heuristic below can never prove
// exhaustion and every scroll would append the same rows forever. The identical
// first row is the proof; stop instead of looping.
hasMoreResults = false;
return;
}
lastPageStartKey = thisPageStart;
for (int i = 0; i < response.log_message_size(); ++i) {
ServerInfo_ChatMessage message = response.log_message(i);
const QString targetType = QString::fromStdString(message.target_type());
if (targetType == "room" || targetType == "room_channel") {
roomTable->insertRow(roomCounter);
roomTable->setItem(roomCounter, 0, new QTableWidgetItem(QString::fromStdString(message.time())));
roomTable->setItem(roomCounter, 1, new QTableWidgetItem(QString::fromStdString(message.sender_name())));
roomTable->setItem(roomCounter, 2, new QTableWidgetItem(QString::fromStdString(message.sender_ip())));
roomTable->setItem(roomCounter, 3, new QTableWidgetItem(QString::fromStdString(message.message())));
roomTable->setItem(roomCounter, 4, new QTableWidgetItem(QString::fromStdString(message.target_id())));
roomTable->setItem(roomCounter, 5, new QTableWidgetItem(QString::fromStdString(message.target_name())));
roomTable->setItem(roomCounter, 6, new QTableWidgetItem(QString::fromStdString(message.channel())));
++roomCounter;
appendChatMessage(roomTable, roomCounter, QString::fromStdString(message.time()),
QString::fromStdString(message.sender_name()),
QString::fromStdString(message.sender_ip()), QString::fromStdString(message.message()),
QString::fromStdString(message.target_id()),
QString::fromStdString(message.target_name()), QString::fromStdString(message.channel()));
}
if (targetType == "game") {
gameTable->insertRow(gameCounter);
gameTable->setItem(gameCounter, 0, new QTableWidgetItem(QString::fromStdString(message.time())));
gameTable->setItem(gameCounter, 1, new QTableWidgetItem(QString::fromStdString(message.sender_name())));
gameTable->setItem(gameCounter, 2, new QTableWidgetItem(QString::fromStdString(message.sender_ip())));
gameTable->setItem(gameCounter, 3, new QTableWidgetItem(QString::fromStdString(message.message())));
gameTable->setItem(gameCounter, 4, new QTableWidgetItem(QString::fromStdString(message.target_id())));
gameTable->setItem(gameCounter, 5, new QTableWidgetItem(QString::fromStdString(message.target_name())));
++gameCounter;
appendChatMessage(gameTable, gameCounter, QString::fromStdString(message.time()),
QString::fromStdString(message.sender_name()),
QString::fromStdString(message.sender_ip()), QString::fromStdString(message.message()),
QString::fromStdString(message.target_id()),
QString::fromStdString(message.target_name()));
}
if (targetType == "chat") {
chatTable->insertRow(chatCounter);
chatTable->setItem(chatCounter, 0, new QTableWidgetItem(QString::fromStdString(message.time())));
chatTable->setItem(chatCounter, 1, new QTableWidgetItem(QString::fromStdString(message.sender_name())));
chatTable->setItem(chatCounter, 2, new QTableWidgetItem(QString::fromStdString(message.sender_ip())));
chatTable->setItem(chatCounter, 3, new QTableWidgetItem(QString::fromStdString(message.message())));
chatTable->setItem(chatCounter, 4, new QTableWidgetItem(QString::fromStdString(message.target_id())));
chatTable->setItem(chatCounter, 5, new QTableWidgetItem(QString::fromStdString(message.target_name())));
++chatCounter;
appendChatMessage(chatTable, chatCounter, QString::fromStdString(message.time()),
QString::fromStdString(message.sender_name()),
QString::fromStdString(message.sender_ip()), QString::fromStdString(message.message()),
QString::fromStdString(message.target_id()),
QString::fromStdString(message.target_name()));
}
}
currentOffset += response.log_message_size();
if (response.has_has_more()) {
hasMoreResults = response.has_more();
} else {
// Legacy server without has_more: a full page implies more may exist.
hasMoreResults = (response.log_message_size() >= pageSize);
}
if (roomCounter) {
roomTable->show();
roomTable->resizeColumnsToContents();
if (isFirstPage) {
roomTable->resizeColumnsToContents();
}
} else {
roomTable->hide();
}
if (gameCounter) {
gameTable->resizeColumnsToContents();
if (isFirstPage) {
gameTable->resizeColumnsToContents();
}
gameTable->show();
} else {
gameTable->hide();
}
if (chatCounter) {
chatTable->resizeColumnsToContents();
if (isFirstPage) {
chatTable->resizeColumnsToContents();
}
chatTable->show();
} else {
chatTable->hide();
}
// The new rows may already fill less than a viewport (or the user is
// still parked at the bottom), so chain the next page without waiting
// for another scroll event. The prefetch stays invisible: rows simply
// keep appearing as you scroll.
checkFetchMore();
}
void TabLog::restartLayout()

View file

@ -10,6 +10,7 @@
#include "tab.h"
#include <QString>
#include <libcockatrice/protocol/pb/moderator_commands.pb.h>
#include <qtmetamacros.h>
class AbstractClient;
@ -25,6 +26,7 @@ class QWidget;
class QGridLayout;
class QVBoxLayout;
class QTableWidget;
class QTabWidget;
class Response;
class TabSupervisor;
@ -48,8 +50,37 @@ private:
*buttonGroupBox;
QVBoxLayout *mainLayout;
QTableWidget *roomTable, *gameTable, *chatTable;
QTabWidget *tabManager = nullptr;
//! The filters of the search in flight, captured when Get was clicked so that
//! later pages cannot pick up edits the moderator made after starting it.
Command_ViewLogHistory currentQuery;
bool hasCurrentQuery = false;
//! Newest `log_time` of the first page, sent back as the upper bound of every
//! later page so messages logged in the meantime do not shift the window.
QString snapshotBefore;
//! Identity of the previous page's first row, used to spot a server that ignores
//! `offset` and hands back the same page forever.
QString lastPageStartKey;
int currentOffset = 0;
int pageSize = 200;
int roomCounter = 0, gameCounter = 0, chatCounter = 0;
bool isFetching = false;
bool hasMoreResults = false;
void createDock();
[[nodiscard]] int dateRangeForSelection() const;
void fetchPage(int offset);
void appendChatMessage(QTableWidget *table,
int &counter,
const QString &time,
const QString &senderName,
const QString &senderIp,
const QString &messageText,
const QString &targetId,
const QString &targetName,
const QString &channel = QString());
void checkFetchMore();
signals:
private slots:

View file

@ -0,0 +1,203 @@
#include "card_database_visual_view.h"
#include "../../card_picture_loader/card_picture_loader.h"
#include "../cards/card_info_picture_widget.h"
#include "../cards/card_picture_delegate.h"
#include "../cards/card_size_widget.h"
#include "card_database_visual_display_model.h"
#include "libcockatrice/card/printing/exact_card.h"
#include <QAbstractItemModel>
#include <QAbstractItemView>
#include <QApplication>
#include <QCursor>
#include <QEvent>
#include <QFrame>
#include <QModelIndex>
#include <QMouseEvent>
#include <QScrollBar>
#include <QSize>
#include <QWheelEvent>
#include <QWidget>
#include <QtGlobal>
#include <qnamespace.h>
CardDatabaseVisualView::CardDatabaseVisualView(CardDatabaseVisualDisplayModel *_model, QWidget *parent)
: QListView(parent), model(_model)
{
auto *cardDelegate = new CardPictureDelegate(model, this);
setItemDelegate(cardDelegate);
setModel(model);
setViewMode(QListView::IconMode);
setFlow(QListView::LeftToRight);
setWrapping(true);
setResizeMode(QListView::Adjust);
setMovement(QListView::Static);
setUniformItemSizes(true);
setLayoutMode(QListView::SinglePass);
setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setFrameShape(QFrame::NoFrame);
setWordWrap(false);
// QListView hands the delegate's item height to the scroll bar as its single
// step under ScrollPerPixel, so one wheel notch would jump wheelScrollLines()
// whole card rows. Give the bar a text line instead; the wheel handler below
// scales that line to the grid. A positive step also stops Qt from taking the
// item height back on the next layout.
verticalScrollBar()->setSingleStep(qMax(1, fontMetrics().height()));
// The grid is a read-only surface: clicks are actions on a card, not a
// selection, and they must not steal focus from the search field.
setSelectionMode(QAbstractItemView::NoSelection);
setEditTriggers(QAbstractItemView::NoEditTriggers);
setFocusPolicy(Qt::NoFocus);
// Hover drives the card preview.
setMouseTracking(true);
viewport()->setMouseTracking(true);
connect(&CardPictureLoader::getInstance(), &CardPictureLoader::pixmapLoaded, this,
[this] { viewport()->update(); });
// A new search result starts at the top, like the widget-per-card grid did.
connect(model, &QAbstractItemModel::modelReset, this, [this] {
verticalScrollBar()->setValue(0);
// The rows under a stationary cursor changed with the reset: re-resolve the
// hover so the preview follows the grid, as enterEvent did per card widget.
hoveredRow = -1;
updateHoveredRow(viewport()->mapFromGlobal(QCursor::pos()));
});
setCardSizeFactor(cardSizeFactor);
}
void CardDatabaseVisualView::setCardSizeFactor(int factor)
{
cardSizeFactor = qBound(CardSizeWidget::MIN_CARD_SIZE, factor, CardSizeWidget::MAX_CARD_SIZE);
const QSize cardSize = CardInfoPictureWidget::sizeForScaleFactor(cardSizeFactor);
if (auto *cardDelegate = qobject_cast<CardPictureDelegate *>(itemDelegate())) {
cardDelegate->setCardSize(cardSize);
}
// The delegate's sizeHint does not emit sizeHintChanged, so the grid size is what
// makes a resize take effect. Keep the two in step.
setGridSize(cardSize + QSize(CardPictureDelegate::SPACING, CardPictureDelegate::SPACING));
}
void CardDatabaseVisualView::mousePressEvent(QMouseEvent *event)
{
const QModelIndex index = indexAt(event->pos());
if (index.isValid() && model != nullptr) {
const ExactCard card = model->cardAt(index.row());
if (card) {
emit cardClicked(event, card);
event->accept();
return;
}
}
QListView::mousePressEvent(event);
}
void CardDatabaseVisualView::mouseMoveEvent(QMouseEvent *event)
{
QListView::mouseMoveEvent(event);
updateHoveredRow(event->pos());
}
bool CardDatabaseVisualView::viewportEvent(QEvent *event)
{
// A per-card widget reported its hover from enterEvent, which also fires when the
// grid changes under a stationary cursor. A QListView only reports moves, so
// re-resolve the hover on enter and clear it on leave.
if (event->type() == QEvent::Enter) {
updateHoveredRow(viewport()->mapFromGlobal(QCursor::pos()));
} else if (event->type() == QEvent::Leave) {
hoveredRow = -1;
}
return QListView::viewportEvent(event);
}
void CardDatabaseVisualView::wheelEvent(QWheelEvent *event)
{
// Ctrl+wheel is the card-size gesture and is filtered out before it reaches
// the view; any other modifier keeps QAbstractSlider's own page scrolling.
if (event->modifiers() != Qt::NoModifier) {
QListView::wheelEvent(event);
return;
}
QScrollBar *bar = verticalScrollBar();
if (!bar->isVisible()) {
event->ignore();
return;
}
// Touchpads and high-resolution wheels already report pixels: follow them.
if (!event->pixelDelta().isNull()) {
bar->setValue(bar->value() - event->pixelDelta().y());
event->accept();
return;
}
if (event->angleDelta().y() == 0) {
QListView::wheelEvent(event);
return;
}
// Accumulate wheel deltas smaller than a notch, like QAbstractSlider does, so
// a fine-grained wheel still moves.
wheelRemainder += static_cast<qreal>(event->angleDelta().y()) / 120.0;
const int notches = static_cast<int>(wheelRemainder);
if (notches == 0) {
event->accept();
return;
}
wheelRemainder -= notches;
const int pixels = -notches * wheelStep();
bar->setValue(qBound(bar->minimum(), bar->value() + pixels, bar->maximum()));
event->accept();
}
int CardDatabaseVisualView::wheelStep() const
{
// A card row is far too coarse for one wheel line, and a text line is too
// fine, so a line is an eighth of the grid cell. A notch then moves about a
// third of a card row at any card size, and the OS line count still applies.
const int lineHeight = qMax(1, gridSize().height() / 8);
return qMax(1, QApplication::wheelScrollLines()) * lineHeight;
}
void CardDatabaseVisualView::updateHoveredRow(const QPoint &position)
{
// indexAt() also answers for points outside the viewport, so a cursor parked
// below the grid (on the size slider, say) would resolve to the row just under
// the visible area. Only a point inside the viewport can hover a card.
if (model == nullptr || !viewport()->rect().contains(position)) {
return;
}
const QModelIndex index = indexAt(position);
const int row = index.isValid() ? index.row() : -1;
if (row == hoveredRow) {
return;
}
hoveredRow = row;
if (row >= 0) {
const ExactCard card = model->cardAt(row);
if (card) {
emit cardHovered(card);
}
}
}
void CardDatabaseVisualView::leaveEvent(QEvent *event)
{
hoveredRow = -1;
QListView::leaveEvent(event);
}

View file

@ -0,0 +1,67 @@
/**
* @file card_database_visual_view.h
* @ingroup VisualCardDatabaseWidgets
* @brief Icon-mode view of the card database, one card picture per item.
*/
#ifndef CARD_DATABASE_VISUAL_VIEW_H
#define CARD_DATABASE_VISUAL_VIEW_H
#include <QListView>
#include <QSize>
#include <qtmetamacros.h>
class CardDatabaseVisualDisplayModel;
class ExactCard;
class QEvent;
class QMouseEvent;
class QPoint;
class QWheelEvent;
/**
* @brief A wrapping grid of card pictures.
*
* The view virtualizes painting: it only asks the delegate for the items that are
* on screen, so the whole card database can sit in the model without a widget (or
* pixmap) per card. Clicks and hover are reported with the exact card under the
* cursor, which is what the deck editor needs to add, decrement or preview a card.
*/
class CardDatabaseVisualView : public QListView
{
Q_OBJECT
public:
explicit CardDatabaseVisualView(CardDatabaseVisualDisplayModel *_model, QWidget *parent = nullptr);
/**
* @brief Sets the card size as a percentage of the base card width.
* @param factor Percentage in the range of the card size slider.
*/
void setCardSizeFactor(int factor);
signals:
/** @brief A card was pressed. The event is valid for the duration of the emission. */
void cardClicked(QMouseEvent *event, const ExactCard &card);
/** @brief The cursor moved onto a different card. */
void cardHovered(const ExactCard &card);
protected:
void mousePressEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void leaveEvent(QEvent *event) override;
bool viewportEvent(QEvent *event) override;
void wheelEvent(QWheelEvent *event) override;
private:
void updateHoveredRow(const QPoint &position);
/// Pixels one wheel notch moves, scaled to the grid and the OS line count.
[[nodiscard]] int wheelStep() const;
CardDatabaseVisualDisplayModel *model;
/// Fraction of a line left over from a wheel event smaller than one notch.
qreal wheelRemainder = 0.0;
int cardSizeFactor = 100;
int hoveredRow = -1;
};
#endif // CARD_DATABASE_VISUAL_VIEW_H

View file

@ -5,40 +5,29 @@
#include "../../../filters/syntax_help.h"
#include "../../key_signals.h"
#include "../../pixel_map_generator.h"
#include "../cards/card_info_picture_widget.h"
#include "../cards/card_info_picture_with_text_overlay_widget.h"
#include "../cards/card_size_widget.h"
#include "../deck_editor/card_database_view.h"
#include "../general/layout_containers/flow_widget.h"
#include "../utility/custom_line_edit.h"
#include "card_database_display_model.h"
#include "card_database_model.h"
#include "card_database_visual_display_model.h"
#include "card_database_visual_view.h"
#include "deck_list_model.h"
#include "libcockatrice/card/card_localization.h"
#include "libcockatrice/card/database/card_database_loader.h"
#include "libcockatrice/card/database/card_database_querier.h"
#include "libcockatrice/card/printing/exact_card.h"
#include "libcockatrice/card/printing/printing_info.h"
#include "libcockatrice/filters/filter_card.h"
#include "libcockatrice/filters/filter_tree.h"
#include "visual_database_display_color_filter_widget.h"
#include "visual_database_display_filter_toolbar_widget.h"
#include <QAbstractItemModel>
#include <QAction>
#include <QDebug>
#include <QIcon>
#include <QLabel>
#include <QLineEdit>
#include <QList>
#include <QMessageLogger>
#include <QModelIndex>
#include <QPair>
#include <QPersistentModelIndex>
#include <QPushButton>
#include <QScrollArea>
#include <QScrollBar>
#include <QSharedPointer>
#include <QSizePolicy>
#include <QSlider>
#include <QStringLiteral>
@ -48,14 +37,11 @@
#include <QVBoxLayout>
#include <QVariant>
#include <QtGlobal>
#include <algorithm>
#include <libcockatrice/card/card_info_comparator.h>
#include <libcockatrice/card/database/card_database.h>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/deck_list/tree/inner_deck_list_node.h>
#include <libcockatrice/settings/cards_display_settings.h>
#include <libcockatrice/utility/qt_utils.h>
#include <utility>
class QMouseEvent;
@ -64,11 +50,6 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent,
DeckListModel *deckListModel)
: QWidget(parent)
{
debounceTimer = new QTimer(this);
debounceTimer->setSingleShot(true); // Ensure it only fires once after the timeout
connect(debounceTimer, &QTimer::timeout, this, &VisualDatabaseDisplayWidget::onSearchModelChanged);
// Create display model
databaseDisplayModel = new CardDatabaseDisplayModel(this);
databaseDisplayModel->setObjectName("databaseDisplayModel");
@ -86,23 +67,12 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent,
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardSearchLanguageChanged, this,
applyCardSearchLanguage);
cards = new QList<ExactCard>;
connect(databaseDisplayModel, &CardDatabaseDisplayModel::modelDirty, this,
&VisualDatabaseDisplayWidget::modelDirty);
// Set up main layout and widgets
setMinimumSize(0, 0);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
mainLayout = QtUtils::makeLayout<QVBoxLayout>(this, 0, 1);
setLayout(mainLayout);
flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarPolicy::ScrollBarAsNeeded);
cardSizeWidget = new CardSizeWidget(this, flowWidget,
SettingsCache::instance().cardsDisplay().getVisualDatabaseDisplayCardSize());
connect(cardSizeWidget, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance().cardsDisplay(),
&CardsDisplaySettings::setVisualDatabaseDisplayCardSize);
cardSizeWidget->enableCtrlScrollResize(flowWidget);
searchContainer = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAlwaysOff);
searchEdit = new SearchLineEdit();
@ -159,6 +129,25 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent,
connect(databaseView, &CardDatabaseView::relatedCardClicked, this,
&VisualDatabaseDisplayWidget::onRelatedCardClicked);
// The visual grid is a real view on a flattened model: one row per displayed
// card picture, painted on demand.
visualDisplayModel = new CardDatabaseVisualDisplayModel(databaseDisplayModel, CardDatabaseManager::query(), this);
cardView = new CardDatabaseVisualView(visualDisplayModel, this);
cardView->setObjectName("visualCardView");
connect(cardView, &CardDatabaseVisualView::cardClicked, this,
&VisualDatabaseDisplayWidget::cardClickedDatabaseDisplay);
connect(cardView, &CardDatabaseVisualView::cardHovered, this,
&VisualDatabaseDisplayWidget::cardHoveredDatabaseDisplay);
const int cardSize = SettingsCache::instance().cardsDisplay().getVisualDatabaseDisplayCardSize();
cardSizeWidget = new CardSizeWidget(this, nullptr, cardSize);
connect(cardSizeWidget, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance().cardsDisplay(),
&CardsDisplaySettings::setVisualDatabaseDisplayCardSize);
connect(cardSizeWidget->getSlider(), &QSlider::valueChanged, cardView, &CardDatabaseVisualView::setCardSizeFactor);
cardView->setCardSizeFactor(cardSize);
cardSizeWidget->enableCtrlScrollResize(cardView);
colorFilterWidget = new VisualDatabaseDisplayColorFilterWidget(this, filterModel);
filterContainer = new VisualDatabaseDisplayFilterToolbarWidget(this, deckListModel);
@ -191,8 +180,6 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent,
databaseLoadIndicator->setVisible(false);
}
QScrollBar *scrollBar = flowWidget->scrollArea->verticalScrollBar();
connect(scrollBar, &QScrollBar::valueChanged, [this](int /*value*/) { loadCurrentPage(); });
retranslateUi();
}
@ -211,7 +198,7 @@ void VisualDatabaseDisplayWidget::initialize()
mainLayout->addWidget(databaseView);
mainLayout->addWidget(flowWidget);
mainLayout->addWidget(cardView);
mainLayout->addWidget(cardSizeWidget);
@ -242,8 +229,6 @@ void VisualDatabaseDisplayWidget::initializeFilters()
databaseDisplayModel->setFilterTree(filterModel->filterTree());
QTimer::singleShot(5000, this, [this] { loadCurrentPage(); });
retranslateUi();
});
}
@ -279,71 +264,36 @@ void VisualDatabaseDisplayWidget::updateDisplayModeButton()
displayModeButton->setText(displayModeButton->isChecked() ? tr("Table") : tr("Visual"));
}
void VisualDatabaseDisplayWidget::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
loadCurrentPage();
}
void VisualDatabaseDisplayWidget::onDisplayModeChanged(bool checked)
{
updateDisplayModeButton();
if (checked) {
// Table mode
flowWidget->setVisible(false);
cardView->setVisible(false);
cardSizeWidget->setVisible(false);
databaseView->setItemDelegate(new QStyledItemDelegate(databaseView));
databaseView->setVisible(true);
} else {
// Visual mode
flowWidget->setVisible(true);
cardView->setVisible(true);
cardSizeWidget->setVisible(true);
databaseView->setVisible(false);
populateCards();
}
}
void VisualDatabaseDisplayWidget::onClick(QMouseEvent *event, const ExactCard &card)
{
emit cardClickedDatabaseDisplay(event, card);
}
void VisualDatabaseDisplayWidget::onHover(const ExactCard &hoveredCard)
{
emit cardHoveredDatabaseDisplay(hoveredCard);
}
void VisualDatabaseDisplayWidget::addCardToDisplay(const ExactCard &cardToAdd)
{
cards->append(cardToAdd);
auto *display = new CardInfoPictureWithTextOverlayWidget(flowWidget, false);
display->setScaleFactor(cardSizeWidget->getSlider()->value());
display->setCard(cardToAdd);
flowWidget->addWidget(display);
connect(display, &CardInfoPictureWithTextOverlayWidget::cardClicked, this, &VisualDatabaseDisplayWidget::onClick);
connect(display, &CardInfoPictureWithTextOverlayWidget::hoveredOnCard, this, &VisualDatabaseDisplayWidget::onHover);
connect(cardSizeWidget->getSlider(), &QSlider::valueChanged, display, &CardInfoPictureWidget::setScaleFactor);
}
bool VisualDatabaseDisplayWidget::isVisualDisplayMode() const
{
return !displayModeButton->isChecked();
}
QStringList VisualDatabaseDisplayWidget::collectSetFilterTerms() const
{
return filterModel->getFilterTermsOfType(CardFilter::AttrSet);
}
void VisualDatabaseDisplayWidget::onSearchModelChanged()
{
if (flowWidget->isVisible()) {
// Clear the current page and prepare for new data
flowWidget->clearLayout(); // Clear existing cards
cards->clear(); // Clear the card list
// Reset scrollbar position to the top after loading new cards
QScrollBar *scrollBar = flowWidget->scrollArea->verticalScrollBar();
scrollBar->setValue(0); // Reset scrollbar to top
currentPage = 0;
loadCurrentPage();
qCDebug(VisualDatabaseDisplayLog) << "Search model changed";
}
visualDisplayModel->setSetFilterTerms(collectSetFilterTerms());
}
void VisualDatabaseDisplayWidget::onSelectedCardChanged(const QString &cardName)
@ -369,113 +319,3 @@ void VisualDatabaseDisplayWidget::onRelatedCardClicked(const QString &relatedCar
ExactCard exactCard = CardDatabaseManager::query()->guessCard({relatedCard});
emit cardInfoRequested(exactCard);
}
bool VisualDatabaseDisplayWidget::nearEndOfPage() const
{
if (!flowWidget->isVisible()) {
return false;
}
QScrollBar *scrollBar = flowWidget->scrollArea->verticalScrollBar();
if (scrollBar->value() + scrollBar->pageStep() * 2 < scrollBar->maximum()) {
return false; // there is at least two pages of space to scroll remaining
}
return true;
}
void VisualDatabaseDisplayWidget::loadCurrentPage()
{
// Ensure only the initial page is loaded
if (currentPage == 0) {
if (!initialLoadScheduled) {
initialLoadScheduled = true;
qCDebug(VisualDatabaseDisplayLog) << "Loading the first page";
// Defer the first page so the tab switch stays responsive. The card
// grid builds one event loop turn later. This also applies to
// search-driven reloads, which reset currentPage back to 0.
QTimer::singleShot(0, this, [this] {
initialLoadScheduled = false;
populateCards();
});
}
} else if (nearEndOfPage()) {
// If not the first page, just load the next page and append to the flow widget
loadNextPage();
}
}
void VisualDatabaseDisplayWidget::populateCards()
{
cards->clear();
loadNextPage();
}
void VisualDatabaseDisplayWidget::loadNextPage()
{
// Calculate the start and end indices for the next page
int rowCount = databaseDisplayModel->rowCount();
int start = currentPage * cardsPerPage;
int end = qMin(start + cardsPerPage, rowCount);
// Load more cards if we are at the end of the current list and can fetch more
if (end >= rowCount && databaseDisplayModel->canFetchMore(QModelIndex())) {
databaseDisplayModel->fetchMore(QModelIndex());
}
// Load the next page of cards and add them to the flow widget
flowWidget->setUpdatesEnabled(false);
loadPage(start, end);
flowWidget->setUpdatesEnabled(true);
}
void VisualDatabaseDisplayWidget::loadPage(int start, int end)
{
QList<const CardFilter *> setFilters = filterModel->getFiltersOfType(CardFilter::AttrSet);
for (int row = start; row < end; ++row) {
qCDebug(VisualDatabaseDisplayLog) << "Adding " << row;
QModelIndex index = databaseDisplayModel->index(row, CardDatabaseModel::NameColumn);
QVariant name = databaseDisplayModel->data(index, Qt::DisplayRole);
qCDebug(VisualDatabaseDisplayLog) << name.toString();
if (CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(name.toString())) {
if (!setFilters.empty()) {
const auto &setMap = info->getSets();
for (const CardFilter *setFilter : setFilters) {
const auto it = setMap.constFind(setFilter->term());
if (it != setMap.constEnd()) {
for (const PrintingInfo &printing : it.value()) {
addCardToDisplay(ExactCard(info, printing));
}
}
}
} else {
addCardToDisplay(CardDatabaseManager::query()->getPreferredCard(info));
}
} else {
qCDebug(VisualDatabaseDisplayLog) << "Card not found in database!";
}
}
currentPage++;
}
void VisualDatabaseDisplayWidget::modelDirty() const
{
debounceTimer->start(debounceTime);
}
void VisualDatabaseDisplayWidget::sortCardList(const QStringList &properties,
Qt::SortOrder order = Qt::AscendingOrder) const
{
CardInfoComparator comparator(properties, order);
std::sort(cards->begin(), cards->end(), [comparator](const ExactCard &a, const ExactCard &b) {
return comparator(a.getCardPtr(), b.getCardPtr());
});
}
void VisualDatabaseDisplayWidget::databaseDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight)
{
(void)topLeft;
(void)bottomRight;
qCDebug(VisualDatabaseDisplayLog) << "Database Data changed";
}

View file

@ -9,8 +9,6 @@
#include "libcockatrice/card/card_info.h"
#include <QLoggingCategory>
#include <QMouseEvent>
#include <QString>
#include <QStringList>
#include <QWidget>
@ -21,26 +19,21 @@
class CardDatabaseDisplayModel;
class CardDatabaseModel;
class CardDatabaseView;
class CardDatabaseVisualDisplayModel;
class CardDatabaseVisualView;
class CardSizeWidget;
class DeckList;
class DeckListModel;
class ExactCard;
class FilterTreeModel;
class FlowWidget;
class OverlapControlWidget;
class QLabel;
class QModelIndex;
class QMouseEvent;
class QPushButton;
class QScrollArea;
class QTimer;
class QToolButton;
class QVBoxLayout;
class SearchLineEdit;
class VisualDatabaseDisplayColorFilterWidget;
class VisualDatabaseDisplayFilterToolbarWidget;
template <typename T> class QList;
inline Q_LOGGING_CATEGORY(VisualDatabaseDisplayLog, "visual_database_display");
class VisualDatabaseDisplayWidget : public QWidget
{
@ -52,14 +45,6 @@ public:
DeckListModel *deckListModel = nullptr);
void retranslateUi();
void adjustCardsPerPage();
void populateCards();
void loadPage(int start, int end);
void loadNextPage();
void loadCurrentPage();
void sortCardList(const QStringList &properties, Qt::SortOrder order) const;
void setDeckList(const DeckList &new_deck_list_model);
/**
* @brief Sets the callback used to create a custom zone from the add-to-zone menu.
* The callback returns the name of the created zone, or an empty string if creation was cancelled.
@ -87,6 +72,13 @@ public:
bool isVisualDisplayMode() const;
public slots:
/**
* @brief Pushes the active set filters to the visual grid.
*
* The grid's rows follow the display model's filter and sort signals on their
* own; this only forwards the set filters, which decide which printings the
* grid shows.
*/
void onSearchModelChanged();
signals:
@ -101,11 +93,6 @@ signals:
protected slots:
void initialize();
void onClick(QMouseEvent *event, const ExactCard &card);
void onHover(const ExactCard &hoveredCard);
void addCardToDisplay(const ExactCard &cardToAdd);
void databaseDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight);
void modelDirty() const;
void onDisplayModeChanged(bool checked);
void onSelectedCardChanged(const QString &cardName);
@ -125,31 +112,21 @@ private:
QToolButton *clearFilterWidget;
VisualDatabaseDisplayFilterToolbarWidget *filterContainer;
CardDatabaseDisplayModel *databaseDisplayModel;
CardDatabaseVisualDisplayModel *visualDisplayModel;
CardDatabaseView *databaseView;
CardDatabaseVisualView *cardView;
std::function<QString()> newZoneCreator;
QList<ExactCard> *cards;
QVBoxLayout *mainLayout;
QScrollArea *scrollArea;
FlowWidget *flowWidget;
QWidget *overlapCategories;
QVBoxLayout *overlapCategoriesLayout;
OverlapControlWidget *overlapControlWidget;
CardSizeWidget *cardSizeWidget;
QTimer *debounceTimer;
int debounceTime = 300; // in Ms
int currentPage = 0; // Current page index
int cardsPerPage = 100; // Number of cards per page
bool filtersInitialized = false;
bool initialLoadScheduled = false;
void initializeFilters();
void highlightAllSearchEdit();
void updateDisplayModeButton();
bool nearEndOfPage() const;
[[nodiscard]] QStringList collectSetFilterTerms() const;
protected:
void resizeEvent(QResizeEvent *event) override;
void showEvent(QShowEvent *event) override;
};

View file

@ -254,7 +254,11 @@ void MainWindow::actWatchReplay()
void MainWindow::localGameEnded()
{
delete localServer;
// Defer deletion so the LocalClients (already scheduled with deleteLater() in TabSupervisor::stop)
// and any queued commands are processed before the server and its interfaces are destroyed.
if (localServer) {
localServer->deleteLater();
}
localServer = nullptr;
aConnect->setEnabled(true);