From 5a518603703f515f42d35e528ae18cee2badd6d7 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 (#25) Fixes tasks: QtUiDocument QtTsDocument RCDocument text view RCDocument central view RCDocument left tree --- src/core/qttsdocument.cpp | 1 - src/core/qttsdocument.h | 2 + src/gui/CMakeLists.txt | 2 + src/gui/findwidget.cpp | 53 ++++- src/gui/findwidget.h | 9 + src/gui/findwidget.ui | 294 ++++++++++++++++++---------- src/gui/highlightsearchdelegate.cpp | 83 ++++++++ src/gui/highlightsearchdelegate.h | 34 ++++ src/gui/mainwindow.cpp | 30 ++- src/gui/mainwindow.h | 1 + src/gui/qttsview.cpp | 82 +++++++- src/gui/qttsview.h | 35 +++- src/gui/qtuiview.cpp | 82 +++++++- src/gui/qtuiview.h | 31 ++- src/rcui/rcfileview.cpp | 221 ++++++++++++++++++++- src/rcui/rcfileview.h | 35 ++++ src/rcui/rcfileview.ui | 128 ++++++++++-- 17 files changed, 979 insertions(+), 144 deletions(-) create mode 100644 src/gui/highlightsearchdelegate.cpp create mode 100644 src/gui/highlightsearchdelegate.h diff --git a/src/core/qttsdocument.cpp b/src/core/qttsdocument.cpp index adff5d3c..f2135dba 100644 --- a/src/core/qttsdocument.cpp +++ b/src/core/qttsdocument.cpp @@ -10,7 +10,6 @@ #include "qttsdocument.h" #include "logger.h" -#include "utils/log.h" #include #include diff --git a/src/core/qttsdocument.h b/src/core/qttsdocument.h index 5fb31618..9237c8f7 100644 --- a/src/core/qttsdocument.h +++ b/src/core/qttsdocument.h @@ -66,6 +66,7 @@ class QtTsDocument : public Document Q_PROPERTY(QString language READ language WRITE setLanguage NOTIFY languageChanged) Q_PROPERTY(QString sourceLanguage READ sourceLanguage WRITE setSourceLanguage NOTIFY sourceLanguageChanged) Q_PROPERTY(QList messages READ messages NOTIFY messagesChanged) + public: explicit QtTsDocument(QObject *parent = nullptr); @@ -75,6 +76,7 @@ class QtTsDocument : public Document const QString &translation, const QString &comment = QString()); Q_INVOKABLE void setMessageContext(const QString &context, const QString &comment, const QString &source, const QString &newContext); + QString language() const; QString sourceLanguage() const; QList messages() const; diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 73a96ef1..648f6497 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -28,6 +28,8 @@ set(PROJECT_SOURCES gui_constants.h guisettings.h guisettings.cpp + highlightsearchdelegate.h + highlightsearchdelegate.cpp historypanel.h historypanel.cpp imageview.h diff --git a/src/gui/findwidget.cpp b/src/gui/findwidget.cpp index 0cc4b8a1..41604409 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 @@ -86,11 +90,35 @@ QString FindWidget::findString() void FindWidget::findNext() { + auto qtUiDocument = qobject_cast(Core::Project::instance()->currentDocument()); + if (qtUiDocument) { + Q_EMIT search(ui->findEdit->text(), QtUiView::FindForward); + return; + } + + auto qtTsDocument = qobject_cast(Core::Project::instance()->currentDocument()); + if (qtTsDocument) { + Q_EMIT search(ui->findEdit->text(), QtTsView::FindForward); + return; + } + find(findFlags()); } void FindWidget::findPrevious() { + auto qtUiDocument = qobject_cast(Core::Project::instance()->currentDocument()); + if (qtUiDocument) { + Q_EMIT search(ui->findEdit->text(), QtUiView::FindBackward); + return; + } + + auto qtTsDocument = qobject_cast(Core::Project::instance()->currentDocument()); + if (qtTsDocument) { + Q_EMIT search(ui->findEdit->text(), QtTsView::FindBackward); + return; + } + find(findFlags() | Core::TextDocument::FindBackward); } @@ -120,9 +148,12 @@ void FindWidget::open() void FindWidget::find(int options) { - if (ui->findEdit->text().isEmpty()) - return; auto document = Core::Project::instance()->currentDocument(); + + const QString searchText = ui->findEdit->text(); + if (searchText.isEmpty()) + return; + if (auto textDocument = qobject_cast(document)) textDocument->find(findString(), options); } @@ -139,13 +170,14 @@ void FindWidget::replaceAll() void FindWidget::replace(bool onlyOne) { - const QString &before = findString(); + const QString &before = ui->findEdit->text(); const QString &after = ui->replaceEdit->text(); - if (before.isEmpty()) - return; auto document = Core::Project::instance()->currentDocument(); if (auto textDocument = qobject_cast(document)) { + const QString &before = findString(); + if (before.isEmpty()) + return; if (onlyOne) textDocument->replaceOne(before, after, findFlags()); else @@ -153,4 +185,15 @@ void FindWidget::replace(bool onlyOne) } } +void FindWidget::hideEvent(QHideEvent *event) +{ + Q_EMIT findWidgetHiding(); + QWidget::hideEvent(event); +} + +void FindWidget::showReplaceFeature(bool show) +{ + ui->replaceFrame->setVisible(show); +} + } // namespace Gui diff --git a/src/gui/findwidget.h b/src/gui/findwidget.h index b06ab622..2e831503 100644 --- a/src/gui/findwidget.h +++ b/src/gui/findwidget.h @@ -31,6 +31,15 @@ class FindWidget : public QWidget void open(); + void showReplaceFeature(bool show = true); + +signals: + void search(const QString &text, int options); + void findWidgetHiding(); + +protected: + void hideEvent(QHideEvent *event) override; + private: int findFlags() const; QString findString(); diff --git a/src/gui/findwidget.ui b/src/gui/findwidget.ui index 91de74d9..48738af3 100644 --- a/src/gui/findwidget.ui +++ b/src/gui/findwidget.ui @@ -6,128 +6,214 @@ 0 0 - 844 - 53 + 914 + 110 + + + 96 + 0 + + - 6 + 0 - 1 + 0 0 - 1 + 0 - 1 + 0 - - - - - - - - - Find Previous - - - true - - - - - - - Find Next - - - true - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Close - - - true - - - - - - - - - - - - - - Replace - - - true - - - - - - - Replace All - - - true - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - Replace with: + + + + QFrame::NoFrame + + + QFrame::Plain + + + 0 + + + 6 + + + 1 + + + 6 + + + 1 + + + + + + 81 + 0 + + + + Find: + + + + + + + true + + + + + + + + + + 0 + 0 + + + + Find Previous + + + true + + + + + + + Find Next + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Close + + + true + + + + + + - - - - Find: + + + + QFrame::NoFrame + + + QFrame::Plain + + + 0 + + + 6 + + + 1 + + + 6 + + + 1 + + + + + Replace with: + + + + + + + true + + + + + + + + + + 96 + 0 + + + + Replace + + + true + + + + + + + Replace All + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + diff --git a/src/gui/highlightsearchdelegate.cpp b/src/gui/highlightsearchdelegate.cpp new file mode 100644 index 00000000..9eec4aa1 --- /dev/null +++ b/src/gui/highlightsearchdelegate.cpp @@ -0,0 +1,83 @@ +/* + 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 "highlightsearchdelegate.h" +#include + +namespace Gui { + +HighlightSearchDelegate::HighlightSearchDelegate(QObject *parent) + : QItemDelegate(parent) +{ +} + +void HighlightSearchDelegate::setSearchText(const QString &searchText, int offset) +{ + m_searchText = searchText; + m_offset = offset; +} + +void HighlightSearchDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + QString text = index.data().toString(); + if (m_searchText.isEmpty() || !text.contains(m_searchText, Qt::CaseInsensitive)) { + QItemDelegate::paint(painter, option, index); + return; + } + + // Check for multiline strings + QStringList multilineStrings = text.split("\n", Qt::SkipEmptyParts); + bool isMultiline = multilineStrings.count() > 1; + if (isMultiline) { + // Make it a one line text for the search highlight, + // otherwise the painting is error prone (needs too much manipulation). + // That does not impact the model itself. + text = text.replace("\n", " - "); + } + // Find the substring to highlight + const QRegularExpression regex(m_searchText, QRegularExpression::CaseInsensitiveOption); + int start = regex.match(text).capturedStart(); + int end = start + regex.match(text).capturedLength(); + + // Calculate the drawing positions. + // The TreeViews display its Items excentred in relation to the left side of the cell. + // Correct the x position accordingly using the caller offset value. + int x = option.rect.left() + m_offset; + int y = option.rect.top(); + int height = option.rect.height(); + int width = painter->fontMetrics().horizontalAdvance(text.left(start)); + + // Paint the seach result string 'bold'. + QFont highlightFont = option.font; + highlightFont.setBold(true); + + painter->save(); + // Make sure we don't paint over the selected item highlighted background. + if (option.state & QStyle::State_Selected) { + // Set the background color to the selection color + painter->fillRect(option.rect, option.palette.brush(QPalette::Active, QPalette::Highlight)); + } + + painter->drawText(x, y, width, height, option.displayAlignment, text.left(start)); + // Paint the searched string with bold font. + painter->setFont(highlightFont); + x += width; + width = painter->fontMetrics().horizontalAdvance(text.mid(start, end - start)); + painter->drawText(x, y, width, height, option.displayAlignment, text.mid(start, end - start)); + // Reset the painter font to original font. + painter->setFont(option.font); + x += width; + width = painter->fontMetrics().horizontalAdvance(text.right(text.length() - end)); + painter->drawText(x, y, width, height, option.displayAlignment, text.right(text.length() - end)); + painter->restore(); +} + +} // namespace Gui diff --git a/src/gui/highlightsearchdelegate.h b/src/gui/highlightsearchdelegate.h new file mode 100644 index 00000000..b10a01dc --- /dev/null +++ b/src/gui/highlightsearchdelegate.h @@ -0,0 +1,34 @@ +/* + 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 HighlightSearchDelegate : public QItemDelegate +{ + Q_OBJECT + +public: + explicit HighlightSearchDelegate(QObject *parent = nullptr); + + void setSearchText(const QString &searchText, int offset = 0); + +protected: + void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + +private: + QString m_searchText; + int m_offset; +}; + +} // namespace Gui diff --git a/src/gui/mainwindow.cpp b/src/gui/mainwindow.cpp index e62c5e53..af9daa90 100644 --- a/src/gui/mainwindow.cpp +++ b/src/gui/mainwindow.cpp @@ -579,12 +579,17 @@ void MainWindow::updateActions() ui->actionCloseDocument->setEnabled(document != nullptr); + auto *qtTsDocument = qobject_cast(document); + auto *qtUiDocument = qobject_cast(document); auto *textDocument = qobject_cast(document); + + const bool enableFindWidgetFeatures = + (qtTsDocument != nullptr || qtUiDocument != nullptr || textDocument != nullptr); ui->actionSelectAll->setEnabled(textDocument != nullptr); - ui->actionFind->setEnabled(textDocument != nullptr); + ui->actionFind->setEnabled(enableFindWidgetFeatures); ui->actionReplace->setEnabled(textDocument != nullptr); - ui->actionFindNext->setEnabled(textDocument != nullptr); - ui->actionFindPrevious->setEnabled(textDocument != nullptr); + ui->actionFindNext->setEnabled(enableFindWidgetFeatures); + ui->actionFindPrevious->setEnabled(enableFindWidgetFeatures); ui->actionDeleteLine->setEnabled(textDocument != nullptr); ui->actionUndo->setEnabled(textDocument != nullptr); ui->actionRedo->setEnabled(textDocument != nullptr); @@ -622,6 +627,22 @@ void MainWindow::updateActions() ui->actionCreateUi->setEnabled(rcEnabled); } +void MainWindow::updateFindWidgetDisplay() +{ + // Handle the 'FindWidget' display depending on the document type. + auto document = Core::Project::instance()->currentDocument(); + + auto *qtTsDocument = qobject_cast(document); + auto *qtUiDocument = qobject_cast(document); + auto *rcDocument = qobject_cast(document); + // Hide the replace widgets frame. + const bool hideReplaceFeature = (qtTsDocument != nullptr || qtUiDocument != nullptr); + ui->findWidget->showReplaceFeature(!hideReplaceFeature); + // Do not show if the current document is a 'RcDocuments'. It has its own search widgets + if (rcDocument != nullptr) + ui->findWidget->hide(); +} + void MainWindow::updateScriptActions() { ui->actionStartRecordingScript->setEnabled(!m_historyPanel->isRecording()); @@ -701,6 +722,7 @@ QWidget *MainWindow::widgetForDocument(Core::Document *document) } case Core::Document::Type::QtUi: { auto uiview = new QtUiView(); + uiview->setFindWidget(ui->findWidget); uiview->setUiDocument(qobject_cast(document)); return uiview; } @@ -722,6 +744,7 @@ QWidget *MainWindow::widgetForDocument(Core::Document *document) } case Core::Document::Type::QtTs: { auto tsView = new QtTsView(); + tsView->setFindWidget(ui->findWidget); tsView->setTsDocument(qobject_cast(document)); return tsView; } @@ -799,6 +822,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 1089d08a..0032de1a 100644 --- a/src/gui/mainwindow.h +++ b/src/gui/mainwindow.h @@ -101,6 +101,7 @@ class MainWindow : public QMainWindow void updateActions(); void updateScriptActions(); + 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 98bd6c56..6d48d21f 100644 --- a/src/gui/qttsview.cpp +++ b/src/gui/qttsview.cpp @@ -9,7 +9,8 @@ */ #include "qttsview.h" -#include "core/qttsdocument.h" +#include "findwidget.h" +#include "highlightsearchdelegate.h" #include #include @@ -125,10 +126,13 @@ QtTsView::QtTsView(QWidget *parent) , m_tableView(new QTableView(this)) , m_searchLineEdit(new QLineEdit(this)) , m_contentProxyModel(new QtTsProxy(this)) + , m_currentSearchText(QString()) { auto mainWidgetLayout = new QVBoxLayout(this); m_tableView->verticalHeader()->hide(); m_tableView->setSelectionBehavior(QAbstractItemView::SelectRows); + // Set the delegate + m_tableView->setItemDelegate(new HighlightSearchDelegate(this)); mainWidgetLayout->addWidget(m_tableView); mainWidgetLayout->addWidget(m_searchLineEdit); m_searchLineEdit->setPlaceholderText(tr("Filter...")); @@ -147,12 +151,25 @@ 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::setFindWidget(FindWidget *findWidget) +{ + Q_ASSERT(findWidget); + // Connections + connect(findWidget, &FindWidget::search, this, &QtTsView::search); + // Update display. + connect(findWidget, &FindWidget::findWidgetHiding, this, [this]() { + search(""); // Reset delegate highlighting + m_tableView->viewport()->update(); + m_tableView->clearSelection(); + }); +} + void QtTsView::updateView() { m_contentProxyModel->setSourceModel(nullptr); @@ -170,6 +187,65 @@ void QtTsView::updateView() m_tableView->horizontalHeader()->setStretchLastSection(true); } +void QtTsView::search(const QString &searchText, int options) +{ + // Search only in case the search was not already processed (inclusive empty string search). + if (searchText != m_currentSearchText || !m_initialSearchProcessed) { + // Colorize search hits (This gives an overview over the search result) + static_cast(m_tableView->itemDelegate())->setSearchText(searchText); + // Update display. + m_tableView->viewport()->update(); + // Search... + m_currentSearchResult = searchModel(searchText, m_tableView->model()); + // Reset the search index. + m_currentSearchIndex = 0; + m_initialSearchProcessed = true; + } else { // Search was already processed. Handle options (Backward, Forward) + if (!m_currentSearchResult.isEmpty()) { + int index = m_currentSearchIndex; + switch (options) { + case QtTsView::FindForward: { + index = m_currentSearchIndex + 1; + // if it is last match continue with the first match. + index = (index < m_currentSearchResult.count()) ? index : 0; + break; + } + case QtTsView::FindBackward: { + index = m_currentSearchIndex - 1; + // if it is the first match continue with the last match. + index = (index >= 0) ? index : m_currentSearchResult.count() - 1; + break; + } + } + // Update the current search index + m_currentSearchIndex = index; + } + } + // Update the current search text + m_currentSearchText = searchText; + // Select (highlight) the cell that matched, as the current index. + if (!m_currentSearchResult.isEmpty()) { + m_tableView->selectionModel()->setCurrentIndex(m_currentSearchResult[m_currentSearchIndex], + QItemSelectionModel::SelectCurrent); + } +} + +QModelIndexList QtTsView::searchModel(const QString &searchText, const QAbstractItemModel *model) const +{ + 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 ((searchText.isEmpty() && data == searchText) + || (!searchText.isEmpty() && data.contains(searchText, Qt::CaseInsensitive))) { + searchResults.append(index); + } + } + } + return searchResults; +} + void QtTsProxy::setFilterText(const QString &str) { m_filterText = str; diff --git a/src/gui/qttsview.h b/src/gui/qttsview.h index acc5b8a9..374f4c44 100644 --- a/src/gui/qttsview.h +++ b/src/gui/qttsview.h @@ -10,31 +10,56 @@ #pragma once +#include "core/qttsdocument.h" + #include #include + class QTableView; class QLineEdit; -namespace Core { -class QtTsDocument; -} namespace Gui { - +class FindWidget; class QtTsProxy; + class QtTsView : public QWidget { Q_OBJECT + public: + enum FindFlag { + NoFindFlags = 0x0, + FindBackward = 0X2, + FindForward = 0x4, + }; + Q_DECLARE_FLAGS(FindFlags, FindFlag) + Q_ENUM(FindFlag) + explicit QtTsView(QWidget *parent = nullptr); void setTsDocument(Core::QtTsDocument *document); + void setFindWidget(FindWidget *findWidget); + +public slots: + // Accessible from QML + void search(const QString &searchText, int option = Gui::QtTsView::NoFindFlags); private: + QModelIndexList searchModel(const QString &searchText, const QAbstractItemModel *model) const; void updateView(); + QTableView *const m_tableView; QLineEdit *const m_searchLineEdit; Core::QtTsDocument *m_document = nullptr; QtTsProxy *const m_contentProxyModel; QAbstractItemModel *m_contentModel = nullptr; + // Search feature + QModelIndexList m_currentSearchResult; + QString m_currentSearchText; + int m_currentSearchIndex = 0; + bool m_initialSearchProcessed = false; }; -} + +} // namespace Gui + +Q_DECLARE_OPERATORS_FOR_FLAGS(Gui::QtTsView::FindFlags) diff --git a/src/gui/qtuiview.cpp b/src/gui/qtuiview.cpp index 0277bcf3..f7467b48 100644 --- a/src/gui/qtuiview.cpp +++ b/src/gui/qtuiview.cpp @@ -10,7 +10,8 @@ #include "qtuiview.h" #include "core/qtuidocument.h" -#include "core/rcdocument.h" +#include "findwidget.h" +#include "highlightsearchdelegate.h" #include #include @@ -103,6 +104,7 @@ QtUiView::QtUiView(QWidget *parent) : QSplitter(parent) , m_tableView(new QTableView(this)) , m_previewArea(new QMdiArea(this)) + , m_currentSearchText(QString()) { addWidget(m_previewArea); addWidget(m_tableView); @@ -111,6 +113,68 @@ QtUiView::QtUiView(QWidget *parent) m_tableView->setSelectionBehavior(QAbstractItemView::SelectRows); m_previewArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); m_previewArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + + // Set the delegate + m_tableView->setItemDelegate(new HighlightSearchDelegate(this)); +} + +void QtUiView::search(const QString &searchText, int options) +{ + // Search only in case the search was not already processed (inclusive empty string search). + if (searchText != m_currentSearchText || !m_initialSearchProcessed) { + // Colorize search hits (This gives an overview over the search result) + static_cast(m_tableView->itemDelegate())->setSearchText(searchText); + // Update display. + m_tableView->viewport()->update(); + // Search... + m_currentSearchResult = searchModel(searchText, m_tableView->model()); + // Reset the search index. + m_currentSearchIndex = 0; + m_initialSearchProcessed = true; + } else { // Search was already processed. Handle options (Backward, Forward) + if (!m_currentSearchResult.isEmpty()) { + int index = m_currentSearchIndex; + switch (options) { + case QtUiView::FindForward: { + index = m_currentSearchIndex + 1; + // if it is last match continue with the first match. + index = (index < m_currentSearchResult.count()) ? index : 0; + break; + } + case QtUiView::FindBackward: { + index = m_currentSearchIndex - 1; + // if it is the first match continue with the last match. + index = (index >= 0) ? index : m_currentSearchResult.count() - 1; + break; + } + } + // Update the current search index + m_currentSearchIndex = index; + } + } + // Update the current search text + m_currentSearchText = searchText; + // Select (highlight) the cell that matched, as the current index. + if (!m_currentSearchResult.isEmpty()) { + m_tableView->selectionModel()->setCurrentIndex(m_currentSearchResult[m_currentSearchIndex], + QItemSelectionModel::SelectCurrent); + } +} + +QModelIndexList QtUiView::searchModel(const QString &searchText, const QAbstractItemModel *model) const +{ + 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 ((searchText.isEmpty() && data == searchText) + || (!searchText.isEmpty() && data.contains(searchText, Qt::CaseInsensitive))) { + searchResults.append(index); + } + } + } + return searchResults; } void QtUiView::setUiDocument(Core::QtUiDocument *document) @@ -121,12 +185,24 @@ 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::setFindWidget(FindWidget *findWidget) +{ + Q_ASSERT(findWidget); + connect(findWidget, &FindWidget::search, this, &QtUiView::search); + // Update display. + connect(findWidget, &FindWidget::findWidgetHiding, this, [this]() { + search(""); // 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..faee34da 100644 --- a/src/gui/qtuiview.h +++ b/src/gui/qtuiview.h @@ -10,33 +10,56 @@ #pragma once +#include "core/qtuidocument.h" + +#include #include class QTableView; class QMdiArea; class QMdiSubWindow; -namespace Core { -class QtUiDocument; -} - namespace Gui { +class FindWidget; + class QtUiView : public QSplitter { Q_OBJECT + public: + enum FindFlag { + NoFindFlags = 0x0, + FindBackward = 0X2, + FindForward = 0x4, + }; + Q_DECLARE_FLAGS(FindFlags, FindFlag) + Q_ENUM(FindFlag) + explicit QtUiView(QWidget *parent = nullptr); + void setFindWidget(FindWidget *findWidget); void setUiDocument(Core::QtUiDocument *document); +public slots: + // Accessible from QML + void search(const QString &searchText, int option = QtUiView::NoFindFlags); + private: + QModelIndexList searchModel(const QString &searchText, const QAbstractItemModel *model) const; void updateView(); QTableView *const m_tableView; QMdiArea *const m_previewArea; Core::QtUiDocument *m_document = nullptr; QMdiSubWindow *m_previewWindow = nullptr; + // Search feature + QModelIndexList m_currentSearchResult; + QString m_currentSearchText; + int m_currentSearchIndex = 0; + bool m_initialSearchProcessed = false; }; } // namespace Gui + +Q_DECLARE_OPERATORS_FOR_FLAGS(Gui::QtUiView::FindFlags) diff --git a/src/rcui/rcfileview.cpp b/src/rcui/rcfileview.cpp index 4fc61212..0312ca1b 100644 --- a/src/rcui/rcfileview.cpp +++ b/src/rcui/rcfileview.cpp @@ -1,4 +1,4 @@ -/* +/* This file is part of Knut. SPDX-FileCopyrightText: 2024 Klarälvdalens Datakonsult AB, a KDAB Group company @@ -13,6 +13,7 @@ #include "assetmodel.h" #include "datamodel.h" #include "dialogmodel.h" +#include "gui/highlightsearchdelegate.h" #include "includemodel.h" #include "menumodel.h" #include "rccore/rcfile.h" @@ -25,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -56,11 +58,27 @@ RcFileView::RcFileView(QWidget *parent) ui->dataView->setSortingEnabled(true); ui->dataView->setContextMenuPolicy(Qt::CustomContextMenu); ui->dataView->setModel(m_dataProxyModel); + ui->dataView->setItemDelegate(new Gui::HighlightSearchDelegate(this)); connect(ui->dataView, &QTreeView::customContextMenuRequested, this, [this](const QPoint &pos) { slotContextMenu(ui->dataView, pos); }); connect(ui->dataView->selectionModel(), &QItemSelectionModel::currentChanged, this, &RcFileView::changeDataItem); + connect(ui->dataView->selectionModel(), &QItemSelectionModel::currentChanged, this, [this] { + searchView(View::Content); + }); connect(ui->dataView, &QTreeView::doubleClicked, this, &RcFileView::previewData); + connect(ui->dataSearch, &QLineEdit::textChanged, this, [this] { + searchView(View::Data); + }); + connect(ui->dataSearch, &QLineEdit::returnPressed, this, [this] { + viewFindNext(View::Data); + }); + connect(ui->dataFindPrevious, &QPushButton::clicked, this, [this] { + viewFindPrevious(View::Data); + }); + connect(ui->dataFindNext, &QPushButton::clicked, this, [this] { + viewFindNext(View::Data); + }); m_dataProxyModel->setRecursiveFilteringEnabled(true); connect(ui->dataFilter, &QLineEdit::textChanged, m_dataProxyModel, &QSortFilterProxyModel::setFilterFixedString); @@ -73,10 +91,24 @@ RcFileView::RcFileView(QWidget *parent) ui->contentView->setContextMenuPolicy(Qt::CustomContextMenu); ui->contentView->setModel(m_contentProxyModel); + ui->contentView->setItemDelegate(new Gui::HighlightSearchDelegate(this)); connect(ui->contentView, &QTreeView::customContextMenuRequested, this, [this](const QPoint &pos) { slotContextMenu(ui->contentView, pos); }); + connect(ui->contentSearch, &QLineEdit::textChanged, this, [this] { + searchView(View::Content); + }); + connect(ui->contentSearch, &QLineEdit::returnPressed, this, [this] { + viewFindNext(View::Content); + }); + connect(ui->contentFindPrevious, &QPushButton::clicked, this, [this] { + viewFindPrevious(View::Content); + }); + connect(ui->contentFindNext, &QPushButton::clicked, this, [this] { + viewFindNext(View::Content); + }); + m_contentProxyModel->setRecursiveFilteringEnabled(true); connect(ui->contentFilter, &QLineEdit::textChanged, m_contentProxyModel, &QSortFilterProxyModel::setFilterFixedString); @@ -87,10 +119,12 @@ RcFileView::RcFileView(QWidget *parent) auto findNext = new QShortcut(QKeySequence(QKeySequence::FindNext), this); findNext->setContext(Qt::WidgetWithChildrenShortcut); connect(findNext, &QShortcut::activated, this, &RcFileView::slotSearchNext); + connect(ui->findNextText, &QPushButton::clicked, this, &RcFileView::slotSearchNext); auto findPrevious = new QShortcut(QKeySequence(QKeySequence::FindPrevious), this); findPrevious->setContext(Qt::WidgetWithChildrenShortcut); connect(findPrevious, &QShortcut::activated, this, &RcFileView::slotSearchPrevious); + connect(ui->findPreviousText, &QPushButton::clicked, this, &RcFileView::slotSearchPrevious); connect(ui->languageCombo, &QComboBox::currentTextChanged, this, &RcFileView::languageChanged); } @@ -305,4 +339,189 @@ const RcCore::Data &RcFileView::data() const return const_cast(m_rcFile)->data[ui->languageCombo->currentText()]; } +void RcFileView::searchView(View view) +{ + const ViewData data = viewData(view); + QModelIndexList currentSearchResult; + // Highlight results. + // offset: The flat representation of the content treeView model displays its data indented with offset value. + static_cast(data.treeView->itemDelegate()) + ->setSearchText(data.searchText, data.offset); + // Update display. + data.treeView->viewport()->update(); + // Search + currentSearchResult = searchModel(view); + if (view == View::Content) { + // Store the search result + m_viewSearchData.contentCurrentSearchResult = currentSearchResult; + // Update the search text + m_viewSearchData.contentCurrentSearchText = data.searchText; + // Reset the search index + m_viewSearchData.contentCurrentSearchIndex = 0; + } else { // View::Data + m_viewSearchData.dataCurrentSearchResult = currentSearchResult; + m_viewSearchData.dataCurrentSearchText = data.searchText; + m_viewSearchData.dataCurrentSearchIndex = 0; + } + + // No hits + if (currentSearchResult.isEmpty()) + return; + + if (data.searchText.isEmpty()) { + // Do not select Empty results. + data.treeView->clearSelection(); + return; + } + + // Select the item that matched as the current index. + data.treeView->selectionModel()->select(QItemSelection(currentSearchResult[0], currentSearchResult[0]), + QItemSelectionModel::ClearAndSelect); + data.treeView->scrollTo(currentSearchResult[0], QAbstractItemView::EnsureVisible); + // Update view. + if (view == View::Data) + Q_EMIT data.treeView->selectionModel()->currentChanged(currentSearchResult[0], currentSearchResult[0]); +} + +void RcFileView::viewFindPrevious(View view) +{ + const ViewData data = viewData(view); + int index = data.currentSearchIndex - 1; + if (data.currentSearchResult.isEmpty()) { + searchView(view); + } else { + // If the index point to the first match continue with the last match. + index = (index >= 0) ? index : data.currentSearchResult.count() - 1; + // Select the Item + data.treeView->selectionModel()->select( + QItemSelection(data.currentSearchResult[index], data.currentSearchResult[index]), + QItemSelectionModel::ClearAndSelect); + data.treeView->scrollTo(data.currentSearchResult[index], QAbstractItemView::EnsureVisible); + + if (view == View::Content) { + // Update current index + m_viewSearchData.contentCurrentSearchIndex = index; + } else { // Data view. + // Update view. + Q_EMIT ui->dataView->selectionModel()->currentChanged(data.currentSearchResult[index], + data.currentSearchResult[index]); + // Update current index + m_viewSearchData.dataCurrentSearchIndex = index; + } + } +} + +void RcFileView::viewFindNext(View view) +{ + const ViewData data = viewData(view); + int index = data.currentSearchIndex + 1; + if (data.currentSearchResult.isEmpty()) { + searchView(view); + } else { + // if it is last match continue with the first match. + index = (index < data.currentSearchResult.count()) ? index : 0; + // Select the item + data.treeView->selectionModel()->select( + QItemSelection(data.currentSearchResult[index], data.currentSearchResult[index]), + QItemSelectionModel::ClearAndSelect); + data.treeView->scrollTo(data.currentSearchResult[index], QAbstractItemView::EnsureVisible); + if (view == View::Content) { + // Update current index + m_viewSearchData.contentCurrentSearchIndex = index; + } else { // Data view. + // Update view. + Q_EMIT ui->dataView->selectionModel()->currentChanged(data.currentSearchResult[index], + data.currentSearchResult[index]); + // Update current index + m_viewSearchData.dataCurrentSearchIndex = index; + } + } +} + +RcFileView::ViewData RcFileView::viewData(View view) +{ + QTreeView *treeView = nullptr; + QString searchText; + QModelIndexList currentSearchResult; + int currentSearchIndex; + int offset; + + switch (view) { + case View::Content: + treeView = ui->contentView; + searchText = ui->contentSearch->text(); + currentSearchResult = m_viewSearchData.contentCurrentSearchResult; + currentSearchIndex = m_viewSearchData.contentCurrentSearchIndex; + offset = 3; + break; + case View::Data: + treeView = ui->dataView; + searchText = ui->dataSearch->text(); + currentSearchResult = m_viewSearchData.dataCurrentSearchResult; + currentSearchIndex = m_viewSearchData.dataCurrentSearchIndex; + offset = 3; + } + + Q_ASSERT(treeView); + + return ViewData {treeView, searchText, offset, currentSearchResult, currentSearchIndex}; +} + +QModelIndexList RcFileView::searchModel(const View &view) const +{ + const QAbstractItemModel *model = nullptr; + QString searchText; + + switch (view) { + case View::Content: + model = ui->contentView->model(); + searchText = ui->contentSearch->text(); + break; + case View::Data: + model = ui->dataView->model(); + searchText = ui->dataSearch->text(); + } + + Q_ASSERT(model); + + // Retrieve and store the indexes which match the search (root and child indexes). + QModelIndexList searchResults; + for (int row = 0; row < model->rowCount(); ++row) { + const QModelIndex rootIndex = model->index(row, 0); + if (hasMatch(rootIndex, searchText)) { + searchResults.append(rootIndex); + } + + // Iterate through the child indexes (tree model) + for (int childRow = 0; childRow < model->rowCount(rootIndex); ++childRow) { + for (int column = 0; column < model->columnCount(); ++column) { + const QModelIndex childIndex = model->index(childRow, column, rootIndex); + if (hasMatch(childIndex, searchText)) { + searchResults.append(childIndex); + } + } + } + // Iterarate through the columns (flat model) + for (int column = 0; column < model->columnCount(); ++column) { + const QModelIndex index = model->index(row, column); + if (!searchResults.contains(index) && hasMatch(index, searchText)) { + searchResults.append(index); + } + } + } + return searchResults; +} + +bool RcFileView::hasMatch(const QModelIndex &index, const QString &searchText) const +{ + if (index.isValid()) { + const QString data = index.data().toString(); + if ((searchText.isEmpty() && data == searchText) + || (!searchText.isEmpty() && data.contains(searchText, Qt::CaseInsensitive))) { + return true; + } + } + return false; +} + } // namespace RcUi diff --git a/src/rcui/rcfileview.h b/src/rcui/rcfileview.h index 9f722ce4..5fe1f16b 100644 --- a/src/rcui/rcfileview.h +++ b/src/rcui/rcfileview.h @@ -10,6 +10,7 @@ #pragma once +#include #include #include @@ -45,6 +46,28 @@ class RcFileView : public QWidget void languageChanged(const QString &language); private: + enum class View { Content, Data }; + struct ViewSearchData + { + // Content View + QModelIndexList contentCurrentSearchResult; + QString contentCurrentSearchText; + int contentCurrentSearchIndex = 0; + // Data View + QModelIndexList dataCurrentSearchResult; + QString dataCurrentSearchText; + int dataCurrentSearchIndex = 0; + }; + + struct ViewData + { + QTreeView *treeView; + QString searchText; + int offset = 0; + QModelIndexList currentSearchResult; + int currentSearchIndex = 0; + }; + void changeDataItem(const QModelIndex ¤t); void changeContentItem(const QModelIndex ¤t); void setData(int type, int index); @@ -52,12 +75,22 @@ class RcFileView : public QWidget void previewData(const QModelIndex &index); void slotContextMenu(QTreeView *treeView, const QPoint &pos); + // Text view search. void highlightLine(int line); void slotSearchText(const QString &text); void slotSearchNext(); void slotSearchPrevious(); + // Views search (Content or Data view). + void searchView(View view); + void viewFindPrevious(View view); + void viewFindNext(View view); const RcCore::Data &data() const; + // Dearch content or data views model. + QModelIndexList searchModel(const View &view) const; + // Convenience functions (avoid code repetition) + bool hasMatch(const QModelIndex &index, const QString &searchText) const; + ViewData viewData(View view); private: std::unique_ptr ui; @@ -65,6 +98,8 @@ class RcFileView : public QWidget QSortFilterProxyModel *const m_dataProxyModel; QSortFilterProxyModel *const m_contentProxyModel; QAbstractItemModel *m_contentModel = nullptr; + // Search data members for both content and data views + ViewSearchData m_viewSearchData; }; } // namespace RcUi diff --git a/src/rcui/rcfileview.ui b/src/rcui/rcfileview.ui index f9ae2ae1..49618c9a 100644 --- a/src/rcui/rcfileview.ui +++ b/src/rcui/rcfileview.ui @@ -6,7 +6,7 @@ 0 0 - 1024 + 1210 768 @@ -36,17 +36,17 @@ - 275 + 400 0 - 275 + 400 16777215 - + 0 @@ -59,10 +59,10 @@ 0 - + - + true @@ -72,8 +72,14 @@ - + + + + 1 + 0 + + Filter @@ -82,13 +88,43 @@ + + + + + 1 + 0 + + + + Search + + + true + + + + + + + Find Previous + + + + + + + Find Next + + + - QTabWidget::TabPosition::East + QTabWidget::North 0 @@ -100,7 +136,7 @@ Data - + 0 @@ -113,7 +149,7 @@ 0 - + @@ -131,14 +167,26 @@ - + + + + 800 + 0 + + + + QAbstractItemView::SelectRows + + + 20 + false - + Filter @@ -148,13 +196,43 @@ + + + + + 1 + 0 + + + + Search + + + true + + + + + + + Find Previous + + + + + + + Find Next + + + Text - + 0 @@ -167,15 +245,21 @@ 0 - + true - + + + + 1 + 0 + + Search @@ -184,6 +268,20 @@ + + + + Find Previous + + + + + + + Find Next + + +