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