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
CrawlerWidgetContext(QList<int> sizes,bool ignore_case,bool auto_refresh,bool follow_file)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
sizes() const70 QList<int> sizes() const { return sizes_; }
71
ignoreCase() const72 bool ignoreCase() const { return ignore_case_; }
autoRefresh() const73 bool autoRefresh() const { return auto_refresh_; }
followFile() const74 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.
CrawlerWidget(QWidget * parent)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
getTopLine() const108 int CrawlerWidget::getTopLine() const
109 {
110 return logMainView->getTopLine();
111 }
112
getSelectedText() const113 QString CrawlerWidget::getSelectedText() const
114 {
115 if ( filteredView->hasFocus() )
116 return filteredView->getSelection();
117 else
118 return logMainView->getSelection();
119 }
120
selectAll()121 void CrawlerWidget::selectAll()
122 {
123 activeView()->selectAll();
124 }
125
encodingSetting() const126 Encoding CrawlerWidget::encodingSetting() const
127 {
128 return encodingSetting_;
129 }
130
isFollowEnabled() const131 bool CrawlerWidget::isFollowEnabled() const
132 {
133 return logMainView->isFollowEnabled();
134 }
135
encodingText() const136 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
doGetActiveSearchable() const142 SearchableWidgetInterface* CrawlerWidget::doGetActiveSearchable() const
143 {
144 return activeView();
145 }
146
147 // Return all the searchable widgets (views)
doGetAllSearchables() const148 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
doSendAllStateSignals()157 void CrawlerWidget::doSendAllStateSignals()
158 {
159 emit updateLineNumber( currentLineNumber_ );
160 if ( !loadingInProgress_ )
161 emit loadingFinished( LoadingStatus::Successful );
162 }
163
keyPressEvent(QKeyEvent * keyEvent)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
stopLoading()187 void CrawlerWidget::stopLoading()
188 {
189 logFilteredData_->interruptSearch();
190 logData_->interruptLoading();
191 }
192
reload()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
setEncoding(Encoding encoding)208 void CrawlerWidget::setEncoding( Encoding encoding )
209 {
210 encodingSetting_ = encoding;
211 updateEncoding();
212
213 update();
214 }
215
216 //
217 // Protected functions
218 //
doSetData(std::shared_ptr<LogData> log_data,std::shared_ptr<LogFilteredData> filtered_data)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
doSetQuickFindPattern(std::shared_ptr<QuickFindPattern> qfp)227 void CrawlerWidget::doSetQuickFindPattern(
228 std::shared_ptr<QuickFindPattern> qfp )
229 {
230 quickFindPattern_ = qfp;
231 }
232
doSetSavedSearches(std::shared_ptr<SavedSearches> saved_searches)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
doSetViewContext(const char * view_context)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>
doGetViewContext() const262 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
startNewSearch()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
stopSearch()291 void CrawlerWidget::stopSearch()
292 {
293 logFilteredData_->interruptSearch();
294 searchState_.stopSearch();
295 printSearchInfoMessage();
296 }
297
298 // When receiving the 'newDataAvailable' signal from LogFilteredData
updateFilteredView(int nbMatches,int progress,qint64 initial_position)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
jumpToMatchingLine(int filteredLineNb)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
updateLineNumberHandler(int line)358 void CrawlerWidget::updateLineNumberHandler( int line )
359 {
360 currentLineNumber_ = line;
361 emit updateLineNumber( line );
362 }
363
markLineFromMain(qint64 line)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
markLineFromFiltered(qint64 line)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
applyConfiguration()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
enteringQuickFind()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
exitingQuickFind()456 void CrawlerWidget::exitingQuickFind()
457 {
458 // Restore the focus once the QFBar has been hidden
459 if ( qfSavedFocus_ )
460 qfSavedFocus_->setFocus();
461 }
462
loadingFinishedHandler(LoadingStatus status)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
fileChangedHandler(LogData::MonitoredFileStatus status)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
activeView() const519 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
searchForward()540 void CrawlerWidget::searchForward()
541 {
542 LOG(logDEBUG) << "CrawlerWidget::searchForward";
543
544 activeView()->searchForward();
545 }
546
searchBackward()547 void CrawlerWidget::searchBackward()
548 {
549 LOG(logDEBUG) << "CrawlerWidget::searchBackward";
550
551 activeView()->searchBackward();
552 }
553
searchRefreshChangedHandler(int state)554 void CrawlerWidget::searchRefreshChangedHandler( int state )
555 {
556 searchState_.setAutorefresh( state == Qt::Checked );
557 printSearchInfoMessage( logFilteredData_->getNbMatches() );
558 }
559
searchTextChangeHandler()560 void CrawlerWidget::searchTextChangeHandler()
561 {
562 // We suspend auto-refresh
563 searchState_.changeExpression();
564 printSearchInfoMessage( logFilteredData_->getNbMatches() );
565 }
566
changeFilteredViewVisibility(int index)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
addToSearch(const QString & string)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
mouseHoveredOverMatch(qint64 line)596 void CrawlerWidget::mouseHoveredOverMatch( qint64 line )
597 {
598 qint64 line_in_mainview = logFilteredData_->getMatchingLineNumber( line );
599
600 overviewWidget_->highlightLine( line_in_mainview );
601 }
602
activityDetected()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.
setup()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.
replaceCurrentSearch(const QString & searchText)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.
updateSearchCombo()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.
printSearchInfoMessage(int nbMatches)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.
changeDataStatus(DataStatus status)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.
updateEncoding()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_BIG5:
1026 encoding = encodingSetting_;
1027 encoding_text_ = tr( "Displayed as Big5" );
1028 break;
1029 case Encoding::ENCODING_GB18030:
1030 encoding = encodingSetting_;
1031 encoding_text_ = tr( "Displayed as GB18030/GB2312" );
1032 break;
1033 case Encoding::ENCODING_SHIFT_JIS:
1034 encoding = encodingSetting_;
1035 encoding_text_ = tr( "Displayed as Shift_JIS" );
1036 break;
1037 case Encoding::ENCODING_KOI8R:
1038 encoding = encodingSetting_;
1039 encoding_text_ = tr( "Displayed as KOI8-R" );
1040 break;
1041 case Encoding::ENCODING_ISO_8859_1:
1042 default:
1043 encoding = Encoding::ENCODING_ISO_8859_1;
1044 encoding_text_ = tr( "Displayed as ISO-8859-1" );
1045 break;
1046 }
1047
1048 logData_->setDisplayEncoding( encoding );
1049 logMainView->forceRefresh();
1050 logFilteredData_->setDisplayEncoding( encoding );
1051 filteredView->forceRefresh();
1052 }
1053
1054 // Change the respective size of the two views
changeTopViewSize(int32_t delta)1055 void CrawlerWidget::changeTopViewSize( int32_t delta )
1056 {
1057 int min, max;
1058 getRange( 1, &min, &max );
1059 LOG(logDEBUG) << "CrawlerWidget::changeTopViewSize " << sizes()[0] << " " << min << " " << max;
1060 moveSplitter( closestLegalPosition( sizes()[0] + ( delta * 10 ), 1 ), 1 );
1061 LOG(logDEBUG) << "CrawlerWidget::changeTopViewSize " << sizes()[0];
1062 }
1063
1064 //
1065 // SearchState implementation
1066 //
resetState()1067 void CrawlerWidget::SearchState::resetState()
1068 {
1069 state_ = NoSearch;
1070 }
1071
setAutorefresh(bool refresh)1072 void CrawlerWidget::SearchState::setAutorefresh( bool refresh )
1073 {
1074 autoRefreshRequested_ = refresh;
1075
1076 if ( refresh ) {
1077 if ( state_ == Static )
1078 state_ = Autorefreshing;
1079 /*
1080 else if ( state_ == FileTruncated )
1081 state_ = TruncatedAutorefreshing;
1082 */
1083 }
1084 else {
1085 if ( state_ == Autorefreshing )
1086 state_ = Static;
1087 else if ( state_ == TruncatedAutorefreshing )
1088 state_ = FileTruncated;
1089 }
1090 }
1091
truncateFile()1092 void CrawlerWidget::SearchState::truncateFile()
1093 {
1094 if ( state_ == Autorefreshing || state_ == TruncatedAutorefreshing ) {
1095 state_ = TruncatedAutorefreshing;
1096 }
1097 else {
1098 state_ = FileTruncated;
1099 }
1100 }
1101
changeExpression()1102 void CrawlerWidget::SearchState::changeExpression()
1103 {
1104 if ( state_ == Autorefreshing )
1105 state_ = Static;
1106 }
1107
stopSearch()1108 void CrawlerWidget::SearchState::stopSearch()
1109 {
1110 if ( state_ == Autorefreshing )
1111 state_ = Static;
1112 }
1113
startSearch()1114 void CrawlerWidget::SearchState::startSearch()
1115 {
1116 if ( autoRefreshRequested_ )
1117 state_ = Autorefreshing;
1118 else
1119 state_ = Static;
1120 }
1121
1122 /*
1123 * CrawlerWidgetContext
1124 */
CrawlerWidgetContext(const char * string)1125 CrawlerWidgetContext::CrawlerWidgetContext( const char* string )
1126 {
1127 QRegularExpression regex( "S(\\d+):(\\d+)" );
1128 QRegularExpressionMatch match = regex.match( string );
1129 if ( match.hasMatch() ) {
1130 sizes_ = { match.captured(1).toInt(), match.captured(2).toInt() };
1131 LOG(logDEBUG) << "sizes_: " << sizes_[0] << " " << sizes_[1];
1132 }
1133 else {
1134 LOG(logWARNING) << "Unrecognised view size: " << string;
1135
1136 // Default values;
1137 sizes_ = { 100, 400 };
1138 }
1139
1140 QRegularExpression case_refresh_regex( "IC(\\d+):AR(\\d+)" );
1141 match = case_refresh_regex.match( string );
1142 if ( match.hasMatch() ) {
1143 ignore_case_ = ( match.captured(1).toInt() == 1 );
1144 auto_refresh_ = ( match.captured(2).toInt() == 1 );
1145
1146 LOG(logDEBUG) << "ignore_case_: " << ignore_case_ << " auto_refresh_: "
1147 << auto_refresh_;
1148 }
1149 else {
1150 LOG(logWARNING) << "Unrecognised case/refresh: " << string;
1151 ignore_case_ = false;
1152 auto_refresh_ = false;
1153 }
1154
1155 QRegularExpression follow_regex( "FF(\\d+)" );
1156 match = follow_regex.match( string );
1157 if ( match.hasMatch() ) {
1158 follow_file_ = ( match.captured(1).toInt() == 1 );
1159
1160 LOG(logDEBUG) << "follow_file_: " << follow_file_;
1161 }
1162 else {
1163 LOG(logWARNING) << "Unrecognised follow: " << string;
1164 follow_file_ = false;
1165 }
1166 }
1167
toString() const1168 std::string CrawlerWidgetContext::toString() const
1169 {
1170 char string[160];
1171
1172 snprintf( string, sizeof string, "S%d:%d:IC%d:AR%d:FF%d",
1173 sizes_[0], sizes_[1],
1174 ignore_case_, auto_refresh_, follow_file_ );
1175
1176 return { string };
1177 }
1178