/***************************************************************************
syntaxpalette.cpp
-------------------
begin : 2026
copyright : (C) 2009-2026 by Andre Simon
email : a.simon@mailbox.org
***************************************************************************/
/*
This file is part of Highlight.
Highlight is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Highlight is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Highlight. If not, see <http://www.gnu.org/licenses/>.
*/
#include "syntaxpalette.h"
#include <QCoreApplication>
#include <QKeyEvent>
#include <QLineEdit>
#include <QListView>
#include <QScreen>
#include <QVBoxLayout>
SyntaxFilterProxyModel::SyntaxFilterProxyModel(QObject *parent)
: QSortFilterProxyModel(parent)
{
}
void SyntaxFilterProxyModel::setSearchText(const QString &text)
{
searchText = text.trimmed();
invalidate();
}
bool SyntaxFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
if (searchText.isEmpty())
return true;
QModelIndex idx = sourceModel()->index(sourceRow, 0, sourceParent);
return idx.data(Qt::DisplayRole).toString().contains(searchText, Qt::CaseInsensitive)
|| idx.data(Qt::UserRole).toString().contains(searchText, Qt::CaseInsensitive);
}
int SyntaxFilterProxyModel::rank(const QModelIndex &index) const
{
if (searchText.isEmpty())
return 0;
if (index.data(Qt::DisplayRole).toString().startsWith(searchText, Qt::CaseInsensitive)
|| index.data(Qt::UserRole).toString().compare(searchText, Qt::CaseInsensitive) == 0)
return 0;
return 1;
}
bool SyntaxFilterProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
{
int leftRank = rank(left);
int rightRank = rank(right);
if (leftRank != rightRank)
return leftRank < rightRank;
// keep the order of the source model within a rank
return left.row() < right.row();
}
SyntaxPalette::SyntaxPalette(QAbstractItemModel *model, QWidget *parent)
: QFrame(parent, Qt::Popup)
{
setFrameStyle(QFrame::StyledPanel | QFrame::Raised);
leSearch = new QLineEdit(this);
leSearch->setPlaceholderText(tr("Search syntax or file extension"));
leSearch->setClearButtonEnabled(true);
leSearch->installEventFilter(this);
proxy = new SyntaxFilterProxyModel(this);
proxy->setSourceModel(model);
proxy->sort(0);
lvSyntaxes = new QListView(this);
lvSyntaxes->setModel(proxy);
lvSyntaxes->setEditTriggers(QAbstractItemView::NoEditTriggers);
lvSyntaxes->setUniformItemSizes(true);
lvSyntaxes->setFocusPolicy(Qt::NoFocus);
QVBoxLayout *layout = new QVBoxLayout(this);
layout->setContentsMargins(4, 4, 4, 4);
layout->setSpacing(4);
layout->addWidget(leSearch);
layout->addWidget(lvSyntaxes);
connect(leSearch, &QLineEdit::textChanged, this, &SyntaxPalette::onSearchTextChanged);
connect(leSearch, &QLineEdit::returnPressed, this, &SyntaxPalette::acceptCurrent);
connect(lvSyntaxes, &QListView::clicked, this, &SyntaxPalette::acceptCurrent);
}
void SyntaxPalette::popup(QWidget *anchor, int currentRow)
{
leSearch->clear();
QModelIndex current = proxy->mapFromSource(proxy->sourceModel()->index(currentRow, 0));
lvSyntaxes->setCurrentIndex(current);
lvSyntaxes->scrollTo(current, QAbstractItemView::PositionAtCenter);
resize(qMax(anchor->width(), 280), 320);
// open below the anchor, or above it if there is not enough space on the screen
QPoint pos = anchor->mapToGlobal(QPoint(0, anchor->height()));
QRect screenRect = anchor->screen()->availableGeometry();
if (pos.y() + height() > screenRect.bottom())
pos.setY(anchor->mapToGlobal(QPoint(0, 0)).y() - height());
pos.setX(qBound(screenRect.left(), pos.x(), screenRect.right() - width()));
move(pos);
show();
leSearch->setFocus();
}
bool SyntaxPalette::eventFilter(QObject *watched, QEvent *event)
{
// let the list be navigated while the search field keeps the focus
if (watched == leSearch && event->type() == QEvent::KeyPress) {
switch (static_cast<QKeyEvent *>(event)->key()) {
case Qt::Key_Up:
case Qt::Key_Down:
case Qt::Key_PageUp:
case Qt::Key_PageDown:
QCoreApplication::sendEvent(lvSyntaxes, event);
return true;
default:
break;
}
}
return QFrame::eventFilter(watched, event);
}
void SyntaxPalette::onSearchTextChanged(const QString &text)
{
proxy->setSearchText(text);
lvSyntaxes->setCurrentIndex(proxy->index(0, 0));
}
void SyntaxPalette::acceptCurrent()
{
QModelIndex current = lvSyntaxes->currentIndex();
if (!current.isValid())
return;
close();
emit syntaxSelected(proxy->mapToSource(current).row());
}