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