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