From 542ae528113dc4d39a0bf0407435b721c6f6d9b6 Mon Sep 17 00:00:00 2001 From: leka1986 Date: Wed, 22 Jul 2026 22:16:18 +0200 Subject: [PATCH 1/5] Add filtered-only view --- src/ui/include/crawlerwidget.h | 6 +++ src/ui/src/crawlerwidget.cpp | 76 ++++++++++++++++++++++++++++----- tests/ui/crawlerwidget_test.cpp | 72 +++++++++++++++++++++++++++++++ 3 files changed, 144 insertions(+), 10 deletions(-) diff --git a/src/ui/include/crawlerwidget.h b/src/ui/include/crawlerwidget.h index 7fe1a4dd6..aa28da3aa 100644 --- a/src/ui/include/crawlerwidget.h +++ b/src/ui/include/crawlerwidget.h @@ -233,6 +233,9 @@ class CrawlerWidget : public QSplitter, // Called when the user change the visibility combobox void changeFilteredViewVisibility( int index ); + // Show only the filtered view, hiding the unfiltered top view + void setFilteredOnly( bool filteredOnly ); + // Called when the user add the string to the search void addToSearch( const QString& string ); @@ -394,6 +397,9 @@ class CrawlerWidget : public QSplitter, QToolButton* inverseButton_; QToolButton* booleanButton_; QToolButton* searchRefreshButton_; + QToolButton* filteredOnlyButton_; + + QList normalSplitterSizes_; std::map shortcuts_; diff --git a/src/ui/src/crawlerwidget.cpp b/src/ui/src/crawlerwidget.cpp index c8d86f0f3..969895c78 100644 --- a/src/ui/src/crawlerwidget.cpp +++ b/src/ui/src/crawlerwidget.cpp @@ -90,7 +90,7 @@ class CrawlerWidgetContext : public ViewContextInterface { // Construct from the value passsed CrawlerWidgetContext( QList sizes, bool ignoreCase, bool autoRefresh, bool followFile, bool useRegexp, bool inverseRegexp, bool useBooleanCombination, - QList markedLines ) + bool filteredOnly, QList markedLines ) : sizes_( sizes ) , ignoreCase_( ignoreCase ) , autoRefresh_( autoRefresh ) @@ -98,6 +98,7 @@ class CrawlerWidgetContext : public ViewContextInterface { , useRegexp_( useRegexp ) , inverseRegexp_( inverseRegexp ) , useBooleanCombination_( useBooleanCombination ) + , filteredOnly_( filteredOnly ) { std::transform( markedLines.cbegin(), markedLines.cend(), std::back_inserter( marks_ ), []( const auto& m ) { return m.get(); } ); @@ -136,6 +137,10 @@ class CrawlerWidgetContext : public ViewContextInterface { { return useBooleanCombination_; } + bool filteredOnly() const + { + return filteredOnly_; + } QList marks() const { @@ -155,6 +160,7 @@ class CrawlerWidgetContext : public ViewContextInterface { bool useRegexp_; bool inverseRegexp_; bool useBooleanCombination_; + bool filteredOnly_; QList marks_; }; @@ -175,7 +181,7 @@ LineNumber CrawlerWidget::getTopLine() const QString CrawlerWidget::getSelectedText() const { - if ( filteredView_->hasFocus() ) + if ( filteredOnlyButton_->isChecked() || filteredView_->hasFocus() ) return filteredView_->getSelectedText(); else return logMainView_->getSelectedText(); @@ -183,7 +189,7 @@ QString CrawlerWidget::getSelectedText() const bool CrawlerWidget::isPartialSelection() const { - if ( filteredView_->hasFocus() ) + if ( filteredOnlyButton_->isChecked() || filteredView_->hasFocus() ) return filteredView_->isPartialSelection(); else return logMainView_->isPartialSelection(); @@ -340,11 +346,13 @@ void CrawlerWidget::doSetViewContext( const QString& view_context ) const auto context = CrawlerWidgetContext{ view_context }; - setSizes( context.sizes() ); + normalSplitterSizes_ = context.sizes(); + setSizes( normalSplitterSizes_ ); matchCaseButton_->setChecked( !context.ignoreCase() ); useRegexpButton_->setChecked( context.useRegexp() ); inverseButton_->setChecked( context.inverseRegexp() ); booleanButton_->setChecked( context.useBooleanCombination() ); + filteredOnlyButton_->setChecked( context.filteredOnly() ); searchRefreshButton_->setChecked( context.autoRefresh() ); // Manually call the handler as it is not called when changing the state programmatically @@ -360,10 +368,13 @@ void CrawlerWidget::doSetViewContext( const QString& view_context ) std::shared_ptr CrawlerWidget::doGetViewContext() const { + const auto splitterSizes + = filteredOnlyButton_->isChecked() ? normalSplitterSizes_ : sizes(); auto context = std::make_shared( - sizes(), ( !matchCaseButton_->isChecked() ), searchRefreshButton_->isChecked(), + splitterSizes, ( !matchCaseButton_->isChecked() ), searchRefreshButton_->isChecked(), logMainView_->isFollowEnabled(), useRegexpButton_->isChecked(), inverseButton_->isChecked(), - booleanButton_->isChecked(), logFilteredData_->getMarks() ); + booleanButton_->isChecked(), filteredOnlyButton_->isChecked(), + logFilteredData_->getMarks() ); return static_cast>( context ); } @@ -728,7 +739,10 @@ void CrawlerWidget::loadingFinishedHandler( LoadingStatus status ) for ( const auto& m : savedMarkedLines_ ) { logFilteredData_->addMark( m ); } - logMainView_->setFocus(); + if ( filteredOnlyButton_->isChecked() ) + filteredView_->setFocus(); + else + logMainView_->setFocus(); } loadingInProgress_ = false; @@ -770,8 +784,9 @@ AbstractLogView* CrawlerWidget::activeView() const return view; } else { - LOG_WARNING << "No active view, defaulting to logMainView"; - return logMainView_; + LOG_WARNING << "No active view, defaulting to visible log view"; + return filteredOnlyButton_->isChecked() ? static_cast( filteredView_ ) + : static_cast( logMainView_ ); } } @@ -840,6 +855,20 @@ void CrawlerWidget::changeFilteredViewVisibility( int index ) } } +void CrawlerWidget::setFilteredOnly( bool filteredOnly ) +{ + if ( filteredOnly ) { + normalSplitterSizes_ = sizes(); + logMainView_->hide(); + filteredView_->setFocus(); + } + else { + logMainView_->show(); + setSizes( normalSplitterSizes_ ); + logMainView_->setFocus(); + } +} + void CrawlerWidget::setSearchPatternFromPredefinedFilters( const QList& filters ) { QString searchPattern; @@ -1074,6 +1103,14 @@ void CrawlerWidget::setup() searchRefreshButton_->setFocusPolicy( Qt::NoFocus ); searchRefreshButton_->setContentsMargins( 2, 2, 2, 2 ); + filteredOnlyButton_ = new QToolButton(); + filteredOnlyButton_->setText( tr( "Filtered only" ) ); + filteredOnlyButton_->setToolTip( + tr( "Hide the unfiltered view and expand the filtered results" ) ); + filteredOnlyButton_->setCheckable( true ); + filteredOnlyButton_->setFocusPolicy( Qt::NoFocus ); + filteredOnlyButton_->setContentsMargins( 2, 2, 2, 2 ); + // Construct the Search line searchLineCompleter_ = new QCompleter( savedSearches_->recentSearches(), this ); searchLineEdit_ = new QComboBox; @@ -1128,6 +1165,7 @@ void CrawlerWidget::setup() searchLineLayout->setContentsMargins( 2, 2, 2, 2 ); searchLineLayout->addWidget( visibilityBox_ ); + searchLineLayout->addWidget( filteredOnlyButton_ ); searchLineLayout->addWidget( matchCaseButton_ ); searchLineLayout->addWidget( useRegexpButton_ ); searchLineLayout->addWidget( inverseButton_ ); @@ -1172,6 +1210,7 @@ void CrawlerWidget::setup() // Default splitter position (usually overridden by the config file) setSizes( config.splitterSizes() ); + normalSplitterSizes_ = sizes(); registerShortcuts(); loadIcons(); @@ -1203,6 +1242,9 @@ void CrawlerWidget::setup() connect( visibilityBox_, QOverload::of( &QComboBox::currentIndexChanged ), this, &CrawlerWidget::changeFilteredViewVisibility ); + connect( filteredOnlyButton_, &QToolButton::toggled, this, + &CrawlerWidget::setFilteredOnly ); + connect( logMainView_, &LogMainView::newSelection, [ this ]( auto ) { logMainView_->update(); } ); @@ -1333,7 +1375,8 @@ void CrawlerWidget::saveSplitterSizes() const { LOG_INFO << "Saving default splitter size"; auto& splitterConfig = Configuration::get(); - splitterConfig.setSplitterSizes( sizes() ); + splitterConfig.setSplitterSizes( + filteredOnlyButton_->isChecked() ? normalSplitterSizes_ : sizes() ); splitterConfig.save(); } @@ -1723,6 +1766,10 @@ void CrawlerWidget::updateEncoding() // Change the respective size of the two views void CrawlerWidget::changeTopViewSize( int32_t delta ) { + if ( filteredOnlyButton_->isChecked() ) { + return; + } + int min, max; getRange( 1, &min, &max ); LOG_DEBUG << "CrawlerWidget::changeTopViewSize " << sizes().at( 0 ) << " " << min << " " << max; @@ -1865,6 +1912,7 @@ void CrawlerWidgetContext::loadFromString( const QString& string ) } useRegexp_ = Configuration::get().mainRegexpType() == SearchRegexpType::ExtendedRegexp; + filteredOnly_ = false; } void CrawlerWidgetContext::loadFromJson( const QString& json ) @@ -1902,6 +1950,13 @@ void CrawlerWidgetContext::loadFromJson( const QString& json ) useBooleanCombination_ = false; } + if ( properties.contains( "FO" ) ) { + filteredOnly_ = properties.value( "FO" ).toBool(); + } + else { + filteredOnly_ = false; + } + if ( properties.contains( "M" ) ) { const auto marks = properties.value( "M" ).toList(); for ( const auto& m : marks ) { @@ -1929,6 +1984,7 @@ QString CrawlerWidgetContext::toString() const properies[ "RE" ] = useRegexp_; properies[ "IR" ] = inverseRegexp_; properies[ "BC" ] = useBooleanCombination_; + properies[ "FO" ] = filteredOnly_; properies[ "M" ] = toVariantList( marks_ ); return QJsonDocument::fromVariant( properies ).toJson( QJsonDocument::Compact ); diff --git a/tests/ui/crawlerwidget_test.cpp b/tests/ui/crawlerwidget_test.cpp index fec033149..655b3dea6 100644 --- a/tests/ui/crawlerwidget_test.cpp +++ b/tests/ui/crawlerwidget_test.cpp @@ -135,6 +135,42 @@ struct CrawlerWidget::access_by { } } + void enableFilteredOnly() + { + if ( !crawler->filteredOnlyButton_->isChecked() ) { + QTest::mouseClick( crawler->filteredOnlyButton_, Qt::LeftButton ); + QTest::qWait( 100 ); + } + } + + void disableFilteredOnly() + { + if ( crawler->filteredOnlyButton_->isChecked() ) { + QTest::mouseClick( crawler->filteredOnlyButton_, Qt::LeftButton ); + QTest::qWait( 100 ); + } + } + + bool isFilteredOnlyEnabled() const + { + return crawler->filteredOnlyButton_->isChecked(); + } + + bool isMainViewHidden() const + { + return crawler->logMainView_->isHidden(); + } + + QString viewContext() const + { + return crawler->doGetViewContext()->toString(); + } + + void restoreViewContext( const QString& context ) + { + crawler->doSetViewContext( context ); + } + void runSearch() { QTest::mouseClick( crawler->searchButton_, Qt::LeftButton ); @@ -266,3 +302,39 @@ SCENARIO( "Crawler widget search", "[ui]" ) } } } + +SCENARIO( "Crawler widget filtered-only view", "[ui]" ) +{ + QTemporaryFile file{ "crawler_filtered_only_test_XXXXXX" }; + REQUIRE( generateDataFiles( file ) ); + + Session session; + CrawlerWidgetVisitor crawlerVisitor; + crawlerVisitor.crawler.reset( static_cast( + session.open( file.fileName(), []() { return new CrawlerWidget(); } ) ) ); + + waitUiState( [ & ]() { return crawlerVisitor.getLogNbLines().get() == SL_NB_LINES; } ); + waitUiState( [ & ]() { return crawlerVisitor.isLoadingFinished(); } ); + crawlerVisitor.render(); + + REQUIRE_FALSE( crawlerVisitor.isFilteredOnlyEnabled() ); + REQUIRE_FALSE( crawlerVisitor.isMainViewHidden() ); + + crawlerVisitor.enableFilteredOnly(); + + REQUIRE( crawlerVisitor.isFilteredOnlyEnabled() ); + REQUIRE( crawlerVisitor.isMainViewHidden() ); + + const auto savedContext = crawlerVisitor.viewContext(); + REQUIRE( savedContext.contains( "\"FO\":true" ) ); + + crawlerVisitor.disableFilteredOnly(); + + REQUIRE_FALSE( crawlerVisitor.isFilteredOnlyEnabled() ); + REQUIRE_FALSE( crawlerVisitor.isMainViewHidden() ); + + crawlerVisitor.restoreViewContext( savedContext ); + + REQUIRE( crawlerVisitor.isFilteredOnlyEnabled() ); + REQUIRE( crawlerVisitor.isMainViewHidden() ); +} From 2f5b67928400f771aba49597390346c53ebee67d Mon Sep 17 00:00:00 2001 From: leka1986 Date: Wed, 22 Jul 2026 22:20:50 +0200 Subject: [PATCH 2/5] Update CI artifact and cache actions --- .github/actions/agent-setup/action.yml | 4 ++-- .github/workflows/ci-build.yml | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/actions/agent-setup/action.yml b/.github/actions/agent-setup/action.yml index 7669e2de1..67b7242f7 100644 --- a/.github/actions/agent-setup/action.yml +++ b/.github/actions/agent-setup/action.yml @@ -8,7 +8,7 @@ runs: - name: Cache Qt id: cache-qt - uses: actions/cache@v1 + uses: actions/cache@v4 with: path: ../Qt key: ${{ matrix.config.os }}-${{ matrix.config.arch }}-${{ matrix.config.qt_version }}-QtCache @@ -30,7 +30,7 @@ runs: echo "BOOST_URL=https://sourceforge.net/projects/boost/files/boost/1.86.0/boost_1_86_0.tar.bz2/download" >> $GITHUB_ENV - name: Restore Boost cache - uses: actions/cache@v2 + uses: actions/cache@v4 id: cache-boost with: path: ${{env.BOOST_ROOT}} diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 27090b427..4ea7b321f 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -34,7 +34,7 @@ jobs: - name: Save version run: echo $KLOGG_VERSION > klogg_version.txt - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: klogg_version path: 'klogg_version.txt' @@ -136,7 +136,7 @@ jobs: - uses: ./.github/actions/docker-package # Final upload of all packages - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: packages-${{ matrix.config.artifacts_id }} path: '${{ env.KLOGG_BUILD_ROOT }}/packages/*' @@ -197,7 +197,7 @@ jobs: notarization-team: ${{ secrets.NOTARIZATION_TEAM }} notarization-password: ${{ secrets.NOTARIZATION_PASSWORD }} - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: packages-${{ matrix.config.artifacts_id }} path: '${{ env.KLOGG_BUILD_ROOT }}/packages/*' @@ -233,7 +233,7 @@ jobs: - name: Cache openssl id: cache-openssl - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ${{ github.workspace }}\openssl-1.1 key: OpensslCache-1-1-1w @@ -268,7 +268,7 @@ jobs: s3-bucket: ${{ secrets.WIN_CS_BUCKET }} # Final upload of all packages - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: packages-${{ matrix.config.artifacts_id }} path: '${{ env.KLOGG_BUILD_ROOT }}/packages/*' From 95fd2851946b9010cb512b007c5a0023905ab458 Mon Sep 17 00:00:00 2001 From: leka1986 Date: Wed, 22 Jul 2026 22:25:55 +0200 Subject: [PATCH 3/5] Support current CMake policy floor --- .github/actions/agent-build/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/agent-build/action.yml b/.github/actions/agent-build/action.yml index e1d823a17..ecb8d86d0 100644 --- a/.github/actions/agent-build/action.yml +++ b/.github/actions/agent-build/action.yml @@ -15,7 +15,7 @@ runs: run: | mkdir $KLOGG_BUILD_ROOT cd $KLOGG_BUILD_ROOT - cmake $KLOGG_CMAKE_OPTS -DCPM_SOURCE_CACHE=$KLOGG_WORKSPACE/cpm_cache -DKLOGG_OVERRIDE_MALLOC=OFF $KLOGG_WORKSPACE + cmake $KLOGG_CMAKE_OPTS -DCMAKE_POLICY_VERSION_MINIMUM=3.5 -DCPM_SOURCE_CACHE=$KLOGG_WORKSPACE/cpm_cache -DKLOGG_OVERRIDE_MALLOC=OFF $KLOGG_WORKSPACE - name: build shell: sh From 5d619cc8ab0c5a973cc7b4f4c8d0c7e9ada0afdb Mon Sep 17 00:00:00 2001 From: leka1986 Date: Wed, 22 Jul 2026 22:39:48 +0200 Subject: [PATCH 4/5] Fix Windows packaging on current MSVC --- packaging/windows/prepare_release.cmd | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/packaging/windows/prepare_release.cmd b/packaging/windows/prepare_release.cmd index 9b5338bae..8df9020d6 100644 --- a/packaging/windows/prepare_release.cmd +++ b/packaging/windows/prepare_release.cmd @@ -12,15 +12,8 @@ xcopy %KLOGG_WORKSPACE%\%KLOGG_BUILD_ROOT%\output\klogg.pdb %KLOGG_WORKSPACE%\re xcopy %KLOGG_WORKSPACE%\%KLOGG_BUILD_ROOT%\output\klogg_crashpad_handler.exe %KLOGG_WORKSPACE%\release\ /y xcopy %KLOGG_WORKSPACE%\%KLOGG_BUILD_ROOT%\output\klogg_minidump_dump.exe %KLOGG_WORKSPACE%\release\ /y -xcopy %KLOGG_WORKSPACE%\%KLOGG_BUILD_ROOT%\msvc_19.41_cxx17_64_md_relwithdebinfo\tbb12.dll %KLOGG_WORKSPACE%\release\ /y -xcopy %KLOGG_WORKSPACE%\%KLOGG_BUILD_ROOT%\msvc_19.41_cxx17_64_md_relwithdebinfo\tbb12.pdb %KLOGG_WORKSPACE%\release\ /y -xcopy %KLOGG_WORKSPACE%\%KLOGG_BUILD_ROOT%\msvc_19.41_cxx17_32_md_relwithdebinfo\tbb12.dll %KLOGG_WORKSPACE%\release\ /y -xcopy %KLOGG_WORKSPACE%\%KLOGG_BUILD_ROOT%\msvc_19.41_cxx17_32_md_relwithdebinfo\tbb12.pdb %KLOGG_WORKSPACE%\release\ /y - -xcopy %KLOGG_WORKSPACE%\%KLOGG_BUILD_ROOT%\msvc_19.42_cxx17_64_md_relwithdebinfo\tbb12.dll %KLOGG_WORKSPACE%\release\ /y -xcopy %KLOGG_WORKSPACE%\%KLOGG_BUILD_ROOT%\msvc_19.42_cxx17_64_md_relwithdebinfo\tbb12.pdb %KLOGG_WORKSPACE%\release\ /y -xcopy %KLOGG_WORKSPACE%\%KLOGG_BUILD_ROOT%\msvc_19.42_cxx17_32_md_relwithdebinfo\tbb12.dll %KLOGG_WORKSPACE%\release\ /y -xcopy %KLOGG_WORKSPACE%\%KLOGG_BUILD_ROOT%\msvc_19.42_cxx17_32_md_relwithdebinfo\tbb12.pdb %KLOGG_WORKSPACE%\release\ /y +for /r "%KLOGG_WORKSPACE%\%KLOGG_BUILD_ROOT%" %%F in (tbb12.dll) do xcopy "%%F" %KLOGG_WORKSPACE%\release\ /y +for /r "%KLOGG_WORKSPACE%\%KLOGG_BUILD_ROOT%" %%F in (tbb12.pdb) do xcopy "%%F" %KLOGG_WORKSPACE%\release\ /y xcopy %KLOGG_WORKSPACE%\%KLOGG_BUILD_ROOT%\generated\documentation.html %KLOGG_WORKSPACE%\release\ /y xcopy %KLOGG_WORKSPACE%\COPYING %KLOGG_WORKSPACE%\release\ /y From 7f267b2bc21efd3aa5fd768436e08180aa46acbf Mon Sep 17 00:00:00 2001 From: leka1986 Date: Wed, 22 Jul 2026 23:32:01 +0200 Subject: [PATCH 5/5] Add persistent top-view filters --- src/ui/include/crawlerwidget.h | 10 +- src/ui/include/logmainview.h | 59 +++++++- src/ui/include/predefinedfilters.h | 5 +- src/ui/src/abstractlogview.cpp | 15 +- src/ui/src/crawlerwidget.cpp | 137 ++++++++--------- src/ui/src/logmainview.cpp | 194 ++++++++++++++++++++++++- src/ui/src/predefinedfilters.cpp | 4 +- src/ui/src/predefinedfiltersdialog.cpp | 17 ++- tests/ui/CMakeLists.txt | 3 +- tests/ui/crawlerwidget_test.cpp | 90 +++++------- tests/ui/predefinedfilters_test.cpp | 72 +++++++++ 11 files changed, 447 insertions(+), 159 deletions(-) create mode 100644 tests/ui/predefinedfilters_test.cpp diff --git a/src/ui/include/crawlerwidget.h b/src/ui/include/crawlerwidget.h index aa28da3aa..57ffc2f7d 100644 --- a/src/ui/include/crawlerwidget.h +++ b/src/ui/include/crawlerwidget.h @@ -233,9 +233,6 @@ class CrawlerWidget : public QSplitter, // Called when the user change the visibility combobox void changeFilteredViewVisibility( int index ); - // Show only the filtered view, hiding the unfiltered top view - void setFilteredOnly( bool filteredOnly ); - // Called when the user add the string to the search void addToSearch( const QString& string ); @@ -338,7 +335,7 @@ class CrawlerWidget : public QSplitter, void updatePredefinedFiltersWidget(); // Reload predefined filters after changing settings - void reloadPredefinedFilters() const; + void reloadPredefinedFilters(); QString escapeSearchPattern( const QString& searchPattern, bool isRegex = false ) const; QString& combinePatterns( QString& currentPattern, const QString& newPattern ) const; @@ -362,6 +359,7 @@ class CrawlerWidget : public QSplitter, SavedSearches* savedSearches_ = nullptr; std::shared_ptr logData_; + std::unique_ptr logMainViewData_; std::shared_ptr logFilteredData_; // Matches overview @@ -397,10 +395,6 @@ class CrawlerWidget : public QSplitter, QToolButton* inverseButton_; QToolButton* booleanButton_; QToolButton* searchRefreshButton_; - QToolButton* filteredOnlyButton_; - - QList normalSplitterSizes_; - std::map shortcuts_; // Default palette to be remembered diff --git a/src/ui/include/logmainview.h b/src/ui/include/logmainview.h index 2c75f2bc3..694222dab 100644 --- a/src/ui/include/logmainview.h +++ b/src/ui/include/logmainview.h @@ -40,20 +40,72 @@ #ifndef LOGMAINVIEW_H #define LOGMAINVIEW_H +#include + #include "abstractlogview.h" #include "logdata.h" +#include "logfiltereddata.h" + +class LogMainViewData : public AbstractLogData +{ + Q_OBJECT + + public: + explicit LogMainViewData( const LogData* sourceLogData ); + + void setTopFilter( const RegularExpressionPattern& pattern ); + void prepareFullRefresh(); + void refreshSearch(); + void interruptSearch(); + + LineNumber getSourceLineNumber( LineNumber index ) const; + LineNumber getLineIndexNumber( LineNumber sourceLine ) const; + LinesCount getNbTotalLines() const; + + Q_SIGNALS: + void searchProgressed( LinesCount nbMatches, int progress, LineNumber initialLine ); + + protected: + QString doGetLineString( LineNumber line ) const override; + QString doGetExpandedLineString( LineNumber line ) const override; + klogg::vector doGetLines( LineNumber firstLine, LinesCount number ) const override; + klogg::vector doGetExpandedLines( LineNumber firstLine, + LinesCount number ) const override; + LineNumber doGetLineNumber( LineNumber index ) const override; + LinesCount doGetNbLine() const override; + LineLength doGetMaxLength() const override; + LineLength doGetLineLength( LineNumber line ) const override; + void doSetDisplayEncoding( const char* encoding ) override; + QTextCodec* doGetDisplayEncoding() const override; + void doAttachReader() const override; + void doDetachReader() const override; + + private: + const AbstractLogData* activeData() const; + + const LogData* sourceLogData_; + std::unique_ptr filteredData_; + RegularExpressionPattern filterPattern_; + bool filterEnabled_ = false; + bool fullRefreshRequired_ = false; +}; // Class implementing the main (top) view widget. class LogMainView : public AbstractLogView { Q_OBJECT public: - LogMainView( const LogData* newLogData, + LogMainView( LogMainViewData* newLogData, const QuickFindPattern* const quickFindPattern, Overview* overview, OverviewWidget* overview_widget, QWidget* parent = nullptr ); + LineNumber getTopSourceLine() const; + void trySelectSourceLine( LineNumber sourceLine ); + void selectSourcePortionAndDisplayLine( LineNumber sourceLine, LinesCount nLines, + LineColumn startCol, LineLength nSymbols ); + // Configure the view to use the passed filtered list // (used for couloured bullets) // Should be NULL or the empty LFD if no filtering is used @@ -63,10 +115,15 @@ class LogMainView : public AbstractLogView // Implements the virtual function LogData::LineType lineType( LineNumber lineNumber ) const override; + LineNumber displayLineNumber( LineNumber lineNumber ) const override; + LineNumber lineIndex( LineNumber lineNumber ) const override; + LineNumber maxDisplayLineNumber() const override; + void doRegisterShortcuts() override; private: LogFilteredData* filteredData_; + LogMainViewData* logMainViewData_; }; #endif diff --git a/src/ui/include/predefinedfilters.h b/src/ui/include/predefinedfilters.h index 09983b1be..dd9c6208e 100644 --- a/src/ui/include/predefinedfilters.h +++ b/src/ui/include/predefinedfilters.h @@ -49,6 +49,7 @@ struct PredefinedFilter { QString name; QString pattern; bool useRegex; + bool filterTop = false; }; // Represents collection of filters read from settings file. @@ -70,11 +71,11 @@ class PredefinedFiltersCollection final : public PersistableupdateCurrentPosition( firstLine_, lastLine ); + if ( lastLineNumber > 0_lnum ) { + const auto firstSourceLine = displayLineNumber( firstLine_ ) - 1_lcount; + const auto lastSourceLine = lastLine > firstLine_ + ? displayLineNumber( lastLine - 1_lcount ) + : firstSourceLine; + overview_->updateCurrentPosition( firstSourceLine, lastSourceLine ); + } + else { + overview_->updateCurrentPosition( 0_lnum, 0_lnum ); + } } forceRefresh(); diff --git a/src/ui/src/crawlerwidget.cpp b/src/ui/src/crawlerwidget.cpp index 969895c78..b17a059f8 100644 --- a/src/ui/src/crawlerwidget.cpp +++ b/src/ui/src/crawlerwidget.cpp @@ -90,7 +90,7 @@ class CrawlerWidgetContext : public ViewContextInterface { // Construct from the value passsed CrawlerWidgetContext( QList sizes, bool ignoreCase, bool autoRefresh, bool followFile, bool useRegexp, bool inverseRegexp, bool useBooleanCombination, - bool filteredOnly, QList markedLines ) + QList markedLines ) : sizes_( sizes ) , ignoreCase_( ignoreCase ) , autoRefresh_( autoRefresh ) @@ -98,7 +98,6 @@ class CrawlerWidgetContext : public ViewContextInterface { , useRegexp_( useRegexp ) , inverseRegexp_( inverseRegexp ) , useBooleanCombination_( useBooleanCombination ) - , filteredOnly_( filteredOnly ) { std::transform( markedLines.cbegin(), markedLines.cend(), std::back_inserter( marks_ ), []( const auto& m ) { return m.get(); } ); @@ -137,11 +136,6 @@ class CrawlerWidgetContext : public ViewContextInterface { { return useBooleanCombination_; } - bool filteredOnly() const - { - return filteredOnly_; - } - QList marks() const { return marks_; @@ -160,8 +154,6 @@ class CrawlerWidgetContext : public ViewContextInterface { bool useRegexp_; bool inverseRegexp_; bool useBooleanCombination_; - bool filteredOnly_; - QList marks_; }; @@ -176,12 +168,12 @@ CrawlerWidget::CrawlerWidget( QWidget* parent ) // The top line is first one on the main display LineNumber CrawlerWidget::getTopLine() const { - return logMainView_->getTopLine(); + return logMainView_->getTopSourceLine(); } QString CrawlerWidget::getSelectedText() const { - if ( filteredOnlyButton_->isChecked() || filteredView_->hasFocus() ) + if ( filteredView_->hasFocus() ) return filteredView_->getSelectedText(); else return logMainView_->getSelectedText(); @@ -189,7 +181,7 @@ QString CrawlerWidget::getSelectedText() const bool CrawlerWidget::isPartialSelection() const { - if ( filteredOnlyButton_->isChecked() || filteredView_->hasFocus() ) + if ( filteredView_->hasFocus() ) return filteredView_->isPartialSelection(); else return logMainView_->isPartialSelection(); @@ -215,9 +207,26 @@ bool CrawlerWidget::isTextWrapEnabled() const return logMainView_->isTextWrapEnabled(); } -void CrawlerWidget::reloadPredefinedFilters() const +void CrawlerWidget::reloadPredefinedFilters() { predefinedFilters_->populatePredefinedFilters(); + + QStringList topPatterns; + const auto filters = PredefinedFiltersCollection::getSynced().getFilters(); + for ( const auto& filter : filters ) { + if ( !filter.filterTop ) { + continue; + } + + const auto pattern + = filter.useRegex ? filter.pattern : QRegularExpression::escape( filter.pattern ); + topPatterns.append( QString( "(?:%1)" ).arg( pattern ) ); + } + + const RegularExpressionPattern topFilterPattern{ topPatterns.join( '|' ), false, true, false, + false }; + logMainViewData_->setTopFilter( topFilterPattern ); + logMainView_->updateData(); } QString CrawlerWidget::encodingText() const @@ -265,6 +274,7 @@ void CrawlerWidget::changeEvent( QEvent* event ) void CrawlerWidget::stopLoading() { + logMainViewData_->interruptSearch(); logFilteredData_->interruptSearch(); logData_->interruptLoading(); } @@ -272,6 +282,7 @@ void CrawlerWidget::stopLoading() void CrawlerWidget::reload() { searchState_.resetState(); + logMainViewData_->prepareFullRefresh(); constexpr auto DropCache = true; logFilteredData_->clearSearch( DropCache ); logFilteredData_->clearMarks(); @@ -312,7 +323,7 @@ void CrawlerWidget::goToLine() const auto selectedLine = LineNumber( static_cast( newLine - 1 ) ); filteredView_->trySelectLine( logFilteredData_->getLineIndexNumber( selectedLine ) ); - logMainView_->trySelectLine( selectedLine ); + logMainView_->trySelectSourceLine( selectedLine ); } } @@ -346,13 +357,11 @@ void CrawlerWidget::doSetViewContext( const QString& view_context ) const auto context = CrawlerWidgetContext{ view_context }; - normalSplitterSizes_ = context.sizes(); - setSizes( normalSplitterSizes_ ); + setSizes( context.sizes() ); matchCaseButton_->setChecked( !context.ignoreCase() ); useRegexpButton_->setChecked( context.useRegexp() ); inverseButton_->setChecked( context.inverseRegexp() ); booleanButton_->setChecked( context.useBooleanCombination() ); - filteredOnlyButton_->setChecked( context.filteredOnly() ); searchRefreshButton_->setChecked( context.autoRefresh() ); // Manually call the handler as it is not called when changing the state programmatically @@ -368,13 +377,10 @@ void CrawlerWidget::doSetViewContext( const QString& view_context ) std::shared_ptr CrawlerWidget::doGetViewContext() const { - const auto splitterSizes - = filteredOnlyButton_->isChecked() ? normalSplitterSizes_ : sizes(); auto context = std::make_shared( - splitterSizes, ( !matchCaseButton_->isChecked() ), searchRefreshButton_->isChecked(), + sizes(), ( !matchCaseButton_->isChecked() ), searchRefreshButton_->isChecked(), logMainView_->isFollowEnabled(), useRegexpButton_->isChecked(), inverseButton_->isChecked(), - booleanButton_->isChecked(), filteredOnlyButton_->isChecked(), - logFilteredData_->getMarks() ); + booleanButton_->isChecked(), logFilteredData_->getMarks() ); return static_cast>( context ); } @@ -559,8 +565,8 @@ void CrawlerWidget::jumpToMatchingLine( LineNumber filteredLineNb, LinesCount nL LineColumn startCol, LineLength nSymbols ) { const auto mainViewLine = logFilteredData_->getMatchingLineNumber( filteredLineNb ); - logMainView_->selectPortionAndDisplayLine( mainViewLine, nLines, startCol, - nSymbols ); // FIXME: should be done with a signal. + logMainView_->selectSourcePortionAndDisplayLine( + mainViewLine, nLines, startCol, nSymbols ); // FIXME: should be done with a signal. } void CrawlerWidget::updateLineNumberHandler( LineNumber line, LinesCount nLines, @@ -706,6 +712,8 @@ void CrawlerWidget::loadingFinishedHandler( LoadingStatus status ) // overview have probably changed. overview_.updateData( logData_->getNbLine() ); + logMainViewData_->refreshSearch(); + // FIXME, handle topLine // logMainView_->updateData( logData_, topLine ); logMainView_->updateData(); @@ -739,10 +747,7 @@ void CrawlerWidget::loadingFinishedHandler( LoadingStatus status ) for ( const auto& m : savedMarkedLines_ ) { logFilteredData_->addMark( m ); } - if ( filteredOnlyButton_->isChecked() ) - filteredView_->setFocus(); - else - logMainView_->setFocus(); + logMainView_->setFocus(); } loadingInProgress_ = false; @@ -753,6 +758,8 @@ void CrawlerWidget::fileChangedHandler( MonitoredFileStatus status ) { // Handle the case where the file has been truncated if ( status == MonitoredFileStatus::Truncated ) { + logMainViewData_->prepareFullRefresh(); + // Clear all marks (TODO offer the option to keep them) logFilteredData_->clearMarks(); if ( !searchInfoLine_->text().isEmpty() ) { @@ -784,9 +791,8 @@ AbstractLogView* CrawlerWidget::activeView() const return view; } else { - LOG_WARNING << "No active view, defaulting to visible log view"; - return filteredOnlyButton_->isChecked() ? static_cast( filteredView_ ) - : static_cast( logMainView_ ); + LOG_WARNING << "No active view, defaulting to logMainView"; + return logMainView_; } } @@ -855,20 +861,6 @@ void CrawlerWidget::changeFilteredViewVisibility( int index ) } } -void CrawlerWidget::setFilteredOnly( bool filteredOnly ) -{ - if ( filteredOnly ) { - normalSplitterSizes_ = sizes(); - logMainView_->hide(); - filteredView_->setFocus(); - } - else { - logMainView_->show(); - setSizes( normalSplitterSizes_ ); - logMainView_->setFocus(); - } -} - void CrawlerWidget::setSearchPatternFromPredefinedFilters( const QList& filters ) { QString searchPattern; @@ -997,8 +989,10 @@ void CrawlerWidget::setup() bottomWindow->setContentsMargins( 2, 0, 2, 0 ); overviewWidget_ = new OverviewWidget(); + logMainViewData_ = std::make_unique( logData_.get() ); logMainView_ - = new LogMainView( logData_.get(), quickFindPattern_.get(), &overview_, overviewWidget_ ); + = new LogMainView( logMainViewData_.get(), quickFindPattern_.get(), &overview_, + overviewWidget_ ); logMainView_->setContentsMargins( 2, 0, 2, 0 ); filteredView_ = new FilteredView( logFilteredData_.get(), quickFindPattern_.get() ); @@ -1103,14 +1097,6 @@ void CrawlerWidget::setup() searchRefreshButton_->setFocusPolicy( Qt::NoFocus ); searchRefreshButton_->setContentsMargins( 2, 2, 2, 2 ); - filteredOnlyButton_ = new QToolButton(); - filteredOnlyButton_->setText( tr( "Filtered only" ) ); - filteredOnlyButton_->setToolTip( - tr( "Hide the unfiltered view and expand the filtered results" ) ); - filteredOnlyButton_->setCheckable( true ); - filteredOnlyButton_->setFocusPolicy( Qt::NoFocus ); - filteredOnlyButton_->setContentsMargins( 2, 2, 2, 2 ); - // Construct the Search line searchLineCompleter_ = new QCompleter( savedSearches_->recentSearches(), this ); searchLineEdit_ = new QComboBox; @@ -1165,7 +1151,6 @@ void CrawlerWidget::setup() searchLineLayout->setContentsMargins( 2, 2, 2, 2 ); searchLineLayout->addWidget( visibilityBox_ ); - searchLineLayout->addWidget( filteredOnlyButton_ ); searchLineLayout->addWidget( matchCaseButton_ ); searchLineLayout->addWidget( useRegexpButton_ ); searchLineLayout->addWidget( inverseButton_ ); @@ -1210,7 +1195,6 @@ void CrawlerWidget::setup() // Default splitter position (usually overridden by the config file) setSizes( config.splitterSizes() ); - normalSplitterSizes_ = sizes(); registerShortcuts(); loadIcons(); @@ -1242,16 +1226,25 @@ void CrawlerWidget::setup() connect( visibilityBox_, QOverload::of( &QComboBox::currentIndexChanged ), this, &CrawlerWidget::changeFilteredViewVisibility ); - connect( filteredOnlyButton_, &QToolButton::toggled, this, - &CrawlerWidget::setFilteredOnly ); - connect( logMainView_, &LogMainView::newSelection, [ this ]( auto ) { logMainView_->update(); } ); connect( logMainView_, &LogMainView::newSelection, this, - &CrawlerWidget::updateLineNumberHandler ); - - connect( logMainView_, &LogMainView::markLines, this, &CrawlerWidget::markLinesFromMain ); + [ this ]( LineNumber line, LinesCount nLines, LineColumn startCol, + LineLength nSymbols ) { + updateLineNumberHandler( logMainViewData_->getSourceLineNumber( line ), nLines, + startCol, nSymbols ); + } ); + + connect( logMainView_, &LogMainView::markLines, this, + [ this ]( const klogg::vector& lines ) { + klogg::vector sourceLines( lines.size() ); + std::transform( lines.cbegin(), lines.cend(), sourceLines.begin(), + [ this ]( const auto& line ) { + return logMainViewData_->getSourceLineNumber( line ); + } ); + markLinesFromMain( sourceLines ); + } ); connect( logMainView_, &LogMainView::highlightersChange, this, &CrawlerWidget::applyConfiguration ); @@ -1295,6 +1288,10 @@ void CrawlerWidget::setup() connect( logFilteredData_.get(), &LogFilteredData::searchProgressed, this, &CrawlerWidget::updateFilteredView, Qt::QueuedConnection ); + connect( logMainViewData_.get(), &LogMainViewData::searchProgressed, this, + [ this ]( LinesCount, int, LineNumber ) { logMainView_->updateData(); }, + Qt::QueuedConnection ); + // Sent load file update to MainWindow (for status update) connect( logData_.get(), &LogData::loadingProgressed, this, &CrawlerWidget::loadingProgressed ); connect( logData_.get(), &LogData::loadingFinished, this, @@ -1375,8 +1372,7 @@ void CrawlerWidget::saveSplitterSizes() const { LOG_INFO << "Saving default splitter size"; auto& splitterConfig = Configuration::get(); - splitterConfig.setSplitterSizes( - filteredOnlyButton_->isChecked() ? normalSplitterSizes_ : sizes() ); + splitterConfig.setSplitterSizes( sizes() ); splitterConfig.save(); } @@ -1766,10 +1762,6 @@ void CrawlerWidget::updateEncoding() // Change the respective size of the two views void CrawlerWidget::changeTopViewSize( int32_t delta ) { - if ( filteredOnlyButton_->isChecked() ) { - return; - } - int min, max; getRange( 1, &min, &max ); LOG_DEBUG << "CrawlerWidget::changeTopViewSize " << sizes().at( 0 ) << " " << min << " " << max; @@ -1912,7 +1904,6 @@ void CrawlerWidgetContext::loadFromString( const QString& string ) } useRegexp_ = Configuration::get().mainRegexpType() == SearchRegexpType::ExtendedRegexp; - filteredOnly_ = false; } void CrawlerWidgetContext::loadFromJson( const QString& json ) @@ -1950,13 +1941,6 @@ void CrawlerWidgetContext::loadFromJson( const QString& json ) useBooleanCombination_ = false; } - if ( properties.contains( "FO" ) ) { - filteredOnly_ = properties.value( "FO" ).toBool(); - } - else { - filteredOnly_ = false; - } - if ( properties.contains( "M" ) ) { const auto marks = properties.value( "M" ).toList(); for ( const auto& m : marks ) { @@ -1984,7 +1968,6 @@ QString CrawlerWidgetContext::toString() const properies[ "RE" ] = useRegexp_; properies[ "IR" ] = inverseRegexp_; properies[ "BC" ] = useBooleanCombination_; - properies[ "FO" ] = filteredOnly_; properies[ "M" ] = toVariantList( marks_ ); return QJsonDocument::fromVariant( properies ).toJson( QJsonDocument::Compact ); diff --git a/src/ui/src/logmainview.cpp b/src/ui/src/logmainview.cpp index e6506b982..43f14d6a6 100644 --- a/src/ui/src/logmainview.cpp +++ b/src/ui/src/logmainview.cpp @@ -50,9 +50,152 @@ #include "shortcuts.h" -LogMainView::LogMainView( const LogData* newLogData, const QuickFindPattern* const quickFindPattern, +LogMainViewData::LogMainViewData( const LogData* sourceLogData ) + : sourceLogData_( sourceLogData ) + , filteredData_( sourceLogData->getNewFilteredData() ) +{ + connect( filteredData_.get(), &LogFilteredData::searchProgressed, this, + &LogMainViewData::searchProgressed ); +} + +void LogMainViewData::setTopFilter( const RegularExpressionPattern& pattern ) +{ + const auto enableFilter = !pattern.pattern.isEmpty(); + if ( filterEnabled_ == enableFilter && filterPattern_.pattern == pattern.pattern + && filterPattern_.isCaseSensitive == pattern.isCaseSensitive ) { + return; + } + + filteredData_->interruptSearch(); + filterPattern_ = pattern; + filterEnabled_ = enableFilter; + fullRefreshRequired_ = false; + + if ( filterEnabled_ ) { + filteredData_->runSearch( filterPattern_ ); + } + else { + filteredData_->clearSearch(); + } +} + +void LogMainViewData::prepareFullRefresh() +{ + if ( filterEnabled_ ) { + constexpr auto DropCache = true; + filteredData_->clearSearch( DropCache ); + fullRefreshRequired_ = true; + } +} + +void LogMainViewData::refreshSearch() +{ + if ( !filterEnabled_ ) { + return; + } + + if ( fullRefreshRequired_ ) { + fullRefreshRequired_ = false; + filteredData_->runSearch( filterPattern_ ); + } + else { + filteredData_->updateSearch( 0_lnum, LineNumber( sourceLogData_->getNbLine().get() ) ); + } +} + +void LogMainViewData::interruptSearch() +{ + filteredData_->interruptSearch(); +} + +LineNumber LogMainViewData::getSourceLineNumber( LineNumber index ) const +{ + return filterEnabled_ ? filteredData_->getMatchingLineNumber( index ) : index; +} + +LineNumber LogMainViewData::getLineIndexNumber( LineNumber sourceLine ) const +{ + return filterEnabled_ ? filteredData_->getLineIndexNumber( sourceLine ) : sourceLine; +} + +LinesCount LogMainViewData::getNbTotalLines() const +{ + return sourceLogData_->getNbLine(); +} + +const AbstractLogData* LogMainViewData::activeData() const +{ + return filterEnabled_ ? static_cast( filteredData_.get() ) + : static_cast( sourceLogData_ ); +} + +QString LogMainViewData::doGetLineString( LineNumber line ) const +{ + return activeData()->getLineString( line ); +} + +QString LogMainViewData::doGetExpandedLineString( LineNumber line ) const +{ + return activeData()->getExpandedLineString( line ); +} + +klogg::vector LogMainViewData::doGetLines( LineNumber firstLine, + LinesCount number ) const +{ + return activeData()->getLines( firstLine, number ); +} + +klogg::vector LogMainViewData::doGetExpandedLines( LineNumber firstLine, + LinesCount number ) const +{ + return activeData()->getExpandedLines( firstLine, number ); +} + +LineNumber LogMainViewData::doGetLineNumber( LineNumber index ) const +{ + return getSourceLineNumber( index ); +} + +LinesCount LogMainViewData::doGetNbLine() const +{ + return activeData()->getNbLine(); +} + +LineLength LogMainViewData::doGetMaxLength() const +{ + return activeData()->getMaxLength(); +} + +LineLength LogMainViewData::doGetLineLength( LineNumber line ) const +{ + return activeData()->getLineLength( line ); +} + +void LogMainViewData::doSetDisplayEncoding( const char* encoding ) +{ + Q_UNUSED( encoding ) +} + +QTextCodec* LogMainViewData::doGetDisplayEncoding() const +{ + return sourceLogData_->getDisplayEncoding(); +} + +void LogMainViewData::doAttachReader() const +{ + sourceLogData_->attachReader(); +} + +void LogMainViewData::doDetachReader() const +{ + sourceLogData_->detachReader(); +} + +LogMainView::LogMainView( LogMainViewData* newLogData, + const QuickFindPattern* const quickFindPattern, Overview* overview, OverviewWidget* overview_widget, QWidget* parent ) : AbstractLogView( newLogData, quickFindPattern, parent ) + , logMainViewData_( newLogData ) { filteredData_ = nullptr; @@ -60,6 +203,27 @@ LogMainView::LogMainView( const LogData* newLogData, const QuickFindPattern* con setOverview( overview, overview_widget ); } +LineNumber LogMainView::getTopSourceLine() const +{ + return logMainViewData_->getSourceLineNumber( getTopLine() ); +} + +void LogMainView::trySelectSourceLine( LineNumber sourceLine ) +{ + if ( logMainViewData_->getNbLine() > 0_lcount ) { + trySelectLine( logMainViewData_->getLineIndexNumber( sourceLine ) ); + } +} + +void LogMainView::selectSourcePortionAndDisplayLine( LineNumber sourceLine, LinesCount nLines, + LineColumn startCol, LineLength nSymbols ) +{ + if ( logMainViewData_->getNbLine() > 0_lcount ) { + selectPortionAndDisplayLine( logMainViewData_->getLineIndexNumber( sourceLine ), nLines, + startCol, nSymbols ); + } +} + // Just update our internal record. void LogMainView::useNewFiltering( LogFilteredData* filteredData ) { @@ -74,25 +238,43 @@ void LogMainView::useNewFiltering( LogFilteredData* filteredData ) AbstractLogData::LineType LogMainView::lineType( LineNumber lineNumber ) const { if ( filteredData_ ) { - return filteredData_->lineTypeByLine( lineNumber ); + return filteredData_->lineTypeByLine( + logMainViewData_->getSourceLineNumber( lineNumber ) ); } return AbstractLogData::LineTypeFlags::Plain; } +LineNumber LogMainView::displayLineNumber( LineNumber lineNumber ) const +{ + return logMainViewData_->getSourceLineNumber( lineNumber ) + 1_lcount; +} + +LineNumber LogMainView::lineIndex( LineNumber lineNumber ) const +{ + return logMainViewData_->getLineIndexNumber( lineNumber ); +} + +LineNumber LogMainView::maxDisplayLineNumber() const +{ + return LineNumber( logMainViewData_->getNbTotalLines().get() ); +} + void LogMainView::doRegisterShortcuts() { LOG_INFO << "Registering shortcuts for main view"; AbstractLogView::doRegisterShortcuts(); registerShortcut( ShortcutAction::LogViewNextMark, [ this ] { - const auto line = filteredData_->getMarkAfter( getViewPosition() ); + const auto line = filteredData_->getMarkAfter( + logMainViewData_->getSourceLineNumber( getViewPosition() ) ); if ( line.has_value() ) { - selectAndDisplayLine( *line ); + trySelectSourceLine( *line ); } } ); registerShortcut( ShortcutAction::LogViewPrevMark, [ this ] { - const auto line = filteredData_->getMarkBefore( getViewPosition() ); + const auto line = filteredData_->getMarkBefore( + logMainViewData_->getSourceLineNumber( getViewPosition() ) ); if ( line.has_value() ) { - selectAndDisplayLine( *line ); + trySelectSourceLine( *line ); } } ); } diff --git a/src/ui/src/predefinedfilters.cpp b/src/ui/src/predefinedfilters.cpp index f83a84c29..b6866eed1 100644 --- a/src/ui/src/predefinedfilters.cpp +++ b/src/ui/src/predefinedfilters.cpp @@ -57,7 +57,8 @@ void PredefinedFiltersCollection::retrieveFromStorage( QSettings& settings ) filters_.push_back( { settings.value( "name" ).toString(), settings.value( "filter" ).toString(), - settings.value( "regex", true ).toBool() } ); + settings.value( "regex", true ).toBool(), + settings.value( "filterTop", false ).toBool() } ); } settings.endArray(); } @@ -84,6 +85,7 @@ void PredefinedFiltersCollection::saveToStorage( QSettings& settings ) const settings.setValue( "name", filter.name ); settings.setValue( "filter", filter.pattern ); settings.setValue( "regex", filter.useRegex ); + settings.setValue( "filterTop", filter.filterTop ); arrayIndex++; } diff --git a/src/ui/src/predefinedfiltersdialog.cpp b/src/ui/src/predefinedfiltersdialog.cpp index 7884f69ea..9f3da1d98 100644 --- a/src/ui/src/predefinedfiltersdialog.cpp +++ b/src/ui/src/predefinedfiltersdialog.cpp @@ -153,10 +153,11 @@ void PredefinedFiltersDialog::populateFiltersTable( filtersTableWidget->clear(); filtersTableWidget->setRowCount( static_cast( filters.size() ) ); - filtersTableWidget->setColumnCount( 3 ); + filtersTableWidget->setColumnCount( 4 ); filtersTableWidget->setHorizontalHeaderLabels( QStringList() << tr( "Name" ) << tr( "Pattern" ) - << tr( "Regex" ) ); + << tr( "Regex" ) + << tr( "Filter Top" ) ); int filterIndex = 0; for ( const auto& filter : filters ) { @@ -165,12 +166,17 @@ void PredefinedFiltersDialog::populateFiltersTable( auto* regexCheckbox = new CenteredCheckbox; regexCheckbox->setChecked( filter.useRegex ); filtersTableWidget->setCellWidget( filterIndex, 2, regexCheckbox ); + auto* filterTopCheckbox = new CenteredCheckbox; + filterTopCheckbox->setChecked( filter.filterTop ); + filtersTableWidget->setCellWidget( filterIndex, 3, filterTopCheckbox ); filterIndex++; } filtersTableWidget->horizontalHeader()->setSectionResizeMode( 0, QHeaderView::ResizeToContents ); filtersTableWidget->horizontalHeader()->setSectionResizeMode( 1, QHeaderView::Stretch ); + filtersTableWidget->horizontalHeader()->setSectionResizeMode( 2, QHeaderView::ResizeToContents ); + filtersTableWidget->horizontalHeader()->setSectionResizeMode( 3, QHeaderView::ResizeToContents ); filtersTableWidget->verticalHeader()->setSectionResizeMode( QHeaderView::ResizeToContents ); filtersTableWidget->setWordWrap( false ); @@ -200,9 +206,12 @@ PredefinedFiltersCollection::Collection PredefinedFiltersDialog::readFiltersTabl const auto useRegexCheckbox = static_cast( filtersTableWidget->cellWidget( i, 2 ) ); const auto useRegex = useRegexCheckbox ? useRegexCheckbox->isChecked() : false; + const auto filterTopCheckbox + = static_cast( filtersTableWidget->cellWidget( i, 3 ) ); + const auto filterTop = filterTopCheckbox ? filterTopCheckbox->isChecked() : false; if ( !name.isEmpty() && !value.isEmpty() ) { - currentFilters.push_back( { name, value, useRegex } ); + currentFilters.push_back( { name, value, useRegex, filterTop } ); } } @@ -222,6 +231,8 @@ void PredefinedFiltersDialog::addFilterRow( const QString& newFilter ) filtersTableWidget->setItem( newRow, 0, new QTableWidgetItem( "" ) ); auto regexCheckBox = new CenteredCheckbox; filtersTableWidget->setCellWidget( newRow, 2, regexCheckBox ); + auto filterTopCheckBox = new CenteredCheckbox; + filtersTableWidget->setCellWidget( newRow, 3, filterTopCheckBox ); filtersTableWidget->scrollToItem( filtersTableWidget->item( newRow, 0 ) ); filtersTableWidget->setCurrentCell( newRow, 0 ); diff --git a/tests/ui/CMakeLists.txt b/tests/ui/CMakeLists.txt index c126c12e0..adbe9846f 100644 --- a/tests/ui/CMakeLists.txt +++ b/tests/ui/CMakeLists.txt @@ -3,6 +3,7 @@ set(UI_TEST_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/logdata_test.cpp ${CMAKE_CURRENT_SOURCE_DIR}/logfiltereddata_test.cpp ${CMAKE_CURRENT_SOURCE_DIR}/crawlerwidget_test.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/predefinedfilters_test.cpp ) if(NOT APPLE) @@ -19,4 +20,4 @@ add_backward(klogg_itests) add_test( NAME klogg_itests COMMAND klogg_itests -platform offscreen -) \ No newline at end of file +) diff --git a/tests/ui/crawlerwidget_test.cpp b/tests/ui/crawlerwidget_test.cpp index 655b3dea6..b484c50df 100644 --- a/tests/ui/crawlerwidget_test.cpp +++ b/tests/ui/crawlerwidget_test.cpp @@ -61,7 +61,6 @@ bool generateDataFiles( QTemporaryFile& file ) return true; } - } // namespace struct CrawlerWidgetPrivate { @@ -86,6 +85,27 @@ struct CrawlerWidget::access_by { return crawler->logFilteredData_->getNbLine(); } + LinesCount getTopViewNbLines() + { + return crawler->logMainViewData_->getNbLine(); + } + + QString getTopViewLine( LineNumber line ) + { + return crawler->logMainViewData_->getLineString( line ); + } + + LineNumber getTopViewSourceLine( LineNumber line ) + { + return crawler->logMainViewData_->getSourceLineNumber( line ); + } + + void setTopFilter( const QString& pattern ) + { + crawler->logMainViewData_->setTopFilter( + RegularExpressionPattern{ pattern, false, true, false, false } ); + } + void selectAllInMainView() { crawler->logMainView_->selectAll(); @@ -135,42 +155,6 @@ struct CrawlerWidget::access_by { } } - void enableFilteredOnly() - { - if ( !crawler->filteredOnlyButton_->isChecked() ) { - QTest::mouseClick( crawler->filteredOnlyButton_, Qt::LeftButton ); - QTest::qWait( 100 ); - } - } - - void disableFilteredOnly() - { - if ( crawler->filteredOnlyButton_->isChecked() ) { - QTest::mouseClick( crawler->filteredOnlyButton_, Qt::LeftButton ); - QTest::qWait( 100 ); - } - } - - bool isFilteredOnlyEnabled() const - { - return crawler->filteredOnlyButton_->isChecked(); - } - - bool isMainViewHidden() const - { - return crawler->logMainView_->isHidden(); - } - - QString viewContext() const - { - return crawler->doGetViewContext()->toString(); - } - - void restoreViewContext( const QString& context ) - { - crawler->doSetViewContext( context ); - } - void runSearch() { QTest::mouseClick( crawler->searchButton_, Qt::LeftButton ); @@ -303,9 +287,9 @@ SCENARIO( "Crawler widget search", "[ui]" ) } } -SCENARIO( "Crawler widget filtered-only view", "[ui]" ) +SCENARIO( "Top filters are independent from bottom searches", "[ui]" ) { - QTemporaryFile file{ "crawler_filtered_only_test_XXXXXX" }; + QTemporaryFile file{ "crawler_top_filter_test_XXXXXX" }; REQUIRE( generateDataFiles( file ) ); Session session; @@ -315,26 +299,18 @@ SCENARIO( "Crawler widget filtered-only view", "[ui]" ) waitUiState( [ & ]() { return crawlerVisitor.getLogNbLines().get() == SL_NB_LINES; } ); waitUiState( [ & ]() { return crawlerVisitor.isLoadingFinished(); } ); - crawlerVisitor.render(); - - REQUIRE_FALSE( crawlerVisitor.isFilteredOnlyEnabled() ); - REQUIRE_FALSE( crawlerVisitor.isMainViewHidden() ); - - crawlerVisitor.enableFilteredOnly(); - - REQUIRE( crawlerVisitor.isFilteredOnlyEnabled() ); - REQUIRE( crawlerVisitor.isMainViewHidden() ); - - const auto savedContext = crawlerVisitor.viewContext(); - REQUIRE( savedContext.contains( "\"FO\":true" ) ); - crawlerVisitor.disableFilteredOnly(); + crawlerVisitor.setTopFilter( "line 00000[0-4]" ); - REQUIRE_FALSE( crawlerVisitor.isFilteredOnlyEnabled() ); - REQUIRE_FALSE( crawlerVisitor.isMainViewHidden() ); + waitUiState( [ & ]() { return crawlerVisitor.getTopViewNbLines().get() == 95; } ); + REQUIRE( crawlerVisitor.getTopViewSourceLine( 0_lnum ) == 5_lnum ); + REQUIRE( crawlerVisitor.getTopViewLine( 0_lnum ).contains( "line 000005" ) ); - crawlerVisitor.restoreViewContext( savedContext ); + crawlerVisitor.setSearchPattern( "line" ); + crawlerVisitor.runSearch(); - REQUIRE( crawlerVisitor.isFilteredOnlyEnabled() ); - REQUIRE( crawlerVisitor.isMainViewHidden() ); + waitUiState( [ & ]() { + return crawlerVisitor.getLogFilteredNbLines().get() == SL_NB_LINES; + } ); + REQUIRE( crawlerVisitor.getTopViewNbLines().get() == 95 ); } diff --git a/tests/ui/predefinedfilters_test.cpp b/tests/ui/predefinedfilters_test.cpp new file mode 100644 index 000000000..f69233eff --- /dev/null +++ b/tests/ui/predefinedfilters_test.cpp @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2026 klogg contributors + * + * This file is part of klogg. + * + * klogg is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ + +#include + +#include +#include + +#include "predefinedfilters.h" + +SCENARIO( "Top filter settings are persisted", "[ui]" ) +{ + QTemporaryFile file{ "predefined_filters_test_XXXXXX.conf" }; + REQUIRE( file.open() ); + file.close(); + + GIVEN( "a saved top filter" ) + { + QSettings settings{ file.fileName(), QSettings::IniFormat }; + PredefinedFiltersCollection savedCollection; + savedCollection.setFilters( { { "Noise", "group change option", false, true } } ); + savedCollection.saveToStorage( settings ); + settings.sync(); + + WHEN( "the filters are loaded" ) + { + PredefinedFiltersCollection loadedCollection; + loadedCollection.retrieveFromStorage( settings ); + const auto filters = loadedCollection.getFilters(); + + THEN( "the top filter flag is restored" ) + { + REQUIRE( filters.size() == 1 ); + REQUIRE( filters.front().filterTop ); + } + } + } +} + +SCENARIO( "Older predefined filters remain compatible", "[ui]" ) +{ + QTemporaryFile file{ "predefined_filters_legacy_test_XXXXXX.conf" }; + REQUIRE( file.open() ); + file.close(); + + QSettings settings{ file.fileName(), QSettings::IniFormat }; + settings.beginGroup( "PredefinedFiltersCollection" ); + settings.setValue( "version", 2 ); + settings.beginWriteArray( "filters" ); + settings.setArrayIndex( 0 ); + settings.setValue( "name", "Existing filter" ); + settings.setValue( "filter", "ShipTrail.lua" ); + settings.setValue( "regex", false ); + settings.endArray(); + settings.endGroup(); + settings.sync(); + + PredefinedFiltersCollection collection; + collection.retrieveFromStorage( settings ); + const auto filters = collection.getFilters(); + + REQUIRE( filters.size() == 1 ); + REQUIRE_FALSE( filters.front().filterTop ); +}