subsurface/desktop-widgets/tagwidget.cpp
Berthold Stoeger 803727395b desktop: improve composition on TagWidgets
The TagWidgets hook into the textChanged() signal to invoke
the word-completer. However, that signal is also emitted for
composition-keys, making composition impossible if the completer
decides that it should show some entries.

Instead, hook into the inputMethodEvent() function, where one
can test whether a real character was input.

Also, don't hook into cursorPositionChanged(), since that led
to an uncanny cascade of reparse() calls when editing text.

The UI experience is still rough as sometimes the completer
popup steals the focus and hinders further entry.

Also, this doesn't fix the location field, which is its own class.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2022-08-13 11:19:37 -07:00

261 lines
6.5 KiB
C++

// SPDX-License-Identifier: GPL-2.0
#include "tagwidget.h"
#include "mainwindow.h"
#include "tab-widgets/maintab.h"
#include <QCompleter>
#include <QMimeData>
TagWidget::TagWidget(QWidget *parent) : GroupedLineEdit(parent), m_completer(NULL), lastFinishedTag(false)
{
QColor textColor = palette().color(QPalette::Text);
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
float h, s, l, a;
#else
qreal h, s, l, a;
#endif
textColor.getHslF(&h, &s, &l, &a);
// I use dark themes
if (l <= 0.3) { // very dark text. get a brigth background
addColor(QColor(Qt::red).lighter(120));
addColor(QColor(Qt::green).lighter(120));
addColor(QColor(Qt::blue).lighter(120));
} else if (l <= 0.6) { // moderated dark text. get a somewhat bright background
addColor(QColor(Qt::red).lighter(60));
addColor(QColor(Qt::green).lighter(60));
addColor(QColor(Qt::blue).lighter(60));
} else {
addColor(QColor(Qt::red).darker(120));
addColor(QColor(Qt::green).darker(120));
addColor(QColor(Qt::blue).darker(120));
} // light text. get a dark background.
setTabChangesFocus(true);
setFocusPolicy(Qt::StrongFocus);
}
void TagWidget::setCompleter(QCompleter *completer)
{
m_completer = completer;
m_completer->setWidget(this);
connect(m_completer, QOverload<const QString &>::of(&QCompleter::activated), this, &TagWidget::completionSelected);
connect(m_completer, QOverload<const QString &>::of(&QCompleter::highlighted), this, &TagWidget::completionHighlighted);
}
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
#define SKIP_EMPTY Qt::SkipEmptyParts
#else
#define SKIP_EMPTY QString::SkipEmptyParts
#endif
QPair<int, int> TagWidget::getCursorTagPosition()
{
int i = 0, start = 0, end = 0;
/* Parse string near cursor */
i = cursorPosition();
while (--i > 0) {
if (text().at(i) == ',') {
if (i > 0 && text().at(i - 1) != '\\') {
i++;
break;
}
}
}
start = i;
while (++i < text().length()) {
if (text().at(i) == ',') {
if (i > 0 && text().at(i - 1) != '\\')
break;
}
}
end = i;
if (start < 0 || end < 0) {
start = 0;
end = 0;
}
return qMakePair(start, end);
}
void TagWidget::highlight()
{
removeAllBlocks();
int lastPos = 0;
const auto l = text().split(QChar(','), SKIP_EMPTY);
for (const QString &s: l) {
QString trimmed = s.trimmed();
if (trimmed.isEmpty())
continue;
int start = text().indexOf(trimmed, lastPos);
addBlock(start, trimmed.size() + start);
lastPos = trimmed.size() + start;
}
}
void TagWidget::inputMethodEvent(QInputMethodEvent *e)
{
GroupedLineEdit::inputMethodEvent(e);
if (!e->commitString().isEmpty())
reparse();
}
void TagWidget::reparse()
{
highlight();
QPair<int, int> pos = getCursorTagPosition();
QString currentText;
if (pos.first >= 0 && pos.second > 0)
currentText = text().mid(pos.first, pos.second - pos.first).trimmed();
if (m_completer) {
m_completer->setCompletionPrefix(currentText);
if (m_completer->completionCount() == 1) {
if (m_completer->currentCompletion() == currentText) {
QAbstractItemView *popup = m_completer->popup();
if (popup)
popup->hide();
} else {
m_completer->complete();
}
} else {
m_completer->complete();
}
}
}
void TagWidget::completionSelected(const QString &completion)
{
completionHighlighted(completion);
emit textChanged();
}
void TagWidget::completionHighlighted(const QString &completion)
{
QPair<int, int> pos = getCursorTagPosition();
setText(text().remove(pos.first, pos.second - pos.first).insert(pos.first, completion));
setCursorPosition(pos.first + completion.length());
}
void TagWidget::setCursorPosition(int position)
{
blockSignals(true);
GroupedLineEdit::setCursorPosition(position);
blockSignals(false);
}
void TagWidget::setText(const QString &text)
{
blockSignals(true);
GroupedLineEdit::setText(text);
blockSignals(false);
highlight();
}
void TagWidget::clear()
{
blockSignals(true);
GroupedLineEdit::clear();
blockSignals(false);
}
void TagWidget::keyPressEvent(QKeyEvent *e)
{
QPair<int, int> pos;
QAbstractItemView *popup;
bool finishedTag = false;
switch (e->key()) {
case Qt::Key_Escape:
pos = getCursorTagPosition();
if (pos.first >= 0 && pos.second > 0) {
setText(text().remove(pos.first, pos.second - pos.first));
setCursorPosition(pos.first);
}
popup = m_completer->popup();
if (popup)
popup->hide();
return;
case Qt::Key_Return:
case Qt::Key_Enter:
case Qt::Key_Tab:
/*
* Fake the QLineEdit behaviour by simply
* closing the QAbstractViewitem
*/
if (m_completer) {
popup = m_completer->popup();
if (popup)
popup->hide();
}
finishedTag = true;
break;
case Qt::Key_Comma: { /* if this is the last key, and the previous string is empty, ignore the comma. */
QString temp = text();
if (temp.split(QChar(',')).last().trimmed().isEmpty()){
e->ignore();
return;
}
}
}
if (e->key() == Qt::Key_Tab && lastFinishedTag) { // if we already end in comma, go to next/prev field
MainWindow::instance()->mainTab->nextInputField(e); // by sending the key event to the MainTab widget
} else if (e->key() == Qt::Key_Tab || e->key() == Qt::Key_Return) { // otherwise let's pretend this is a comma instead
QKeyEvent fakeEvent(e->type(), Qt::Key_Comma, e->modifiers(), QString(","));
keyPressEvent(&fakeEvent);
} else {
GroupedLineEdit::keyPressEvent(e);
reparse();
}
lastFinishedTag = finishedTag;
}
void TagWidget::wheelEvent(QWheelEvent *event)
{
if (hasFocus()) {
GroupedLineEdit::wheelEvent(event);
}
}
// Since we capture enter / return / tab, we never send an editingFinished() signal.
// Therefore, override the focusOutEvent()
void TagWidget::focusOutEvent(QFocusEvent *ev)
{
GroupedLineEdit::focusOutEvent(ev);
emit editingFinished();
}
// Implement simple drag and drop: text dropped onto the widget
// will be added as a new tag at the end. This overrides
// Qt's implementation which resulted in weird UI behavior,
// as the user may succeed in scrolling the view port.
static void handleDragEvent(QDragMoveEvent *e)
{
if (e->mimeData()->hasFormat(QStringLiteral("text/plain")))
e->acceptProposedAction();
}
void TagWidget::dragEnterEvent(QDragEnterEvent *e)
{
handleDragEvent(e);
}
void TagWidget::dragMoveEvent(QDragMoveEvent *e)
{
handleDragEvent(e);
}
void TagWidget::dragLeaveEvent(QDragLeaveEvent *)
{
}
void TagWidget::dropEvent(QDropEvent *e)
{
e->acceptProposedAction();
QString newTag = e->mimeData()->text().trimmed();
if (newTag.isEmpty())
return;
QString s = text().trimmed();
if (!s.isEmpty())
s += ", ";
s += newTag;
setText(s);
emit editingFinished();
}