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