From 9e26f359f3a90c64a0137c41f35116612de6e292 Mon Sep 17 00:00:00 2001 From: Michel Boyer de la Giroday Date: Fri, 23 Aug 2024 06:41:17 +0200 Subject: [PATCH] feat: improve search in document - add a new interface for searching into document view - move the textdocument search to this new interface - implement the interface for QtUiDocument and QtTsDocument Related to #25 --- src/gui/CMakeLists.txt | 5 ++ src/gui/findinterface.h | 51 +++++++++++++++++ src/gui/findwidget.cpp | 37 ++++++++----- src/gui/findwidget.h | 12 +++- src/gui/findwidget.ui | 6 +- src/gui/highlightdelegate.cpp | 86 +++++++++++++++++++++++++++++ src/gui/highlightdelegate.h | 35 ++++++++++++ src/gui/mainwindow.cpp | 97 +++++++++++++++++++++++---------- src/gui/mainwindow.h | 2 + src/gui/qttsview.cpp | 77 ++++++++++++++++---------- src/gui/qttsview.h | 21 +++++-- src/gui/qtuiview.cpp | 22 +++++++- src/gui/qtuiview.h | 18 ++++-- src/gui/searchabletableview.cpp | 90 ++++++++++++++++++++++++++++++ src/gui/searchabletableview.h | 36 ++++++++++++ src/gui/textview.cpp | 16 ++++++ src/gui/textview.h | 6 +- 17 files changed, 525 insertions(+), 92 deletions(-) create mode 100644 src/gui/findinterface.h create mode 100644 src/gui/highlightdelegate.cpp create mode 100644 src/gui/highlightdelegate.h create mode 100644 src/gui/searchabletableview.cpp create mode 100644 src/gui/searchabletableview.h diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 73a96ef1..c08104ae 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -20,6 +20,7 @@ set(PROJECT_SOURCES documentpalette.cpp fileselector.h fileselector.cpp + findinterface.h findwidget.h findinfilespanel.cpp findinfilespanel.h @@ -28,6 +29,8 @@ set(PROJECT_SOURCES gui_constants.h guisettings.h guisettings.cpp + highlightdelegate.h + highlightdelegate.cpp historypanel.h historypanel.cpp imageview.h @@ -68,6 +71,8 @@ set(PROJECT_SOURCES scriptpanel.cpp scriptlistpanel.cpp scriptlistpanel.h + searchabletableview.h + searchabletableview.cpp shortcutmanager.h shortcutmanager.cpp shortcutsettings.h diff --git a/src/gui/findinterface.h b/src/gui/findinterface.h new file mode 100644 index 00000000..ac05d082 --- /dev/null +++ b/src/gui/findinterface.h @@ -0,0 +1,51 @@ +/* + This file is part of Knut. + + SPDX-FileCopyrightText: 2024 Klarälvdalens Datakonsult AB, a KDAB Group company + + SPDX-License-Identifier: GPL-3.0-only + + Contact KDAB at for commercial licensing options. +*/ + +#pragma once + +#include + +namespace Gui { + +/** + * @brief Find interface to handle find and replace in views. + */ +class FindInterface +{ +public: + enum FindFlag { + NoFind = 0x0, + CanSearch = 0x1, + CanReplace = 0x2, + }; + FindInterface(int flags) + : m_findFlags(flags) {}; + + int findFlags() const { return m_findFlags; } + + virtual void find(const QString &text, int options) + { + Q_UNUSED(text); + Q_UNUSED(options); + }; + virtual void cancelFind() {}; + virtual void replace(const QString &before, const QString &after, int options, bool replaceAll) + { + Q_UNUSED(before); + Q_UNUSED(after); + Q_UNUSED(options); + Q_UNUSED(replaceAll); + }; + +private: + int m_findFlags = NoFind; +}; + +} // namespace Gui diff --git a/src/gui/findwidget.cpp b/src/gui/findwidget.cpp index 0cc4b8a1..626234f4 100644 --- a/src/gui/findwidget.cpp +++ b/src/gui/findwidget.cpp @@ -11,8 +11,12 @@ #include "findwidget.h" #include "core/logger.h" #include "core/project.h" +#include "core/qttsdocument.h" +#include "core/qtuidocument.h" #include "core/textdocument.h" #include "guisettings.h" +#include "qttsview.h" +#include "qtuiview.h" #include "ui_findwidget.h" #include @@ -122,35 +126,42 @@ void FindWidget::find(int options) { if (ui->findEdit->text().isEmpty()) return; - auto document = Core::Project::instance()->currentDocument(); - if (auto textDocument = qobject_cast(document)) - textDocument->find(findString(), options); + Q_EMIT findRequested(ui->findEdit->text(), options); } void FindWidget::replaceOne() { - replace(true); + replace(false); } void FindWidget::replaceAll() { - replace(false); + replace(true); } -void FindWidget::replace(bool onlyOne) +void FindWidget::replace(bool replaceAll) { const QString &before = findString(); const QString &after = ui->replaceEdit->text(); if (before.isEmpty()) return; - auto document = Core::Project::instance()->currentDocument(); - if (auto textDocument = qobject_cast(document)) { - if (onlyOne) - textDocument->replaceOne(before, after, findFlags()); - else - textDocument->replaceAll(before, after, findFlags()); - } + Q_EMIT replaceRequested(before, after, findFlags(), replaceAll); +} + +void FindWidget::hideEvent(QHideEvent *event) +{ + Q_EMIT widgetClosed(); + QWidget::hideEvent(event); +} + +void FindWidget::setReplaceVisible(bool show) +{ + // We don't want to use a wrapping frame here due to layouting issues... + ui->replaceWithLabel->setVisible(show); + ui->replaceEdit->setVisible(show); + ui->replaceAllbutton->setVisible(show); + ui->replaceButton->setVisible(show); } } // namespace Gui diff --git a/src/gui/findwidget.h b/src/gui/findwidget.h index b06ab622..b23cc959 100644 --- a/src/gui/findwidget.h +++ b/src/gui/findwidget.h @@ -31,6 +31,16 @@ class FindWidget : public QWidget void open(); + void setReplaceVisible(bool show = true); + +signals: + void findRequested(const QString &text, int options); + void replaceRequested(const QString &before, const QString &after, int options, bool replaceAll); + void widgetClosed(); + +protected: + void hideEvent(QHideEvent *event) override; + private: int findFlags() const; QString findString(); @@ -38,7 +48,7 @@ class FindWidget : public QWidget void find(int options); void replaceOne(); void replaceAll(); - void replace(bool onlyOne); + void replace(bool replaceAll); std::unique_ptr ui; QAction *m_matchCase = nullptr; diff --git a/src/gui/findwidget.ui b/src/gui/findwidget.ui index 91de74d9..74b64b65 100644 --- a/src/gui/findwidget.ui +++ b/src/gui/findwidget.ui @@ -102,7 +102,7 @@ - + Qt::Horizontal @@ -117,14 +117,14 @@ - + Replace with: - + Find: diff --git a/src/gui/highlightdelegate.cpp b/src/gui/highlightdelegate.cpp new file mode 100644 index 00000000..7fe1085b --- /dev/null +++ b/src/gui/highlightdelegate.cpp @@ -0,0 +1,86 @@ +/* + This file is part of Knut. + + SPDX-FileCopyrightText: 2024 Klarälvdalens Datakonsult AB, a KDAB Group company + + SPDX-License-Identifier: GPL-3.0-only + + Contact KDAB at for commercial licensing options. +*/ + +#include "highlightdelegate.h" +#include "core/textdocument.h" + +#include +#include +#include + +namespace Gui { + +HighlightDelegate::HighlightDelegate(QObject *parent) + : QItemDelegate(parent) +{ +} + +void HighlightDelegate::setHighlightedText(const QString &searchText, int options) +{ + m_highlightedText = searchText; + m_options = options; +} + +QString HighlightDelegate::transform(QString text, const QString &textColor, const QString &backgroundColor) const +{ + if (m_highlightedText.isEmpty()) + return text; + + if (m_options & Core::TextDocument::FindRegexp) { + const auto re = QRegularExpression {m_highlightedText}; + QRegularExpressionMatch match; + int index = text.indexOf(re, 0, &match); + while (index != -1) { + const auto oldText = match.captured(0); + const auto newText = QString("%3") + .arg(textColor, backgroundColor, oldText); + text.replace(index, oldText.size(), newText); + index = text.indexOf(re, index + newText.size(), &match); + } + } else { + const bool caseSensitive = m_options & Core::TextDocument::FindCaseSensitively; + int index = text.indexOf(m_highlightedText, 0, caseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive); + while (index != -1) { + const auto oldText = text.mid(index, m_highlightedText.size()); + const auto newText = QString("%3") + .arg(textColor, backgroundColor, oldText); + text.replace(index, oldText.size(), newText); + index = text.indexOf(m_highlightedText, index + newText.size(), + caseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive); + } + } + return text; +} + +void HighlightDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + const auto textColor = option.palette.color(QPalette::HighlightedText).name(); + const auto backgroundColor = option.palette.color(QPalette::Highlight).name(); + + // Display the text with the search string highlighted. + QString text = transform(index.data(Qt::DisplayRole).toString(), textColor, backgroundColor); + QTextDocument doc; + doc.setHtml(text); + + painter->save(); + + // Adjust the painter's transformation to fit the text within the given rectangle + painter->translate(option.rect.topLeft()); + QRect clip(0, 0, option.rect.width(), option.rect.height()); + doc.setTextWidth(option.rect.width()); + + QAbstractTextDocumentLayout::PaintContext ctx; + ctx.clip = clip; + doc.documentLayout()->draw(painter, ctx); + + painter->restore(); +} + +} // namespace Gui diff --git a/src/gui/highlightdelegate.h b/src/gui/highlightdelegate.h new file mode 100644 index 00000000..6113e756 --- /dev/null +++ b/src/gui/highlightdelegate.h @@ -0,0 +1,35 @@ +/* + This file is part of Knut. + + SPDX-FileCopyrightText: 2024 Klarälvdalens Datakonsult AB, a KDAB Group company + + SPDX-License-Identifier: GPL-3.0-only + + Contact KDAB at for commercial licensing options. +*/ + +#pragma once + +#include + +namespace Gui { + +class HighlightDelegate : public QItemDelegate +{ + Q_OBJECT + +public: + explicit HighlightDelegate(QObject *parent = nullptr); + + void setHighlightedText(const QString &searchText, int options); + +protected: + QString transform(QString text, const QString &textColor, const QString &backgroundColor) const; + void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + +private: + QString m_highlightedText; + int m_options; +}; + +} // namespace Gui diff --git a/src/gui/mainwindow.cpp b/src/gui/mainwindow.cpp index 82b290fe..8f46ba23 100644 --- a/src/gui/mainwindow.cpp +++ b/src/gui/mainwindow.cpp @@ -194,6 +194,7 @@ MainWindow::MainWindow(QWidget *parent) // Edit ui->findWidget->hide(); ui->findWidget->installEventFilter(this); + connectFindWidget(); connect(ui->actionSelectAll, &QAction::triggered, this, &MainWindow::selectAll); connect(ui->actionFind, &QAction::triggered, ui->findWidget, &FindWidget::open); connect(ui->actionReplace, &QAction::triggered, ui->findWidget, &FindWidget::open); @@ -578,51 +579,65 @@ static TextView *textViewForDocument(Core::Document *document) void MainWindow::updateActions() { - auto document = Core::Project::instance()->currentDocument(); - - ui->actionCloseDocument->setEnabled(document != nullptr); + auto findInterface = dynamic_cast(ui->tabWidget->currentWidget()); + const bool canSearch = findInterface && findInterface->findFlags() & FindInterface::CanSearch; + const bool canReplace = findInterface && findInterface->findFlags() & FindInterface::CanReplace; + auto document = Core::Project::instance()->currentDocument(); auto *textDocument = qobject_cast(document); - ui->actionSelectAll->setEnabled(textDocument != nullptr); - ui->actionFind->setEnabled(textDocument != nullptr); - ui->actionReplace->setEnabled(textDocument != nullptr); - ui->actionFindNext->setEnabled(textDocument != nullptr); - ui->actionFindPrevious->setEnabled(textDocument != nullptr); - ui->actionDeleteLine->setEnabled(textDocument != nullptr); - ui->actionUndo->setEnabled(textDocument != nullptr); - ui->actionRedo->setEnabled(textDocument != nullptr); + const bool isTextDocument = textDocument != nullptr; auto *textView = textViewForDocument(textDocument); - ui->actionToggleMark->setEnabled(textDocument != nullptr); - ui->actionGotoMark->setEnabled(textDocument != nullptr && textView->hasMark()); - ui->actionSelectToMark->setEnabled(textDocument != nullptr && textView->hasMark()); - ui->actionExecuteAPI->setEnabled(textDocument != nullptr); - + const bool hasMark = textView && textView->hasMark(); auto *codeDocument = qobject_cast(document); const bool lspEnabled = codeDocument && codeDocument->hasLspClient(); + const bool isCodeDocument = codeDocument != nullptr; + const bool isCppDocument = codeDocument && qobject_cast(document); + const bool isRcDocument = qobject_cast(document); + + ui->actionCloseDocument->setEnabled(document != nullptr); + ui->actionExecuteAPI->setEnabled(document != nullptr); + + ui->actionSelectAll->setEnabled(isTextDocument); + ui->actionFind->setEnabled(canSearch); + ui->actionReplace->setEnabled(canReplace); + ui->actionFindNext->setEnabled(canSearch); + ui->actionFindPrevious->setEnabled(canSearch); + ui->actionDeleteLine->setEnabled(isTextDocument); + ui->actionUndo->setEnabled(isTextDocument); + ui->actionRedo->setEnabled(isTextDocument); + + ui->actionToggleMark->setEnabled(isTextDocument); + ui->actionGotoMark->setEnabled(hasMark); + ui->actionSelectToMark->setEnabled(hasMark); + ui->actionFollowSymbol->setEnabled(lspEnabled); ui->actionSwitchDeclDef->setEnabled(lspEnabled); - const bool cppEnabled = codeDocument && qobject_cast(document); - ui->actionSwitchHeaderSource->setEnabled(cppEnabled); - ui->actionCommentSelection->setEnabled(cppEnabled); - ui->actionToggleSection->setEnabled(cppEnabled); - ui->actionGotoBlockEnd->setEnabled(cppEnabled); - ui->actionGotoBlockStart->setEnabled(cppEnabled); - ui->actionSelectToBlockEnd->setEnabled(cppEnabled); - ui->actionSelectToBlockStart->setEnabled(cppEnabled); - ui->actionSelectBlockUp->setEnabled(cppEnabled); - ui->actionDeleteMethod->setEnabled(cppEnabled); + ui->actionSwitchHeaderSource->setEnabled(isCppDocument); + ui->actionCommentSelection->setEnabled(isCppDocument); + ui->actionToggleSection->setEnabled(isCppDocument); + ui->actionGotoBlockEnd->setEnabled(isCppDocument); + ui->actionGotoBlockStart->setEnabled(isCppDocument); + ui->actionSelectToBlockEnd->setEnabled(isCppDocument); + ui->actionSelectToBlockStart->setEnabled(isCppDocument); + ui->actionSelectBlockUp->setEnabled(isCppDocument); + ui->actionDeleteMethod->setEnabled(isCppDocument); - const bool isCodeDocument = codeDocument != nullptr; ui->actionSelectLargerSyntaxNode->setEnabled(isCodeDocument); ui->actionSelectSmallerSyntaxNode->setEnabled(isCodeDocument); ui->actionSelectNextSyntaxNode->setEnabled(isCodeDocument); ui->actionSelectPreviousSyntaxNode->setEnabled(isCodeDocument); ui->actionTreeSitterInspector->setEnabled(isCodeDocument); - const bool rcEnabled = qobject_cast(document); - ui->actionCreateQrc->setEnabled(rcEnabled); - ui->actionCreateUi->setEnabled(rcEnabled); + ui->actionCreateQrc->setEnabled(isRcDocument); + ui->actionCreateUi->setEnabled(isRcDocument); +} + +void MainWindow::updateFindWidgetDisplay() +{ + if (auto findInterface = dynamic_cast(ui->tabWidget->currentWidget())) { + ui->findWidget->setReplaceVisible(findInterface->findFlags() & FindInterface::CanReplace); + } } void MainWindow::updateScriptActions() @@ -632,6 +647,27 @@ void MainWindow::updateScriptActions() ui->actionPlayLastScript->setEnabled(!m_historyPanel->isRecording() && m_scriptPanel->hasScript()); } +void MainWindow::connectFindWidget() +{ + auto handleFindRequest = [this](const QString &text, int options) { + if (auto findInterface = dynamic_cast(ui->tabWidget->currentWidget())) + findInterface->find(text, options); + }; + connect(ui->findWidget, &FindWidget::findRequested, this, handleFindRequest); + + auto handleCloseRequest = [this]() { + if (auto findInterface = dynamic_cast(ui->tabWidget->currentWidget())) + findInterface->cancelFind(); + }; + connect(ui->findWidget, &FindWidget::widgetClosed, this, handleCloseRequest); + + auto handleReplace = [this](const QString &before, const QString &after, int options, bool replaceAll) { + if (auto findInterface = dynamic_cast(ui->tabWidget->currentWidget())) + findInterface->replace(before, after, options, replaceAll); + }; + connect(ui->findWidget, &FindWidget::replaceRequested, this, handleReplace); +} + void MainWindow::returnToEditor() { if (ui->tabWidget->count() == 0) @@ -802,6 +838,7 @@ void MainWindow::changeCurrentDocument() const QModelIndex &index = m_fileModel->index(fileName); m_projectView->setCurrentIndex(index); updateActions(); + updateFindWidgetDisplay(); } } // namespace Gui diff --git a/src/gui/mainwindow.h b/src/gui/mainwindow.h index 2a3f071e..61960867 100644 --- a/src/gui/mainwindow.h +++ b/src/gui/mainwindow.h @@ -101,6 +101,8 @@ class MainWindow : public QMainWindow void updateActions(); void updateScriptActions(); + void connectFindWidget(); + void updateFindWidgetDisplay(); void initProject(const QString &path); void openDocument(const QModelIndex &index); diff --git a/src/gui/qttsview.cpp b/src/gui/qttsview.cpp index 60adefad..fceedc39 100644 --- a/src/gui/qttsview.cpp +++ b/src/gui/qttsview.cpp @@ -9,7 +9,9 @@ */ #include "qttsview.h" -#include "core/qttsdocument.h" +#include "core/textdocument.h" +#include "highlightdelegate.h" +#include "searchabletableview.h" #include #include @@ -19,6 +21,9 @@ namespace Gui { +//============================================================================= +// Model with all the translations +//============================================================================= class QtTsModelView : public QAbstractTableModel { public: @@ -105,24 +110,50 @@ class QtTsModelView : public QAbstractTableModel Core::QtTsDocument *const m_document; }; +//============================================================================= +// Proxy model used to filter data +//============================================================================= class QtTsProxy : public QSortFilterProxyModel { - Q_OBJECT public: using QSortFilterProxyModel::QSortFilterProxyModel; - void setFilterText(const QString &str); + void setFilterText(const QString &str) + { + m_filterText = str; + invalidateFilter(); + } protected: - bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; + bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const + { + auto match = [&](int role) { + return m_filterText.isEmpty() + || sourceModel() + ->index(source_row, role, source_parent) + .data() + .toString() + .contains(m_filterText, Qt::CaseInsensitive); + }; + if (!match(QtTsModelView::TsColumn::Comment) && !match(QtTsModelView::TsColumn::Source) + && !match(QtTsModelView::TsColumn::Translation) && !match(QtTsModelView::TsColumn::Context)) { + return false; + } + + return true; + } private: QString m_filterText; }; +//============================================================================= +// QtTsView +//============================================================================= QtTsView::QtTsView(QWidget *parent) : QWidget(parent) - , m_tableView(new QTableView(this)) + , FindInterface(FindInterface::CanSearch) + , m_tableView(new SearchableTableView(this)) , m_searchLineEdit(new QLineEdit(this)) , m_contentProxyModel(new QtTsProxy(this)) { @@ -147,12 +178,19 @@ void QtTsView::setTsDocument(Core::QtTsDocument *document) m_document->disconnect(this); m_document = document; - if (m_document) + if (m_document) { connect(m_document, &Core::QtTsDocument::fileUpdated, this, &QtTsView::updateView); - + } updateView(); } +void QtTsView::cancelFind() +{ + find("", Core::TextDocument::NoFindFlags); // Reset delegate highlighting + m_tableView->viewport()->update(); + m_tableView->clearSelection(); +} + void QtTsView::updateView() { m_contentProxyModel->setSourceModel(nullptr); @@ -170,28 +208,9 @@ void QtTsView::updateView() m_tableView->horizontalHeader()->setStretchLastSection(true); } -void QtTsProxy::setFilterText(const QString &str) +void QtTsView::find(const QString &searchText, int options) { - m_filterText = str; - invalidateFilter(); + m_tableView->find(searchText, options); } -bool QtTsProxy::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const -{ - auto match = [&](int role) { - return m_filterText.isEmpty() - || sourceModel() - ->index(source_row, role, source_parent) - .data() - .toString() - .contains(m_filterText, Qt::CaseInsensitive); - }; - if (!match(QtTsModelView::TsColumn::Comment) && !match(QtTsModelView::TsColumn::Source) - && !match(QtTsModelView::TsColumn::Translation) && !match(QtTsModelView::TsColumn::Context)) { - return false; - } - - return true; -} -} -#include "qttsview.moc" +} // namespace Gui diff --git a/src/gui/qttsview.h b/src/gui/qttsview.h index acc5b8a9..fd9ba4dd 100644 --- a/src/gui/qttsview.h +++ b/src/gui/qttsview.h @@ -10,31 +10,40 @@ #pragma once +#include "core/qttsdocument.h" +#include "findinterface.h" + #include #include + class QTableView; class QLineEdit; -namespace Core { -class QtTsDocument; -} namespace Gui { class QtTsProxy; -class QtTsView : public QWidget +class SearchableTableView; + +class QtTsView : public QWidget, public FindInterface { Q_OBJECT + public: explicit QtTsView(QWidget *parent = nullptr); void setTsDocument(Core::QtTsDocument *document); + void find(const QString &text, int options) override; + void cancelFind() override; + private: void updateView(); - QTableView *const m_tableView; + + SearchableTableView *const m_tableView; QLineEdit *const m_searchLineEdit; Core::QtTsDocument *m_document = nullptr; QtTsProxy *const m_contentProxyModel; QAbstractItemModel *m_contentModel = nullptr; }; -} + +} // namespace Gui diff --git a/src/gui/qtuiview.cpp b/src/gui/qtuiview.cpp index c6e0cd68..8e1d7b1f 100644 --- a/src/gui/qtuiview.cpp +++ b/src/gui/qtuiview.cpp @@ -10,6 +10,9 @@ #include "qtuiview.h" #include "core/qtuidocument.h" +#include "core/textdocument.h" +#include "highlightdelegate.h" +#include "searchabletableview.h" #include #include @@ -100,7 +103,8 @@ class QtUiModelView : public QAbstractTableModel QtUiView::QtUiView(QWidget *parent) : QSplitter(parent) - , m_tableView(new QTableView(this)) + , FindInterface(FindInterface::CanSearch) + , m_tableView(new SearchableTableView(this)) , m_previewArea(new QMdiArea(this)) { addWidget(m_previewArea); @@ -112,6 +116,11 @@ QtUiView::QtUiView(QWidget *parent) m_previewArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); } +void QtUiView::find(const QString &searchText, int options) +{ + m_tableView->find(searchText, options); +} + void QtUiView::setUiDocument(Core::QtUiDocument *document) { Q_ASSERT(document); @@ -120,12 +129,19 @@ void QtUiView::setUiDocument(Core::QtUiDocument *document) m_document->disconnect(this); m_document = document; - if (m_document) + if (m_document) { connect(m_document, &Core::QtUiDocument::fileUpdated, this, &QtUiView::updateView); - + } updateView(); } +void QtUiView::cancelFind() +{ + find("", Core::TextDocument::NoFindFlags); // Reset delegate highlighting + m_tableView->viewport()->update(); + m_tableView->clearSelection(); +} + void QtUiView::updateView() { delete m_tableView->model(); diff --git a/src/gui/qtuiview.h b/src/gui/qtuiview.h index 2e326c19..040bdd91 100644 --- a/src/gui/qtuiview.h +++ b/src/gui/qtuiview.h @@ -10,30 +10,36 @@ #pragma once +#include "core/qtuidocument.h" +#include "findinterface.h" + +#include #include class QTableView; class QMdiArea; class QMdiSubWindow; -namespace Core { -class QtUiDocument; -} - namespace Gui { -class QtUiView : public QSplitter +class SearchableTableView; + +class QtUiView : public QSplitter, public FindInterface { Q_OBJECT + public: explicit QtUiView(QWidget *parent = nullptr); void setUiDocument(Core::QtUiDocument *document); + void find(const QString &text, int options) override; + void cancelFind() override; + private: void updateView(); - QTableView *const m_tableView; + SearchableTableView *const m_tableView; QMdiArea *const m_previewArea; Core::QtUiDocument *m_document = nullptr; QMdiSubWindow *m_previewWindow = nullptr; diff --git a/src/gui/searchabletableview.cpp b/src/gui/searchabletableview.cpp new file mode 100644 index 00000000..25b99d1b --- /dev/null +++ b/src/gui/searchabletableview.cpp @@ -0,0 +1,90 @@ +/* + This file is part of Knut. + + SPDX-FileCopyrightText: 2024 Klarälvdalens Datakonsult AB, a KDAB Group company + + SPDX-License-Identifier: GPL-3.0-only + + Contact KDAB at for commercial licensing options. +*/ + +#include "searchabletableview.h" +#include "core/textdocument.h" +#include "highlightdelegate.h" + +namespace Gui { + +SearchableTableView::SearchableTableView(QWidget *parent) + : QTableView(parent) +{ + setItemDelegate(new HighlightDelegate(this)); +} + +SearchableTableView::~SearchableTableView() = default; + +void SearchableTableView::find(const QString &text, int options) +{ + if (text.isEmpty()) { + // Clear the search results + static_cast(itemDelegate())->setHighlightedText({}, Core::TextDocument::NoFindFlags); + m_searchResults.clear(); + m_currentResultIndex = 0; + m_highlightedText.clear(); + m_options = 0; + viewport()->update(); + return; + } + + // Search only in case the search was not already processed + if (text != m_highlightedText || options != m_options) { + // Update delegate with the search text + static_cast(itemDelegate())->setHighlightedText(text, options); + viewport()->update(); + + m_searchResults = searchModel(text, options); + m_currentResultIndex = 0; + + // Update the current search text + m_highlightedText = text; + m_options = options; + } else { + // Search was already processed. Handle options (Backward, Forward) + if (!m_searchResults.isEmpty()) { + if (options & Core::TextDocument::FindBackward) { + m_currentResultIndex = (m_currentResultIndex - 1) % m_searchResults.count(); + } else { + m_currentResultIndex = (m_currentResultIndex + 1) % m_searchResults.count(); + } + } + } + + if (!m_searchResults.isEmpty()) { + selectionModel()->setCurrentIndex(m_searchResults.at(m_currentResultIndex), QItemSelectionModel::SelectCurrent); + } +} + +QModelIndexList SearchableTableView::searchModel(const QString &text, int options) const +{ + if (text.isEmpty()) + return {}; + + QModelIndexList searchResults; + for (int row = 0; row < model()->rowCount(); ++row) { + for (int column = 0; column < model()->columnCount(); ++column) { + const QModelIndex index = model()->index(row, column); + const QString data = model()->data(index).toString(); + if (options & Core::TextDocument::FindRegexp) { + QRegularExpression re(text); + if (data.contains(re)) + searchResults.append(index); + } else { + const bool caseSensitive = options & Core::TextDocument::FindCaseSensitively; + if (data.contains(text, caseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive)) + searchResults.append(index); + } + } + } + return searchResults; +} + +} // namespace Gui diff --git a/src/gui/searchabletableview.h b/src/gui/searchabletableview.h new file mode 100644 index 00000000..0ca4375b --- /dev/null +++ b/src/gui/searchabletableview.h @@ -0,0 +1,36 @@ +/* + This file is part of Knut. + + SPDX-FileCopyrightText: 2024 Klarälvdalens Datakonsult AB, a KDAB Group company + + SPDX-License-Identifier: GPL-3.0-only + + Contact KDAB at for commercial licensing options. +*/ + +#pragma once + +#include + +namespace Gui { + +class SearchableTableView : public QTableView +{ + Q_OBJECT + +public: + explicit SearchableTableView(QWidget *parent = nullptr); + ~SearchableTableView() override; + + void find(const QString &text, int options); + +private: + QModelIndexList searchModel(const QString &text, int options) const; + + QString m_highlightedText; + int m_options = 0; + QModelIndexList m_searchResults; + int m_currentResultIndex = 0; +}; + +} // namespace Gui diff --git a/src/gui/textview.cpp b/src/gui/textview.cpp index 68554ff1..76aac967 100644 --- a/src/gui/textview.cpp +++ b/src/gui/textview.cpp @@ -66,6 +66,7 @@ class MarkRect : public QWidget TextView::TextView(QWidget *parent) : QWidget {parent} + , FindInterface(FindInterface::CanSearch | FindInterface::CanReplace) , m_quickActionButton(new QToolButton(this)) { m_quickActionButton->hide(); @@ -167,6 +168,21 @@ bool TextView::eventFilter(QObject *obj, QEvent *event) return false; } +void TextView::find(const QString &text, int options) +{ + document()->find(text, options); +} + +void TextView::replace(const QString &before, const QString &after, int options, bool replaceAll) +{ + if (replaceAll) { + document()->replaceAll(before, after, options); + + } else { + document()->replaceOne(before, after, options); + } +} + Core::TextDocument *TextView::document() const { return m_document; diff --git a/src/gui/textview.h b/src/gui/textview.h index 4c2bad33..9dc16fc8 100644 --- a/src/gui/textview.h +++ b/src/gui/textview.h @@ -11,6 +11,7 @@ #pragma once #include "core/mark.h" +#include "findinterface.h" #include @@ -21,7 +22,7 @@ class TextDocument; } namespace Gui { -class TextView : public QWidget +class TextView : public QWidget, public FindInterface { Q_OBJECT public: @@ -36,6 +37,9 @@ class TextView : public QWidget bool eventFilter(QObject *obj, QEvent *event) override; + void find(const QString &text, int options) override; + void replace(const QString &before, const QString &after, int options, bool replaceAll) override; + protected: Core::TextDocument *document() const;