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