kate-deft/src/palette/palettewidget.cpp

245 lines
7.1 KiB
C++

/*
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "palettewidget.h"
#include <QAbstractTextDocumentLayout>
#include <QApplication>
#include <QKeyEvent>
#include <QLineEdit>
#include <QListView>
#include <QStyledItemDelegate>
#include <QTextCharFormat>
#include <QTextDocument>
#include <QTimer>
#include <QVBoxLayout>
#include <QPainter>
#include <QPoint>
namespace deft
{
namespace
{
// Renders a palette row, emboldening the matched character ranges supplied via
// PaletteModel::HighlightRangesRole.
class HighlightDelegate : public QStyledItemDelegate
{
public:
using QStyledItemDelegate::QStyledItemDelegate;
void paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const override
{
QStyleOptionViewItem opt = option;
initStyleOption(&opt, index);
// Draw selection background / focus via the style, but suppress the
// style's own text so we can render rich text ourselves.
const QString text = opt.text;
opt.text.clear();
QStyle *style = opt.widget ? opt.widget->style() : QApplication::style();
style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, opt.widget);
const QColor baseColor = (opt.state & QStyle::State_Selected)
? opt.palette.color(QPalette::HighlightedText)
: opt.palette.color(QPalette::Text);
QTextDocument doc;
QTextCharFormat plain;
plain.setForeground(baseColor);
QTextCharFormat strong = plain;
strong.setFontWeight(QFont::Bold);
const QVariantList ranges =
index.data(PaletteModel::HighlightRangesRole).toList();
// Build a per-character bold mask.
QList<bool> boldMask(text.size(), false);
for (const QVariant &v : ranges) {
const QPoint r = v.toPoint(); // x=start, y=length
for (int i = r.x(); i < r.x() + r.y() && i < text.size(); ++i) {
if (i >= 0) {
boldMask[i] = true;
}
}
}
QTextCursor cur(&doc);
int i = 0;
while (i < text.size()) {
const bool bold = boldMask[i];
int j = i;
while (j < text.size() && boldMask[j] == bold) {
++j;
}
cur.insertText(text.mid(i, j - i), bold ? strong : plain);
i = j;
}
painter->save();
const QRect textRect = style->subElementRect(QStyle::SE_ItemViewItemText, &opt, opt.widget);
painter->translate(textRect.topLeft());
QRect clip(0, 0, textRect.width(), textRect.height());
// Vertically center the single text line.
const int dy = (textRect.height() - doc.size().toSize().height()) / 2;
painter->translate(0, dy > 0 ? dy : 0);
QAbstractTextDocumentLayout::PaintContext ctx;
ctx.clip = clip;
doc.documentLayout()->draw(painter, ctx);
painter->restore();
}
};
} // namespace
PaletteWidget::PaletteWidget(QWidget *parent)
: QFrame(parent, Qt::Popup)
, m_model(new PaletteModel(this))
, m_filter(new QLineEdit(this))
, m_list(new QListView(this))
{
setFrameShape(QFrame::StyledPanel);
setFrameShadow(QFrame::Raised);
m_filter->setPlaceholderText(tr("Type to filter commands…"));
m_filter->setClearButtonEnabled(true);
m_list->setModel(m_model);
m_list->setItemDelegate(new HighlightDelegate(m_list));
m_list->setSelectionMode(QAbstractItemView::SingleSelection);
m_list->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_list->setUniformItemSizes(true);
m_list->setFocusPolicy(Qt::NoFocus); // keep focus/typing in the filter line
auto *layout = new QVBoxLayout(this);
layout->setContentsMargins(6, 6, 6, 6);
layout->setSpacing(4);
layout->addWidget(m_filter);
layout->addWidget(m_list);
// Live updates, but debounced: a fast typist generates several keystrokes
// in a few ms, and on a large project each re-rank scores thousands of
// items. Coalesce bursts into a single re-rank so early (broad) queries do
// not stall the UI. 80 ms is below the threshold of feeling laggy while
// still absorbing a typing burst.
m_filterDebounce = new QTimer(this);
m_filterDebounce->setSingleShot(true);
m_filterDebounce->setInterval(80);
connect(m_filterDebounce, &QTimer::timeout, this, [this]() { applyQueryNow(); });
connect(m_filter, &QLineEdit::textChanged, this, [this](const QString &text) {
m_pendingQuery = text;
m_filterDebounce->start(); // (re)arm; applied on timeout
});
// Mouse: a click activates immediately.
connect(m_list, &QListView::clicked, this, [this](const QModelIndex &) {
acceptCurrent();
});
m_filter->installEventFilter(this);
resize(480, 320);
}
void PaletteWidget::setItems(const QList<PaletteItem> &items)
{
m_model->setItems(items);
if (m_model->rowCount() > 0) {
m_list->setCurrentIndex(m_model->index(0, 0));
}
}
void PaletteWidget::applyQueryNow()
{
m_filterDebounce->stop();
m_model->setQuery(m_pendingQuery);
if (m_model->rowCount() > 0) {
m_list->setCurrentIndex(m_model->index(0, 0));
}
}
void PaletteWidget::activate()
{
m_filterDebounce->stop();
m_filter->clear();
m_pendingQuery.clear();
m_model->setQuery(QString());
if (m_model->rowCount() > 0) {
m_list->setCurrentIndex(m_model->index(0, 0));
}
show();
m_filter->setFocus();
}
void PaletteWidget::moveSelection(int delta)
{
const int count = m_model->rowCount();
if (count == 0) {
return;
}
int row = m_list->currentIndex().row();
if (row < 0) {
row = 0;
}
row = qBound(0, row + delta, count - 1);
m_list->setCurrentIndex(m_model->index(row, 0));
}
void PaletteWidget::acceptCurrent()
{
// Flush any pending debounced query so Enter acts on the latest text even
// if the user typed and immediately hit Enter within the debounce window.
if (m_filterDebounce->isActive()) {
applyQueryNow();
}
const int row = m_list->currentIndex().row();
const PaletteItem *item = m_model->itemAt(row);
if (!item) {
return;
}
const QString id = item->id;
hide();
Q_EMIT activatedId(id);
}
void PaletteWidget::dismiss()
{
hide();
Q_EMIT cancelled();
}
bool PaletteWidget::eventFilter(QObject *obj, QEvent *event)
{
if (obj == m_filter && event->type() == QEvent::KeyPress) {
auto *ke = static_cast<QKeyEvent *>(event);
switch (ke->key()) {
case Qt::Key_Down:
moveSelection(1);
return true;
case Qt::Key_Up:
moveSelection(-1);
return true;
case Qt::Key_PageDown:
moveSelection(8);
return true;
case Qt::Key_PageUp:
moveSelection(-8);
return true;
case Qt::Key_Return:
case Qt::Key_Enter:
acceptCurrent();
return true;
case Qt::Key_Escape:
dismiss();
return true;
default:
break;
}
}
return QFrame::eventFilter(obj, event);
}
} // namespace deft