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