/* * Copyright (C) 2009, 2010, 2011, 2012, 2013, 2015 Nicolas Bonnefon * and other contributors * * This file is part of glogg. * * glogg is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * glogg is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with glogg. If not, see . */ // This file implements the AbstractLogView base class. // Most of the actual drawing and event management common to the two views // is implemented in this class. The class only calls protected virtual // functions when view specific behaviour is desired, using the template // pattern. #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "log.h" #include "persistentinfo.h" #include "filterset.h" #include "logmainview.h" #include "quickfind.h" #include "quickfindpattern.h" #include "overview.h" #include "configuration.h" namespace { int mapPullToFollowLength( int length ); }; namespace { int countDigits( quint64 n ) { if (n == 0) return 1; // We must force the compiler to not store intermediate results // in registers because this causes incorrect result on some // systems under optimizations level >0. For the skeptical: // // #include // #include // int main(int argc, char **argv) { // (void)argc; // long long int n = atoll(argv[1]); // return floor( log( n ) / log( 10 ) + 1 ); // } // // This is on Thinkpad T60 (Genuine Intel(R) CPU T2300). // $ g++ --version // g++ (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3 // $ g++ -O0 -Wall -W -o math math.cpp -lm; ./math 10; echo $? // 2 // $ g++ -O1 -Wall -W -o math math.cpp -lm; ./math 10; echo $? // 1 // // A fix is to (1) explicitly place intermediate results in // variables *and* (2) [A] mark them as 'volatile', or [B] pass // -ffloat-store to g++ (note that approach [A] is more portable). volatile qreal ln_n = qLn( n ); volatile qreal ln_10 = qLn( 10 ); volatile qreal lg_n = ln_n / ln_10; volatile qreal lg_n_1 = lg_n + 1; volatile qreal fl_lg_n_1 = qFloor( lg_n_1 ); return fl_lg_n_1; } } // anon namespace LineChunk::LineChunk( int first_col, int last_col, ChunkType type ) { // LOG(logDEBUG) << "new LineChunk: " << first_col << " " << last_col; start_ = first_col; end_ = last_col; type_ = type; } QList LineChunk::select( int sel_start, int sel_end ) const { QList list; if ( ( sel_start < start_ ) && ( sel_end < start_ ) ) { // Selection BEFORE this chunk: no change list << LineChunk( *this ); } else if ( sel_start > end_ ) { // Selection AFTER this chunk: no change list << LineChunk( *this ); } else /* if ( ( sel_start >= start_ ) && ( sel_end <= end_ ) ) */ { // We only want to consider what's inside THIS chunk sel_start = qMax( sel_start, start_ ); sel_end = qMin( sel_end, end_ ); if ( sel_start > start_ ) list << LineChunk( start_, sel_start - 1, type_ ); list << LineChunk( sel_start, sel_end, Selected ); if ( sel_end < end_ ) list << LineChunk( sel_end + 1, end_, type_ ); } return list; } inline void LineDrawer::addChunk( int first_col, int last_col, QColor fore, QColor back ) { if ( first_col < 0 ) first_col = 0; int length = last_col - first_col + 1; if ( length > 0 ) { list << Chunk ( first_col, length, fore, back ); } } inline void LineDrawer::addChunk( const LineChunk& chunk, QColor fore, QColor back ) { int first_col = chunk.start(); int last_col = chunk.end(); addChunk( first_col, last_col, fore, back ); } inline void LineDrawer::draw( QPainter& painter, int initialXPos, int initialYPos, int line_width, const QString& line, int leftExtraBackgroundPx ) { QFontMetrics fm = painter.fontMetrics(); const int fontHeight = fm.height(); const int fontAscent = fm.ascent(); // For some reason on Qt 4.8.2 for Win, maxWidth() is wrong but the // following give the right result, not sure why: const int fontWidth = fm.width( QChar('a') ); int xPos = initialXPos; int yPos = initialYPos; foreach ( Chunk chunk, list ) { // Draw each chunk // LOG(logDEBUG) << "Chunk: " << chunk.start() << " " << chunk.length(); QString cutline = line.mid( chunk.start(), chunk.length() ); const int chunk_width = cutline.length() * fontWidth; if ( xPos == initialXPos ) { // First chunk, we extend the left background a bit, // it looks prettier. painter.fillRect( xPos - leftExtraBackgroundPx, yPos, chunk_width + leftExtraBackgroundPx, fontHeight, chunk.backColor() ); } else { // other chunks... painter.fillRect( xPos, yPos, chunk_width, fontHeight, chunk.backColor() ); } painter.setPen( chunk.foreColor() ); painter.drawText( xPos, yPos + fontAscent, cutline ); xPos += chunk_width; } // Draw the empty block at the end of the line int blank_width = line_width - xPos; if ( blank_width > 0 ) painter.fillRect( xPos, yPos, blank_width, fontHeight, backColor_ ); } const int DigitsBuffer::timeout_ = 2000; DigitsBuffer::DigitsBuffer() : QObject() { } void DigitsBuffer::reset() { LOG(logDEBUG) << "DigitsBuffer::reset()"; timer_.stop(); digits_.clear(); } void DigitsBuffer::add( char character ) { LOG(logDEBUG) << "DigitsBuffer::add()"; digits_.append( QChar( character ) ); timer_.start( timeout_ , this ); } int DigitsBuffer::content() { int result = digits_.toInt(); reset(); return result; } void DigitsBuffer::timerEvent( QTimerEvent* event ) { if ( event->timerId() == timer_.timerId() ) { reset(); } else { QObject::timerEvent( event ); } } AbstractLogView::AbstractLogView(const AbstractLogData* newLogData, const QuickFindPattern* const quickFindPattern, QWidget* parent) : QAbstractScrollArea( parent ), followElasticHook_( HOOK_THRESHOLD ), lineNumbersVisible_( false ), selectionStartPos_(), selectionCurrentEndPos_(), autoScrollTimer_(), selection_(), quickFindPattern_( quickFindPattern ), quickFind_( newLogData, &selection_, quickFindPattern ) { logData = newLogData; followMode_ = false; selectionStarted_ = false; markingClickInitiated_ = false; firstLine = 0; lastLineAligned = false; firstCol = 0; overview_ = NULL; overviewWidget_ = NULL; // Display leftMarginPx_ = 0; // Fonts (sensible default for overview widget) charWidth_ = 1; charHeight_ = 10; // Create the viewport QWidget setViewport( 0 ); // Hovering setMouseTracking( true ); lastHoveredLine_ = -1; // Init the popup menu createMenu(); // Signals connect( quickFindPattern_, SIGNAL( patternUpdated() ), this, SLOT ( handlePatternUpdated() ) ); connect( &quickFind_, SIGNAL( notify( const QFNotification& ) ), this, SIGNAL( notifyQuickFind( const QFNotification& ) ) ); connect( &quickFind_, SIGNAL( clearNotification() ), this, SIGNAL( clearQuickFindNotification() ) ); connect( &followElasticHook_, SIGNAL( lengthChanged() ), this, SLOT( repaint() ) ); connect( &followElasticHook_, SIGNAL( hooked( bool ) ), this, SIGNAL( followModeChanged( bool ) ) ); } AbstractLogView::~AbstractLogView() { } // // Received events // void AbstractLogView::changeEvent( QEvent* changeEvent ) { QAbstractScrollArea::changeEvent( changeEvent ); // Stop the timer if the widget becomes inactive if ( changeEvent->type() == QEvent::ActivationChange ) { if ( ! isActiveWindow() ) autoScrollTimer_.stop(); } viewport()->update(); } void AbstractLogView::mousePressEvent( QMouseEvent* mouseEvent ) { static std::shared_ptr config = Persistent( "settings" ); if ( mouseEvent->button() == Qt::LeftButton ) { int line = convertCoordToLine( mouseEvent->y() ); if ( mouseEvent->modifiers() & Qt::ShiftModifier ) { selection_.selectRangeFromPrevious( line ); emit updateLineNumber( line ); update(); } else { if ( mouseEvent->x() < bulletZoneWidthPx_ ) { // Mark a line if it is clicked in the left margin // (only if click and release in the same area) markingClickInitiated_ = true; markingClickLine_ = line; } else { // Select the line, and start a selection if ( line < logData->getNbLine() ) { selection_.selectLine( line ); emit updateLineNumber( line ); emit newSelection( line ); } // Remember the click in case we're starting a selection selectionStarted_ = true; selectionStartPos_ = convertCoordToFilePos( mouseEvent->pos() ); selectionCurrentEndPos_ = selectionStartPos_; } } // Invalidate our cache textAreaCache_.invalid_ = true; } else if ( mouseEvent->button() == Qt::RightButton ) { // Prepare the popup depending on selection type if ( selection_.isSingleLine() ) { copyAction_->setText( "&Copy this line" ); } else { copyAction_->setText( "&Copy" ); copyAction_->setStatusTip( tr("Copy the selection") ); } if ( selection_.isPortion() ) { findNextAction_->setEnabled( true ); findPreviousAction_->setEnabled( true ); addToSearchAction_->setEnabled( true ); } else { findNextAction_->setEnabled( false ); findPreviousAction_->setEnabled( false ); addToSearchAction_->setEnabled( false ); } // "Add to search" only makes sense in regexp mode if ( config->mainRegexpType() != ExtendedRegexp ) addToSearchAction_->setEnabled( false ); // Display the popup (blocking) popupMenu_->exec( QCursor::pos() ); } emit activity(); } void AbstractLogView::mouseMoveEvent( QMouseEvent* mouseEvent ) { // Selection implementation if ( selectionStarted_ ) { // Invalidate our cache textAreaCache_.invalid_ = true; QPoint thisEndPos = convertCoordToFilePos( mouseEvent->pos() ); if ( thisEndPos != selectionCurrentEndPos_ ) { // Are we on a different line? if ( selectionStartPos_.y() != thisEndPos.y() ) { if ( thisEndPos.y() != selectionCurrentEndPos_.y() ) { // This is a 'range' selection selection_.selectRange( selectionStartPos_.y(), thisEndPos.y() ); emit updateLineNumber( thisEndPos.y() ); update(); } } // So we are on the same line. Are we moving horizontaly? else if ( thisEndPos.x() != selectionCurrentEndPos_.x() ) { // This is a 'portion' selection selection_.selectPortion( thisEndPos.y(), selectionStartPos_.x(), thisEndPos.x() ); update(); } // On the same line, and moving vertically then else { // This is a 'line' selection selection_.selectLine( thisEndPos.y() ); emit updateLineNumber( thisEndPos.y() ); update(); } selectionCurrentEndPos_ = thisEndPos; // Do we need to scroll while extending the selection? QRect visible = viewport()->rect(); if ( visible.contains( mouseEvent->pos() ) ) autoScrollTimer_.stop(); else if ( ! autoScrollTimer_.isActive() ) autoScrollTimer_.start( 100, this ); } } else { considerMouseHovering( mouseEvent->x(), mouseEvent->y() ); } } void AbstractLogView::mouseReleaseEvent( QMouseEvent* mouseEvent ) { if ( markingClickInitiated_ ) { markingClickInitiated_ = false; int line = convertCoordToLine( mouseEvent->y() ); if ( line == markingClickLine_ ) { // Invalidate our cache textAreaCache_.invalid_ = true; emit markLine( line ); } } else { selectionStarted_ = false; if ( autoScrollTimer_.isActive() ) autoScrollTimer_.stop(); updateGlobalSelection(); } } void AbstractLogView::mouseDoubleClickEvent( QMouseEvent* mouseEvent ) { if ( mouseEvent->button() == Qt::LeftButton ) { // Invalidate our cache textAreaCache_.invalid_ = true; const QPoint pos = convertCoordToFilePos( mouseEvent->pos() ); selectWordAtPosition( pos ); } emit activity(); } void AbstractLogView::timerEvent( QTimerEvent* timerEvent ) { if ( timerEvent->timerId() == autoScrollTimer_.timerId() ) { QRect visible = viewport()->rect(); const QPoint globalPos = QCursor::pos(); const QPoint pos = viewport()->mapFromGlobal( globalPos ); QMouseEvent ev( QEvent::MouseMove, pos, globalPos, Qt::LeftButton, Qt::LeftButton, Qt::NoModifier ); mouseMoveEvent( &ev ); int deltaX = qMax( pos.x() - visible.left(), visible.right() - pos.x() ) - visible.width(); int deltaY = qMax( pos.y() - visible.top(), visible.bottom() - pos.y() ) - visible.height(); int delta = qMax( deltaX, deltaY ); if ( delta >= 0 ) { if ( delta < 7 ) delta = 7; int timeout = 4900 / ( delta * delta ); autoScrollTimer_.start( timeout, this ); if ( deltaX > 0 ) horizontalScrollBar()->triggerAction( pos.x() 0 ) verticalScrollBar()->triggerAction( pos.y() modifiers() & Qt::ControlModifier) == Qt::ControlModifier; bool shiftModifier = (keyEvent->modifiers() & Qt::ShiftModifier) == Qt::ShiftModifier; bool noModifier = keyEvent->modifiers() == Qt::NoModifier; if ( keyEvent->key() == Qt::Key_Left && noModifier ) horizontalScrollBar()->triggerAction(QScrollBar::SliderPageStepSub); else if ( keyEvent->key() == Qt::Key_Right && noModifier ) horizontalScrollBar()->triggerAction(QScrollBar::SliderPageStepAdd); else if ( keyEvent->key() == Qt::Key_Home && !controlModifier) jumpToStartOfLine(); else if ( keyEvent->key() == Qt::Key_End && !controlModifier) jumpToRightOfScreen(); else if ( (keyEvent->key() == Qt::Key_PageDown && controlModifier) || (keyEvent->key() == Qt::Key_End && controlModifier) ) { disableFollow(); // duplicate of 'G' action. selection_.selectLine( logData->getNbLine() - 1 ); emit updateLineNumber( logData->getNbLine() - 1 ); jumpToBottom(); } else if ( (keyEvent->key() == Qt::Key_PageUp && controlModifier) || (keyEvent->key() == Qt::Key_Home && controlModifier) ) { disableFollow(); // like 'g' but 0 input first line action. selectAndDisplayLine( 0 ); emit updateLineNumber( 0 ); } else if ( keyEvent->key() == Qt::Key_F3 && !shiftModifier ) searchNext(); // duplicate of 'n' action. else if ( keyEvent->key() == Qt::Key_F3 && shiftModifier ) searchPrevious(); // duplicate of 'N' action. else { const char character = (keyEvent->text())[0].toLatin1(); if ( keyEvent->modifiers() == Qt::NoModifier && ( character >= '0' ) && ( character <= '9' ) ) { // Adds the digit to the timed buffer digitsBuffer_.add( character ); } else { switch ( (keyEvent->text())[0].toLatin1() ) { case 'j': { int delta = qMax( 1, digitsBuffer_.content() ); disableFollow(); //verticalScrollBar()->triggerAction( //QScrollBar::SliderSingleStepAdd); moveSelection( delta ); break; } case 'k': { int delta = qMin( -1, - digitsBuffer_.content() ); disableFollow(); //verticalScrollBar()->triggerAction( //QScrollBar::SliderSingleStepSub); moveSelection( delta ); break; } case 'h': horizontalScrollBar()->triggerAction( QScrollBar::SliderSingleStepSub); break; case 'l': horizontalScrollBar()->triggerAction( QScrollBar::SliderSingleStepAdd); break; case '0': jumpToStartOfLine(); break; case '$': jumpToEndOfLine(); break; case 'g': { int newLine = qMax( 0, digitsBuffer_.content() - 1 ); if ( newLine >= logData->getNbLine() ) newLine = logData->getNbLine() - 1; disableFollow(); selectAndDisplayLine( newLine ); emit updateLineNumber( newLine ); break; } case 'G': disableFollow(); selection_.selectLine( logData->getNbLine() - 1 ); emit updateLineNumber( logData->getNbLine() - 1 ); jumpToBottom(); break; case 'n': emit searchNext(); break; case 'N': emit searchPrevious(); break; case '*': // Use the selected 'word' and search forward findNextSelected(); break; case '#': // Use the selected 'word' and search backward findPreviousSelected(); break; default: keyEvent->ignore(); } } } if ( keyEvent->isAccepted() ) { emit activity(); } else { // Only pass bare keys to the superclass this is so that // shortcuts such as Ctrl+Alt+Arrow are handled by the parent. LOG(logDEBUG) << std::hex << keyEvent->modifiers(); if ( keyEvent->modifiers() == Qt::NoModifier || keyEvent->modifiers() == Qt::KeypadModifier ) { QAbstractScrollArea::keyPressEvent( keyEvent ); } } } void AbstractLogView::wheelEvent( QWheelEvent* wheelEvent ) { emit activity(); // LOG(logDEBUG) << "wheelEvent"; // This is to handle the case where follow mode is on, but the user // has moved using the scroll bar. We take them back to the bottom. if ( followMode_ ) jumpToBottom(); int y_delta = 0; if ( verticalScrollBar()->value() == verticalScrollBar()->maximum() ) { // First see if we need to block the elastic (on Mac) if ( wheelEvent->phase() == Qt::ScrollBegin ) followElasticHook_.hold(); else if ( wheelEvent->phase() == Qt::ScrollEnd ) followElasticHook_.release(); auto pixel_delta = wheelEvent->pixelDelta(); if ( pixel_delta.isNull() ) { y_delta = wheelEvent->angleDelta().y() / 0.7; } else { y_delta = pixel_delta.y(); } // LOG(logDEBUG) << "Elastic " << y_delta; followElasticHook_.move( - y_delta ); } // LOG(logDEBUG) << "Length = " << followElasticHook_.length(); if ( followElasticHook_.length() == 0 && !followElasticHook_.isHooked() ) { QAbstractScrollArea::wheelEvent( wheelEvent ); } } void AbstractLogView::resizeEvent( QResizeEvent* ) { if ( logData == NULL ) return; LOG(logDEBUG) << "resizeEvent received"; updateDisplaySize(); } bool AbstractLogView::event( QEvent* e ) { LOG(logDEBUG4) << "Event! Type: " << e->type(); // Make sure we ignore the gesture events as // they seem to be accepted by default. if ( e->type() == QEvent::Gesture ) { auto gesture_event = dynamic_cast( e ); if ( gesture_event ) { foreach( QGesture* gesture, gesture_event->gestures() ) { LOG(logDEBUG4) << "Gesture: " << gesture->gestureType(); gesture_event->ignore( gesture ); } // Ensure the event is sent up to parents who might care return false; } } return QAbstractScrollArea::event( e ); } void AbstractLogView::scrollContentsBy( int dx, int dy ) { LOG(logDEBUG) << "scrollContentsBy received " << dy << "position " << verticalScrollBar()->value(); int32_t last_top_line = ( logData->getNbLine() - getNbVisibleLines() ); if ( ( last_top_line > 0 ) && verticalScrollBar()->value() > last_top_line ) { // The user is going further than the last line, we need to lock the last line at the bottom LOG(logDEBUG) << "scrollContentsBy beyond!"; firstLine = last_top_line; lastLineAligned = true; } else { firstLine = verticalScrollBar()->value(); lastLineAligned = false; } firstCol = (firstCol - dx) > 0 ? firstCol - dx : 0; LineNumber last_line = firstLine + getNbVisibleLines(); // Update the overview if we have one if ( overview_ != NULL ) overview_->updateCurrentPosition( firstLine, last_line ); // Are we hovering over a new line? const QPoint mouse_pos = mapFromGlobal( QCursor::pos() ); considerMouseHovering( mouse_pos.x(), mouse_pos.y() ); // Redraw update(); } void AbstractLogView::paintEvent( QPaintEvent* paintEvent ) { const QRect invalidRect = paintEvent->rect(); if ( (invalidRect.isEmpty()) || (logData == NULL) ) return; LOG(logDEBUG4) << "paintEvent received, firstLine=" << firstLine << " lastLineAligned=" << lastLineAligned << " rect: " << invalidRect.topLeft().x() << ", " << invalidRect.topLeft().y() << ", " << invalidRect.bottomRight().x() << ", " << invalidRect.bottomRight().y(); #ifdef GLOGG_PERF_MEASURE_FPS static uint32_t maxline = logData->getNbLine(); if ( ! perfCounter_.addEvent() && logData->getNbLine() > maxline ) { LOG(logWARNING) << "Redraw per second: " << perfCounter_.readAndReset() << " lines: " << logData->getNbLine(); perfCounter_.addEvent(); maxline = logData->getNbLine(); } #endif auto start = std::chrono::system_clock::now(); // Can we use our cache? int32_t delta_y = textAreaCache_.first_line_ - firstLine; if ( textAreaCache_.invalid_ || ( textAreaCache_.first_column_ != firstCol ) ) { // Force a full redraw delta_y = INT32_MAX; } if ( delta_y != 0 ) { // Full or partial redraw drawTextArea( &textAreaCache_.pixmap_, delta_y ); textAreaCache_.invalid_ = false; textAreaCache_.first_line_ = firstLine; textAreaCache_.first_column_ = firstCol; LOG(logDEBUG) << "End of writing " << std::chrono::duration_cast ( std::chrono::system_clock::now() - start ).count(); } else { // Use the cache as is: nothing to do! } // Height including the potentially invisible last line const int whole_height = getNbVisibleLines() * charHeight_; // Height in pixels of the "pull to follow" bottom bar. int pullToFollowHeight = mapPullToFollowLength( followElasticHook_.length() ) + ( followElasticHook_.isHooked() ? ( whole_height - viewport()->height() ) + PULL_TO_FOLLOW_HOOKED_HEIGHT : 0 ); if ( pullToFollowHeight && ( pullToFollowCache_.nb_columns_ != getNbVisibleCols() ) ) { LOG(logDEBUG) << "Drawing pull to follow bar"; pullToFollowCache_.pixmap_ = drawPullToFollowBar( viewport()->width(), viewport()->devicePixelRatio() ); pullToFollowCache_.nb_columns_ = getNbVisibleCols(); } QPainter devicePainter( viewport() ); int drawingTopPosition = - pullToFollowHeight; int drawingPullToFollowTopPosition = drawingTopPosition + whole_height; // This is to cover the special case where there is less than a screenful // worth of data, we want to see the document from the top, rather than // pushing the first couple of lines above the viewport. if ( followElasticHook_.isHooked() && ( logData->getNbLine() < getNbVisibleLines() ) ) { drawingTopOffset_ = 0; drawingTopPosition += ( whole_height - viewport()->height() ) + PULL_TO_FOLLOW_HOOKED_HEIGHT; drawingPullToFollowTopPosition = drawingTopPosition + viewport()->height() - PULL_TO_FOLLOW_HOOKED_HEIGHT; } // This is the case where the user is on the 'extra' slot at the end // and is aligned on the last line (but no elastic shown) else if ( lastLineAligned && !followElasticHook_.isHooked() ) { drawingTopOffset_ = - ( whole_height - viewport()->height() ); drawingTopPosition += drawingTopOffset_; drawingPullToFollowTopPosition = drawingTopPosition + whole_height; } else { drawingTopOffset_ = - pullToFollowHeight; } devicePainter.drawPixmap( 0, drawingTopPosition, textAreaCache_.pixmap_ ); // Draw the "pull to follow" zone if needed if ( pullToFollowHeight ) { devicePainter.drawPixmap( 0, drawingPullToFollowTopPosition, pullToFollowCache_.pixmap_ ); } LOG(logDEBUG) << "End of repaint " << std::chrono::duration_cast ( std::chrono::system_clock::now() - start ).count(); } // These two functions are virtual and this implementation is clearly // only valid for a non-filtered display. // We count on the 'filtered' derived classes to override them. qint64 AbstractLogView::displayLineNumber( int lineNumber ) const { return lineNumber + 1; // show a 1-based index } qint64 AbstractLogView::maxDisplayLineNumber() const { return logData->getNbLine(); } void AbstractLogView::setOverview( Overview* overview, OverviewWidget* overview_widget ) { overview_ = overview; overviewWidget_ = overview_widget; if ( overviewWidget_ ) { connect( overviewWidget_, SIGNAL( lineClicked ( int ) ), this, SIGNAL( followDisabled() ) ); connect( overviewWidget_, SIGNAL( lineClicked ( int ) ), this, SLOT( jumpToLine( int ) ) ); } refreshOverview(); } void AbstractLogView::searchUsingFunction( qint64 (QuickFind::*search_function)() ) { disableFollow(); int line = (quickFind_.*search_function)(); if ( line >= 0 ) { LOG(logDEBUG) << "search " << line; displayLine( line ); emit updateLineNumber( line ); } } void AbstractLogView::searchForward() { searchUsingFunction( &QuickFind::searchForward ); } void AbstractLogView::searchBackward() { searchUsingFunction( &QuickFind::searchBackward ); } void AbstractLogView::incrementallySearchForward() { searchUsingFunction( &QuickFind::incrementallySearchForward ); } void AbstractLogView::incrementallySearchBackward() { searchUsingFunction( &QuickFind::incrementallySearchBackward ); } void AbstractLogView::incrementalSearchAbort() { quickFind_.incrementalSearchAbort(); emit changeQuickFind( "", QuickFindMux::Forward ); } void AbstractLogView::incrementalSearchStop() { quickFind_.incrementalSearchStop(); } void AbstractLogView::followSet( bool checked ) { followMode_ = checked; followElasticHook_.hook( checked ); update(); if ( checked ) jumpToBottom(); } void AbstractLogView::refreshOverview() { assert( overviewWidget_ ); // Create space for the Overview if needed if ( ( getOverview() != NULL ) && getOverview()->isVisible() ) { setViewportMargins( 0, 0, OVERVIEW_WIDTH, 0 ); overviewWidget_->show(); } else { setViewportMargins( 0, 0, 0, 0 ); overviewWidget_->hide(); } } // Reset the QuickFind when the pattern is changed. void AbstractLogView::handlePatternUpdated() { LOG(logDEBUG) << "AbstractLogView::handlePatternUpdated()"; quickFind_.resetLimits(); update(); } // OR the current with the current search expression void AbstractLogView::addToSearch() { if ( selection_.isPortion() ) { LOG(logDEBUG) << "AbstractLogView::addToSearch()"; emit addToSearch( selection_.getSelectedText( logData ) ); } else { LOG(logERROR) << "AbstractLogView::addToSearch called for a wrong type of selection"; } } // Find next occurence of the selected text (*) void AbstractLogView::findNextSelected() { // Use the selected 'word' and search forward if ( selection_.isPortion() ) { emit changeQuickFind( selection_.getSelectedText( logData ), QuickFindMux::Forward ); emit searchNext(); } } // Find next previous of the selected text (#) void AbstractLogView::findPreviousSelected() { if ( selection_.isPortion() ) { emit changeQuickFind( selection_.getSelectedText( logData ), QuickFindMux::Backward ); emit searchNext(); } } // Copy the selection to the clipboard void AbstractLogView::copy() { static QClipboard* clipboard = QApplication::clipboard(); clipboard->setText( selection_.getSelectedText( logData ) ); } // // Public functions // void AbstractLogView::updateData() { LOG(logDEBUG) << "AbstractLogView::updateData"; // Check the top Line is within range if ( firstLine >= logData->getNbLine() ) { firstLine = 0; firstCol = 0; verticalScrollBar()->setValue( 0 ); horizontalScrollBar()->setValue( 0 ); } // Crop selection if it become out of range selection_.crop( logData->getNbLine() - 1 ); // Adapt the scroll bars to the new content updateScrollBars(); // Calculate the index of the last line shown LineNumber last_line = std::min( static_cast( logData->getNbLine() ), static_cast( firstLine + getNbVisibleLines() ) ); // Reset the QuickFind in case we have new stuff to search into quickFind_.resetLimits(); if ( followMode_ ) jumpToBottom(); // Update the overview if we have one if ( overview_ != NULL ) overview_->updateCurrentPosition( firstLine, last_line ); // Invalidate our cache textAreaCache_.invalid_ = true; // Repaint! update(); } void AbstractLogView::updateDisplaySize() { // Font is assumed to be mono-space (is restricted by options dialog) QFontMetrics fm = fontMetrics(); charHeight_ = fm.height(); // For some reason on Qt 4.8.2 for Win, maxWidth() is wrong but the // following give the right result, not sure why: charWidth_ = fm.width( QChar('a') ); // Update the scroll bars updateScrollBars(); verticalScrollBar()->setPageStep( getNbVisibleLines() ); if ( followMode_ ) jumpToBottom(); LOG(logDEBUG) << "viewport.width()=" << viewport()->width(); LOG(logDEBUG) << "viewport.height()=" << viewport()->height(); LOG(logDEBUG) << "width()=" << width(); LOG(logDEBUG) << "height()=" << height(); if ( overviewWidget_ ) overviewWidget_->setGeometry( viewport()->width() + 2, 1, OVERVIEW_WIDTH - 1, viewport()->height() ); // Our text area cache is now invalid textAreaCache_.invalid_ = true; textAreaCache_.pixmap_ = QPixmap { viewport()->width() * viewport()->devicePixelRatio(), static_cast( getNbVisibleLines() ) * charHeight_ * viewport()->devicePixelRatio() }; textAreaCache_.pixmap_.setDevicePixelRatio( viewport()->devicePixelRatio() ); } int AbstractLogView::getTopLine() const { return firstLine; } QString AbstractLogView::getSelection() const { return selection_.getSelectedText( logData ); } void AbstractLogView::selectAll() { selection_.selectRange( 0, logData->getNbLine() - 1 ); textAreaCache_.invalid_ = true; update(); } void AbstractLogView::selectAndDisplayLine( int line ) { disableFollow(); selection_.selectLine( line ); displayLine( line ); emit updateLineNumber( line ); } // The difference between this function and displayLine() is quite // subtle: this one always jump, even if the line passed is visible. void AbstractLogView::jumpToLine( int line ) { // Put the selected line in the middle if possible int newTopLine = line - ( getNbVisibleLines() / 2 ); if ( newTopLine < 0 ) newTopLine = 0; // This will also trigger a scrollContents event verticalScrollBar()->setValue( newTopLine ); } void AbstractLogView::setLineNumbersVisible( bool lineNumbersVisible ) { lineNumbersVisible_ = lineNumbersVisible; } void AbstractLogView::forceRefresh() { // Invalidate our cache textAreaCache_.invalid_ = true; } // // Private functions // // Returns the number of lines visible in the viewport LineNumber AbstractLogView::getNbVisibleLines() const { return static_cast( viewport()->height() / charHeight_ + 1 ); } // Returns the number of columns visible in the viewport int AbstractLogView::getNbVisibleCols() const { return ( viewport()->width() - leftMarginPx_ ) / charWidth_ + 1; } // Converts the mouse x, y coordinates to the line number in the file int AbstractLogView::convertCoordToLine(int yPos) const { int line = firstLine + ( yPos - drawingTopOffset_ ) / charHeight_; return line; } // Converts the mouse x, y coordinates to the char coordinates (in the file) // This function ensure the pos exists in the file. QPoint AbstractLogView::convertCoordToFilePos( const QPoint& pos ) const { int line = convertCoordToLine( pos.y() ); if ( line >= logData->getNbLine() ) line = logData->getNbLine() - 1; if ( line < 0 ) line = 0; // Determine column in screen space and convert it to file space int column = firstCol + ( pos.x() - leftMarginPx_ ) / charWidth_; QString this_line = logData->getExpandedLineString( line ); const int length = this_line.length(); if ( column >= length ) column = length - 1; if ( column < 0 ) column = 0; LOG(logDEBUG4) << "AbstractLogView::convertCoordToFilePos col=" << column << " line=" << line; QPoint point( column, line ); return point; } // Makes the widget adjust itself to display the passed line. // Doing so, it will throw itself a scrollContents event. void AbstractLogView::displayLine( LineNumber line ) { // If the line is already the screen if ( ( line >= firstLine ) && ( line < ( firstLine + getNbVisibleLines() ) ) ) { // Invalidate our cache textAreaCache_.invalid_ = true; // ... don't scroll and just repaint update(); } else { jumpToLine( line ); } } // Move the selection up and down by the passed number of lines void AbstractLogView::moveSelection( int delta ) { LOG(logDEBUG) << "AbstractLogView::moveSelection delta=" << delta; QList selection = selection_.getLines(); int new_line; // If nothing is selected, do as if line -1 was. if ( selection.isEmpty() ) selection.append( -1 ); if ( delta < 0 ) new_line = selection.first() + delta; else new_line = selection.last() + delta; if ( new_line < 0 ) new_line = 0; else if ( new_line >= logData->getNbLine() ) new_line = logData->getNbLine() - 1; // Select and display the new line selection_.selectLine( new_line ); displayLine( new_line ); emit updateLineNumber( new_line ); emit newSelection( new_line ); } // Make the start of the lines visible void AbstractLogView::jumpToStartOfLine() { horizontalScrollBar()->setValue( 0 ); } // Make the end of the lines in the selection visible void AbstractLogView::jumpToEndOfLine() { QList selection = selection_.getLines(); // Search the longest line in the selection int max_length = 0; foreach ( int line, selection ) { int length = logData->getLineLength( line ); if ( length > max_length ) max_length = length; } horizontalScrollBar()->setValue( max_length - getNbVisibleCols() ); } // Make the end of the lines on the screen visible void AbstractLogView::jumpToRightOfScreen() { QList selection = selection_.getLines(); // Search the longest line on screen int max_length = 0; for ( auto i = firstLine; i <= ( firstLine + getNbVisibleLines() ); i++ ) { int length = logData->getLineLength( i ); if ( length > max_length ) max_length = length; } horizontalScrollBar()->setValue( max_length - getNbVisibleCols() ); } // Jump to the first line void AbstractLogView::jumpToTop() { // This will also trigger a scrollContents event verticalScrollBar()->setValue( 0 ); update(); // in case the screen hasn't moved } // Jump to the last line void AbstractLogView::jumpToBottom() { const int new_top_line = qMax( logData->getNbLine() - getNbVisibleLines() + 1, 0LL ); // This will also trigger a scrollContents event verticalScrollBar()->setValue( new_top_line ); update(); // in case the screen hasn't moved } // Returns whether the character passed is a 'word' character inline bool AbstractLogView::isCharWord( char c ) { if ( ( ( c >= 'A' ) && ( c <= 'Z' ) ) || ( ( c >= 'a' ) && ( c <= 'z' ) ) || ( ( c >= '0' ) && ( c <= '9' ) ) || ( ( c == '_' ) ) ) return true; else return false; } // Select the word under the given position void AbstractLogView::selectWordAtPosition( const QPoint& pos ) { const int x = pos.x(); const QString line = logData->getExpandedLineString( pos.y() ); if ( isCharWord( line[x].toLatin1() ) ) { // Search backward for the first character in the word int currentPos = x; for ( ; currentPos > 0; currentPos-- ) if ( ! isCharWord( line[currentPos].toLatin1() ) ) break; // Exclude the first char of the line if needed if ( ! isCharWord( line[currentPos].toLatin1() ) ) currentPos++; int start = currentPos; // Now search for the end currentPos = x; for ( ; currentPos < line.length() - 1; currentPos++ ) if ( ! isCharWord( line[currentPos].toLatin1() ) ) break; // Exclude the last char of the line if needed if ( ! isCharWord( line[currentPos].toLatin1() ) ) currentPos--; int end = currentPos; selection_.selectPortion( pos.y(), start, end ); updateGlobalSelection(); update(); } } // Update the system global (middle click) selection (X11 only) void AbstractLogView::updateGlobalSelection() { static QClipboard* const clipboard = QApplication::clipboard(); // Updating it only for "non-trivial" (range or portion) selections if ( ! selection_.isSingleLine() ) clipboard->setText( selection_.getSelectedText( logData ), QClipboard::Selection ); } // Create the pop-up menu void AbstractLogView::createMenu() { copyAction_ = new QAction( tr("&Copy"), this ); // No text as this action title depends on the type of selection connect( copyAction_, SIGNAL(triggered()), this, SLOT(copy()) ); // For '#' and '*', shortcuts doesn't seem to work but // at least it displays them in the menu, we manually handle those keys // as keys event anyway (in keyPressEvent). findNextAction_ = new QAction(tr("Find &next"), this); findNextAction_->setShortcut( Qt::Key_Asterisk ); findNextAction_->setStatusTip( tr("Find the next occurence") ); connect( findNextAction_, SIGNAL(triggered()), this, SLOT( findNextSelected() ) ); findPreviousAction_ = new QAction( tr("Find &previous"), this ); findPreviousAction_->setShortcut( tr("#") ); findPreviousAction_->setStatusTip( tr("Find the previous occurence") ); connect( findPreviousAction_, SIGNAL(triggered()), this, SLOT( findPreviousSelected() ) ); addToSearchAction_ = new QAction( tr("&Add to search"), this ); addToSearchAction_->setStatusTip( tr("Add the selection to the current search") ); connect( addToSearchAction_, SIGNAL( triggered() ), this, SLOT( addToSearch() ) ); popupMenu_ = new QMenu( this ); popupMenu_->addAction( copyAction_ ); popupMenu_->addSeparator(); popupMenu_->addAction( findNextAction_ ); popupMenu_->addAction( findPreviousAction_ ); popupMenu_->addAction( addToSearchAction_ ); } void AbstractLogView::considerMouseHovering( int x_pos, int y_pos ) { int line = convertCoordToLine( y_pos ); if ( ( x_pos < leftMarginPx_ ) && ( line >= 0 ) && ( line < logData->getNbLine() ) ) { // Mouse moved in the margin, send event up // (possibly to highlight the overview) if ( line != lastHoveredLine_ ) { LOG(logDEBUG) << "Mouse moved in margin line: " << line; emit mouseHoveredOverLine( line ); lastHoveredLine_ = line; } } else { if ( lastHoveredLine_ != -1 ) { emit mouseLeftHoveringZone(); lastHoveredLine_ = -1; } } } void AbstractLogView::updateScrollBars() { verticalScrollBar()->setRange( 0, std::max( 0LL, logData->getNbLine() - getNbVisibleLines() + 1 ) ); const int hScrollMaxValue = std::max( 0, logData->getMaxLength() - getNbVisibleCols() + 1 ); horizontalScrollBar()->setRange( 0, hScrollMaxValue ); } void AbstractLogView::drawTextArea( QPaintDevice* paint_device, int32_t delta_y ) { // LOG( logDEBUG ) << "devicePixelRatio: " << viewport()->devicePixelRatio(); // LOG( logDEBUG ) << "viewport size: " << viewport()->size().width(); // LOG( logDEBUG ) << "pixmap size: " << textPixmap.width(); // Repaint the viewport QPainter painter( paint_device ); // LOG( logDEBUG ) << "font: " << viewport()->font().family().toStdString(); // LOG( logDEBUG ) << "font painter: " << painter.font().family().toStdString(); painter.setFont( this->font() ); const int fontHeight = charHeight_; const int fontAscent = painter.fontMetrics().ascent(); const int nbCols = getNbVisibleCols(); const int paintDeviceHeight = paint_device->height() / viewport()->devicePixelRatio(); const int paintDeviceWidth = paint_device->width() / viewport()->devicePixelRatio(); const QPalette& palette = viewport()->palette(); std::shared_ptr filterSet = Persistent( "filterSet" ); QColor foreColor, backColor; static const QBrush normalBulletBrush = QBrush( Qt::white ); static const QBrush matchBulletBrush = QBrush( Qt::red ); static const QBrush markBrush = QBrush( "dodgerblue" ); static const int SEPARATOR_WIDTH = 1; static const qreal BULLET_AREA_WIDTH = 11; static const int CONTENT_MARGIN_WIDTH = 1; static const int LINE_NUMBER_PADDING = 3; // First check the lines to be drawn are within range (might not be the case if // the file has just changed) const int64_t lines_in_file = logData->getNbLine(); if ( firstLine > lines_in_file ) firstLine = lines_in_file ? lines_in_file - 1 : 0; const int64_t nbLines = std::min( static_cast( getNbVisibleLines() ), lines_in_file - firstLine ); const int bottomOfTextPx = nbLines * fontHeight; LOG(logDEBUG) << "drawing lines from " << firstLine << " (" << nbLines << " lines)"; LOG(logDEBUG) << "bottomOfTextPx: " << bottomOfTextPx; LOG(logDEBUG) << "Height: " << paintDeviceHeight; // Lines to write const QStringList lines = logData->getExpandedLines( firstLine, nbLines ); // First draw the bullet left margin painter.setPen(palette.color(QPalette::Text)); painter.fillRect( 0, 0, BULLET_AREA_WIDTH, paintDeviceHeight, Qt::darkGray ); // Column at which the content should start (pixels) qreal contentStartPosX = BULLET_AREA_WIDTH + SEPARATOR_WIDTH; // This is also the bullet zone width, used for marking clicks bulletZoneWidthPx_ = contentStartPosX; // Update the length of line numbers const int nbDigitsInLineNumber = countDigits( maxDisplayLineNumber() ); // Draw the line numbers area int lineNumberAreaStartX = 0; if ( lineNumbersVisible_ ) { int lineNumberWidth = charWidth_ * nbDigitsInLineNumber; int lineNumberAreaWidth = 2 * LINE_NUMBER_PADDING + lineNumberWidth; lineNumberAreaStartX = contentStartPosX; painter.setPen(palette.color(QPalette::Text)); /* Not sure if it looks good... painter.drawLine( contentStartPosX + lineNumberAreaWidth, 0, contentStartPosX + lineNumberAreaWidth, viewport()->height() ); */ painter.fillRect( contentStartPosX - SEPARATOR_WIDTH, 0, lineNumberAreaWidth + SEPARATOR_WIDTH, paintDeviceHeight, Qt::lightGray ); // Update for drawing the actual text contentStartPosX += lineNumberAreaWidth; } else { painter.fillRect( contentStartPosX - SEPARATOR_WIDTH, 0, SEPARATOR_WIDTH + 1, paintDeviceHeight, Qt::lightGray ); // contentStartPosX += SEPARATOR_WIDTH; } painter.drawLine( BULLET_AREA_WIDTH, 0, BULLET_AREA_WIDTH, paintDeviceHeight - 1 ); // This is the total width of the 'margin' (including line number if any) // used for mouse calculation etc... leftMarginPx_ = contentStartPosX + SEPARATOR_WIDTH; // Then draw each line for (int i = 0; i < nbLines; i++) { const LineNumber line_index = i + firstLine; // Position in pixel of the base line of the line to print const int yPos = i * fontHeight; const int xPos = contentStartPosX + CONTENT_MARGIN_WIDTH; // string to print, cut to fit the length and position of the view const QString line = lines[i]; const QString cutLine = line.mid( firstCol, nbCols ); if ( selection_.isLineSelected( line_index ) ) { // Reverse the selected line foreColor = palette.color( QPalette::HighlightedText ); backColor = palette.color( QPalette::Highlight ); painter.setPen(palette.color(QPalette::Text)); } else if ( filterSet->matchLine( logData->getLineString( line_index ), &foreColor, &backColor ) ) { // Apply a filter to the line } else { // Use the default colors foreColor = palette.color( QPalette::Text ); backColor = palette.color( QPalette::Base ); } // Is there something selected in the line? int sel_start, sel_end; bool isSelection = selection_.getPortionForLine( line_index, &sel_start, &sel_end ); // Has the line got elements to be highlighted QList qfMatchList; bool isMatch = quickFindPattern_->matchLine( line, qfMatchList ); if ( isSelection || isMatch ) { // We use the LineDrawer and its chunks because the // line has to be somehow highlighted LineDrawer lineDrawer( backColor ); // First we create a list of chunks with the highlights QList chunkList; int column = 0; // Current column in line space foreach( const QuickFindMatch match, qfMatchList ) { int start = match.startColumn() - firstCol; int end = start + match.length(); // Ignore matches that are *completely* outside view area if ( ( start < 0 && end < 0 ) || start >= nbCols ) continue; if ( start > column ) chunkList << LineChunk( column, start - 1, LineChunk::Normal ); column = qMin( start + match.length() - 1, nbCols ); chunkList << LineChunk( qMax( start, 0 ), column, LineChunk::Highlighted ); column++; } if ( column <= cutLine.length() - 1 ) chunkList << LineChunk( column, cutLine.length() - 1, LineChunk::Normal ); // Then we add the selection if needed QList newChunkList; if ( isSelection ) { sel_start -= firstCol; // coord in line space sel_end -= firstCol; foreach ( const LineChunk chunk, chunkList ) { newChunkList << chunk.select( sel_start, sel_end ); } } else newChunkList = chunkList; foreach ( const LineChunk chunk, newChunkList ) { // Select the colours QColor fore; QColor back; switch ( chunk.type() ) { case LineChunk::Normal: fore = foreColor; back = backColor; break; case LineChunk::Highlighted: fore = QColor( "black" ); back = QColor( "yellow" ); // fore = highlightForeColor; // back = highlightBackColor; break; case LineChunk::Selected: fore = palette.color( QPalette::HighlightedText ), back = palette.color( QPalette::Highlight ); break; } lineDrawer.addChunk ( chunk, fore, back ); } lineDrawer.draw( painter, xPos, yPos, viewport()->width(), cutLine, CONTENT_MARGIN_WIDTH ); } else { // Nothing to be highlighted, we print the whole line! painter.fillRect( xPos - CONTENT_MARGIN_WIDTH, yPos, viewport()->width(), fontHeight, backColor ); // (the rectangle is extended on the left to cover the small // margin, it looks better (LineDrawer does the same) ) painter.setPen( foreColor ); painter.drawText( xPos, yPos + fontAscent, cutLine ); } // Then draw the bullet painter.setPen( palette.color( QPalette::Text ) ); const qreal circleSize = 3; const qreal arrowHeight = 4; const qreal middleXLine = BULLET_AREA_WIDTH / 2; const qreal middleYLine = yPos + (fontHeight / 2); const LineType line_type = lineType( line_index ); if ( line_type == Marked ) { // A pretty arrow if the line is marked const QPointF points[7] = { QPointF(1, middleYLine - 2), QPointF(middleXLine, middleYLine - 2), QPointF(middleXLine, middleYLine - arrowHeight), QPointF(BULLET_AREA_WIDTH - 1, middleYLine), QPointF(middleXLine, middleYLine + arrowHeight), QPointF(middleXLine, middleYLine + 2), QPointF(1, middleYLine + 2 ), }; painter.setBrush( markBrush ); painter.drawPolygon( points, 7 ); } else { // For pretty circles painter.setRenderHint( QPainter::Antialiasing ); if ( lineType( line_index ) == Match ) painter.setBrush( matchBulletBrush ); else painter.setBrush( normalBulletBrush ); painter.drawEllipse( middleXLine - circleSize, middleYLine - circleSize, circleSize * 2, circleSize * 2 ); } // Draw the line number if ( lineNumbersVisible_ ) { static const QString lineNumberFormat( "%1" ); const QString& lineNumberStr = lineNumberFormat.arg( displayLineNumber( line_index ), nbDigitsInLineNumber ); painter.setPen( palette.color( QPalette::Text ) ); painter.drawText( lineNumberAreaStartX + LINE_NUMBER_PADDING, yPos + fontAscent, lineNumberStr ); } } // For each line if ( bottomOfTextPx < paintDeviceHeight ) { // The lines don't cover the whole device painter.fillRect( contentStartPosX, bottomOfTextPx, paintDeviceWidth - contentStartPosX, paintDeviceHeight, palette.color( QPalette::Window ) ); } } // Draw the "pull to follow" bar and return a pixmap. // The width is passed in "logic" pixels. QPixmap AbstractLogView::drawPullToFollowBar( int width, float pixel_ratio ) { static constexpr int barWidth = 40; QPixmap pixmap ( static_cast( width ) * pixel_ratio, barWidth * 6.0 ); pixmap.setDevicePixelRatio( pixel_ratio ); pixmap.fill( this->palette().color( this->backgroundRole() ) ); const int nbBars = width / (barWidth * 2) + 1; QPainter painter( &pixmap ); painter.setPen( QPen( QColor( 0, 0, 0, 0 ) ) ); painter.setBrush( QBrush( QColor( "lightyellow" ) ) ); for ( int i = 0; i < nbBars; ++i ) { QPoint points[4] = { { (i*2+1)*barWidth, 0 }, { 0, (i*2+1)*barWidth }, { 0, (i+1)*2*barWidth }, { (i+1)*2*barWidth, 0 } }; painter.drawConvexPolygon( points, 4 ); } return pixmap; } void AbstractLogView::disableFollow() { emit followModeChanged( false ); followElasticHook_.hook( false ); } namespace { // Convert the length of the pull to follow bar to pixels int mapPullToFollowLength( int length ) { return length / 14; } };