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
countDigits(quint64 n)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
LineChunk(int first_col,int last_col,ChunkType type)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
select(int sel_start,int sel_end) const109 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
addChunk(int first_col,int last_col,QColor fore,QColor back)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
addChunk(const LineChunk & chunk,QColor fore,QColor back)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
draw(QPainter & painter,int initialXPos,int initialYPos,int line_width,const QString & line,int leftExtraBackgroundPx)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
DigitsBuffer()203 DigitsBuffer::DigitsBuffer() : QObject()
204 {
205 }
206
reset()207 void DigitsBuffer::reset()
208 {
209 LOG(logDEBUG) << "DigitsBuffer::reset()";
210
211 timer_.stop();
212 digits_.clear();
213 }
214
add(char character)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
content()223 int DigitsBuffer::content()
224 {
225 int result = digits_.toInt();
226 reset();
227
228 return result;
229 }
230
timerEvent(QTimerEvent * event)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
AbstractLogView(const AbstractLogData * newLogData,const QuickFindPattern * const quickFindPattern,QWidget * parent)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
~AbstractLogView()297 AbstractLogView::~AbstractLogView()
298 {
299 }
300
301
302 //
303 // Received events
304 //
305
changeEvent(QEvent * changeEvent)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
mousePressEvent(QMouseEvent * mouseEvent)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
mouseMoveEvent(QMouseEvent * mouseEvent)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
mouseReleaseEvent(QMouseEvent * mouseEvent)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
mouseDoubleClickEvent(QMouseEvent * mouseEvent)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
timerEvent(QTimerEvent * timerEvent)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
keyPressEvent(QKeyEvent * keyEvent)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
wheelEvent(QWheelEvent * wheelEvent)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
resizeEvent(QResizeEvent *)686 void AbstractLogView::resizeEvent( QResizeEvent* )
687 {
688 if ( logData == NULL )
689 return;
690
691 LOG(logDEBUG) << "resizeEvent received";
692
693 updateDisplaySize();
694 }
695
event(QEvent * e)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
scrollContentsBy(int dx,int dy)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
paintEvent(QPaintEvent * paintEvent)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.
displayLineNumber(int lineNumber) const854 qint64 AbstractLogView::displayLineNumber( int lineNumber ) const
855 {
856 return lineNumber + 1; // show a 1-based index
857 }
858
maxDisplayLineNumber() const859 qint64 AbstractLogView::maxDisplayLineNumber() const
860 {
861 return logData->getNbLine();
862 }
863
setOverview(Overview * overview,OverviewWidget * overview_widget)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, SLOT( jumpToLine( int ) ) );
873 }
874 refreshOverview();
875 }
876
getViewPosition() const877 LineNumber AbstractLogView::getViewPosition() const
878 {
879 LineNumber line;
880
881 qint64 m_line = selection_.selectedLine();
882 if ( m_line >= 0 ) {
883 line = m_line;
884 }
885 else {
886 // Middle of the view
887 line = firstLine + getNbVisibleLines() / 2;
888 }
889
890 return line;
891 }
892
searchUsingFunction(qint64 (QuickFind::* search_function)())893 void AbstractLogView::searchUsingFunction(
894 qint64 (QuickFind::*search_function)() )
895 {
896 disableFollow();
897
898 int line = (quickFind_.*search_function)();
899 if ( line >= 0 ) {
900 LOG(logDEBUG) << "Found line " << line;
901 displayLine( line );
902 emit updateLineNumber( line );
903 }
904 }
905
searchForward()906 void AbstractLogView::searchForward()
907 {
908 searchUsingFunction( &QuickFind::searchForward );
909 }
910
searchBackward()911 void AbstractLogView::searchBackward()
912 {
913 searchUsingFunction( &QuickFind::searchBackward );
914 }
915
incrementallySearchForward()916 void AbstractLogView::incrementallySearchForward()
917 {
918 searchUsingFunction( &QuickFind::incrementallySearchForward );
919 }
920
incrementallySearchBackward()921 void AbstractLogView::incrementallySearchBackward()
922 {
923 searchUsingFunction( &QuickFind::incrementallySearchBackward );
924 }
925
incrementalSearchAbort()926 void AbstractLogView::incrementalSearchAbort()
927 {
928 quickFind_.incrementalSearchAbort();
929 emit changeQuickFind(
930 "",
931 QuickFindMux::Forward );
932 }
933
incrementalSearchStop()934 void AbstractLogView::incrementalSearchStop()
935 {
936 quickFind_.incrementalSearchStop();
937 }
938
followSet(bool checked)939 void AbstractLogView::followSet( bool checked )
940 {
941 followMode_ = checked;
942 followElasticHook_.hook( checked );
943 update();
944 if ( checked )
945 jumpToBottom();
946 }
947
refreshOverview()948 void AbstractLogView::refreshOverview()
949 {
950 assert( overviewWidget_ );
951
952 // Create space for the Overview if needed
953 if ( ( getOverview() != NULL ) && getOverview()->isVisible() ) {
954 setViewportMargins( 0, 0, OVERVIEW_WIDTH, 0 );
955 overviewWidget_->show();
956 }
957 else {
958 setViewportMargins( 0, 0, 0, 0 );
959 overviewWidget_->hide();
960 }
961 }
962
963 // Reset the QuickFind when the pattern is changed.
handlePatternUpdated()964 void AbstractLogView::handlePatternUpdated()
965 {
966 LOG(logDEBUG) << "AbstractLogView::handlePatternUpdated()";
967
968 quickFind_.resetLimits();
969 update();
970 }
971
972 // OR the current with the current search expression
addToSearch()973 void AbstractLogView::addToSearch()
974 {
975 if ( selection_.isPortion() ) {
976 LOG(logDEBUG) << "AbstractLogView::addToSearch()";
977 emit addToSearch( selection_.getSelectedText( logData ) );
978 }
979 else {
980 LOG(logERROR) << "AbstractLogView::addToSearch called for a wrong type of selection";
981 }
982 }
983
984 // Find next occurence of the selected text (*)
findNextSelected()985 void AbstractLogView::findNextSelected()
986 {
987 // Use the selected 'word' and search forward
988 if ( selection_.isPortion() ) {
989 emit changeQuickFind(
990 selection_.getSelectedText( logData ),
991 QuickFindMux::Forward );
992 emit searchNext();
993 }
994 }
995
996 // Find next previous of the selected text (#)
findPreviousSelected()997 void AbstractLogView::findPreviousSelected()
998 {
999 if ( selection_.isPortion() ) {
1000 emit changeQuickFind(
1001 selection_.getSelectedText( logData ),
1002 QuickFindMux::Backward );
1003 emit searchNext();
1004 }
1005 }
1006
1007 // Copy the selection to the clipboard
copy()1008 void AbstractLogView::copy()
1009 {
1010 static QClipboard* clipboard = QApplication::clipboard();
1011
1012 clipboard->setText( selection_.getSelectedText( logData ) );
1013 }
1014
1015 //
1016 // Public functions
1017 //
1018
updateData()1019 void AbstractLogView::updateData()
1020 {
1021 LOG(logDEBUG) << "AbstractLogView::updateData";
1022
1023 // Check the top Line is within range
1024 if ( firstLine >= logData->getNbLine() ) {
1025 firstLine = 0;
1026 firstCol = 0;
1027 verticalScrollBar()->setValue( 0 );
1028 horizontalScrollBar()->setValue( 0 );
1029 }
1030
1031 // Crop selection if it become out of range
1032 selection_.crop( logData->getNbLine() - 1 );
1033
1034 // Adapt the scroll bars to the new content
1035 updateScrollBars();
1036
1037 // Calculate the index of the last line shown
1038 LineNumber last_line = std::min( static_cast<int64_t>( logData->getNbLine() ),
1039 static_cast<int64_t>( firstLine + getNbVisibleLines() ) );
1040
1041 // Reset the QuickFind in case we have new stuff to search into
1042 quickFind_.resetLimits();
1043
1044 if ( followMode_ )
1045 jumpToBottom();
1046
1047 // Update the overview if we have one
1048 if ( overview_ != NULL )
1049 overview_->updateCurrentPosition( firstLine, last_line );
1050
1051 // Invalidate our cache
1052 textAreaCache_.invalid_ = true;
1053
1054 // Repaint!
1055 update();
1056 }
1057
updateDisplaySize()1058 void AbstractLogView::updateDisplaySize()
1059 {
1060 // Font is assumed to be mono-space (is restricted by options dialog)
1061 QFontMetrics fm = fontMetrics();
1062 charHeight_ = fm.height();
1063 // For some reason on Qt 4.8.2 for Win, maxWidth() is wrong but the
1064 // following give the right result, not sure why:
1065 charWidth_ = fm.width( QChar('a') );
1066
1067 // Update the scroll bars
1068 updateScrollBars();
1069 verticalScrollBar()->setPageStep( getNbVisibleLines() );
1070
1071 if ( followMode_ )
1072 jumpToBottom();
1073
1074 LOG(logDEBUG) << "viewport.width()=" << viewport()->width();
1075 LOG(logDEBUG) << "viewport.height()=" << viewport()->height();
1076 LOG(logDEBUG) << "width()=" << width();
1077 LOG(logDEBUG) << "height()=" << height();
1078
1079 if ( overviewWidget_ )
1080 overviewWidget_->setGeometry( viewport()->width() + 2, 1,
1081 OVERVIEW_WIDTH - 1, viewport()->height() );
1082
1083 // Our text area cache is now invalid
1084 textAreaCache_.invalid_ = true;
1085 textAreaCache_.pixmap_ = QPixmap {
1086 viewport()->width() * viewport()->devicePixelRatio(),
1087 static_cast<int32_t>( getNbVisibleLines() ) * charHeight_ * viewport()->devicePixelRatio() };
1088 textAreaCache_.pixmap_.setDevicePixelRatio( viewport()->devicePixelRatio() );
1089 }
1090
getTopLine() const1091 int AbstractLogView::getTopLine() const
1092 {
1093 return firstLine;
1094 }
1095
getSelection() const1096 QString AbstractLogView::getSelection() const
1097 {
1098 return selection_.getSelectedText( logData );
1099 }
1100
selectAll()1101 void AbstractLogView::selectAll()
1102 {
1103 selection_.selectRange( 0, logData->getNbLine() - 1 );
1104 textAreaCache_.invalid_ = true;
1105 update();
1106 }
1107
selectAndDisplayLine(int line)1108 void AbstractLogView::selectAndDisplayLine( int line )
1109 {
1110 disableFollow();
1111 selection_.selectLine( line );
1112 displayLine( line );
1113 emit updateLineNumber( line );
1114 emit newSelection( line );
1115 }
1116
1117 // The difference between this function and displayLine() is quite
1118 // subtle: this one always jump, even if the line passed is visible.
jumpToLine(int line)1119 void AbstractLogView::jumpToLine( int line )
1120 {
1121 // Put the selected line in the middle if possible
1122 int newTopLine = line - ( getNbVisibleLines() / 2 );
1123 if ( newTopLine < 0 )
1124 newTopLine = 0;
1125
1126 // This will also trigger a scrollContents event
1127 verticalScrollBar()->setValue( newTopLine );
1128 }
1129
setLineNumbersVisible(bool lineNumbersVisible)1130 void AbstractLogView::setLineNumbersVisible( bool lineNumbersVisible )
1131 {
1132 lineNumbersVisible_ = lineNumbersVisible;
1133 }
1134
forceRefresh()1135 void AbstractLogView::forceRefresh()
1136 {
1137 // Invalidate our cache
1138 textAreaCache_.invalid_ = true;
1139 }
1140
1141 //
1142 // Private functions
1143 //
1144
1145 // Returns the number of lines visible in the viewport
getNbVisibleLines() const1146 LineNumber AbstractLogView::getNbVisibleLines() const
1147 {
1148 return static_cast<LineNumber>( viewport()->height() / charHeight_ + 1 );
1149 }
1150
1151 // Returns the number of columns visible in the viewport
getNbVisibleCols() const1152 int AbstractLogView::getNbVisibleCols() const
1153 {
1154 return ( viewport()->width() - leftMarginPx_ ) / charWidth_ + 1;
1155 }
1156
1157 // Converts the mouse x, y coordinates to the line number in the file
convertCoordToLine(int yPos) const1158 int AbstractLogView::convertCoordToLine(int yPos) const
1159 {
1160 int line = firstLine + ( yPos - drawingTopOffset_ ) / charHeight_;
1161
1162 return line;
1163 }
1164
1165 // Converts the mouse x, y coordinates to the char coordinates (in the file)
1166 // This function ensure the pos exists in the file.
convertCoordToFilePos(const QPoint & pos) const1167 QPoint AbstractLogView::convertCoordToFilePos( const QPoint& pos ) const
1168 {
1169 int line = convertCoordToLine( pos.y() );
1170 if ( line >= logData->getNbLine() )
1171 line = logData->getNbLine() - 1;
1172 if ( line < 0 )
1173 line = 0;
1174
1175 // Determine column in screen space and convert it to file space
1176 int column = firstCol + ( pos.x() - leftMarginPx_ ) / charWidth_;
1177
1178 QString this_line = logData->getExpandedLineString( line );
1179 const int length = this_line.length();
1180
1181 if ( column >= length )
1182 column = length - 1;
1183 if ( column < 0 )
1184 column = 0;
1185
1186 LOG(logDEBUG4) << "AbstractLogView::convertCoordToFilePos col="
1187 << column << " line=" << line;
1188 QPoint point( column, line );
1189
1190 return point;
1191 }
1192
1193 // Makes the widget adjust itself to display the passed line.
1194 // Doing so, it will throw itself a scrollContents event.
displayLine(LineNumber line)1195 void AbstractLogView::displayLine( LineNumber line )
1196 {
1197 // If the line is already the screen
1198 if ( ( line >= firstLine ) &&
1199 ( line < ( firstLine + getNbVisibleLines() ) ) ) {
1200 // Invalidate our cache
1201 textAreaCache_.invalid_ = true;
1202
1203 // ... don't scroll and just repaint
1204 update();
1205 } else {
1206 jumpToLine( line );
1207 }
1208 }
1209
1210 // Move the selection up and down by the passed number of lines
moveSelection(int delta)1211 void AbstractLogView::moveSelection( int delta )
1212 {
1213 LOG(logDEBUG) << "AbstractLogView::moveSelection delta=" << delta;
1214
1215 QList<int> selection = selection_.getLines();
1216 int new_line;
1217
1218 // If nothing is selected, do as if line -1 was.
1219 if ( selection.isEmpty() )
1220 selection.append( -1 );
1221
1222 if ( delta < 0 )
1223 new_line = selection.first() + delta;
1224 else
1225 new_line = selection.last() + delta;
1226
1227 if ( new_line < 0 )
1228 new_line = 0;
1229 else if ( new_line >= logData->getNbLine() )
1230 new_line = logData->getNbLine() - 1;
1231
1232 // Select and display the new line
1233 selection_.selectLine( new_line );
1234 displayLine( new_line );
1235 emit updateLineNumber( new_line );
1236 emit newSelection( new_line );
1237 }
1238
1239 // Make the start of the lines visible
jumpToStartOfLine()1240 void AbstractLogView::jumpToStartOfLine()
1241 {
1242 horizontalScrollBar()->setValue( 0 );
1243 }
1244
1245 // Make the end of the lines in the selection visible
jumpToEndOfLine()1246 void AbstractLogView::jumpToEndOfLine()
1247 {
1248 QList<int> selection = selection_.getLines();
1249
1250 // Search the longest line in the selection
1251 int max_length = 0;
1252 foreach ( int line, selection ) {
1253 int length = logData->getLineLength( line );
1254 if ( length > max_length )
1255 max_length = length;
1256 }
1257
1258 horizontalScrollBar()->setValue( max_length - getNbVisibleCols() );
1259 }
1260
1261 // Make the end of the lines on the screen visible
jumpToRightOfScreen()1262 void AbstractLogView::jumpToRightOfScreen()
1263 {
1264 QList<int> selection = selection_.getLines();
1265
1266 // Search the longest line on screen
1267 int max_length = 0;
1268 for ( auto i = firstLine; i <= ( firstLine + getNbVisibleLines() ); i++ ) {
1269 int length = logData->getLineLength( i );
1270 if ( length > max_length )
1271 max_length = length;
1272 }
1273
1274 horizontalScrollBar()->setValue( max_length - getNbVisibleCols() );
1275 }
1276
1277 // Jump to the first line
jumpToTop()1278 void AbstractLogView::jumpToTop()
1279 {
1280 // This will also trigger a scrollContents event
1281 verticalScrollBar()->setValue( 0 );
1282 update(); // in case the screen hasn't moved
1283 }
1284
1285 // Jump to the last line
jumpToBottom()1286 void AbstractLogView::jumpToBottom()
1287 {
1288 const int new_top_line =
1289 qMax( logData->getNbLine() - getNbVisibleLines() + 1, 0LL );
1290
1291 // This will also trigger a scrollContents event
1292 verticalScrollBar()->setValue( new_top_line );
1293 update(); // in case the screen hasn't moved
1294 }
1295
1296 // Returns whether the character passed is a 'word' character
isCharWord(char c)1297 inline bool AbstractLogView::isCharWord( char c )
1298 {
1299 if ( ( ( c >= 'A' ) && ( c <= 'Z' ) ) ||
1300 ( ( c >= 'a' ) && ( c <= 'z' ) ) ||
1301 ( ( c >= '0' ) && ( c <= '9' ) ) ||
1302 ( ( c == '_' ) ) )
1303 return true;
1304 else
1305 return false;
1306 }
1307
1308 // Select the word under the given position
selectWordAtPosition(const QPoint & pos)1309 void AbstractLogView::selectWordAtPosition( const QPoint& pos )
1310 {
1311 const int x = pos.x();
1312 const QString line = logData->getExpandedLineString( pos.y() );
1313
1314 if ( isCharWord( line[x].toLatin1() ) ) {
1315 // Search backward for the first character in the word
1316 int currentPos = x;
1317 for ( ; currentPos > 0; currentPos-- )
1318 if ( ! isCharWord( line[currentPos].toLatin1() ) )
1319 break;
1320 // Exclude the first char of the line if needed
1321 if ( ! isCharWord( line[currentPos].toLatin1() ) )
1322 currentPos++;
1323 int start = currentPos;
1324
1325 // Now search for the end
1326 currentPos = x;
1327 for ( ; currentPos < line.length() - 1; currentPos++ )
1328 if ( ! isCharWord( line[currentPos].toLatin1() ) )
1329 break;
1330 // Exclude the last char of the line if needed
1331 if ( ! isCharWord( line[currentPos].toLatin1() ) )
1332 currentPos--;
1333 int end = currentPos;
1334
1335 selection_.selectPortion( pos.y(), start, end );
1336 updateGlobalSelection();
1337 update();
1338 }
1339 }
1340
1341 // Update the system global (middle click) selection (X11 only)
updateGlobalSelection()1342 void AbstractLogView::updateGlobalSelection()
1343 {
1344 static QClipboard* const clipboard = QApplication::clipboard();
1345
1346 // Updating it only for "non-trivial" (range or portion) selections
1347 if ( ! selection_.isSingleLine() )
1348 clipboard->setText( selection_.getSelectedText( logData ),
1349 QClipboard::Selection );
1350 }
1351
1352 // Create the pop-up menu
createMenu()1353 void AbstractLogView::createMenu()
1354 {
1355 copyAction_ = new QAction( tr("&Copy"), this );
1356 // No text as this action title depends on the type of selection
1357 connect( copyAction_, SIGNAL(triggered()), this, SLOT(copy()) );
1358
1359 // For '#' and '*', shortcuts doesn't seem to work but
1360 // at least it displays them in the menu, we manually handle those keys
1361 // as keys event anyway (in keyPressEvent).
1362 findNextAction_ = new QAction(tr("Find &next"), this);
1363 findNextAction_->setShortcut( Qt::Key_Asterisk );
1364 findNextAction_->setStatusTip( tr("Find the next occurence") );
1365 connect( findNextAction_, SIGNAL(triggered()),
1366 this, SLOT( findNextSelected() ) );
1367
1368 findPreviousAction_ = new QAction( tr("Find &previous"), this );
1369 findPreviousAction_->setShortcut( tr("#") );
1370 findPreviousAction_->setStatusTip( tr("Find the previous occurence") );
1371 connect( findPreviousAction_, SIGNAL(triggered()),
1372 this, SLOT( findPreviousSelected() ) );
1373
1374 addToSearchAction_ = new QAction( tr("&Add to search"), this );
1375 addToSearchAction_->setStatusTip(
1376 tr("Add the selection to the current search") );
1377 connect( addToSearchAction_, SIGNAL( triggered() ),
1378 this, SLOT( addToSearch() ) );
1379
1380 popupMenu_ = new QMenu( this );
1381 popupMenu_->addAction( copyAction_ );
1382 popupMenu_->addSeparator();
1383 popupMenu_->addAction( findNextAction_ );
1384 popupMenu_->addAction( findPreviousAction_ );
1385 popupMenu_->addAction( addToSearchAction_ );
1386 }
1387
considerMouseHovering(int x_pos,int y_pos)1388 void AbstractLogView::considerMouseHovering( int x_pos, int y_pos )
1389 {
1390 int line = convertCoordToLine( y_pos );
1391 if ( ( x_pos < leftMarginPx_ )
1392 && ( line >= 0 )
1393 && ( line < logData->getNbLine() ) ) {
1394 // Mouse moved in the margin, send event up
1395 // (possibly to highlight the overview)
1396 if ( line != lastHoveredLine_ ) {
1397 LOG(logDEBUG) << "Mouse moved in margin line: " << line;
1398 emit mouseHoveredOverLine( line );
1399 lastHoveredLine_ = line;
1400 }
1401 }
1402 else {
1403 if ( lastHoveredLine_ != -1 ) {
1404 emit mouseLeftHoveringZone();
1405 lastHoveredLine_ = -1;
1406 }
1407 }
1408 }
1409
updateScrollBars()1410 void AbstractLogView::updateScrollBars()
1411 {
1412 verticalScrollBar()->setRange( 0, std::max( 0LL,
1413 logData->getNbLine() - getNbVisibleLines() + 1 ) );
1414
1415 const int hScrollMaxValue = std::max( 0,
1416 logData->getMaxLength() - getNbVisibleCols() + 1 );
1417 horizontalScrollBar()->setRange( 0, hScrollMaxValue );
1418 }
1419
drawTextArea(QPaintDevice * paint_device,int32_t)1420 void AbstractLogView::drawTextArea( QPaintDevice* paint_device, int32_t )
1421 {
1422 // LOG( logDEBUG ) << "devicePixelRatio: " << viewport()->devicePixelRatio();
1423 // LOG( logDEBUG ) << "viewport size: " << viewport()->size().width();
1424 // LOG( logDEBUG ) << "pixmap size: " << textPixmap.width();
1425 // Repaint the viewport
1426 QPainter painter( paint_device );
1427 // LOG( logDEBUG ) << "font: " << viewport()->font().family().toStdString();
1428 // LOG( logDEBUG ) << "font painter: " << painter.font().family().toStdString();
1429
1430 painter.setFont( this->font() );
1431
1432 const int fontHeight = charHeight_;
1433 const int fontAscent = painter.fontMetrics().ascent();
1434 const int nbCols = getNbVisibleCols();
1435 const int paintDeviceHeight = paint_device->height() / viewport()->devicePixelRatio();
1436 const int paintDeviceWidth = paint_device->width() / viewport()->devicePixelRatio();
1437 const QPalette& palette = viewport()->palette();
1438 std::shared_ptr<const FilterSet> filterSet =
1439 Persistent<FilterSet>( "filterSet" );
1440 QColor foreColor, backColor;
1441
1442 static const QBrush normalBulletBrush = QBrush( Qt::white );
1443 static const QBrush matchBulletBrush = QBrush( Qt::red );
1444 static const QBrush markBrush = QBrush( "dodgerblue" );
1445
1446 static const int SEPARATOR_WIDTH = 1;
1447 static const qreal BULLET_AREA_WIDTH = 11;
1448 static const int CONTENT_MARGIN_WIDTH = 1;
1449 static const int LINE_NUMBER_PADDING = 3;
1450
1451 // First check the lines to be drawn are within range (might not be the case if
1452 // the file has just changed)
1453 const int64_t lines_in_file = logData->getNbLine();
1454
1455 if ( firstLine > lines_in_file )
1456 firstLine = lines_in_file ? lines_in_file - 1 : 0;
1457
1458 const int64_t nbLines = std::min(
1459 static_cast<int64_t>( getNbVisibleLines() ), lines_in_file - firstLine );
1460
1461 const int bottomOfTextPx = nbLines * fontHeight;
1462
1463 LOG(logDEBUG) << "drawing lines from " << firstLine << " (" << nbLines << " lines)";
1464 LOG(logDEBUG) << "bottomOfTextPx: " << bottomOfTextPx;
1465 LOG(logDEBUG) << "Height: " << paintDeviceHeight;
1466
1467 // Lines to write
1468 const QStringList lines = logData->getExpandedLines( firstLine, nbLines );
1469
1470 // First draw the bullet left margin
1471 painter.setPen(palette.color(QPalette::Text));
1472 painter.fillRect( 0, 0,
1473 BULLET_AREA_WIDTH, paintDeviceHeight,
1474 Qt::darkGray );
1475
1476 // Column at which the content should start (pixels)
1477 qreal contentStartPosX = BULLET_AREA_WIDTH + SEPARATOR_WIDTH;
1478
1479 // This is also the bullet zone width, used for marking clicks
1480 bulletZoneWidthPx_ = contentStartPosX;
1481
1482 // Update the length of line numbers
1483 const int nbDigitsInLineNumber = countDigits( maxDisplayLineNumber() );
1484
1485 // Draw the line numbers area
1486 int lineNumberAreaStartX = 0;
1487 if ( lineNumbersVisible_ ) {
1488 int lineNumberWidth = charWidth_ * nbDigitsInLineNumber;
1489 int lineNumberAreaWidth =
1490 2 * LINE_NUMBER_PADDING + lineNumberWidth;
1491 lineNumberAreaStartX = contentStartPosX;
1492
1493 painter.setPen(palette.color(QPalette::Text));
1494 /* Not sure if it looks good...
1495 painter.drawLine( contentStartPosX + lineNumberAreaWidth,
1496 0,
1497 contentStartPosX + lineNumberAreaWidth,
1498 viewport()->height() );
1499 */
1500 painter.fillRect( contentStartPosX - SEPARATOR_WIDTH, 0,
1501 lineNumberAreaWidth + SEPARATOR_WIDTH, paintDeviceHeight,
1502 Qt::lightGray );
1503
1504 // Update for drawing the actual text
1505 contentStartPosX += lineNumberAreaWidth;
1506 }
1507 else {
1508 painter.fillRect( contentStartPosX - SEPARATOR_WIDTH, 0,
1509 SEPARATOR_WIDTH + 1, paintDeviceHeight,
1510 Qt::lightGray );
1511 // contentStartPosX += SEPARATOR_WIDTH;
1512 }
1513
1514 painter.drawLine( BULLET_AREA_WIDTH, 0,
1515 BULLET_AREA_WIDTH, paintDeviceHeight - 1 );
1516
1517 // This is the total width of the 'margin' (including line number if any)
1518 // used for mouse calculation etc...
1519 leftMarginPx_ = contentStartPosX + SEPARATOR_WIDTH;
1520
1521 // Then draw each line
1522 for (int i = 0; i < nbLines; i++) {
1523 const LineNumber line_index = i + firstLine;
1524
1525 // Position in pixel of the base line of the line to print
1526 const int yPos = i * fontHeight;
1527 const int xPos = contentStartPosX + CONTENT_MARGIN_WIDTH;
1528
1529 // string to print, cut to fit the length and position of the view
1530 const QString line = lines[i];
1531 const QString cutLine = line.mid( firstCol, nbCols );
1532
1533 if ( selection_.isLineSelected( line_index ) ) {
1534 // Reverse the selected line
1535 foreColor = palette.color( QPalette::HighlightedText );
1536 backColor = palette.color( QPalette::Highlight );
1537 painter.setPen(palette.color(QPalette::Text));
1538 }
1539 else if ( filterSet->matchLine( logData->getLineString( line_index ),
1540 &foreColor, &backColor ) ) {
1541 // Apply a filter to the line
1542 }
1543 else {
1544 // Use the default colors
1545 foreColor = palette.color( QPalette::Text );
1546 backColor = palette.color( QPalette::Base );
1547 }
1548
1549 // Is there something selected in the line?
1550 int sel_start, sel_end;
1551 bool isSelection =
1552 selection_.getPortionForLine( line_index, &sel_start, &sel_end );
1553 // Has the line got elements to be highlighted
1554 QList<QuickFindMatch> qfMatchList;
1555 bool isMatch =
1556 quickFindPattern_->matchLine( line, qfMatchList );
1557
1558 if ( isSelection || isMatch ) {
1559 // We use the LineDrawer and its chunks because the
1560 // line has to be somehow highlighted
1561 LineDrawer lineDrawer( backColor );
1562
1563 // First we create a list of chunks with the highlights
1564 QList<LineChunk> chunkList;
1565 int column = 0; // Current column in line space
1566 foreach( const QuickFindMatch match, qfMatchList ) {
1567 int start = match.startColumn() - firstCol;
1568 int end = start + match.length();
1569 // Ignore matches that are *completely* outside view area
1570 if ( ( start < 0 && end < 0 ) || start >= nbCols )
1571 continue;
1572 if ( start > column )
1573 chunkList << LineChunk( column, start - 1, LineChunk::Normal );
1574 column = qMin( start + match.length() - 1, nbCols );
1575 chunkList << LineChunk( qMax( start, 0 ), column,
1576 LineChunk::Highlighted );
1577 column++;
1578 }
1579 if ( column <= cutLine.length() - 1 )
1580 chunkList << LineChunk( column, cutLine.length() - 1, LineChunk::Normal );
1581
1582 // Then we add the selection if needed
1583 QList<LineChunk> newChunkList;
1584 if ( isSelection ) {
1585 sel_start -= firstCol; // coord in line space
1586 sel_end -= firstCol;
1587
1588 foreach ( const LineChunk chunk, chunkList ) {
1589 newChunkList << chunk.select( sel_start, sel_end );
1590 }
1591 }
1592 else
1593 newChunkList = chunkList;
1594
1595 foreach ( const LineChunk chunk, newChunkList ) {
1596 // Select the colours
1597 QColor fore;
1598 QColor back;
1599 switch ( chunk.type() ) {
1600 case LineChunk::Normal:
1601 fore = foreColor;
1602 back = backColor;
1603 break;
1604 case LineChunk::Highlighted:
1605 fore = QColor( "black" );
1606 back = QColor( "yellow" );
1607 // fore = highlightForeColor;
1608 // back = highlightBackColor;
1609 break;
1610 case LineChunk::Selected:
1611 fore = palette.color( QPalette::HighlightedText ),
1612 back = palette.color( QPalette::Highlight );
1613 break;
1614 }
1615 lineDrawer.addChunk ( chunk, fore, back );
1616 }
1617
1618 lineDrawer.draw( painter, xPos, yPos,
1619 viewport()->width(), cutLine,
1620 CONTENT_MARGIN_WIDTH );
1621 }
1622 else {
1623 // Nothing to be highlighted, we print the whole line!
1624 painter.fillRect( xPos - CONTENT_MARGIN_WIDTH, yPos,
1625 viewport()->width(), fontHeight, backColor );
1626 // (the rectangle is extended on the left to cover the small
1627 // margin, it looks better (LineDrawer does the same) )
1628 painter.setPen( foreColor );
1629 painter.drawText( xPos, yPos + fontAscent, cutLine );
1630 }
1631
1632 // Then draw the bullet
1633 painter.setPen( palette.color( QPalette::Text ) );
1634 const qreal circleSize = 3;
1635 const qreal arrowHeight = 4;
1636 const qreal middleXLine = BULLET_AREA_WIDTH / 2;
1637 const qreal middleYLine = yPos + (fontHeight / 2);
1638
1639 const LineType line_type = lineType( line_index );
1640 if ( line_type == Marked ) {
1641 // A pretty arrow if the line is marked
1642 const QPointF points[7] = {
1643 QPointF(1, middleYLine - 2),
1644 QPointF(middleXLine, middleYLine - 2),
1645 QPointF(middleXLine, middleYLine - arrowHeight),
1646 QPointF(BULLET_AREA_WIDTH - 1, middleYLine),
1647 QPointF(middleXLine, middleYLine + arrowHeight),
1648 QPointF(middleXLine, middleYLine + 2),
1649 QPointF(1, middleYLine + 2 ),
1650 };
1651
1652 painter.setBrush( markBrush );
1653 painter.drawPolygon( points, 7 );
1654 }
1655 else {
1656 // For pretty circles
1657 painter.setRenderHint( QPainter::Antialiasing );
1658
1659 if ( lineType( line_index ) == Match )
1660 painter.setBrush( matchBulletBrush );
1661 else
1662 painter.setBrush( normalBulletBrush );
1663 painter.drawEllipse( middleXLine - circleSize,
1664 middleYLine - circleSize,
1665 circleSize * 2, circleSize * 2 );
1666 }
1667
1668 // Draw the line number
1669 if ( lineNumbersVisible_ ) {
1670 static const QString lineNumberFormat( "%1" );
1671 const QString& lineNumberStr =
1672 lineNumberFormat.arg( displayLineNumber( line_index ),
1673 nbDigitsInLineNumber );
1674 painter.setPen( palette.color( QPalette::Text ) );
1675 painter.drawText( lineNumberAreaStartX + LINE_NUMBER_PADDING,
1676 yPos + fontAscent, lineNumberStr );
1677 }
1678 } // For each line
1679
1680 if ( bottomOfTextPx < paintDeviceHeight ) {
1681 // The lines don't cover the whole device
1682 painter.fillRect( contentStartPosX, bottomOfTextPx,
1683 paintDeviceWidth - contentStartPosX,
1684 paintDeviceHeight, palette.color( QPalette::Window ) );
1685 }
1686 }
1687
1688 // Draw the "pull to follow" bar and return a pixmap.
1689 // The width is passed in "logic" pixels.
drawPullToFollowBar(int width,float pixel_ratio)1690 QPixmap AbstractLogView::drawPullToFollowBar( int width, float pixel_ratio )
1691 {
1692 static constexpr int barWidth = 40;
1693 QPixmap pixmap ( static_cast<float>( width ) * pixel_ratio, barWidth * 6.0 );
1694 pixmap.setDevicePixelRatio( pixel_ratio );
1695 pixmap.fill( this->palette().color( this->backgroundRole() ) );
1696 const int nbBars = width / (barWidth * 2) + 1;
1697
1698 QPainter painter( &pixmap );
1699 painter.setPen( QPen( QColor( 0, 0, 0, 0 ) ) );
1700 painter.setBrush( QBrush( QColor( "lightyellow" ) ) );
1701
1702 for ( int i = 0; i < nbBars; ++i ) {
1703 QPoint points[4] = {
1704 { (i*2+1)*barWidth, 0 },
1705 { 0, (i*2+1)*barWidth },
1706 { 0, (i+1)*2*barWidth },
1707 { (i+1)*2*barWidth, 0 }
1708 };
1709 painter.drawConvexPolygon( points, 4 );
1710 }
1711
1712 return pixmap;
1713 }
1714
disableFollow()1715 void AbstractLogView::disableFollow()
1716 {
1717 emit followModeChanged( false );
1718 followElasticHook_.hook( false );
1719 }
1720
1721 namespace {
1722
1723 // Convert the length of the pull to follow bar to pixels
mapPullToFollowLength(int length)1724 int mapPullToFollowLength( int length )
1725 {
1726 return length / 14;
1727 }
1728
1729 };
1730