diff options
| author | Akshay Nair <phenax5@gmail.com> | 2025-03-15 19:17:03 +0530 |
|---|---|---|
| committer | Akshay Nair <phenax5@gmail.com> | 2025-03-15 20:08:30 +0530 |
| commit | 387cf24e389854ecc97f6236fdd6acbc2486dd52 (patch) | |
| tree | a51b51de0064fb10685d2da85d11748c4a32ce73 /src | |
| parent | 05ae8976a8e1ab5411058de244c4e2fcc87ec369 (diff) | |
| download | null-browser-387cf24e389854ecc97f6236fdd6acbc2486dd52.tar.gz null-browser-387cf24e389854ecc97f6236fdd6acbc2486dd52.zip | |
Replaced QCompleter with custom implementation
Diffstat (limited to 'src')
| -rw-r--r-- | src/CommandParser.cpp | 2 | ||||
| -rw-r--r-- | src/InputMediator.cpp | 23 | ||||
| -rw-r--r-- | src/completion/CommandsAdapter.cpp | 6 | ||||
| -rw-r--r-- | src/completion/CommandsModel.cpp | 5 | ||||
| -rw-r--r-- | src/completion/Completer.cpp | 92 | ||||
| -rw-r--r-- | src/completion/CompleterDelegate.cpp | 8 | ||||
| -rw-r--r-- | src/completion/FuzzySearchProxyModel.cpp | 24 | ||||
| -rw-r--r-- | src/completion/TabsAdapter.cpp | 20 | ||||
| -rw-r--r-- | src/completion/TabsModel.cpp | 47 | ||||
| -rw-r--r-- | src/completion/UrlAdapter.cpp | 6 | ||||
| -rw-r--r-- | src/completion/UrlModel.cpp | 8 | ||||
| -rw-r--r-- | src/widgets/InputLine.cpp | 27 |
12 files changed, 222 insertions, 46 deletions
diff --git a/src/CommandParser.cpp b/src/CommandParser.cpp index 773d37c..2b9b422 100644 --- a/src/CommandParser.cpp +++ b/src/CommandParser.cpp @@ -30,6 +30,8 @@ CommandType CommandParser::toCommandType(QString cmd) { return TabNext; if (cmd == "tp" || cmd == "tabprev") return TabPrev; + if (cmd == "tabs") + return TabSelect; return Noop; } diff --git a/src/InputMediator.cpp b/src/InputMediator.cpp index 8bcadd0..ee7a552 100644 --- a/src/InputMediator.cpp +++ b/src/InputMediator.cpp @@ -5,6 +5,7 @@ #include "InputMediator.hpp" #include "LuaRuntime.hpp" #include "completion/CommandsAdapter.hpp" +#include "completion/TabsAdapter.hpp" #include "completion/UrlAdapter.hpp" #include "widgets/WebViewStack.hpp" @@ -32,6 +33,9 @@ void InputMediator::onInputSubmit(QString input) { if (auto urlEval = dynamic_cast<UrlEval *>(currentEvaluationType)) return webViewStack->openUrl(input, urlEval->type()); + + if (dynamic_cast<BufferEval *>(currentEvaluationType)) + return webViewStack->focusWebView(1); } void InputMediator::hideInputLine() { @@ -52,6 +56,13 @@ void InputMediator::showCommandInput(QString cmd) { showInputLine(); } +void InputMediator::showBufferInput(QString url) { + setEvaluationType(new CommandEval()); + inputLine->setAdapter(new TabsAdapter); + inputLine->setInputText(url); + showInputLine(); +} + void InputMediator::setEvaluationType(EvaluationType *evalType) { if (currentEvaluationType) delete currentEvaluationType; @@ -77,19 +88,23 @@ void InputMediator::evaluateCommand(QString command) { if (cmd.argsString.trimmed().isEmpty()) showURLInput("", OpenType::OpenUrl); else - webViewStack->openUrl(cmd.argsString, OpenType::OpenUrl); + openUrl(cmd.argsString, OpenType::OpenUrl); break; case CommandType::TabOpen: if (cmd.argsString.trimmed().isEmpty()) showURLInput("", OpenType::OpenUrlInTab); else - webViewStack->openUrl(cmd.argsString, OpenType::OpenUrlInTab); + openUrl(cmd.argsString, OpenType::OpenUrlInTab); break; case CommandType::TabNext: - webViewStack->next(); + nextWebView(); break; case CommandType::TabPrev: - webViewStack->previous(); + previousWebView(); + break; + case CommandType::TabSelect: + showBufferInput(); + // focusWebView(long index) // TODO: parse index and select break; case CommandType::Noop: break; diff --git a/src/completion/CommandsAdapter.cpp b/src/completion/CommandsAdapter.cpp index df71661..020c115 100644 --- a/src/completion/CommandsAdapter.cpp +++ b/src/completion/CommandsAdapter.cpp @@ -8,8 +8,8 @@ const QString cmdPrompt = "[exec]"; CommandsAdapter::CommandsAdapter() : Adapter() { - completerInstance = new Completer(this); - completerInstance->setModel(new CommandsModel(this)); + completerInstance = new Completer(); + completerInstance->setSourceModel(new CommandsModel(this)); } Completer *CommandsAdapter::completer() { return completerInstance; } @@ -17,6 +17,6 @@ Completer *CommandsAdapter::completer() { return completerInstance; } QString CommandsAdapter::prompt() { return cmdPrompt; } CommandsAdapter::~CommandsAdapter() { - delete completerInstance->model(); + delete completerInstance->sourceModel(); delete completerInstance; } diff --git a/src/completion/CommandsModel.cpp b/src/completion/CommandsModel.cpp index 0f66233..670c0e8 100644 --- a/src/completion/CommandsModel.cpp +++ b/src/completion/CommandsModel.cpp @@ -16,10 +16,10 @@ QVariant CommandsModel::data(const QModelIndex &index, int role) const { if (role == Qt::DisplayRole) return item.name; - else if (role == Qt::ToolTipRole) - return item.description; else if (role == Qt::UserRole) return item.description; + else if (role == Qt::AccessibleDescriptionRole) + return item.name + item.description; return QVariant(); } @@ -28,6 +28,7 @@ QHash<int, QByteArray> CommandsModel::roleNames() const { QHash<int, QByteArray> roles; roles[Qt::DisplayRole] = "name"; roles[Qt::UserRole] = "description"; + roles[Qt::AccessibleDescriptionRole] = "full"; return roles; } diff --git a/src/completion/Completer.cpp b/src/completion/Completer.cpp index a38d3f2..27a7b77 100644 --- a/src/completion/Completer.cpp +++ b/src/completion/Completer.cpp @@ -1,8 +1,13 @@ #include <QAbstractItemView> #include <QHeaderView> +#include <QKeyEvent> #include <QLineEdit> #include <QTreeView> #include <QWidget> +#include <QtCore/qabstractitemmodel.h> +#include <QtCore/qitemselectionmodel.h> +#include <QtCore/qnamespace.h> +#include <QtCore/qsortfilterproxymodel.h> #include <QtCore> #include "completion/Completer.hpp" @@ -12,33 +17,70 @@ const char *completerStyles = R"( background-color: #111; color: #fff; border-radius: 0; + width: 100%; )"; -Completer::Completer(QObject *parent) : QCompleter(parent) { - setFilterMode(Qt::MatchContains); - setCaseSensitivity(Qt::CaseInsensitive); - setCompletionRole(Qt::DisplayRole); - setCompletionMode(QCompleter::PopupCompletion); +Completer::Completer() : QWidget() { + view = new QTreeView(this); + viewDelegate = new CompleterDelegate(view); + view->setStyleSheet(completerStyles); + view->setItemDelegate(viewDelegate); + view->setRootIsDecorated(false); + view->setUniformRowHeights(true); + view->header()->hide(); + view->setColumnWidth(1, 500); + view->setModel(&proxyModel); + view->setFocusPolicy(Qt::NoFocus); + view->setEditTriggers(QAbstractItemView::NoEditTriggers); + view->setSelectionMode(QAbstractItemView::SingleSelection); +} + +void Completer::setSourceModel(QAbstractItemModel *model) { + proxyModel.setSourceModel(model); +} + +void Completer::onTextChange(QString text) { + proxyModel.setFilterWildcard(text); +} + +void Completer::setHighlightedRow(uint32_t row) { + viewDelegate->setCurrentRow(row); + view->update(); +} - auto treeView = new QTreeView(); - setPopup(treeView); - treeView->setStyleSheet(completerStyles); - treeView->setItemDelegate(new CompleterDelegate(treeView)); - treeView->setRootIsDecorated(false); - treeView->setUniformRowHeights(true); - treeView->header()->hide(); - treeView->setColumnWidth(1, 300); +void Completer::acceptHighlighted() { + auto index = proxyModel.index(viewDelegate->currentRow(), 0); + auto text = proxyModel.data(index, Qt::DisplayRole).toString(); + emit accepted(text); } -// bool Completer::eventFilter(QObject *watched, QEvent *event) { -// auto isFocusIn = event->type() == QEvent::FocusIn; -// auto isKeyRelease = event->type() == QEvent::KeyRelease; -// -// QLineEdit *lineEdit = qobject_cast<QLineEdit *>(watched); -// bool isInputEmpty = lineEdit && lineEdit->text().isEmpty(); -// -// if ((isFocusIn || isKeyRelease) && isInputEmpty) -// complete(); -// -// return QCompleter::eventFilter(watched, event); -// } +bool Completer::onKeyPressEvent(QKeyEvent *event) { + auto combo = event->keyCombination(); + auto row = viewDelegate->currentRow(); + + // If there are no matches, nothing to handle here + if (proxyModel.rowCount() == 0) + return false; + + if (combo.key() == Qt::Key_Up) { + setHighlightedRow(row <= 0 ? proxyModel.rowCount() - 1 : row - 1); + return true; + } + + if (combo.key() == Qt::Key_Down) { + setHighlightedRow((row + 1) % proxyModel.rowCount()); + return true; + } + + if (combo.key() == Qt::Key_Tab) { + acceptHighlighted(); + return true; + } + + if (combo.key() == Qt::Key_Return) { + acceptHighlighted(); + return false; + } + + return false; +} diff --git a/src/completion/CompleterDelegate.cpp b/src/completion/CompleterDelegate.cpp index a35305f..ce446b2 100644 --- a/src/completion/CompleterDelegate.cpp +++ b/src/completion/CompleterDelegate.cpp @@ -9,13 +9,19 @@ CompleterDelegate::CompleterDelegate(QObject *parent) : QStyledItemDelegate(parent) {} +void CompleterDelegate::setCurrentRow(uint32_t r) { row = r; } + +uint32_t CompleterDelegate::currentRow() { return row; } + void CompleterDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { painter->save(); + bool selected = index.row() == row; + // Style - if (option.state & QStyle::State_Selected) { + if (selected) { painter->fillRect(option.rect, QColor("#aaa")); painter->setPen(Qt::black); } else { diff --git a/src/completion/FuzzySearchProxyModel.cpp b/src/completion/FuzzySearchProxyModel.cpp new file mode 100644 index 0000000..151f60e --- /dev/null +++ b/src/completion/FuzzySearchProxyModel.cpp @@ -0,0 +1,24 @@ +#include <QWidget> +#include <QtCore> + +#include "completion/FuzzySearchProxyModel.hpp" + +bool FuzzySearchProxyModel::filterAcceptsRow( + int sourceRow, const QModelIndex &sourceParent) const { + QAbstractItemModel *model = sourceModel(); + QString filterText = filterRegularExpression().pattern(); + qDebug() << "Searching: " << filterText; + + for (int col = 0; col < model->columnCount(); ++col) { + QModelIndex index = model->index(sourceRow, col, sourceParent); + QString data = model->data(index, Qt::AccessibleDescriptionRole).toString(); + + qDebug() << " against: " << data; + + // TODO: Fuzzy eet up + if (data.contains(filterText, Qt::CaseInsensitive)) { + return true; + } + } + return false; +} diff --git a/src/completion/TabsAdapter.cpp b/src/completion/TabsAdapter.cpp new file mode 100644 index 0000000..4854b7d --- /dev/null +++ b/src/completion/TabsAdapter.cpp @@ -0,0 +1,20 @@ +#include <QWidget> +#include <QtCore> + +#include "completion/Completer.hpp" +#include "completion/TabsAdapter.hpp" +#include "completion/TabsModel.hpp" + +TabsAdapter::TabsAdapter() : Adapter() { + completerInstance = new Completer(); + completerInstance->setSourceModel(new TabsModel(this)); +} + +Completer *TabsAdapter::completer() { return completerInstance; } + +QString TabsAdapter::prompt() { return "[tabs]"; } + +TabsAdapter::~TabsAdapter() { + delete completerInstance->sourceModel(); + delete completerInstance; +} diff --git a/src/completion/TabsModel.cpp b/src/completion/TabsModel.cpp new file mode 100644 index 0000000..37eb1dd --- /dev/null +++ b/src/completion/TabsModel.cpp @@ -0,0 +1,47 @@ +#include <QAbstractListModel> +#include <QWidget> +#include <QtCore> + +#include "completion/TabsModel.hpp" + +TabsModel::TabsModel(QObject *parent) : QAbstractListModel(parent) { + items = { + {.url = "https://ediblemonad.dev", .title = "EdibleMonad website"}, + {.url = "https://lite.duckduckgo.com", .title = "DDG lite"}, + {.url = "https://github.com/trending", .title = "Github trending"}, + }; +} + +QVariant TabsModel::data(const QModelIndex &index, int role) const { + if (!index.isValid() || index.row() >= items.size()) + return QVariant(); + + const Buffer &item = items.at(index.row()); + + if (role == Qt::DisplayRole) + return index.row(); + else if (role == Qt::UserRole) + return item.title; + else if (role == Qt::AccessibleDescriptionRole) + return item.url + item.title; + + return QVariant(); +} + +QHash<int, QByteArray> TabsModel::roleNames() const { + QHash<int, QByteArray> roles; + roles[Qt::DisplayRole] = "url"; + roles[Qt::UserRole] = "title"; + roles[Qt::AccessibleDescriptionRole] = "full"; + return roles; +} + +int TabsModel::rowCount(const QModelIndex &parent) const { + Q_UNUSED(parent); + return items.size(); +} + +int TabsModel::columnCount(const QModelIndex &parent) const { + Q_UNUSED(parent); + return 2; // url + title +} diff --git a/src/completion/UrlAdapter.cpp b/src/completion/UrlAdapter.cpp index 2f1bb08..54caa0d 100644 --- a/src/completion/UrlAdapter.cpp +++ b/src/completion/UrlAdapter.cpp @@ -6,8 +6,8 @@ #include "completion/UrlModel.hpp" UrlAdapter::UrlAdapter() : Adapter() { - completerInstance = new Completer(this); - completerInstance->setModel(new UrlModel(this)); + completerInstance = new Completer(); + completerInstance->setSourceModel(new UrlModel(this)); } Completer *UrlAdapter::completer() { return completerInstance; } @@ -15,6 +15,6 @@ Completer *UrlAdapter::completer() { return completerInstance; } QString UrlAdapter::prompt() { return "[url]"; } UrlAdapter::~UrlAdapter() { - delete completerInstance->model(); + delete completerInstance->sourceModel(); delete completerInstance; } diff --git a/src/completion/UrlModel.cpp b/src/completion/UrlModel.cpp index 7671fc3..8c5d75c 100644 --- a/src/completion/UrlModel.cpp +++ b/src/completion/UrlModel.cpp @@ -1,5 +1,6 @@ #include <QAbstractListModel> #include <QWidget> +#include <QtCore/qnamespace.h> #include <QtCore> #include "completion/UrlModel.hpp" @@ -22,10 +23,10 @@ QVariant UrlModel::data(const QModelIndex &index, int role) const { if (role == Qt::DisplayRole) return item.url; - else if (role == Qt::ToolTipRole) - return item.description; else if (role == Qt::UserRole) return item.description; + else if (role == Qt::AccessibleDescriptionRole) + return item.url + " " + item.description; return QVariant(); } @@ -34,6 +35,7 @@ QHash<int, QByteArray> UrlModel::roleNames() const { QHash<int, QByteArray> roles; roles[Qt::DisplayRole] = "url"; roles[Qt::UserRole] = "description"; + roles[Qt::AccessibleDescriptionRole] = "full"; return roles; } @@ -44,5 +46,5 @@ int UrlModel::rowCount(const QModelIndex &parent) const { int UrlModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent); - return 2; // name + description + return 2; // url + description } diff --git a/src/widgets/InputLine.cpp b/src/widgets/InputLine.cpp index 130732c..7e0a2dd 100644 --- a/src/widgets/InputLine.cpp +++ b/src/widgets/InputLine.cpp @@ -9,6 +9,8 @@ #include <QWidgetAction> #include <QWindow> #include <QtCore> +#include <QtWidgets/qboxlayout.h> +#include <QtWidgets/qlineedit.h> #include "completion/CommandsAdapter.hpp" #include "widgets/InputLine.hpp" @@ -29,17 +31,19 @@ InputLine::InputLine(QString defaultInput, QWidget *parentNode) : QWidget(parentNode) { setContentsMargins(0, 0, 0, 0); move(0, 0); - setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); setStyleSheet(rootStyles); auto layout = new QVBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); + setLayout(layout); auto cmdLineBox = new QWidget(); layout->addWidget(cmdLineBox); auto cmdLineLayout = new QHBoxLayout(); cmdLineBox->setLayout(cmdLineLayout); cmdLineBox->layout()->setContentsMargins(0, 0, 0, 0); + cmdLineBox->setContentsMargins(0, 0, 0, 0); cmdLineLayout->setSpacing(0); promptPrefix = new QLabel(); @@ -47,31 +51,44 @@ InputLine::InputLine(QString defaultInput, QWidget *parentNode) promptPrefix->setContentsMargins(0, 0, 0, 0); input = new QLineEdit(defaultInput); input->setFocusPolicy(Qt::StrongFocus); + input->setContentsMargins(0, 0, 0, 0); cmdLineLayout->addWidget(promptPrefix); cmdLineLayout->addWidget(input); - setFixedHeight(input->sizeHint().height()); + cmdLineBox->setFixedHeight(input->sizeHint().height()); setAdapter(new CommandsAdapter()); } void InputLine::setAdapter(Adapter *adapter) { - if (this->adapterInstance) + if (this->adapterInstance) { + layout()->removeWidget(this->adapterInstance->completer()); delete this->adapterInstance; + } this->adapterInstance = adapter; promptPrefix->setText(adapter->prompt()); - input->setCompleter(adapter->completer()); + + auto completer = adapter->completer(); + layout()->addWidget(completer); + completer->move(0, 0); + connect(input, &QLineEdit::textChanged, completer, &Completer::onTextChange); + connect(completer, &Completer::accepted, input, &QLineEdit::setText); } void InputLine::emitSubmit() { emit submitted(getInputText()); } void InputLine::keyPressEvent(QKeyEvent *event) { auto combo = event->keyCombination(); + + // Forward events to completer first + bool shouldStop = adapterInstance->completer()->onKeyPressEvent(event); + if (shouldStop) + return; + auto ctrlL = combo.key() == Qt::Key_L && combo.keyboardModifiers().testFlag(Qt::ControlModifier); auto esc = combo.key() == Qt::Key_Escape; auto enter = combo.key() == Qt::Key_Return || combo.key() == Qt::Key_Enter; - if (esc || ctrlL) emit cancelled(); else if (enter) |
