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