mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-06 01:53:25 -08:00
An open [[ card link now takes precedence over a space-preceded @ mention, so typing an @ before a card no longer shadows the card completer. Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
272 lines
9.2 KiB
C++
272 lines
9.2 KiB
C++
#include "line_edit_completer.h"
|
|
|
|
#include <QAbstractItemModel>
|
|
#include <QAbstractItemView>
|
|
#include <QChar>
|
|
#include <QCompleter>
|
|
#include <QFocusEvent>
|
|
#include <QItemSelectionModel>
|
|
#include <QKeyEvent>
|
|
#include <QModelIndex>
|
|
#include <QVariant>
|
|
#include <QtGlobal>
|
|
#include <qnamespace.h>
|
|
|
|
class QWidget;
|
|
|
|
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)
|
|
{
|
|
LineEditUnfocusable::focusOutEvent(e);
|
|
|
|
// Commit completion when focus moves away via Tab if popup is visible.
|
|
// Other focus losses (e.g. the unfocus shortcut / Escape) must simply close
|
|
// the popup without inserting anything.
|
|
bool popupWasVisible = false;
|
|
for (auto &info : completers) {
|
|
if (info.completer->popup()->isVisible()) {
|
|
popupWasVisible = true;
|
|
const QModelIndex currentIndex = info.completer->popup()->currentIndex();
|
|
QString completionText;
|
|
if (currentIndex.isValid()) {
|
|
completionText = currentIndex.data().toString();
|
|
} else {
|
|
completionText = info.completer->currentCompletion();
|
|
}
|
|
if (!completionText.isEmpty()) {
|
|
insertCompletion(info.completer, completionText);
|
|
}
|
|
}
|
|
}
|
|
hideCompleterPopups();
|
|
|
|
if (popupWasVisible && e->reason() == Qt::TabFocusReason) {
|
|
// Refocus the edit so Tab doesn't move focus away
|
|
// Use a queued connection to ensure focus is restored after the focus change completes
|
|
QMetaObject::invokeMethod(this, "setFocus", Qt::QueuedConnection);
|
|
e->accept();
|
|
return;
|
|
}
|
|
|
|
if (popupWasVisible) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
void LineEditCompleter::keyPressEvent(QKeyEvent *event)
|
|
{
|
|
if (event->key() == Qt::Key_Escape) {
|
|
if (hasVisibleCompleterPopup()) {
|
|
event->ignore();
|
|
hideCompleterPopups();
|
|
return;
|
|
}
|
|
LineEditUnfocusable::keyPressEvent(event);
|
|
return;
|
|
}
|
|
|
|
if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter || event->key() == Qt::Key_Space) {
|
|
for (auto &info : completers) {
|
|
if (info.completer->popup()->isVisible()) {
|
|
event->ignore();
|
|
const QModelIndex currentIndex = info.completer->popup()->currentIndex();
|
|
QString completionText;
|
|
if (currentIndex.isValid()) {
|
|
completionText = currentIndex.data().toString();
|
|
} else {
|
|
completionText = info.completer->currentCompletion();
|
|
}
|
|
if (!completionText.isEmpty()) {
|
|
insertCompletion(info.completer, completionText);
|
|
}
|
|
hideCompleterPopups();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (event->key() == Qt::Key_Tab) {
|
|
// Let Tab proceed normally; focusOutEvent will handle completion if popup is visible
|
|
LineEditUnfocusable::keyPressEvent(event);
|
|
return;
|
|
}
|
|
|
|
LineEditUnfocusable::keyPressEvent(event);
|
|
|
|
QString textValue = text();
|
|
int cursorPos = cursorPosition();
|
|
|
|
CompleterInfo *active = nullptr;
|
|
QString prefix;
|
|
|
|
// An open "[[" card link takes precedence over a mention: while the cursor is
|
|
// inside it, an earlier "@" must not shadow the card completer.
|
|
const int openCardPos = textValue.lastIndexOf("[[", cursorPos - 1);
|
|
const int closeCardPos = openCardPos == -1 ? -1 : textValue.indexOf("]]", openCardPos + 2);
|
|
const bool insideCardLink = openCardPos != -1 && (closeCardPos == -1 || closeCardPos >= cursorPos);
|
|
|
|
for (auto &info : completers) {
|
|
bool triggered = false;
|
|
switch (info.trigger) {
|
|
case CompleterTrigger::Mention: {
|
|
if (insideCardLink) {
|
|
break;
|
|
}
|
|
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;
|
|
}
|
|
|
|
active->completer->setCompletionPrefix(prefix);
|
|
|
|
switch (active->trigger) {
|
|
case CompleterTrigger::Card:
|
|
emit cardPartialChanged(prefix);
|
|
return;
|
|
case CompleterTrigger::Mention:
|
|
break;
|
|
}
|
|
|
|
active->completer->complete();
|
|
|
|
// The popup opens without a current row, so nothing is highlighted and there
|
|
// is no visual indication of what Enter will insert. Select the row that
|
|
// currentCompletion() would use so the current mention stays visible.
|
|
auto *completionModel = active->completer->completionModel();
|
|
const int currentRow = active->completer->currentRow();
|
|
if (currentRow >= 0 && currentRow < completionModel->rowCount()) {
|
|
active->completer->popup()->selectionModel()->setCurrentIndex(
|
|
completionModel->index(currentRow, active->completer->completionColumn()),
|
|
QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
|
|
}
|
|
}
|
|
|
|
void LineEditCompleter::insertCompletion(const QString &completion)
|
|
{
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|