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