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