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 © 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 #if QT_VERSION > 0x040500 432 defaultLocale.toString( lastModified, QLocale::NarrowFormat ); 433 #else 434 defaultLocale.toString( lastModified.date(), QLocale::ShortFormat ) 435 .append( " " ).append( defaultLocale.toString( 436 lastModified.time(), QLocale::ShortFormat ) ); 437 #endif 438 infoLine->setText( tr( "%1 (%2 - %3 lines - modified on %4)" ) 439 .arg(currentFile).arg(readableSize(fileSize)) 440 .arg(fileNbLine).arg( date ) ); 441 } 442 else { 443 infoLine->setText( tr( "%1 (%2 - %3 lines)" ) 444 .arg(currentFile).arg(readableSize(fileSize)) 445 .arg(fileNbLine) ); 446 } 447 448 infoLine->hideGauge(); 449 stopAction->setEnabled( false ); 450 } 451 452 // 453 // Events 454 // 455 456 // Closes the application 457 void MainWindow::closeEvent( QCloseEvent *event ) 458 { 459 writeSettings(); 460 event->accept(); 461 } 462 463 // Accepts the drag event if it looks like a filename 464 void MainWindow::dragEnterEvent( QDragEnterEvent* event ) 465 { 466 if ( event->mimeData()->hasFormat( "text/uri-list" ) ) 467 event->acceptProposedAction(); 468 } 469 470 // Tries and loads the file if the URL dropped is local 471 void MainWindow::dropEvent( QDropEvent* event ) 472 { 473 QList<QUrl> urls = event->mimeData()->urls(); 474 if ( urls.isEmpty() ) 475 return; 476 477 QString fileName = urls.first().toLocalFile(); 478 if ( fileName.isEmpty() ) 479 return; 480 481 loadFile( fileName ); 482 } 483 484 // 485 // Private functions 486 // 487 488 // Loads the passed file into the CrawlerWidget and update the title bar. 489 // The loading is done asynchronously. 490 bool MainWindow::loadFile( const QString& fileName ) 491 { 492 LOG(logDEBUG) << "loadFile ( " << fileName.toStdString() << " )"; 493 494 int topLine = 0; 495 496 // If we're loading the same file, put the same line on top. 497 if ( fileName == currentFile ) 498 topLine = crawlerWidget->getTopLine(); 499 500 // Load the file 501 loadingFileName = fileName; 502 if ( crawlerWidget->readFile( fileName, topLine ) ) { 503 LOG(logDEBUG) << "Success loading file " << fileName.toStdString(); 504 return true; 505 } 506 else { 507 LOG(logWARNING) << "Cannot load file " << fileName.toStdString(); 508 displayNormalStatus( false ); 509 return false; 510 } 511 } 512 513 // Strips the passed filename from its directory part. 514 QString MainWindow::strippedName( const QString& fullFileName ) const 515 { 516 return QFileInfo( fullFileName ).fileName(); 517 } 518 519 // Add the filename to the recent files list and update the title bar. 520 void MainWindow::setCurrentFile( const QString& fileName ) 521 { 522 // Change the current file 523 currentFile = fileName; 524 QString shownName = tr( "Untitled" ); 525 if ( !currentFile.isEmpty() ) { 526 // (reload the list first in case another glogg changed it) 527 GetPersistentInfo().retrieve( "recentFiles" ); 528 recentFiles.addRecent( currentFile ); 529 GetPersistentInfo().save( "recentFiles" ); 530 updateRecentFileActions(); 531 shownName = strippedName( currentFile ); 532 } 533 534 setWindowTitle( 535 tr("%1 - %2").arg(shownName).arg(tr("glogg")) 536 #ifdef GLOGG_COMMIT 537 + " (dev build " GLOGG_VERSION ")" 538 #endif 539 ); 540 } 541 542 // Updates the actions for the recent files. 543 // Must be called after having added a new name to the list. 544 void MainWindow::updateRecentFileActions() 545 { 546 QStringList recent_files = recentFiles.recentFiles(); 547 548 for ( int j = 0; j < MaxRecentFiles; ++j ) { 549 if ( j < recent_files.count() ) { 550 QString text = tr("&%1 %2").arg(j + 1).arg(strippedName(recent_files[j])); 551 recentFileActions[j]->setText( text ); 552 recentFileActions[j]->setToolTip( recent_files[j] ); 553 recentFileActions[j]->setData( recent_files[j] ); 554 recentFileActions[j]->setVisible( true ); 555 } 556 else { 557 recentFileActions[j]->setVisible( false ); 558 } 559 } 560 561 // separatorAction->setVisible(!recentFiles.isEmpty()); 562 } 563 564 // Write settings to permanent storage 565 void MainWindow::writeSettings() 566 { 567 // Save the session 568 SessionInfo& session = Persistent<SessionInfo>( "session" ); 569 session.setGeometry( saveGeometry() ); 570 session.setCrawlerState( crawlerWidget->saveState() ); 571 session.setCurrentFile( currentFile ); 572 GetPersistentInfo().save( QString( "session" ) ); 573 574 // User settings 575 GetPersistentInfo().save( QString( "settings" ) ); 576 } 577 578 // Read settings from permanent storage 579 void MainWindow::readSettings() 580 { 581 // Get and restore the session 582 GetPersistentInfo().retrieve( QString( "session" ) ); 583 SessionInfo session = Persistent<SessionInfo>( "session" ); 584 restoreGeometry( session.geometry() ); 585 crawlerWidget->restoreState( session.crawlerState() ); 586 previousFile = session.currentFile(); 587 588 // History of recent files 589 GetPersistentInfo().retrieve( QString( "recentFiles" ) ); 590 updateRecentFileActions(); 591 592 GetPersistentInfo().retrieve( QString( "savedSearches" ) ); 593 GetPersistentInfo().retrieve( QString( "settings" ) ); 594 GetPersistentInfo().retrieve( QString( "filterSet" ) ); 595 } 596 597 // Returns the size in human readable format 598 QString MainWindow::readableSize( qint64 size ) const 599 { 600 static const QString sizeStrs[] = { 601 tr("B"), tr("KiB"), tr("MiB"), tr("GiB"), tr("TiB") }; 602 603 QLocale defaultLocale; 604 unsigned int i; 605 double humanSize = size; 606 607 for ( i=0; i+1 < (sizeof(sizeStrs)/sizeof(QString)) && (humanSize/1024.0) >= 1024.0; i++ ) 608 humanSize /= 1024.0; 609 610 if ( humanSize >= 1024.0 ) { 611 humanSize /= 1024.0; 612 i++; 613 } 614 615 QString output; 616 if ( i == 0 ) 617 // No decimal part if we display straight bytes. 618 output = defaultLocale.toString( (int) humanSize ); 619 else 620 output = defaultLocale.toString( humanSize, 'f', 1 ); 621 622 output += QString(" ") + sizeStrs[i]; 623 624 return output; 625 } 626