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