xref: /glogg/src/mainwindow.cpp (revision d96f3f211e1e2a3a5b219145986c18e10b0f96aa)
1 /*
2  * Copyright (C) 2009, 2010, 2011, 2013 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 
26 #include <QAction>
27 #include <QDesktopWidget>
28 #include <QMenuBar>
29 #include <QToolBar>
30 #include <QFileInfo>
31 #include <QFileDialog>
32 #include <QClipboard>
33 #include <QMessageBox>
34 #include <QCloseEvent>
35 #include <QDragEnterEvent>
36 #include <QMimeData>
37 #include <QUrl>
38 
39 #include "log.h"
40 
41 #include "mainwindow.h"
42 
43 #include "sessioninfo.h"
44 #include "recentfiles.h"
45 #include "crawlerwidget.h"
46 #include "filtersdialog.h"
47 #include "optionsdialog.h"
48 #include "persistentinfo.h"
49 #include "savedsearches.h"
50 #include "menuactiontooltipbehavior.h"
51 
52 MainWindow::MainWindow( std::unique_ptr<Session> session ) :
53     session_( std::move( session )  ),
54     recentFiles( Persistent<RecentFiles>( "recentFiles" ) ),
55     mainIcon_()
56 {
57     createActions();
58     createMenus();
59     // createContextMenu();
60     createToolBars();
61     // createStatusBar();
62     createCrawler();
63 
64     setAcceptDrops( true );
65 
66     // Default geometry
67     const QRect geometry = QApplication::desktop()->availableGeometry( this );
68     setGeometry( geometry.x() + 20, geometry.y() + 40,
69             geometry.width() - 140, geometry.height() - 140 );
70 
71     // Send actions to the crawlerwidget
72     connect( this, SIGNAL( followSet( bool ) ),
73             crawlerWidget, SIGNAL( followSet( bool ) ) );
74     connect( this, SIGNAL( optionsChanged() ),
75             crawlerWidget, SLOT( applyConfiguration() ) );
76 
77     // Actions from the CrawlerWidget
78     connect( crawlerWidget, SIGNAL( followDisabled() ),
79             this, SLOT( disableFollow() ) );
80     connect( crawlerWidget, SIGNAL( updateLineNumber( int ) ),
81             this, SLOT( lineNumberHandler( int ) ) );
82 
83     readSettings();
84     emit optionsChanged();
85 
86     // We start with the empty file
87     setCurrentFile( "" );
88 
89     mainIcon_.addFile( ":/images/hicolor/16x16/glogg.png" );
90     mainIcon_.addFile( ":/images/hicolor/24x24/glogg.png" );
91     mainIcon_.addFile( ":/images/hicolor/32x32/glogg.png" );
92     mainIcon_.addFile( ":/images/hicolor/48x48/glogg.png" );
93 
94     // Register for progress status bar
95     connect( crawlerWidget, SIGNAL( loadingProgressed( int ) ),
96             this, SLOT( updateLoadingProgress( int ) ) );
97     connect( crawlerWidget, SIGNAL( loadingFinished( bool ) ),
98             this, SLOT( displayNormalStatus( bool ) ) );
99 
100     setWindowIcon( mainIcon_ );
101     setCentralWidget(crawlerWidget);
102 }
103 
104 void MainWindow::loadInitialFile( QString fileName )
105 {
106     LOG(logDEBUG) << "loadInitialFile";
107 
108     // Is there a file passed as argument?
109     if ( !fileName.isEmpty() )
110         loadFile( fileName );
111     else if ( !previousFile.isEmpty() )
112         loadFile( previousFile );
113 }
114 
115 //
116 // Private functions
117 //
118 
119 void MainWindow::createCrawler()
120 {
121     // First get the global search history
122     savedSearches = &(Persistent<SavedSearches>( "savedSearches" ));
123 
124     crawlerWidget = new CrawlerWidget( savedSearches );
125 }
126 
127 // Menu actions
128 void MainWindow::createActions()
129 {
130     Configuration& config = Persistent<Configuration>( "settings" );
131 
132     openAction = new QAction(tr("&Open..."), this);
133     openAction->setShortcut(QKeySequence::Open);
134     openAction->setIcon( QIcon(":/images/open16.png") );
135     openAction->setStatusTip(tr("Open a file"));
136     connect(openAction, SIGNAL(triggered()), this, SLOT(open()));
137 
138     // Recent files
139     for (int i = 0; i < MaxRecentFiles; ++i) {
140         recentFileActions[i] = new QAction(this);
141         recentFileActions[i]->setVisible(false);
142         connect(recentFileActions[i], SIGNAL(triggered()),
143                 this, SLOT(openRecentFile()));
144     }
145 
146     exitAction = new QAction(tr("E&xit"), this);
147     exitAction->setShortcut(tr("Ctrl+Q"));
148     exitAction->setStatusTip(tr("Exit the application"));
149     connect( exitAction, SIGNAL(triggered()), this, SLOT(close()) );
150 
151     copyAction = new QAction(tr("&Copy"), this);
152     copyAction->setShortcut(QKeySequence::Copy);
153     copyAction->setStatusTip(tr("Copy the selection"));
154     connect( copyAction, SIGNAL(triggered()), this, SLOT(copy()) );
155 
156     selectAllAction = new QAction(tr("Select &All"), this);
157     selectAllAction->setShortcut(tr("Ctrl+A"));
158     selectAllAction->setStatusTip(tr("Select all the text"));
159     connect( selectAllAction, SIGNAL(triggered()),
160              this, SLOT( selectAll() ) );
161 
162     findAction = new QAction(tr("&Find..."), this);
163     findAction->setShortcut(QKeySequence::Find);
164     findAction->setStatusTip(tr("Find the text"));
165     connect( findAction, SIGNAL(triggered()),
166             this, SLOT( find() ) );
167 
168     overviewVisibleAction = new QAction( tr("Matches &overview"), this );
169     overviewVisibleAction->setCheckable( true );
170     overviewVisibleAction->setChecked( config.isOverviewVisible() );
171     connect( overviewVisibleAction, SIGNAL( toggled( bool ) ),
172             this, SLOT( toggleOverviewVisibility( bool )) );
173 
174     lineNumbersVisibleInMainAction =
175         new QAction( tr("Line &numbers in main view"), this );
176     lineNumbersVisibleInMainAction->setCheckable( true );
177     lineNumbersVisibleInMainAction->setChecked( config.mainLineNumbersVisible() );
178     connect( lineNumbersVisibleInMainAction, SIGNAL( toggled( bool ) ),
179             this, SLOT( toggleMainLineNumbersVisibility( bool )) );
180 
181     lineNumbersVisibleInFilteredAction =
182         new QAction( tr("Line &numbers in filtered view"), this );
183     lineNumbersVisibleInFilteredAction->setCheckable( true );
184     lineNumbersVisibleInFilteredAction->setChecked( config.filteredLineNumbersVisible() );
185     connect( lineNumbersVisibleInFilteredAction, SIGNAL( toggled( bool ) ),
186             this, SLOT( toggleFilteredLineNumbersVisibility( bool )) );
187 
188     followAction = new QAction( tr("&Follow File"), this );
189     followAction->setShortcut(Qt::Key_F);
190     followAction->setCheckable(true);
191     connect( followAction, SIGNAL(toggled( bool )),
192             this, SIGNAL(followSet( bool )) );
193 
194     reloadAction = new QAction( tr("&Reload"), this );
195     reloadAction->setShortcut(QKeySequence::Refresh);
196     reloadAction->setIcon( QIcon(":/images/reload16.png") );
197     connect( reloadAction, SIGNAL(triggered()), this, SLOT(reload()) );
198 
199     stopAction = new QAction( tr("&Stop"), this );
200     stopAction->setIcon( QIcon(":/images/stop16.png") );
201     stopAction->setEnabled( false );
202     connect( stopAction, SIGNAL(triggered()), this, SLOT(stop()) );
203 
204     filtersAction = new QAction(tr("&Filters..."), this);
205     filtersAction->setStatusTip(tr("Show the Filters box"));
206     connect( filtersAction, SIGNAL(triggered()), this, SLOT(filters()) );
207 
208     optionsAction = new QAction(tr("&Options..."), this);
209     optionsAction->setStatusTip(tr("Show the Options box"));
210     connect( optionsAction, SIGNAL(triggered()), this, SLOT(options()) );
211 
212     aboutAction = new QAction(tr("&About"), this);
213     aboutAction->setStatusTip(tr("Show the About box"));
214     connect( aboutAction, SIGNAL(triggered()), this, SLOT(about()) );
215 
216     aboutQtAction = new QAction(tr("About &Qt"), this);
217     aboutAction->setStatusTip(tr("Show the Qt library's About box"));
218     connect( aboutQtAction, SIGNAL(triggered()), this, SLOT(aboutQt()) );
219 }
220 
221 void MainWindow::createMenus()
222 {
223     fileMenu = menuBar()->addMenu( tr("&File") );
224     fileMenu->addAction( openAction );
225     fileMenu->addSeparator();
226     for (int i = 0; i < MaxRecentFiles; ++i) {
227         fileMenu->addAction( recentFileActions[i] );
228         recentFileActionBehaviors[i] =
229             new MenuActionToolTipBehavior(recentFileActions[i], fileMenu, this);
230     }
231     fileMenu->addSeparator();
232     fileMenu->addAction( exitAction );
233 
234     editMenu = menuBar()->addMenu( tr("&Edit") );
235     editMenu->addAction( copyAction );
236     editMenu->addAction( selectAllAction );
237     editMenu->addSeparator();
238     editMenu->addAction( findAction );
239 
240     viewMenu = menuBar()->addMenu( tr("&View") );
241     viewMenu->addAction( overviewVisibleAction );
242     viewMenu->addSeparator();
243     viewMenu->addAction( lineNumbersVisibleInMainAction );
244     viewMenu->addAction( lineNumbersVisibleInFilteredAction );
245     viewMenu->addSeparator();
246     viewMenu->addAction( followAction );
247     viewMenu->addSeparator();
248     viewMenu->addAction( reloadAction );
249 
250     toolsMenu = menuBar()->addMenu( tr("&Tools") );
251     toolsMenu->addAction( filtersAction );
252     toolsMenu->addSeparator();
253     toolsMenu->addAction( optionsAction );
254 
255     menuBar()->addSeparator();
256 
257     helpMenu = menuBar()->addMenu( tr("&Help") );
258     helpMenu->addAction( aboutAction );
259 }
260 
261 void MainWindow::createToolBars()
262 {
263     infoLine = new InfoLine();
264     infoLine->setFrameStyle( QFrame::WinPanel | QFrame::Sunken );
265     infoLine->setLineWidth( 0 );
266 
267     lineNbField = new QLabel( );
268     lineNbField->setText( "Line 0" );
269     lineNbField->setAlignment( Qt::AlignLeft | Qt::AlignVCenter );
270     lineNbField->setMinimumSize(
271             lineNbField->fontMetrics().size( 0, "Line 0000000") );
272 
273     toolBar = addToolBar( tr("&Toolbar") );
274     toolBar->setIconSize( QSize( 16, 16 ) );
275     toolBar->setMovable( false );
276     toolBar->addAction( openAction );
277     toolBar->addAction( reloadAction );
278     toolBar->addWidget( infoLine );
279     toolBar->addAction( stopAction );
280     toolBar->addWidget( lineNbField );
281 }
282 
283 //
284 // Slots
285 //
286 
287 // Opens the file selection dialog to select a new log file
288 void MainWindow::open()
289 {
290     QString defaultDir = ".";
291 
292     // Default to the path of the current file if there is one
293     if ( !currentFile.isEmpty() ) {
294         QFileInfo fileInfo = QFileInfo( currentFile );
295         defaultDir = fileInfo.path();
296     }
297 
298     QString fileName = QFileDialog::getOpenFileName(this,
299             tr("Open file"), defaultDir, tr("All files (*)"));
300     if (!fileName.isEmpty())
301         loadFile(fileName);
302 }
303 
304 // Opens a log file from the recent files list
305 void MainWindow::openRecentFile()
306 {
307     QAction* action = qobject_cast<QAction*>(sender());
308     if (action)
309         loadFile(action->data().toString());
310 }
311 
312 // Select all the text in the currently selected view
313 void MainWindow::selectAll()
314 {
315     crawlerWidget->selectAll();
316 }
317 
318 // Copy the currently selected line into the clipboard
319 void MainWindow::copy()
320 {
321     static QClipboard* clipboard = QApplication::clipboard();
322 
323     clipboard->setText( crawlerWidget->getSelectedText() );
324 
325     // Put it in the global selection as well (X11 only)
326     clipboard->setText( crawlerWidget->getSelectedText(),
327             QClipboard::Selection );
328 }
329 
330 // Display the QuickFind bar
331 void MainWindow::find()
332 {
333     crawlerWidget->displayQuickFindBar( QuickFindMux::Forward );
334 }
335 
336 // Reload the current log file
337 void MainWindow::reload()
338 {
339     if ( !currentFile.isEmpty() )
340         loadFile( currentFile );
341 }
342 
343 // Stop the loading operation
344 void MainWindow::stop()
345 {
346     crawlerWidget->stopLoading();
347 }
348 
349 // Opens the 'Filters' dialog box
350 void MainWindow::filters()
351 {
352     FiltersDialog dialog(this);
353     connect(&dialog, SIGNAL( optionsChanged() ), crawlerWidget, SLOT( applyConfiguration() ));
354     dialog.exec();
355 }
356 
357 // Opens the 'Options' modal dialog box
358 void MainWindow::options()
359 {
360     OptionsDialog dialog(this);
361     connect(&dialog, SIGNAL( optionsChanged() ), crawlerWidget, SLOT( applyConfiguration() ));
362     dialog.exec();
363 }
364 
365 // Opens the 'About' dialog box.
366 void MainWindow::about()
367 {
368     QMessageBox::about(this, tr("About glogg"),
369             tr("<h2>glogg " GLOGG_VERSION "</h2>"
370                 "<p>A fast, advanced log explorer."
371 #ifdef GLOGG_COMMIT
372                 "<p>Built " GLOGG_DATE " from " GLOGG_COMMIT
373 #endif
374                 "<p>Copyright &copy; 2009, 2010, 2011, 2012, 2013 Nicolas Bonnefon and other contributors"
375                 "<p>You may modify and redistribute the program under the terms of the GPL (version 3 or later)." ) );
376 }
377 
378 // Opens the 'About Qt' dialog box.
379 void MainWindow::aboutQt()
380 {
381 }
382 
383 void MainWindow::toggleOverviewVisibility( bool isVisible )
384 {
385     Configuration& config = Persistent<Configuration>( "settings" );
386     config.setOverviewVisible( isVisible );
387     emit optionsChanged();
388 }
389 
390 void MainWindow::toggleMainLineNumbersVisibility( bool isVisible )
391 {
392     Configuration& config = Persistent<Configuration>( "settings" );
393     config.setMainLineNumbersVisible( isVisible );
394     emit optionsChanged();
395 }
396 
397 void MainWindow::toggleFilteredLineNumbersVisibility( bool isVisible )
398 {
399     Configuration& config = Persistent<Configuration>( "settings" );
400     config.setFilteredLineNumbersVisible( isVisible );
401     emit optionsChanged();
402 }
403 
404 void MainWindow::disableFollow()
405 {
406     followAction->setChecked( false );
407 }
408 
409 void MainWindow::lineNumberHandler( int line )
410 {
411     // The line number received is the internal (starts at 0)
412     lineNbField->setText( tr( "Line %1" ).arg( line + 1 ) );
413 }
414 
415 void MainWindow::updateLoadingProgress( int progress )
416 {
417     LOG(logDEBUG) << "Loading progress: " << progress;
418 
419     // We ignore 0% and 100% to avoid a flash when the file (or update)
420     // is very short.
421     if ( progress > 0 && progress < 100 ) {
422         infoLine->setText( loadingFileName + tr( " - Indexing lines... (%1 %)" ).arg( progress ) );
423         infoLine->displayGauge( progress );
424 
425         stopAction->setEnabled( true );
426     }
427 }
428 
429 void MainWindow::displayNormalStatus( bool success )
430 {
431     QLocale defaultLocale;
432 
433     LOG(logDEBUG) << "displayNormalStatus";
434 
435     if ( success )
436         setCurrentFile( loadingFileName );
437 
438     qint64 fileSize;
439     int fileNbLine;
440     QDateTime lastModified;
441 
442     crawlerWidget->getFileInfo( &fileSize, &fileNbLine, &lastModified );
443     if ( lastModified.isValid() ) {
444         const QString date =
445             defaultLocale.toString( lastModified, QLocale::NarrowFormat );
446         infoLine->setText( tr( "%1 (%2 - %3 lines - modified on %4)" )
447                 .arg(currentFile).arg(readableSize(fileSize))
448                 .arg(fileNbLine).arg( date ) );
449     }
450     else {
451         infoLine->setText( tr( "%1 (%2 - %3 lines)" )
452                 .arg(currentFile).arg(readableSize(fileSize))
453                 .arg(fileNbLine) );
454     }
455 
456     infoLine->hideGauge();
457     stopAction->setEnabled( false );
458 }
459 
460 //
461 // Events
462 //
463 
464 // Closes the application
465 void MainWindow::closeEvent( QCloseEvent *event )
466 {
467     writeSettings();
468     event->accept();
469 }
470 
471 // Accepts the drag event if it looks like a filename
472 void MainWindow::dragEnterEvent( QDragEnterEvent* event )
473 {
474     if ( event->mimeData()->hasFormat( "text/uri-list" ) )
475         event->acceptProposedAction();
476 }
477 
478 // Tries and loads the file if the URL dropped is local
479 void MainWindow::dropEvent( QDropEvent* event )
480 {
481     QList<QUrl> urls = event->mimeData()->urls();
482     if ( urls.isEmpty() )
483         return;
484 
485     QString fileName = urls.first().toLocalFile();
486     if ( fileName.isEmpty() )
487         return;
488 
489     loadFile( fileName );
490 }
491 
492 //
493 // Private functions
494 //
495 
496 // Loads the passed file into the CrawlerWidget and update the title bar.
497 // The loading is done asynchronously.
498 bool MainWindow::loadFile( const QString& fileName )
499 {
500     LOG(logDEBUG) << "loadFile ( " << fileName.toStdString() << " )";
501 
502     int topLine = 0;
503 
504     // If we're loading the same file, put the same line on top.
505     if ( fileName == currentFile )
506         topLine = crawlerWidget->getTopLine();
507 
508     // Load the file
509     loadingFileName = fileName;
510     if ( crawlerWidget->readFile( fileName, topLine ) ) {
511         LOG(logDEBUG) << "Success loading file " << fileName.toStdString();
512         return true;
513     }
514     else {
515         LOG(logWARNING) << "Cannot load file " << fileName.toStdString();
516         displayNormalStatus( false );
517         return false;
518     }
519 }
520 
521 // Strips the passed filename from its directory part.
522 QString MainWindow::strippedName( const QString& fullFileName ) const
523 {
524     return QFileInfo( fullFileName ).fileName();
525 }
526 
527 // Add the filename to the recent files list and update the title bar.
528 void MainWindow::setCurrentFile( const QString& fileName )
529 {
530     // Change the current file
531     currentFile = fileName;
532     QString shownName = tr( "Untitled" );
533     if ( !currentFile.isEmpty() ) {
534         // (reload the list first in case another glogg changed it)
535         GetPersistentInfo().retrieve( "recentFiles" );
536         recentFiles.addRecent( currentFile );
537         GetPersistentInfo().save( "recentFiles" );
538         updateRecentFileActions();
539         shownName = strippedName( currentFile );
540     }
541 
542     setWindowTitle(
543             tr("%1 - %2").arg(shownName).arg(tr("glogg"))
544 #ifdef GLOGG_COMMIT
545             + " (dev build " GLOGG_VERSION ")"
546 #endif
547             );
548 }
549 
550 // Updates the actions for the recent files.
551 // Must be called after having added a new name to the list.
552 void MainWindow::updateRecentFileActions()
553 {
554     QStringList recent_files = recentFiles.recentFiles();
555 
556     for ( int j = 0; j < MaxRecentFiles; ++j ) {
557         if ( j < recent_files.count() ) {
558             QString text = tr("&%1 %2").arg(j + 1).arg(strippedName(recent_files[j]));
559             recentFileActions[j]->setText( text );
560             recentFileActions[j]->setToolTip( recent_files[j] );
561             recentFileActions[j]->setData( recent_files[j] );
562             recentFileActions[j]->setVisible( true );
563         }
564         else {
565             recentFileActions[j]->setVisible( false );
566         }
567     }
568 
569     // separatorAction->setVisible(!recentFiles.isEmpty());
570 }
571 
572 // Write settings to permanent storage
573 void MainWindow::writeSettings()
574 {
575     // Save the session
576     SessionInfo& session = Persistent<SessionInfo>( "session" );
577     session.setGeometry( saveGeometry() );
578     session.setCrawlerState( crawlerWidget->saveState() );
579     session.setCurrentFile( currentFile );
580     GetPersistentInfo().save( QString( "session" ) );
581 
582     // User settings
583     GetPersistentInfo().save( QString( "settings" ) );
584 }
585 
586 // Read settings from permanent storage
587 void MainWindow::readSettings()
588 {
589     // Get and restore the session
590     GetPersistentInfo().retrieve( QString( "session" ) );
591     SessionInfo session = Persistent<SessionInfo>( "session" );
592     restoreGeometry( session.geometry() );
593     crawlerWidget->restoreState( session.crawlerState() );
594     previousFile = session.currentFile();
595 
596     // History of recent files
597     GetPersistentInfo().retrieve( QString( "recentFiles" ) );
598     updateRecentFileActions();
599 
600     GetPersistentInfo().retrieve( QString( "savedSearches" ) );
601     GetPersistentInfo().retrieve( QString( "settings" ) );
602     GetPersistentInfo().retrieve( QString( "filterSet" ) );
603 }
604 
605 // Returns the size in human readable format
606 QString MainWindow::readableSize( qint64 size ) const
607 {
608     static const QString sizeStrs[] = {
609         tr("B"), tr("KiB"), tr("MiB"), tr("GiB"), tr("TiB") };
610 
611     QLocale defaultLocale;
612     unsigned int i;
613     double humanSize = size;
614 
615     for ( i=0; i+1 < (sizeof(sizeStrs)/sizeof(QString)) && (humanSize/1024.0) >= 1024.0; i++ )
616         humanSize /= 1024.0;
617 
618     if ( humanSize >= 1024.0 ) {
619         humanSize /= 1024.0;
620         i++;
621     }
622 
623     QString output;
624     if ( i == 0 )
625         // No decimal part if we display straight bytes.
626         output = defaultLocale.toString( (int) humanSize );
627     else
628         output = defaultLocale.toString( humanSize, 'f', 1 );
629 
630     output += QString(" ") + sizeStrs[i];
631 
632     return output;
633 }
634