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