[utilities/konsole] src: Add search tab button to the tab bar
Kurt Hindenburg
null at kde.org
Wed Dec 4 14:18:07 GMT 2024
Git commit 74c648c0fc842af4746892205330a77377c90cbd by Kurt Hindenburg, on behalf of Troy Hoover.
Committed on 04/12/2024 at 14:18.
Pushed by hindenburg into branch 'master'.
Add search tab button to the tab bar
The button takes user input and sorts through a list of tab names for the tab that matches
SearchTabs pulls from Kate's similar tab sort
function called QuickOpen
FEATURE: 298775
GUI:
M +3 -0 src/CMakeLists.txt
A +318 -0 src/searchtabs/SearchTabs.cpp [License: GPL(v2.0+)]
A +68 -0 src/searchtabs/SearchTabs.h [License: GPL(v2.0+)]
A +69 -0 src/searchtabs/SearchTabsModel.cpp [License: GPL(v2.0+)]
A +68 -0 src/searchtabs/SearchTabsModel.h [License: GPL(v2.0+)]
A +431 -0 src/searchtabs/kfts_fuzzy_match.h [License: LGPL(v2.0+)]
M +115 -8 src/settings/TabBarSettings.ui
M +8 -0 src/settings/konsole.kcfg
M +34 -1 src/widgets/ViewContainer.cpp
M +2 -0 src/widgets/ViewContainer.h
https://invent.kde.org/utilities/konsole/-/commit/74c648c0fc842af4746892205330a77377c90cbd
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 00ac5b30bd..7cc2e7a434 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -200,6 +200,9 @@ set(konsoleprivate_SRCS ${windowadaptors_SRCS}
widgets/RenameTabWidget.cpp
widgets/TabTitleFormatButton.cpp
+ searchtabs/SearchTabs.cpp
+ searchtabs/SearchTabsModel.cpp
+
terminalDisplay/extras/CompositeWidgetFocusWatcher.cpp
terminalDisplay/extras/AutoScrollHandler.cpp
terminalDisplay/extras/HighlightScrolledLines.cpp
diff --git a/src/searchtabs/SearchTabs.cpp b/src/searchtabs/SearchTabs.cpp
new file mode 100644
index 0000000000..a2c91add94
--- /dev/null
+++ b/src/searchtabs/SearchTabs.cpp
@@ -0,0 +1,318 @@
+/*
+ SPDX-FileCopyrightText: 2024 Troy Hoover <troytjh98 at gmail.com>
+
+ SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+// Own
+#include "SearchTabs.h"
+#include "kfts_fuzzy_match.h"
+
+// Qt
+#include <QApplication>
+#include <QKeyEvent>
+#include <QModelIndex>
+#include <QSortFilterProxyModel>
+#include <QVBoxLayout>
+
+// KDE
+#include <KLocalizedString>
+
+// Konsole
+#include "KonsoleSettings.h"
+
+using namespace Konsole;
+
+/* ------------------------------------------------------------------------- */
+/* */
+/* Fuzzy Search Model */
+/* */
+/* ------------------------------------------------------------------------- */
+
+class SearchTabsFilterProxyModel final : public QSortFilterProxyModel
+{
+public:
+ SearchTabsFilterProxyModel(QObject *parent = nullptr)
+ : QSortFilterProxyModel(parent)
+ {
+ }
+
+protected:
+ bool lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const override
+ {
+ auto sm = sourceModel();
+
+ const int l = static_cast<SearchTabsModel *>(sm)->idxScore(sourceLeft);
+ const int r = static_cast<SearchTabsModel *>(sm)->idxScore(sourceRight);
+ return l < r;
+ }
+
+ bool filterAcceptsRow(int sourceRow, const QModelIndex &parent) const override
+ {
+ Q_UNUSED(parent)
+
+ if (pattern.isEmpty()) {
+ return true;
+ }
+
+ auto sm = static_cast<SearchTabsModel *>(sourceModel());
+ if (!sm->isValid(sourceRow)) {
+ return false;
+ }
+
+ QStringView tabNameMatchPattern = pattern;
+
+ const QString &name = sm->idxToName(sourceRow);
+
+ int score = 0;
+ bool result;
+ // dont use the QStringView(QString) ctor
+ if (tabNameMatchPattern.isEmpty()) {
+ result = true;
+ } else {
+ result = filterByName(QStringView(name.data(), name.size()), tabNameMatchPattern, score);
+ }
+ // if (result && pattern == QStringLiteral(""))
+ // qDebug() << score << ", " << name << "==================== END\n";
+
+ sm->setScoreForIndex(sourceRow, score);
+
+ return result;
+ }
+
+public Q_SLOTS:
+ bool setFilterText(const QString &text)
+ {
+ beginResetModel();
+ pattern = text;
+ endResetModel();
+
+ return true;
+ }
+
+private:
+ static inline bool filterByName(QStringView name, QStringView pattern, int &score)
+ {
+ return kfts::fuzzy_match(pattern, name, score);
+ }
+
+private:
+ QString pattern;
+};
+
+/* ------------------------------------------------------------------------- */
+/* */
+/* Search Tabs */
+/* */
+/* ------------------------------------------------------------------------- */
+
+SearchTabs::SearchTabs(ViewManager *viewManager)
+ : QFrame(viewManager->activeContainer()->window())
+ , m_viewManager(viewManager)
+{
+ setFrameStyle(QFrame::StyledPanel | QFrame::Sunken);
+ setProperty("_breeze_force_frame", true);
+
+ // handle resizing of MainWindow
+ window()->installEventFilter(this);
+
+ // ensure the components have some proper frame
+ QVBoxLayout *layout = new QVBoxLayout();
+ layout->setSpacing(0);
+ layout->setContentsMargins(QMargins());
+ setLayout(layout);
+
+ // create input line for search query
+ m_inputLine = new QLineEdit(this);
+ m_inputLine->setClearButtonEnabled(true);
+ m_inputLine->addAction(QIcon::fromTheme(QStringLiteral("search")), QLineEdit::LeadingPosition);
+ m_inputLine->setTextMargins(QMargins() + style()->pixelMetric(QStyle::PM_ButtonMargin));
+ m_inputLine->setPlaceholderText(i18nc("@label:textbox", "Search..."));
+ m_inputLine->setToolTip(i18nc("@info:tooltip", "Enter a tab name to search for here"));
+ m_inputLine->setCursor(Qt::IBeamCursor);
+ m_inputLine->setFont(QApplication::font());
+ m_inputLine->setFrame(false);
+ // When the widget focus is set, focus input box instead
+ setFocusProxy(m_inputLine);
+
+ layout->addWidget(m_inputLine);
+
+ m_listView = new QTreeView(this);
+ layout->addWidget(m_listView, 1);
+ m_listView->setProperty("_breeze_borders_sides", QVariant::fromValue(QFlags(Qt::TopEdge)));
+ m_listView->setTextElideMode(Qt::ElideLeft);
+ m_listView->setUniformRowHeights(true);
+
+ // model stores tab information
+ m_model = new SearchTabsModel(this);
+
+ // switch to selected tab
+ connect(m_inputLine, &QLineEdit::returnPressed, this, &SearchTabs::slotReturnPressed);
+ connect(m_listView, &QTreeView::activated, this, &SearchTabs::slotReturnPressed);
+ connect(m_listView, &QTreeView::clicked, this, &SearchTabs::slotReturnPressed); // for single click
+
+ m_inputLine->installEventFilter(this);
+ m_listView->installEventFilter(this);
+ m_listView->setHeaderHidden(true);
+ m_listView->setRootIsDecorated(false);
+ m_listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ m_listView->setModel(m_model);
+
+ // use fuzzy sort to identify tabs with matching titles
+ connect(m_inputLine, &QLineEdit::textChanged, this, [this](const QString &text) {
+ // initialize the proxy model when there is something to filter
+ bool didFilter = false;
+ if (!m_proxyModel) {
+ m_proxyModel = new SearchTabsFilterProxyModel(this);
+ m_proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
+ didFilter = m_proxyModel->setFilterText(text);
+ m_proxyModel->setSourceModel(m_model);
+ m_listView->setModel(m_proxyModel);
+ } else {
+ didFilter = m_proxyModel->setFilterText(text);
+ }
+ if (didFilter) {
+ m_listView->viewport()->update();
+ reselectFirst();
+ }
+ });
+
+ setHidden(true);
+
+ // fill stuff
+ updateState();
+}
+
+bool SearchTabs::eventFilter(QObject *obj, QEvent *event)
+{
+ // catch key presses + shortcut overrides to allow to have
+ // ESC as application wide shortcut, too, see bug 409856
+ if (event->type() == QEvent::KeyPress || event->type() == QEvent::ShortcutOverride) {
+ QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
+ if (obj == m_inputLine) {
+ const bool forward2list = (keyEvent->key() == Qt::Key_Up) || (keyEvent->key() == Qt::Key_Down) || (keyEvent->key() == Qt::Key_PageUp)
+ || (keyEvent->key() == Qt::Key_PageDown);
+
+ if (forward2list) {
+ QCoreApplication::sendEvent(m_listView, event);
+ return true;
+ }
+
+ } else if (obj == m_listView) {
+ const bool forward2input = (keyEvent->key() != Qt::Key_Up) && (keyEvent->key() != Qt::Key_Down) && (keyEvent->key() != Qt::Key_PageUp)
+ && (keyEvent->key() != Qt::Key_PageDown) && (keyEvent->key() != Qt::Key_Tab) && (keyEvent->key() != Qt::Key_Backtab);
+
+ if (forward2input) {
+ QCoreApplication::sendEvent(m_inputLine, event);
+ return true;
+ }
+ }
+
+ if (keyEvent->key() == Qt::Key_Escape) {
+ hide();
+ deleteLater();
+ return true;
+ }
+ }
+
+ if (event->type() == QEvent::FocusOut && !(m_inputLine->hasFocus() || m_listView->hasFocus())) {
+ hide();
+ deleteLater();
+ return true;
+ }
+
+ // handle resizing
+ if (window() == obj && event->type() == QEvent::Resize) {
+ updateViewGeometry();
+ }
+
+ return QWidget::eventFilter(obj, event);
+}
+
+void SearchTabs::reselectFirst()
+{
+ int first = 0;
+ const auto *model = m_listView->model();
+
+ if (m_viewManager->viewProperties().size() > 1 && model->rowCount() > 1 && m_inputLine->text().isEmpty()) {
+ first = 1;
+ }
+
+ QModelIndex index = model->index(first, 0);
+ m_listView->setCurrentIndex(index);
+}
+
+void SearchTabs::updateState()
+{
+ m_model->refresh(m_viewManager);
+ reselectFirst();
+
+ updateViewGeometry();
+ show();
+ raise();
+ setFocus();
+}
+
+void SearchTabs::slotReturnPressed()
+{
+ // switch to tab using the unique ViewProperties identifier
+ // (the view identifier is off by 1)
+ const QModelIndex index = m_listView->currentIndex();
+ m_viewManager->setCurrentView(index.data(SearchTabsModel::View).toInt() - 1);
+
+ hide();
+ deleteLater();
+
+ window()->setFocus();
+}
+
+void SearchTabs::updateViewGeometry()
+{
+ // find MainWindow rectangle
+ QRect boundingRect = window()->contentsRect();
+
+ // set search tabs window size
+ static constexpr int minWidth = 125;
+ const int maxWidth = boundingRect.width();
+ const int preferredWidth = maxWidth / 4.8;
+
+ static constexpr int minHeight = 250;
+ const int maxHeight = boundingRect.height();
+ const int preferredHeight = maxHeight / 4;
+
+ const QSize size{qMin(maxWidth, qMax(preferredWidth, minWidth)), qMin(maxHeight, qMax(preferredHeight, minHeight))};
+
+ // resize() doesn't work here, so use setFixedSize() instead
+ setFixedSize(size);
+
+ // set the position just below/above the tab bar
+ int y;
+ int mainWindowHeight = window()->geometry().height();
+ int containerHeight = m_viewManager->activeContainer()->geometry().height();
+
+ // only calculate the tab bar height if it's visible
+ int tabBarHeight = 0;
+ auto isTabBarVisible = m_viewManager->activeContainer()->tabBar()->isVisible();
+ if (isTabBarVisible) {
+ tabBarHeight = m_viewManager->activeContainer()->tabBar()->geometry().height();
+ }
+
+ // set the position above the south tab bar
+ auto tabBarPos = (QTabWidget::TabPosition)KonsoleSettings::tabBarPosition();
+ if (tabBarPos == QTabWidget::South && isTabBarVisible) {
+ y = mainWindowHeight - tabBarHeight - size.height() - 6;
+ }
+ // set the position below the north tab bar
+ // set the position to the top right corner if the tab bar is hidden
+ else {
+ y = mainWindowHeight - containerHeight + tabBarHeight + 6;
+ }
+ boundingRect.setTop(y);
+
+ // set the position to the right of the window
+ int scrollBarWidth = qApp->style()->pixelMetric(QStyle::PM_ScrollBarExtent);
+ int mainWindowWidth = window()->geometry().width();
+ int x = mainWindowWidth - size.width() - scrollBarWidth - 6;
+ const QPoint position{x, y};
+ move(position);
+}
diff --git a/src/searchtabs/SearchTabs.h b/src/searchtabs/SearchTabs.h
new file mode 100644
index 0000000000..c1b96c3a31
--- /dev/null
+++ b/src/searchtabs/SearchTabs.h
@@ -0,0 +1,68 @@
+/*
+ SPDX-FileCopyrightText: 2024 Troy Hoover <troytjh98 at gmail.com>
+
+ SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#pragma once
+
+// Own
+#include "SearchTabsModel.h"
+
+// Qt
+#include <QEvent>
+#include <QFrame>
+#include <QLineEdit>
+#include <QTreeView>
+
+// Konsole
+#include "ViewManager.h"
+#include "widgets/ViewContainer.h"
+
+class SearchTabsFilterProxyModel;
+
+namespace Konsole
+{
+
+class KONSOLEPRIVATE_EXPORT SearchTabs : public QFrame
+{
+public:
+ SearchTabs(ViewManager *viewManager);
+
+ /**
+ * update state
+ * will fill model with current open tabs
+ */
+ void updateState();
+ void updateViewGeometry();
+
+protected:
+ bool eventFilter(QObject *obj, QEvent *event) override;
+
+private:
+ void reselectFirst();
+
+ /**
+ * Return pressed, activate the selected tab
+ * and go back to background
+ */
+ void slotReturnPressed();
+
+private:
+ ViewManager *m_viewManager;
+
+ QLineEdit *m_inputLine;
+ QTreeView *m_listView;
+
+ /**
+ * tab model
+ */
+ SearchTabsModel *m_model = nullptr;
+
+ /**
+ * fuzzy filter model
+ */
+ SearchTabsFilterProxyModel *m_proxyModel = nullptr;
+};
+
+}
diff --git a/src/searchtabs/SearchTabsModel.cpp b/src/searchtabs/SearchTabsModel.cpp
new file mode 100644
index 0000000000..e9f91c9122
--- /dev/null
+++ b/src/searchtabs/SearchTabsModel.cpp
@@ -0,0 +1,69 @@
+/*
+ SPDX-FileCopyrightText: 2024 Troy Hoover <troytjh98 at gmail.com>
+
+ SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+// Own
+#include "SearchTabsModel.h"
+
+// Konsole
+#include "ViewProperties.h"
+
+using namespace Konsole;
+
+SearchTabsModel::SearchTabsModel(QObject *parent)
+ : QAbstractTableModel(parent)
+{
+}
+
+int SearchTabsModel::rowCount(const QModelIndex &parent) const
+{
+ if (parent.isValid()) {
+ return 0;
+ }
+ return (int)m_tabEntries.size();
+}
+
+int SearchTabsModel::columnCount(const QModelIndex &parent) const
+{
+ Q_UNUSED(parent);
+ return 1;
+}
+
+QVariant SearchTabsModel::data(const QModelIndex &idx, int role) const
+{
+ if (!idx.isValid()) {
+ return {};
+ }
+
+ const TabEntry &tab = m_tabEntries.at(idx.row());
+ switch (role) {
+ case Qt::DisplayRole:
+ case Role::Name:
+ return tab.name;
+ case Role::Score:
+ return tab.score;
+ case Role::View:
+ return tab.view;
+ default:
+ return {};
+ }
+
+ return {};
+}
+
+void SearchTabsModel::refresh(ViewManager *viewManager)
+{
+ QList<ViewProperties *> viewProperties = viewManager->viewProperties();
+
+ QVector<TabEntry> tabs;
+ tabs.reserve(viewProperties.size());
+ for (const auto view : std::as_const(viewProperties)) {
+ tabs.push_back(TabEntry{view->title(), view->identifier(), -1});
+ }
+
+ beginResetModel();
+ m_tabEntries = std::move(tabs);
+ endResetModel();
+}
diff --git a/src/searchtabs/SearchTabsModel.h b/src/searchtabs/SearchTabsModel.h
new file mode 100644
index 0000000000..c55dd82fac
--- /dev/null
+++ b/src/searchtabs/SearchTabsModel.h
@@ -0,0 +1,68 @@
+/*
+ SPDX-FileCopyrightText: 2024 Troy Hoover <troytjh98 at gmail.com>
+
+ SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#pragma once
+
+// Qt
+#include <QAbstractTableModel>
+#include <QStringList>
+#include <QVector>
+
+// Konsole
+#include "ViewManager.h"
+
+namespace Konsole
+{
+
+struct TabEntry {
+ QString name;
+ int view;
+ int score = -1;
+};
+
+class SearchTabsModel : public QAbstractTableModel
+{
+public:
+ enum Role {
+ Name = Qt::UserRole + 1,
+ View,
+ Score,
+ };
+ explicit SearchTabsModel(QObject *parent = nullptr);
+
+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+ int columnCount(const QModelIndex &parent) const override;
+ QVariant data(const QModelIndex &idx, int role) const override;
+ void refresh(ViewManager *viewManager);
+
+ bool isValid(int row) const
+ {
+ return row >= 0 && row < m_tabEntries.size();
+ }
+
+ void setScoreForIndex(int row, int score)
+ {
+ m_tabEntries[row].score = score;
+ }
+
+ const QString &idxToName(int row) const
+ {
+ return m_tabEntries.at(row).name;
+ }
+
+ int idxScore(const QModelIndex &idx) const
+ {
+ if (!idx.isValid()) {
+ return {};
+ }
+ return m_tabEntries.at(idx.row()).score;
+ }
+
+private:
+ QVector<TabEntry> m_tabEntries;
+};
+
+}
diff --git a/src/searchtabs/kfts_fuzzy_match.h b/src/searchtabs/kfts_fuzzy_match.h
new file mode 100644
index 0000000000..0e608d8d96
--- /dev/null
+++ b/src/searchtabs/kfts_fuzzy_match.h
@@ -0,0 +1,431 @@
+/*
+ SPDX-FileCopyrightText: 2017 Forrest Smith
+ SPDX-FileCopyrightText: 2020 Waqar Ahmed
+
+ SPDX-License-Identifier: LGPL-2.0-or-later
+*/
+
+#pragma once
+
+#include <QDebug>
+#include <QString>
+#include <QTextLayout>
+
+/**
+ * This is based on https://github.com/forrestthewoods/lib_fts/blob/master/code/fts_fuzzy_match.h
+ * with modifications for Qt
+ *
+ * Dont include this file in a header file, please :)
+ */
+
+namespace kfts
+{
+/**
+ * @brief simple fuzzy matching of chars in @a pattern with chars in @a str sequentially
+ */
+Q_DECL_UNUSED static bool fuzzy_match_simple(const QStringView pattern, const QStringView str);
+
+/**
+ * @brief This should be the main function you should use. @a outscore is the score
+ * of this match and should be used to sort the results later. Without sorting of the
+ * results this function won't be as effective.
+ */
+Q_DECL_UNUSED static bool fuzzy_match(const QStringView pattern, const QStringView str, int &outScore);
+Q_DECL_UNUSED static bool fuzzy_match(const QStringView pattern, const QStringView str, int &outScore, uint8_t *matches);
+
+/**
+ * @brief get string for display in treeview / listview. This should be used from style delegate.
+ * For example: with @a pattern = "kate", @a str = "kateapp" and @htmlTag = "<b>
+ * the output will be <b>k</b><b>a</b><b>t</b><b>e</b>app which will be visible to user as
+ * <b>kate</b>app.
+ *
+ * TODO: improve this so that we don't have to put html tags on every char probably using some kind
+ * of interval container
+ */
+Q_DECL_UNUSED static QString to_fuzzy_matched_display_string(const QStringView pattern, QString &str, const QString &htmlTag, const QString &htmlTagClose);
+Q_DECL_UNUSED static QString
+to_scored_fuzzy_matched_display_string(const QStringView pattern, QString &str, const QString &htmlTag, const QString &htmlTagClose);
+}
+
+namespace kfts
+{
+// Forward declarations for "private" implementation
+namespace fuzzy_internal
+{
+static inline constexpr QChar toLower(QChar c)
+{
+ return c.isLower() ? c : c.toLower();
+}
+
+static bool fuzzy_match_recursive(QStringView::const_iterator pattern,
+ QStringView::const_iterator str,
+ int &outScore,
+ const QStringView::const_iterator strBegin,
+ const QStringView::const_iterator strEnd,
+ const QStringView::const_iterator patternEnd,
+ uint8_t const *srcMatches,
+ uint8_t *newMatches,
+ int nextMatch,
+ int &totalMatches,
+ int &recursionCount);
+}
+
+// Public interface
+static bool fuzzy_match_simple(const QStringView pattern, const QStringView str)
+{
+ if (pattern.length() == 1) {
+ return str.contains(pattern, Qt::CaseInsensitive);
+ }
+
+ auto patternIt = pattern.cbegin();
+ for (auto strIt = str.cbegin(); strIt != str.cend() && patternIt != pattern.cend(); ++strIt) {
+ if (strIt->toLower() == patternIt->toLower()) {
+ ++patternIt;
+ }
+ }
+ return patternIt == pattern.cend();
+}
+
+static bool fuzzy_match(const QStringView pattern, const QStringView str, int &outScore)
+{
+ if (pattern.length() == 1) {
+ const int found = str.indexOf(pattern, Qt::CaseInsensitive);
+ if (found >= 0) {
+ outScore = 250 - found;
+ outScore += pattern.at(0) == str.at(found);
+ return true;
+ } else {
+ outScore = 0;
+ return false;
+ }
+ }
+
+ // simple substring matching to flush out non-matching stuff
+ auto patternIt = pattern.cbegin();
+ bool lower = patternIt->isLower();
+ QChar cUp = lower ? patternIt->toUpper() : *patternIt;
+ QChar cLow = lower ? *patternIt : patternIt->toLower();
+ for (auto strIt = str.cbegin(); strIt != str.cend() && patternIt != pattern.cend(); ++strIt) {
+ if (*strIt == cLow || *strIt == cUp) {
+ ++patternIt;
+ lower = patternIt->isLower();
+ cUp = lower ? patternIt->toUpper() : *patternIt;
+ cLow = lower ? *patternIt : patternIt->toLower();
+ }
+ }
+
+ if (patternIt != pattern.cend()) {
+ outScore = 0;
+ return false;
+ }
+
+ uint8_t matches[256];
+ return fuzzy_match(pattern, str, outScore, matches);
+}
+
+static bool fuzzy_match(const QStringView pattern, const QStringView str, int &outScore, uint8_t *matches)
+{
+ int recursionCount = 0;
+
+ auto strIt = str.cbegin();
+ auto patternIt = pattern.cbegin();
+ const auto patternEnd = pattern.cend();
+ const auto strEnd = str.cend();
+ int totalMatches = 0;
+
+ return fuzzy_internal::fuzzy_match_recursive(patternIt, strIt, outScore, strIt, strEnd, patternEnd, nullptr, matches, 0, totalMatches, recursionCount);
+}
+
+// Private implementation
+static bool fuzzy_internal::fuzzy_match_recursive(QStringView::const_iterator pattern,
+ QStringView::const_iterator str,
+ int &outScore,
+ const QStringView::const_iterator strBegin,
+ const QStringView::const_iterator strEnd,
+ const QStringView::const_iterator patternEnd,
+ const uint8_t *srcMatches,
+ uint8_t *matches,
+ int nextMatch,
+ int &totalMatches,
+ int &recursionCount)
+{
+ static constexpr int recursionLimit = 10;
+ // max number of matches allowed, this should be enough
+ static constexpr int maxMatches = 256;
+
+ // Count recursions
+ ++recursionCount;
+ if (recursionCount >= recursionLimit) {
+ return false;
+ }
+
+ // Detect end of strings
+ if (pattern == patternEnd || str == strEnd) {
+ return false;
+ }
+
+ // Recursion params
+ bool recursiveMatch = false;
+ uint8_t bestRecursiveMatches[maxMatches];
+ int bestRecursiveScore = 0;
+
+ // Loop through pattern and str looking for a match
+ bool firstMatch = true;
+ QChar currentPatternChar = toLower(*pattern);
+ // Are we matching in sequence start from start?
+ while (pattern != patternEnd && str != strEnd) {
+ // Found match
+ if (currentPatternChar == toLower(*str)) {
+ // Supplied matches buffer was too short
+ if (nextMatch >= maxMatches) {
+ return false;
+ }
+
+ // "Copy-on-Write" srcMatches into matches
+ if (firstMatch && srcMatches) {
+ memcpy(matches, srcMatches, nextMatch);
+ firstMatch = false;
+ }
+
+ // Recursive call that "skips" this match
+ uint8_t recursiveMatches[maxMatches];
+ int recursiveScore = 0;
+ const auto strNextChar = std::next(str);
+ if (fuzzy_match_recursive(pattern,
+ strNextChar,
+ recursiveScore,
+ strBegin,
+ strEnd,
+ patternEnd,
+ matches,
+ recursiveMatches,
+ nextMatch,
+ totalMatches,
+ recursionCount)) {
+ // Pick best recursive score
+ if (!recursiveMatch || recursiveScore > bestRecursiveScore) {
+ memcpy(bestRecursiveMatches, recursiveMatches, maxMatches);
+ bestRecursiveScore = recursiveScore;
+ }
+ recursiveMatch = true;
+ }
+
+ // Advance
+ matches[nextMatch++] = (uint8_t)(std::distance(strBegin, str));
+ ++pattern;
+ currentPatternChar = toLower(*pattern);
+ }
+ ++str;
+ }
+
+ // Determine if full pattern was matched
+ const bool matched = pattern == patternEnd;
+
+ // Calculate score
+ if (matched) {
+ static constexpr int firstSepScoreDiff = 3;
+
+ static constexpr int sequentialBonus = 20;
+ static constexpr int separatorBonus = 25; // bonus if match occurs after a separator
+ static constexpr int firstLetterBonus = 15; // bonus if the first letter is matched
+ static constexpr int firstLetterSepMatchBonus = firstLetterBonus - firstSepScoreDiff; // bonus if the first matched letter is camel or separator
+
+ static constexpr int unmatchedLetterPenalty = -1; // penalty for every letter that doesn't matter
+
+ int nonBeginSequenceBonus = 10;
+ // points by which nonBeginSequenceBonus is increment on every matched letter
+ static constexpr int nonBeginSequenceIncrement = 4;
+
+ // Initialize score
+ outScore = 100;
+
+#define debug_algo 0
+#if debug_algo
+#define dbg(...) qDebug(__VA_ARGS__)
+#else
+#define dbg(...)
+#endif
+
+ // Apply unmatched penalty
+ const int unmatched = (int)(std::distance(strBegin, strEnd)) - nextMatch;
+ outScore += unmatchedLetterPenalty * unmatched;
+ dbg("unmatchedLetterPenalty, unmatched count: %d, outScore: %d", unmatched, outScore);
+
+ bool inSeparatorSeq = false;
+ int i = 0;
+ if (matches[i] == 0) {
+ // First letter match has the highest score
+ outScore += firstLetterBonus + separatorBonus;
+ dbg("firstLetterBonus, outScore: %d", outScore);
+ inSeparatorSeq = true;
+ } else {
+ const QChar neighbor = *(strBegin + matches[i] - 1);
+ const QChar curr = *(strBegin + matches[i]);
+ const bool neighborSeparator = neighbor == QLatin1Char('_') || neighbor == QLatin1Char(' ');
+ if (neighborSeparator || (neighbor.isLower() && curr.isUpper())) {
+ // the first letter that got matched was a sepcial char .e., camel or at a separator
+ outScore += firstLetterSepMatchBonus + separatorBonus;
+ dbg("firstLetterSepMatchBonus at %d, letter: %c, outScore: %d", matches[i], curr.toLatin1(), outScore);
+ inSeparatorSeq = true;
+ } else {
+ // nothing
+ nonBeginSequenceBonus += nonBeginSequenceIncrement;
+ }
+ // We didn't match any special positions, apply leading penalty
+ outScore += -(matches[i]);
+ dbg("LeadingPenalty because no first letter match, outScore: %d", outScore);
+ }
+ i++;
+
+ bool allConsecutive = true;
+ // Apply ordering bonuses
+ for (; i < nextMatch; ++i) {
+ const uint8_t currIdx = matches[i];
+ const uint8_t prevIdx = matches[i - 1];
+ // Sequential
+ if (currIdx == (prevIdx + 1)) {
+ if (i == matches[i]) {
+ // We are in a sequence beginning from first letter
+ outScore += sequentialBonus;
+ dbg("sequentialBonus at %d, letter: %c, outScore: %d", matches[i], (strBegin + currIdx)->toLatin1(), outScore);
+ } else if (inSeparatorSeq) {
+ // we are in a sequnce beginning from a separator like camelHump or underscore
+ outScore += sequentialBonus - firstSepScoreDiff;
+ dbg("in separator seq, [sequentialBonus - 5] at %d, letter: %c, outScore: %d", matches[i], (strBegin + currIdx)->toLatin1(), outScore);
+ } else {
+ // We are in a random sequence
+ outScore += nonBeginSequenceBonus;
+ nonBeginSequenceBonus += nonBeginSequenceIncrement;
+ dbg("nonBeginSequenceBonus at %d, letter: %c, outScore: %d", matches[i], (strBegin + currIdx)->toLatin1(), outScore);
+ }
+ } else {
+ allConsecutive = false;
+
+ // there is a gap between matching chars, apply penalty
+ int penalty = -((currIdx - prevIdx)) - 2;
+ outScore += penalty;
+ inSeparatorSeq = false;
+ nonBeginSequenceBonus = 10;
+ dbg("gap penalty[%d] at %d, letter: %c, outScore: %d", penalty, matches[i], (strBegin + currIdx)->toLatin1(), outScore);
+ }
+
+ // Check for bonuses based on neighbor character value
+ // Camel case
+ const QChar neighbor = *(strBegin + currIdx - 1);
+ const QChar curr = *(strBegin + currIdx);
+ // if camel case bonus, then not snake / separator.
+ // This prevents double bonuses
+ const bool neighborSeparator = neighbor == QLatin1Char('_') || neighbor == QLatin1Char(' ');
+ if (neighborSeparator || (neighbor.isLower() && curr.isUpper())) {
+ outScore += separatorBonus;
+ dbg("separatorBonus at %d, letter: %c, outScore: %d", matches[i], (strBegin + currIdx)->toLatin1(), outScore);
+ inSeparatorSeq = true;
+ continue;
+ }
+ }
+
+ if (allConsecutive && nextMatch >= 4) {
+ outScore *= 2;
+ dbg("allConsecutive double the score, outScore: %d", outScore);
+ }
+ }
+
+ totalMatches = nextMatch;
+ // Return best result
+ if (recursiveMatch && (!matched || bestRecursiveScore > outScore)) {
+ // Recursive score is better than "this"
+ memcpy(matches, bestRecursiveMatches, maxMatches);
+ outScore = bestRecursiveScore;
+ return true;
+ } else if (matched) {
+ // "this" score is better than recursive
+ return true;
+ } else {
+ // no match
+ return false;
+ }
+}
+
+static QString to_fuzzy_matched_display_string(const QStringView pattern, QString &str, const QString &htmlTag, const QString &htmlTagClose)
+{
+ /**
+ * FIXME Don't do so many appends. Instead consider using some interval based solution to wrap a range
+ * of text with the html <tag></tag>
+ */
+ int j = 0;
+ for (int i = 0; i < str.size() && j < pattern.size(); ++i) {
+ if (fuzzy_internal::toLower(str.at(i)) == fuzzy_internal::toLower(pattern.at(j))) {
+ str.replace(i, 1, htmlTag + str.at(i) + htmlTagClose);
+ i += htmlTag.size() + htmlTagClose.size();
+ ++j;
+ }
+ }
+ return str;
+}
+
+static QString to_scored_fuzzy_matched_display_string(const QStringView pattern, QString &str, const QString &htmlTag, const QString &htmlTagClose)
+{
+ if (pattern.isEmpty()) {
+ return str;
+ }
+
+ uint8_t matches[256];
+ int totalMatches = 0;
+ {
+ int score = 0;
+ int recursionCount = 0;
+
+ auto strIt = str.cbegin();
+ auto patternIt = pattern.cbegin();
+ const auto patternEnd = pattern.cend();
+ const auto strEnd = str.cend();
+
+ fuzzy_internal::fuzzy_match_recursive(patternIt, strIt, score, strIt, strEnd, patternEnd, nullptr, matches, 0, totalMatches, recursionCount);
+ }
+
+ int offset = 0;
+ for (int i = 0; i < totalMatches; ++i) {
+ str.insert(matches[i] + offset, htmlTag);
+ offset += htmlTag.size();
+ str.insert(matches[i] + offset + 1, htmlTagClose);
+ offset += htmlTagClose.size();
+ }
+
+ return str;
+}
+
+Q_DECL_UNUSED static QList<QTextLayout::FormatRange>
+get_fuzzy_match_formats(const QStringView pattern, const QStringView str, int offset, const QTextCharFormat &fmt)
+{
+ QList<QTextLayout::FormatRange> ranges;
+ if (pattern.isEmpty()) {
+ return ranges;
+ }
+
+ int totalMatches = 0;
+ int score = 0;
+ int recursionCount = 0;
+
+ auto strIt = str.cbegin();
+ auto patternIt = pattern.cbegin();
+ const auto patternEnd = pattern.cend();
+ const auto strEnd = str.cend();
+
+ uint8_t matches[256];
+ fuzzy_internal::fuzzy_match_recursive(patternIt, strIt, score, strIt, strEnd, patternEnd, nullptr, matches, 0, totalMatches, recursionCount);
+
+ int j = 0;
+ for (int i = 0; i < totalMatches; ++i) {
+ auto matchPos = matches[i];
+ if (!ranges.isEmpty() && matchPos == j + 1) {
+ ranges.last().length++;
+ } else {
+ ranges.append({matchPos + offset, 1, fmt});
+ }
+ j = matchPos;
+ }
+
+ return ranges;
+}
+
+} // namespace kfts
diff --git a/src/settings/TabBarSettings.ui b/src/settings/TabBarSettings.ui
index 3981a743de..13bfaed7f3 100644
--- a/src/settings/TabBarSettings.ui
+++ b/src/settings/TabBarSettings.ui
@@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>507</width>
- <height>473</height>
+ <height>618</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
@@ -226,6 +226,52 @@
</spacer>
</item>
<item row="13" column="0">
+ <widget class="QLabel" name="showSearchTabsButtonLabel">
+ <property name="text">
+ <string>Show Search Tabs button:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="13" column="1" colspan="2">
+ <widget class="QRadioButton" name="ShowSearchTabsButton">
+ <property name="text">
+ <string>Always</string>
+ </property>
+ <attribute name="buttonGroup">
+ <string notr="true">kcfg_SearchTabsButton</string>
+ </attribute>
+ </widget>
+ </item>
+ <item row="14" column="1" colspan="2">
+ <widget class="QRadioButton" name="HideSearchTabsButton">
+ <property name="text">
+ <string>Never</string>
+ </property>
+ <attribute name="buttonGroup">
+ <string notr="true">kcfg_SearchTabsButton</string>
+ </attribute>
+ </widget>
+ </item>
+ <item row="15" column="1" colspan="2">
+ <spacer>
+ <property name="orientation">
+ <enum>Qt::Orientation::Vertical</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Policy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>16</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item row="16" column="0">
<widget class="QLabel" name="miscellaneousAppearanceLabel">
<property name="text">
<string comment="@item:intext Miscellaneous Options">Miscellaneous:</string>
@@ -235,28 +281,28 @@
</property>
</widget>
</item>
- <item row="13" column="1" colspan="2">
+ <item row="16" column="1" colspan="2">
<widget class="QCheckBox" name="kcfg_NewTabButton">
<property name="text">
<string>Show 'New Tab' button</string>
</property>
</widget>
</item>
- <item row="14" column="1" colspan="2">
+ <item row="17" column="1" colspan="2">
<widget class="QCheckBox" name="kcfg_ExpandTabWidth">
<property name="text">
<string>Expand individual tab widths to full window</string>
</property>
</widget>
</item>
- <item row="15" column="1" colspan="2">
+ <item row="18" column="1" colspan="2">
<widget class="QCheckBox" name="kcfg_TabBarUseUserStyleSheet">
<property name="text">
<string>Use user-defined stylesheet:</string>
</property>
</widget>
</item>
- <item row="16" column="1">
+ <item row="19" column="1">
<spacer>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
@@ -272,7 +318,7 @@
</property>
</spacer>
</item>
- <item row="16" column="2">
+ <item row="19" column="2">
<widget class="KUrlRequester" name="kcfg_TabBarUserStyleSheetFile">
<property name="enabled">
<bool>false</bool>
@@ -522,16 +568,28 @@
<tabstop>ShowTabBarWhenNeeded</tabstop>
<tabstop>AlwaysShowTabBar</tabstop>
<tabstop>AlwaysHideTabBar</tabstop>
- <tabstop>Bottom</tabstop>
<tabstop>Top</tabstop>
+ <tabstop>Bottom</tabstop>
+ <tabstop>Left</tabstop>
+ <tabstop>Right</tabstop>
<tabstop>OnEachTab</tabstop>
<tabstop>OnTabBar</tabstop>
<tabstop>None</tabstop>
+ <tabstop>ShowSearchTabsButton</tabstop>
+ <tabstop>HideSearchTabsButton</tabstop>
+ <tabstop>kcfg_NewTabButton</tabstop>
<tabstop>kcfg_ExpandTabWidth</tabstop>
<tabstop>kcfg_TabBarUseUserStyleSheet</tabstop>
+ <tabstop>kcfg_TabBarUserStyleSheetFile</tabstop>
+ <tabstop>kcfg_CloseTabOnMiddleMouseButton</tabstop>
<tabstop>PutNewTabAtTheEnd</tabstop>
<tabstop>PutNewTabAfterCurrentTab</tabstop>
- <tabstop>kcfg_CloseTabOnMiddleMouseButton</tabstop>
+ <tabstop>ShowSplitHeaderWhenNeeded</tabstop>
+ <tabstop>AlwaysHideSplitHeader</tabstop>
+ <tabstop>SplitDragHandleSmall</tabstop>
+ <tabstop>SplitDragHandleMedium</tabstop>
+ <tabstop>SplitDragHandleLarge</tabstop>
+ <tabstop>AlwaysShowSplitHeader</tabstop>
</tabstops>
<resources/>
<connections>
@@ -775,9 +833,58 @@
</hint>
</hints>
</connection>
+ <connection>
+ <sender>AlwaysHideTabBar</sender>
+ <signal>toggled(bool)</signal>
+ <receiver>showSearchTabsButtonLabel</receiver>
+ <slot>setDisabled(bool)</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>20</x>
+ <y>20</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>89</x>
+ <y>429</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>AlwaysHideTabBar</sender>
+ <signal>toggled(bool)</signal>
+ <receiver>ShowSearchTabsButton</receiver>
+ <slot>setDisabled(bool)</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>20</x>
+ <y>20</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>20</x>
+ <y>20</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>AlwaysHideTabBar</sender>
+ <signal>toggled(bool)</signal>
+ <receiver>HideSearchTabsButton</receiver>
+ <slot>setDisabled(bool)</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>20</x>
+ <y>20</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>20</x>
+ <y>20</y>
+ </hint>
+ </hints>
+ </connection>
</connections>
<buttongroups>
<buttongroup name="kcfg_SplitViewVisibility"/>
+ <buttongroup name="kcfg_SearchTabsButton"/>
<buttongroup name="kcfg_CloseTabButton"/>
<buttongroup name="kcfg_NewTabBehavior"/>
<buttongroup name="kcfg_SplitDragHandleSize"/>
diff --git a/src/settings/konsole.kcfg b/src/settings/konsole.kcfg
index 82bd09d43c..b71c8c5289 100644
--- a/src/settings/konsole.kcfg
+++ b/src/settings/konsole.kcfg
@@ -155,6 +155,14 @@
<label>Control the visibility of 'New Tab' button on the tab bar</label>
<default>false</default>
</entry>
+ <entry name="SearchTabsButton" type="Enum">
+ <label>Control the visibility of 'Search Tabs' button on the tab bar</label>
+ <choices>
+ <choice name="ShowSearchTabsButton" />
+ <choice name="HideSearchTabsButton" />
+ </choices>
+ <default>ShowSearchTabsButton</default>
+ </entry>
<entry name="CloseTabButton" type="Enum">
<label>Control where the "Close tab" button will be displayed</label>
<choices>
diff --git a/src/widgets/ViewContainer.cpp b/src/widgets/ViewContainer.cpp
index 8a44432a7b..8aae7d3aad 100644
--- a/src/widgets/ViewContainer.cpp
+++ b/src/widgets/ViewContainer.cpp
@@ -9,6 +9,7 @@
#include "config-konsole.h"
// Qt
+#include <QBoxLayout>
#include <QFile>
#include <QKeyEvent>
#include <QMenu>
@@ -25,6 +26,7 @@
#include "KonsoleSettings.h"
#include "ViewProperties.h"
#include "profile/ProfileList.h"
+#include "searchtabs/SearchTabs.h"
#include "session/SessionController.h"
#include "session/SessionManager.h"
#include "terminalDisplay/TerminalDisplay.h"
@@ -39,6 +41,7 @@ TabbedViewContainer::TabbedViewContainer(ViewManager *connectedViewManager, QWid
: QTabWidget(parent)
, _connectedViewManager(connectedViewManager)
, _newTabButton(new QToolButton(this))
+ , _searchTabsButton(new QToolButton(this))
, _closeTabButton(new QToolButton(this))
, _contextMenuTabIndex(-1)
, _newTabBehavior(PutNewTabAtTheEnd)
@@ -56,6 +59,11 @@ TabbedViewContainer::TabbedViewContainer(ViewManager *connectedViewManager, QWid
_newTabButton->setToolTip(i18nc("@info:tooltip", "Open a new tab"));
connect(_newTabButton, &QToolButton::clicked, this, &TabbedViewContainer::newViewRequest);
+ _searchTabsButton->setIcon(QIcon::fromTheme(QStringLiteral("quickopen")));
+ _searchTabsButton->setAutoRaise(true);
+ _searchTabsButton->setToolTip(i18nc("@info:tooltip", "Search Tabs"));
+ connect(_searchTabsButton, &QToolButton::clicked, this, &TabbedViewContainer::searchTabs);
+
_closeTabButton->setIcon(QIcon::fromTheme(QStringLiteral("tab-close")));
_closeTabButton->setAutoRaise(true);
_closeTabButton->setToolTip(i18nc("@info:tooltip", "Close this tab"));
@@ -205,9 +213,23 @@ void TabbedViewContainer::konsoleConfigChanged()
setCornerWidget(KonsoleSettings::newTabButton() ? _newTabButton : nullptr, Qt::TopLeftCorner);
_newTabButton->setVisible(KonsoleSettings::newTabButton());
- setCornerWidget(KonsoleSettings::closeTabButton() == 1 ? _closeTabButton : nullptr, Qt::TopRightCorner);
+ // Add Layout for right corner tool buttons
+ auto layout = new QHBoxLayout();
+ layout->setStretch(0, 10);
+ layout->setContentsMargins(0, 0, 0, 0);
+ layout->setSpacing(0);
+
+ layout->addWidget(KonsoleSettings::searchTabsButton() == 0 ? _searchTabsButton : nullptr);
+ _searchTabsButton->setVisible(KonsoleSettings::searchTabsButton() == 0);
+
+ layout->addWidget(KonsoleSettings::closeTabButton() == 1 ? _closeTabButton : nullptr);
_closeTabButton->setVisible(KonsoleSettings::closeTabButton() == 1);
+ QWidget *rightCornerWidget = new QWidget();
+ rightCornerWidget->setLayout(layout);
+ setCornerWidget(rightCornerWidget, Qt::TopRightCorner);
+ rightCornerWidget->setVisible(true);
+
tabBar()->setTabsClosable(KonsoleSettings::closeTabButton() == 0);
tabBar()->setExpanding(KonsoleSettings::expandTabWidth());
@@ -467,6 +489,17 @@ void TabbedViewContainer::renameTab(int index)
}
}
+void TabbedViewContainer::searchTabs()
+{
+ /**
+ * show tab search and pass focus to it
+ */
+ SearchTabs *searchTabs = new SearchTabs(this->connectedViewManager());
+ setFocusProxy(searchTabs);
+ searchTabs->raise();
+ searchTabs->show();
+}
+
void TabbedViewContainer::openTabContextMenu(const QPoint &point)
{
if (point.isNull()) {
diff --git a/src/widgets/ViewContainer.h b/src/widgets/ViewContainer.h
index 6c6f8d7c60..d6f56c61ef 100644
--- a/src/widgets/ViewContainer.h
+++ b/src/widgets/ViewContainer.h
@@ -117,6 +117,7 @@ public:
// TODO: Re-enable this later.
// void setNewViewMenu(QMenu *menu);
void renameTab(int index);
+ void searchTabs();
ViewManager *connectedViewManager();
void currentTabChanged(int index);
void closeCurrentTab();
@@ -245,6 +246,7 @@ private:
ViewManager *_connectedViewManager;
QMenu *_contextPopupMenu;
QToolButton *_newTabButton;
+ QToolButton *_searchTabsButton;
QToolButton *_closeTabButton;
int _contextMenuTabIndex;
NewTabBehavior _newTabBehavior;
More information about the kde-doc-english
mailing list