xref: /glogg/src/mainwindow.cpp (revision 05467f52d72e282658c219cc889ed6bc08e5dc01)
1 /*
2  * Copyright (C) 2009, 2010, 2011, 2013, 2014 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 MainWindow. It is responsible for creating and
21 // managing the menus, the toolbar, and the CrawlerWidget. It also
22 // load/save the settings on opening/closing of the app
23 
24 #include <iostream>
25 #include <cassert>
26 
27 #include <QAction>
28 #include <QDesktopWidget>
29 #include <QMenuBar>
30 #include <QToolBar>
31 #include <QFileInfo>
32 #include <QFileDialog>
33 #include <QClipboard>
34 #include <QMessageBox>
35 #include <QCloseEvent>
36 #include <QDragEnterEvent>
37 #include <QMimeData>
38 #include <QUrl>
39 
40 #include "log.h"
41 
42 #include "mainwindow.h"
43 
44 #include "sessioninfo.h"
45 #include "recentfiles.h"
46 #include "crawlerwidget.h"
47 #include "filtersdialog.h"
48 #include "optionsdialog.h"
49 #include "persistentinfo.h"
50 #include "menuactiontooltipbehavior.h"
51 #include "tabbedcrawlerwidget.h"
52 #include "externalcom.h"
53 
54 // Returns the size in human readable format
55 static QString readableSize( qint64 size );
56 
57 MainWindow::MainWindow( std::unique_ptr<Session> session,
58         std::shared_ptr<ExternalCommunicator> external_communicator ) :
59     session_( std::move( session )  ),
60     externalCommunicator_( external_communicator ),
61     recentFiles_( Persistent<RecentFiles>( "recentFiles" ) ),
62     mainIcon_(),
63     signalMux_(),
64     quickFindMux_( session_->getQuickFindPattern() ),
65     mainTabWidget_()
66 #ifdef GLOGG_SUPPORTS_VERSION_CHECKING
67     ,versionChecker_()
68 #endif
69 {
70     createActions();
71     createMenus();
72     createToolBars();
73     // createStatusBar();
74 
75     setAcceptDrops( true );
76 
77     // Default geometry
78     const QRect geometry = QApplication::desktop()->availableGeometry( this );
79     setGeometry( geometry.x() + 20, geometry.y() + 40,
80             geometry.width() - 140, geometry.height() - 140 );
81 
82     mainIcon_.addFile( ":/images/hicolor/16x16/glogg.png" );
83     mainIcon_.addFile( ":/images/hicolor/24x24/glogg.png" );
84     mainIcon_.addFile( ":/images/hicolor/32x32/glogg.png" );
85     mainIcon_.addFile( ":/images/hicolor/48x48/glogg.png" );
86 
87     setWindowIcon( mainIcon_ );
88 
89     readSettings();
90 
91     // Connect the signals to the mux (they will be forwarded to the
92     // "current" crawlerwidget
93 
94     // Send actions to the crawlerwidget
95     signalMux_.connect( this, SIGNAL( followSet( bool ) ),
96             SIGNAL( followSet( bool ) ) );
97     signalMux_.connect( this, SIGNAL( optionsChanged() ),
98             SLOT( applyConfiguration() ) );
99     signalMux_.connect( this, SIGNAL( enteringQuickFind() ),
100             SLOT( enteringQuickFind() ) );
101     signalMux_.connect( &quickFindWidget_, SIGNAL( close() ),
102             SLOT( exitingQuickFind() ) );
103 
104     // Actions from the CrawlerWidget
105     signalMux_.connect( SIGNAL( followModeChanged( bool ) ),
106             this, SLOT( changeFollowMode( bool ) ) );
107     signalMux_.connect( SIGNAL( updateLineNumber( int ) ),
108             this, SLOT( lineNumberHandler( int ) ) );
109 
110     // Register for progress status bar
111     signalMux_.connect( SIGNAL( loadingProgressed( int ) ),
112             this, SLOT( updateLoadingProgress( int ) ) );
113     signalMux_.connect( SIGNAL( loadingFinished( LoadingStatus ) ),
114             this, SLOT( handleLoadingFinished( LoadingStatus ) ) );
115 
116     // Register for checkbox changes
117     signalMux_.connect( SIGNAL( searchRefreshChanged( int ) ),
118             this, SLOT( handleSearchRefreshChanged( int ) ) );
119     signalMux_.connect( SIGNAL( ignoreCaseChanged( int ) ),
120             this, SLOT( handleIgnoreCaseChanged( int ) ) );
121 
122     // Configure the main tabbed widget
123     mainTabWidget_.setDocumentMode( true );
124     mainTabWidget_.setMovable( true );
125     //mainTabWidget_.setTabShape( QTabWidget::Triangular );
126     mainTabWidget_.setTabsClosable( true );
127 
128     connect( &mainTabWidget_, SIGNAL( tabCloseRequested( int ) ),
129             this, SLOT( closeTab( int ) ) );
130     connect( &mainTabWidget_, SIGNAL( currentChanged( int ) ),
131             this, SLOT( currentTabChanged( int ) ) );
132 
133     // Establish the QuickFindWidget and mux ( to send requests from the
134     // QFWidget to the right window )
135     connect( &quickFindWidget_, SIGNAL( patternConfirmed( const QString&, bool ) ),
136              &quickFindMux_, SLOT( confirmPattern( const QString&, bool ) ) );
137     connect( &quickFindWidget_, SIGNAL( patternUpdated( const QString&, bool ) ),
138              &quickFindMux_, SLOT( setNewPattern( const QString&, bool ) ) );
139     connect( &quickFindWidget_, SIGNAL( cancelSearch() ),
140              &quickFindMux_, SLOT( cancelSearch() ) );
141     connect( &quickFindWidget_, SIGNAL( searchForward() ),
142              &quickFindMux_, SLOT( searchForward() ) );
143     connect( &quickFindWidget_, SIGNAL( searchBackward() ),
144              &quickFindMux_, SLOT( searchBackward() ) );
145     connect( &quickFindWidget_, SIGNAL( searchNext() ),
146              &quickFindMux_, SLOT( searchNext() ) );
147 
148     // QuickFind changes coming from the views
149     connect( &quickFindMux_, SIGNAL( patternChanged( const QString& ) ),
150              this, SLOT( changeQFPattern( const QString& ) ) );
151     connect( &quickFindMux_, SIGNAL( notify( const QFNotification& ) ),
152              &quickFindWidget_, SLOT( notify( const QFNotification& ) ) );
153     connect( &quickFindMux_, SIGNAL( clearNotification() ),
154              &quickFindWidget_, SLOT( clearNotification() ) );
155 
156     // Actions from external instances
157     connect( externalCommunicator_.get(), SIGNAL( loadFile( const QString& ) ),
158              this, SLOT( loadFileNonInteractive( const QString& ) ) );
159 
160 #ifdef GLOGG_SUPPORTS_VERSION_CHECKING
161     // Version checker notification
162     connect( &versionChecker_, SIGNAL( newVersionFound( const QString& ) ),
163             this, SLOT( newVersionNotification( const QString& ) ) );
164 #endif
165 
166     // Construct the QuickFind bar
167     quickFindWidget_.hide();
168 
169     QWidget* central_widget = new QWidget();
170     QVBoxLayout* main_layout = new QVBoxLayout();
171     main_layout->setContentsMargins( 0, 0, 0, 0 );
172     main_layout->addWidget( &mainTabWidget_ );
173     main_layout->addWidget( &quickFindWidget_ );
174     central_widget->setLayout( main_layout );
175 
176     setCentralWidget( central_widget );
177 }
178 
179 void MainWindow::reloadGeometry()
180 {
181     QByteArray geometry;
182 
183     session_->storedGeometry( &geometry );
184     restoreGeometry( geometry );
185 }
186 
187 void MainWindow::reloadSession()
188 {
189     int current_file_index = -1;
190 
191     for ( auto open_file: session_->restore(
192                []() { return new CrawlerWidget(); },
193                &current_file_index ) )
194     {
195         QString file_name = { open_file.first.c_str() };
196         CrawlerWidget* crawler_widget = dynamic_cast<CrawlerWidget*>(
197                 open_file.second );
198 
199         assert( crawler_widget );
200 
201         mainTabWidget_.addTab( crawler_widget, strippedName( file_name ) );
202     }
203 
204     if ( current_file_index >= 0 )
205         mainTabWidget_.setCurrentIndex( current_file_index );
206 }
207 
208 void MainWindow::loadInitialFile( QString fileName )
209 {
210     LOG(logDEBUG) << "loadInitialFile";
211 
212     // Is there a file passed as argument?
213     if ( !fileName.isEmpty() )
214         loadFile( fileName );
215 }
216 
217 void MainWindow::startBackgroundTasks()
218 {
219     LOG(logDEBUG) << "startBackgroundTasks";
220 
221 #ifdef GLOGG_SUPPORTS_VERSION_CHECKING
222     versionChecker_.startCheck();
223 #endif
224 }
225 
226 //
227 // Private functions
228 //
229 
230 const MainWindow::EncodingList MainWindow::encoding_list[] = {
231     { "&Auto" },
232     { "ASCII / &ISO-8859-1" },
233     { "&UTF-8" },
234     { "UTF-16LE" },
235     { "UTF-16BE" },
236 };
237 
238 // Menu actions
239 void MainWindow::createActions()
240 {
241     std::shared_ptr<Configuration> config =
242         Persistent<Configuration>( "settings" );
243 
244     openAction = new QAction(tr("&Open..."), this);
245     openAction->setShortcut(QKeySequence::Open);
246     openAction->setIcon( QIcon( ":/images/open14.png" ) );
247     openAction->setStatusTip(tr("Open a file"));
248     connect(openAction, SIGNAL(triggered()), this, SLOT(open()));
249 
250     closeAction = new QAction(tr("&Close"), this);
251     closeAction->setShortcut(tr("Ctrl+W"));
252     closeAction->setStatusTip(tr("Close document"));
253     connect(closeAction, SIGNAL(triggered()), this, SLOT(closeTab()));
254 
255     closeAllAction = new QAction(tr("Close &All"), this);
256     closeAllAction->setStatusTip(tr("Close all documents"));
257     connect(closeAllAction, SIGNAL(triggered()), this, SLOT(closeAll()));
258 
259     // Recent files
260     for (int i = 0; i < MaxRecentFiles; ++i) {
261         recentFileActions[i] = new QAction(this);
262         recentFileActions[i]->setVisible(false);
263         connect(recentFileActions[i], SIGNAL(triggered()),
264                 this, SLOT(openRecentFile()));
265     }
266 
267     exitAction = new QAction(tr("E&xit"), this);
268     exitAction->setShortcut(tr("Ctrl+Q"));
269     exitAction->setStatusTip(tr("Exit the application"));
270     connect( exitAction, SIGNAL(triggered()), this, SLOT(close()) );
271 
272     copyAction = new QAction(tr("&Copy"), this);
273     copyAction->setShortcut(QKeySequence::Copy);
274     copyAction->setStatusTip(tr("Copy the selection"));
275     connect( copyAction, SIGNAL(triggered()), this, SLOT(copy()) );
276 
277     selectAllAction = new QAction(tr("Select &All"), this);
278     selectAllAction->setShortcut(tr("Ctrl+A"));
279     selectAllAction->setStatusTip(tr("Select all the text"));
280     connect( selectAllAction, SIGNAL(triggered()),
281              this, SLOT( selectAll() ) );
282 
283     findAction = new QAction(tr("&Find..."), this);
284     findAction->setShortcut(QKeySequence::Find);
285     findAction->setStatusTip(tr("Find the text"));
286     connect( findAction, SIGNAL(triggered()),
287             this, SLOT( find() ) );
288 
289     overviewVisibleAction = new QAction( tr("Matches &overview"), this );
290     overviewVisibleAction->setCheckable( true );
291     overviewVisibleAction->setChecked( config->isOverviewVisible() );
292     connect( overviewVisibleAction, SIGNAL( toggled( bool ) ),
293             this, SLOT( toggleOverviewVisibility( bool )) );
294 
295     lineNumbersVisibleInMainAction =
296         new QAction( tr("Line &numbers in main view"), this );
297     lineNumbersVisibleInMainAction->setCheckable( true );
298     lineNumbersVisibleInMainAction->setChecked( config->mainLineNumbersVisible() );
299     connect( lineNumbersVisibleInMainAction, SIGNAL( toggled( bool ) ),
300             this, SLOT( toggleMainLineNumbersVisibility( bool )) );
301 
302     lineNumbersVisibleInFilteredAction =
303         new QAction( tr("Line &numbers in filtered view"), this );
304     lineNumbersVisibleInFilteredAction->setCheckable( true );
305     lineNumbersVisibleInFilteredAction->setChecked( config->filteredLineNumbersVisible() );
306     connect( lineNumbersVisibleInFilteredAction, SIGNAL( toggled( bool ) ),
307             this, SLOT( toggleFilteredLineNumbersVisibility( bool )) );
308 
309     followAction = new QAction( tr("&Follow File"), this );
310     followAction->setShortcut(Qt::Key_F);
311     followAction->setCheckable(true);
312     connect( followAction, SIGNAL(toggled( bool )),
313             this, SIGNAL(followSet( bool )) );
314 
315     reloadAction = new QAction( tr("&Reload"), this );
316     reloadAction->setShortcut(QKeySequence::Refresh);
317     reloadAction->setIcon( QIcon(":/images/reload14.png") );
318     signalMux_.connect( reloadAction, SIGNAL(triggered()), SLOT(reload()) );
319 
320     stopAction = new QAction( tr("&Stop"), this );
321     stopAction->setIcon( QIcon(":/images/stop14.png") );
322     stopAction->setEnabled( true );
323     signalMux_.connect( stopAction, SIGNAL(triggered()), SLOT(stopLoading()) );
324 
325     filtersAction = new QAction(tr("&Filters..."), this);
326     filtersAction->setStatusTip(tr("Show the Filters box"));
327     connect( filtersAction, SIGNAL(triggered()), this, SLOT(filters()) );
328 
329     optionsAction = new QAction(tr("&Options..."), this);
330     optionsAction->setStatusTip(tr("Show the Options box"));
331     connect( optionsAction, SIGNAL(triggered()), this, SLOT(options()) );
332 
333     aboutAction = new QAction(tr("&About"), this);
334     aboutAction->setStatusTip(tr("Show the About box"));
335     connect( aboutAction, SIGNAL(triggered()), this, SLOT(about()) );
336 
337     aboutQtAction = new QAction(tr("About &Qt"), this);
338     aboutQtAction->setStatusTip(tr("Show the Qt library's About box"));
339     connect( aboutQtAction, SIGNAL(triggered()), this, SLOT(aboutQt()) );
340 
341     encodingGroup = new QActionGroup( this );
342 
343     for ( int i = 0; i < CrawlerWidget::ENCODING_MAX; ++i ) {
344         encodingAction[i] = new QAction( tr( encoding_list[i].name ), this );
345         encodingAction[i]->setCheckable( true );
346         encodingGroup->addAction( encodingAction[i] );
347     }
348 
349     encodingAction[0]->setStatusTip(tr("Automatically detect the file's encoding"));
350     encodingAction[0]->setChecked( true );
351 
352     connect( encodingGroup, SIGNAL( triggered( QAction* ) ),
353             this, SLOT( encodingChanged( QAction* ) ) );
354 }
355 
356 void MainWindow::createMenus()
357 {
358     fileMenu = menuBar()->addMenu( tr("&File") );
359     fileMenu->addAction( openAction );
360     fileMenu->addAction( closeAction );
361     fileMenu->addAction( closeAllAction );
362     fileMenu->addSeparator();
363     for (int i = 0; i < MaxRecentFiles; ++i) {
364         fileMenu->addAction( recentFileActions[i] );
365         recentFileActionBehaviors[i] =
366             new MenuActionToolTipBehavior(recentFileActions[i], fileMenu, this);
367     }
368     fileMenu->addSeparator();
369     fileMenu->addAction( exitAction );
370 
371     editMenu = menuBar()->addMenu( tr("&Edit") );
372     editMenu->addAction( copyAction );
373     editMenu->addAction( selectAllAction );
374     editMenu->addSeparator();
375     editMenu->addAction( findAction );
376 
377     viewMenu = menuBar()->addMenu( tr("&View") );
378     viewMenu->addAction( overviewVisibleAction );
379     viewMenu->addSeparator();
380     viewMenu->addAction( lineNumbersVisibleInMainAction );
381     viewMenu->addAction( lineNumbersVisibleInFilteredAction );
382     viewMenu->addSeparator();
383     viewMenu->addAction( followAction );
384     viewMenu->addSeparator();
385     viewMenu->addAction( reloadAction );
386 
387     toolsMenu = menuBar()->addMenu( tr("&Tools") );
388     toolsMenu->addAction( filtersAction );
389     toolsMenu->addSeparator();
390     toolsMenu->addAction( optionsAction );
391 
392     encodingMenu = menuBar()->addMenu( tr("En&coding") );
393     encodingMenu->addAction( encodingAction[0] );
394     encodingMenu->addSeparator();
395     for ( int i = 1; i < CrawlerWidget::ENCODING_MAX; ++i ) {
396         encodingMenu->addAction( encodingAction[i] );
397     }
398 
399     menuBar()->addSeparator();
400 
401     helpMenu = menuBar()->addMenu( tr("&Help") );
402     helpMenu->addAction( aboutAction );
403 }
404 
405 void MainWindow::createToolBars()
406 {
407     infoLine = new InfoLine();
408     infoLine->setFrameStyle( QFrame::WinPanel | QFrame::Sunken );
409     infoLine->setLineWidth( 0 );
410 
411     lineNbField = new QLabel( );
412     lineNbField->setText( "Line 0" );
413     lineNbField->setAlignment( Qt::AlignLeft | Qt::AlignVCenter );
414     lineNbField->setMinimumSize(
415             lineNbField->fontMetrics().size( 0, "Line 0000000") );
416 
417     toolBar = addToolBar( tr("&Toolbar") );
418     toolBar->setIconSize( QSize( 14, 14 ) );
419     toolBar->setMovable( false );
420     toolBar->addAction( openAction );
421     toolBar->addAction( reloadAction );
422     toolBar->addWidget( infoLine );
423     toolBar->addAction( stopAction );
424     toolBar->addWidget( lineNbField );
425 }
426 
427 //
428 // Slots
429 //
430 
431 // Opens the file selection dialog to select a new log file
432 void MainWindow::open()
433 {
434     QString defaultDir = ".";
435 
436     // Default to the path of the current file if there is one
437     if ( auto current = currentCrawlerWidget() )
438     {
439         std::string current_file = session_->getFilename( current );
440         QFileInfo fileInfo = QFileInfo( QString( current_file.c_str() ) );
441         defaultDir = fileInfo.path();
442     }
443 
444     QString fileName = QFileDialog::getOpenFileName(this,
445             tr("Open file"), defaultDir, tr("All files (*)"));
446     if (!fileName.isEmpty())
447         loadFile(fileName);
448 }
449 
450 // Opens a log file from the recent files list
451 void MainWindow::openRecentFile()
452 {
453     QAction* action = qobject_cast<QAction*>(sender());
454     if (action)
455         loadFile(action->data().toString());
456 }
457 
458 // Close current tab
459 void MainWindow::closeTab()
460 {
461     int currentIndex = mainTabWidget_.currentIndex();
462 
463     if ( currentIndex >= 0 )
464     {
465         closeTab(currentIndex);
466     }
467 }
468 
469 // Close all tabs
470 void MainWindow::closeAll()
471 {
472     while ( mainTabWidget_.count() )
473     {
474         closeTab(0);
475     }
476 }
477 
478 // Select all the text in the currently selected view
479 void MainWindow::selectAll()
480 {
481     CrawlerWidget* current = currentCrawlerWidget();
482 
483     if ( current )
484         current->selectAll();
485 }
486 
487 // Copy the currently selected line into the clipboard
488 void MainWindow::copy()
489 {
490     static QClipboard* clipboard = QApplication::clipboard();
491     CrawlerWidget* current = currentCrawlerWidget();
492 
493     if ( current ) {
494         clipboard->setText( current->getSelectedText() );
495 
496         // Put it in the global selection as well (X11 only)
497         clipboard->setText( current->getSelectedText(),
498                 QClipboard::Selection );
499     }
500 }
501 
502 // Display the QuickFind bar
503 void MainWindow::find()
504 {
505     displayQuickFindBar( QuickFindMux::Forward );
506 }
507 
508 // Opens the 'Filters' dialog box
509 void MainWindow::filters()
510 {
511     FiltersDialog dialog(this);
512     signalMux_.connect(&dialog, SIGNAL( optionsChanged() ), SLOT( applyConfiguration() ));
513     dialog.exec();
514     signalMux_.disconnect(&dialog, SIGNAL( optionsChanged() ), SLOT( applyConfiguration() ));
515 }
516 
517 // Opens the 'Options' modal dialog box
518 void MainWindow::options()
519 {
520     OptionsDialog dialog(this);
521     signalMux_.connect(&dialog, SIGNAL( optionsChanged() ), SLOT( applyConfiguration() ));
522     dialog.exec();
523     signalMux_.disconnect(&dialog, SIGNAL( optionsChanged() ), SLOT( applyConfiguration() ));
524 }
525 
526 // Opens the 'About' dialog box.
527 void MainWindow::about()
528 {
529     QMessageBox::about(this, tr("About glogg"),
530             tr("<h2>glogg " GLOGG_VERSION "</h2>"
531                 "<p>A fast, advanced log explorer."
532 #ifdef GLOGG_COMMIT
533                 "<p>Built " GLOGG_DATE " from " GLOGG_COMMIT
534 #endif
535                 "<p><a href=\"http://glogg.bonnefon.org/\">http://glogg.bonnefon.org/</a></p>"
536                 "<p>Copyright &copy; 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Nicolas Bonnefon and other contributors"
537                 "<p>You may modify and redistribute the program under the terms of the GPL (version 3 or later)." ) );
538 }
539 
540 // Opens the 'About Qt' dialog box.
541 void MainWindow::aboutQt()
542 {
543 }
544 
545 void MainWindow::encodingChanged( QAction* action )
546 {
547     int i = 0;
548     for ( i = 0; i < CrawlerWidget::ENCODING_MAX; ++i )
549         if ( action == encodingAction[i] )
550             break;
551 
552     LOG(logDEBUG) << "encodingChanged, encoding " << i;
553     currentCrawlerWidget()->setEncoding( static_cast<CrawlerWidget::Encoding>( i ) );
554     updateInfoLine();
555 }
556 
557 void MainWindow::toggleOverviewVisibility( bool isVisible )
558 {
559     std::shared_ptr<Configuration> config =
560         Persistent<Configuration>( "settings" );
561     config->setOverviewVisible( isVisible );
562     emit optionsChanged();
563 }
564 
565 void MainWindow::toggleMainLineNumbersVisibility( bool isVisible )
566 {
567     std::shared_ptr<Configuration> config =
568         Persistent<Configuration>( "settings" );
569     config->setMainLineNumbersVisible( isVisible );
570     emit optionsChanged();
571 }
572 
573 void MainWindow::toggleFilteredLineNumbersVisibility( bool isVisible )
574 {
575     std::shared_ptr<Configuration> config =
576         Persistent<Configuration>( "settings" );
577     config->setFilteredLineNumbersVisible( isVisible );
578     emit optionsChanged();
579 }
580 
581 void MainWindow::changeFollowMode( bool follow )
582 {
583     followAction->setChecked( follow );
584 }
585 
586 void MainWindow::lineNumberHandler( int line )
587 {
588     // The line number received is the internal (starts at 0)
589     lineNbField->setText( tr( "Line %1" ).arg( line + 1 ) );
590 }
591 
592 void MainWindow::updateLoadingProgress( int progress )
593 {
594     LOG(logDEBUG) << "Loading progress: " << progress;
595 
596     QString current_file =
597         session_->getFilename( currentCrawlerWidget() ).c_str();
598 
599     // We ignore 0% and 100% to avoid a flash when the file (or update)
600     // is very short.
601     if ( progress > 0 && progress < 100 ) {
602         infoLine->setText( current_file +
603                 tr( " - Indexing lines... (%1 %)" ).arg( progress ) );
604         infoLine->displayGauge( progress );
605 
606         stopAction->setEnabled( true );
607         reloadAction->setEnabled( false );
608     }
609 }
610 
611 void MainWindow::handleLoadingFinished( LoadingStatus status )
612 {
613     LOG(logDEBUG) << "handleLoadingFinished success=" <<
614         ( status == LoadingStatus::Successful );
615 
616     // No file is loading
617     loadingFileName.clear();
618 
619     if ( status == LoadingStatus::Successful )
620     {
621         updateInfoLine();
622 
623         infoLine->hideGauge();
624         stopAction->setEnabled( false );
625         reloadAction->setEnabled( true );
626 
627         // Now everything is ready, we can finally show the file!
628         currentCrawlerWidget()->show();
629     }
630     else
631     {
632         if ( status == LoadingStatus::NoMemory )
633         {
634             QMessageBox alertBox;
635             alertBox.setText( "Not enough memory." );
636             alertBox.setInformativeText( "The system does not have enough \
637 memory to hold the index for this file. The file will now be closed." );
638             alertBox.setIcon( QMessageBox::Critical );
639             alertBox.exec();
640         }
641 
642         closeTab( mainTabWidget_.currentIndex()  );
643     }
644 
645     // mainTabWidget_.setEnabled( true );
646 }
647 
648 void MainWindow::handleSearchRefreshChanged( int state )
649 {
650     auto config = Persistent<Configuration>( "settings" );
651     config->setSearchAutoRefreshDefault( state == Qt::Checked );
652 }
653 
654 void MainWindow::handleIgnoreCaseChanged( int state )
655 {
656     auto config = Persistent<Configuration>( "settings" );
657     config->setSearchIgnoreCaseDefault( state == Qt::Checked );
658 }
659 
660 void MainWindow::closeTab( int index )
661 {
662     auto widget = dynamic_cast<CrawlerWidget*>(
663             mainTabWidget_.widget( index ) );
664 
665     assert( widget );
666 
667     widget->stopLoading();
668     mainTabWidget_.removeTab( index );
669     session_->close( widget );
670     delete widget;
671 }
672 
673 void MainWindow::currentTabChanged( int index )
674 {
675     LOG(logDEBUG) << "currentTabChanged";
676 
677     if ( index >= 0 )
678     {
679         CrawlerWidget* crawler_widget = dynamic_cast<CrawlerWidget*>(
680                 mainTabWidget_.widget( index ) );
681         signalMux_.setCurrentDocument( crawler_widget );
682         quickFindMux_.registerSelector( crawler_widget );
683 
684         // New tab is set up with fonts etc...
685         emit optionsChanged();
686 
687         // Update the menu bar
688         updateMenuBarFromDocument( crawler_widget );
689 
690         // Update the title bar
691         updateTitleBar( QString(
692                     session_->getFilename( crawler_widget ).c_str() ) );
693     }
694     else
695     {
696         // No tab left
697         signalMux_.setCurrentDocument( nullptr );
698         quickFindMux_.registerSelector( nullptr );
699 
700         infoLine->hideGauge();
701         infoLine->clear();
702 
703         updateTitleBar( QString() );
704     }
705 }
706 
707 void MainWindow::changeQFPattern( const QString& newPattern )
708 {
709     quickFindWidget_.changeDisplayedPattern( newPattern );
710 }
711 
712 void MainWindow::loadFileNonInteractive( const QString& file_name )
713 {
714     LOG(logDEBUG) << "loadFileNonInteractive( "
715         << file_name.toStdString() << " )";
716 
717     loadFile( file_name );
718 
719     // Try to get the window to the front
720     // This is a bit of a hack but has been tested on:
721     // Qt 5.3 / Gnome / Linux
722     // Qt 4.8 / Win7
723 #ifdef _WIN32
724     // Hack copied from http://qt-project.org/forums/viewthread/6164
725     ::SetWindowPos((HWND) effectiveWinId(), HWND_TOPMOST,
726             0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
727     ::SetWindowPos((HWND) effectiveWinId(), HWND_NOTOPMOST,
728             0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
729 #else
730     Qt::WindowFlags window_flags = windowFlags();
731     window_flags |= Qt::WindowStaysOnTopHint;
732     setWindowFlags( window_flags );
733 #endif
734 
735     activateWindow();
736     raise();
737 
738 #ifndef _WIN32
739     window_flags = windowFlags();
740     window_flags &= ~Qt::WindowStaysOnTopHint;
741     setWindowFlags( window_flags );
742 #endif
743 
744     showNormal();
745 }
746 
747 void MainWindow::newVersionNotification( const QString& new_version )
748 {
749     LOG(logDEBUG) << "newVersionNotification( " <<
750         new_version.toStdString() << " )";
751 
752     QMessageBox msgBox;
753     msgBox.setText( QString( "A new version of glogg (%1) is available for download <p>"
754                 "<a href=\"http://glogg.bonnefon.org/download.html\">http://glogg.bonnefon.org/download.html</a>"
755                 ).arg( new_version ) );
756     msgBox.exec();
757 }
758 
759 //
760 // Events
761 //
762 
763 // Closes the application
764 void MainWindow::closeEvent( QCloseEvent *event )
765 {
766     writeSettings();
767     event->accept();
768 }
769 
770 // Accepts the drag event if it looks like a filename
771 void MainWindow::dragEnterEvent( QDragEnterEvent* event )
772 {
773     if ( event->mimeData()->hasFormat( "text/uri-list" ) )
774         event->acceptProposedAction();
775 }
776 
777 // Tries and loads the file if the URL dropped is local
778 void MainWindow::dropEvent( QDropEvent* event )
779 {
780     QList<QUrl> urls = event->mimeData()->urls();
781     if ( urls.isEmpty() )
782         return;
783 
784     QString fileName = urls.first().toLocalFile();
785     if ( fileName.isEmpty() )
786         return;
787 
788     loadFile( fileName );
789 }
790 
791 void MainWindow::keyPressEvent( QKeyEvent* keyEvent )
792 {
793     LOG(logDEBUG4) << "keyPressEvent received";
794 
795     switch ( (keyEvent->text())[0].toLatin1() ) {
796         case '/':
797             displayQuickFindBar( QuickFindMux::Forward );
798             break;
799         case '?':
800             displayQuickFindBar( QuickFindMux::Backward );
801             break;
802         default:
803             keyEvent->ignore();
804     }
805 
806     if ( !keyEvent->isAccepted() )
807         QMainWindow::keyPressEvent( keyEvent );
808 }
809 
810 //
811 // Private functions
812 //
813 
814 // Create a CrawlerWidget for the passed file, start its loading
815 // and update the title bar.
816 // The loading is done asynchronously.
817 bool MainWindow::loadFile( const QString& fileName )
818 {
819     LOG(logDEBUG) << "loadFile ( " << fileName.toStdString() << " )";
820 
821     // First check if the file is already open...
822     CrawlerWidget* existing_crawler = dynamic_cast<CrawlerWidget*>(
823             session_->getViewIfOpen( fileName.toStdString() ) );
824     if ( existing_crawler ) {
825         // ... and switch to it.
826         mainTabWidget_.setCurrentWidget( existing_crawler );
827 
828         return true;
829     }
830 
831     // Load the file
832     loadingFileName = fileName;
833 
834     try {
835         CrawlerWidget* crawler_widget = dynamic_cast<CrawlerWidget*>(
836                 session_->open( fileName.toStdString(),
837                     []() { return new CrawlerWidget(); } ) );
838         assert( crawler_widget );
839 
840         // We won't show the widget until the file is fully loaded
841         crawler_widget->hide();
842 
843         // We disable the tab widget to avoid having someone switch
844         // tab during loading. (maybe FIXME)
845         //mainTabWidget_.setEnabled( false );
846 
847         int index = mainTabWidget_.addTab(
848                 crawler_widget, strippedName( fileName ) );
849 
850         // Setting the new tab, the user will see a blank page for the duration
851         // of the loading, with no way to switch to another tab
852         mainTabWidget_.setCurrentIndex( index );
853 
854         // Update the recent files list
855         // (reload the list first in case another glogg changed it)
856         GetPersistentInfo().retrieve( "recentFiles" );
857         recentFiles_->addRecent( fileName );
858         GetPersistentInfo().save( "recentFiles" );
859         updateRecentFileActions();
860     }
861     catch ( FileUnreadableErr ) {
862         LOG(logDEBUG) << "Can't open file " << fileName.toStdString();
863         return false;
864     }
865 
866     LOG(logDEBUG) << "Success loading file " << fileName.toStdString();
867     return true;
868 
869 }
870 
871 // Strips the passed filename from its directory part.
872 QString MainWindow::strippedName( const QString& fullFileName ) const
873 {
874     return QFileInfo( fullFileName ).fileName();
875 }
876 
877 // Return the currently active CrawlerWidget, or NULL if none
878 CrawlerWidget* MainWindow::currentCrawlerWidget() const
879 {
880     auto current = dynamic_cast<CrawlerWidget*>(
881             mainTabWidget_.currentWidget() );
882 
883     return current;
884 }
885 
886 // Update the title bar.
887 void MainWindow::updateTitleBar( const QString& file_name )
888 {
889     QString shownName = tr( "Untitled" );
890     if ( !file_name.isEmpty() )
891         shownName = strippedName( file_name );
892 
893     setWindowTitle(
894             tr("%1 - %2").arg(shownName).arg(tr("glogg"))
895 #ifdef GLOGG_COMMIT
896             + " (dev build " GLOGG_VERSION ")"
897 #endif
898             );
899 }
900 
901 // Updates the actions for the recent files.
902 // Must be called after having added a new name to the list.
903 void MainWindow::updateRecentFileActions()
904 {
905     QStringList recent_files = recentFiles_->recentFiles();
906 
907     for ( int j = 0; j < MaxRecentFiles; ++j ) {
908         if ( j < recent_files.count() ) {
909             QString text = tr("&%1 %2").arg(j + 1).arg(strippedName(recent_files[j]));
910             recentFileActions[j]->setText( text );
911             recentFileActions[j]->setToolTip( recent_files[j] );
912             recentFileActions[j]->setData( recent_files[j] );
913             recentFileActions[j]->setVisible( true );
914         }
915         else {
916             recentFileActions[j]->setVisible( false );
917         }
918     }
919 
920     // separatorAction->setVisible(!recentFiles.isEmpty());
921 }
922 
923 // Update our menu bar to match the settings of the crawler
924 // (used when the tab is changed)
925 void MainWindow::updateMenuBarFromDocument( const CrawlerWidget* crawler )
926 {
927     auto encoding = crawler->encodingSetting();
928     encodingAction[static_cast<int>( encoding )]->setChecked( true );
929     bool follow = crawler->isFollowEnabled();
930     followAction->setChecked( follow );
931 }
932 
933 // Update the top info line from the session
934 void MainWindow::updateInfoLine()
935 {
936     QLocale defaultLocale;
937 
938     // Following should always work as we will only receive enter
939     // this slot if there is a crawler connected.
940     QString current_file =
941         session_->getFilename( currentCrawlerWidget() ).c_str();
942 
943     uint64_t fileSize;
944     uint32_t fileNbLine;
945     QDateTime lastModified;
946 
947     session_->getFileInfo( currentCrawlerWidget(),
948             &fileSize, &fileNbLine, &lastModified );
949     if ( lastModified.isValid() ) {
950         const QString date =
951             defaultLocale.toString( lastModified, QLocale::NarrowFormat );
952         infoLine->setText( tr( "%1 (%2 - %3 lines - modified on %4 - %5)" )
953                 .arg(current_file).arg(readableSize(fileSize))
954                 .arg(fileNbLine).arg( date )
955                 .arg(currentCrawlerWidget()->encodingText()) );
956     }
957     else {
958         infoLine->setText( tr( "%1 (%2 - %3 lines - %4)" )
959                 .arg(current_file).arg(readableSize(fileSize))
960                 .arg(fileNbLine)
961                 .arg(currentCrawlerWidget()->encodingText()) );
962     }
963 }
964 
965 // Write settings to permanent storage
966 void MainWindow::writeSettings()
967 {
968     // Save the session
969     // Generate the ordered list of widgets and their topLine
970     std::vector<
971             std::tuple<const ViewInterface*, uint64_t, std::shared_ptr<const ViewContextInterface>>
972         > widget_list;
973     for ( int i = 0; i < mainTabWidget_.count(); ++i )
974     {
975         auto view = dynamic_cast<const ViewInterface*>( mainTabWidget_.widget( i ) );
976         widget_list.push_back( std::make_tuple(
977                 view,
978                 0UL,
979                 view->context() ) );
980     }
981     session_->save( widget_list, saveGeometry() );
982 
983     // User settings
984     GetPersistentInfo().save( QString( "settings" ) );
985 }
986 
987 // Read settings from permanent storage
988 void MainWindow::readSettings()
989 {
990     // Get and restore the session
991     // GetPersistentInfo().retrieve( QString( "session" ) );
992     // SessionInfo session = Persistent<SessionInfo>( "session" );
993     /*
994      * FIXME: should be in the session
995     crawlerWidget->restoreState( session.crawlerState() );
996     */
997 
998     // History of recent files
999     GetPersistentInfo().retrieve( QString( "recentFiles" ) );
1000     updateRecentFileActions();
1001 
1002     // GetPersistentInfo().retrieve( QString( "settings" ) );
1003     GetPersistentInfo().retrieve( QString( "filterSet" ) );
1004 }
1005 
1006 void MainWindow::displayQuickFindBar( QuickFindMux::QFDirection direction )
1007 {
1008     LOG(logDEBUG) << "MainWindow::displayQuickFindBar";
1009 
1010     // Warn crawlers so they can save the position of the focus in order
1011     // to do incremental search in the right view.
1012     emit enteringQuickFind();
1013 
1014     quickFindMux_.setDirection( direction );
1015     quickFindWidget_.userActivate();
1016 }
1017 
1018 // Returns the size in human readable format
1019 static QString readableSize( qint64 size )
1020 {
1021     static const QString sizeStrs[] = {
1022         QObject::tr("B"), QObject::tr("KiB"), QObject::tr("MiB"),
1023         QObject::tr("GiB"), QObject::tr("TiB") };
1024 
1025     QLocale defaultLocale;
1026     unsigned int i;
1027     double humanSize = size;
1028 
1029     for ( i=0; i+1 < (sizeof(sizeStrs)/sizeof(QString)) && (humanSize/1024.0) >= 1024.0; i++ )
1030         humanSize /= 1024.0;
1031 
1032     if ( humanSize >= 1024.0 ) {
1033         humanSize /= 1024.0;
1034         i++;
1035     }
1036 
1037     QString output;
1038     if ( i == 0 )
1039         // No decimal part if we display straight bytes.
1040         output = defaultLocale.toString( (int) humanSize );
1041     else
1042         output = defaultLocale.toString( humanSize, 'f', 1 );
1043 
1044     output += QString(" ") + sizeStrs[i];
1045 
1046     return output;
1047 }
1048