245 lines
7.1 KiB
C++
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
|