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