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