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