261030.3 · f854c9dda9a9f955202c1c0ad436b471dd7580d1
gui-qt: replaces syntax dropdown with a searchable pop-up list
reading declarations
@@ -153,6 +153,7 @@ if(BUILD_GUI) ${GUI_QT_DIR}/io_report.cpp ${GUI_QT_DIR}/showtextfile.cpp ${GUI_QT_DIR}/syntax_chooser.cpp+ ${GUI_QT_DIR}/syntaxpalette.cpp ${GUI_QT_DIR}/thememodel.cpp ${GUI_QT_DIR}/themeswatchdelegate.cpp ${GUI_QT_DIR}/highlight-gui.qrc
@@ -14,6 +14,7 @@ SOURCES += main.cpp \ io_report.cpp \ showtextfile.cpp \ syntax_chooser.cpp \+ syntaxpalette.cpp \ thememodel.cpp \ themeswatchdelegate.cpp @@ -22,6 +23,7 @@ HEADERS += mainwindow.h \ io_report.h \ showtextfile.h \ syntax_chooser.h \+ syntaxpalette.h \ thememodel.h \ themeswatchdelegate.h
@@ -243,6 +243,16 @@ MainWindow::MainWindow(QWidget *parent) QObject::connect(ui->comboReformat, SIGNAL(currentIndexChanged(int)), this, SLOT(updatePreview())); QObject::connect(ui->comboSelectSyntax, SIGNAL(currentIndexChanged(int)), this, SLOT(updatePreview())); + // the hidden syntax combo keeps the selection, the palette only offers a searchable view of its entries+ ui->comboSelectSyntax->hide();+ syntaxPalette = new SyntaxPalette(ui->comboSelectSyntax->model(), this);+ QObject::connect(ui->tbSelectSyntax, &QToolButton::clicked, this, [this]() {+ syntaxPalette->popup(ui->tbSelectSyntax, ui->comboSelectSyntax->currentIndex());+ });+ QObject::connect(syntaxPalette, &SyntaxPalette::syntaxSelected, ui->comboSelectSyntax, &QComboBox::setCurrentIndex);+ QObject::connect(ui->comboSelectSyntax, QOverload<int>::of(&QComboBox::currentIndexChanged),+ this, &MainWindow::updateSyntaxChip);+ QObject::connect(ui->comboLSProfiles, SIGNAL(currentIndexChanged(int)), this, SLOT(loadLSProfile())); QObject::connect(ui->sbLineNoWidth, SIGNAL(valueChanged(int)), this, SLOT(updatePreview()));@@ -275,6 +285,7 @@ MainWindow::MainWindow(QWidget *parent) QObject::connect(ui->lvInputFiles->model(), SIGNAL(modelReset()), this, SLOT(plausibility())); plausibility();+ updateSyntaxChip(); // the options panel must not shrink below the width of its widest element optionsScrollArea->setMinimumWidth(optionsPane->minimumSizeHint().width()@@ -287,6 +298,11 @@ MainWindow::MainWindow(QWidget *parent) statusBar()->showMessage(tr("Always at your service"), 2500); } +void MainWindow::updateSyntaxChip()+{+ ui->tbSelectSyntax->setText(ui->comboSelectSyntax->currentText());+}+ bool MainWindow::shortNamesDisabled() { #ifdef Q_OS_WIN QSettings reg("HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\FileSystem",@@ -1611,6 +1627,7 @@ void MainWindow::plausibility() ui->lvThemes->setEnabled(getUserScriptPath("theme").isEmpty()); ui->leThemeSearch->setEnabled(getUserScriptPath("theme").isEmpty()); ui->comboSelectSyntax->setEnabled(getUserScriptPath("lang").isEmpty());+ ui->tbSelectSyntax->setEnabled(getUserScriptPath("lang").isEmpty()); ui->cbHTMLInlineCSS->setEnabled(ui->cbHTMLEmbedStyle->isChecked()); ui->cbHTMLFileNameAnchor->setEnabled(ui->cbHTMLAnchors->isChecked());
@@ -65,6 +65,7 @@ along with Highlight. If not, see <http://www.gnu.org/licenses/>. #include "lspprofile.h" #include "lspclient.h" #include "thememodel.h"+#include "syntaxpalette.h" #include <map> #include <vector>@@ -110,6 +111,7 @@ private: ThemeListModel *themeModel; ThemeFilterProxyModel *themeProxy; QString selectedThemeId;+ SyntaxPalette *syntaxPalette; bool getDataFromCP{false}, runFirstTime{true}, ignoreThemeChange{false}; @@ -128,6 +130,8 @@ private: QString getWindowsShortPath(const QString & path); + void updateSyntaxChip();+ void applyCtrlValues(highlight::CodeGenerator* generator, bool previewMode); void applyEncoding(highlight::CodeGenerator* generator, QString &langDefPath);
@@ -371,6 +371,25 @@ </property> </widget> </item>+ <item>+ <widget class="QToolButton" name="tbSelectSyntax">+ <property name="sizePolicy">+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">+ <horstretch>0</horstretch>+ <verstretch>0</verstretch>+ </sizepolicy>+ </property>+ <property name="toolTip">+ <string>Select the correct syntax of the code snippet.</string>+ </property>+ <property name="toolButtonStyle">+ <enum>Qt::ToolButtonTextBesideIcon</enum>+ </property>+ <property name="arrowType">+ <enum>Qt::DownArrow</enum>+ </property>+ </widget>+ </item> </layout> </item> <item>
@@ -0,0 +1,163 @@+/***************************************************************************+ 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());+}
@@ -0,0 +1,94 @@+/***************************************************************************+ syntaxpalette.h+ -------------------+ 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/>.+*/++#ifndef SYNTAXPALETTE_H+#define SYNTAXPALETTE_H++#include <QFrame>+#include <QSortFilterProxyModel>+#include <QString>++class QAbstractItemModel;+class QLineEdit;+class QListView;++/** Filters syntax names by a search text which must be contained in the+ name or in the file extension (Qt::UserRole); names starting with the+ search text are listed first */+class SyntaxFilterProxyModel : public QSortFilterProxyModel+{+ Q_OBJECT++public:+ explicit SyntaxFilterProxyModel(QObject *parent = nullptr);++public slots:+ void setSearchText(const QString &text);++protected:+ bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;+ bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;++private:+ QString searchText;++ int rank(const QModelIndex &index) const;+};++/** Popup with a search field and a filtered list of syntax names,+ similar to the locator of an IDE */+class SyntaxPalette : public QFrame+{+ Q_OBJECT++public:+ /** \param model list of syntax names (display role) and their file extensions (Qt::UserRole)+ \param parent parent widget */+ explicit SyntaxPalette(QAbstractItemModel *model, QWidget *parent = nullptr);++ /** Show the palette below the anchor widget+ \param anchor widget which opened the palette+ \param currentRow source model row of the current syntax */+ void popup(QWidget *anchor, int currentRow);++signals:+ /** Emitted when the user picked a syntax+ \param row source model row of the selected syntax */+ void syntaxSelected(int row);++protected:+ bool eventFilter(QObject *watched, QEvent *event) override;++private slots:+ void onSearchTextChanged(const QString &text);+ void acceptCurrent();++private:+ QLineEdit *leSearch;+ QListView *lvSyntaxes;+ SyntaxFilterProxyModel *proxy;+};++#endif // SYNTAXPALETTE_H