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