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