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