mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-09 03:23:24 -08:00
* [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 commitc7a3de3595. * Revert "[UI] Auto-select first completion match in popup" This reverts commitae0dfb8622. * [UI] Ensure completion selection updates on model changes * Revert "[UI] Ensure completion selection updates on model changes" This reverts commit686bbef5b2. * [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>
360 lines
10 KiB
C++
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);
|
|
}
|