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