Cockatrice/cockatrice/src/interface/widgets/utility/card_completer_styler.cpp
BruebachL be2c16eca5
[UI] Fix auto-completion acceptance on Space/Tab/Enter (#7466)
* [UI] Fix auto-completion acceptance on Space/Tab/Enter

* [UI] Accept current completion when no item selected in popup

* [UI] Also accept completion on focus out when none selected

* [UI] Keep focus in chat on Tab completion acceptance

* [UI] Prevent Tab from moving focus when no completion popup

* [UI] Refocus on Tab when completing

* [UI] Let focusOut handle Tab completion acceptance

* [UI] Restore Tab behavior to match original (handle in focusOut)

* [UI] Defer refocus on Tab to avoid focus chain issues

* [UI] Auto-select first completion match in popup

* [UI] Auto-select first match when completer popup resets

* Revert "[UI] Auto-select first match when completer popup resets"

This reverts commit c7a3de3595.

* Revert "[UI] Auto-select first completion match in popup"

This reverts commit ae0dfb8622.

* [UI] Ensure completion selection updates on model changes

* Revert "[UI] Ensure completion selection updates on model changes"

This reverts commit 686bbef5b2.

* [UI] Highlight the selected completion in chat popups

The card completer's above-variant moved the current row with
NoUpdate, so the delegate never painted the selection border, and it
used Select which accumulated highlights while navigating. Use
ClearAndSelect so only the current row is highlighted and the old one is
cleared.

The mention popup opened with no current row at all, so nothing was
highlighted and there was no visual hint of what Enter would insert.
Select the row currentCompletion() uses, and install a delegate that
mirrors the active highlight into the inactive group so the selection
stays visible when the transient popup is not the active window.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-10 09:45:42 +02:00

360 lines
10 KiB
C++

#include "card_completer_styler.h"
#include "../cards/card_info_picture_enlarged_widget.h"
#include "card_completer_delegate.h"
#include "libcockatrice/card/database/card_database_querier.h"
#include "libcockatrice/card/printing/exact_card.h"
#include "reversed_completer_model.h"
#include <QAbstractAnimation>
#include <QAbstractItemModel>
#include <QAbstractItemView>
#include <QByteArray>
#include <QCompleter>
#include <QEvent>
#include <QItemSelectionModel>
#include <QKeyEvent>
#include <QList>
#include <QMouseEvent>
#include <QPoint>
#include <QPropertyAnimation>
#include <QRect>
#include <QScreen>
#include <QSharedPointer>
#include <QSize>
#include <QString>
#include <QVariant>
#include <QWidget>
#include <QtGlobal>
#include <libcockatrice/card/card_info.h>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/models/database/card/card_search_model.h>
#include <libcockatrice/utility/card_ref.h>
#include <qnamespace.h>
template <class T> class QSharedPointer;
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) {
const QModelIndex newIndex = completer->completionModel()->index(newRow, completer->completionColumn());
popup->selectionModel()->setCurrentIndex(newIndex,
QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
}
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->selectionModel()->setCurrentIndex(completionModel->index(closestRow, completer->completionColumn()),
QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
}
}
// ---------------------------------------------------------------------------
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);
}