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