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