1 /* 2 * Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015 Nicolas Bonnefon and other contributors 3 * 4 * This file is part of glogg. 5 * 6 * glogg is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation, either version 3 of the License, or 9 * (at your option) any later version. 10 * 11 * glogg is distributed in the hope that it will be useful, 12 * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 * GNU General Public License for more details. 15 * 16 * You should have received a copy of the GNU General Public License 17 * along with glogg. If not, see <http://www.gnu.org/licenses/>. 18 */ 19 20 // This file implements the CrawlerWidget class. 21 // It is responsible for creating and managing the two views and all 22 // the UI elements. It implements the connection between the UI elements. 23 // It also interacts with the sets of data (full and filtered). 24 25 #include "log.h" 26 27 #include <cassert> 28 29 #include <Qt> 30 #include <QApplication> 31 #include <QFile> 32 #include <QLineEdit> 33 #include <QFileInfo> 34 #include <QStandardItemModel> 35 #include <QHeaderView> 36 #include <QListView> 37 38 #include "crawlerwidget.h" 39 40 #include "quickfindpattern.h" 41 #include "overview.h" 42 #include "infoline.h" 43 #include "savedsearches.h" 44 #include "quickfindwidget.h" 45 #include "persistentinfo.h" 46 #include "configuration.h" 47 48 // Palette for error signaling (yellow background) 49 const QPalette CrawlerWidget::errorPalette( QColor( "yellow" ) ); 50 51 // Implementation of the view context for the CrawlerWidget 52 class CrawlerWidgetContext : public ViewContextInterface { 53 public: 54 // Construct from the stored string representation 55 CrawlerWidgetContext( const char* string ); 56 // Construct from the value passsed 57 CrawlerWidgetContext( QList<int> sizes, 58 bool ignore_case, 59 bool auto_refresh ) 60 : sizes_( sizes ), 61 ignore_case_( ignore_case ), 62 auto_refresh_( auto_refresh ) {} 63 64 // Implementation of the ViewContextInterface function 65 std::string toString() const; 66 67 // Access the Qt sizes array for the QSplitter 68 QList<int> sizes() const { return sizes_; } 69 70 bool ignoreCase() const { return ignore_case_; } 71 bool autoRefresh() const { return auto_refresh_; } 72 73 private: 74 QList<int> sizes_; 75 76 bool ignore_case_; 77 bool auto_refresh_; 78 }; 79 80 // Constructor only does trivial construction. The real work is done once 81 // the data is attached. 82 CrawlerWidget::CrawlerWidget( QWidget *parent ) 83 : QSplitter( parent ), overview_() 84 { 85 logData_ = nullptr; 86 logFilteredData_ = nullptr; 87 88 quickFindPattern_ = nullptr; 89 savedSearches_ = nullptr; 90 qfSavedFocus_ = nullptr; 91 92 // Until we have received confirmation loading is finished, we 93 // should consider we are loading something. 94 loadingInProgress_ = true; 95 // and it's the first time 96 firstLoadDone_ = false; 97 nbMatches_ = 0; 98 dataStatus_ = DataStatus::OLD_DATA; 99 100 currentLineNumber_ = 0; 101 } 102 103 // The top line is first one on the main display 104 int CrawlerWidget::getTopLine() const 105 { 106 return logMainView->getTopLine(); 107 } 108 109 QString CrawlerWidget::getSelectedText() const 110 { 111 if ( filteredView->hasFocus() ) 112 return filteredView->getSelection(); 113 else 114 return logMainView->getSelection(); 115 } 116 117 void CrawlerWidget::selectAll() 118 { 119 activeView()->selectAll(); 120 } 121 122 Encoding CrawlerWidget::encodingSetting() const 123 { 124 return encodingSetting_; 125 } 126 127 bool CrawlerWidget::isFollowEnabled() const 128 { 129 return logMainView->isFollowEnabled(); 130 } 131 132 QString CrawlerWidget::encodingText() const 133 { 134 return encoding_text_; 135 } 136 137 // Return a pointer to the view in which we should do the QuickFind 138 SearchableWidgetInterface* CrawlerWidget::doGetActiveSearchable() const 139 { 140 return activeView(); 141 } 142 143 // Return all the searchable widgets (views) 144 std::vector<QObject*> CrawlerWidget::doGetAllSearchables() const 145 { 146 std::vector<QObject*> searchables = 147 { logMainView, filteredView }; 148 149 return searchables; 150 } 151 152 // Update the state of the parent 153 void CrawlerWidget::doSendAllStateSignals() 154 { 155 emit updateLineNumber( currentLineNumber_ ); 156 if ( !loadingInProgress_ ) 157 emit loadingFinished( LoadingStatus::Successful ); 158 } 159 160 void CrawlerWidget::keyPressEvent( QKeyEvent* keyEvent ) 161 { 162 bool noModifier = keyEvent->modifiers() == Qt::NoModifier; 163 164 if ( keyEvent->key() == Qt::Key_V && noModifier ) 165 visibilityBox->setCurrentIndex( 166 ( visibilityBox->currentIndex() + 1 ) % visibilityBox->count() ); 167 else { 168 const char character = (keyEvent->text())[0].toLatin1(); 169 170 if ( character == '+' ) 171 changeTopViewSize( 1 ); 172 else if ( character == '-' ) 173 changeTopViewSize( -1 ); 174 else 175 QSplitter::keyPressEvent( keyEvent ); 176 } 177 } 178 179 // 180 // Public slots 181 // 182 183 void CrawlerWidget::stopLoading() 184 { 185 logFilteredData_->interruptSearch(); 186 logData_->interruptLoading(); 187 } 188 189 void CrawlerWidget::reload() 190 { 191 searchState_.resetState(); 192 logFilteredData_->clearSearch(); 193 logFilteredData_->clearMarks(); 194 filteredView->updateData(); 195 printSearchInfoMessage(); 196 197 logData_->reload(); 198 199 // A reload is considered as a first load, 200 // this is to prevent the "new data" icon to be triggered. 201 firstLoadDone_ = false; 202 } 203 204 void CrawlerWidget::setEncoding( Encoding encoding ) 205 { 206 encodingSetting_ = encoding; 207 updateEncoding(); 208 209 update(); 210 } 211 212 // 213 // Protected functions 214 // 215 void CrawlerWidget::doSetData( 216 std::shared_ptr<LogData> log_data, 217 std::shared_ptr<LogFilteredData> filtered_data ) 218 { 219 logData_ = log_data.get(); 220 logFilteredData_ = filtered_data.get(); 221 } 222 223 void CrawlerWidget::doSetQuickFindPattern( 224 std::shared_ptr<QuickFindPattern> qfp ) 225 { 226 quickFindPattern_ = qfp; 227 } 228 229 void CrawlerWidget::doSetSavedSearches( 230 std::shared_ptr<SavedSearches> saved_searches ) 231 { 232 savedSearches_ = saved_searches; 233 234 // We do setup now, assuming doSetData has been called before 235 // us, that's not great really... 236 setup(); 237 } 238 239 void CrawlerWidget::doSetViewContext( 240 const char* view_context ) 241 { 242 LOG(logDEBUG) << "CrawlerWidget::doSetViewContext: " << view_context; 243 244 CrawlerWidgetContext context = { view_context }; 245 246 setSizes( context.sizes() ); 247 ignoreCaseCheck->setCheckState( context.ignoreCase() ? Qt::Checked : Qt::Unchecked ); 248 249 auto auto_refresh_check_state = context.autoRefresh() ? Qt::Checked : Qt::Unchecked; 250 searchRefreshCheck->setCheckState( auto_refresh_check_state ); 251 // Manually call the handler as it is not called when changing the state programmatically 252 searchRefreshChangedHandler( auto_refresh_check_state ); 253 } 254 255 std::shared_ptr<const ViewContextInterface> 256 CrawlerWidget::doGetViewContext() const 257 { 258 auto context = std::make_shared<const CrawlerWidgetContext>( 259 sizes(), 260 ( ignoreCaseCheck->checkState() == Qt::Checked ), 261 ( searchRefreshCheck->checkState() == Qt::Checked ) ); 262 263 return static_cast<std::shared_ptr<const ViewContextInterface>>( context ); 264 } 265 266 // 267 // Slots 268 // 269 270 void CrawlerWidget::startNewSearch() 271 { 272 // Record the search line in the recent list 273 // (reload the list first in case another glogg changed it) 274 GetPersistentInfo().retrieve( "savedSearches" ); 275 savedSearches_->addRecent( searchLineEdit->currentText() ); 276 GetPersistentInfo().save( "savedSearches" ); 277 278 // Update the SearchLine (history) 279 updateSearchCombo(); 280 // Call the private function to do the search 281 replaceCurrentSearch( searchLineEdit->currentText() ); 282 } 283 284 void CrawlerWidget::stopSearch() 285 { 286 logFilteredData_->interruptSearch(); 287 searchState_.stopSearch(); 288 printSearchInfoMessage(); 289 } 290 291 // When receiving the 'newDataAvailable' signal from LogFilteredData 292 void CrawlerWidget::updateFilteredView( int nbMatches, int progress ) 293 { 294 LOG(logDEBUG) << "updateFilteredView received."; 295 296 if ( progress == 100 ) { 297 // Searching done 298 printSearchInfoMessage( nbMatches ); 299 searchInfoLine->hideGauge(); 300 // De-activate the stop button 301 stopButton->setEnabled( false ); 302 } 303 else { 304 // Search in progress 305 // We ignore 0% and 100% to avoid a flash when the search is very short 306 if ( progress > 0 ) { 307 searchInfoLine->setText( 308 tr("Search in progress (%1 %)... %2 match%3 found so far.") 309 .arg( progress ) 310 .arg( nbMatches ) 311 .arg( nbMatches > 1 ? "es" : "" ) ); 312 searchInfoLine->displayGauge( progress ); 313 } 314 } 315 316 // If more (or less, e.g. come back to 0) matches have been found 317 if ( nbMatches != nbMatches_ ) { 318 nbMatches_ = nbMatches; 319 320 // Recompute the content of the filtered window. 321 filteredView->updateData(); 322 323 // Update the match overview 324 overview_.updateData( logData_->getNbLine() ); 325 326 // New data found icon 327 changeDataStatus( DataStatus::NEW_FILTERED_DATA ); 328 329 // Also update the top window for the coloured bullets. 330 update(); 331 } 332 333 if ( progress == 100 ) { 334 const int currenLineIndex = logFilteredData_->getLineIndexNumber(currentLineNumber_); 335 LOG(logDEBUG) << "updateFilteredView: restoring selection: " 336 << " absolute line number (0based) " << currentLineNumber_ 337 << " index " << currenLineIndex; 338 filteredView->selectAndDisplayLine(currenLineIndex); 339 } 340 } 341 342 void CrawlerWidget::jumpToMatchingLine(int filteredLineNb) 343 { 344 int mainViewLine = logFilteredData_->getMatchingLineNumber(filteredLineNb); 345 logMainView->selectAndDisplayLine(mainViewLine); // FIXME: should be done with a signal. 346 } 347 348 void CrawlerWidget::updateLineNumberHandler( int line ) 349 { 350 currentLineNumber_ = line; 351 emit updateLineNumber( line ); 352 } 353 354 void CrawlerWidget::markLineFromMain( qint64 line ) 355 { 356 if ( line < logData_->getNbLine() ) { 357 if ( logFilteredData_->isLineMarked( line ) ) 358 logFilteredData_->deleteMark( line ); 359 else 360 logFilteredData_->addMark( line ); 361 362 // Recompute the content of both window. 363 filteredView->updateData(); 364 logMainView->updateData(); 365 366 // Update the match overview 367 overview_.updateData( logData_->getNbLine() ); 368 369 // Also update the top window for the coloured bullets. 370 update(); 371 } 372 } 373 374 void CrawlerWidget::markLineFromFiltered( qint64 line ) 375 { 376 if ( line < logFilteredData_->getNbLine() ) { 377 qint64 line_in_file = logFilteredData_->getMatchingLineNumber( line ); 378 if ( logFilteredData_->filteredLineTypeByIndex( line ) 379 == LogFilteredData::Mark ) 380 logFilteredData_->deleteMark( line_in_file ); 381 else 382 logFilteredData_->addMark( line_in_file ); 383 384 // Recompute the content of both window. 385 filteredView->updateData(); 386 logMainView->updateData(); 387 388 // Update the match overview 389 overview_.updateData( logData_->getNbLine() ); 390 391 // Also update the top window for the coloured bullets. 392 update(); 393 } 394 } 395 396 void CrawlerWidget::applyConfiguration() 397 { 398 std::shared_ptr<Configuration> config = 399 Persistent<Configuration>( "settings" ); 400 QFont font = config->mainFont(); 401 402 LOG(logDEBUG) << "CrawlerWidget::applyConfiguration"; 403 404 // Whatever font we use, we should NOT use kerning 405 font.setKerning( false ); 406 font.setFixedPitch( true ); 407 #if QT_VERSION > 0x040700 408 // Necessary on systems doing subpixel positionning (e.g. Ubuntu 12.04) 409 font.setStyleStrategy( QFont::ForceIntegerMetrics ); 410 #endif 411 logMainView->setFont(font); 412 filteredView->setFont(font); 413 414 logMainView->setLineNumbersVisible( config->mainLineNumbersVisible() ); 415 filteredView->setLineNumbersVisible( config->filteredLineNumbersVisible() ); 416 417 overview_.setVisible( config->isOverviewVisible() ); 418 logMainView->refreshOverview(); 419 420 logMainView->updateDisplaySize(); 421 logMainView->update(); 422 filteredView->updateDisplaySize(); 423 filteredView->update(); 424 425 // Polling interval 426 logData_->setPollingInterval( 427 config->pollingEnabled() ? config->pollIntervalMs() : 0 ); 428 429 // Update the SearchLine (history) 430 updateSearchCombo(); 431 } 432 433 void CrawlerWidget::enteringQuickFind() 434 { 435 LOG(logDEBUG) << "CrawlerWidget::enteringQuickFind"; 436 437 // Remember who had the focus (only if it is one of our views) 438 QWidget* focus_widget = QApplication::focusWidget(); 439 440 if ( ( focus_widget == logMainView ) || ( focus_widget == filteredView ) ) 441 qfSavedFocus_ = focus_widget; 442 else 443 qfSavedFocus_ = nullptr; 444 } 445 446 void CrawlerWidget::exitingQuickFind() 447 { 448 // Restore the focus once the QFBar has been hidden 449 if ( qfSavedFocus_ ) 450 qfSavedFocus_->setFocus(); 451 } 452 453 void CrawlerWidget::loadingFinishedHandler( LoadingStatus status ) 454 { 455 loadingInProgress_ = false; 456 457 // We need to refresh the main window because the view lines on the 458 // overview have probably changed. 459 overview_.updateData( logData_->getNbLine() ); 460 461 // FIXME, handle topLine 462 // logMainView->updateData( logData_, topLine ); 463 logMainView->updateData(); 464 465 // Shall we Forbid starting a search when loading in progress? 466 // searchButton->setEnabled( false ); 467 468 // searchButton->setEnabled( true ); 469 470 // See if we need to auto-refresh the search 471 if ( searchState_.isAutorefreshAllowed() ) { 472 if ( searchState_.isFileTruncated() ) 473 // We need to restart the search 474 replaceCurrentSearch( searchLineEdit->currentText() ); 475 else 476 logFilteredData_->updateSearch(); 477 } 478 479 // Set the encoding for the views 480 updateEncoding(); 481 482 emit loadingFinished( status ); 483 484 // Also change the data available icon 485 if ( firstLoadDone_ ) 486 changeDataStatus( DataStatus::NEW_DATA ); 487 else 488 firstLoadDone_ = true; 489 } 490 491 void CrawlerWidget::fileChangedHandler( LogData::MonitoredFileStatus status ) 492 { 493 // Handle the case where the file has been truncated 494 if ( status == LogData::Truncated ) { 495 // Clear all marks (TODO offer the option to keep them) 496 logFilteredData_->clearMarks(); 497 if ( ! searchInfoLine->text().isEmpty() ) { 498 // Invalidate the search 499 logFilteredData_->clearSearch(); 500 filteredView->updateData(); 501 searchState_.truncateFile(); 502 printSearchInfoMessage(); 503 nbMatches_ = 0; 504 } 505 } 506 } 507 508 // Returns a pointer to the window in which the search should be done 509 AbstractLogView* CrawlerWidget::activeView() const 510 { 511 QWidget* activeView; 512 513 // Search in the window that has focus, or the window where 'Find' was 514 // called from, or the main window. 515 if ( filteredView->hasFocus() || logMainView->hasFocus() ) 516 activeView = QApplication::focusWidget(); 517 else 518 activeView = qfSavedFocus_; 519 520 if ( activeView ) { 521 AbstractLogView* view = qobject_cast<AbstractLogView*>( activeView ); 522 return view; 523 } 524 else { 525 LOG(logWARNING) << "No active view, defaulting to logMainView"; 526 return logMainView; 527 } 528 } 529 530 void CrawlerWidget::searchForward() 531 { 532 LOG(logDEBUG) << "CrawlerWidget::searchForward"; 533 534 activeView()->searchForward(); 535 } 536 537 void CrawlerWidget::searchBackward() 538 { 539 LOG(logDEBUG) << "CrawlerWidget::searchBackward"; 540 541 activeView()->searchBackward(); 542 } 543 544 void CrawlerWidget::searchRefreshChangedHandler( int state ) 545 { 546 searchState_.setAutorefresh( state == Qt::Checked ); 547 printSearchInfoMessage( logFilteredData_->getNbMatches() ); 548 } 549 550 void CrawlerWidget::searchTextChangeHandler() 551 { 552 // We suspend auto-refresh 553 searchState_.changeExpression(); 554 printSearchInfoMessage( logFilteredData_->getNbMatches() ); 555 } 556 557 void CrawlerWidget::changeFilteredViewVisibility( int index ) 558 { 559 QStandardItem* item = visibilityModel_->item( index ); 560 FilteredView::Visibility visibility = 561 static_cast< FilteredView::Visibility>( item->data().toInt() ); 562 563 filteredView->setVisibility( visibility ); 564 565 const int lineIndex = logFilteredData_->getLineIndexNumber( currentLineNumber_ ); 566 filteredView->selectAndDisplayLine( lineIndex ); 567 } 568 569 void CrawlerWidget::addToSearch( const QString& string ) 570 { 571 QString text = searchLineEdit->currentText(); 572 573 if ( text.isEmpty() ) 574 text = string; 575 else { 576 // Escape the regexp chars from the string before adding it. 577 text += ( '|' + QRegularExpression::escape( string ) ); 578 } 579 580 searchLineEdit->setEditText( text ); 581 582 // Set the focus to lineEdit so that the user can press 'Return' immediately 583 searchLineEdit->lineEdit()->setFocus(); 584 } 585 586 void CrawlerWidget::mouseHoveredOverMatch( qint64 line ) 587 { 588 qint64 line_in_mainview = logFilteredData_->getMatchingLineNumber( line ); 589 590 overviewWidget_->highlightLine( line_in_mainview ); 591 } 592 593 void CrawlerWidget::activityDetected() 594 { 595 changeDataStatus( DataStatus::OLD_DATA ); 596 } 597 598 // 599 // Private functions 600 // 601 602 // Build the widget and connect all the signals, this must be done once 603 // the data are attached. 604 void CrawlerWidget::setup() 605 { 606 setOrientation(Qt::Vertical); 607 608 assert( logData_ ); 609 assert( logFilteredData_ ); 610 611 // The views 612 bottomWindow = new QWidget; 613 overviewWidget_ = new OverviewWidget(); 614 logMainView = new LogMainView( 615 logData_, quickFindPattern_.get(), &overview_, overviewWidget_ ); 616 filteredView = new FilteredView( 617 logFilteredData_, quickFindPattern_.get() ); 618 619 overviewWidget_->setOverview( &overview_ ); 620 overviewWidget_->setParent( logMainView ); 621 622 // Connect the search to the top view 623 logMainView->useNewFiltering( logFilteredData_ ); 624 625 // Construct the visibility button 626 visibilityModel_ = new QStandardItemModel( this ); 627 628 QStandardItem *marksAndMatchesItem = new QStandardItem( tr( "Marks and matches" ) ); 629 QPixmap marksAndMatchesPixmap( 16, 10 ); 630 marksAndMatchesPixmap.fill( Qt::gray ); 631 marksAndMatchesItem->setIcon( QIcon( marksAndMatchesPixmap ) ); 632 marksAndMatchesItem->setData( FilteredView::MarksAndMatches ); 633 visibilityModel_->appendRow( marksAndMatchesItem ); 634 635 QStandardItem *marksItem = new QStandardItem( tr( "Marks" ) ); 636 QPixmap marksPixmap( 16, 10 ); 637 marksPixmap.fill( Qt::blue ); 638 marksItem->setIcon( QIcon( marksPixmap ) ); 639 marksItem->setData( FilteredView::MarksOnly ); 640 visibilityModel_->appendRow( marksItem ); 641 642 QStandardItem *matchesItem = new QStandardItem( tr( "Matches" ) ); 643 QPixmap matchesPixmap( 16, 10 ); 644 matchesPixmap.fill( Qt::red ); 645 matchesItem->setIcon( QIcon( matchesPixmap ) ); 646 matchesItem->setData( FilteredView::MatchesOnly ); 647 visibilityModel_->appendRow( matchesItem ); 648 649 QListView *visibilityView = new QListView( this ); 650 visibilityView->setMovement( QListView::Static ); 651 visibilityView->setMinimumWidth( 170 ); // Only needed with custom style-sheet 652 653 visibilityBox = new QComboBox(); 654 visibilityBox->setModel( visibilityModel_ ); 655 visibilityBox->setView( visibilityView ); 656 657 // Select "Marks and matches" by default (same default as the filtered view) 658 visibilityBox->setCurrentIndex( 0 ); 659 660 // TODO: Maybe there is some way to set the popup width to be 661 // sized-to-content (as it is when the stylesheet is not overriden) in the 662 // stylesheet as opposed to setting a hard min-width on the view above. 663 visibilityBox->setStyleSheet( " \ 664 QComboBox:on {\ 665 padding: 1px 2px 1px 6px;\ 666 width: 19px;\ 667 } \ 668 QComboBox:!on {\ 669 padding: 1px 2px 1px 7px;\ 670 width: 19px;\ 671 height: 16px;\ 672 border: 1px solid gray;\ 673 } \ 674 QComboBox::drop-down::down-arrow {\ 675 width: 0px;\ 676 border-width: 0px;\ 677 } \ 678 " ); 679 680 // Construct the Search Info line 681 searchInfoLine = new InfoLine(); 682 searchInfoLine->setFrameStyle( QFrame::WinPanel | QFrame::Sunken ); 683 searchInfoLine->setLineWidth( 1 ); 684 searchInfoLineDefaultPalette = searchInfoLine->palette(); 685 686 ignoreCaseCheck = new QCheckBox( "Ignore &case" ); 687 searchRefreshCheck = new QCheckBox( "Auto-&refresh" ); 688 689 // Construct the Search line 690 searchLabel = new QLabel(tr("&Text: ")); 691 searchLineEdit = new QComboBox; 692 searchLineEdit->setEditable( true ); 693 searchLineEdit->setCompleter( 0 ); 694 searchLineEdit->addItems( savedSearches_->recentSearches() ); 695 searchLineEdit->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Minimum ); 696 searchLineEdit->setSizeAdjustPolicy( QComboBox::AdjustToMinimumContentsLengthWithIcon ); 697 698 searchLabel->setBuddy( searchLineEdit ); 699 700 searchButton = new QToolButton(); 701 searchButton->setText( tr("&Search") ); 702 searchButton->setAutoRaise( true ); 703 704 stopButton = new QToolButton(); 705 stopButton->setIcon( QIcon(":/images/stop14.png") ); 706 stopButton->setAutoRaise( true ); 707 stopButton->setEnabled( false ); 708 709 QHBoxLayout* searchLineLayout = new QHBoxLayout; 710 searchLineLayout->addWidget(searchLabel); 711 searchLineLayout->addWidget(searchLineEdit); 712 searchLineLayout->addWidget(searchButton); 713 searchLineLayout->addWidget(stopButton); 714 searchLineLayout->setContentsMargins(6, 0, 6, 0); 715 stopButton->setSizePolicy( QSizePolicy( QSizePolicy::Maximum, QSizePolicy::Maximum ) ); 716 searchButton->setSizePolicy( QSizePolicy( QSizePolicy::Maximum, QSizePolicy::Maximum ) ); 717 718 QHBoxLayout* searchInfoLineLayout = new QHBoxLayout; 719 searchInfoLineLayout->addWidget( visibilityBox ); 720 searchInfoLineLayout->addWidget( searchInfoLine ); 721 searchInfoLineLayout->addWidget( ignoreCaseCheck ); 722 searchInfoLineLayout->addWidget( searchRefreshCheck ); 723 724 // Construct the bottom window 725 QVBoxLayout* bottomMainLayout = new QVBoxLayout; 726 bottomMainLayout->addLayout(searchLineLayout); 727 bottomMainLayout->addLayout(searchInfoLineLayout); 728 bottomMainLayout->addWidget(filteredView); 729 bottomMainLayout->setContentsMargins(2, 1, 2, 1); 730 bottomWindow->setLayout(bottomMainLayout); 731 732 addWidget( logMainView ); 733 addWidget( bottomWindow ); 734 735 // Default splitter position (usually overridden by the config file) 736 QList<int> splitterSizes; 737 splitterSizes += 400; 738 splitterSizes += 100; 739 setSizes( splitterSizes ); 740 741 // Default search checkboxes 742 auto config = Persistent<Configuration>( "settings" ); 743 searchRefreshCheck->setCheckState( config->isSearchAutoRefreshDefault() ? 744 Qt::Checked : Qt::Unchecked ); 745 // Manually call the handler as it is not called when changing the state programmatically 746 searchRefreshChangedHandler( searchRefreshCheck->checkState() ); 747 ignoreCaseCheck->setCheckState( config->isSearchIgnoreCaseDefault() ? 748 Qt::Checked : Qt::Unchecked ); 749 750 // Connect the signals 751 connect(searchLineEdit->lineEdit(), SIGNAL( returnPressed() ), 752 searchButton, SIGNAL( clicked() )); 753 connect(searchLineEdit->lineEdit(), SIGNAL( textEdited( const QString& ) ), 754 this, SLOT( searchTextChangeHandler() )); 755 connect(searchButton, SIGNAL( clicked() ), 756 this, SLOT( startNewSearch() ) ); 757 connect(stopButton, SIGNAL( clicked() ), 758 this, SLOT( stopSearch() ) ); 759 760 connect(visibilityBox, SIGNAL( currentIndexChanged( int ) ), 761 this, SLOT( changeFilteredViewVisibility( int ) ) ); 762 763 connect(logMainView, SIGNAL( newSelection( int ) ), 764 logMainView, SLOT( update() ) ); 765 connect(filteredView, SIGNAL( newSelection( int ) ), 766 this, SLOT( jumpToMatchingLine( int ) ) ); 767 connect(filteredView, SIGNAL( newSelection( int ) ), 768 filteredView, SLOT( update() ) ); 769 connect(logMainView, SIGNAL( updateLineNumber( int ) ), 770 this, SLOT( updateLineNumberHandler( int ) ) ); 771 connect(logMainView, SIGNAL( markLine( qint64 ) ), 772 this, SLOT( markLineFromMain( qint64 ) ) ); 773 connect(filteredView, SIGNAL( markLine( qint64 ) ), 774 this, SLOT( markLineFromFiltered( qint64 ) ) ); 775 776 connect(logMainView, SIGNAL( addToSearch( const QString& ) ), 777 this, SLOT( addToSearch( const QString& ) ) ); 778 connect(filteredView, SIGNAL( addToSearch( const QString& ) ), 779 this, SLOT( addToSearch( const QString& ) ) ); 780 781 connect(filteredView, SIGNAL( mouseHoveredOverLine( qint64 ) ), 782 this, SLOT( mouseHoveredOverMatch( qint64 ) ) ); 783 connect(filteredView, SIGNAL( mouseLeftHoveringZone() ), 784 overviewWidget_, SLOT( removeHighlight() ) ); 785 786 // Follow option (up and down) 787 connect(this, SIGNAL( followSet( bool ) ), 788 logMainView, SLOT( followSet( bool ) ) ); 789 connect(this, SIGNAL( followSet( bool ) ), 790 filteredView, SLOT( followSet( bool ) ) ); 791 connect(logMainView, SIGNAL( followModeChanged( bool ) ), 792 this, SIGNAL( followModeChanged( bool ) ) ); 793 connect(filteredView, SIGNAL( followModeChanged( bool ) ), 794 this, SIGNAL( followModeChanged( bool ) ) ); 795 796 // Detect activity in the views 797 connect(logMainView, SIGNAL( activity() ), 798 this, SLOT( activityDetected() ) ); 799 connect(filteredView, SIGNAL( activity() ), 800 this, SLOT( activityDetected() ) ); 801 802 connect( logFilteredData_, SIGNAL( searchProgressed( int, int ) ), 803 this, SLOT( updateFilteredView( int, int ) ) ); 804 805 // Sent load file update to MainWindow (for status update) 806 connect( logData_, SIGNAL( loadingProgressed( int ) ), 807 this, SIGNAL( loadingProgressed( int ) ) ); 808 connect( logData_, SIGNAL( loadingFinished( LoadingStatus ) ), 809 this, SLOT( loadingFinishedHandler( LoadingStatus ) ) ); 810 connect( logData_, SIGNAL( fileChanged( LogData::MonitoredFileStatus ) ), 811 this, SLOT( fileChangedHandler( LogData::MonitoredFileStatus ) ) ); 812 813 // Search auto-refresh 814 connect( searchRefreshCheck, SIGNAL( stateChanged( int ) ), 815 this, SLOT( searchRefreshChangedHandler( int ) ) ); 816 817 // Advise the parent the checkboxes have been changed 818 // (for maintaining default config) 819 connect( searchRefreshCheck, SIGNAL( stateChanged( int ) ), 820 this, SIGNAL( searchRefreshChanged( int ) ) ); 821 connect( ignoreCaseCheck, SIGNAL( stateChanged( int ) ), 822 this, SIGNAL( ignoreCaseChanged( int ) ) ); 823 824 // Switch between views 825 connect( logMainView, SIGNAL( exitView() ), 826 filteredView, SLOT( setFocus() ) ); 827 connect( filteredView, SIGNAL( exitView() ), 828 logMainView, SLOT( setFocus() ) ); 829 } 830 831 // Create a new search using the text passed, replace the currently 832 // used one and destroy the old one. 833 void CrawlerWidget::replaceCurrentSearch( const QString& searchText ) 834 { 835 // Interrupt the search if it's ongoing 836 logFilteredData_->interruptSearch(); 837 838 // We have to wait for the last search update (100%) 839 // before clearing/restarting to avoid having remaining results. 840 841 // FIXME: this is a bit of a hack, we call processEvents 842 // for Qt to empty its event queue, including (hopefully) 843 // the search update event sent by logFilteredData_. It saves 844 // us the overhead of having proper sync. 845 QApplication::processEvents( QEventLoop::ExcludeUserInputEvents ); 846 847 nbMatches_ = 0; 848 849 // Clear and recompute the content of the filtered window. 850 logFilteredData_->clearSearch(); 851 filteredView->updateData(); 852 853 // Update the match overview 854 overview_.updateData( logData_->getNbLine() ); 855 856 if ( !searchText.isEmpty() ) { 857 858 QString pattern; 859 860 // Determine the type of regexp depending on the config 861 static std::shared_ptr<Configuration> config = 862 Persistent<Configuration>( "settings" ); 863 switch ( config->mainRegexpType() ) { 864 case FixedString: 865 pattern = QRegularExpression::escape(searchText); 866 break; 867 default: 868 pattern = searchText; 869 break; 870 } 871 872 // Set the pattern case insensitive if needed 873 QRegularExpression::PatternOptions patternOptions = 874 QRegularExpression::UseUnicodePropertiesOption 875 | QRegularExpression::OptimizeOnFirstUsageOption; 876 877 if ( ignoreCaseCheck->checkState() == Qt::Checked ) 878 patternOptions |= QRegularExpression::CaseInsensitiveOption; 879 880 // Constructs the regexp 881 QRegularExpression regexp( pattern, patternOptions ); 882 883 if ( regexp.isValid() ) { 884 // Activate the stop button 885 stopButton->setEnabled( true ); 886 // Start a new asynchronous search 887 logFilteredData_->runSearch( regexp ); 888 // Accept auto-refresh of the search 889 searchState_.startSearch(); 890 } 891 else { 892 // The regexp is wrong 893 logFilteredData_->clearSearch(); 894 filteredView->updateData(); 895 searchState_.resetState(); 896 897 // Inform the user 898 QString errorMessage = tr("Error in expression"); 899 const int offset = regexp.patternErrorOffset(); 900 if (offset != -1) { 901 errorMessage += " at position "; 902 errorMessage += QString::number(offset); 903 } 904 errorMessage += ": "; 905 errorMessage += regexp.errorString(); 906 searchInfoLine->setPalette( errorPalette ); 907 searchInfoLine->setText( errorMessage ); 908 } 909 } 910 else { 911 searchState_.resetState(); 912 printSearchInfoMessage(); 913 } 914 } 915 916 // Updates the content of the drop down list for the saved searches, 917 // called when the SavedSearch has been changed. 918 void CrawlerWidget::updateSearchCombo() 919 { 920 const QString text = searchLineEdit->lineEdit()->text(); 921 searchLineEdit->clear(); 922 searchLineEdit->addItems( savedSearches_->recentSearches() ); 923 // In case we had something that wasn't added to the list (blank...): 924 searchLineEdit->lineEdit()->setText( text ); 925 } 926 927 // Print the search info message. 928 void CrawlerWidget::printSearchInfoMessage( int nbMatches ) 929 { 930 QString text; 931 932 switch ( searchState_.getState() ) { 933 case SearchState::NoSearch: 934 // Blank text is fine 935 break; 936 case SearchState::Static: 937 text = tr("%1 match%2 found.").arg( nbMatches ) 938 .arg( nbMatches > 1 ? "es" : "" ); 939 break; 940 case SearchState::Autorefreshing: 941 text = tr("%1 match%2 found. Search is auto-refreshing...").arg( nbMatches ) 942 .arg( nbMatches > 1 ? "es" : "" ); 943 break; 944 case SearchState::FileTruncated: 945 case SearchState::TruncatedAutorefreshing: 946 text = tr("File truncated on disk, previous search results are not valid anymore."); 947 break; 948 } 949 950 searchInfoLine->setPalette( searchInfoLineDefaultPalette ); 951 searchInfoLine->setText( text ); 952 } 953 954 // Change the data status and, if needed, advise upstream. 955 void CrawlerWidget::changeDataStatus( DataStatus status ) 956 { 957 if ( ( status != dataStatus_ ) 958 && (! ( dataStatus_ == DataStatus::NEW_FILTERED_DATA 959 && status == DataStatus::NEW_DATA ) ) ) { 960 dataStatus_ = status; 961 emit dataStatusChanged( dataStatus_ ); 962 } 963 } 964 965 // Determine the right encoding and set the views. 966 void CrawlerWidget::updateEncoding() 967 { 968 Encoding encoding = Encoding::ENCODING_MAX; 969 970 switch ( encodingSetting_ ) { 971 case Encoding::ENCODING_AUTO: 972 switch ( logData_->getDetectedEncoding() ) { 973 case EncodingSpeculator::Encoding::ASCII7: 974 encoding = Encoding::ENCODING_ISO_8859_1; 975 encoding_text_ = tr( "US-ASCII" ); 976 break; 977 case EncodingSpeculator::Encoding::ASCII8: 978 encoding = Encoding::ENCODING_ISO_8859_1; 979 encoding_text_ = tr( "ISO-8859-1" ); 980 break; 981 case EncodingSpeculator::Encoding::UTF8: 982 encoding = Encoding::ENCODING_UTF8; 983 encoding_text_ = tr( "UTF-8" ); 984 break; 985 case EncodingSpeculator::Encoding::UTF16LE: 986 encoding = Encoding::ENCODING_UTF16LE; 987 encoding_text_ = tr( "UTF-16LE" ); 988 break; 989 case EncodingSpeculator::Encoding::UTF16BE: 990 encoding = Encoding::ENCODING_UTF16BE; 991 encoding_text_ = tr( "UTF-16BE" ); 992 break; 993 } 994 break; 995 case Encoding::ENCODING_UTF8: 996 encoding = encodingSetting_; 997 encoding_text_ = tr( "Displayed as UTF-8" ); 998 break; 999 case Encoding::ENCODING_UTF16LE: 1000 encoding = encodingSetting_; 1001 encoding_text_ = tr( "Displayed as UTF-16LE" ); 1002 break; 1003 case Encoding::ENCODING_UTF16BE: 1004 encoding = encodingSetting_; 1005 encoding_text_ = tr( "Displayed as UTF-16BE" ); 1006 break; 1007 case Encoding::ENCODING_CP1251: 1008 encoding = encodingSetting_; 1009 encoding_text_ = tr( "Displayed as CP1251" ); 1010 break; 1011 case Encoding::ENCODING_CP1252: 1012 encoding = encodingSetting_; 1013 encoding_text_ = tr( "Displayed as CP1252" ); 1014 break; 1015 case Encoding::ENCODING_ISO_8859_1: 1016 default: 1017 encoding = Encoding::ENCODING_ISO_8859_1; 1018 encoding_text_ = tr( "Displayed as ISO-8859-1" ); 1019 break; 1020 } 1021 1022 logData_->setDisplayEncoding( encoding ); 1023 logMainView->forceRefresh(); 1024 logFilteredData_->setDisplayEncoding( encoding ); 1025 filteredView->forceRefresh(); 1026 } 1027 1028 // Change the respective size of the two views 1029 void CrawlerWidget::changeTopViewSize( int32_t delta ) 1030 { 1031 int min, max; 1032 getRange( 1, &min, &max ); 1033 LOG(logDEBUG) << "CrawlerWidget::changeTopViewSize " << sizes()[0] << " " << min << " " << max; 1034 moveSplitter( closestLegalPosition( sizes()[0] + ( delta * 10 ), 1 ), 1 ); 1035 LOG(logDEBUG) << "CrawlerWidget::changeTopViewSize " << sizes()[0]; 1036 } 1037 1038 // 1039 // SearchState implementation 1040 // 1041 void CrawlerWidget::SearchState::resetState() 1042 { 1043 state_ = NoSearch; 1044 } 1045 1046 void CrawlerWidget::SearchState::setAutorefresh( bool refresh ) 1047 { 1048 autoRefreshRequested_ = refresh; 1049 1050 if ( refresh ) { 1051 if ( state_ == Static ) 1052 state_ = Autorefreshing; 1053 /* 1054 else if ( state_ == FileTruncated ) 1055 state_ = TruncatedAutorefreshing; 1056 */ 1057 } 1058 else { 1059 if ( state_ == Autorefreshing ) 1060 state_ = Static; 1061 else if ( state_ == TruncatedAutorefreshing ) 1062 state_ = FileTruncated; 1063 } 1064 } 1065 1066 void CrawlerWidget::SearchState::truncateFile() 1067 { 1068 if ( state_ == Autorefreshing || state_ == TruncatedAutorefreshing ) { 1069 state_ = TruncatedAutorefreshing; 1070 } 1071 else { 1072 state_ = FileTruncated; 1073 } 1074 } 1075 1076 void CrawlerWidget::SearchState::changeExpression() 1077 { 1078 if ( state_ == Autorefreshing ) 1079 state_ = Static; 1080 } 1081 1082 void CrawlerWidget::SearchState::stopSearch() 1083 { 1084 if ( state_ == Autorefreshing ) 1085 state_ = Static; 1086 } 1087 1088 void CrawlerWidget::SearchState::startSearch() 1089 { 1090 if ( autoRefreshRequested_ ) 1091 state_ = Autorefreshing; 1092 else 1093 state_ = Static; 1094 } 1095 1096 /* 1097 * CrawlerWidgetContext 1098 */ 1099 CrawlerWidgetContext::CrawlerWidgetContext( const char* string ) 1100 { 1101 QRegularExpression regex( "S(\\d+):(\\d+)" ); 1102 QRegularExpressionMatch match = regex.match( string ); 1103 if ( match.hasMatch() ) { 1104 sizes_ = { match.captured(1).toInt(), match.captured(2).toInt() }; 1105 LOG(logDEBUG) << "sizes_: " << sizes_[0] << " " << sizes_[1]; 1106 } 1107 else { 1108 LOG(logWARNING) << "Unrecognised view size: " << string; 1109 1110 // Default values; 1111 sizes_ = { 100, 400 }; 1112 } 1113 1114 QRegularExpression case_refresh_regex( "IC(\\d+):AR(\\d+)" ); 1115 match = case_refresh_regex.match( string ); 1116 if ( match.hasMatch() ) { 1117 ignore_case_ = ( match.captured(1).toInt() == 1 ); 1118 auto_refresh_ = ( match.captured(2).toInt() == 1 ); 1119 1120 LOG(logDEBUG) << "ignore_case_: " << ignore_case_ << " auto_refresh_: " 1121 << auto_refresh_; 1122 } 1123 else { 1124 LOG(logWARNING) << "Unrecognised case/refresh: " << string; 1125 ignore_case_ = false; 1126 auto_refresh_ = false; 1127 } 1128 } 1129 1130 std::string CrawlerWidgetContext::toString() const 1131 { 1132 char string[160]; 1133 1134 snprintf( string, sizeof string, "S%d:%d:IC%d:AR%d", 1135 sizes_[0], sizes_[1], 1136 ignore_case_, auto_refresh_ ); 1137 1138 return { string }; 1139 } 1140