xref: /glogg/src/crawlerwidget.cpp (revision 702af59ea138e3124b906092de415e3601c74d3e)
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 // Return a pointer to the view in which we should do the QuickFind
123 SearchableWidgetInterface* CrawlerWidget::doGetActiveSearchable() const
124 {
125     return activeView();
126 }
127 
128 // Return all the searchable widgets (views)
129 std::vector<QObject*> CrawlerWidget::doGetAllSearchables() const
130 {
131     std::vector<QObject*> searchables =
132     { logMainView, filteredView };
133 
134     return searchables;
135 }
136 
137 // Update the state of the parent
138 void CrawlerWidget::doSendAllStateSignals()
139 {
140     emit updateLineNumber( currentLineNumber_ );
141     if ( !loadingInProgress_ )
142         emit loadingFinished( LoadingStatus::Successful );
143 }
144 
145 //
146 // Public slots
147 //
148 
149 void CrawlerWidget::stopLoading()
150 {
151     logFilteredData_->interruptSearch();
152     logData_->interruptLoading();
153 }
154 
155 void CrawlerWidget::reload()
156 {
157     searchState_.resetState();
158     logFilteredData_->clearSearch();
159     filteredView->updateData();
160     printSearchInfoMessage();
161 
162     logData_->reload();
163 
164     // A reload is considered as a first load,
165     // this is to prevent the "new data" icon to be triggered.
166     firstLoadDone_ = false;
167 }
168 
169 //
170 // Protected functions
171 //
172 void CrawlerWidget::doSetData(
173         std::shared_ptr<LogData> log_data,
174         std::shared_ptr<LogFilteredData> filtered_data )
175 {
176     logData_         = log_data.get();
177     logFilteredData_ = filtered_data.get();
178 }
179 
180 void CrawlerWidget::doSetQuickFindPattern(
181         std::shared_ptr<QuickFindPattern> qfp )
182 {
183     quickFindPattern_ = qfp;
184 }
185 
186 void CrawlerWidget::doSetSavedSearches(
187         std::shared_ptr<SavedSearches> saved_searches )
188 {
189     savedSearches_ = saved_searches;
190 
191     // We do setup now, assuming doSetData has been called before
192     // us, that's not great really...
193     setup();
194 }
195 
196 void CrawlerWidget::doSetViewContext(
197         const char* view_context )
198 {
199     LOG(logDEBUG) << "CrawlerWidget::doSetViewContext: " << view_context;
200 
201     CrawlerWidgetContext context = { view_context };
202 
203     setSizes( context.sizes() );
204     ignoreCaseCheck->setCheckState( context.ignoreCase() ? Qt::Checked : Qt::Unchecked );
205 
206     auto auto_refresh_check_state = context.autoRefresh() ? Qt::Checked : Qt::Unchecked;
207     searchRefreshCheck->setCheckState( auto_refresh_check_state );
208     // Manually call the handler as it is not called when changing the state programmatically
209     searchRefreshChangedHandler( auto_refresh_check_state );
210 }
211 
212 std::shared_ptr<const ViewContextInterface>
213 CrawlerWidget::doGetViewContext() const
214 {
215     auto context = std::make_shared<const CrawlerWidgetContext>(
216             sizes(),
217             ( ignoreCaseCheck->checkState() == Qt::Checked ),
218             ( searchRefreshCheck->checkState() == Qt::Checked ) );
219 
220     return static_cast<std::shared_ptr<const ViewContextInterface>>( context );
221 }
222 
223 //
224 // Slots
225 //
226 
227 void CrawlerWidget::startNewSearch()
228 {
229     // Record the search line in the recent list
230     // (reload the list first in case another glogg changed it)
231     GetPersistentInfo().retrieve( "savedSearches" );
232     savedSearches_->addRecent( searchLineEdit->currentText() );
233     GetPersistentInfo().save( "savedSearches" );
234 
235     // Update the SearchLine (history)
236     updateSearchCombo();
237     // Call the private function to do the search
238     replaceCurrentSearch( searchLineEdit->currentText() );
239 }
240 
241 void CrawlerWidget::stopSearch()
242 {
243     logFilteredData_->interruptSearch();
244     searchState_.stopSearch();
245     printSearchInfoMessage();
246 }
247 
248 // When receiving the 'newDataAvailable' signal from LogFilteredData
249 void CrawlerWidget::updateFilteredView( int nbMatches, int progress )
250 {
251     LOG(logDEBUG) << "updateFilteredView received.";
252 
253     if ( progress == 100 ) {
254         // Searching done
255         printSearchInfoMessage( nbMatches );
256         searchInfoLine->hideGauge();
257         // De-activate the stop button
258         stopButton->setEnabled( false );
259     }
260     else {
261         // Search in progress
262         // We ignore 0% and 100% to avoid a flash when the search is very short
263         if ( progress > 0 ) {
264             searchInfoLine->setText(
265                     tr("Search in progress (%1 %)... %2 match%3 found so far.")
266                     .arg( progress )
267                     .arg( nbMatches )
268                     .arg( nbMatches > 1 ? "es" : "" ) );
269             searchInfoLine->displayGauge( progress );
270         }
271     }
272 
273     // If more matches have been found
274     if ( nbMatches > nbMatches_ ) {
275         nbMatches_ = nbMatches;
276 
277         // Recompute the content of the filtered window.
278         filteredView->updateData();
279 
280         // Update the match overview
281         overview_.updateData( logData_->getNbLine() );
282 
283         // New data found icon
284         changeDataStatus( DataStatus::NEW_FILTERED_DATA );
285 
286         // Also update the top window for the coloured bullets.
287         update();
288     }
289 }
290 
291 void CrawlerWidget::jumpToMatchingLine(int filteredLineNb)
292 {
293     int mainViewLine = logFilteredData_->getMatchingLineNumber(filteredLineNb);
294     logMainView->selectAndDisplayLine(mainViewLine);  // FIXME: should be done with a signal.
295 }
296 
297 void CrawlerWidget::updateLineNumberHandler( int line )
298 {
299     currentLineNumber_ = line;
300     emit updateLineNumber( line );
301 }
302 
303 void CrawlerWidget::markLineFromMain( qint64 line )
304 {
305     if ( line < logData_->getNbLine() ) {
306         if ( logFilteredData_->isLineMarked( line ) )
307             logFilteredData_->deleteMark( line );
308         else
309             logFilteredData_->addMark( line );
310 
311         // Recompute the content of the filtered window.
312         filteredView->updateData();
313 
314         // Update the match overview
315         overview_.updateData( logData_->getNbLine() );
316 
317         // Also update the top window for the coloured bullets.
318         update();
319     }
320 }
321 
322 void CrawlerWidget::markLineFromFiltered( qint64 line )
323 {
324     if ( line < logFilteredData_->getNbLine() ) {
325         qint64 line_in_file = logFilteredData_->getMatchingLineNumber( line );
326         if ( logFilteredData_->filteredLineTypeByIndex( line )
327                 == LogFilteredData::Mark )
328             logFilteredData_->deleteMark( line_in_file );
329         else
330             logFilteredData_->addMark( line_in_file );
331 
332         // Recompute the content of the filtered window.
333         filteredView->updateData();
334 
335         // Update the match overview
336         overview_.updateData( logData_->getNbLine() );
337 
338         // Also update the top window for the coloured bullets.
339         update();
340     }
341 }
342 
343 void CrawlerWidget::applyConfiguration()
344 {
345     std::shared_ptr<Configuration> config =
346         Persistent<Configuration>( "settings" );
347     QFont font = config->mainFont();
348 
349     LOG(logDEBUG) << "CrawlerWidget::applyConfiguration";
350 
351     // Whatever font we use, we should NOT use kerning
352     font.setKerning( false );
353     font.setFixedPitch( true );
354 #if QT_VERSION > 0x040700
355     // Necessary on systems doing subpixel positionning (e.g. Ubuntu 12.04)
356     font.setStyleStrategy( QFont::ForceIntegerMetrics );
357 #endif
358     logMainView->setFont(font);
359     filteredView->setFont(font);
360 
361     logMainView->setLineNumbersVisible( config->mainLineNumbersVisible() );
362     filteredView->setLineNumbersVisible( config->filteredLineNumbersVisible() );
363 
364     overview_.setVisible( config->isOverviewVisible() );
365     logMainView->refreshOverview();
366 
367     logMainView->updateDisplaySize();
368     logMainView->update();
369     filteredView->updateDisplaySize();
370     filteredView->update();
371 
372     // Polling interval
373     logData_->setPollingInterval(
374             config->pollingEnabled() ? config->pollIntervalMs() : 0 );
375 
376     // Update the SearchLine (history)
377     updateSearchCombo();
378 }
379 
380 void CrawlerWidget::enteringQuickFind()
381 {
382     LOG(logDEBUG) << "CrawlerWidget::enteringQuickFind";
383 
384     // Remember who had the focus (only if it is one of our views)
385     QWidget* focus_widget =  QApplication::focusWidget();
386 
387     if ( ( focus_widget == logMainView ) || ( focus_widget == filteredView ) )
388         qfSavedFocus_ = focus_widget;
389     else
390         qfSavedFocus_ = nullptr;
391 }
392 
393 void CrawlerWidget::exitingQuickFind()
394 {
395     // Restore the focus once the QFBar has been hidden
396     if ( qfSavedFocus_ )
397         qfSavedFocus_->setFocus();
398 }
399 
400 void CrawlerWidget::loadingFinishedHandler( LoadingStatus status )
401 {
402     loadingInProgress_ = false;
403 
404     // We need to refresh the main window because the view lines on the
405     // overview have probably changed.
406     overview_.updateData( logData_->getNbLine() );
407 
408     // FIXME, handle topLine
409     // logMainView->updateData( logData_, topLine );
410     logMainView->updateData();
411 
412         // Shall we Forbid starting a search when loading in progress?
413         // searchButton->setEnabled( false );
414 
415     // searchButton->setEnabled( true );
416 
417     // See if we need to auto-refresh the search
418     if ( searchState_.isAutorefreshAllowed() ) {
419         if ( searchState_.isFileTruncated() )
420             // We need to restart the search
421             replaceCurrentSearch( searchLineEdit->currentText() );
422         else
423             logFilteredData_->updateSearch();
424     }
425 
426     emit loadingFinished( status );
427 
428     // Also change the data available icon
429     if ( firstLoadDone_ )
430         changeDataStatus( DataStatus::NEW_DATA );
431     else
432         firstLoadDone_ = true;
433 }
434 
435 void CrawlerWidget::fileChangedHandler( LogData::MonitoredFileStatus status )
436 {
437     // Handle the case where the file has been truncated
438     if ( status == LogData::Truncated ) {
439         // Clear all marks (TODO offer the option to keep them)
440         logFilteredData_->clearMarks();
441         if ( ! searchInfoLine->text().isEmpty() ) {
442             // Invalidate the search
443             logFilteredData_->clearSearch();
444             filteredView->updateData();
445             searchState_.truncateFile();
446             printSearchInfoMessage();
447             nbMatches_ = 0;
448         }
449     }
450 }
451 
452 // Returns a pointer to the window in which the search should be done
453 AbstractLogView* CrawlerWidget::activeView() const
454 {
455     QWidget* activeView;
456 
457     // Search in the window that has focus, or the window where 'Find' was
458     // called from, or the main window.
459     if ( filteredView->hasFocus() || logMainView->hasFocus() )
460         activeView = QApplication::focusWidget();
461     else
462         activeView = qfSavedFocus_;
463 
464     if ( activeView ) {
465         AbstractLogView* view = qobject_cast<AbstractLogView*>( activeView );
466         return view;
467     }
468     else {
469         LOG(logWARNING) << "No active view, defaulting to logMainView";
470         return logMainView;
471     }
472 }
473 
474 void CrawlerWidget::searchForward()
475 {
476     LOG(logDEBUG) << "CrawlerWidget::searchForward";
477 
478     activeView()->searchForward();
479 }
480 
481 void CrawlerWidget::searchBackward()
482 {
483     LOG(logDEBUG) << "CrawlerWidget::searchBackward";
484 
485     activeView()->searchBackward();
486 }
487 
488 void CrawlerWidget::searchRefreshChangedHandler( int state )
489 {
490     searchState_.setAutorefresh( state == Qt::Checked );
491     printSearchInfoMessage( logFilteredData_->getNbMatches() );
492 }
493 
494 void CrawlerWidget::searchTextChangeHandler()
495 {
496     // We suspend auto-refresh
497     searchState_.changeExpression();
498     printSearchInfoMessage( logFilteredData_->getNbMatches() );
499 }
500 
501 void CrawlerWidget::changeFilteredViewVisibility( int index )
502 {
503     QStandardItem* item = visibilityModel_->item( index );
504     FilteredView::Visibility visibility =
505         static_cast< FilteredView::Visibility>( item->data().toInt() );
506 
507     filteredView->setVisibility( visibility );
508 }
509 
510 void CrawlerWidget::addToSearch( const QString& string )
511 {
512     QString text = searchLineEdit->currentText();
513 
514     if ( text.isEmpty() )
515         text = string;
516     else {
517         // Escape the regexp chars from the string before adding it.
518         text += ( '|' + QRegExp::escape( string ) );
519     }
520 
521     searchLineEdit->setEditText( text );
522 
523     // Set the focus to lineEdit so that the user can press 'Return' immediately
524     searchLineEdit->lineEdit()->setFocus();
525 }
526 
527 void CrawlerWidget::mouseHoveredOverMatch( qint64 line )
528 {
529     qint64 line_in_mainview = logFilteredData_->getMatchingLineNumber( line );
530 
531     overviewWidget_->highlightLine( line_in_mainview );
532 }
533 
534 void CrawlerWidget::activityDetected()
535 {
536     changeDataStatus( DataStatus::OLD_DATA );
537 }
538 
539 //
540 // Private functions
541 //
542 
543 // Build the widget and connect all the signals, this must be done once
544 // the data are attached.
545 void CrawlerWidget::setup()
546 {
547     setOrientation(Qt::Vertical);
548 
549     assert( logData_ );
550     assert( logFilteredData_ );
551 
552     // The views
553     bottomWindow = new QWidget;
554     overviewWidget_ = new OverviewWidget();
555     logMainView     = new LogMainView(
556             logData_, quickFindPattern_.get(), &overview_, overviewWidget_ );
557     filteredView    = new FilteredView(
558             logFilteredData_, quickFindPattern_.get() );
559 
560     overviewWidget_->setOverview( &overview_ );
561     overviewWidget_->setParent( logMainView );
562 
563     // Connect the search to the top view
564     logMainView->useNewFiltering( logFilteredData_ );
565 
566     // Construct the visibility button
567     visibilityModel_ = new QStandardItemModel( this );
568 
569     QStandardItem *marksAndMatchesItem = new QStandardItem( tr( "Marks and matches" ) );
570     QPixmap marksAndMatchesPixmap( 16, 10 );
571     marksAndMatchesPixmap.fill( Qt::gray );
572     marksAndMatchesItem->setIcon( QIcon( marksAndMatchesPixmap ) );
573     marksAndMatchesItem->setData( FilteredView::MarksAndMatches );
574     visibilityModel_->appendRow( marksAndMatchesItem );
575 
576     QStandardItem *marksItem = new QStandardItem( tr( "Marks" ) );
577     QPixmap marksPixmap( 16, 10 );
578     marksPixmap.fill( Qt::blue );
579     marksItem->setIcon( QIcon( marksPixmap ) );
580     marksItem->setData( FilteredView::MarksOnly );
581     visibilityModel_->appendRow( marksItem );
582 
583     QStandardItem *matchesItem = new QStandardItem( tr( "Matches" ) );
584     QPixmap matchesPixmap( 16, 10 );
585     matchesPixmap.fill( Qt::red );
586     matchesItem->setIcon( QIcon( matchesPixmap ) );
587     matchesItem->setData( FilteredView::MatchesOnly );
588     visibilityModel_->appendRow( matchesItem );
589 
590     QListView *visibilityView = new QListView( this );
591     visibilityView->setMovement( QListView::Static );
592     visibilityView->setMinimumWidth( 170 ); // Only needed with custom style-sheet
593 
594     visibilityBox = new QComboBox();
595     visibilityBox->setModel( visibilityModel_ );
596     visibilityBox->setView( visibilityView );
597 
598     // Select "Marks and matches" by default (same default as the filtered view)
599     visibilityBox->setCurrentIndex( 0 );
600 
601     // TODO: Maybe there is some way to set the popup width to be
602     // sized-to-content (as it is when the stylesheet is not overriden) in the
603     // stylesheet as opposed to setting a hard min-width on the view above.
604     visibilityBox->setStyleSheet( " \
605         QComboBox:on {\
606             padding: 1px 2px 1px 6px;\
607             width: 19px;\
608         } \
609         QComboBox:!on {\
610             padding: 1px 2px 1px 7px;\
611             width: 19px;\
612             height: 16px;\
613             border: 1px solid gray;\
614         } \
615         QComboBox::drop-down::down-arrow {\
616             width: 0px;\
617             border-width: 0px;\
618         } \
619 " );
620 
621     // Construct the Search Info line
622     searchInfoLine = new InfoLine();
623     searchInfoLine->setFrameStyle( QFrame::WinPanel | QFrame::Sunken );
624     searchInfoLine->setLineWidth( 1 );
625     searchInfoLineDefaultPalette = searchInfoLine->palette();
626 
627     ignoreCaseCheck = new QCheckBox( "Ignore &case" );
628     searchRefreshCheck = new QCheckBox( "Auto-&refresh" );
629 
630     // Construct the Search line
631     searchLabel = new QLabel(tr("&Text: "));
632     searchLineEdit = new QComboBox;
633     searchLineEdit->setEditable( true );
634     searchLineEdit->setCompleter( 0 );
635     searchLineEdit->addItems( savedSearches_->recentSearches() );
636     searchLineEdit->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Minimum );
637     searchLineEdit->setSizeAdjustPolicy( QComboBox::AdjustToMinimumContentsLengthWithIcon );
638 
639     searchLabel->setBuddy( searchLineEdit );
640 
641     searchButton = new QToolButton();
642     searchButton->setText( tr("&Search") );
643     searchButton->setAutoRaise( true );
644 
645     stopButton = new QToolButton();
646     stopButton->setIcon( QIcon(":/images/stop14.png") );
647     stopButton->setAutoRaise( true );
648     stopButton->setEnabled( false );
649 
650     QHBoxLayout* searchLineLayout = new QHBoxLayout;
651     searchLineLayout->addWidget(searchLabel);
652     searchLineLayout->addWidget(searchLineEdit);
653     searchLineLayout->addWidget(searchButton);
654     searchLineLayout->addWidget(stopButton);
655     searchLineLayout->setContentsMargins(6, 0, 6, 0);
656     stopButton->setSizePolicy( QSizePolicy( QSizePolicy::Maximum, QSizePolicy::Maximum ) );
657     searchButton->setSizePolicy( QSizePolicy( QSizePolicy::Maximum, QSizePolicy::Maximum ) );
658 
659     QHBoxLayout* searchInfoLineLayout = new QHBoxLayout;
660     searchInfoLineLayout->addWidget( visibilityBox );
661     searchInfoLineLayout->addWidget( searchInfoLine );
662     searchInfoLineLayout->addWidget( ignoreCaseCheck );
663     searchInfoLineLayout->addWidget( searchRefreshCheck );
664 
665     // Construct the bottom window
666     QVBoxLayout* bottomMainLayout = new QVBoxLayout;
667     bottomMainLayout->addLayout(searchLineLayout);
668     bottomMainLayout->addLayout(searchInfoLineLayout);
669     bottomMainLayout->addWidget(filteredView);
670     bottomMainLayout->setContentsMargins(2, 1, 2, 1);
671     bottomWindow->setLayout(bottomMainLayout);
672 
673     addWidget( logMainView );
674     addWidget( bottomWindow );
675 
676     // Default splitter position (usually overridden by the config file)
677     QList<int> splitterSizes;
678     splitterSizes += 400;
679     splitterSizes += 100;
680     setSizes( splitterSizes );
681 
682     // Default search checkboxes
683     auto config = Persistent<Configuration>( "settings" );
684     searchRefreshCheck->setCheckState( config->isSearchAutoRefreshDefault() ?
685             Qt::Checked : Qt::Unchecked );
686     // Manually call the handler as it is not called when changing the state programmatically
687     searchRefreshChangedHandler( searchRefreshCheck->checkState() );
688     ignoreCaseCheck->setCheckState( config->isSearchIgnoreCaseDefault() ?
689             Qt::Checked : Qt::Unchecked );
690 
691     // Connect the signals
692     connect(searchLineEdit->lineEdit(), SIGNAL( returnPressed() ),
693             searchButton, SIGNAL( clicked() ));
694     connect(searchLineEdit->lineEdit(), SIGNAL( textEdited( const QString& ) ),
695             this, SLOT( searchTextChangeHandler() ));
696     connect(searchButton, SIGNAL( clicked() ),
697             this, SLOT( startNewSearch() ) );
698     connect(stopButton, SIGNAL( clicked() ),
699             this, SLOT( stopSearch() ) );
700 
701     connect(visibilityBox, SIGNAL( currentIndexChanged( int ) ),
702             this, SLOT( changeFilteredViewVisibility( int ) ) );
703 
704     connect(logMainView, SIGNAL( newSelection( int ) ),
705             logMainView, SLOT( update() ) );
706     connect(filteredView, SIGNAL( newSelection( int ) ),
707             this, SLOT( jumpToMatchingLine( int ) ) );
708     connect(filteredView, SIGNAL( newSelection( int ) ),
709             filteredView, SLOT( update() ) );
710     connect(logMainView, SIGNAL( updateLineNumber( int ) ),
711             this, SLOT( updateLineNumberHandler( int ) ) );
712     connect(logMainView, SIGNAL( markLine( qint64 ) ),
713             this, SLOT( markLineFromMain( qint64 ) ) );
714     connect(filteredView, SIGNAL( markLine( qint64 ) ),
715             this, SLOT( markLineFromFiltered( qint64 ) ) );
716 
717     connect(logMainView, SIGNAL( addToSearch( const QString& ) ),
718             this, SLOT( addToSearch( const QString& ) ) );
719     connect(filteredView, SIGNAL( addToSearch( const QString& ) ),
720             this, SLOT( addToSearch( const QString& ) ) );
721 
722     connect(filteredView, SIGNAL( mouseHoveredOverLine( qint64 ) ),
723             this, SLOT( mouseHoveredOverMatch( qint64 ) ) );
724     connect(filteredView, SIGNAL( mouseLeftHoveringZone() ),
725             overviewWidget_, SLOT( removeHighlight() ) );
726 
727     // Follow option (up and down)
728     connect(this, SIGNAL( followSet( bool ) ),
729             logMainView, SLOT( followSet( bool ) ) );
730     connect(this, SIGNAL( followSet( bool ) ),
731             filteredView, SLOT( followSet( bool ) ) );
732     connect(logMainView, SIGNAL( followModeChanged( bool ) ),
733             this, SIGNAL( followModeChanged( bool ) ) );
734     connect(filteredView, SIGNAL( followModeChanged( bool ) ),
735             this, SIGNAL( followModeChanged( bool ) ) );
736 
737     // Detect activity in the views
738     connect(logMainView, SIGNAL( activity() ),
739             this, SLOT( activityDetected() ) );
740     connect(filteredView, SIGNAL( activity() ),
741             this, SLOT( activityDetected() ) );
742 
743     connect( logFilteredData_, SIGNAL( searchProgressed( int, int ) ),
744             this, SLOT( updateFilteredView( int, int ) ) );
745 
746     // Sent load file update to MainWindow (for status update)
747     connect( logData_, SIGNAL( loadingProgressed( int ) ),
748             this, SIGNAL( loadingProgressed( int ) ) );
749     connect( logData_, SIGNAL( loadingFinished( LoadingStatus ) ),
750             this, SLOT( loadingFinishedHandler( LoadingStatus ) ) );
751     connect( logData_, SIGNAL( fileChanged( LogData::MonitoredFileStatus ) ),
752             this, SLOT( fileChangedHandler( LogData::MonitoredFileStatus ) ) );
753 
754     // Search auto-refresh
755     connect( searchRefreshCheck, SIGNAL( stateChanged( int ) ),
756             this, SLOT( searchRefreshChangedHandler( int ) ) );
757 
758     // Advise the parent the checkboxes have been changed
759     // (for maintaining default config)
760     connect( searchRefreshCheck, SIGNAL( stateChanged( int ) ),
761             this, SIGNAL( searchRefreshChanged( int ) ) );
762     connect( ignoreCaseCheck, SIGNAL( stateChanged( int ) ),
763             this, SIGNAL( ignoreCaseChanged( int ) ) );
764 }
765 
766 // Create a new search using the text passed, replace the currently
767 // used one and destroy the old one.
768 void CrawlerWidget::replaceCurrentSearch( const QString& searchText )
769 {
770     // Interrupt the search if it's ongoing
771     logFilteredData_->interruptSearch();
772 
773     nbMatches_ = 0;
774 
775     // We have to wait for the last search update (100%)
776     // before clearing/restarting to avoid having remaining results.
777 
778     // FIXME: this is a bit of a hack, we call processEvents
779     // for Qt to empty its event queue, including (hopefully)
780     // the search update event sent by logFilteredData_. It saves
781     // us the overhead of having proper sync.
782     QApplication::processEvents( QEventLoop::ExcludeUserInputEvents );
783 
784     if ( !searchText.isEmpty() ) {
785         // Determine the type of regexp depending on the config
786         QRegExp::PatternSyntax syntax;
787         static std::shared_ptr<Configuration> config =
788             Persistent<Configuration>( "settings" );
789         switch ( config->mainRegexpType() ) {
790             case Wildcard:
791                 syntax = QRegExp::Wildcard;
792                 break;
793             case FixedString:
794                 syntax = QRegExp::FixedString;
795                 break;
796             default:
797                 syntax = QRegExp::RegExp2;
798                 break;
799         }
800 
801         // Set the pattern case insensitive if needed
802         Qt::CaseSensitivity case_sensitivity = Qt::CaseSensitive;
803         if ( ignoreCaseCheck->checkState() == Qt::Checked )
804             case_sensitivity = Qt::CaseInsensitive;
805 
806         // Constructs the regexp
807         QRegExp regexp( searchText, case_sensitivity, syntax );
808 
809         if ( regexp.isValid() ) {
810             // Activate the stop button
811             stopButton->setEnabled( true );
812             // Start a new asynchronous search
813             logFilteredData_->runSearch( regexp );
814             // Accept auto-refresh of the search
815             searchState_.startSearch();
816         }
817         else {
818             // The regexp is wrong
819             logFilteredData_->clearSearch();
820             filteredView->updateData();
821             searchState_.resetState();
822 
823             // Inform the user
824             QString errorMessage = tr("Error in expression: ");
825             errorMessage += regexp.errorString();
826             searchInfoLine->setPalette( errorPalette );
827             searchInfoLine->setText( errorMessage );
828         }
829     }
830     else {
831         logFilteredData_->clearSearch();
832         filteredView->updateData();
833         searchState_.resetState();
834         printSearchInfoMessage();
835     }
836 }
837 
838 // Updates the content of the drop down list for the saved searches,
839 // called when the SavedSearch has been changed.
840 void CrawlerWidget::updateSearchCombo()
841 {
842     const QString text = searchLineEdit->lineEdit()->text();
843     searchLineEdit->clear();
844     searchLineEdit->addItems( savedSearches_->recentSearches() );
845     // In case we had something that wasn't added to the list (blank...):
846     searchLineEdit->lineEdit()->setText( text );
847 }
848 
849 // Print the search info message.
850 void CrawlerWidget::printSearchInfoMessage( int nbMatches )
851 {
852     QString text;
853 
854     switch ( searchState_.getState() ) {
855         case SearchState::NoSearch:
856             // Blank text is fine
857             break;
858         case SearchState::Static:
859             text = tr("%1 match%2 found.").arg( nbMatches )
860                 .arg( nbMatches > 1 ? "es" : "" );
861             break;
862         case SearchState::Autorefreshing:
863             text = tr("%1 match%2 found. Search is auto-refreshing...").arg( nbMatches )
864                 .arg( nbMatches > 1 ? "es" : "" );
865             break;
866         case SearchState::FileTruncated:
867         case SearchState::TruncatedAutorefreshing:
868             text = tr("File truncated on disk, previous search results are not valid anymore.");
869             break;
870     }
871 
872     searchInfoLine->setPalette( searchInfoLineDefaultPalette );
873     searchInfoLine->setText( text );
874 }
875 
876 // Change the data status and, if needed, advise upstream.
877 void CrawlerWidget::changeDataStatus( DataStatus status )
878 {
879     if ( ( status != dataStatus_ )
880             && (! ( dataStatus_ == DataStatus::NEW_FILTERED_DATA
881                     && status == DataStatus::NEW_DATA ) ) ) {
882         dataStatus_ = status;
883         emit dataStatusChanged( dataStatus_ );
884     }
885 }
886 
887 //
888 // SearchState implementation
889 //
890 void CrawlerWidget::SearchState::resetState()
891 {
892     state_ = NoSearch;
893 }
894 
895 void CrawlerWidget::SearchState::setAutorefresh( bool refresh )
896 {
897     autoRefreshRequested_ = refresh;
898 
899     if ( refresh ) {
900         if ( state_ == Static )
901             state_ = Autorefreshing;
902         /*
903         else if ( state_ == FileTruncated )
904             state_ = TruncatedAutorefreshing;
905         */
906     }
907     else {
908         if ( state_ == Autorefreshing )
909             state_ = Static;
910         else if ( state_ == TruncatedAutorefreshing )
911             state_ = FileTruncated;
912     }
913 }
914 
915 void CrawlerWidget::SearchState::truncateFile()
916 {
917     if ( state_ == Autorefreshing || state_ == TruncatedAutorefreshing ) {
918         state_ = TruncatedAutorefreshing;
919     }
920     else {
921         state_ = FileTruncated;
922     }
923 }
924 
925 void CrawlerWidget::SearchState::changeExpression()
926 {
927     if ( state_ == Autorefreshing )
928         state_ = Static;
929 }
930 
931 void CrawlerWidget::SearchState::stopSearch()
932 {
933     if ( state_ == Autorefreshing )
934         state_ = Static;
935 }
936 
937 void CrawlerWidget::SearchState::startSearch()
938 {
939     if ( autoRefreshRequested_ )
940         state_ = Autorefreshing;
941     else
942         state_ = Static;
943 }
944 
945 /*
946  * CrawlerWidgetContext
947  */
948 CrawlerWidgetContext::CrawlerWidgetContext( const char* string )
949 {
950     QRegExp regex = QRegExp( "S(\\d+):(\\d+)" );
951 
952     if ( regex.indexIn( string ) > -1 ) {
953         sizes_ = { regex.cap(1).toInt(), regex.cap(2).toInt() };
954         LOG(logDEBUG) << "sizes_: " << sizes_[0] << " " << sizes_[1];
955     }
956     else {
957         LOG(logWARNING) << "Unrecognised view size: " << string;
958 
959         // Default values;
960         sizes_ = { 100, 400 };
961     }
962 
963     QRegExp case_refresh_regex = QRegExp( "IC(\\d+):AR(\\d+)" );
964 
965     if ( case_refresh_regex.indexIn( string ) > -1 ) {
966         ignore_case_ = ( case_refresh_regex.cap(1).toInt() == 1 );
967         auto_refresh_ = ( case_refresh_regex.cap(2).toInt() == 1 );
968 
969         LOG(logDEBUG) << "ignore_case_: " << ignore_case_ << " auto_refresh_: "
970             << auto_refresh_;
971     }
972     else {
973         LOG(logWARNING) << "Unrecognised case/refresh: " << string;
974         ignore_case_ = false;
975         auto_refresh_ = false;
976     }
977 }
978 
979 std::string CrawlerWidgetContext::toString() const
980 {
981     char string[160];
982 
983     snprintf( string, sizeof string, "S%d:%d:IC%d:AR%d",
984             sizes_[0], sizes_[1],
985             ignore_case_, auto_refresh_ );
986 
987     return { string };
988 }
989