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