mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-27 00:14:40 -07:00
[CardSearch] Add card completion popups to chats and search fields (#7089)
* Add card completion popups to chats and search fields Completes @mention and [[card]] in chat, and card names in the deck editor, EDHREC, Archidekt, card art rules, and user card settings searches. Pops up a styled list with mana pips and a card image preview, flipping the list order when the popup opens above the text field. --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
parent
a0e76607b5
commit
83fd65b34b
25 changed files with 1526 additions and 290 deletions
|
|
@ -143,7 +143,11 @@ set(cockatrice_SOURCES
|
||||||
src/interface/intents/intent_wait_for_database_load.h
|
src/interface/intents/intent_wait_for_database_load.h
|
||||||
src/interface/layouts/flow_layout.cpp
|
src/interface/layouts/flow_layout.cpp
|
||||||
src/interface/layouts/overlap_layout.cpp
|
src/interface/layouts/overlap_layout.cpp
|
||||||
|
src/interface/widgets/utility/card_completer_delegate.cpp
|
||||||
|
src/interface/widgets/utility/card_completer_styler.cpp
|
||||||
|
src/interface/widgets/utility/completer_utils.cpp
|
||||||
src/interface/widgets/utility/line_edit_completer.cpp
|
src/interface/widgets/utility/line_edit_completer.cpp
|
||||||
|
src/interface/widgets/utility/reversed_completer_model.cpp
|
||||||
src/interface/pixel_map_generator.cpp
|
src/interface/pixel_map_generator.cpp
|
||||||
src/interface/theme_config.cpp
|
src/interface/theme_config.cpp
|
||||||
src/interface/theme_manager.cpp
|
src/interface/theme_manager.cpp
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ class ManaCostWidget : public QWidget
|
||||||
public:
|
public:
|
||||||
explicit ManaCostWidget(QWidget *parent, CardInfoPtr card);
|
explicit ManaCostWidget(QWidget *parent, CardInfoPtr card);
|
||||||
|
|
||||||
QStringList parseManaCost(const QString &manaString);
|
static QStringList parseManaCost(const QString &manaString);
|
||||||
public slots:
|
public slots:
|
||||||
void resizeEvent(QResizeEvent *event) override;
|
void resizeEvent(QResizeEvent *event) override;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,12 +52,29 @@ void CardInfoPictureEnlargedWidget::loadPixmap(const QSize &size)
|
||||||
* @param size The desired size for the pixmap.
|
* @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.
|
* 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)
|
void CardInfoPictureEnlargedWidget::setCardPixmap(const ExactCard &_card, const QSize size)
|
||||||
{
|
{
|
||||||
|
if (card.getCardPtr()) {
|
||||||
|
disconnect(card.getCardPtr().data(), nullptr, this, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
card = _card;
|
card = _card;
|
||||||
|
|
||||||
|
// Clear any previous card's art so we never paint a stale pixmap while the new image loads
|
||||||
|
enlargedPixmap = QPixmap();
|
||||||
loadPixmap(size);
|
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
|
setFixedSize(size); // Set the widget size to the enlarged size
|
||||||
|
|
||||||
update(); // Trigger a repaint
|
update(); // Trigger a repaint
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
#include "user_card_settings_dialog.h"
|
#include "user_card_settings_dialog.h"
|
||||||
|
|
||||||
#include "../../../card_picture_loader/card_picture_loader.h"
|
#include "../../../card_picture_loader/card_picture_loader.h"
|
||||||
#include "card/card_completer_proxy_model.h"
|
#include "../../utility/completer_utils.h"
|
||||||
#include "card/card_search_model.h"
|
|
||||||
#include "card_database_display_model.h"
|
#include "card_database_display_model.h"
|
||||||
#include "card_database_model.h"
|
#include "card_database_model.h"
|
||||||
#include "user_card_art_provider.h"
|
#include "user_card_art_provider.h"
|
||||||
|
|
@ -19,7 +18,6 @@
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QPainterPath>
|
#include <QPainterPath>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QRegularExpression>
|
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
#include <libcockatrice/card/database/card_database_manager.h>
|
#include <libcockatrice/card/database/card_database_manager.h>
|
||||||
|
|
||||||
|
|
@ -133,29 +131,14 @@ void UserCardArtSettingsDialog::initializeSearchBar()
|
||||||
cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this);
|
cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this);
|
||||||
cardDatabaseDisplayModel = new CardDatabaseDisplayModel(this);
|
cardDatabaseDisplayModel = new CardDatabaseDisplayModel(this);
|
||||||
cardDatabaseDisplayModel->setSourceModel(cardDatabaseModel);
|
cardDatabaseDisplayModel->setSourceModel(cardDatabaseModel);
|
||||||
searchModel = new CardSearchModel(cardDatabaseDisplayModel, this);
|
|
||||||
|
|
||||||
proxyModel = new CardCompleterProxyModel(this);
|
const CardCompleterSetup cardSetup = createCardCompleter(cardDatabaseDisplayModel, this, 15);
|
||||||
proxyModel->setSourceModel(searchModel);
|
searchModel = cardSetup.searchModel;
|
||||||
proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
|
proxyModel = cardSetup.proxyModel;
|
||||||
proxyModel->setFilterRole(Qt::DisplayRole);
|
completer = cardSetup.completer;
|
||||||
|
|
||||||
completer = new QCompleter(proxyModel, this);
|
|
||||||
completer->setCompletionRole(Qt::DisplayRole);
|
|
||||||
completer->setCompletionMode(QCompleter::PopupCompletion);
|
|
||||||
completer->setCaseSensitivity(Qt::CaseInsensitive);
|
|
||||||
completer->setFilterMode(Qt::MatchContains);
|
|
||||||
completer->setMaxVisibleItems(15);
|
|
||||||
searchBar->setCompleter(completer);
|
searchBar->setCompleter(completer);
|
||||||
|
|
||||||
connect(searchBar, &QLineEdit::textEdited, searchModel, &CardSearchModel::updateSearchResults);
|
connectCardCompleterSearch(searchBar, cardSetup);
|
||||||
connect(searchBar, &QLineEdit::textEdited, this, [this](const QString &text) {
|
|
||||||
const QString pattern = ".*" + QRegularExpression::escape(text) + ".*";
|
|
||||||
proxyModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption));
|
|
||||||
if (!text.isEmpty()) {
|
|
||||||
completer->complete();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
connect(completer, static_cast<void (QCompleter::*)(const QString &)>(&QCompleter::activated), this,
|
connect(completer, static_cast<void (QCompleter::*)(const QString &)>(&QCompleter::activated), this,
|
||||||
[this](const QString &completion) {
|
[this](const QString &completion) {
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,12 @@
|
||||||
|
|
||||||
#include "../../../../../client/settings/cache_settings.h"
|
#include "../../../../../client/settings/cache_settings.h"
|
||||||
#include "../../../cards/additional_info/mana_symbol_widget.h"
|
#include "../../../cards/additional_info/mana_symbol_widget.h"
|
||||||
|
#include "../../../utility/completer_utils.h"
|
||||||
#include "../../tab_supervisor.h"
|
#include "../../tab_supervisor.h"
|
||||||
#include "api_response/archidekt_deck_listing_api_response.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_display_widget.h"
|
||||||
#include "display/archidekt_api_response_deck_listings_display_widget.h"
|
#include "display/archidekt_api_response_deck_listings_display_widget.h"
|
||||||
|
|
||||||
#include <QCompleter>
|
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QFormLayout>
|
#include <QFormLayout>
|
||||||
#include <QGridLayout>
|
#include <QGridLayout>
|
||||||
|
|
@ -19,7 +19,6 @@
|
||||||
#include <QNetworkAccessManager>
|
#include <QNetworkAccessManager>
|
||||||
#include <QNetworkReply>
|
#include <QNetworkReply>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QRegularExpression>
|
|
||||||
#include <QResizeEvent>
|
#include <QResizeEvent>
|
||||||
#include <QScrollArea>
|
#include <QScrollArea>
|
||||||
#include <QScrollBar>
|
#include <QScrollBar>
|
||||||
|
|
@ -279,41 +278,14 @@ void TabArchidekt::setupFilterWidgets()
|
||||||
auto cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this);
|
auto cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this);
|
||||||
auto displayModel = new CardDatabaseDisplayModel(this);
|
auto displayModel = new CardDatabaseDisplayModel(this);
|
||||||
displayModel->setSourceModel(cardDatabaseModel);
|
displayModel->setSourceModel(cardDatabaseModel);
|
||||||
auto *searchModel = new CardSearchModel(displayModel, this);
|
|
||||||
|
|
||||||
auto *proxyModel = new CardCompleterProxyModel(this);
|
const CardCompleterSetup cardSetup = createCardCompleter(displayModel, this);
|
||||||
proxyModel->setSourceModel(searchModel);
|
cardsField->setCompleter(cardSetup.completer);
|
||||||
proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
|
commandersField->setCompleter(cardSetup.completer);
|
||||||
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);
|
|
||||||
|
|
||||||
// Keep autocomplete working for both fields
|
// Keep autocomplete working for both fields
|
||||||
connect(cardsField, &QLineEdit::textChanged, this, [=](const QString &text) {
|
connectCardCompleterSearch(cardsField, cardSetup);
|
||||||
searchModel->updateSearchResults(text);
|
connectCardCompleterSearch(commandersField, cardSetup);
|
||||||
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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assemble secondary toolbar
|
// Assemble secondary toolbar
|
||||||
secondaryToolbarLayout->addWidget(bracketLabel);
|
secondaryToolbarLayout->addWidget(bracketLabel);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
#include "tab_edhrec_main.h"
|
#include "tab_edhrec_main.h"
|
||||||
|
|
||||||
#include "../../../../../client/settings/cache_settings.h"
|
#include "../../../../../client/settings/cache_settings.h"
|
||||||
|
#include "../../../utility/completer_utils.h"
|
||||||
#include "../../tab_supervisor.h"
|
#include "../../tab_supervisor.h"
|
||||||
#include "api_response/average_deck/edhrec_average_deck_api_response.h"
|
#include "api_response/average_deck/edhrec_average_deck_api_response.h"
|
||||||
#include "api_response/commander/edhrec_commander_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_commander/edhrec_top_commanders_api_response_display_widget.h"
|
||||||
#include "display/top_tags/edhrec_top_tags_api_response_display_widget.h"
|
#include "display/top_tags/edhrec_top_tags_api_response_display_widget.h"
|
||||||
|
|
||||||
#include <QCompleter>
|
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
|
|
@ -63,32 +63,12 @@ TabEdhRecMain::TabEdhRecMain(TabSupervisor *_tabSupervisor) : Tab(_tabSupervisor
|
||||||
auto cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this);
|
auto cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this);
|
||||||
auto displayModel = new CardDatabaseDisplayModel(this);
|
auto displayModel = new CardDatabaseDisplayModel(this);
|
||||||
displayModel->setSourceModel(cardDatabaseModel);
|
displayModel->setSourceModel(cardDatabaseModel);
|
||||||
auto *searchModel = new CardSearchModel(displayModel, this);
|
|
||||||
|
|
||||||
auto *proxyModel = new CardCompleterProxyModel(this);
|
const CardCompleterSetup cardSetup = createCardCompleter(displayModel, this);
|
||||||
proxyModel->setSourceModel(searchModel);
|
searchBar->setCompleter(cardSetup.completer);
|
||||||
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);
|
|
||||||
|
|
||||||
// Update suggestions dynamically
|
// Update suggestions dynamically
|
||||||
connect(searchBar, &QLineEdit::textChanged, searchModel, &CardSearchModel::updateSearchResults);
|
connectCardCompleterSearch(searchBar, cardSetup);
|
||||||
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
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
searchPushButton = new QPushButton(navigationContainer);
|
searchPushButton = new QPushButton(navigationContainer);
|
||||||
connect(searchPushButton, &QPushButton::clicked, this, [=, this]() { doSearch(); });
|
connect(searchPushButton, &QPushButton::clicked, this, [=, this]() { doSearch(); });
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#include "tab_card_art_rules.h"
|
#include "tab_card_art_rules.h"
|
||||||
|
|
||||||
|
#include "../utility/completer_utils.h"
|
||||||
#include "libcockatrice/card/database/card_database_manager.h"
|
#include "libcockatrice/card/database/card_database_manager.h"
|
||||||
|
|
||||||
#include <QCompleter>
|
#include <QCompleter>
|
||||||
|
|
@ -194,29 +195,14 @@ void TabCardArtRules::initSearchBar()
|
||||||
cardDbModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this);
|
cardDbModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this);
|
||||||
cardDbDisplayModel = new CardDatabaseDisplayModel(this);
|
cardDbDisplayModel = new CardDatabaseDisplayModel(this);
|
||||||
cardDbDisplayModel->setSourceModel(cardDbModel);
|
cardDbDisplayModel->setSourceModel(cardDbModel);
|
||||||
cardSearchModel = new CardSearchModel(cardDbDisplayModel, this);
|
|
||||||
|
|
||||||
cardProxyModel = new CardCompleterProxyModel(this);
|
const CardCompleterSetup cardSetup = createCardCompleter(cardDbDisplayModel, this, 15);
|
||||||
cardProxyModel->setSourceModel(cardSearchModel);
|
cardSearchModel = cardSetup.searchModel;
|
||||||
cardProxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
|
cardProxyModel = cardSetup.proxyModel;
|
||||||
|
searchCompleter = cardSetup.completer;
|
||||||
searchCompleter = new QCompleter(cardProxyModel, this);
|
|
||||||
searchCompleter->setCompletionRole(Qt::DisplayRole);
|
|
||||||
searchCompleter->setCompletionMode(QCompleter::PopupCompletion);
|
|
||||||
searchCompleter->setCaseSensitivity(Qt::CaseInsensitive);
|
|
||||||
searchCompleter->setFilterMode(Qt::MatchContains);
|
|
||||||
searchCompleter->setMaxVisibleItems(15);
|
|
||||||
searchEdit->setCompleter(searchCompleter);
|
searchEdit->setCompleter(searchCompleter);
|
||||||
|
|
||||||
connect(searchEdit, &QLineEdit::textEdited, cardSearchModel, &CardSearchModel::updateSearchResults);
|
connectCardCompleterSearch(searchEdit, cardSetup);
|
||||||
connect(searchEdit, &QLineEdit::textEdited, this, [this](const QString &text) {
|
|
||||||
const QString pattern = ".*" + QRegularExpression::escape(text) + ".*";
|
|
||||||
cardProxyModel->setFilterRegularExpression(
|
|
||||||
QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption));
|
|
||||||
if (!text.isEmpty()) {
|
|
||||||
searchCompleter->complete();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
connect(searchCompleter, static_cast<void (QCompleter::*)(const QString &)>(&QCompleter::activated), this,
|
connect(searchCompleter, static_cast<void (QCompleter::*)(const QString &)>(&QCompleter::activated), this,
|
||||||
[this](const QString &name) { searchEdit->setText(name); });
|
[this](const QString &name) { searchEdit->setText(name); });
|
||||||
connect(searchEdit, &QLineEdit::editingFinished, this,
|
connect(searchEdit, &QLineEdit::editingFinished, this,
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,15 @@
|
||||||
#include "../interface/widgets/cards/card_info_frame_widget.h"
|
#include "../interface/widgets/cards/card_info_frame_widget.h"
|
||||||
#include "../interface/widgets/dialogs/dlg_create_game.h"
|
#include "../interface/widgets/dialogs/dlg_create_game.h"
|
||||||
#include "../interface/widgets/server/user/user_list_manager.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/widgets/utility/line_edit_completer.h"
|
||||||
#include "../interface/window_main.h"
|
#include "../interface/window_main.h"
|
||||||
#include "../main.h"
|
#include "../main.h"
|
||||||
#include "../utility/visibility_change_listener.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 "tab_supervisor.h"
|
||||||
|
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
|
|
@ -35,7 +40,9 @@
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
|
#include <QRegularExpression>
|
||||||
#include <QStackedWidget>
|
#include <QStackedWidget>
|
||||||
|
#include <QStringListModel>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
#include <libcockatrice/card/database/card_database.h>
|
#include <libcockatrice/card/database/card_database.h>
|
||||||
|
|
@ -538,7 +545,8 @@ bool TabGame::leaveGame()
|
||||||
|
|
||||||
void TabGame::actSay()
|
void TabGame::actSay()
|
||||||
{
|
{
|
||||||
if (completer->popup()->isVisible()) {
|
if (sayEdit->hasVisibleCompleterPopup()) {
|
||||||
|
sayEdit->hideCompleterPopups();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -558,14 +566,14 @@ void TabGame::addPlayerToAutoCompleteList(QString playerName)
|
||||||
{
|
{
|
||||||
if (sayEdit && !autocompleteUserList.contains(playerName)) {
|
if (sayEdit && !autocompleteUserList.contains(playerName)) {
|
||||||
autocompleteUserList << playerName;
|
autocompleteUserList << playerName;
|
||||||
sayEdit->setCompletionList(autocompleteUserList);
|
mentionModel->setStringList(autocompleteUserList);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void TabGame::removePlayerFromAutoCompleteList(QString playerName)
|
void TabGame::removePlayerFromAutoCompleteList(QString playerName)
|
||||||
{
|
{
|
||||||
if (sayEdit && autocompleteUserList.removeOne(playerName)) {
|
if (sayEdit && autocompleteUserList.removeOne(playerName)) {
|
||||||
sayEdit->setCompletionList(autocompleteUserList);
|
mentionModel->setStringList(autocompleteUserList);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -628,8 +636,8 @@ void TabGame::actRotateViewCCW()
|
||||||
|
|
||||||
void TabGame::actCompleterChanged()
|
void TabGame::actCompleterChanged()
|
||||||
{
|
{
|
||||||
SettingsCache::instance().chat().getChatMentionCompleter() ? completer->setCompletionRole(2)
|
SettingsCache::instance().chat().getChatMentionCompleter() ? mentionCompleter->setCompletionRole(2)
|
||||||
: completer->setCompletionRole(1);
|
: mentionCompleter->setCompletionRole(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void TabGame::notifyPlayerJoin(QString playerName)
|
void TabGame::notifyPlayerJoin(QString playerName)
|
||||||
|
|
@ -1281,12 +1289,25 @@ void TabGame::createMessageDock(bool bReplay)
|
||||||
sayEdit->setMaxLength(MAX_TEXT_LENGTH);
|
sayEdit->setMaxLength(MAX_TEXT_LENGTH);
|
||||||
sayLabel->setBuddy(sayEdit);
|
sayLabel->setBuddy(sayEdit);
|
||||||
connect(this, &TabGame::chatMessageSent, game->getGameEventHandler(), &GameEventHandler::handleChatMessageSent);
|
connect(this, &TabGame::chatMessageSent, game->getGameEventHandler(), &GameEventHandler::handleChatMessageSent);
|
||||||
completer = new QCompleter(autocompleteUserList, sayEdit);
|
mentionModel = new QStringListModel(autocompleteUserList, sayEdit);
|
||||||
completer->setCaseSensitivity(Qt::CaseInsensitive);
|
mentionCompleter = createMentionCompleter(mentionModel, sayEdit);
|
||||||
completer->setMaxVisibleItems(5);
|
sayEdit->addCompleter(mentionCompleter, CompleterTrigger::Mention);
|
||||||
completer->setFilterMode(Qt::MatchStartsWith);
|
|
||||||
|
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();
|
actCompleterChanged();
|
||||||
|
|
||||||
if (game->getPlayerManager()->isSpectator()) {
|
if (game->getPlayerManager()->isSpectator()) {
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
#include <QCompleter>
|
#include <QCompleter>
|
||||||
#include <QLoggingCategory>
|
#include <QLoggingCategory>
|
||||||
#include <QMap>
|
#include <QMap>
|
||||||
|
#include <QStringListModel>
|
||||||
|
|
||||||
class CardMenu;
|
class CardMenu;
|
||||||
class ServerInfo_PlayerProperties;
|
class ServerInfo_PlayerProperties;
|
||||||
|
|
@ -61,7 +62,8 @@ private:
|
||||||
const UserListProxy *userListProxy;
|
const UserListProxy *userListProxy;
|
||||||
ReplayWidget *replayWidget = nullptr;
|
ReplayWidget *replayWidget = nullptr;
|
||||||
QStringList gameTypes;
|
QStringList gameTypes;
|
||||||
QCompleter *completer;
|
QCompleter *mentionCompleter;
|
||||||
|
QStringListModel *mentionModel;
|
||||||
QStringList autocompleteUserList;
|
QStringList autocompleteUserList;
|
||||||
QStackedWidget *mainWidget;
|
QStackedWidget *mainWidget;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,11 @@
|
||||||
#include "../interface/widgets/server/user/user_list_manager.h"
|
#include "../interface/widgets/server/user/user_list_manager.h"
|
||||||
#include "../interface/widgets/server/user/user_list_widget.h"
|
#include "../interface/widgets/server/user/user_list_widget.h"
|
||||||
#include "../main.h"
|
#include "../main.h"
|
||||||
|
#include "../utility/completer_utils.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_account.h"
|
#include "tab_account.h"
|
||||||
#include "tab_supervisor.h"
|
#include "tab_supervisor.h"
|
||||||
|
|
||||||
|
|
@ -17,11 +22,14 @@
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
|
#include <QRegularExpression>
|
||||||
#include <QSplitter>
|
#include <QSplitter>
|
||||||
|
#include <QStringListModel>
|
||||||
#include <QSystemTrayIcon>
|
#include <QSystemTrayIcon>
|
||||||
#include <QToolButton>
|
#include <QToolButton>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
#include <QtCore/qdatetime.h>
|
#include <QtCore/qdatetime.h>
|
||||||
|
#include <libcockatrice/card/database/card_database_manager.h>
|
||||||
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
||||||
#include <libcockatrice/protocol/get_pb_extension.h>
|
#include <libcockatrice/protocol/get_pb_extension.h>
|
||||||
#include <libcockatrice/protocol/pb/event_join_room.pb.h>
|
#include <libcockatrice/protocol/pb/event_join_room.pb.h>
|
||||||
|
|
@ -137,13 +145,27 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor,
|
||||||
gameSelector->processGameInfo(info.game_list(i));
|
gameSelector->processGameInfo(info.game_list(i));
|
||||||
}
|
}
|
||||||
|
|
||||||
completer = new QCompleter(autocompleteUserList, sayEdit);
|
mentionModel = new QStringListModel(autocompleteUserList, sayEdit);
|
||||||
completer->setCaseSensitivity(Qt::CaseInsensitive);
|
mentionCompleter = createMentionCompleter(mentionModel, sayEdit);
|
||||||
completer->setMaxVisibleItems(5);
|
sayEdit->addCompleter(mentionCompleter, CompleterTrigger::Mention);
|
||||||
completer->setFilterMode(Qt::MatchStartsWith);
|
|
||||||
|
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();
|
actCompleterChanged();
|
||||||
|
|
||||||
connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this,
|
connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this,
|
||||||
&TabRoom::refreshShortcuts);
|
&TabRoom::refreshShortcuts);
|
||||||
refreshShortcuts();
|
refreshShortcuts();
|
||||||
|
|
@ -213,8 +235,8 @@ void TabRoom::sendMessage()
|
||||||
{
|
{
|
||||||
if (sayEdit->text().isEmpty()) {
|
if (sayEdit->text().isEmpty()) {
|
||||||
return;
|
return;
|
||||||
} else if (completer->popup()->isVisible()) {
|
} else if (sayEdit->hasVisibleCompleterPopup()) {
|
||||||
completer->popup()->hide();
|
sayEdit->hideCompleterPopups();
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
Command_RoomSay cmd;
|
Command_RoomSay cmd;
|
||||||
|
|
@ -248,8 +270,8 @@ void TabRoom::actOpenChatSettings()
|
||||||
|
|
||||||
void TabRoom::actCompleterChanged()
|
void TabRoom::actCompleterChanged()
|
||||||
{
|
{
|
||||||
SettingsCache::instance().chat().getChatMentionCompleter() ? completer->setCompletionRole(2)
|
SettingsCache::instance().chat().getChatMentionCompleter() ? mentionCompleter->setCompletionRole(2)
|
||||||
: completer->setCompletionRole(1);
|
: mentionCompleter->setCompletionRole(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void TabRoom::processRoomEvent(const RoomEvent &event)
|
void TabRoom::processRoomEvent(const RoomEvent &event)
|
||||||
|
|
@ -285,16 +307,18 @@ void TabRoom::processListGamesEvent(const Event_ListGames &event)
|
||||||
|
|
||||||
void TabRoom::processJoinRoomEvent(const Event_JoinRoom &event)
|
void TabRoom::processJoinRoomEvent(const Event_JoinRoom &event)
|
||||||
{
|
{
|
||||||
if (!autocompleteUserList.contains("@" + QString::fromStdString(event.user_info().name()))) {
|
QString mention = "@" + QString::fromStdString(event.user_info().name());
|
||||||
autocompleteUserList << "@" + QString::fromStdString(event.user_info().name());
|
if (!autocompleteUserList.contains(mention)) {
|
||||||
sayEdit->setCompletionList(autocompleteUserList);
|
autocompleteUserList << mention;
|
||||||
|
mentionModel->setStringList(autocompleteUserList);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void TabRoom::processLeaveRoomEvent(const Event_LeaveRoom &event)
|
void TabRoom::processLeaveRoomEvent(const Event_LeaveRoom &event)
|
||||||
{
|
{
|
||||||
autocompleteUserList.removeOne("@" + QString::fromStdString(event.name()));
|
QString mention = "@" + QString::fromStdString(event.name());
|
||||||
sayEdit->setCompletionList(autocompleteUserList);
|
autocompleteUserList.removeOne(mention);
|
||||||
|
mentionModel->setStringList(autocompleteUserList);
|
||||||
}
|
}
|
||||||
|
|
||||||
void TabRoom::processRoomSayEvent(const Event_RoomSay &event)
|
void TabRoom::processRoomSayEvent(const Event_RoomSay &event)
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
#include <QFocusEvent>
|
#include <QFocusEvent>
|
||||||
#include <QGroupBox>
|
#include <QGroupBox>
|
||||||
#include <QMap>
|
#include <QMap>
|
||||||
|
#include <QStringListModel>
|
||||||
|
|
||||||
class UserListProxy;
|
class UserListProxy;
|
||||||
class UserListManager;
|
class UserListManager;
|
||||||
|
|
@ -63,6 +64,7 @@ private:
|
||||||
ChatView *chatView;
|
ChatView *chatView;
|
||||||
QLabel *sayLabel;
|
QLabel *sayLabel;
|
||||||
LineEditCompleter *sayEdit;
|
LineEditCompleter *sayEdit;
|
||||||
|
QStringListModel *mentionModel;
|
||||||
QGroupBox *chatGroupBox;
|
QGroupBox *chatGroupBox;
|
||||||
|
|
||||||
QMenu *roomMenu;
|
QMenu *roomMenu;
|
||||||
|
|
@ -72,7 +74,7 @@ private:
|
||||||
[[nodiscard]] QString sanitizeHtml(QString dirty) const;
|
[[nodiscard]] QString sanitizeHtml(QString dirty) const;
|
||||||
|
|
||||||
QStringList autocompleteUserList;
|
QStringList autocompleteUserList;
|
||||||
QCompleter *completer;
|
QCompleter *mentionCompleter;
|
||||||
signals:
|
signals:
|
||||||
void roomClosing(TabRoom *tab);
|
void roomClosing(TabRoom *tab);
|
||||||
void openMessageDialog(const QString &userName, bool focus);
|
void openMessageDialog(const QString &userName, bool focus);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,439 @@
|
||||||
|
#include "card_completer_delegate.h"
|
||||||
|
|
||||||
|
#include "../cards/additional_info/mana_cost_widget.h"
|
||||||
|
|
||||||
|
#include <QFontMetrics>
|
||||||
|
#include <QLinearGradient>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QPainterPath>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QSet>
|
||||||
|
#include <QStyleOptionViewItem>
|
||||||
|
#include <libcockatrice/card/card_info.h>
|
||||||
|
#include <libcockatrice/card/database/card_database_manager.h>
|
||||||
|
#include <libcockatrice/card/printing/printing_info.h>
|
||||||
|
#include <libcockatrice/models/database/card/card_search_model.h>
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Internal colour helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
struct ManaColor
|
||||||
|
{
|
||||||
|
QColor fill;
|
||||||
|
QColor rim;
|
||||||
|
QColor text;
|
||||||
|
};
|
||||||
|
|
||||||
|
ManaColor manaColor(QChar symbol)
|
||||||
|
{
|
||||||
|
switch (symbol.unicode()) {
|
||||||
|
case 'W':
|
||||||
|
return {QColor(248, 248, 246), QColor(190, 180, 160), QColor(80, 70, 50)};
|
||||||
|
case 'U':
|
||||||
|
return {QColor(55, 130, 210), QColor(30, 90, 160), QColor(255, 255, 255)};
|
||||||
|
case 'B':
|
||||||
|
return {QColor(90, 65, 160), QColor(55, 38, 110), QColor(220, 200, 255)};
|
||||||
|
case 'R':
|
||||||
|
return {QColor(210, 55, 55), QColor(150, 30, 30), QColor(255, 255, 255)};
|
||||||
|
case 'G':
|
||||||
|
return {QColor(45, 148, 90), QColor(28, 95, 58), QColor(255, 255, 255)};
|
||||||
|
default:
|
||||||
|
return {QColor(100, 115, 135), QColor(65, 78, 95), QColor(230, 235, 240)};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QColor blend(QColor a, QColor b, qreal t)
|
||||||
|
{
|
||||||
|
return QColor::fromRgbF(a.redF() + (b.redF() - a.redF()) * t, a.greenF() + (b.greenF() - a.greenF()) * t,
|
||||||
|
a.blueF() + (b.blueF() - a.blueF()) * t, a.alphaF() + (b.alphaF() - a.alphaF()) * t);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
QColor CardCompleterDelegate::accentForColors(const QString &colors)
|
||||||
|
{
|
||||||
|
if (colors.isEmpty()) {
|
||||||
|
return QColor(100, 115, 135);
|
||||||
|
}
|
||||||
|
|
||||||
|
QSet<QChar> seen;
|
||||||
|
for (const QChar c : colors) {
|
||||||
|
if (QString("WUBRG").contains(c)) {
|
||||||
|
seen.insert(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seen.size() > 1) {
|
||||||
|
return QColor(205, 145, 25);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seen.isEmpty()) {
|
||||||
|
return QColor(100, 115, 135);
|
||||||
|
}
|
||||||
|
|
||||||
|
return manaColor(*seen.begin()).fill;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
CardCompleterDelegate::CardCompleterDelegate(QObject *parent) : QStyledItemDelegate(parent)
|
||||||
|
{
|
||||||
|
symbolCache.setMaxCost(64);
|
||||||
|
setCodeCache.setMaxCost(64);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// sizeHint
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
QSize CardCompleterDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
|
||||||
|
{
|
||||||
|
Q_UNUSED(option)
|
||||||
|
|
||||||
|
if (!index.isValid()) {
|
||||||
|
return QStyledItemDelegate::sizeHint(option, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fixed wide rows so the popup has room for name, type line, set and mana
|
||||||
|
return {480, CardRowHeight};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Mana symbol painting
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const QPixmap *CardCompleterDelegate::cachedSymbolPixmap(const QString &symbol, int size) const
|
||||||
|
{
|
||||||
|
const QString key = symbol + QString::number(size);
|
||||||
|
|
||||||
|
if (symbolCache.contains(key)) {
|
||||||
|
return symbolCache[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
QPixmap src(QString("theme:icons/mana/%1").arg(symbol));
|
||||||
|
|
||||||
|
if (!src.isNull()) {
|
||||||
|
auto *pm = new QPixmap(src.scaled(size, size, Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||||||
|
|
||||||
|
symbolCache.insert(key, pm);
|
||||||
|
return pm;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void CardCompleterDelegate::drawManaSymbol(QPainter *p, QPoint centre, const QString &symbol, int radius) const
|
||||||
|
{
|
||||||
|
const QRect pip(centre.x() - radius, centre.y() - radius, radius * 2, radius * 2);
|
||||||
|
|
||||||
|
const QPixmap *px = cachedSymbolPixmap(symbol, radius * 2);
|
||||||
|
|
||||||
|
if (px && !px->isNull()) {
|
||||||
|
p->drawPixmap(pip, *px);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isNumeric = false;
|
||||||
|
const int numVal = symbol.toInt(&isNumeric);
|
||||||
|
|
||||||
|
const QString label = isNumeric ? QString::number(numVal) : symbol;
|
||||||
|
|
||||||
|
const ManaColor mc =
|
||||||
|
(symbol.length() == 1 && QString("WUBRG").contains(symbol)) ? manaColor(symbol[0]) : manaColor(QChar('X'));
|
||||||
|
|
||||||
|
QPainterPath circle;
|
||||||
|
circle.addEllipse(pip);
|
||||||
|
|
||||||
|
p->save();
|
||||||
|
p->setClipPath(circle);
|
||||||
|
p->fillPath(circle, mc.fill);
|
||||||
|
p->restore();
|
||||||
|
|
||||||
|
p->setPen(QPen(mc.rim, 1.2));
|
||||||
|
p->setBrush(Qt::NoBrush);
|
||||||
|
p->drawEllipse(pip.adjusted(1, 1, -1, -1));
|
||||||
|
|
||||||
|
QFont f = p->font();
|
||||||
|
f.setPixelSize(qMax(radius - 1, 7));
|
||||||
|
f.setBold(true);
|
||||||
|
|
||||||
|
p->setFont(f);
|
||||||
|
p->setPen(mc.text);
|
||||||
|
p->drawText(pip, Qt::AlignCenter, label);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
int CardCompleterDelegate::drawManaCost(QPainter *p, const QRect &row, const QString &manaCost, int radius) const
|
||||||
|
{
|
||||||
|
if (manaCost.isEmpty()) {
|
||||||
|
return row.right();
|
||||||
|
}
|
||||||
|
|
||||||
|
const int diam = radius * 2;
|
||||||
|
|
||||||
|
// Split, adventure, aftermath and prepare cards store both halves of the
|
||||||
|
// cost joined by "//" (e.g. "1W // W"); draw each half as its own group.
|
||||||
|
static const QRegularExpression splitRegex("\\s*//\\s*");
|
||||||
|
|
||||||
|
QList<QStringList> parts;
|
||||||
|
|
||||||
|
for (const QString &part : manaCost.split(splitRegex, Qt::SkipEmptyParts)) {
|
||||||
|
const QStringList symbols = ManaCostWidget::parseManaCost(part);
|
||||||
|
|
||||||
|
if (!symbols.isEmpty()) {
|
||||||
|
parts.append(symbols);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int totalW = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < parts.size(); ++i) {
|
||||||
|
if (i > 0) {
|
||||||
|
totalW += PartGap;
|
||||||
|
}
|
||||||
|
|
||||||
|
totalW += parts.at(i).size() * diam + qMax(0, parts.at(i).size() - 1) * SymbolSpacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int rightPad = 14;
|
||||||
|
|
||||||
|
int x = row.right() - rightPad - totalW + radius;
|
||||||
|
|
||||||
|
const int cy = row.center().y();
|
||||||
|
|
||||||
|
for (int i = 0; i < parts.size(); ++i) {
|
||||||
|
const QStringList &symbols = parts.at(i);
|
||||||
|
|
||||||
|
for (const QString &sym : symbols) {
|
||||||
|
drawManaSymbol(p, {x, cy}, sym, radius);
|
||||||
|
x += diam + SymbolSpacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i < parts.size() - 1) {
|
||||||
|
x += PartGap - SymbolSpacing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return row.right() - rightPad - totalW - 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
QString CardCompleterDelegate::setCodeForCard(const QSharedPointer<CardInfo> &card) const
|
||||||
|
{
|
||||||
|
if (!card) {
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString name = card->getName();
|
||||||
|
|
||||||
|
if (setCodeCache.contains(name)) {
|
||||||
|
return *setCodeCache[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
QString code;
|
||||||
|
|
||||||
|
const PrintingInfo printing = CardDatabaseManager::query()->getPreferredPrinting(card);
|
||||||
|
|
||||||
|
if (auto set = printing.getSet()) {
|
||||||
|
code = set->getShortName();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *cached = new QString(code);
|
||||||
|
setCodeCache.insert(name, cached);
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// paint
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void CardCompleterDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
|
||||||
|
{
|
||||||
|
if (!index.isValid()) {
|
||||||
|
QStyledItemDelegate::paint(painter, option, index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QRect rect = option.rect;
|
||||||
|
const QPalette &pal = option.palette;
|
||||||
|
|
||||||
|
const bool selected = option.state & QStyle::State_Selected;
|
||||||
|
const bool hovered = option.state & QStyle::State_MouseOver;
|
||||||
|
|
||||||
|
painter->save();
|
||||||
|
painter->setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing | QPainter::SmoothPixmapTransform);
|
||||||
|
|
||||||
|
auto card = index.data(CardSearchModel::CardInfoRole).value<QSharedPointer<CardInfo>>();
|
||||||
|
|
||||||
|
if (!card) {
|
||||||
|
painter->fillRect(rect, pal.color(QPalette::Base));
|
||||||
|
painter->restore();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString cardName = card->getName();
|
||||||
|
const QString manaCost = card->getManaCost();
|
||||||
|
const QString typeLine = card->getCardType();
|
||||||
|
const QString setCode = setCodeForCard(card);
|
||||||
|
|
||||||
|
const QColor accent = accentForColors(card->getColors());
|
||||||
|
|
||||||
|
const QColor base = pal.color(QPalette::Base);
|
||||||
|
const QColor textColor = pal.color(QPalette::Text);
|
||||||
|
const QColor secondaryColor = pal.color(QPalette::PlaceholderText);
|
||||||
|
|
||||||
|
QColor tinted = blend(base, accent, 0.40);
|
||||||
|
|
||||||
|
if (hovered) {
|
||||||
|
tinted = blend(tinted, Qt::white, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
const QRectF cardRect = rect.adjusted(3, 2, -3, -2);
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Main card body
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
QPainterPath path;
|
||||||
|
path.addRoundedRect(cardRect, 7, 7);
|
||||||
|
|
||||||
|
painter->save();
|
||||||
|
painter->setClipPath(path);
|
||||||
|
|
||||||
|
QLinearGradient bodyGrad(cardRect.topLeft(), cardRect.bottomLeft());
|
||||||
|
|
||||||
|
bodyGrad.setColorAt(0.0, blend(tinted, Qt::white, 0.10));
|
||||||
|
bodyGrad.setColorAt(1.0, blend(tinted, Qt::black, 0.18));
|
||||||
|
|
||||||
|
painter->fillPath(path, bodyGrad);
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Accent strip
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
QRectF accentRect(cardRect.left(), cardRect.top(), AccentBarWidth, cardRect.height());
|
||||||
|
|
||||||
|
QLinearGradient accentGrad(accentRect.topLeft(), accentRect.bottomLeft());
|
||||||
|
|
||||||
|
accentGrad.setColorAt(0.0, blend(accent, Qt::white, 0.20));
|
||||||
|
accentGrad.setColorAt(1.0, blend(accent, Qt::black, 0.25));
|
||||||
|
|
||||||
|
painter->fillRect(accentRect, accentGrad);
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Right mana zone
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
const QRectF manaZone(cardRect.right() - 110, cardRect.top(), 110, cardRect.height());
|
||||||
|
|
||||||
|
QLinearGradient manaGrad(manaZone.topLeft(), manaZone.bottomLeft());
|
||||||
|
|
||||||
|
manaGrad.setColorAt(0, QColor(0, 0, 0, 18));
|
||||||
|
manaGrad.setColorAt(1, QColor(0, 0, 0, 42));
|
||||||
|
|
||||||
|
painter->fillRect(manaZone, manaGrad);
|
||||||
|
|
||||||
|
painter->restore();
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Border
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
QColor border = blend(accent, Qt::black, 0.45);
|
||||||
|
|
||||||
|
if (hovered) {
|
||||||
|
border = blend(border, Qt::white, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
painter->setPen(QPen(border, 1.2));
|
||||||
|
painter->drawPath(path);
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Selection glow
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
if (selected) {
|
||||||
|
QColor glow = pal.color(QPalette::Highlight);
|
||||||
|
glow.setAlpha(30);
|
||||||
|
|
||||||
|
painter->fillPath(path, glow);
|
||||||
|
|
||||||
|
painter->setPen(QPen(pal.color(QPalette::Highlight), 2));
|
||||||
|
painter->drawPath(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Mana cost
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
const int costLeft = drawManaCost(painter, cardRect.toRect(), manaCost, SymbolRadius);
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Card name + type line + set code
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
const int textLeft = cardRect.left() + AccentBarWidth + 12;
|
||||||
|
const int textRight = costLeft - 8;
|
||||||
|
const int textWidth = qMax(0, textRight - textLeft);
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Card name (top band)
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
{
|
||||||
|
const QRect nameRect(textLeft, rect.top() + 2, textWidth, 20);
|
||||||
|
|
||||||
|
QFont f = option.font;
|
||||||
|
f.setPixelSize(13);
|
||||||
|
f.setBold(true);
|
||||||
|
|
||||||
|
painter->setFont(f);
|
||||||
|
|
||||||
|
const QString nameText = QFontMetrics(f).elidedText(cardName, Qt::ElideRight, nameRect.width());
|
||||||
|
|
||||||
|
painter->setPen(QColor(0, 0, 0, 140));
|
||||||
|
painter->drawText(nameRect.translated(0, 1), Qt::AlignLeft | Qt::AlignVCenter, nameText);
|
||||||
|
|
||||||
|
painter->setPen(textColor);
|
||||||
|
painter->drawText(nameRect, Qt::AlignLeft | Qt::AlignVCenter, nameText);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Type line + set code (bottom band)
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
{
|
||||||
|
const QRect infoRect(textLeft, rect.top() + 22, textWidth, rect.height() - 24);
|
||||||
|
|
||||||
|
QFont f = option.font;
|
||||||
|
f.setPixelSize(10);
|
||||||
|
|
||||||
|
painter->setFont(f);
|
||||||
|
|
||||||
|
QString infoLine = typeLine;
|
||||||
|
|
||||||
|
if (!setCode.isEmpty()) {
|
||||||
|
infoLine += " \u00b7 " + setCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString infoText = QFontMetrics(f).elidedText(infoLine, Qt::ElideRight, infoRect.width());
|
||||||
|
|
||||||
|
painter->setPen(QColor(0, 0, 0, 120));
|
||||||
|
painter->drawText(infoRect.translated(0, 1), Qt::AlignLeft | Qt::AlignVCenter, infoText);
|
||||||
|
|
||||||
|
painter->setPen(secondaryColor);
|
||||||
|
painter->drawText(infoRect, Qt::AlignLeft | Qt::AlignVCenter, infoText);
|
||||||
|
}
|
||||||
|
|
||||||
|
painter->restore();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
/**
|
||||||
|
* @file card_completer_delegate.h
|
||||||
|
* @ingroup UtilityWidgets
|
||||||
|
*/
|
||||||
|
//! \todo Document this file.
|
||||||
|
|
||||||
|
#ifndef CARD_COMPLETER_DELEGATE_H
|
||||||
|
#define CARD_COMPLETER_DELEGATE_H
|
||||||
|
|
||||||
|
#include <QCache>
|
||||||
|
#include <QColor>
|
||||||
|
#include <QPixmap>
|
||||||
|
#include <QStyledItemDelegate>
|
||||||
|
|
||||||
|
class CardInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Paints styled card completer popup rows.
|
||||||
|
*
|
||||||
|
* Each row shows the card name, type line, set code and mana cost pips,
|
||||||
|
* color-coded by the card's color identity. Card data is read directly from
|
||||||
|
* the CardSearchModel::CardInfoRole so no extra database lookups are needed.
|
||||||
|
*/
|
||||||
|
class CardCompleterDelegate : public QStyledItemDelegate
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit CardCompleterDelegate(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
|
||||||
|
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Mana symbol pixmaps, loaded once and cached
|
||||||
|
mutable QCache<QString, QPixmap> symbolCache;
|
||||||
|
|
||||||
|
// Set short codes, resolved once per card name and cached
|
||||||
|
mutable QCache<QString, QString> setCodeCache;
|
||||||
|
|
||||||
|
// Resolve the card's color string ("RG", "W", "", ...) → accent QColor
|
||||||
|
static QColor accentForColors(const QString &colors);
|
||||||
|
|
||||||
|
// Draw a single mana symbol pip at centre point
|
||||||
|
void drawManaSymbol(QPainter *p, QPoint centre, const QString &symbol, int radius) const;
|
||||||
|
|
||||||
|
// Draw all mana pips for a cost string like "2RG" or "{2}{R}{G}"; split and
|
||||||
|
// adventure costs ("1W // W") are drawn as separate groups. Returns the left-most x used
|
||||||
|
int drawManaCost(QPainter *p, const QRect &row, const QString &manaCost, int radius) const;
|
||||||
|
|
||||||
|
// Load (or return cached) a mana icon pixmap; falls back to painted circle
|
||||||
|
const QPixmap *cachedSymbolPixmap(const QString &symbol, int size) const;
|
||||||
|
|
||||||
|
// Resolve the preferred printing's set short code for a card
|
||||||
|
QString setCodeForCard(const QSharedPointer<CardInfo> &card) const;
|
||||||
|
|
||||||
|
static constexpr int CardRowHeight = 40;
|
||||||
|
static constexpr int AccentBarWidth = 5;
|
||||||
|
static constexpr int SymbolRadius = 9;
|
||||||
|
static constexpr int SymbolSpacing = 2;
|
||||||
|
static constexpr int PartGap = 14;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // CARD_COMPLETER_DELEGATE_H
|
||||||
|
|
@ -0,0 +1,340 @@
|
||||||
|
#include "card_completer_styler.h"
|
||||||
|
|
||||||
|
#include "../cards/card_info_picture_enlarged_widget.h"
|
||||||
|
#include "card_completer_delegate.h"
|
||||||
|
#include "reversed_completer_model.h"
|
||||||
|
|
||||||
|
#include <QAbstractItemModel>
|
||||||
|
#include <QAbstractItemView>
|
||||||
|
#include <QCompleter>
|
||||||
|
#include <QEvent>
|
||||||
|
#include <QKeyEvent>
|
||||||
|
#include <QMouseEvent>
|
||||||
|
#include <QPropertyAnimation>
|
||||||
|
#include <QScreen>
|
||||||
|
#include <QSize>
|
||||||
|
#include <libcockatrice/card/card_info.h>
|
||||||
|
#include <libcockatrice/card/database/card_database_manager.h>
|
||||||
|
#include <libcockatrice/models/database/card/card_search_model.h>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
const QSize PreviewSize(300, 419);
|
||||||
|
const int PreviewMargin = 16;
|
||||||
|
const int FadeDuration = 120;
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void CardCompleterStyler::apply(QCompleter *completer)
|
||||||
|
{
|
||||||
|
if (!completer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The styler lives as long as the completer
|
||||||
|
new CardCompleterStyler(completer, completer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
CardCompleterStyler::CardCompleterStyler(QCompleter *completer, QObject *parent)
|
||||||
|
: QObject(parent), completer(completer), reversedModel(nullptr), preview(nullptr), above(false)
|
||||||
|
{
|
||||||
|
auto *popup = completer->popup();
|
||||||
|
|
||||||
|
// Wrap the completer's model so its row order can be reversed when the
|
||||||
|
// popup is shown above the text edit
|
||||||
|
QAbstractItemModel *sourceModel = completer->model();
|
||||||
|
reversedModel = new ReversedCompleterModel(completer);
|
||||||
|
reversedModel->setSourceModel(sourceModel);
|
||||||
|
completer->setModel(reversedModel);
|
||||||
|
|
||||||
|
popup->setItemDelegate(new CardCompleterDelegate(popup));
|
||||||
|
|
||||||
|
popup->viewport()->setMouseTracking(true);
|
||||||
|
|
||||||
|
popup->installEventFilter(this);
|
||||||
|
popup->viewport()->installEventFilter(this);
|
||||||
|
|
||||||
|
connect(popup->selectionModel(), &QItemSelectionModel::currentRowChanged, this,
|
||||||
|
&CardCompleterStyler::updatePreview);
|
||||||
|
connect(completer, qOverload<const QString &>(&QCompleter::activated), this, &CardCompleterStyler::hidePreview);
|
||||||
|
connect(completer->completionModel(), &QAbstractItemModel::modelReset, this,
|
||||||
|
&CardCompleterStyler::onCompletionReset);
|
||||||
|
}
|
||||||
|
|
||||||
|
CardCompleterStyler::~CardCompleterStyler()
|
||||||
|
{
|
||||||
|
if (preview) {
|
||||||
|
preview->deleteLater();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
bool CardCompleterStyler::eventFilter(QObject *obj, QEvent *ev)
|
||||||
|
{
|
||||||
|
auto *popup = completer->popup();
|
||||||
|
|
||||||
|
if (obj == popup->viewport()) {
|
||||||
|
if (ev->type() == QEvent::MouseMove) {
|
||||||
|
updatePreviewFromHover(static_cast<QMouseEvent *>(ev)->pos());
|
||||||
|
}
|
||||||
|
} else if (obj == popup) {
|
||||||
|
switch (ev->type()) {
|
||||||
|
case QEvent::Show:
|
||||||
|
case QEvent::Move:
|
||||||
|
case QEvent::Resize:
|
||||||
|
updateOrientation();
|
||||||
|
reposition();
|
||||||
|
break;
|
||||||
|
case QEvent::Hide:
|
||||||
|
hidePreview();
|
||||||
|
break;
|
||||||
|
case QEvent::KeyPress:
|
||||||
|
if (handlePopupKeyPress(static_cast<QKeyEvent *>(ev))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return QObject::eventFilter(obj, ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
bool CardCompleterStyler::handlePopupKeyPress(QKeyEvent *event)
|
||||||
|
{
|
||||||
|
const int key = event->key();
|
||||||
|
if (key != Qt::Key_Up && key != Qt::Key_Down) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!above) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *popup = completer->popup();
|
||||||
|
const int rowCount = completer->completionModel()->rowCount();
|
||||||
|
if (rowCount == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// With the popup above the text edit the rows are shown in reverse order and
|
||||||
|
// the closest match is in the bottom-most row. The up arrow then advances
|
||||||
|
// through the list (towards its end at the top of the popup).
|
||||||
|
const int currentRow = popup->currentIndex().row();
|
||||||
|
const int step = (key == Qt::Key_Up) ? -1 : 1;
|
||||||
|
const int newRow = qBound(0, currentRow + step, rowCount - 1);
|
||||||
|
|
||||||
|
if (newRow != currentRow) {
|
||||||
|
popup->setCurrentIndex(completer->completionModel()->index(newRow, completer->completionColumn()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void CardCompleterStyler::updateOrientation()
|
||||||
|
{
|
||||||
|
above = isPopupAboveWidget();
|
||||||
|
reversedModel->setEnabled(above);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void CardCompleterStyler::ensureClosestSelected()
|
||||||
|
{
|
||||||
|
auto *popup = completer->popup();
|
||||||
|
auto *completionModel = completer->completionModel();
|
||||||
|
const int rowCount = completionModel->rowCount();
|
||||||
|
if (rowCount == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int currentRow = popup->currentIndex().row();
|
||||||
|
const int closestRow = rowCount - 1;
|
||||||
|
|
||||||
|
if (currentRow != closestRow) {
|
||||||
|
popup->setCurrentIndex(completionModel->index(closestRow, completer->completionColumn()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void CardCompleterStyler::onCompletionReset()
|
||||||
|
{
|
||||||
|
if (above) {
|
||||||
|
ensureClosestSelected();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
bool CardCompleterStyler::isPopupAboveWidget() const
|
||||||
|
{
|
||||||
|
auto *popup = completer->popup();
|
||||||
|
QWidget *widget = completer->widget();
|
||||||
|
|
||||||
|
if (!widget || !popup->isVisible()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QPoint popupBottom = popup->mapToGlobal(QPoint(0, popup->height()));
|
||||||
|
const QPoint fieldTop = widget->mapToGlobal(QPoint(0, 0));
|
||||||
|
|
||||||
|
return popupBottom.y() <= fieldTop.y();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void CardCompleterStyler::updatePreviewFromHover(const QPoint &pos)
|
||||||
|
{
|
||||||
|
const QModelIndex index = completer->popup()->indexAt(pos);
|
||||||
|
|
||||||
|
// Hovering updates the preview but must not change the current completion
|
||||||
|
if (index.isValid() && index != previewedIndex) {
|
||||||
|
updatePreview(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void CardCompleterStyler::updatePreview(const QModelIndex &index)
|
||||||
|
{
|
||||||
|
previewedIndex = index;
|
||||||
|
|
||||||
|
if (!index.isValid()) {
|
||||||
|
hidePreview();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto card = index.data(CardSearchModel::CardInfoRole).value<QSharedPointer<CardInfo>>();
|
||||||
|
|
||||||
|
if (!card) {
|
||||||
|
hidePreview();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!preview) {
|
||||||
|
QWidget *contextWindow = completer->widget() ? completer->widget()->window() : nullptr;
|
||||||
|
|
||||||
|
preview = new CardInfoPictureEnlargedWidget(contextWindow);
|
||||||
|
preview->hide();
|
||||||
|
preview->setWindowOpacity(0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ExactCard exact = CardDatabaseManager::query()->getCard({card->getName()});
|
||||||
|
|
||||||
|
if (!exact) {
|
||||||
|
hidePreview();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
preview->setCardPixmap(exact, PreviewSize);
|
||||||
|
|
||||||
|
reposition();
|
||||||
|
showPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void CardCompleterStyler::showPreview()
|
||||||
|
{
|
||||||
|
if (!preview) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
stopPreviewFade();
|
||||||
|
|
||||||
|
preview->show();
|
||||||
|
preview->raise();
|
||||||
|
|
||||||
|
auto *fade = new QPropertyAnimation(preview, "windowOpacity", preview);
|
||||||
|
|
||||||
|
fade->setDuration(FadeDuration);
|
||||||
|
fade->setStartValue(preview->windowOpacity());
|
||||||
|
fade->setEndValue(1.0);
|
||||||
|
fade->start(QAbstractAnimation::DeleteWhenStopped);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void CardCompleterStyler::hidePreview()
|
||||||
|
{
|
||||||
|
if (!preview) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop any in-flight fade first so a stale one cannot keep the preview visible
|
||||||
|
stopPreviewFade();
|
||||||
|
preview->hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void CardCompleterStyler::stopPreviewFade()
|
||||||
|
{
|
||||||
|
if (!preview) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto animations = preview->findChildren<QPropertyAnimation *>();
|
||||||
|
for (auto *animation : animations) {
|
||||||
|
animation->stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void CardCompleterStyler::reposition()
|
||||||
|
{
|
||||||
|
if (!preview) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *popup = completer->popup();
|
||||||
|
|
||||||
|
const QRect popupGlobalRect(popup->mapToGlobal(popup->rect().topLeft()), popup->rect().size());
|
||||||
|
|
||||||
|
int anchorY;
|
||||||
|
|
||||||
|
if (previewedIndex.isValid()) {
|
||||||
|
const QRect itemRect = popup->visualRect(previewedIndex);
|
||||||
|
const QPoint itemCenter = popup->viewport()->mapToGlobal(itemRect.center());
|
||||||
|
anchorY = itemCenter.y();
|
||||||
|
} else {
|
||||||
|
anchorY = popupGlobalRect.center().y();
|
||||||
|
}
|
||||||
|
|
||||||
|
const QScreen *screen = popup->screen();
|
||||||
|
const QRect screenGeom = screen ? screen->availableGeometry() : QRect();
|
||||||
|
|
||||||
|
const int rightX = popupGlobalRect.right() + PreviewMargin;
|
||||||
|
const int leftX = popupGlobalRect.left() - PreviewMargin - preview->width();
|
||||||
|
|
||||||
|
int x;
|
||||||
|
int y = anchorY - preview->height() / 2;
|
||||||
|
|
||||||
|
if (screenGeom.isEmpty()) {
|
||||||
|
x = rightX;
|
||||||
|
} else if (rightX + preview->width() <= screenGeom.right()) {
|
||||||
|
x = rightX;
|
||||||
|
} else if (leftX >= screenGeom.left()) {
|
||||||
|
x = leftX;
|
||||||
|
} else {
|
||||||
|
x = rightX;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!screenGeom.isEmpty()) {
|
||||||
|
x = qBound(screenGeom.left(), x, qMax(screenGeom.left(), screenGeom.right() - preview->width()));
|
||||||
|
y = qBound(screenGeom.top(), y, qMax(screenGeom.top(), screenGeom.bottom() - preview->height()));
|
||||||
|
}
|
||||||
|
|
||||||
|
preview->move(x, y);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
/**
|
||||||
|
* @file card_completer_styler.h
|
||||||
|
* @ingroup UtilityWidgets
|
||||||
|
*/
|
||||||
|
//! \todo Document this file.
|
||||||
|
|
||||||
|
#ifndef CARD_COMPLETER_STYLER_H
|
||||||
|
#define CARD_COMPLETER_STYLER_H
|
||||||
|
|
||||||
|
#include <QModelIndex>
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
class CardInfoPictureEnlargedWidget;
|
||||||
|
class QCompleter;
|
||||||
|
class QKeyEvent;
|
||||||
|
class QPoint;
|
||||||
|
class ReversedCompleterModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Applies styled row painting and a card-image preview to a card completer.
|
||||||
|
*
|
||||||
|
* The completer popup rows are painted by CardCompleterDelegate and the image of
|
||||||
|
* the currently selected (or hovered) row is shown beside the popup.
|
||||||
|
*/
|
||||||
|
class CardCompleterStyler : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Styles @p completer and follows its current selection.
|
||||||
|
*
|
||||||
|
* The styler is parented to the completer so it lives exactly as long as the
|
||||||
|
* completer itself.
|
||||||
|
*/
|
||||||
|
static void apply(QCompleter *completer);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool eventFilter(QObject *obj, QEvent *ev) override;
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void updatePreview(const QModelIndex &index);
|
||||||
|
void updatePreviewFromHover(const QPoint &pos);
|
||||||
|
void onCompletionReset();
|
||||||
|
|
||||||
|
private:
|
||||||
|
explicit CardCompleterStyler(QCompleter *completer, QObject *parent = nullptr);
|
||||||
|
~CardCompleterStyler() override;
|
||||||
|
|
||||||
|
void showPreview();
|
||||||
|
void hidePreview();
|
||||||
|
void stopPreviewFade();
|
||||||
|
void reposition();
|
||||||
|
|
||||||
|
void updateOrientation();
|
||||||
|
void ensureClosestSelected();
|
||||||
|
|
||||||
|
bool handlePopupKeyPress(QKeyEvent *event);
|
||||||
|
bool isPopupAboveWidget() const;
|
||||||
|
|
||||||
|
QCompleter *completer;
|
||||||
|
ReversedCompleterModel *reversedModel;
|
||||||
|
CardInfoPictureEnlargedWidget *preview;
|
||||||
|
QModelIndex previewedIndex;
|
||||||
|
bool above;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // CARD_COMPLETER_STYLER_H
|
||||||
52
cockatrice/src/interface/widgets/utility/completer_utils.cpp
Normal file
52
cockatrice/src/interface/widgets/utility/completer_utils.cpp
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
#include "completer_utils.h"
|
||||||
|
|
||||||
|
#include "card_completer_styler.h"
|
||||||
|
|
||||||
|
#include <QCompleter>
|
||||||
|
#include <QLineEdit>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QStringListModel>
|
||||||
|
#include <libcockatrice/models/database/card/card_completer_proxy_model.h>
|
||||||
|
#include <libcockatrice/models/database/card/card_search_model.h>
|
||||||
|
#include <libcockatrice/models/database/card_database_display_model.h>
|
||||||
|
|
||||||
|
CardCompleterSetup createCardCompleter(CardDatabaseDisplayModel *displayModel, QObject *parent, int maxVisibleItems)
|
||||||
|
{
|
||||||
|
auto *searchModel = new CardSearchModel(displayModel, parent);
|
||||||
|
|
||||||
|
auto *proxyModel = new CardCompleterProxyModel(parent);
|
||||||
|
proxyModel->setSourceModel(searchModel);
|
||||||
|
proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
|
||||||
|
|
||||||
|
auto *completer = new QCompleter(proxyModel, parent);
|
||||||
|
completer->setCompletionRole(Qt::DisplayRole);
|
||||||
|
completer->setCompletionMode(QCompleter::PopupCompletion);
|
||||||
|
completer->setCaseSensitivity(Qt::CaseInsensitive);
|
||||||
|
completer->setFilterMode(Qt::MatchContains);
|
||||||
|
completer->setMaxVisibleItems(maxVisibleItems);
|
||||||
|
CardCompleterStyler::apply(completer);
|
||||||
|
|
||||||
|
return {searchModel, proxyModel, completer};
|
||||||
|
}
|
||||||
|
|
||||||
|
void connectCardCompleterSearch(QLineEdit *edit, const CardCompleterSetup &setup)
|
||||||
|
{
|
||||||
|
QObject::connect(edit, &QLineEdit::textEdited, setup.searchModel, &CardSearchModel::updateSearchResults);
|
||||||
|
QObject::connect(edit, &QLineEdit::textEdited, setup.completer, [setup](const QString &text) {
|
||||||
|
setup.proxyModel->setFilterRegularExpression(
|
||||||
|
QRegularExpression(QRegularExpression::escape(text), QRegularExpression::CaseInsensitiveOption));
|
||||||
|
if (!text.isEmpty()) {
|
||||||
|
setup.completer->complete();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QCompleter *createMentionCompleter(QStringListModel *model, QObject *parent)
|
||||||
|
{
|
||||||
|
auto *completer = new QCompleter(model, parent);
|
||||||
|
completer->setCaseSensitivity(Qt::CaseInsensitive);
|
||||||
|
completer->setMaxVisibleItems(5);
|
||||||
|
completer->setFilterMode(Qt::MatchStartsWith);
|
||||||
|
return completer;
|
||||||
|
}
|
||||||
32
cockatrice/src/interface/widgets/utility/completer_utils.h
Normal file
32
cockatrice/src/interface/widgets/utility/completer_utils.h
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
/**
|
||||||
|
* @file completer_utils.h
|
||||||
|
* @ingroup UtilityWidgets
|
||||||
|
*/
|
||||||
|
//! \todo Document this file.
|
||||||
|
|
||||||
|
#ifndef COMPLETER_UTILS_H
|
||||||
|
#define COMPLETER_UTILS_H
|
||||||
|
|
||||||
|
class CardCompleterProxyModel;
|
||||||
|
class CardDatabaseDisplayModel;
|
||||||
|
class CardSearchModel;
|
||||||
|
class QCompleter;
|
||||||
|
class QLineEdit;
|
||||||
|
class QObject;
|
||||||
|
class QStringListModel;
|
||||||
|
|
||||||
|
struct CardCompleterSetup
|
||||||
|
{
|
||||||
|
CardSearchModel *searchModel;
|
||||||
|
CardCompleterProxyModel *proxyModel;
|
||||||
|
QCompleter *completer;
|
||||||
|
};
|
||||||
|
|
||||||
|
CardCompleterSetup
|
||||||
|
createCardCompleter(CardDatabaseDisplayModel *displayModel, QObject *parent, int maxVisibleItems = 10);
|
||||||
|
|
||||||
|
void connectCardCompleterSearch(QLineEdit *edit, const CardCompleterSetup &setup);
|
||||||
|
|
||||||
|
QCompleter *createMentionCompleter(QStringListModel *model, QObject *parent);
|
||||||
|
|
||||||
|
#endif // CARD_COMPLETER_UTILS_H
|
||||||
|
|
@ -1,135 +1,199 @@
|
||||||
#include "line_edit_completer.h"
|
#include "line_edit_completer.h"
|
||||||
|
|
||||||
#include <QAbstractItemView>
|
#include <QAbstractItemView>
|
||||||
#include <QCompleter>
|
|
||||||
#include <QFocusEvent>
|
#include <QFocusEvent>
|
||||||
#include <QScrollBar>
|
#include <QKeyEvent>
|
||||||
#include <QStringListModel>
|
|
||||||
#include <QTextCursor>
|
|
||||||
#include <QWidget>
|
|
||||||
|
|
||||||
LineEditCompleter::LineEditCompleter(QWidget *parent) : LineEditUnfocusable(parent), c(nullptr)
|
LineEditCompleter::LineEditCompleter(QWidget *parent) : LineEditUnfocusable(parent)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void LineEditCompleter::addCompleter(QCompleter *c, CompleterTrigger trigger)
|
||||||
|
{
|
||||||
|
c->setWidget(this);
|
||||||
|
c->setCompletionMode(QCompleter::PopupCompletion);
|
||||||
|
c->setCaseSensitivity(Qt::CaseInsensitive);
|
||||||
|
connect(c, qOverload<const QString &>(&QCompleter::activated), this,
|
||||||
|
qOverload<const QString &>(&LineEditCompleter::insertCompletion));
|
||||||
|
|
||||||
|
completers.append({c, trigger});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LineEditCompleter::hasVisibleCompleterPopup() const
|
||||||
|
{
|
||||||
|
for (const auto &info : completers) {
|
||||||
|
if (info.completer->popup()->isVisible()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LineEditCompleter::hideCompleterPopups()
|
||||||
|
{
|
||||||
|
for (const auto &info : completers) {
|
||||||
|
info.completer->popup()->hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void LineEditCompleter::focusOutEvent(QFocusEvent *e)
|
void LineEditCompleter::focusOutEvent(QFocusEvent *e)
|
||||||
{
|
{
|
||||||
LineEditUnfocusable::focusOutEvent(e);
|
LineEditUnfocusable::focusOutEvent(e);
|
||||||
if (c->popup()->isVisible()) {
|
|
||||||
// Remove Popup
|
// Only commit the highlighted completion when focus moves away via Tab.
|
||||||
c->popup()->hide();
|
// Other focus losses (e.g. the unfocus shortcut / Escape) must simply close
|
||||||
// Truncate the line to last space or whole string
|
// the popup without inserting anything.
|
||||||
QString textValue = text();
|
if (e->reason() != Qt::TabFocusReason) {
|
||||||
int lastIndex = textValue.length();
|
hideCompleterPopups();
|
||||||
int lastWordStartIndex = textValue.lastIndexOf(" ") + 1;
|
return;
|
||||||
int leftShift = qMin(lastIndex, lastWordStartIndex);
|
|
||||||
setText(textValue.left(leftShift));
|
|
||||||
// Insert highlighted line from popup
|
|
||||||
insert(c->completionModel()->index(c->popup()->currentIndex().row(), 0).data().toString() + " ");
|
|
||||||
// Set focus back to the textbox since tab was pressed
|
|
||||||
setFocus();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (auto &info : completers) {
|
||||||
|
if (!info.completer->popup()->isVisible()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QModelIndex currentIndex = info.completer->popup()->currentIndex();
|
||||||
|
if (currentIndex.isValid()) {
|
||||||
|
insertCompletion(info.completer, currentIndex.data().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hideCompleterPopups();
|
||||||
}
|
}
|
||||||
|
|
||||||
void LineEditCompleter::keyPressEvent(QKeyEvent *event)
|
void LineEditCompleter::keyPressEvent(QKeyEvent *event)
|
||||||
{
|
{
|
||||||
switch (event->key()) {
|
|
||||||
case Qt::Key_Return:
|
|
||||||
case Qt::Key_Enter:
|
|
||||||
case Qt::Key_Escape:
|
|
||||||
if (c->popup()->isVisible()) {
|
|
||||||
event->ignore();
|
|
||||||
// Remove Popup
|
|
||||||
c->popup()->hide();
|
|
||||||
// Truncate the line to last space or whole string
|
|
||||||
QString textValue = text();
|
|
||||||
int lastIndexof = qMax(0, textValue.lastIndexOf(" "));
|
|
||||||
QString finalString = textValue.left(lastIndexof);
|
|
||||||
// Add a space if there's a word
|
|
||||||
if (finalString != "") {
|
|
||||||
finalString += " ";
|
|
||||||
}
|
|
||||||
setText(finalString);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case Qt::Key_Space:
|
|
||||||
if (c->popup()->isVisible()) {
|
|
||||||
event->ignore();
|
|
||||||
// Remove Popup
|
|
||||||
c->popup()->hide();
|
|
||||||
// Truncate the line to last space or whole string
|
|
||||||
QString textValue = text();
|
|
||||||
int lastIndex = textValue.length();
|
|
||||||
int lastWordStartIndex = textValue.lastIndexOf(" ") + 1;
|
|
||||||
int leftShift = qMin(lastIndex, lastWordStartIndex);
|
|
||||||
setText(textValue.left(leftShift));
|
|
||||||
// Insert highlighted line from popup
|
|
||||||
insert(c->completionModel()->index(c->popup()->currentIndex().row(), 0).data().toString() + " ");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
LineEditUnfocusable::keyPressEvent(event);
|
LineEditUnfocusable::keyPressEvent(event);
|
||||||
// return if the completer is null or if the most recently typed char was '@'.
|
|
||||||
// Only want the popup AFTER typing the first char of the mention.
|
if (event->key() == Qt::Key_Escape) {
|
||||||
if (!c || text().right(1).contains("@")) {
|
hideCompleterPopups();
|
||||||
c->popup()->hide();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set new completion prefix
|
QString textValue = text();
|
||||||
c->setCompletionPrefix(cursorWord(text()));
|
int cursorPos = cursorPosition();
|
||||||
if (c->completionPrefix().length() < 1) {
|
|
||||||
c->popup()->hide();
|
CompleterInfo *active = nullptr;
|
||||||
|
QString prefix;
|
||||||
|
|
||||||
|
for (auto &info : completers) {
|
||||||
|
bool triggered = false;
|
||||||
|
switch (info.trigger) {
|
||||||
|
case CompleterTrigger::Mention: {
|
||||||
|
int triggerPos = textValue.lastIndexOf("@", cursorPos - 1);
|
||||||
|
if (triggerPos != -1 && (triggerPos == 0 || textValue[triggerPos - 1].isSpace())) {
|
||||||
|
triggered = true;
|
||||||
|
// Keep the "@" so the prefix matches the "@"-prefixed mention model entries.
|
||||||
|
prefix = textValue.mid(triggerPos, cursorPos - triggerPos);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case CompleterTrigger::Card: {
|
||||||
|
int triggerPos = textValue.lastIndexOf("[[", cursorPos - 1);
|
||||||
|
int closePos = textValue.indexOf("]]", triggerPos + 2);
|
||||||
|
if (triggerPos != -1 && (closePos == -1 || closePos >= cursorPos)) {
|
||||||
|
triggered = true;
|
||||||
|
prefix = textValue.mid(triggerPos + 2, cursorPos - (triggerPos + 2));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (triggered) {
|
||||||
|
active = &info;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!active) {
|
||||||
|
for (auto &info : completers) {
|
||||||
|
info.completer->popup()->hide();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw completion box
|
active->completer->setCompletionPrefix(prefix);
|
||||||
QRect cr = cursorRect();
|
|
||||||
cr.setWidth(c->popup()->sizeHintForColumn(0) + c->popup()->verticalScrollBar()->sizeHint().width());
|
|
||||||
c->complete(cr);
|
|
||||||
|
|
||||||
// Select first item in the completion popup
|
switch (active->trigger) {
|
||||||
QItemSelectionModel *sm = new QItemSelectionModel(c->completionModel());
|
case CompleterTrigger::Card:
|
||||||
c->popup()->setSelectionModel(sm);
|
emit cardPartialChanged(prefix);
|
||||||
sm->select(c->completionModel()->index(0, 0), QItemSelectionModel::ClearAndSelect);
|
return;
|
||||||
sm->setCurrentIndex(c->completionModel()->index(0, 0), QItemSelectionModel::NoUpdate);
|
case CompleterTrigger::Mention:
|
||||||
}
|
break;
|
||||||
|
|
||||||
QString LineEditCompleter::cursorWord(const QString &line) const
|
|
||||||
{
|
|
||||||
return line.mid(line.left(cursorPosition()).lastIndexOf(" ") + 1,
|
|
||||||
cursorPosition() - line.left(cursorPosition()).lastIndexOf(" ") - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
void LineEditCompleter::insertCompletion(QString arg)
|
|
||||||
{
|
|
||||||
QString s_arg = arg + " ";
|
|
||||||
setText(text().replace(text().left(cursorPosition()).lastIndexOf(" ") + 1,
|
|
||||||
cursorPosition() - text().left(cursorPosition()).lastIndexOf(" ") - 1, s_arg));
|
|
||||||
}
|
|
||||||
|
|
||||||
void LineEditCompleter::setCompleter(QCompleter *completer)
|
|
||||||
{
|
|
||||||
c = completer;
|
|
||||||
c->setWidget(this);
|
|
||||||
connect(c, qOverload<const QString &>(&QCompleter::activated), this, &LineEditCompleter::insertCompletion);
|
|
||||||
}
|
|
||||||
|
|
||||||
void LineEditCompleter::setCompletionList(QStringList completionList)
|
|
||||||
{
|
|
||||||
if (!c || c->popup()->isVisible()) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QStringListModel *model;
|
active->completer->complete();
|
||||||
model = (QStringListModel *)(c->model());
|
}
|
||||||
if (model == NULL) {
|
|
||||||
model = new QStringListModel();
|
void LineEditCompleter::insertCompletion(const QString &completion)
|
||||||
}
|
{
|
||||||
model->setStringList(completionList);
|
for (auto &info : completers) {
|
||||||
|
if (info.completer == sender()) {
|
||||||
|
insertCompletion(info.completer, completion);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LineEditCompleter::insertCompletion(QCompleter *completer, const QString &completion)
|
||||||
|
{
|
||||||
|
QString t = text();
|
||||||
|
int pos = cursorPosition();
|
||||||
|
|
||||||
|
for (const auto &info : completers) {
|
||||||
|
if (info.completer != completer) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (info.trigger) {
|
||||||
|
case CompleterTrigger::Card: {
|
||||||
|
int triggerPos = t.lastIndexOf("[[", pos - 1);
|
||||||
|
if (triggerPos == -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If an earlier "[[" is still open it also encloses the cursor, so
|
||||||
|
// replace from its start. Otherwise completing in text such as
|
||||||
|
// "[[Opt[[Amok" would leave a stray "[[" behind.
|
||||||
|
int startPos = triggerPos;
|
||||||
|
for (int searchFrom = triggerPos; searchFrom > 0;) {
|
||||||
|
const int earlier = t.lastIndexOf("[[", searchFrom - 1);
|
||||||
|
if (earlier == -1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const int earlierClose = t.indexOf("]]", earlier + 2);
|
||||||
|
if (earlierClose != -1 && earlierClose < pos) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
startPos = earlier;
|
||||||
|
searchFrom = earlier;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the cursor sits inside an already-closed [[...]] pair, replace
|
||||||
|
// the whole construct instead of leaving a duplicate closing bracket
|
||||||
|
// behind.
|
||||||
|
int insertEnd = pos;
|
||||||
|
const int closePos = t.indexOf("]]", startPos + 2);
|
||||||
|
if (closePos != -1 && closePos >= pos) {
|
||||||
|
insertEnd = closePos + 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString after = t.mid(insertEnd);
|
||||||
|
QString replaced = t.left(startPos + 2) + completion + "]] ";
|
||||||
|
setText(replaced + after);
|
||||||
|
setCursorPosition(replaced.length());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case CompleterTrigger::Mention: {
|
||||||
|
int triggerPos = t.lastIndexOf("@", pos - 1);
|
||||||
|
if (triggerPos == -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setText(t.replace(triggerPos, pos - triggerPos, completion + " "));
|
||||||
|
setCursorPosition(triggerPos + completion.length() + 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* @file line_edit_completer.h
|
* @file line_edit_completer.h
|
||||||
* @ingroup UI
|
* @ingroup UI
|
||||||
|
* @brief Line edit with support for multiple trigger-based completers, e.g., @mention and [[card]].
|
||||||
*/
|
*/
|
||||||
//! \todo Document this file.
|
//! \todo Document this file.
|
||||||
|
|
||||||
|
|
@ -9,25 +10,49 @@
|
||||||
|
|
||||||
#include "custom_line_edit.h"
|
#include "custom_line_edit.h"
|
||||||
|
|
||||||
|
#include <QCompleter>
|
||||||
|
#include <QFocusEvent>
|
||||||
#include <QKeyEvent>
|
#include <QKeyEvent>
|
||||||
|
#include <QList>
|
||||||
|
#include <QString>
|
||||||
|
#include <QVector>
|
||||||
|
|
||||||
|
enum class CompleterTrigger
|
||||||
|
{
|
||||||
|
Mention, //< "@" prefix, replaced in place with a trailing space.
|
||||||
|
Card //< "[[" prefix, replaced with the completion plus a closing "]] ".
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CompleterInfo
|
||||||
|
{
|
||||||
|
QCompleter *completer;
|
||||||
|
CompleterTrigger trigger;
|
||||||
|
};
|
||||||
|
|
||||||
class LineEditCompleter : public LineEditUnfocusable
|
class LineEditCompleter : public LineEditUnfocusable
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
signals:
|
||||||
|
void cardPartialChanged(const QString &partial);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QString cursorWord(const QString &line) const;
|
QList<CompleterInfo> completers;
|
||||||
QCompleter *c;
|
|
||||||
|
void insertCompletion(QCompleter *completer, const QString &completion);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void insertCompletion(QString);
|
void insertCompletion(const QString &text);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void keyPressEvent(QKeyEvent *event);
|
void keyPressEvent(QKeyEvent *event) override;
|
||||||
void focusOutEvent(QFocusEvent *e);
|
void focusOutEvent(QFocusEvent *e) override;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit LineEditCompleter(QWidget *parent = nullptr);
|
explicit LineEditCompleter(QWidget *parent = nullptr);
|
||||||
void setCompleter(QCompleter *);
|
void addCompleter(QCompleter *c, CompleterTrigger trigger);
|
||||||
void setCompletionList(QStringList);
|
|
||||||
|
bool hasVisibleCompleterPopup() const;
|
||||||
|
void hideCompleterPopups();
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
#include "reversed_completer_model.h"
|
||||||
|
|
||||||
|
#include <QAbstractItemModel>
|
||||||
|
#include <QModelIndex>
|
||||||
|
#include <QVariant>
|
||||||
|
|
||||||
|
void ReversedCompleterModel::setSourceModel(QAbstractItemModel *sourceModel)
|
||||||
|
{
|
||||||
|
if (sourceModel == this->sourceModel()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (QAbstractItemModel *old = this->sourceModel()) {
|
||||||
|
disconnect(old, nullptr, this, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
QAbstractProxyModel::setSourceModel(sourceModel);
|
||||||
|
|
||||||
|
if (sourceModel) {
|
||||||
|
connect(sourceModel, &QAbstractItemModel::modelReset, this, &ReversedCompleterModel::invalidate);
|
||||||
|
connect(sourceModel, &QAbstractItemModel::rowsInserted, this, &ReversedCompleterModel::invalidate);
|
||||||
|
connect(sourceModel, &QAbstractItemModel::rowsRemoved, this, &ReversedCompleterModel::invalidate);
|
||||||
|
connect(sourceModel, &QAbstractItemModel::rowsMoved, this, &ReversedCompleterModel::invalidate);
|
||||||
|
connect(sourceModel, &QAbstractItemModel::columnsInserted, this, &ReversedCompleterModel::invalidate);
|
||||||
|
connect(sourceModel, &QAbstractItemModel::columnsRemoved, this, &ReversedCompleterModel::invalidate);
|
||||||
|
connect(sourceModel, &QAbstractItemModel::dataChanged, this, &ReversedCompleterModel::invalidate);
|
||||||
|
connect(sourceModel, &QAbstractItemModel::layoutChanged, this, &ReversedCompleterModel::invalidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReversedCompleterModel::setEnabled(bool enabled)
|
||||||
|
{
|
||||||
|
if (enabled == isEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isEnabled = enabled;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
QModelIndex ReversedCompleterModel::mapToSource(const QModelIndex &proxyIndex) const
|
||||||
|
{
|
||||||
|
if (!proxyIndex.isValid() || !sourceModel()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const int sourceRow = isEnabled ? sourceRowCount() - 1 - proxyIndex.row() : proxyIndex.row();
|
||||||
|
return sourceModel()->index(sourceRow, proxyIndex.column());
|
||||||
|
}
|
||||||
|
|
||||||
|
QModelIndex ReversedCompleterModel::mapFromSource(const QModelIndex &sourceIndex) const
|
||||||
|
{
|
||||||
|
if (!sourceIndex.isValid() || !sourceModel()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const int proxyRow = isEnabled ? sourceRowCount() - 1 - sourceIndex.row() : sourceIndex.row();
|
||||||
|
return index(proxyRow, sourceIndex.column());
|
||||||
|
}
|
||||||
|
|
||||||
|
QModelIndex ReversedCompleterModel::index(int row, int column, const QModelIndex &parent) const
|
||||||
|
{
|
||||||
|
if (parent.isValid() || !sourceModel() || row < 0 || row >= rowCount() || column < 0 || column >= columnCount()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return createIndex(row, column);
|
||||||
|
}
|
||||||
|
|
||||||
|
QModelIndex ReversedCompleterModel::parent(const QModelIndex &) const
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
int ReversedCompleterModel::rowCount(const QModelIndex &parent) const
|
||||||
|
{
|
||||||
|
return parent.isValid() || !sourceModel() ? 0 : sourceModel()->rowCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
int ReversedCompleterModel::columnCount(const QModelIndex &parent) const
|
||||||
|
{
|
||||||
|
return parent.isValid() || !sourceModel() ? 0 : sourceModel()->columnCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant ReversedCompleterModel::data(const QModelIndex &proxyIndex, int role) const
|
||||||
|
{
|
||||||
|
return sourceModel() ? sourceModel()->data(mapToSource(proxyIndex), role) : QVariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant ReversedCompleterModel::headerData(int section, Qt::Orientation orientation, int role) const
|
||||||
|
{
|
||||||
|
return sourceModel() ? sourceModel()->headerData(section, orientation, role) : QVariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
int ReversedCompleterModel::sourceRowCount() const
|
||||||
|
{
|
||||||
|
return sourceModel() ? sourceModel()->rowCount() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReversedCompleterModel::invalidate()
|
||||||
|
{
|
||||||
|
beginResetModel();
|
||||||
|
endResetModel();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
/**
|
||||||
|
* @file reversed_completer_model.h
|
||||||
|
* @ingroup UtilityWidgets
|
||||||
|
*/
|
||||||
|
//! \todo Document this file.
|
||||||
|
|
||||||
|
#ifndef REVERSED_COMPLETER_MODEL_H
|
||||||
|
#define REVERSED_COMPLETER_MODEL_H
|
||||||
|
|
||||||
|
#include <QAbstractProxyModel>
|
||||||
|
|
||||||
|
class QAbstractItemModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A completer model that can present its rows bottom-to-top.
|
||||||
|
*
|
||||||
|
* The original row order is kept intact in the source model (row 0 is always the
|
||||||
|
* closest match). When enabled, the proxy maps the source rows in reverse so the
|
||||||
|
* popup shows the closest match in the row nearest to the text edit. Any change
|
||||||
|
* in the source model is forwarded as a full reset, which is all QCompleter
|
||||||
|
* needs to rebuild its completion list.
|
||||||
|
*/
|
||||||
|
class ReversedCompleterModel : public QAbstractProxyModel
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using QAbstractProxyModel::QAbstractProxyModel;
|
||||||
|
|
||||||
|
void setSourceModel(QAbstractItemModel *sourceModel) override;
|
||||||
|
void setEnabled(bool enabled);
|
||||||
|
|
||||||
|
QModelIndex mapToSource(const QModelIndex &proxyIndex) const override;
|
||||||
|
QModelIndex mapFromSource(const QModelIndex &sourceIndex) const override;
|
||||||
|
QModelIndex index(int row, int column, const QModelIndex &parent = {}) const override;
|
||||||
|
QModelIndex parent(const QModelIndex &) const override;
|
||||||
|
int rowCount(const QModelIndex &parent = {}) const override;
|
||||||
|
int columnCount(const QModelIndex &parent = {}) const override;
|
||||||
|
QVariant data(const QModelIndex &proxyIndex, int role = Qt::DisplayRole) const override;
|
||||||
|
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
int sourceRowCount() const;
|
||||||
|
void invalidate();
|
||||||
|
|
||||||
|
bool isEnabled = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // REVERSED_COMPLETER_MODEL_H
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
#include "../tabs/visual_deck_editor/tab_deck_editor_visual.h"
|
#include "../tabs/visual_deck_editor/tab_deck_editor_visual.h"
|
||||||
#include "../tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.h"
|
#include "../tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.h"
|
||||||
#include "../utility/compact_push_button.h"
|
#include "../utility/compact_push_button.h"
|
||||||
|
#include "../utility/completer_utils.h"
|
||||||
#include "visual_deck_display_options_widget.h"
|
#include "visual_deck_display_options_widget.h"
|
||||||
|
|
||||||
#include <QCheckBox>
|
#include <QCheckBox>
|
||||||
|
|
@ -21,8 +22,6 @@
|
||||||
#include <libcockatrice/card/card_info_comparator.h>
|
#include <libcockatrice/card/card_info_comparator.h>
|
||||||
#include <libcockatrice/card/database/card_database.h>
|
#include <libcockatrice/card/database/card_database.h>
|
||||||
#include <libcockatrice/card/database/card_database_manager.h>
|
#include <libcockatrice/card/database/card_database_manager.h>
|
||||||
#include <libcockatrice/models/database/card/card_completer_proxy_model.h>
|
|
||||||
#include <libcockatrice/models/database/card/card_search_model.h>
|
|
||||||
#include <libcockatrice/models/database/card_database_model.h>
|
#include <libcockatrice/models/database/card_database_model.h>
|
||||||
#include <libcockatrice/models/deck_list/deck_list_model.h>
|
#include <libcockatrice/models/deck_list/deck_list_model.h>
|
||||||
#include <libcockatrice/settings/cards_display_settings.h>
|
#include <libcockatrice/settings/cards_display_settings.h>
|
||||||
|
|
@ -98,32 +97,14 @@ void VisualDeckEditorWidget::initializeSearchBarAndCompleter()
|
||||||
cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this);
|
cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this);
|
||||||
cardDatabaseDisplayModel = new CardDatabaseDisplayModel(this);
|
cardDatabaseDisplayModel = new CardDatabaseDisplayModel(this);
|
||||||
cardDatabaseDisplayModel->setSourceModel(cardDatabaseModel);
|
cardDatabaseDisplayModel->setSourceModel(cardDatabaseModel);
|
||||||
CardSearchModel *searchModel = new CardSearchModel(cardDatabaseDisplayModel, this);
|
|
||||||
|
|
||||||
proxyModel = new CardCompleterProxyModel(this);
|
const CardCompleterSetup cardSetup = createCardCompleter(cardDatabaseDisplayModel, this, 15);
|
||||||
proxyModel->setSourceModel(searchModel);
|
proxyModel = cardSetup.proxyModel;
|
||||||
proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
|
completer = cardSetup.completer;
|
||||||
proxyModel->setFilterRole(Qt::DisplayRole);
|
|
||||||
|
|
||||||
completer = new QCompleter(proxyModel, this);
|
|
||||||
completer->setCompletionRole(Qt::DisplayRole);
|
|
||||||
completer->setCompletionMode(QCompleter::PopupCompletion);
|
|
||||||
completer->setCaseSensitivity(Qt::CaseInsensitive);
|
|
||||||
completer->setFilterMode(Qt::MatchContains);
|
|
||||||
completer->setMaxVisibleItems(15);
|
|
||||||
searchBar->setCompleter(completer);
|
searchBar->setCompleter(completer);
|
||||||
|
|
||||||
// Update suggestions dynamically
|
// Update suggestions dynamically
|
||||||
connect(searchBar, &QLineEdit::textEdited, searchModel, &CardSearchModel::updateSearchResults);
|
connectCardCompleterSearch(searchBar, cardSetup);
|
||||||
connect(searchBar, &QLineEdit::textEdited, this, [=, 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
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
connect(completer, static_cast<void (QCompleter::*)(const QString &)>(&QCompleter::activated), this,
|
connect(completer, static_cast<void (QCompleter::*)(const QString &)>(&QCompleter::activated), this,
|
||||||
[=, this](const QString &completion) {
|
[=, this](const QString &completion) {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,10 @@ CardCompleterProxyModel::CardCompleterProxyModel(QObject *parent) : QSortFilterP
|
||||||
|
|
||||||
bool CardCompleterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
|
bool CardCompleterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
|
||||||
{
|
{
|
||||||
|
if (!sourceModel()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (filterRegularExpression().pattern().isEmpty()) {
|
if (filterRegularExpression().pattern().isEmpty()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -13,6 +17,5 @@ bool CardCompleterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex
|
||||||
QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
|
QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
|
||||||
QString data = index.data(Qt::DisplayRole).toString();
|
QString data = index.data(Qt::DisplayRole).toString();
|
||||||
|
|
||||||
// Ensure substring matching
|
|
||||||
return data.contains(filterRegularExpression());
|
return data.contains(filterRegularExpression());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,14 @@ QVariant CardSearchModel::data(const QModelIndex &index, int role) const
|
||||||
return QVariant();
|
return QVariant();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SearchResult &result = searchResults.at(index.row());
|
||||||
|
|
||||||
if (role == Qt::DisplayRole) {
|
if (role == Qt::DisplayRole) {
|
||||||
return searchResults.at(index.row()).card->getName();
|
return result.card->getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role == CardInfoRole) {
|
||||||
|
return QVariant::fromValue(result.card);
|
||||||
}
|
}
|
||||||
|
|
||||||
return QVariant();
|
return QVariant();
|
||||||
|
|
@ -36,43 +42,62 @@ void CardSearchModel::updateSearchResults(const QString &query)
|
||||||
searchResults.clear();
|
searchResults.clear();
|
||||||
|
|
||||||
if (query.isEmpty() || !sourceModel) {
|
if (query.isEmpty() || !sourceModel) {
|
||||||
|
endResetModel();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the filter for the display model
|
CardDatabaseModel *sourceDbModel = qobject_cast<CardDatabaseModel *>(sourceModel->sourceModel());
|
||||||
sourceModel->setCardName(query);
|
if (!sourceDbModel) {
|
||||||
|
endResetModel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Collect matching cards and compute Levenshtein distance
|
const QString lowerQuery = query.toLower();
|
||||||
for (int i = 0; i < sourceModel->rowCount(); ++i) {
|
|
||||||
QModelIndex modelIndex = sourceModel->index(i, 0);
|
|
||||||
QModelIndex sourceIndex = sourceModel->mapToSource(modelIndex);
|
|
||||||
CardDatabaseModel *sourceDbModel = qobject_cast<CardDatabaseModel *>(sourceModel->sourceModel());
|
|
||||||
|
|
||||||
if (!sourceDbModel || !sourceIndex.isValid()) {
|
QList<SearchResult> prefixMatches;
|
||||||
return;
|
QList<SearchResult> containsMatches;
|
||||||
}
|
|
||||||
|
|
||||||
CardInfoPtr card = sourceDbModel->getCard(sourceIndex.row());
|
|
||||||
|
|
||||||
|
// Iterate the raw database model directly so results are always complete and fresh
|
||||||
|
const int rowCount = sourceDbModel->rowCount();
|
||||||
|
for (int i = 0; i < rowCount; ++i) {
|
||||||
|
CardInfoPtr card = sourceDbModel->getCard(i);
|
||||||
if (!card) {
|
if (!card) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
int distance = levenshteinDistance(query.toLower(), card->getName().toLower());
|
const QString lowerName = card->getName().toLower();
|
||||||
searchResults.append({card, distance});
|
if (!lowerName.contains(lowerQuery)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int distance = levenshteinDistance(lowerQuery, lowerName);
|
||||||
|
|
||||||
|
if (lowerName.startsWith(lowerQuery)) {
|
||||||
|
prefixMatches.append({card, distance});
|
||||||
|
} else {
|
||||||
|
containsMatches.append({card, distance});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by Levenshtein distance (lower distance = better match)
|
auto sortByDistanceThenLength = [](const SearchResult &a, const SearchResult &b) {
|
||||||
std::sort(searchResults.begin(), searchResults.end(),
|
if (a.distance != b.distance) {
|
||||||
[](const SearchResult &a, const SearchResult &b) { return a.distance < b.distance; });
|
return a.distance < b.distance;
|
||||||
|
}
|
||||||
|
return a.card->getName().size() < b.card->getName().size();
|
||||||
|
};
|
||||||
|
|
||||||
// Keep only the top 5 results
|
std::sort(prefixMatches.begin(), prefixMatches.end(), sortByDistanceThenLength);
|
||||||
|
std::sort(containsMatches.begin(), containsMatches.end(), sortByDistanceThenLength);
|
||||||
|
|
||||||
|
// Prefix matches always come first, then contains-only matches
|
||||||
|
searchResults.reserve(prefixMatches.size() + containsMatches.size());
|
||||||
|
searchResults.append(prefixMatches);
|
||||||
|
searchResults.append(containsMatches);
|
||||||
|
|
||||||
|
// Keep only the top 10 results
|
||||||
if (searchResults.size() > 10) {
|
if (searchResults.size() > 10) {
|
||||||
searchResults = searchResults.mid(0, 10);
|
searchResults = searchResults.mid(0, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
emit dataChanged(index(0, 0), index(rowCount() - 1, 0));
|
|
||||||
emit layoutChanged();
|
|
||||||
|
|
||||||
endResetModel();
|
endResetModel();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,11 @@ class CardSearchModel : public QAbstractListModel
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
|
enum CardSearchRoles
|
||||||
|
{
|
||||||
|
CardInfoRole = Qt::UserRole + 1,
|
||||||
|
};
|
||||||
|
|
||||||
explicit CardSearchModel(CardDatabaseDisplayModel *sourceModel, QObject *parent = nullptr);
|
explicit CardSearchModel(CardDatabaseDisplayModel *sourceModel, QObject *parent = nullptr);
|
||||||
|
|
||||||
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue