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