xref: /glogg/src/crawlerwidget.cpp (revision 45ef183cd93d6c0b14e535ca8e31874f61b2ffa4)
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 <QKeyEvent>
35 #include <QStandardItemModel>
36 #include <QHeaderView>
37 #include <QListView>
38 
39 #include "crawlerwidget.h"
40 
41 #include "quickfindpattern.h"
42 #include "overview.h"
43 #include "infoline.h"
44 #include "savedsearches.h"
45 #include "quickfindwidget.h"
46 #include "persistentinfo.h"
47 #include "configuration.h"
48 
49 // Palette for error signaling (yellow background)
50 const QPalette CrawlerWidget::errorPalette( QColor( "yellow" ) );
51 
52 // Implementation of the view context for the CrawlerWidget
53 class CrawlerWidgetContext : public ViewContextInterface {
54   public:
55     // Construct from the stored string representation
56     CrawlerWidgetContext( const char* string );
57     // Construct from the value passsed
58     CrawlerWidgetContext( QList<int> sizes,
59            bool ignore_case,
60            bool auto_refresh )
61         : sizes_( sizes ),
62           ignore_case_( ignore_case ),
63           auto_refresh_( auto_refresh ) {}
64 
65     // Implementation of the ViewContextInterface function
66     std::string toString() const;
67 
68     // Access the Qt sizes array for the QSplitter
69     QList<int> sizes() const { return sizes_; }
70 
71     bool ignoreCase() const { return ignore_case_; }
72     bool autoRefresh() const { return auto_refresh_; }
73 
74   private:
75     QList<int> sizes_;
76 
77     bool ignore_case_;
78     bool auto_refresh_;
79 };
80 
81 // Constructor only does trivial construction. The real work is done once
82 // the data is attached.
83 CrawlerWidget::CrawlerWidget( QWidget *parent )
84         : QSplitter( parent ), overview_()
85 {
86     logData_         = nullptr;
87     logFilteredData_ = nullptr;
88 
89     quickFindPattern_ = nullptr;
90     savedSearches_   = nullptr;
91     qfSavedFocus_    = nullptr;
92 
93     // Until we have received confirmation loading is finished, we
94     // should consider we are loading something.
95     loadingInProgress_ = true;
96     // and it's the first time
97     firstLoadDone_     = false;
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     // Recompute the content of the filtered window.
274     filteredView->updateData();
275 
276     // Update the match overview
277     overview_.updateData( logData_->getNbLine() );
278 
279     // Also update the top window for the coloured bullets.
280     update();
281 }
282 
283 void CrawlerWidget::jumpToMatchingLine(int filteredLineNb)
284 {
285     int mainViewLine = logFilteredData_->getMatchingLineNumber(filteredLineNb);
286     logMainView->selectAndDisplayLine(mainViewLine);  // FIXME: should be done with a signal.
287 }
288 
289 void CrawlerWidget::updateLineNumberHandler( int line )
290 {
291     currentLineNumber_ = line;
292     emit updateLineNumber( line );
293 }
294 
295 void CrawlerWidget::markLineFromMain( qint64 line )
296 {
297     if ( logFilteredData_->isLineMarked( line ) )
298         logFilteredData_->deleteMark( line );
299     else
300         logFilteredData_->addMark( line );
301 
302     // Recompute the content of the filtered window.
303     filteredView->updateData();
304 
305     // Update the match overview
306     overview_.updateData( logData_->getNbLine() );
307 
308     // Also update the top window for the coloured bullets.
309     update();
310 }
311 
312 void CrawlerWidget::markLineFromFiltered( qint64 line )
313 {
314     qint64 line_in_file = logFilteredData_->getMatchingLineNumber( line );
315     if ( logFilteredData_->filteredLineTypeByIndex( line )
316             == LogFilteredData::Mark )
317         logFilteredData_->deleteMark( line_in_file );
318     else
319         logFilteredData_->addMark( line_in_file );
320 
321     // Recompute the content of the filtered window.
322     filteredView->updateData();
323 
324     // Update the match overview
325     overview_.updateData( logData_->getNbLine() );
326 
327     // Also update the top window for the coloured bullets.
328     update();
329 }
330 
331 void CrawlerWidget::applyConfiguration()
332 {
333     std::shared_ptr<Configuration> config =
334         Persistent<Configuration>( "settings" );
335     QFont font = config->mainFont();
336 
337     LOG(logDEBUG) << "CrawlerWidget::applyConfiguration";
338 
339     // Whatever font we use, we should NOT use kerning
340     font.setKerning( false );
341     font.setFixedPitch( true );
342 #if QT_VERSION > 0x040700
343     // Necessary on systems doing subpixel positionning (e.g. Ubuntu 12.04)
344     font.setStyleStrategy( QFont::ForceIntegerMetrics );
345 #endif
346     logMainView->setFont(font);
347     filteredView->setFont(font);
348 
349     logMainView->setLineNumbersVisible( config->mainLineNumbersVisible() );
350     filteredView->setLineNumbersVisible( config->filteredLineNumbersVisible() );
351 
352     overview_.setVisible( config->isOverviewVisible() );
353     logMainView->refreshOverview();
354 
355     logMainView->updateDisplaySize();
356     logMainView->update();
357     filteredView->updateDisplaySize();
358     filteredView->update();
359 
360     // Polling interval
361     logData_->setPollingInterval(
362             config->pollingEnabled() ? config->pollIntervalMs() : 0 );
363 
364     // Update the SearchLine (history)
365     updateSearchCombo();
366 }
367 
368 void CrawlerWidget::enteringQuickFind()
369 {
370     LOG(logDEBUG) << "CrawlerWidget::enteringQuickFind";
371 
372     // Remember who had the focus (only if it is one of our views)
373     QWidget* focus_widget =  QApplication::focusWidget();
374 
375     if ( ( focus_widget == logMainView ) || ( focus_widget == filteredView ) )
376         qfSavedFocus_ = focus_widget;
377     else
378         qfSavedFocus_ = nullptr;
379 }
380 
381 void CrawlerWidget::exitingQuickFind()
382 {
383     // Restore the focus once the QFBar has been hidden
384     if ( qfSavedFocus_ )
385         qfSavedFocus_->setFocus();
386 }
387 
388 void CrawlerWidget::loadingFinishedHandler( LoadingStatus status )
389 {
390     loadingInProgress_ = false;
391 
392     // We need to refresh the main window because the view lines on the
393     // overview have probably changed.
394     overview_.updateData( logData_->getNbLine() );
395 
396     // FIXME, handle topLine
397     // logMainView->updateData( logData_, topLine );
398     logMainView->updateData();
399 
400         // Shall we Forbid starting a search when loading in progress?
401         // searchButton->setEnabled( false );
402 
403     // searchButton->setEnabled( true );
404 
405     // See if we need to auto-refresh the search
406     if ( searchState_.isAutorefreshAllowed() ) {
407         if ( searchState_.isFileTruncated() )
408             // We need to restart the search
409             replaceCurrentSearch( searchLineEdit->currentText() );
410         else
411             logFilteredData_->updateSearch();
412     }
413 
414     emit loadingFinished( status );
415 
416     // Also change the data available icon
417     if ( firstLoadDone_ )
418         changeDataStatus( DataStatus::NEW_DATA );
419     else
420         firstLoadDone_ = true;
421 }
422 
423 void CrawlerWidget::fileChangedHandler( LogData::MonitoredFileStatus status )
424 {
425     // Handle the case where the file has been truncated
426     if ( status == LogData::Truncated ) {
427         // Clear all marks (TODO offer the option to keep them)
428         logFilteredData_->clearMarks();
429         if ( ! searchInfoLine->text().isEmpty() ) {
430             // Invalidate the search
431             logFilteredData_->clearSearch();
432             filteredView->updateData();
433             searchState_.truncateFile();
434             printSearchInfoMessage();
435         }
436     }
437 }
438 
439 // Returns a pointer to the window in which the search should be done
440 AbstractLogView* CrawlerWidget::activeView() const
441 {
442     QWidget* activeView;
443 
444     // Search in the window that has focus, or the window where 'Find' was
445     // called from, or the main window.
446     if ( filteredView->hasFocus() || logMainView->hasFocus() )
447         activeView = QApplication::focusWidget();
448     else
449         activeView = qfSavedFocus_;
450 
451     if ( activeView ) {
452         AbstractLogView* view = qobject_cast<AbstractLogView*>( activeView );
453         return view;
454     }
455     else {
456         LOG(logWARNING) << "No active view, defaulting to logMainView";
457         return logMainView;
458     }
459 }
460 
461 void CrawlerWidget::searchForward()
462 {
463     LOG(logDEBUG) << "CrawlerWidget::searchForward";
464 
465     activeView()->searchForward();
466 }
467 
468 void CrawlerWidget::searchBackward()
469 {
470     LOG(logDEBUG) << "CrawlerWidget::searchBackward";
471 
472     activeView()->searchBackward();
473 }
474 
475 void CrawlerWidget::searchRefreshChangedHandler( int state )
476 {
477     searchState_.setAutorefresh( state == Qt::Checked );
478     printSearchInfoMessage( logFilteredData_->getNbMatches() );
479 }
480 
481 void CrawlerWidget::searchTextChangeHandler()
482 {
483     // We suspend auto-refresh
484     searchState_.changeExpression();
485     printSearchInfoMessage( logFilteredData_->getNbMatches() );
486 }
487 
488 void CrawlerWidget::changeFilteredViewVisibility( int index )
489 {
490     QStandardItem* item = visibilityModel_->item( index );
491     FilteredView::Visibility visibility =
492         static_cast< FilteredView::Visibility>( item->data().toInt() );
493 
494     filteredView->setVisibility( visibility );
495 }
496 
497 void CrawlerWidget::addToSearch( const QString& string )
498 {
499     QString text = searchLineEdit->currentText();
500 
501     if ( text.isEmpty() )
502         text = string;
503     else {
504         // Escape the regexp chars from the string before adding it.
505         text += ( '|' + QRegExp::escape( string ) );
506     }
507 
508     searchLineEdit->setEditText( text );
509 
510     // Set the focus to lineEdit so that the user can press 'Return' immediately
511     searchLineEdit->lineEdit()->setFocus();
512 }
513 
514 void CrawlerWidget::mouseHoveredOverMatch( qint64 line )
515 {
516     qint64 line_in_mainview = logFilteredData_->getMatchingLineNumber( line );
517 
518     overviewWidget_->highlightLine( line_in_mainview );
519 }
520 
521 void CrawlerWidget::activityDetected()
522 {
523     changeDataStatus( DataStatus::OLD_DATA );
524 }
525 
526 //
527 // Private functions
528 //
529 
530 // Build the widget and connect all the signals, this must be done once
531 // the data are attached.
532 void CrawlerWidget::setup()
533 {
534     setOrientation(Qt::Vertical);
535 
536     assert( logData_ );
537     assert( logFilteredData_ );
538 
539     // The views
540     bottomWindow = new QWidget;
541     overviewWidget_ = new OverviewWidget();
542     logMainView     = new LogMainView(
543             logData_, quickFindPattern_.get(), &overview_, overviewWidget_ );
544     filteredView    = new FilteredView(
545             logFilteredData_, quickFindPattern_.get() );
546 
547     overviewWidget_->setOverview( &overview_ );
548     overviewWidget_->setParent( logMainView );
549 
550     // Connect the search to the top view
551     logMainView->useNewFiltering( logFilteredData_ );
552 
553     // Construct the visibility button
554     visibilityModel_ = new QStandardItemModel( this );
555 
556     QStandardItem *marksAndMatchesItem = new QStandardItem( tr( "Marks and matches" ) );
557     QPixmap marksAndMatchesPixmap( 16, 10 );
558     marksAndMatchesPixmap.fill( Qt::gray );
559     marksAndMatchesItem->setIcon( QIcon( marksAndMatchesPixmap ) );
560     marksAndMatchesItem->setData( FilteredView::MarksAndMatches );
561     visibilityModel_->appendRow( marksAndMatchesItem );
562 
563     QStandardItem *marksItem = new QStandardItem( tr( "Marks" ) );
564     QPixmap marksPixmap( 16, 10 );
565     marksPixmap.fill( Qt::blue );
566     marksItem->setIcon( QIcon( marksPixmap ) );
567     marksItem->setData( FilteredView::MarksOnly );
568     visibilityModel_->appendRow( marksItem );
569 
570     QStandardItem *matchesItem = new QStandardItem( tr( "Matches" ) );
571     QPixmap matchesPixmap( 16, 10 );
572     matchesPixmap.fill( Qt::red );
573     matchesItem->setIcon( QIcon( matchesPixmap ) );
574     matchesItem->setData( FilteredView::MatchesOnly );
575     visibilityModel_->appendRow( matchesItem );
576 
577     QListView *visibilityView = new QListView( this );
578     visibilityView->setMovement( QListView::Static );
579     visibilityView->setMinimumWidth( 170 ); // Only needed with custom style-sheet
580 
581     visibilityBox = new QComboBox();
582     visibilityBox->setModel( visibilityModel_ );
583     visibilityBox->setView( visibilityView );
584 
585     // Select "Marks and matches" by default (same default as the filtered view)
586     visibilityBox->setCurrentIndex( 0 );
587 
588     // TODO: Maybe there is some way to set the popup width to be
589     // sized-to-content (as it is when the stylesheet is not overriden) in the
590     // stylesheet as opposed to setting a hard min-width on the view above.
591     visibilityBox->setStyleSheet( " \
592         QComboBox:on {\
593             padding: 1px 2px 1px 6px;\
594             width: 19px;\
595         } \
596         QComboBox:!on {\
597             padding: 1px 2px 1px 7px;\
598             width: 19px;\
599             height: 16px;\
600             border: 1px solid gray;\
601         } \
602         QComboBox::drop-down::down-arrow {\
603             width: 0px;\
604             border-width: 0px;\
605         } \
606 " );
607 
608     // Construct the Search Info line
609     searchInfoLine = new InfoLine();
610     searchInfoLine->setFrameStyle( QFrame::WinPanel | QFrame::Sunken );
611     searchInfoLine->setLineWidth( 1 );
612     searchInfoLineDefaultPalette = searchInfoLine->palette();
613 
614     ignoreCaseCheck = new QCheckBox( "Ignore &case" );
615     searchRefreshCheck = new QCheckBox( "Auto-&refresh" );
616 
617     // Construct the Search line
618     searchLabel = new QLabel(tr("&Text: "));
619     searchLineEdit = new QComboBox;
620     searchLineEdit->setEditable( true );
621     searchLineEdit->setCompleter( 0 );
622     searchLineEdit->addItems( savedSearches_->recentSearches() );
623     searchLineEdit->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Minimum );
624     searchLineEdit->setSizeAdjustPolicy( QComboBox::AdjustToMinimumContentsLengthWithIcon );
625 
626     searchLabel->setBuddy( searchLineEdit );
627 
628     searchButton = new QToolButton();
629     searchButton->setText( tr("&Search") );
630     searchButton->setAutoRaise( true );
631 
632     stopButton = new QToolButton();
633     stopButton->setIcon( QIcon(":/images/stop16.png") );
634     stopButton->setAutoRaise( true );
635     stopButton->setEnabled( false );
636 
637     QHBoxLayout* searchLineLayout = new QHBoxLayout;
638     searchLineLayout->addWidget(searchLabel);
639     searchLineLayout->addWidget(searchLineEdit);
640     searchLineLayout->addWidget(searchButton);
641     searchLineLayout->addWidget(stopButton);
642     searchLineLayout->setContentsMargins(6, 0, 6, 0);
643     stopButton->setSizePolicy( QSizePolicy( QSizePolicy::Maximum, QSizePolicy::Maximum ) );
644     searchButton->setSizePolicy( QSizePolicy( QSizePolicy::Maximum, QSizePolicy::Maximum ) );
645 
646     QHBoxLayout* searchInfoLineLayout = new QHBoxLayout;
647     searchInfoLineLayout->addWidget( visibilityBox );
648     searchInfoLineLayout->addWidget( searchInfoLine );
649     searchInfoLineLayout->addWidget( ignoreCaseCheck );
650     searchInfoLineLayout->addWidget( searchRefreshCheck );
651 
652     // Construct the bottom window
653     QVBoxLayout* bottomMainLayout = new QVBoxLayout;
654     bottomMainLayout->addLayout(searchLineLayout);
655     bottomMainLayout->addLayout(searchInfoLineLayout);
656     bottomMainLayout->addWidget(filteredView);
657     bottomMainLayout->setContentsMargins(2, 1, 2, 1);
658     bottomWindow->setLayout(bottomMainLayout);
659 
660     addWidget( logMainView );
661     addWidget( bottomWindow );
662 
663     // Default splitter position (usually overridden by the config file)
664     QList<int> splitterSizes;
665     splitterSizes += 400;
666     splitterSizes += 100;
667     setSizes( splitterSizes );
668 
669     // Default search checkboxes
670     auto config = Persistent<Configuration>( "settings" );
671     searchRefreshCheck->setCheckState( config->isSearchAutoRefreshDefault() ?
672             Qt::Checked : Qt::Unchecked );
673     // Manually call the handler as it is not called when changing the state programmatically
674     searchRefreshChangedHandler( searchRefreshCheck->checkState() );
675     ignoreCaseCheck->setCheckState( config->isSearchIgnoreCaseDefault() ?
676             Qt::Checked : Qt::Unchecked );
677 
678     // Connect the signals
679     connect(searchLineEdit->lineEdit(), SIGNAL( returnPressed() ),
680             searchButton, SIGNAL( clicked() ));
681     connect(searchLineEdit->lineEdit(), SIGNAL( textEdited( const QString& ) ),
682             this, SLOT( searchTextChangeHandler() ));
683     connect(searchButton, SIGNAL( clicked() ),
684             this, SLOT( startNewSearch() ) );
685     connect(stopButton, SIGNAL( clicked() ),
686             this, SLOT( stopSearch() ) );
687 
688     connect(visibilityBox, SIGNAL( currentIndexChanged( int ) ),
689             this, SLOT( changeFilteredViewVisibility( int ) ) );
690 
691     connect(logMainView, SIGNAL( newSelection( int ) ),
692             logMainView, SLOT( update() ) );
693     connect(filteredView, SIGNAL( newSelection( int ) ),
694             this, SLOT( jumpToMatchingLine( int ) ) );
695     connect(filteredView, SIGNAL( newSelection( int ) ),
696             filteredView, SLOT( update() ) );
697     connect(logMainView, SIGNAL( updateLineNumber( int ) ),
698             this, SLOT( updateLineNumberHandler( int ) ) );
699     connect(logMainView, SIGNAL( markLine( qint64 ) ),
700             this, SLOT( markLineFromMain( qint64 ) ) );
701     connect(filteredView, SIGNAL( markLine( qint64 ) ),
702             this, SLOT( markLineFromFiltered( qint64 ) ) );
703 
704     connect(logMainView, SIGNAL( addToSearch( const QString& ) ),
705             this, SLOT( addToSearch( const QString& ) ) );
706     connect(filteredView, SIGNAL( addToSearch( const QString& ) ),
707             this, SLOT( addToSearch( const QString& ) ) );
708 
709     connect(filteredView, SIGNAL( mouseHoveredOverLine( qint64 ) ),
710             this, SLOT( mouseHoveredOverMatch( qint64 ) ) );
711     connect(filteredView, SIGNAL( mouseLeftHoveringZone() ),
712             overviewWidget_, SLOT( removeHighlight() ) );
713 
714     // Follow option (up and down)
715     connect(this, SIGNAL( followSet( bool ) ),
716             logMainView, SLOT( followSet( bool ) ) );
717     connect(this, SIGNAL( followSet( bool ) ),
718             filteredView, SLOT( followSet( bool ) ) );
719     connect(logMainView, SIGNAL( followDisabled() ),
720             this, SIGNAL( followDisabled() ) );
721     connect(filteredView, SIGNAL( followDisabled() ),
722             this, SIGNAL( followDisabled() ) );
723 
724     // Detect activity in the views
725     connect(logMainView, SIGNAL( activity() ),
726             this, SLOT( activityDetected() ) );
727     connect(filteredView, SIGNAL( activity() ),
728             this, SLOT( activityDetected() ) );
729 
730     connect( logFilteredData_, SIGNAL( searchProgressed( int, int ) ),
731             this, SLOT( updateFilteredView( int, int ) ) );
732 
733     // Sent load file update to MainWindow (for status update)
734     connect( logData_, SIGNAL( loadingProgressed( int ) ),
735             this, SIGNAL( loadingProgressed( int ) ) );
736     connect( logData_, SIGNAL( loadingFinished( LoadingStatus ) ),
737             this, SLOT( loadingFinishedHandler( LoadingStatus ) ) );
738     connect( logData_, SIGNAL( fileChanged( LogData::MonitoredFileStatus ) ),
739             this, SLOT( fileChangedHandler( LogData::MonitoredFileStatus ) ) );
740 
741     // Search auto-refresh
742     connect( searchRefreshCheck, SIGNAL( stateChanged( int ) ),
743             this, SLOT( searchRefreshChangedHandler( int ) ) );
744 
745     // Advise the parent the checkboxes have been changed
746     // (for maintaining default config)
747     connect( searchRefreshCheck, SIGNAL( stateChanged( int ) ),
748             this, SIGNAL( searchRefreshChanged( int ) ) );
749     connect( ignoreCaseCheck, SIGNAL( stateChanged( int ) ),
750             this, SIGNAL( ignoreCaseChanged( int ) ) );
751 }
752 
753 // Create a new search using the text passed, replace the currently
754 // used one and destroy the old one.
755 void CrawlerWidget::replaceCurrentSearch( const QString& searchText )
756 {
757     // Interrupt the search if it's ongoing
758     logFilteredData_->interruptSearch();
759 
760     // We have to wait for the last search update (100%)
761     // before clearing/restarting to avoid having remaining results.
762 
763     // FIXME: this is a bit of a hack, we call processEvents
764     // for Qt to empty its event queue, including (hopefully)
765     // the search update event sent by logFilteredData_. It saves
766     // us the overhead of having proper sync.
767     QApplication::processEvents( QEventLoop::ExcludeUserInputEvents );
768 
769     if ( !searchText.isEmpty() ) {
770         // Determine the type of regexp depending on the config
771         QRegExp::PatternSyntax syntax;
772         static std::shared_ptr<Configuration> config =
773             Persistent<Configuration>( "settings" );
774         switch ( config->mainRegexpType() ) {
775             case Wildcard:
776                 syntax = QRegExp::Wildcard;
777                 break;
778             case FixedString:
779                 syntax = QRegExp::FixedString;
780                 break;
781             default:
782                 syntax = QRegExp::RegExp2;
783                 break;
784         }
785 
786         // Set the pattern case insensitive if needed
787         Qt::CaseSensitivity case_sensitivity = Qt::CaseSensitive;
788         if ( ignoreCaseCheck->checkState() == Qt::Checked )
789             case_sensitivity = Qt::CaseInsensitive;
790 
791         // Constructs the regexp
792         QRegExp regexp( searchText, case_sensitivity, syntax );
793 
794         if ( regexp.isValid() ) {
795             // Activate the stop button
796             stopButton->setEnabled( true );
797             // Start a new asynchronous search
798             logFilteredData_->runSearch( regexp );
799             // Accept auto-refresh of the search
800             searchState_.startSearch();
801         }
802         else {
803             // The regexp is wrong
804             logFilteredData_->clearSearch();
805             filteredView->updateData();
806             searchState_.resetState();
807 
808             // Inform the user
809             QString errorMessage = tr("Error in expression: ");
810             errorMessage += regexp.errorString();
811             searchInfoLine->setPalette( errorPalette );
812             searchInfoLine->setText( errorMessage );
813         }
814     }
815     else {
816         logFilteredData_->clearSearch();
817         filteredView->updateData();
818         searchState_.resetState();
819         printSearchInfoMessage();
820     }
821 }
822 
823 // Updates the content of the drop down list for the saved searches,
824 // called when the SavedSearch has been changed.
825 void CrawlerWidget::updateSearchCombo()
826 {
827     const QString text = searchLineEdit->lineEdit()->text();
828     searchLineEdit->clear();
829     searchLineEdit->addItems( savedSearches_->recentSearches() );
830     // In case we had something that wasn't added to the list (blank...):
831     searchLineEdit->lineEdit()->setText( text );
832 }
833 
834 // Print the search info message.
835 void CrawlerWidget::printSearchInfoMessage( int nbMatches )
836 {
837     QString text;
838 
839     switch ( searchState_.getState() ) {
840         case SearchState::NoSearch:
841             // Blank text is fine
842             break;
843         case SearchState::Static:
844             text = tr("%1 match%2 found.").arg( nbMatches )
845                 .arg( nbMatches > 1 ? "es" : "" );
846             break;
847         case SearchState::Autorefreshing:
848             text = tr("%1 match%2 found. Search is auto-refreshing...").arg( nbMatches )
849                 .arg( nbMatches > 1 ? "es" : "" );
850             break;
851         case SearchState::FileTruncated:
852         case SearchState::TruncatedAutorefreshing:
853             text = tr("File truncated on disk, previous search results are not valid anymore.");
854             break;
855     }
856 
857     searchInfoLine->setPalette( searchInfoLineDefaultPalette );
858     searchInfoLine->setText( text );
859 }
860 
861 // Change the data status and, if needed, advise upstream.
862 void CrawlerWidget::changeDataStatus( DataStatus status )
863 {
864     if ( status != dataStatus_ ) {
865         dataStatus_ = status;
866         emit dataStatusChanged( dataStatus_ );
867     }
868 }
869 
870 //
871 // SearchState implementation
872 //
873 void CrawlerWidget::SearchState::resetState()
874 {
875     state_ = NoSearch;
876 }
877 
878 void CrawlerWidget::SearchState::setAutorefresh( bool refresh )
879 {
880     autoRefreshRequested_ = refresh;
881 
882     if ( refresh ) {
883         if ( state_ == Static )
884             state_ = Autorefreshing;
885         /*
886         else if ( state_ == FileTruncated )
887             state_ = TruncatedAutorefreshing;
888         */
889     }
890     else {
891         if ( state_ == Autorefreshing )
892             state_ = Static;
893         else if ( state_ == TruncatedAutorefreshing )
894             state_ = FileTruncated;
895     }
896 }
897 
898 void CrawlerWidget::SearchState::truncateFile()
899 {
900     if ( state_ == Autorefreshing || state_ == TruncatedAutorefreshing ) {
901         state_ = TruncatedAutorefreshing;
902     }
903     else {
904         state_ = FileTruncated;
905     }
906 }
907 
908 void CrawlerWidget::SearchState::changeExpression()
909 {
910     if ( state_ == Autorefreshing )
911         state_ = Static;
912 }
913 
914 void CrawlerWidget::SearchState::stopSearch()
915 {
916     if ( state_ == Autorefreshing )
917         state_ = Static;
918 }
919 
920 void CrawlerWidget::SearchState::startSearch()
921 {
922     if ( autoRefreshRequested_ )
923         state_ = Autorefreshing;
924     else
925         state_ = Static;
926 }
927 
928 /*
929  * CrawlerWidgetContext
930  */
931 CrawlerWidgetContext::CrawlerWidgetContext( const char* string )
932 {
933     QRegExp regex = QRegExp( "S(\\d+):(\\d+)" );
934 
935     if ( regex.indexIn( string ) > -1 ) {
936         sizes_ = { regex.cap(1).toInt(), regex.cap(2).toInt() };
937         LOG(logDEBUG) << "sizes_: " << sizes_[0] << " " << sizes_[1];
938     }
939     else {
940         LOG(logWARNING) << "Unrecognised view size: " << string;
941 
942         // Default values;
943         sizes_ = { 100, 400 };
944     }
945 
946     QRegExp case_refresh_regex = QRegExp( "IC(\\d+):AR(\\d+)" );
947 
948     if ( case_refresh_regex.indexIn( string ) > -1 ) {
949         ignore_case_ = ( case_refresh_regex.cap(1).toInt() == 1 );
950         auto_refresh_ = ( case_refresh_regex.cap(2).toInt() == 1 );
951 
952         LOG(logDEBUG) << "ignore_case_: " << ignore_case_ << " auto_refresh_: "
953             << auto_refresh_;
954     }
955     else {
956         LOG(logWARNING) << "Unrecognised case/refresh: " << string;
957         ignore_case_ = false;
958         auto_refresh_ = false;
959     }
960 }
961 
962 std::string CrawlerWidgetContext::toString() const
963 {
964     char string[160];
965 
966     snprintf( string, sizeof string, "S%d:%d:IC%d:AR%d",
967             sizes_[0], sizes_[1],
968             ignore_case_, auto_refresh_ );
969 
970     return { string };
971 }
972