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