xref: /glogg/src/abstractlogview.cpp (revision ac6602a5bafa9823a03710ce4fc2a79551a0ce64) !
1 /*
2  * Copyright (C) 2009, 2010, 2011, 2012, 2013, 2015 Nicolas Bonnefon
3  * and other contributors
4  *
5  * This file is part of glogg.
6  *
7  * glogg is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation, either version 3 of the License, or
10  * (at your option) any later version.
11  *
12  * glogg is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with glogg.  If not, see <http://www.gnu.org/licenses/>.
19  */
20 
21 // This file implements the AbstractLogView base class.
22 // Most of the actual drawing and event management common to the two views
23 // is implemented in this class.  The class only calls protected virtual
24 // functions when view specific behaviour is desired, using the template
25 // pattern.
26 
27 #include <iostream>
28 #include <cassert>
29 
30 #include <QApplication>
31 #include <QClipboard>
32 #include <QFile>
33 #include <QRect>
34 #include <QPaintEvent>
35 #include <QPainter>
36 #include <QFontMetrics>
37 #include <QScrollBar>
38 #include <QMenu>
39 #include <QAction>
40 #include <QtCore>
41 
42 #include "log.h"
43 
44 #include "persistentinfo.h"
45 #include "filterset.h"
46 #include "logmainview.h"
47 #include "quickfind.h"
48 #include "quickfindpattern.h"
49 #include "overview.h"
50 #include "configuration.h"
51 
52 namespace {
53 int mapPullToFollowLength( int length );
54 };
55 
56 namespace {
57 
58 int countDigits( quint64 n )
59 {
60     if (n == 0)
61         return 1;
62 
63     // We must force the compiler to not store intermediate results
64     // in registers because this causes incorrect result on some
65     // systems under optimizations level >0. For the skeptical:
66     //
67     // #include <math.h>
68     // #include <stdlib.h>
69     // int main(int argc, char **argv) {
70     //     (void)argc;
71     //     long long int n = atoll(argv[1]);
72     //     return floor( log( n ) / log( 10 ) + 1 );
73     // }
74     //
75     // This is on Thinkpad T60 (Genuine Intel(R) CPU T2300).
76     // $ g++ --version
77     // g++ (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3
78     // $ g++ -O0 -Wall -W -o math math.cpp -lm; ./math 10; echo $?
79     // 2
80     // $ g++ -O1 -Wall -W -o math math.cpp -lm; ./math 10; echo $?
81     // 1
82     //
83     // A fix is to (1) explicitly place intermediate results in
84     // variables *and* (2) [A] mark them as 'volatile', or [B] pass
85     // -ffloat-store to g++ (note that approach [A] is more portable).
86 
87     volatile qreal ln_n  = qLn( n );
88     volatile qreal ln_10 = qLn( 10 );
89     volatile qreal lg_n = ln_n / ln_10;
90     volatile qreal lg_n_1 = lg_n + 1;
91     volatile qreal fl_lg_n_1 = qFloor( lg_n_1 );
92 
93     return fl_lg_n_1;
94 }
95 
96 } // anon namespace
97 
98 
99 LineChunk::LineChunk( int first_col, int last_col, ChunkType type )
100 {
101     // LOG(logDEBUG) << "new LineChunk: " << first_col << " " << last_col;
102 
103     start_ = first_col;
104     end_   = last_col;
105     type_  = type;
106 }
107 
108 QList<LineChunk> LineChunk::select( int sel_start, int sel_end ) const
109 {
110     QList<LineChunk> list;
111 
112     if ( ( sel_start < start_ ) && ( sel_end < start_ ) ) {
113         // Selection BEFORE this chunk: no change
114         list << LineChunk( *this );
115     }
116     else if ( sel_start > end_ ) {
117         // Selection AFTER this chunk: no change
118         list << LineChunk( *this );
119     }
120     else /* if ( ( sel_start >= start_ ) && ( sel_end <= end_ ) ) */
121     {
122         // We only want to consider what's inside THIS chunk
123         sel_start = qMax( sel_start, start_ );
124         sel_end   = qMin( sel_end, end_ );
125 
126         if ( sel_start > start_ )
127             list << LineChunk( start_, sel_start - 1, type_ );
128         list << LineChunk( sel_start, sel_end, Selected );
129         if ( sel_end < end_ )
130             list << LineChunk( sel_end + 1, end_, type_ );
131     }
132 
133     return list;
134 }
135 
136 inline void LineDrawer::addChunk( int first_col, int last_col,
137         QColor fore, QColor back )
138 {
139     if ( first_col < 0 )
140         first_col = 0;
141     int length = last_col - first_col + 1;
142     if ( length > 0 ) {
143         list << Chunk ( first_col, length, fore, back );
144     }
145 }
146 
147 inline void LineDrawer::addChunk( const LineChunk& chunk,
148         QColor fore, QColor back )
149 {
150     int first_col = chunk.start();
151     int last_col  = chunk.end();
152 
153     addChunk( first_col, last_col, fore, back );
154 }
155 
156 inline void LineDrawer::draw( QPainter& painter,
157         int initialXPos, int initialYPos,
158         int line_width, const QString& line,
159         int leftExtraBackgroundPx )
160 {
161     QFontMetrics fm = painter.fontMetrics();
162     const int fontHeight = fm.height();
163     const int fontAscent = fm.ascent();
164     // For some reason on Qt 4.8.2 for Win, maxWidth() is wrong but the
165     // following give the right result, not sure why:
166     const int fontWidth = fm.width( QChar('a') );
167 
168     int xPos = initialXPos;
169     int yPos = initialYPos;
170 
171     foreach ( Chunk chunk, list ) {
172         // Draw each chunk
173         // LOG(logDEBUG) << "Chunk: " << chunk.start() << " " << chunk.length();
174         QString cutline = line.mid( chunk.start(), chunk.length() );
175         const int chunk_width = cutline.length() * fontWidth;
176         if ( xPos == initialXPos ) {
177             // First chunk, we extend the left background a bit,
178             // it looks prettier.
179             painter.fillRect( xPos - leftExtraBackgroundPx, yPos,
180                     chunk_width + leftExtraBackgroundPx,
181                     fontHeight, chunk.backColor() );
182         }
183         else {
184             // other chunks...
185             painter.fillRect( xPos, yPos, chunk_width,
186                     fontHeight, chunk.backColor() );
187         }
188         painter.setPen( chunk.foreColor() );
189         painter.drawText( xPos, yPos + fontAscent, cutline );
190         xPos += chunk_width;
191     }
192 
193     // Draw the empty block at the end of the line
194     int blank_width = line_width - xPos;
195 
196     if ( blank_width > 0 )
197         painter.fillRect( xPos, yPos, blank_width, fontHeight, backColor_ );
198 }
199 
200 const int DigitsBuffer::timeout_ = 2000;
201 
202 DigitsBuffer::DigitsBuffer() : QObject()
203 {
204 }
205 
206 void DigitsBuffer::reset()
207 {
208     LOG(logDEBUG) << "DigitsBuffer::reset()";
209 
210     timer_.stop();
211     digits_.clear();
212 }
213 
214 void DigitsBuffer::add( char character )
215 {
216     LOG(logDEBUG) << "DigitsBuffer::add()";
217 
218     digits_.append( QChar( character ) );
219     timer_.start( timeout_ , this );
220 }
221 
222 int DigitsBuffer::content()
223 {
224     int result = digits_.toInt();
225     reset();
226 
227     return result;
228 }
229 
230 void DigitsBuffer::timerEvent( QTimerEvent* event )
231 {
232     if ( event->timerId() == timer_.timerId() ) {
233         reset();
234     }
235     else {
236         QObject::timerEvent( event );
237     }
238 }
239 
240 // Graphic parameters
241 constexpr int AbstractLogView::OVERVIEW_WIDTH = 27;
242 constexpr int AbstractLogView::HOOK_THRESHOLD = 300;
243 
244 AbstractLogView::AbstractLogView(const AbstractLogData* newLogData,
245         const QuickFindPattern* const quickFindPattern, QWidget* parent) :
246     QAbstractScrollArea( parent ),
247     lineNumbersVisible_( false ),
248     selectionStartPos_(),
249     selectionCurrentEndPos_(),
250     autoScrollTimer_(),
251     selection_(),
252     quickFindPattern_( quickFindPattern ),
253     quickFind_( newLogData, &selection_, quickFindPattern ),
254     followElasticHook_( HOOK_THRESHOLD )
255 {
256     logData = newLogData;
257 
258     followMode_ = false;
259 
260     selectionStarted_ = false;
261     markingClickInitiated_ = false;
262 
263     firstLine = 0;
264     lastLine = 0;
265     firstCol = 0;
266 
267     overview_ = NULL;
268     overviewWidget_ = NULL;
269 
270     // Display
271     nbDigitsInLineNumber_ = 0;
272     leftMarginPx_ = 0;
273 
274     // Fonts
275     charWidth_ = 1;
276     charHeight_ = 1;
277 
278     // Create the viewport QWidget
279     setViewport( 0 );
280 
281     setAttribute( Qt::WA_StaticContents );  // Does it work?
282 
283     // Hovering
284     setMouseTracking( true );
285     lastHoveredLine_ = -1;
286 
287     // Init the popup menu
288     createMenu();
289 
290     // Signals
291     connect( quickFindPattern_, SIGNAL( patternUpdated() ),
292             this, SLOT ( handlePatternUpdated() ) );
293     connect( verticalScrollBar(), SIGNAL( sliderMoved( int ) ),
294             this, SIGNAL( followDisabled() ) );
295     connect( &quickFind_, SIGNAL( notify( const QFNotification& ) ),
296             this, SIGNAL( notifyQuickFind( const QFNotification& ) ) );
297     connect( &quickFind_, SIGNAL( clearNotification() ),
298             this, SIGNAL( clearQuickFindNotification() ) );
299     connect( &followElasticHook_, SIGNAL( lengthChanged() ),
300             this, SLOT( repaint() ) );
301 }
302 
303 AbstractLogView::~AbstractLogView()
304 {
305 }
306 
307 
308 //
309 // Received events
310 //
311 
312 void AbstractLogView::changeEvent( QEvent* changeEvent )
313 {
314     QAbstractScrollArea::changeEvent( changeEvent );
315 
316     // Stop the timer if the widget becomes inactive
317     if ( changeEvent->type() == QEvent::ActivationChange ) {
318         if ( ! isActiveWindow() )
319             autoScrollTimer_.stop();
320     }
321     viewport()->update();
322 }
323 
324 void AbstractLogView::mousePressEvent( QMouseEvent* mouseEvent )
325 {
326     static std::shared_ptr<Configuration> config =
327         Persistent<Configuration>( "settings" );
328 
329     if ( mouseEvent->button() == Qt::LeftButton )
330     {
331         int line = convertCoordToLine( mouseEvent->y() );
332 
333         if ( mouseEvent->modifiers() & Qt::ShiftModifier )
334         {
335             selection_.selectRangeFromPrevious( line );
336             emit updateLineNumber( line );
337             update();
338         }
339         else
340         {
341             if ( mouseEvent->x() < bulletZoneWidthPx_ ) {
342                 // Mark a line if it is clicked in the left margin
343                 // (only if click and release in the same area)
344                 markingClickInitiated_ = true;
345                 markingClickLine_ = line;
346             }
347             else {
348                 // Select the line, and start a selection
349                 if ( line < logData->getNbLine() ) {
350                     selection_.selectLine( line );
351                     emit updateLineNumber( line );
352                     emit newSelection( line );
353                 }
354 
355                 // Remember the click in case we're starting a selection
356                 selectionStarted_ = true;
357                 selectionStartPos_ = convertCoordToFilePos( mouseEvent->pos() );
358                 selectionCurrentEndPos_ = selectionStartPos_;
359             }
360         }
361 
362         // Invalidate our cache
363         textAreaCache_.invalid_ = true;
364     }
365     else if ( mouseEvent->button() == Qt::RightButton )
366     {
367         // Prepare the popup depending on selection type
368         if ( selection_.isSingleLine() ) {
369             copyAction_->setText( "&Copy this line" );
370         }
371         else {
372             copyAction_->setText( "&Copy" );
373             copyAction_->setStatusTip( tr("Copy the selection") );
374         }
375 
376         if ( selection_.isPortion() ) {
377             findNextAction_->setEnabled( true );
378             findPreviousAction_->setEnabled( true );
379             addToSearchAction_->setEnabled( true );
380         }
381         else {
382             findNextAction_->setEnabled( false );
383             findPreviousAction_->setEnabled( false );
384             addToSearchAction_->setEnabled( false );
385         }
386 
387         // "Add to search" only makes sense in regexp mode
388         if ( config->mainRegexpType() != ExtendedRegexp )
389             addToSearchAction_->setEnabled( false );
390 
391         // Display the popup (blocking)
392         popupMenu_->exec( QCursor::pos() );
393     }
394 
395     emit activity();
396 }
397 
398 void AbstractLogView::mouseMoveEvent( QMouseEvent* mouseEvent )
399 {
400     // Selection implementation
401     if ( selectionStarted_ )
402     {
403         // Invalidate our cache
404         textAreaCache_.invalid_ = true;
405 
406         QPoint thisEndPos = convertCoordToFilePos( mouseEvent->pos() );
407         if ( thisEndPos != selectionCurrentEndPos_ )
408         {
409             // Are we on a different line?
410             if ( selectionStartPos_.y() != thisEndPos.y() )
411             {
412                 if ( thisEndPos.y() != selectionCurrentEndPos_.y() )
413                 {
414                     // This is a 'range' selection
415                     selection_.selectRange( selectionStartPos_.y(),
416                             thisEndPos.y() );
417                     emit updateLineNumber( thisEndPos.y() );
418                     update();
419                 }
420             }
421             // So we are on the same line. Are we moving horizontaly?
422             else if ( thisEndPos.x() != selectionCurrentEndPos_.x() )
423             {
424                 // This is a 'portion' selection
425                 selection_.selectPortion( thisEndPos.y(),
426                         selectionStartPos_.x(), thisEndPos.x() );
427                 update();
428             }
429             // On the same line, and moving vertically then
430             else
431             {
432                 // This is a 'line' selection
433                 selection_.selectLine( thisEndPos.y() );
434                 emit updateLineNumber( thisEndPos.y() );
435                 update();
436             }
437             selectionCurrentEndPos_ = thisEndPos;
438 
439             // Do we need to scroll while extending the selection?
440             QRect visible = viewport()->rect();
441             if ( visible.contains( mouseEvent->pos() ) )
442                 autoScrollTimer_.stop();
443             else if ( ! autoScrollTimer_.isActive() )
444                 autoScrollTimer_.start( 100, this );
445         }
446     }
447     else {
448         considerMouseHovering( mouseEvent->x(), mouseEvent->y() );
449     }
450 }
451 
452 void AbstractLogView::mouseReleaseEvent( QMouseEvent* mouseEvent )
453 {
454     if ( markingClickInitiated_ ) {
455         markingClickInitiated_ = false;
456         int line = convertCoordToLine( mouseEvent->y() );
457         if ( line == markingClickLine_ ) {
458             // Invalidate our cache
459             textAreaCache_.invalid_ = true;
460 
461             emit markLine( line );
462         }
463     }
464     else {
465         selectionStarted_ = false;
466         if ( autoScrollTimer_.isActive() )
467             autoScrollTimer_.stop();
468         updateGlobalSelection();
469     }
470 }
471 
472 void AbstractLogView::mouseDoubleClickEvent( QMouseEvent* mouseEvent )
473 {
474     if ( mouseEvent->button() == Qt::LeftButton )
475     {
476         // Invalidate our cache
477         textAreaCache_.invalid_ = true;
478 
479         const QPoint pos = convertCoordToFilePos( mouseEvent->pos() );
480         selectWordAtPosition( pos );
481     }
482 
483     emit activity();
484 }
485 
486 void AbstractLogView::timerEvent( QTimerEvent* timerEvent )
487 {
488     if ( timerEvent->timerId() == autoScrollTimer_.timerId() ) {
489         QRect visible = viewport()->rect();
490         const QPoint globalPos = QCursor::pos();
491         const QPoint pos = viewport()->mapFromGlobal( globalPos );
492         QMouseEvent ev( QEvent::MouseMove, pos, globalPos, Qt::LeftButton,
493                 Qt::LeftButton, Qt::NoModifier );
494         mouseMoveEvent( &ev );
495         int deltaX = qMax( pos.x() - visible.left(),
496                 visible.right() - pos.x() ) - visible.width();
497         int deltaY = qMax( pos.y() - visible.top(),
498                 visible.bottom() - pos.y() ) - visible.height();
499         int delta = qMax( deltaX, deltaY );
500 
501         if ( delta >= 0 ) {
502             if ( delta < 7 )
503                 delta = 7;
504             int timeout = 4900 / ( delta * delta );
505             autoScrollTimer_.start( timeout, this );
506 
507             if ( deltaX > 0 )
508                 horizontalScrollBar()->triggerAction(
509                         pos.x() <visible.center().x() ?
510                         QAbstractSlider::SliderSingleStepSub :
511                         QAbstractSlider::SliderSingleStepAdd );
512 
513             if ( deltaY > 0 )
514                 verticalScrollBar()->triggerAction(
515                         pos.y() <visible.center().y() ?
516                         QAbstractSlider::SliderSingleStepSub :
517                         QAbstractSlider::SliderSingleStepAdd );
518         }
519     }
520     QAbstractScrollArea::timerEvent( timerEvent );
521 }
522 
523 void AbstractLogView::keyPressEvent( QKeyEvent* keyEvent )
524 {
525     LOG(logDEBUG4) << "keyPressEvent received";
526     bool controlModifier = (keyEvent->modifiers() & Qt::ControlModifier) == Qt::ControlModifier;
527     bool shiftModifier = (keyEvent->modifiers() & Qt::ShiftModifier) == Qt::ShiftModifier;
528 
529     if ( keyEvent->key() == Qt::Key_Left )
530         horizontalScrollBar()->triggerAction(QScrollBar::SliderPageStepSub);
531     else if ( keyEvent->key() == Qt::Key_Right )
532         horizontalScrollBar()->triggerAction(QScrollBar::SliderPageStepAdd);
533     else if ( keyEvent->key() == Qt::Key_Home && !controlModifier)
534         jumpToStartOfLine();
535     else if ( keyEvent->key() == Qt::Key_End  && !controlModifier)
536         jumpToRightOfScreen();
537     else if ( (keyEvent->key() == Qt::Key_PageDown && controlModifier)
538            || (keyEvent->key() == Qt::Key_End && controlModifier) )
539     {
540         emit followDisabled(); // duplicate of 'G' action.
541         selection_.selectLine( logData->getNbLine() - 1 );
542         emit updateLineNumber( logData->getNbLine() - 1 );
543         jumpToBottom();
544     }
545     else if ( (keyEvent->key() == Qt::Key_PageUp && controlModifier)
546            || (keyEvent->key() == Qt::Key_Home && controlModifier) )
547     {
548         emit followDisabled(); // like 'g' but 0 input first line action.
549         selectAndDisplayLine( 0 );
550         emit updateLineNumber( 0 );
551     }
552     else if ( keyEvent->key() == Qt::Key_F3 && !shiftModifier )
553         searchNext(); // duplicate of 'n' action.
554     else if ( keyEvent->key() == Qt::Key_F3 && shiftModifier )
555         searchPrevious(); // duplicate of 'N' action.
556     else {
557         const char character = (keyEvent->text())[0].toLatin1();
558 
559         if ( keyEvent->modifiers() == Qt::NoModifier &&
560                 ( character >= '0' ) && ( character <= '9' ) ) {
561             // Adds the digit to the timed buffer
562             digitsBuffer_.add( character );
563         }
564         else {
565             switch ( (keyEvent->text())[0].toLatin1() ) {
566                 case 'j':
567                     {
568                         int delta = qMax( 1, digitsBuffer_.content() );
569                         emit followDisabled();
570                         //verticalScrollBar()->triggerAction(
571                         //QScrollBar::SliderSingleStepAdd);
572                         moveSelection( delta );
573                         break;
574                     }
575                 case 'k':
576                     {
577                         int delta = qMin( -1, - digitsBuffer_.content() );
578                         emit followDisabled();
579                         //verticalScrollBar()->triggerAction(
580                         //QScrollBar::SliderSingleStepSub);
581                         moveSelection( delta );
582                         break;
583                     }
584                 case 'h':
585                     horizontalScrollBar()->triggerAction(
586                             QScrollBar::SliderSingleStepSub);
587                     break;
588                 case 'l':
589                     horizontalScrollBar()->triggerAction(
590                             QScrollBar::SliderSingleStepAdd);
591                     break;
592                 case '0':
593                     jumpToStartOfLine();
594                     break;
595                 case '$':
596                     jumpToEndOfLine();
597                     break;
598                 case 'g':
599                     {
600                         int newLine = qMax( 0, digitsBuffer_.content() - 1 );
601                         if ( newLine >= logData->getNbLine() )
602                             newLine = logData->getNbLine() - 1;
603                         emit followDisabled();
604                         selectAndDisplayLine( newLine );
605                         emit updateLineNumber( newLine );
606                         break;
607                     }
608                 case 'G':
609                     emit followDisabled();
610                     selection_.selectLine( logData->getNbLine() - 1 );
611                     emit updateLineNumber( logData->getNbLine() - 1 );
612                     jumpToBottom();
613                     break;
614                 case 'n':
615                     emit searchNext();
616                     break;
617                 case 'N':
618                     emit searchPrevious();
619                     break;
620                 case '*':
621                     // Use the selected 'word' and search forward
622                     findNextSelected();
623                     break;
624                 case '#':
625                     // Use the selected 'word' and search backward
626                     findPreviousSelected();
627                     break;
628                 default:
629                     keyEvent->ignore();
630             }
631         }
632     }
633 
634     if ( keyEvent->isAccepted() ) {
635         emit activity();
636     }
637     else {
638         QAbstractScrollArea::keyPressEvent( keyEvent );
639     }
640 }
641 
642 void AbstractLogView::wheelEvent( QWheelEvent* wheelEvent )
643 {
644     emit followDisabled();
645     emit activity();
646 
647     // LOG(logDEBUG) << "wheelEvent";
648 
649     if ( verticalScrollBar()->value() == verticalScrollBar()->maximum() ) {
650         // LOG(logDEBUG) << "Elastic " << wheelEvent->pixelDelta().y();
651         followElasticHook_.move( - wheelEvent->pixelDelta().y() );
652     }
653 
654     // LOG(logDEBUG) << "Length = " << followElasticHook_.length();
655     if ( followElasticHook_.length() == 0 && !followElasticHook_.isHooked() ) {
656         QAbstractScrollArea::wheelEvent( wheelEvent );
657     }
658 }
659 
660 void AbstractLogView::resizeEvent( QResizeEvent* )
661 {
662     if ( logData == NULL )
663         return;
664 
665     LOG(logDEBUG) << "resizeEvent received";
666 
667     updateDisplaySize();
668 }
669 
670 void AbstractLogView::scrollContentsBy( int dx, int dy )
671 {
672     LOG(logDEBUG) << "scrollContentsBy received " << dy;
673 
674     /*
675     LineNumber visible_lines = std::max(
676             static_cast<LineNumber>( logData->getNbLine() ),
677             static_cast<LineNumber>( getNbVisibleLines() ) );
678     firstLine = std::min( std::max( firstLine - dy, 0LL ),
679             logData->getNbLine() - visible_lines );
680 
681     LOG(logDEBUG) << "scrollContentsBy " << visible_lines << " " << firstLine;
682     */
683 
684     firstLine = std::max( firstLine - dy, 0LL );
685     firstCol  = (firstCol - dx) > 0 ? firstCol - dx : 0;
686     lastLine  = firstLine + getNbVisibleLines();
687 
688     // Update the overview if we have one
689     if ( overview_ != NULL )
690         overview_->updateCurrentPosition( firstLine, lastLine );
691 
692     // Are we hovering over a new line?
693     const QPoint mouse_pos = mapFromGlobal( QCursor::pos() );
694     considerMouseHovering( mouse_pos.x(), mouse_pos.y() );
695 
696     // Redraw
697     update();
698 }
699 
700 void AbstractLogView::paintEvent( QPaintEvent* paintEvent )
701 {
702     QRect invalidRect = paintEvent->rect();
703     if ( (invalidRect.isEmpty()) || (logData == NULL) )
704         return;
705 
706     LOG(logDEBUG4) << "paintEvent received, firstLine=" << firstLine
707         << " lastLine=" << lastLine <<
708         " rect: " << invalidRect.topLeft().x() <<
709         ", " << invalidRect.topLeft().y() <<
710         ", " << invalidRect.bottomRight().x() <<
711         ", " << invalidRect.bottomRight().y();
712 
713 #ifdef GLOGG_PERF_MEASURE_FPS
714     static uint32_t maxline = logData->getNbLine();
715     if ( ! perfCounter_.addEvent() && logData->getNbLine() > maxline ) {
716         LOG(logWARNING) << "Redraw per second: " << perfCounter_.readAndReset()
717             << " lines: " << logData->getNbLine();
718         perfCounter_.addEvent();
719         maxline = logData->getNbLine();
720     }
721 #endif
722 
723     auto start = std::chrono::system_clock::now();
724 
725     // Can we use our cache?
726     int32_t delta_y = textAreaCache_.first_line_ - firstLine;
727 
728     if ( textAreaCache_.invalid_ || ( textAreaCache_.first_column_ != firstCol ) ) {
729         // Force a full redraw
730         delta_y = INT32_MAX;
731     }
732 
733     if ( delta_y != 0 ) {
734         // Full or partial redraw
735         drawTextArea( &textAreaCache_.pixmap_, delta_y );
736 
737         textAreaCache_.invalid_      = false;
738         textAreaCache_.first_line_   = firstLine;
739         textAreaCache_.last_line_    = lastLine;
740         textAreaCache_.first_column_ = firstCol;
741 
742         LOG(logDEBUG) << "End of writing " <<
743             std::chrono::duration_cast<std::chrono::microseconds>
744             ( std::chrono::system_clock::now() - start ).count();
745     }
746     else {
747         // Use the cache as is: nothing to do!
748     }
749 
750     // Height in pixels of the "pull to follow" bottom bar.
751     const int pullToFollowHeight = mapPullToFollowLength( followElasticHook_.length() )
752         + ( followElasticHook_.isHooked() ? 12 : 0 );
753 
754     if ( pullToFollowHeight
755             && ( pullToFollowCache_.nb_columns_ != getNbVisibleCols() ) ) {
756         LOG(logDEBUG) << "Drawing pull to follow bar";
757         pullToFollowCache_.pixmap_ = drawPullToFollowBar(
758                 viewport()->width(), viewport()->devicePixelRatio() );
759         pullToFollowCache_.nb_columns_ = getNbVisibleCols();
760     }
761 
762     QPainter devicePainter( viewport() );
763     devicePainter.drawPixmap( 0, - pullToFollowHeight, textAreaCache_.pixmap_ );
764 
765     // Draw the "pull to follow" zone if needed
766     if ( pullToFollowHeight ) {
767         devicePainter.drawPixmap( 0, viewport()->height() - pullToFollowHeight,
768                 pullToFollowCache_.pixmap_ );
769     }
770 
771     LOG(logDEBUG) << "End of repaint " <<
772         std::chrono::duration_cast<std::chrono::microseconds>
773         ( std::chrono::system_clock::now() - start ).count();
774 }
775 
776 // These two functions are virtual and this implementation is clearly
777 // only valid for a non-filtered display.
778 // We count on the 'filtered' derived classes to override them.
779 qint64 AbstractLogView::displayLineNumber( int lineNumber ) const
780 {
781     return lineNumber + 1; // show a 1-based index
782 }
783 
784 qint64 AbstractLogView::maxDisplayLineNumber() const
785 {
786     return logData->getNbLine();
787 }
788 
789 void AbstractLogView::setOverview( Overview* overview,
790        OverviewWidget* overview_widget )
791 {
792     overview_ = overview;
793     overviewWidget_ = overview_widget;
794 
795     if ( overviewWidget_ ) {
796         connect( overviewWidget_, SIGNAL( lineClicked ( int ) ),
797                 this, SIGNAL( followDisabled() ) );
798         connect( overviewWidget_, SIGNAL( lineClicked ( int ) ),
799                 this, SLOT( jumpToLine( int ) ) );
800     }
801     refreshOverview();
802 }
803 
804 void AbstractLogView::searchUsingFunction(
805         qint64 (QuickFind::*search_function)() )
806 {
807     emit followDisabled();
808 
809     int line = (quickFind_.*search_function)();
810     if ( line >= 0 ) {
811         LOG(logDEBUG) << "search " << line;
812         displayLine( line );
813         emit updateLineNumber( line );
814     }
815 }
816 
817 void AbstractLogView::searchForward()
818 {
819     searchUsingFunction( &QuickFind::searchForward );
820 }
821 
822 void AbstractLogView::searchBackward()
823 {
824     searchUsingFunction( &QuickFind::searchBackward );
825 }
826 
827 void AbstractLogView::incrementallySearchForward()
828 {
829     searchUsingFunction( &QuickFind::incrementallySearchForward );
830 }
831 
832 void AbstractLogView::incrementallySearchBackward()
833 {
834     searchUsingFunction( &QuickFind::incrementallySearchBackward );
835 }
836 
837 void AbstractLogView::incrementalSearchAbort()
838 {
839     quickFind_.incrementalSearchAbort();
840     emit changeQuickFind(
841             "",
842             QuickFindMux::Forward );
843 }
844 
845 void AbstractLogView::incrementalSearchStop()
846 {
847     quickFind_.incrementalSearchStop();
848 }
849 
850 void AbstractLogView::followSet( bool checked )
851 {
852     followMode_ = checked;
853     if ( checked )
854         jumpToBottom();
855 }
856 
857 void AbstractLogView::refreshOverview()
858 {
859     assert( overviewWidget_ );
860 
861     // Create space for the Overview if needed
862     if ( ( getOverview() != NULL ) && getOverview()->isVisible() ) {
863         setViewportMargins( 0, 0, OVERVIEW_WIDTH, 0 );
864         overviewWidget_->show();
865     }
866     else {
867         setViewportMargins( 0, 0, 0, 0 );
868         overviewWidget_->hide();
869     }
870 }
871 
872 // Reset the QuickFind when the pattern is changed.
873 void AbstractLogView::handlePatternUpdated()
874 {
875     LOG(logDEBUG) << "AbstractLogView::handlePatternUpdated()";
876 
877     quickFind_.resetLimits();
878     update();
879 }
880 
881 // OR the current with the current search expression
882 void AbstractLogView::addToSearch()
883 {
884     if ( selection_.isPortion() ) {
885         LOG(logDEBUG) << "AbstractLogView::addToSearch()";
886         emit addToSearch( selection_.getSelectedText( logData ) );
887     }
888     else {
889         LOG(logERROR) << "AbstractLogView::addToSearch called for a wrong type of selection";
890     }
891 }
892 
893 // Find next occurence of the selected text (*)
894 void AbstractLogView::findNextSelected()
895 {
896     // Use the selected 'word' and search forward
897     if ( selection_.isPortion() ) {
898         emit changeQuickFind(
899                 selection_.getSelectedText( logData ),
900                 QuickFindMux::Forward );
901         emit searchNext();
902     }
903 }
904 
905 // Find next previous of the selected text (#)
906 void AbstractLogView::findPreviousSelected()
907 {
908     if ( selection_.isPortion() ) {
909         emit changeQuickFind(
910                 selection_.getSelectedText( logData ),
911                 QuickFindMux::Backward );
912         emit searchNext();
913     }
914 }
915 
916 // Copy the selection to the clipboard
917 void AbstractLogView::copy()
918 {
919     static QClipboard* clipboard = QApplication::clipboard();
920 
921     clipboard->setText( selection_.getSelectedText( logData ) );
922 }
923 
924 //
925 // Public functions
926 //
927 
928 void AbstractLogView::updateData()
929 {
930     LOG(logDEBUG) << "AbstractLogView::updateData";
931 
932     // Check the top Line is within range
933     if ( firstLine >= logData->getNbLine() ) {
934         firstLine = 0;
935         firstCol = 0;
936         verticalScrollBar()->setValue( 0 );
937         horizontalScrollBar()->setValue( 0 );
938     }
939 
940     // Crop selection if it become out of range
941     selection_.crop( logData->getNbLine() - 1 );
942 
943     // Adapt the scroll bars to the new content
944     updateScrollBars();
945 
946     lastLine = qMin( logData->getNbLine(), firstLine + getNbVisibleLines() );
947 
948     // Reset the QuickFind in case we have new stuff to search into
949     quickFind_.resetLimits();
950 
951     if ( followMode_ )
952         jumpToBottom();
953 
954     // Update the overview if we have one
955     if ( overview_ != NULL )
956         overview_->updateCurrentPosition( firstLine, lastLine );
957 
958     // Update the length of line numbers
959     nbDigitsInLineNumber_ = countDigits( maxDisplayLineNumber() );
960 
961     // Repaint!
962     update();
963 }
964 
965 void AbstractLogView::updateDisplaySize()
966 {
967     // Font is assumed to be mono-space (is restricted by options dialog)
968     QFontMetrics fm = fontMetrics();
969     charHeight_ = fm.height();
970     // For some reason on Qt 4.8.2 for Win, maxWidth() is wrong but the
971     // following give the right result, not sure why:
972     charWidth_ = fm.width( QChar('a') );
973 
974     // Calculate the index of the last line shown
975     lastLine = qMin( logData->getNbLine(), firstLine + getNbVisibleLines() );
976 
977     // Update the scroll bars
978     updateScrollBars();
979     verticalScrollBar()->setPageStep( getNbVisibleLines() );
980 
981     LOG(logDEBUG) << "viewport.width()=" << viewport()->width();
982     LOG(logDEBUG) << "viewport.height()=" << viewport()->height();
983     LOG(logDEBUG) << "width()=" << width();
984     LOG(logDEBUG) << "height()=" << height();
985 
986     if ( overviewWidget_ )
987         overviewWidget_->setGeometry( viewport()->width() + 2, 1,
988                 OVERVIEW_WIDTH - 1, viewport()->height() );
989 
990     // Our text area cache is now invalid
991     textAreaCache_.invalid_ = true;
992     textAreaCache_.pixmap_  = QPixmap {
993         viewport()->size() * viewport()->devicePixelRatio() };
994     textAreaCache_.pixmap_.setDevicePixelRatio( viewport()->devicePixelRatio() );
995 }
996 
997 int AbstractLogView::getTopLine() const
998 {
999     return firstLine;
1000 }
1001 
1002 QString AbstractLogView::getSelection() const
1003 {
1004     return selection_.getSelectedText( logData );
1005 }
1006 
1007 void AbstractLogView::selectAll()
1008 {
1009     selection_.selectRange( 0, logData->getNbLine() - 1 );
1010     update();
1011 }
1012 
1013 void AbstractLogView::selectAndDisplayLine( int line )
1014 {
1015     emit followDisabled();
1016     selection_.selectLine( line );
1017     displayLine( line );
1018     emit updateLineNumber( line );
1019 }
1020 
1021 // The difference between this function and displayLine() is quite
1022 // subtle: this one always jump, even if the line passed is visible.
1023 void AbstractLogView::jumpToLine( int line )
1024 {
1025     // Put the selected line in the middle if possible
1026     int newTopLine = line - ( getNbVisibleLines() / 2 );
1027     if ( newTopLine < 0 )
1028         newTopLine = 0;
1029 
1030     // This will also trigger a scrollContents event
1031     verticalScrollBar()->setValue( newTopLine );
1032 }
1033 
1034 void AbstractLogView::setLineNumbersVisible( bool lineNumbersVisible )
1035 {
1036     lineNumbersVisible_ = lineNumbersVisible;
1037 }
1038 
1039 //
1040 // Private functions
1041 //
1042 
1043 // Returns the number of lines visible in the viewport
1044 int AbstractLogView::getNbVisibleLines() const
1045 {
1046     return viewport()->height() / charHeight_ + 1;
1047 }
1048 
1049 // Returns the number of columns visible in the viewport
1050 int AbstractLogView::getNbVisibleCols() const
1051 {
1052     return ( viewport()->width() - leftMarginPx_ ) / charWidth_ + 1;
1053 }
1054 
1055 // Converts the mouse x, y coordinates to the line number in the file
1056 int AbstractLogView::convertCoordToLine(int yPos) const
1057 {
1058     int line = firstLine + yPos / charHeight_;
1059 
1060     return line;
1061 }
1062 
1063 // Converts the mouse x, y coordinates to the char coordinates (in the file)
1064 // This function ensure the pos exists in the file.
1065 QPoint AbstractLogView::convertCoordToFilePos( const QPoint& pos ) const
1066 {
1067     int line = firstLine + pos.y() / charHeight_;
1068     if ( line >= logData->getNbLine() )
1069         line = logData->getNbLine() - 1;
1070     if ( line < 0 )
1071         line = 0;
1072 
1073     // Determine column in screen space and convert it to file space
1074     int column = firstCol + ( pos.x() - leftMarginPx_ ) / charWidth_;
1075 
1076     QString this_line = logData->getExpandedLineString( line );
1077     const int length = this_line.length();
1078 
1079     if ( column >= length )
1080         column = length - 1;
1081     if ( column < 0 )
1082         column = 0;
1083 
1084     LOG(logDEBUG4) << "AbstractLogView::convertCoordToFilePos col="
1085         << column << " line=" << line;
1086     QPoint point( column, line );
1087 
1088     return point;
1089 }
1090 
1091 // Makes the widget adjust itself to display the passed line.
1092 // Doing so, it will throw itself a scrollContents event.
1093 void AbstractLogView::displayLine( int line )
1094 {
1095     // If the line is already the screen
1096     if ( ( line >= firstLine ) &&
1097          ( line < ( firstLine + getNbVisibleLines() ) ) ) {
1098         // ... don't scroll and just repaint
1099         update();
1100     } else {
1101         jumpToLine( line );
1102     }
1103 }
1104 
1105 // Move the selection up and down by the passed number of lines
1106 void AbstractLogView::moveSelection( int delta )
1107 {
1108     LOG(logDEBUG) << "AbstractLogView::moveSelection delta=" << delta;
1109 
1110     QList<int> selection = selection_.getLines();
1111     int new_line;
1112 
1113     // If nothing is selected, do as if line -1 was.
1114     if ( selection.isEmpty() )
1115         selection.append( -1 );
1116 
1117     if ( delta < 0 )
1118         new_line = selection.first() + delta;
1119     else
1120         new_line = selection.last() + delta;
1121 
1122     if ( new_line < 0 )
1123         new_line = 0;
1124     else if ( new_line >= logData->getNbLine() )
1125         new_line = logData->getNbLine() - 1;
1126 
1127     // Select and display the new line
1128     selection_.selectLine( new_line );
1129     displayLine( new_line );
1130     emit updateLineNumber( new_line );
1131 }
1132 
1133 // Make the start of the lines visible
1134 void AbstractLogView::jumpToStartOfLine()
1135 {
1136     horizontalScrollBar()->setValue( 0 );
1137 }
1138 
1139 // Make the end of the lines in the selection visible
1140 void AbstractLogView::jumpToEndOfLine()
1141 {
1142     QList<int> selection = selection_.getLines();
1143 
1144     // Search the longest line in the selection
1145     int max_length = 0;
1146     foreach ( int line, selection ) {
1147         int length = logData->getLineLength( line );
1148         if ( length > max_length )
1149             max_length = length;
1150     }
1151 
1152     horizontalScrollBar()->setValue( max_length - getNbVisibleCols() );
1153 }
1154 
1155 // Make the end of the lines on the screen visible
1156 void AbstractLogView::jumpToRightOfScreen()
1157 {
1158     QList<int> selection = selection_.getLines();
1159 
1160     // Search the longest line on screen
1161     int max_length = 0;
1162     for ( int i = firstLine; i <= ( firstLine + getNbVisibleLines() ); i++ ) {
1163         int length = logData->getLineLength( i );
1164         if ( length > max_length )
1165             max_length = length;
1166     }
1167 
1168     horizontalScrollBar()->setValue( max_length - getNbVisibleCols() );
1169 }
1170 
1171 // Jump to the first line
1172 void AbstractLogView::jumpToTop()
1173 {
1174     // This will also trigger a scrollContents event
1175     verticalScrollBar()->setValue( 0 );
1176     update();       // in case the screen hasn't moved
1177 }
1178 
1179 // Jump to the last line
1180 void AbstractLogView::jumpToBottom()
1181 {
1182     const int new_top_line =
1183         qMax( logData->getNbLine() - getNbVisibleLines() + 1, 0LL );
1184 
1185     // This will also trigger a scrollContents event
1186     verticalScrollBar()->setValue( new_top_line );
1187     update();       // in case the screen hasn't moved
1188 }
1189 
1190 // Returns whether the character passed is a 'word' character
1191 inline bool AbstractLogView::isCharWord( char c )
1192 {
1193     if ( ( ( c >= 'A' ) && ( c <= 'Z' ) ) ||
1194          ( ( c >= 'a' ) && ( c <= 'z' ) ) ||
1195          ( ( c >= '0' ) && ( c <= '9' ) ) ||
1196          ( ( c == '_' ) ) )
1197         return true;
1198     else
1199         return false;
1200 }
1201 
1202 // Select the word under the given position
1203 void AbstractLogView::selectWordAtPosition( const QPoint& pos )
1204 {
1205     const int x = pos.x();
1206     const QString line = logData->getExpandedLineString( pos.y() );
1207 
1208     if ( isCharWord( line[x].toLatin1() ) ) {
1209         // Search backward for the first character in the word
1210         int currentPos = x;
1211         for ( ; currentPos > 0; currentPos-- )
1212             if ( ! isCharWord( line[currentPos].toLatin1() ) )
1213                 break;
1214         // Exclude the first char of the line if needed
1215         if ( ! isCharWord( line[currentPos].toLatin1() ) )
1216             currentPos++;
1217         int start = currentPos;
1218 
1219         // Now search for the end
1220         currentPos = x;
1221         for ( ; currentPos < line.length() - 1; currentPos++ )
1222             if ( ! isCharWord( line[currentPos].toLatin1() ) )
1223                 break;
1224         // Exclude the last char of the line if needed
1225         if ( ! isCharWord( line[currentPos].toLatin1() ) )
1226             currentPos--;
1227         int end = currentPos;
1228 
1229         selection_.selectPortion( pos.y(), start, end );
1230         updateGlobalSelection();
1231         update();
1232     }
1233 }
1234 
1235 // Update the system global (middle click) selection (X11 only)
1236 void AbstractLogView::updateGlobalSelection()
1237 {
1238     static QClipboard* const clipboard = QApplication::clipboard();
1239 
1240     // Updating it only for "non-trivial" (range or portion) selections
1241     if ( ! selection_.isSingleLine() )
1242         clipboard->setText( selection_.getSelectedText( logData ),
1243                 QClipboard::Selection );
1244 }
1245 
1246 // Create the pop-up menu
1247 void AbstractLogView::createMenu()
1248 {
1249     copyAction_ = new QAction( tr("&Copy"), this );
1250     // No text as this action title depends on the type of selection
1251     connect( copyAction_, SIGNAL(triggered()), this, SLOT(copy()) );
1252 
1253     // For '#' and '*', shortcuts doesn't seem to work but
1254     // at least it displays them in the menu, we manually handle those keys
1255     // as keys event anyway (in keyPressEvent).
1256     findNextAction_ = new QAction(tr("Find &next"), this);
1257     findNextAction_->setShortcut( Qt::Key_Asterisk );
1258     findNextAction_->setStatusTip( tr("Find the next occurence") );
1259     connect( findNextAction_, SIGNAL(triggered()),
1260             this, SLOT( findNextSelected() ) );
1261 
1262     findPreviousAction_ = new QAction( tr("Find &previous"), this );
1263     findPreviousAction_->setShortcut( tr("#")  );
1264     findPreviousAction_->setStatusTip( tr("Find the previous occurence") );
1265     connect( findPreviousAction_, SIGNAL(triggered()),
1266             this, SLOT( findPreviousSelected() ) );
1267 
1268     addToSearchAction_ = new QAction( tr("&Add to search"), this );
1269     addToSearchAction_->setStatusTip(
1270             tr("Add the selection to the current search") );
1271     connect( addToSearchAction_, SIGNAL( triggered() ),
1272             this, SLOT( addToSearch() ) );
1273 
1274     popupMenu_ = new QMenu( this );
1275     popupMenu_->addAction( copyAction_ );
1276     popupMenu_->addSeparator();
1277     popupMenu_->addAction( findNextAction_ );
1278     popupMenu_->addAction( findPreviousAction_ );
1279     popupMenu_->addAction( addToSearchAction_ );
1280 }
1281 
1282 void AbstractLogView::considerMouseHovering( int x_pos, int y_pos )
1283 {
1284     int line = convertCoordToLine( y_pos );
1285     if ( ( x_pos < leftMarginPx_ )
1286             && ( line >= 0 )
1287             && ( line < logData->getNbLine() ) ) {
1288         // Mouse moved in the margin, send event up
1289         // (possibly to highlight the overview)
1290         if ( line != lastHoveredLine_ ) {
1291             LOG(logDEBUG) << "Mouse moved in margin line: " << line;
1292             emit mouseHoveredOverLine( line );
1293             lastHoveredLine_ = line;
1294         }
1295     }
1296     else {
1297         if ( lastHoveredLine_ != -1 ) {
1298             emit mouseLeftHoveringZone();
1299             lastHoveredLine_ = -1;
1300         }
1301     }
1302 }
1303 
1304 void AbstractLogView::updateScrollBars()
1305 {
1306     verticalScrollBar()->setRange( 0, std::max( 0LL,
1307             logData->getNbLine() - getNbVisibleLines() ) );
1308 
1309     const int hScrollMaxValue = ( logData->getMaxLength() - getNbVisibleCols() + 1 ) > 0 ?
1310         ( logData->getMaxLength() - getNbVisibleCols() + 1 ) : 0;
1311     horizontalScrollBar()->setRange( 0, hScrollMaxValue );
1312 }
1313 
1314 void AbstractLogView::drawTextArea( QPaintDevice* paint_device, int32_t delta_y )
1315 {
1316     // LOG( logDEBUG ) << "devicePixelRatio: " << viewport()->devicePixelRatio();
1317     // LOG( logDEBUG ) << "viewport size: " << viewport()->size().width();
1318     // LOG( logDEBUG ) << "pixmap size: " << textPixmap.width();
1319     // Repaint the viewport
1320     QPainter painter( paint_device );
1321     // LOG( logDEBUG ) << "font: " << viewport()->font().family().toStdString();
1322     // LOG( logDEBUG ) << "font painter: " << painter.font().family().toStdString();
1323 
1324     // For pretty circles
1325     painter.setRenderHint( QPainter::Antialiasing );
1326     painter.setFont( this->font() );
1327 
1328     const int fontHeight = charHeight_;
1329     const int fontAscent = painter.fontMetrics().ascent();
1330     const int nbCols = getNbVisibleCols();
1331     const QPalette& palette = viewport()->palette();
1332     std::shared_ptr<const FilterSet> filterSet =
1333         Persistent<FilterSet>( "filterSet" );
1334     QColor foreColor, backColor;
1335 
1336     static const QBrush normalBulletBrush = QBrush( Qt::white );
1337     static const QBrush matchBulletBrush = QBrush( Qt::red );
1338     static const QBrush markBrush = QBrush( "dodgerblue" );
1339 
1340     static const int SEPARATOR_WIDTH = 1;
1341     static const int BULLET_AREA_WIDTH = 11;
1342     static const int CONTENT_MARGIN_WIDTH = 1;
1343     static const int LINE_NUMBER_PADDING = 3;
1344 
1345     const int bottomOfTextPx =
1346         ( lastLine - firstLine + 1 ) * fontHeight;
1347 
1348     // First check the lines to be drawn are within range (might not be the case if
1349     // the file has just changed)
1350     const int nbLines = logData->getNbLine();
1351     if ( nbLines == 0 ) {
1352         return;
1353     }
1354     else {
1355         if ( firstLine >= nbLines )
1356             firstLine = nbLines - 1;
1357         if ( lastLine >= nbLines )
1358             lastLine =  nbLines - 1;
1359     }
1360 
1361     // Lines to write
1362     const QStringList lines = logData->getExpandedLines( firstLine, lastLine - firstLine + 1 );
1363 
1364     // First draw the bullet left margin
1365     painter.setPen(palette.color(QPalette::Text));
1366     painter.drawLine( BULLET_AREA_WIDTH, 0,
1367                       BULLET_AREA_WIDTH, bottomOfTextPx - 1 );
1368     painter.fillRect( 0, 0,
1369                       BULLET_AREA_WIDTH, bottomOfTextPx,
1370                       Qt::darkGray );
1371 
1372     // Column at which the content should start (pixels)
1373     int contentStartPosX = BULLET_AREA_WIDTH + SEPARATOR_WIDTH;
1374 
1375     // This is also the bullet zone width, used for marking clicks
1376     bulletZoneWidthPx_ = contentStartPosX;
1377 
1378     // Draw the line numbers area
1379     int lineNumberAreaStartX = 0;
1380     if ( lineNumbersVisible_ ) {
1381         int lineNumberWidth = charWidth_ * nbDigitsInLineNumber_;
1382         int lineNumberAreaWidth =
1383             2 * LINE_NUMBER_PADDING + lineNumberWidth;
1384         lineNumberAreaStartX = contentStartPosX;
1385 
1386         painter.setPen(palette.color(QPalette::Text));
1387         /* Not sure if it looks good...
1388         painter.drawLine( contentStartPosX + lineNumberAreaWidth,
1389                           0,
1390                           contentStartPosX + lineNumberAreaWidth,
1391                           viewport()->height() );
1392         */
1393         painter.fillRect( contentStartPosX, 0,
1394                           lineNumberAreaWidth, bottomOfTextPx,
1395                           Qt::lightGray );
1396 
1397         // Update for drawing the actual text
1398         contentStartPosX += lineNumberAreaWidth;
1399     }
1400     else {
1401         contentStartPosX += SEPARATOR_WIDTH;
1402     }
1403 
1404     // This is the total width of the 'margin' (including line number if any)
1405     // used for mouse calculation etc...
1406     leftMarginPx_ = contentStartPosX;
1407 
1408     // Then draw each line
1409     for (int i = firstLine; i <= lastLine; i++) {
1410         // Position in pixel of the base line of the line to print
1411         const int yPos = (i-firstLine) * fontHeight;
1412         const int xPos = contentStartPosX + CONTENT_MARGIN_WIDTH;
1413 
1414         // string to print, cut to fit the length and position of the view
1415         const QString line = lines[i - firstLine];
1416         const QString cutLine = line.mid( firstCol, nbCols );
1417 
1418         if ( selection_.isLineSelected( i ) ) {
1419             // Reverse the selected line
1420             foreColor = palette.color( QPalette::HighlightedText );
1421             backColor = palette.color( QPalette::Highlight );
1422             painter.setPen(palette.color(QPalette::Text));
1423         }
1424         else if ( filterSet->matchLine( logData->getLineString( i ),
1425                     &foreColor, &backColor ) ) {
1426             // Apply a filter to the line
1427         }
1428         else {
1429             // Use the default colors
1430             foreColor = palette.color( QPalette::Text );
1431             backColor = palette.color( QPalette::Base );
1432         }
1433 
1434         // Is there something selected in the line?
1435         int sel_start, sel_end;
1436         bool isSelection =
1437             selection_.getPortionForLine( i, &sel_start, &sel_end );
1438         // Has the line got elements to be highlighted
1439         QList<QuickFindMatch> qfMatchList;
1440         bool isMatch =
1441             quickFindPattern_->matchLine( line, qfMatchList );
1442 
1443         if ( isSelection || isMatch ) {
1444             // We use the LineDrawer and its chunks because the
1445             // line has to be somehow highlighted
1446             LineDrawer lineDrawer( backColor );
1447 
1448             // First we create a list of chunks with the highlights
1449             QList<LineChunk> chunkList;
1450             int column = 0; // Current column in line space
1451             foreach( const QuickFindMatch match, qfMatchList ) {
1452                 int start = match.startColumn() - firstCol;
1453                 int end = start + match.length();
1454                 // Ignore matches that are *completely* outside view area
1455                 if ( ( start < 0 && end < 0 ) || start >= nbCols )
1456                     continue;
1457                 if ( start > column )
1458                     chunkList << LineChunk( column, start - 1, LineChunk::Normal );
1459                 column = qMin( start + match.length() - 1, nbCols );
1460                 chunkList << LineChunk( qMax( start, 0 ), column,
1461                                         LineChunk::Highlighted );
1462                 column++;
1463             }
1464             if ( column <= cutLine.length() - 1 )
1465                 chunkList << LineChunk( column, cutLine.length() - 1, LineChunk::Normal );
1466 
1467             // Then we add the selection if needed
1468             QList<LineChunk> newChunkList;
1469             if ( isSelection ) {
1470                 sel_start -= firstCol; // coord in line space
1471                 sel_end   -= firstCol;
1472 
1473                 foreach ( const LineChunk chunk, chunkList ) {
1474                     newChunkList << chunk.select( sel_start, sel_end );
1475                 }
1476             }
1477             else
1478                 newChunkList = chunkList;
1479 
1480             foreach ( const LineChunk chunk, newChunkList ) {
1481                 // Select the colours
1482                 QColor fore;
1483                 QColor back;
1484                 switch ( chunk.type() ) {
1485                     case LineChunk::Normal:
1486                         fore = foreColor;
1487                         back = backColor;
1488                         break;
1489                     case LineChunk::Highlighted:
1490                         fore = QColor( "black" );
1491                         back = QColor( "yellow" );
1492                         // fore = highlightForeColor;
1493                         // back = highlightBackColor;
1494                         break;
1495                     case LineChunk::Selected:
1496                         fore = palette.color( QPalette::HighlightedText ),
1497                         back = palette.color( QPalette::Highlight );
1498                         break;
1499                 }
1500                 lineDrawer.addChunk ( chunk, fore, back );
1501             }
1502 
1503             lineDrawer.draw( painter, xPos, yPos,
1504                              viewport()->width(), cutLine,
1505                              CONTENT_MARGIN_WIDTH );
1506         }
1507         else {
1508             // Nothing to be highlighted, we print the whole line!
1509             painter.fillRect( xPos - CONTENT_MARGIN_WIDTH, yPos,
1510                     viewport()->width(), fontHeight, backColor );
1511             // (the rectangle is extended on the left to cover the small
1512             // margin, it looks better (LineDrawer does the same) )
1513             painter.setPen( foreColor );
1514             painter.drawText( xPos, yPos + fontAscent, cutLine );
1515         }
1516 
1517         // Then draw the bullet
1518         painter.setPen( palette.color( QPalette::Text ) );
1519         const int circleSize = 3;
1520         const int arrowHeight = 4;
1521         const int middleXLine = BULLET_AREA_WIDTH / 2;
1522         const int middleYLine = yPos + (fontHeight / 2);
1523 
1524         const LineType line_type = lineType( i );
1525         if ( line_type == Marked ) {
1526             // A pretty arrow if the line is marked
1527             const QPoint points[7] = {
1528                 QPoint(1, middleYLine - 2),
1529                 QPoint(middleXLine, middleYLine - 2),
1530                 QPoint(middleXLine, middleYLine - arrowHeight),
1531                 QPoint(BULLET_AREA_WIDTH - 2, middleYLine),
1532                 QPoint(middleXLine, middleYLine + arrowHeight),
1533                 QPoint(middleXLine, middleYLine + 2),
1534                 QPoint(1, middleYLine + 2 ),
1535             };
1536 
1537             painter.setBrush( markBrush );
1538             painter.drawPolygon( points, 7 );
1539         }
1540         else {
1541             if ( lineType( i ) == Match )
1542                 painter.setBrush( matchBulletBrush );
1543             else
1544                 painter.setBrush( normalBulletBrush );
1545             painter.drawEllipse( middleXLine - circleSize,
1546                     middleYLine - circleSize,
1547                     circleSize * 2, circleSize * 2 );
1548         }
1549 
1550         // Draw the line number
1551         if ( lineNumbersVisible_ ) {
1552             static const QString lineNumberFormat( "%1" );
1553             const QString& lineNumberStr =
1554                 lineNumberFormat.arg( displayLineNumber( i ),
1555                                       nbDigitsInLineNumber_ );
1556             painter.setPen( palette.color( QPalette::Text ) );
1557             painter.drawText( lineNumberAreaStartX + LINE_NUMBER_PADDING,
1558                               yPos + fontAscent, lineNumberStr );
1559         }
1560     } // For each line
1561 }
1562 
1563 // Draw the "pull to follow" bar and return a pixmap.
1564 // The width is passed in "logic" pixels.
1565 QPixmap AbstractLogView::drawPullToFollowBar( int width, float pixel_ratio )
1566 {
1567     static constexpr int barWidth = 40;
1568     QPixmap pixmap ( static_cast<float>( width ) * pixel_ratio, barWidth * 6.0 );
1569     pixmap.setDevicePixelRatio( pixel_ratio );
1570     pixmap.fill( this->palette().color( this->backgroundRole() ) );
1571     const int nbBars = width / (barWidth * 2) + 1;
1572 
1573     QPainter painter( &pixmap );
1574     painter.setPen( QPen( QColor( 0, 0, 0, 0 ) ) );
1575     painter.setBrush( QBrush( QColor( "lightyellow" ) ) );
1576 
1577     for ( int i = 0; i < nbBars; ++i ) {
1578         QPoint points[4] = {
1579             { (i*2+1)*barWidth, 0 },
1580             { 0, (i*2+1)*barWidth },
1581             { 0, (i+1)*2*barWidth },
1582             { (i+1)*2*barWidth, 0 }
1583         };
1584         painter.drawConvexPolygon( points, 4 );
1585     }
1586 
1587     return pixmap;
1588 }
1589 
1590 namespace {
1591 
1592 // Convert the length of the pull to follow bar to pixels
1593 int mapPullToFollowLength( int length )
1594 {
1595     return length / 14;
1596 }
1597 
1598 };
1599