From 22fc3ca33347d2daca5e5167c208b14d238e793d Mon Sep 17 00:00:00 2001 From: Anton Filimonov Date: Wed, 20 Nov 2024 21:20:03 +0100 Subject: [PATCH] fix: improve highlight drawing (#534, #585) --- src/ui/include/highlightedmatch.h | 133 +++++++++++++++++++++++++++++- src/ui/include/highlighterset.h | 2 +- src/ui/src/abstractlogview.cpp | 120 +++++++++++++-------------- src/ui/src/highlighterset.cpp | 41 +++++---- 4 files changed, 208 insertions(+), 88 deletions(-) diff --git a/src/ui/include/highlightedmatch.h b/src/ui/include/highlightedmatch.h index 8ccefc26..7fdbf408 100644 --- a/src/ui/include/highlightedmatch.h +++ b/src/ui/include/highlightedmatch.h @@ -20,12 +20,14 @@ #ifndef KLOGG_HIGHLIGHTEDMATCH_H #define KLOGG_HIGHLIGHTEDMATCH_H +#include "containers.h" #include "linetypes.h" #include +#include // Represents a match result for QuickFind or highlighter class HighlightedMatch { - public: +public: // Construct a match (must be initialised) HighlightedMatch( LineColumn start_column, LineLength size, QColor foreColor, QColor backColor ) : startColumn_{ start_column } @@ -40,6 +42,11 @@ class HighlightedMatch { return startColumn_; } + LineColumn endColumn() const + { + return size_ > 0_length ? startColumn_ + size_ - 1_length : startColumn_; + } + LineLength size() const { return size_; @@ -55,7 +62,7 @@ class HighlightedMatch { return backColor_; } - private: +private: LineColumn startColumn_; LineLength size_; @@ -63,4 +70,126 @@ class HighlightedMatch { QColor backColor_; }; +class HighlightedMatchRanges { + +public: + HighlightedMatchRanges() = default; + explicit HighlightedMatchRanges( klogg::vector matches ) + : matches_( std::move( matches ) ) + { + } + + klogg::vector matches() const + { + return matches_; + } + + void clear() + { + matches_.clear(); + } + + bool empty() const + { + return matches_.empty(); + } + + const HighlightedMatch& front() const + { + return matches_.front(); + } + + const HighlightedMatch& back() const + { + return matches_.back(); + } + + void clamp( LineColumn firstVisibleColumn, LineColumn lastVisibleColumn ) + { + for ( HighlightedMatch& m : matches_ ) { + if ( m.endColumn() < firstVisibleColumn || m.startColumn() > lastVisibleColumn ) { + m = HighlightedMatch{ m.startColumn(), 0_length, m.foreColor(), m.backColor() }; + continue; + } + + if ( m.startColumn() < firstVisibleColumn ) { + m = HighlightedMatch{ firstVisibleColumn, + m.endColumn() - firstVisibleColumn + 1_length, m.foreColor(), + m.backColor() }; + } + + if ( m.endColumn() > lastVisibleColumn ) { + m = HighlightedMatch{ m.startColumn(), + lastVisibleColumn - m.startColumn() + 1_length, m.foreColor(), + m.backColor() }; + } + } + matches_.erase( + std::remove_if( matches_.begin(), matches_.end(), + []( const HighlightedMatch& m ) { return m.size() == 0_length; } ), + matches_.end() ); + } + + void addMatches( const klogg::vector& patternMatches ) + { + for ( HighlightedMatch m : patternMatches ) { + addMatch( m ); + } + } + + void addMatch( HighlightedMatch newMatch ) + { + for ( auto matchIt = matches_.begin(); matchIt != matches_.end(); ++matchIt ) { + HighlightedMatch& m = *matchIt; + const LineColumn oldMatchL = m.startColumn(); + const LineColumn oldMatchR = m.endColumn(); + const LineColumn newMatchL = newMatch.startColumn(); + const LineColumn newMatchR = newMatch.endColumn(); + + if ( oldMatchR < newMatchL ) { + continue; + } + else if ( newMatchR < oldMatchL ) { + break; + } + else if ( newMatchL <= oldMatchL && newMatchR >= oldMatchR ) { + m = HighlightedMatch{ m.startColumn(), 0_length, m.foreColor(), m.backColor() }; + } + else if ( oldMatchL <= newMatchL && oldMatchR >= newMatchR ) { + m = HighlightedMatch{ m.startColumn(), newMatchL - oldMatchL, m.foreColor(), + m.backColor() }; + + HighlightedMatch tailMatch{ newMatchR + 1_length, oldMatchR - newMatchR, + m.foreColor(), m.backColor() }; + matches_.insert( std::next( matchIt ), tailMatch ); + break; + } + else if ( oldMatchL < newMatchL && oldMatchR < newMatchR ) { + m = HighlightedMatch{ m.startColumn(), newMatchL - oldMatchL, m.foreColor(), + m.backColor() }; + } + else if ( oldMatchL <= newMatchR && oldMatchR > newMatchR ) { + m = HighlightedMatch{ newMatchR + 1_length, oldMatchR - newMatchR, m.foreColor(), + m.backColor() }; + break; + } + } + + matches_.erase( + std::remove_if( matches_.begin(), matches_.end(), + []( const HighlightedMatch& m ) { return m.size() == 0_length; } ), + matches_.end() ); + + auto insertPos + = std::lower_bound( matches_.begin(), matches_.end(), newMatch, + []( const HighlightedMatch& lhs, const HighlightedMatch& rhs ) { + return lhs.startColumn() < rhs.startColumn(); + } ); + matches_.insert( insertPos, newMatch ); + } + +private: + klogg::vector matches_; +}; + #endif // KLOGG_HIGHLIGHTEDMATCH_H diff --git a/src/ui/include/highlighterset.h b/src/ui/include/highlighterset.h index 924a8748..5dc235fe 100644 --- a/src/ui/include/highlighterset.h +++ b/src/ui/include/highlighterset.h @@ -136,7 +136,7 @@ class HighlighterSet { // Returns weither the passed line match a filter of the set, // if so, it returns the fore/back colors the line should use. HighlighterMatchType matchLine( const QString& line, - klogg::vector& matches ) const; + HighlightedMatchRanges& matches ) const; bool isEmpty() const; diff --git a/src/ui/src/abstractlogview.cpp b/src/ui/src/abstractlogview.cpp index 57631df7..c2dd00d9 100644 --- a/src/ui/src/abstractlogview.cpp +++ b/src/ui/src/abstractlogview.cpp @@ -80,6 +80,7 @@ #include "abstractlogview.h" #include "containers.h" +#include "highlightedmatch.h" #include "linetypes.h" #include "active_screen.h" @@ -275,6 +276,16 @@ class LineDrawer { // LOG_INFO << "added Chunk of " << length; } + LineColumn endColumn() const + { + return chunks_.empty() ? 0_lcol : chunks_.back().end(); + } + + bool empty() const + { + return chunks_.empty(); + } + // Draw the current line of text using the given painter, // in the passed block (in pixels) // The line must be cut to fit on the screen. @@ -2336,13 +2347,14 @@ void AbstractLogView::drawTextArea( QPaintDevice* paintDevice ) // Position in pixel of the base line of the line to print int yPos = 0; wrappedLinesInfo_.clear(); + klogg::vector> highlightColors; for ( auto currentLine = 0_lcount; currentLine < nbLines; ++currentLine ) { const auto lineNumber = firstLine_ + currentLine; QString logLine = logLines[ currentLine.get() ]; const int xPos = contentStartPosX + ContentMarginWidth; - klogg::vector highlighterMatches; + HighlightedMatchRanges highlighterMatches; if ( selection_.isLineSelected( lineNumber ) && !selection_.isSingleLine() ) { // Reverse the selected line @@ -2369,16 +2381,14 @@ void AbstractLogView::drawTextArea( QPaintDevice* paintDevice ) if ( patternHighlight ) { klogg::vector patternMatches; patternHighlight->matchLine( logLine, patternMatches ); - highlighterMatches.insert( highlighterMatches.end(), patternMatches.begin(), - patternMatches.end() ); + highlighterMatches.addMatches( patternMatches ); } - highlighterMatches.reserve( additionalHighlighters.size() ); + // highlighterMatches.reserve( additionalHighlighters.size() ); for ( const auto& highlighter : additionalHighlighters ) { klogg::vector patternMatches; highlighter.matchLine( logLine, patternMatches ); - highlighterMatches.insert( highlighterMatches.end(), patternMatches.begin(), - patternMatches.end() ); + highlighterMatches.addMatches( patternMatches ); } } } @@ -2408,27 +2418,27 @@ void AbstractLogView::drawTextArea( QPaintDevice* paintDevice ) match.foreColor(), match.backColor() }; }; - klogg::vector allHighlights; - allHighlights.reserve( highlighterMatches.size() ); - std::transform( highlighterMatches.cbegin(), highlighterMatches.cend(), - std::back_inserter( allHighlights ), untabifyHighlight ); + klogg::vector sortedHighlights = highlighterMatches.matches(); + std::transform( sortedHighlights.begin(), sortedHighlights.end(), sortedHighlights.begin(), + untabifyHighlight ); + + HighlightedMatchRanges allHighlights{ std::move( sortedHighlights ) }; // string to print, cut to fit the length and position of the view - const QString& expandedLine = untabify(std::move(logLine)); + const QString& expandedLine = untabify( std::move( logLine ) ); // Has the line got elements to be highlighted klogg::vector quickFindMatches; quickFindPattern_->matchLine( expandedLine, quickFindMatches ); - allHighlights.insert( allHighlights.end(), - std::make_move_iterator( quickFindMatches.begin() ), - std::make_move_iterator( quickFindMatches.end() ) ); + allHighlights.addMatches( quickFindMatches ); // Is there something selected in the line? const auto selectionPortion = selection_.getPortionForLine( lineNumber ); if ( selectionPortion.isValid() ) { - allHighlights.emplace_back( selectionPortion.startColumn(), selectionPortion.size(), - palette.color( QPalette::HighlightedText ), - palette.color( QPalette::Highlight ) ); + allHighlights.addMatch( HighlightedMatch{ selectionPortion.startColumn(), + selectionPortion.size(), + palette.color( QPalette::HighlightedText ), + palette.color( QPalette::Highlight ) } ); } const auto wrappedLineLength @@ -2442,64 +2452,46 @@ void AbstractLogView::drawTextArea( QPaintDevice* paintDevice ) backColor ); LineDrawer lineDrawer( backColor ); + const auto firstVisibleColumn = std::clamp( useTextWrap_ ? 0_lcol : firstCol_, 0_lcol, + LineColumn{ klogg::isize( expandedLine ) } ); + const auto lastVisibleColumn + = useTextWrap_ ? LineColumn{ klogg::isize( expandedLine ) } : firstCol_ + nbVisibleCols; + allHighlights.clamp( firstVisibleColumn, lastVisibleColumn ); + if ( !allHighlights.empty() && !expandedLine.isEmpty() ) { - auto highlightColors = klogg::vector>( - static_cast( expandedLine.size() ), - std::make_pair( foreColor, backColor ) ); + // first part without highlight + if ( allHighlights.front().startColumn() > firstVisibleColumn ) { + lineDrawer.addChunk( firstVisibleColumn, + allHighlights.front().startColumn() - 1_length, foreColor, + backColor ); + } - for ( const auto& match : allHighlights ) { - auto matchEnd = match.startColumn() + match.size(); + for ( const auto& match : allHighlights.matches() ) { + const auto matchStart = match.startColumn(); + + // a part between two highlight regions + if ( !lineDrawer.empty() && matchStart - lineDrawer.endColumn() > 1_length ) { + lineDrawer.addChunk( lineDrawer.endColumn() + 1_length, matchStart - 1_length, + foreColor, backColor ); + } + + const auto matchEnd = match.endColumn(); auto matchLengthInString = match.size(); if ( matchEnd >= LineColumn{ expandedLine.size() } ) { matchLengthInString = LineLength{ klogg::isize( expandedLine ) - match.startColumn().get() }; } if ( matchLengthInString > 0_length ) { - std::fill_n( highlightColors.begin() + match.startColumn().get(), - matchLengthInString.get(), - std::make_pair( match.foreColor(), match.backColor() ) ); + lineDrawer.addChunk( match.startColumn(), matchEnd, match.foreColor(), + match.backColor() ); } } - klogg::vector columnIndexes( highlightColors.size() ); - std::iota( columnIndexes.begin(), columnIndexes.end(), 0_lcol ); - - auto columnIndexIt = columnIndexes.begin(); - - const auto firstVisibleColumn - = std::clamp( useTextWrap_ ? 0_lcol : firstCol_, 0_lcol, - LineColumn{ klogg::isize( expandedLine ) } ); - std::advance( columnIndexIt, firstVisibleColumn.get() ); - while ( columnIndexIt != columnIndexes.end() ) { - auto highlightDiffColumnIt = std::adjacent_find( - columnIndexIt, columnIndexes.end(), - [ &highlightColors ]( LineColumn lhsColumn, LineColumn rhsColumn ) { - return highlightColors[ lhsColumn.get() ] - != highlightColors[ rhsColumn.get() ]; - } ); - - if ( highlightDiffColumnIt != columnIndexes.end() ) { - auto highlightChunkStart = *columnIndexIt; - auto highlightChunkEnd = *highlightDiffColumnIt; - lineDrawer.addChunk( - highlightChunkStart, highlightChunkEnd, - highlightColors[ highlightChunkStart.get() ].first, - highlightColors[ highlightChunkStart.get() ].second ); - - columnIndexIt = highlightDiffColumnIt + 1; - } - else { - break; - } - } - if ( columnIndexIt != columnIndexes.end() && !columnIndexes.empty() ) { - const auto lastHighlightChunkStart = *columnIndexIt; - const auto lastHighlightChunkEnd = *columnIndexes.rbegin(); - if ( lastHighlightChunkEnd >= lastHighlightChunkStart ) { - lineDrawer.addChunk( lastHighlightChunkStart, lastHighlightChunkEnd, - highlightColors.back().first, - highlightColors.back().second ); - } + // last part without highlight + const auto lastHighlightColumn = allHighlights.back().endColumn(); + if ( lastHighlightColumn < lastVisibleColumn ) { + lineDrawer.addChunk( lastHighlightColumn + 1_length, lastVisibleColumn, foreColor, + backColor ); } } else { diff --git a/src/ui/src/highlighterset.cpp b/src/ui/src/highlighterset.cpp index b6215a9e..fb5cbdce 100644 --- a/src/ui/src/highlighterset.cpp +++ b/src/ui/src/highlighterset.cpp @@ -185,8 +185,9 @@ RegularExpressionPattern Highlighter::expressionPattern() const return result; } -void Highlighter::compile() const { - const auto pattern +void Highlighter::compile() const +{ + const auto pattern = useRegex_ ? regexp_.pattern() : QRegularExpression::escape( regexp_.pattern() ); optimizedRegexp_ = QRegularExpression( pattern, regexp_.patternOptions() ); @@ -197,7 +198,7 @@ bool Highlighter::matchLine( const QString& line, klogg::vector& matches ) const + HighlightedMatchRanges& matches ) const { - if (highlighterList_.empty()) { + if ( highlighterList_.empty() ) { return HighlighterMatchType::NoMatch; } - if (!compiledExpression_) { + if ( !compiledExpression_ ) { compile(); } @@ -286,7 +287,7 @@ HighlighterMatchType HighlighterSet::matchLine( const QString& line, auto matchType = HighlighterMatchType::NoMatch; - for ( int index = static_cast(highlighterList_.size()) - 1; index >= 0; --index ) { + for ( int index = static_cast( highlighterList_.size() ) - 1; index >= 0; --index ) { const Highlighter& hl = highlighterList_[ index ]; if ( !matchedPatterns[ static_cast( index ) ].second ) { continue; @@ -297,20 +298,17 @@ HighlighterMatchType HighlighterSet::matchLine( const QString& line, continue; } - if ( !hl.highlightOnlyMatch() ) { - matchType = HighlighterMatchType::LineMatch; - - matches.clear(); - matches.emplace_back( 0_lcol, LineLength{ line.size() }, hl.foreColor(), - hl.backColor() ); - } - else { + if (hl.highlightOnlyMatch()) { if ( matchType != HighlighterMatchType::LineMatch ) { matchType = HighlighterMatchType::WordMatch; } - - matches.insert( matches.end(), std::make_move_iterator( thisMatches.begin() ), - std::make_move_iterator( thisMatches.end() ) ); + matches.addMatches( thisMatches ); + } + else { + matchType = HighlighterMatchType::LineMatch; + matches.clear(); + matches.addMatch( + { 0_lcol, LineLength{ line.size() }, hl.foreColor(), hl.backColor() } ); } } @@ -436,7 +434,8 @@ void HighlighterSetCollection::setHighlighterSets( const QList& updateCombinedSet(); } -const HighlighterSet& HighlighterSetCollection::currentActiveSet() const { +const HighlighterSet& HighlighterSetCollection::currentActiveSet() const +{ return combinedActiveSet_; } @@ -444,8 +443,8 @@ void HighlighterSetCollection::updateCombinedSet() { combinedActiveSet_.highlighterList_.clear(); - for (const HighlighterSet& set : klogg::as_const(highlighters_)) { - if (!activeSets_.contains(set.id())) { + for ( const HighlighterSet& set : klogg::as_const( highlighters_ ) ) { + if ( !activeSets_.contains( set.id() ) ) { continue; } combinedActiveSet_.highlighterList_.append( set.highlighterList_ );