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