1 /*
2  *   File name: MainWindow.cpp
3  *   Summary:	QDirStat main window
4  *   License:	GPL V2 - See file LICENSE for details.
5  *
6  *   Author:	Stefan Hundhammer <Stefan.Hundhammer@gmx.de>
7  */
8 
9 
10 #include <QApplication>
11 #include <QCloseEvent>
12 #include <QMouseEvent>
13 #include <QMessageBox>
14 #include <QFileDialog>
15 #include <QClipboard>
16 
17 #include "MainWindow.h"
18 #include "ActionManager.h"
19 #include "CleanupCollection.h"
20 #include "CleanupConfigPage.h"
21 #include "ConfigDialog.h"
22 #include "DataColumns.h"
23 #include "DebugHelpers.h"
24 #include "DirTree.h"
25 #include "DirTreeCache.h"
26 #include "DirTreeModel.h"
27 #include "Exception.h"
28 #include "ExcludeRules.h"
29 #include "FileDetailsView.h"
30 #include "FileSizeStatsWindow.h"
31 #include "FileTypeStatsWindow.h"
32 #include "Logger.h"
33 #include "MimeCategorizer.h"
34 #include "OpenDirDialog.h"
35 #include "OpenPkgDialog.h"
36 #include "OutputWindow.h"
37 #include "PanelMessage.h"
38 #include "PkgManager.h"
39 #include "PkgQuery.h"
40 #include "QDirStatApp.h"
41 #include "Refresher.h"
42 #include "SelectionModel.h"
43 #include "Settings.h"
44 #include "SettingsHelpers.h"
45 #include "SysUtil.h"
46 #include "Trash.h"
47 #include "UnreadableDirsWindow.h"
48 #include "Version.h"
49 
50 #define LONG_MESSAGE		25*1000
51 #define UPDATE_MILLISEC		200
52 
53 #define USE_CUSTOM_OPEN_DIR_DIALOG 1
54 
55 using namespace QDirStat;
56 
57 
MainWindow()58 MainWindow::MainWindow():
59     QMainWindow(),
60     _ui( new Ui::MainWindow ),
61     _configDialog( 0 ),
62     _enableDirPermissionsWarning( false ),
63     _verboseSelection( false ),
64     _urlInWindowTitle( false ),
65     _useTreemapHover( false ),
66     _statusBarTimeout( 3000 ), // millisec
67     _treeLevelMapper(0),
68     _currentLayout( 0 )
69 {
70     CHECK_PTR( _ui );
71 
72     _ui->setupUi( this );
73     ActionManager::instance()->addWidgetTree( this );
74     initLayoutActions();
75     createLayouts();    // see MainWindowLayout.cpp
76     readSettings();
77     _updateTimer.setInterval( UPDATE_MILLISEC );
78     _treeExpandTimer.setSingleShot( true );
79     _dUrl = _ui->actionDonate->iconText();
80     _futureSelection.setUseRootFallback( false );
81     _ui->menubar->setCornerWidget( new QLabel( MENUBAR_VERSION ) );
82 
83     // The first call to app() creates the QDirStatApp and with it
84     // - the DirTreeModel
85     // - the DirTree (owned and managed by the DirTreeModel)
86     // - the SelectionModel
87     // - the CleanupCollection.
88 
89     _ui->dirTreeView->setModel( app()->dirTreeModel() );
90     _ui->dirTreeView->setSelectionModel( app()->selectionModel() );
91 
92     _ui->treemapView->setDirTree( app()->dirTree() );
93     _ui->treemapView->setSelectionModel( app()->selectionModel() );
94 
95     app()->cleanupCollection()->addToMenu   ( _ui->menuCleanup,
96                                               true ); // keepUpdated
97     app()->cleanupCollection()->addToToolBar( _ui->toolBar,
98                                               true ); // keepUpdated
99 
100 
101     _ui->dirTreeView->setCleanupCollection( app()->cleanupCollection() );
102     _ui->treemapView->setCleanupCollection( app()->cleanupCollection() );
103 
104     _ui->breadcrumbNavigator->clear();
105 
106     _historyButtons = new HistoryButtons( _ui->actionGoBack,
107                                           _ui->actionGoForward );
108     CHECK_NEW( _historyButtons );
109 
110     _discoverActions = new DiscoverActions( this );
111     CHECK_NEW( _discoverActions );
112 
113 #ifdef Q_OS_MACX
114     // This makes the application to look like more "native" on macOS
115     setUnifiedTitleAndToolBarOnMac( true );
116     _ui->toolBar->setMovable( false );
117 #endif
118 
119     connectSignals();
120     connectMenuActions();               // see MainWindowMenus.cpp
121     changeLayout( _layoutName );        // see MainWindowLayout.cpp
122 
123     checkPkgManagerSupport();
124 
125     if ( ! _ui->actionShowTreemap->isChecked() )
126 	_ui->treemapView->disable();
127 
128     toggleVerboseSelection();
129     updateActions();
130 }
131 
132 
~MainWindow()133 MainWindow::~MainWindow()
134 {
135     // logDebug() << "Destroying main window" << endl;
136 
137     if ( _currentLayout )
138 	saveLayout( _currentLayout );   // see MainWindowLayout.cpp
139 
140     writeSettings();
141     ExcludeRules::instance()->writeSettings();
142     MimeCategorizer::instance()->writeSettings();
143 
144     // Relying on the QObject hierarchy to properly clean this up resulted in a
145     //	segfault; there was probably a problem in the deletion order.
146 
147     if ( _configDialog )
148 	delete _configDialog;
149 
150     delete _ui->dirTreeView;
151     delete _ui;
152     delete _historyButtons;
153 
154     qDeleteAll( _layouts );
155 
156     QDirStatApp::deleteInstance();
157 
158     // logDebug() << "Main window destroyed" << endl;
159 }
160 
161 
checkPkgManagerSupport()162 void MainWindow::checkPkgManagerSupport()
163 {
164     if ( ! PkgQuery::haveGetInstalledPkgSupport() ||
165 	 ! PkgQuery::haveFileListSupport()	    )
166     {
167 	logInfo() << "No package manager support "
168 		  << "for getting installed packages or file lists"
169 		  << endl;
170 
171 	_ui->actionOpenPkg->setEnabled( false );
172     }
173 
174     PkgManager * pkgManager = PkgQuery::primaryPkgManager();
175 
176     if ( ! pkgManager || ! pkgManager->supportsFileListCache() )
177     {
178 	logInfo() << "No package manager support "
179 		  << "for getting a file lists cache"
180 		  << endl;
181 
182 	_ui->actionShowUnpkgFiles->setEnabled( false );
183     }
184 }
185 
186 
connectSignals()187 void MainWindow::connectSignals()
188 {
189     connect( app()->selectionModel(),	 SIGNAL( currentBranchChanged( QModelIndex ) ),
190 	     _ui->dirTreeView,		 SLOT  ( closeAllExcept	     ( QModelIndex ) ) );
191 
192     connect( app()->dirTree(),		 SIGNAL( startingReading() ),
193 	     this,			 SLOT  ( startingReading() ) );
194 
195     connect( app()->dirTree(),		 SIGNAL( finished()	   ),
196 	     this,			 SLOT  ( readingFinished() ) );
197 
198     connect( app()->dirTree(),		 SIGNAL( aborted()	   ),
199 	     this,			 SLOT  ( readingAborted()  ) );
200 
201     connect( app()->selectionModel(),	 SIGNAL( selectionChanged() ),
202 	     this,			 SLOT  ( updateActions()    ) );
203 
204     connect( app()->selectionModel(),	 SIGNAL( currentItemChanged( FileInfo *, FileInfo * ) ),
205 	     this,			 SLOT  ( updateActions()			    ) );
206 
207     connect( app()->selectionModel(),	 SIGNAL( currentItemChanged( FileInfo *, FileInfo * ) ),
208 	     _ui->breadcrumbNavigator,	 SLOT  ( setPath	   ( FileInfo *		    ) ) );
209 
210     connect( app()->selectionModel(),	 SIGNAL( currentItemChanged( FileInfo *, FileInfo * ) ),
211 	     _historyButtons,		 SLOT  ( addToHistory	   ( FileInfo *		    ) ) );
212 
213     connect( _historyButtons,		 SIGNAL( navigateToUrl( QString ) ),
214 	     this,			 SLOT  ( navigateToUrl( QString ) ) );
215 
216     connect( _ui->breadcrumbNavigator,	 SIGNAL( pathClicked   ( QString ) ),
217 	     app()->selectionModel(),	 SLOT  ( setCurrentItem( QString ) ) );
218 
219     connect( _ui->treemapView,		 SIGNAL( treemapChanged() ),
220 	     this,			 SLOT  ( updateActions()  ) );
221 
222     connect( app()->cleanupCollection(), SIGNAL( startingCleanup( QString ) ),
223 	     this,			 SLOT  ( startingCleanup( QString ) ) );
224 
225     connect( app()->cleanupCollection(), SIGNAL( cleanupFinished( int ) ),
226 	     this,			 SLOT  ( cleanupFinished( int ) ) );
227 
228     connect( &_updateTimer,		 SIGNAL( timeout()	   ),
229 	     this,			 SLOT  ( showElapsedTime() ) );
230 
231     connect( &_treeExpandTimer,		  SIGNAL( timeout() ),
232 	     _ui->actionExpandTreeLevel1, SLOT( trigger()   ) );
233 
234     if ( _useTreemapHover )
235     {
236 	connect( _ui->treemapView,	  SIGNAL( hoverEnter ( FileInfo * ) ),
237 		 this,			  SLOT	( showCurrent( FileInfo * ) ) );
238 
239 	connect( _ui->treemapView,	  SIGNAL( hoverLeave ( FileInfo * ) ),
240 		 this,			  SLOT	( showSummary()		  ) );
241     }
242 
243     connect( app()->selectionModel(),	  SIGNAL( selectionChanged() ),
244 	     this,			  SLOT	( selectionChanged() ) );
245 
246     connect( app()->selectionModel(),	  SIGNAL( currentItemChanged( FileInfo *, FileInfo * ) ),
247 	     this,			  SLOT	( currentItemChanged( FileInfo *, FileInfo * ) ) );
248 }
249 
250 
updateActions()251 void MainWindow::updateActions()
252 {
253     bool reading	     = app()->dirTree()->isBusy();
254     FileInfo * currentItem   = app()->selectionModel()->currentItem();
255     FileInfo * firstToplevel = app()->dirTree()->firstToplevel();
256     bool pkgView	     = firstToplevel && firstToplevel->isPkgInfo();
257 
258     _ui->actionStopReading->setEnabled( reading );
259     _ui->actionRefreshAll->setEnabled	( ! reading );
260     _ui->actionAskReadCache->setEnabled ( ! reading );
261     _ui->actionAskWriteCache->setEnabled( ! reading );
262 
263     _ui->actionCopyPathToClipboard->setEnabled( currentItem );
264     _ui->actionGoUp->setEnabled( currentItem && currentItem->treeLevel() > 1 );
265     _ui->actionGoToToplevel->setEnabled( firstToplevel && ( ! currentItem || currentItem->treeLevel() > 1 ));
266 
267     FileInfoSet selectedItems = app()->selectionModel()->selectedItems();
268     FileInfo * sel	      = selectedItems.first();
269     int selSize		      = selectedItems.size();
270 
271     bool oneDirSelected	   = selSize == 1 && sel && sel->isDir() && ! sel->isPkgInfo();
272     bool pseudoDirSelected = selectedItems.containsPseudoDir();
273     bool pkgSelected	   = selectedItems.containsPkg();
274 
275     _ui->actionMoveToTrash->setEnabled( sel && ! pseudoDirSelected && ! pkgSelected && ! reading );
276     _ui->actionRefreshSelected->setEnabled( selSize == 1 && ! sel->isExcluded() && ! sel->isMountPoint() && ! pkgView );
277     _ui->actionContinueReadingAtMountPoint->setEnabled( oneDirSelected && sel->isMountPoint() );
278     _ui->actionReadExcludedDirectory->setEnabled      ( oneDirSelected && sel->isExcluded()   );
279 
280     bool nothingOrOneDir = selectedItems.isEmpty() || oneDirSelected;
281 
282     _ui->actionFileSizeStats->setEnabled( ! reading && nothingOrOneDir );
283     _ui->actionFileTypeStats->setEnabled( ! reading && nothingOrOneDir );
284     _ui->actionFileAgeStats->setEnabled ( ! reading && nothingOrOneDir );
285 
286     bool showingTreemap = _ui->treemapView->isVisible();
287 
288     _ui->actionTreemapAsSidePanel->setEnabled( showingTreemap );
289     _ui->actionTreemapZoomIn->setEnabled   ( showingTreemap && _ui->treemapView->canZoomIn() );
290     _ui->actionTreemapZoomOut->setEnabled  ( showingTreemap && _ui->treemapView->canZoomOut() );
291     _ui->actionResetTreemapZoom->setEnabled( showingTreemap && _ui->treemapView->canZoomOut() );
292     _ui->actionTreemapRebuild->setEnabled  ( showingTreemap );
293 
294     _historyButtons->updateActions();
295 }
296 
297 
readSettings()298 void MainWindow::readSettings()
299 {
300     QDirStat::Settings settings;
301     settings.beginGroup( "MainWindow" );
302 
303     _statusBarTimeout	  = settings.value( "StatusBarTimeoutMillisec", 3000  ).toInt();
304     bool showTreemap	  = settings.value( "ShowTreemap"	      , true  ).toBool();
305     bool treemapOnSide	  = settings.value( "TreemapOnSide"	      , false ).toBool();
306 
307     _verboseSelection	  = settings.value( "VerboseSelection"	      , false ).toBool();
308     _urlInWindowTitle	  = settings.value( "UrlInWindowTitle"	      , false ).toBool();
309     _useTreemapHover	  = settings.value( "UseTreemapHover"	      , false ).toBool();
310     _layoutName		  = settings.value( "Layout"		      , "L2"  ).toString();
311 
312     settings.endGroup();
313 
314     settings.beginGroup( "MainWindow-Subwindows" );
315     QByteArray mainSplitterState = settings.value( "MainSplitter" , QByteArray() ).toByteArray();
316     QByteArray topSplitterState	 = settings.value( "TopSplitter"  , QByteArray() ).toByteArray();
317     settings.endGroup();
318 
319     _ui->actionShowTreemap->setChecked( showTreemap );
320     _ui->actionTreemapAsSidePanel->setChecked( treemapOnSide );
321     treemapAsSidePanel();
322 
323     _ui->actionVerboseSelection->setChecked( _verboseSelection );
324 
325     foreach ( QAction * action, _layoutActionGroup->actions() )
326     {
327 	if ( action->data().toString() == _layoutName )
328 	    action->setChecked( true );
329     }
330 
331     readWindowSettings( this, "MainWindow" );
332 
333     if ( ! mainSplitterState.isNull() )
334 	_ui->mainWinSplitter->restoreState( mainSplitterState );
335 
336     if ( ! topSplitterState.isNull() )
337 	_ui->topViewsSplitter->restoreState( topSplitterState );
338     else
339     {
340 	// The Qt designer refuses to let me set a reasonable size for that
341 	// widget, so let's set one here. Yes, that's not really how this is
342 	// supposed to be, but I am fed up with that stuff.
343 
344 	_ui->fileDetailsPanel->resize( QSize( 300, 300 ) );
345     }
346 
347     foreach ( TreeLayout * layout, _layouts )
348 	readLayoutSettings( layout );   // see MainWindowLayout.cpp
349 
350     ExcludeRules::instance()->readSettings();
351     Debug::dumpExcludeRules();
352 }
353 
354 
writeSettings()355 void MainWindow::writeSettings()
356 {
357     QDirStat::Settings settings;
358     settings.beginGroup( "MainWindow" );
359 
360     settings.setValue( "ShowTreemap"	 , _ui->actionShowTreemap->isChecked() );
361     settings.setValue( "TreemapOnSide"	 , _ui->actionTreemapAsSidePanel->isChecked() );
362     settings.setValue( "VerboseSelection", _verboseSelection );
363     settings.setValue( "Layout"		 , _layoutName );
364 
365     // Those are only set if not already in the settings (they might have been
366     // set from a config dialog).
367     settings.setDefaultValue( "StatusBarTimeoutMillisec", _statusBarTimeout );
368     settings.setDefaultValue( "UrlInWindowTitle"	, _urlInWindowTitle );
369     settings.setDefaultValue( "UseTreemapHover"		, _useTreemapHover );
370 
371     settings.endGroup();
372 
373     writeWindowSettings( this, "MainWindow" );
374 
375     settings.beginGroup( "MainWindow-Subwindows" );
376     settings.setValue( "MainSplitter"		 , _ui->mainWinSplitter->saveState()  );
377     settings.setValue( "TopSplitter"		 , _ui->topViewsSplitter->saveState() );
378     settings.endGroup();
379 
380     foreach ( TreeLayout * layout, _layouts )
381 	writeLayoutSettings( layout );  // see MainWindowLayout.cpp
382 }
383 
384 
showTreemapView()385 void MainWindow::showTreemapView()
386 {
387     if ( _ui->actionShowTreemap->isChecked() )
388 	_ui->treemapView->enable();
389     else
390 	_ui->treemapView->disable();
391 }
392 
393 
treemapAsSidePanel()394 void MainWindow::treemapAsSidePanel()
395 {
396     if ( _ui->actionTreemapAsSidePanel->isChecked() )
397 	_ui->mainWinSplitter->setOrientation( Qt::Horizontal );
398     else
399 	_ui->mainWinSplitter->setOrientation( Qt::Vertical );
400 }
401 
402 
busyDisplay()403 void MainWindow::busyDisplay()
404 {
405     _ui->treemapView->disable();
406     updateActions();
407 
408     // If it is open, close the window that lists unreadable directories:
409     // With the next directory read, things might have changed; the user may
410     // have fixed permissions or ownership of those directories.
411 
412     UnreadableDirsWindow::closeSharedInstance();
413 
414     if ( _dirPermissionsWarning )
415         _dirPermissionsWarning->deleteLater();
416 
417     _updateTimer.start();
418 
419     // It would be nice to sort by read jobs during reading, but this confuses
420     // the hell out of the Qt side of the data model; so let's sort by name
421     // instead.
422 
423     int sortCol = QDirStat::DataColumns::toViewCol( QDirStat::NameCol );
424     _ui->dirTreeView->sortByColumn( sortCol, Qt::AscendingOrder );
425 
426     if ( ! PkgFilter::isPkgUrl( app()->dirTree()->url() ) &&
427 	 ! app()->selectionModel()->currentBranch() )
428     {
429         _treeExpandTimer.start( 200 );
430         // This will trigger actionExpandTreeLevel1. Hopefully after those 200
431         // millisec there will be some items in the tree to expand.
432     }
433 }
434 
435 
idleDisplay()436 void MainWindow::idleDisplay()
437 {
438     logInfo() << endl;
439 
440     updateActions();
441     _updateTimer.stop();
442     int sortCol = QDirStat::DataColumns::toViewCol( QDirStat::PercentNumCol );
443     _ui->dirTreeView->sortByColumn( sortCol, Qt::DescendingOrder );
444 
445     if ( ! _futureSelection.isEmpty() )
446     {
447         _treeExpandTimer.stop();
448         applyFutureSelection();
449     }
450     else if ( ! app()->selectionModel()->currentBranch() )
451     {
452 	logDebug() << "No current branch - expanding tree to level 1" << endl;
453 	expandTreeToLevel( 1 );
454     }
455 
456     updateFileDetailsView();
457     showTreemapView();
458 }
459 
460 
updateFileDetailsView()461 void MainWindow::updateFileDetailsView()
462 {
463     if ( _ui->fileDetailsView->isVisible() )
464     {
465 	FileInfoSet sel = app()->selectionModel()->selectedItems();
466 
467 	if ( sel.isEmpty() )
468 	    _ui->fileDetailsView->showDetails( app()->selectionModel()->currentItem() );
469 	else
470 	{
471 	    if ( sel.count() == 1 )
472 		_ui->fileDetailsView->showDetails( sel.first() );
473 	    else
474 		_ui->fileDetailsView->showDetails( sel );
475 	}
476     }
477 }
478 
479 
startingReading()480 void MainWindow::startingReading()
481 {
482     _stopWatch.start();
483     busyDisplay();
484 }
485 
486 
readingFinished()487 void MainWindow::readingFinished()
488 {
489     logInfo() << endl;
490 
491     idleDisplay();
492 
493     QString elapsedTime = formatMillisec( _stopWatch.elapsed() );
494     _ui->statusBar->showMessage( tr( "Finished. Elapsed time: %1").arg( elapsedTime ), LONG_MESSAGE );
495     logInfo() << "Reading finished after " << elapsedTime << endl;
496 
497     if ( app()->dirTree()->firstToplevel() &&
498 	 app()->dirTree()->firstToplevel()->errSubDirCount() > 0 )
499     {
500 	showDirPermissionsWarning();
501     }
502 
503     // Debug::dumpModelTree( app()->dirTreeModel(), QModelIndex(), "" );
504 }
505 
506 
readingAborted()507 void MainWindow::readingAborted()
508 {
509     logInfo() << endl;
510 
511     idleDisplay();
512     QString elapsedTime = formatMillisec( _stopWatch.elapsed() );
513     _ui->statusBar->showMessage( tr( "Aborted. Elapsed time: %1").arg( elapsedTime ), LONG_MESSAGE );
514     logInfo() << "Reading aborted after " << elapsedTime << endl;
515 }
516 
517 
openUrl(const QString & url)518 void MainWindow::openUrl( const QString & url )
519 {
520     _enableDirPermissionsWarning = true;
521     _historyButtons->clearHistory();
522 
523     if ( PkgFilter::isPkgUrl( url ) )
524 	readPkg( url );
525     else if ( isUnpkgUrl( url ) )
526 	showUnpkgFiles( url );  // see MainWinUnpkg.cpp
527     else
528 	openDir( url );
529 }
530 
531 
openDir(const QString & url)532 void MainWindow::openDir( const QString & url )
533 {
534     try
535     {
536 	app()->dirTreeModel()->openUrl( url );
537 	updateWindowTitle( app()->dirTree()->url() );
538     }
539     catch ( const SysCallFailedException & ex )
540     {
541 	CAUGHT( ex );
542         showOpenDirErrorPopup( ex );
543 	askOpenDir();
544     }
545 
546     updateActions();
547     expandTreeToLevel( 1 );
548 }
549 
550 
showOpenDirErrorPopup(const SysCallFailedException & ex)551 void MainWindow::showOpenDirErrorPopup( const SysCallFailedException & ex )
552 {
553     updateWindowTitle( "" );
554     app()->dirTree()->sendFinished();
555 
556     QMessageBox errorPopup( QMessageBox::Warning,	// icon
557                             tr( "Error" ),		// title
558                             tr( "Could not open directory %1" ).arg( ex.resourceName() ), // text
559                             QMessageBox::Ok,            // buttons
560                             this );			// parent
561     errorPopup.setDetailedText( ex.what() );
562     errorPopup.exec();
563 }
564 
565 
askOpenDir()566 void MainWindow::askOpenDir()
567 {
568     QString path;
569     DirTree * tree = app()->dirTree();
570     bool crossFilesystems = tree->crossFilesystems();
571 
572 #if USE_CUSTOM_OPEN_DIR_DIALOG
573     path = QDirStat::OpenDirDialog::askOpenDir( &crossFilesystems, this );
574 #else
575     path = QFileDialog::getExistingDirectory( this, // parent
576                                               tr("Select directory to scan") );
577 #endif
578 
579     if ( ! path.isEmpty() )
580     {
581 	tree->reset();
582 	tree->setCrossFilesystems( crossFilesystems );
583 	openUrl( path );
584     }
585 }
586 
587 
askOpenPkg()588 void MainWindow::askOpenPkg()
589 {
590     bool canceled;
591     PkgFilter pkgFilter = OpenPkgDialog::askPkgFilter( &canceled );
592 
593     if ( ! canceled )
594     {
595 	app()->dirTree()->reset();
596 	readPkg( pkgFilter );
597     }
598 }
599 
600 
readPkg(const PkgFilter & pkgFilter)601 void MainWindow::readPkg( const PkgFilter & pkgFilter )
602 {
603     // logInfo() << "URL: " << pkgFilter.url() << endl;
604 
605     updateWindowTitle( pkgFilter.url() );
606     expandTreeToLevel( 0 );   // Performance boost: Down from 25 to 6 sec.
607     app()->dirTreeModel()->readPkg( pkgFilter );
608 }
609 
610 
refreshAll()611 void MainWindow::refreshAll()
612 {
613     _enableDirPermissionsWarning = true;
614     QString url = app()->dirTree()->url();
615 
616     if ( ! url.isEmpty() )
617     {
618 	logDebug() << "Refreshing " << url << endl;
619 
620 	if ( PkgFilter::isPkgUrl( url ) )
621 	    app()->dirTreeModel()->readPkg( url );
622 	else
623 	    app()->dirTreeModel()->openUrl( url );
624 
625         // No need to check if the URL is an unpkg:/ URL:
626         //
627         // In that case, the previous filters are still set, and just reading
628         // the dir tree again from disk with openUrl() will filter out the
629         // unwanted packaged files, ignored extensions and excluded directories
630         // again.
631 
632 	updateActions();
633     }
634     else
635     {
636 	askOpenDir();
637     }
638 }
639 
640 
refreshSelected()641 void MainWindow::refreshSelected()
642 {
643     busyDisplay();
644     _futureSelection.set( app()->selectionModel()->selectedItems().first() );
645     // logDebug() << "Setting future selection: " << _futureSelection.subtree() << endl;
646     app()->dirTreeModel()->refreshSelected();
647     updateActions();
648 }
649 
650 
applyFutureSelection()651 void MainWindow::applyFutureSelection()
652 {
653     FileInfo * sel = _futureSelection.subtree();
654     // logDebug() << "Using future selection: " << sel << endl;
655 
656     if ( sel )
657     {
658         _treeExpandTimer.stop();
659         _futureSelection.clear();
660         app()->selectionModel()->setCurrentBranch( sel );
661 
662         if ( sel->isMountPoint() )
663             _ui->dirTreeView->setExpanded( sel, true );
664     }
665 }
666 
667 
stopReading()668 void MainWindow::stopReading()
669 {
670     if ( app()->dirTree()->isBusy() )
671     {
672 	app()->dirTree()->abortReading();
673 	_ui->statusBar->showMessage( tr( "Reading aborted." ), LONG_MESSAGE );
674     }
675 }
676 
677 
readCache(const QString & cacheFileName)678 void MainWindow::readCache( const QString & cacheFileName )
679 {
680     app()->dirTreeModel()->clear();
681     _historyButtons->clearHistory();
682 
683     if ( ! cacheFileName.isEmpty() )
684 	app()->dirTree()->readCache( cacheFileName );
685 }
686 
687 
askReadCache()688 void MainWindow::askReadCache()
689 {
690     QString fileName = QFileDialog::getOpenFileName( this, // parent
691 						     tr( "Select QDirStat cache file" ),
692 						     DEFAULT_CACHE_NAME );
693     if ( ! fileName.isEmpty() )
694 	readCache( fileName );
695 
696     updateActions();
697 }
698 
699 
askWriteCache()700 void MainWindow::askWriteCache()
701 {
702     QString fileName = QFileDialog::getSaveFileName( this, // parent
703 						     tr( "Enter name for QDirStat cache file"),
704 						     DEFAULT_CACHE_NAME );
705     if ( ! fileName.isEmpty() )
706     {
707 	bool ok = app()->dirTree()->writeCache( fileName );
708 
709 	if ( ok )
710 	{
711 	    showProgress( tr( "Directory tree written to file %1" ).arg( fileName ) );
712 	}
713 	else
714 	{
715 	    QMessageBox::critical( this,
716 				   tr( "Error" ), // Title
717 				   tr( "ERROR writing cache file %1").arg( fileName ) );
718 	}
719     }
720 }
721 
722 
updateWindowTitle(const QString & url)723 void MainWindow::updateWindowTitle( const QString & url )
724 {
725     QString windowTitle = "QDirStat";
726 
727     if ( SysUtil::runningAsRoot() )
728 	windowTitle += tr( " [root]" );
729 
730     if ( _urlInWindowTitle )
731 	windowTitle += " " + url;
732 
733     setWindowTitle( windowTitle );
734 }
735 
736 
showProgress(const QString & text)737 void MainWindow::showProgress( const QString & text )
738 {
739     _ui->statusBar->showMessage( text, _statusBarTimeout );
740 }
741 
742 
showElapsedTime()743 void MainWindow::showElapsedTime()
744 {
745     showProgress( tr( "Reading... %1" )
746 		  .arg( formatMillisec( _stopWatch.elapsed(), false ) ) );
747 }
748 
749 
showCurrent(FileInfo * item)750 void MainWindow::showCurrent( FileInfo * item )
751 {
752     if ( item )
753     {
754 	QString msg = QString( "%1  (%2%3)" )
755 	    .arg( item->debugUrl() )
756 	    .arg( item->sizePrefix() )
757 	    .arg( formatSize( item->totalSize() ) );
758 
759 	if ( item->readState() == DirPermissionDenied )
760 	    msg += tr( "  [Permission Denied]" );
761         else if ( item->readState() == DirError )
762 	    msg += tr( "  [Read Error]" );
763 
764 	_ui->statusBar->showMessage( msg );
765     }
766     else
767     {
768 	_ui->statusBar->clearMessage();
769     }
770 }
771 
772 
showSummary()773 void MainWindow::showSummary()
774 {
775     FileInfoSet sel = app()->selectionModel()->selectedItems();
776     int count = sel.size();
777 
778     if ( count <= 1 )
779 	showCurrent( app()->selectionModel()->currentItem() );
780     else
781     {
782 	sel = sel.normalized();
783 
784 	_ui->statusBar->showMessage( tr( "%1 items selected (%2 total)" )
785 				     .arg( count )
786 				     .arg( formatSize( sel.totalSize() ) ) );
787     }
788 }
789 
790 
startingCleanup(const QString & cleanupName)791 void MainWindow::startingCleanup( const QString & cleanupName )
792 {
793     showProgress( tr( "Starting cleanup action %1" ).arg( cleanupName ) );
794 }
795 
796 
cleanupFinished(int errorCount)797 void MainWindow::cleanupFinished( int errorCount )
798 {
799     logDebug() << "Error count: " << errorCount << endl;
800 
801     if ( errorCount == 0 )
802 	showProgress( tr( "Cleanup action finished successfully." ) );
803     else
804 	showProgress( tr( "Cleanup action finished with %1 errors." ).arg( errorCount ) );
805 }
806 
807 
notImplemented()808 void MainWindow::notImplemented()
809 {
810     QMessageBox::warning( this, tr( "Error" ), tr( "Not implemented!" ) );
811 }
812 
813 
copyCurrentPathToClipboard()814 void MainWindow::copyCurrentPathToClipboard()
815 {
816     FileInfo * currentItem = app()->selectionModel()->currentItem();
817 
818     if ( currentItem )
819     {
820 	QClipboard * clipboard = QApplication::clipboard();
821 	QString path = currentItem->path();
822 	clipboard->setText( path );
823 	showProgress( tr( "Copied to system clipboard: %1" ).arg( path ) );
824     }
825     else
826     {
827 	showProgress( tr( "No current item" ) );
828     }
829 }
830 
831 
expandTreeToLevel(int level)832 void MainWindow::expandTreeToLevel( int level )
833 {
834     logDebug() << "Expanding tree to level " << level << endl;
835 
836     if ( level < 1 )
837 	_ui->dirTreeView->collapseAll();
838     else
839 	_ui->dirTreeView->expandToDepth( level - 1 );
840 }
841 
842 
navigateUp()843 void MainWindow::navigateUp()
844 {
845     FileInfo * currentItem = app()->selectionModel()->currentItem();
846 
847     if ( currentItem && currentItem->parent() &&
848 	 currentItem->parent() != app()->dirTree()->root() )
849     {
850 	app()->selectionModel()->setCurrentItem( currentItem->parent(),
851 					 true ); // select
852     }
853 }
854 
855 
navigateToToplevel()856 void MainWindow::navigateToToplevel()
857 {
858     FileInfo * toplevel = app()->dirTree()->firstToplevel();
859 
860     if ( toplevel )
861     {
862 	expandTreeToLevel( 1 );
863 	app()->selectionModel()->setCurrentItem( toplevel,
864                                                  true ); // select
865     }
866 }
867 
868 
navigateToUrl(const QString & url)869 void MainWindow::navigateToUrl( const QString & url )
870 {
871     // logDebug() << "Navigating to " << url << endl;
872 
873     if ( ! url.isEmpty() )
874     {
875         FileInfo * sel = app()->dirTree()->locate( url,
876                                                    true ); // findPseudoDirs
877 
878         if ( sel )
879 
880         {
881             app()->selectionModel()->setCurrentItem( sel,
882                                                      true ); // select
883             _ui->dirTreeView->setExpanded( sel, true );
884         }
885     }
886 }
887 
888 
moveToTrash()889 void MainWindow::moveToTrash()
890 {
891     FileInfoSet selectedItems = app()->selectionModel()->selectedItems().normalized();
892 
893     // Prepare output window
894 
895     OutputWindow * outputWindow = new OutputWindow( qApp->activeWindow() );
896     CHECK_NEW( outputWindow );
897 
898     // Prepare refresher
899 
900     FileInfoSet refreshSet = Refresher::parents( selectedItems );
901     app()->selectionModel()->prepareRefresh( refreshSet );
902     Refresher * refresher  = new Refresher( refreshSet, this );
903     CHECK_NEW( refresher );
904 
905     connect( outputWindow, SIGNAL( lastProcessFinished( int ) ),
906 	     refresher,	   SLOT	 ( refresh()		      ) );
907 
908     outputWindow->showAfterTimeout();
909 
910     // Move all selected items to trash
911 
912     foreach ( FileInfo * item, selectedItems )
913     {
914 	bool success = Trash::trash( item->path() );
915 
916 	if ( success )
917 	    outputWindow->addStdout( tr( "Moved to trash: %1" ).arg( item->path() ) );
918 	else
919 	    outputWindow->addStderr( tr( "Move to trash failed for %1" ).arg( item->path() ) );
920     }
921 
922     outputWindow->noMoreProcesses();
923 }
924 
925 
openConfigDialog()926 void MainWindow::openConfigDialog()
927 {
928     if ( _configDialog && _configDialog->isVisible() )
929 	return;
930 
931     // For whatever crazy reason it is considerably faster to delete that
932     // complex dialog and recreate it from scratch than to simply leave it
933     // alive and just show it again. Well, whatever - so be it.
934     //
935     // And yes, I added debug logging here, in the dialog's setup(), in
936     // showEvent(); I added update(). No result whatsoever.
937     // Okay, then let's take the long way around.
938 
939     if ( _configDialog )
940 	delete _configDialog;
941 
942     _configDialog = new ConfigDialog( this );
943     CHECK_PTR( _configDialog );
944     _configDialog->cleanupConfigPage()->setCleanupCollection( app()->cleanupCollection() );
945 
946     if ( ! _configDialog->isVisible() )
947     {
948 	_configDialog->setup();
949 	_configDialog->show();
950     }
951 }
952 
953 
showFileTypeStats()954 void MainWindow::showFileTypeStats()
955 {
956     FileTypeStatsWindow::populateSharedInstance( app()->selectedDirOrRoot() );
957 }
958 
959 
showFileSizeStats()960 void MainWindow::showFileSizeStats()
961 {
962     FileSizeStatsWindow::populateSharedInstance( app()->selectedDirOrRoot() );
963 }
964 
965 
showFileAgeStats()966 void MainWindow::showFileAgeStats()
967 {
968     if ( ! _fileAgeStatsWindow )
969     {
970 	// This deletes itself when the user closes it. The associated QPointer
971 	// keeps track of that and sets the pointer to 0 when it happens.
972 
973 	_fileAgeStatsWindow = new FileAgeStatsWindow( this );
974 
975         connect( app()->selectionModel(), SIGNAL( currentItemChanged( FileInfo *, FileInfo * ) ),
976                  _fileAgeStatsWindow,     SLOT  ( syncedPopulate    ( FileInfo *             ) ) );
977 
978         connect( _fileAgeStatsWindow,     SIGNAL( locateFilesFromYear   ( QString, short ) ),
979                  _discoverActions,        SLOT  ( discoverFilesFromYear ( QString, short ) ) );
980 
981         connect( _fileAgeStatsWindow,     SIGNAL( locateFilesFromMonth  ( QString, short, short ) ),
982                  _discoverActions,        SLOT  ( discoverFilesFromMonth( QString, short, short ) ) );
983     }
984 
985     _fileAgeStatsWindow->populate( app()->selectedDirOrRoot() );
986     _fileAgeStatsWindow->show();
987 }
988 
989 
showFilesystems()990 void MainWindow::showFilesystems()
991 {
992     if ( ! _filesystemsWindow )
993     {
994 	// This deletes itself when the user closes it. The associated QPointer
995 	// keeps track of that and sets the pointer to 0 when it happens.
996 
997 	_filesystemsWindow = new FilesystemsWindow( this );
998 
999         connect( _filesystemsWindow, SIGNAL( readFilesystem( QString ) ),
1000                  this,               SLOT  ( openUrl       ( QString ) ) );
1001     }
1002 
1003     _filesystemsWindow->populate();
1004     _filesystemsWindow->show();
1005 }
1006 
1007 
showDirPermissionsWarning()1008 void MainWindow::showDirPermissionsWarning()
1009 {
1010     if ( _dirPermissionsWarning || ! _enableDirPermissionsWarning )
1011 	return;
1012 
1013     PanelMessage * msg = new PanelMessage( _ui->messagePanel );
1014     CHECK_NEW( msg );
1015 
1016     msg->setHeading( tr( "Some directories could not be read." ) );
1017     msg->setText( tr( "You might not have sufficient permissions." ) );
1018     msg->setIcon( QPixmap( ":/icons/lock-closed.png" ) );
1019 
1020     msg->connectDetailsLink( this, SLOT( showUnreadableDirs() ) );
1021 
1022     _ui->messagePanel->add( msg );
1023     _dirPermissionsWarning = msg;
1024     _enableDirPermissionsWarning = false;
1025 }
1026 
1027 
showUnreadableDirs()1028 void MainWindow::showUnreadableDirs()
1029 {
1030     UnreadableDirsWindow::populateSharedInstance( app()->dirTree()->root() );
1031 }
1032 
1033 
selectionChanged()1034 void MainWindow::selectionChanged()
1035 {
1036     showSummary();
1037     updateFileDetailsView();
1038 
1039     if ( _verboseSelection )
1040     {
1041 	logNewline();
1042 	app()->selectionModel()->dumpSelectedItems();
1043     }
1044 }
1045 
1046 
currentItemChanged(FileInfo * newCurrent,FileInfo * oldCurrent)1047 void MainWindow::currentItemChanged( FileInfo * newCurrent, FileInfo * oldCurrent )
1048 {
1049     showSummary();
1050 
1051     if ( ! oldCurrent )
1052 	updateFileDetailsView();
1053 
1054     if ( _verboseSelection )
1055     {
1056 	logDebug() << "new current: " << newCurrent << endl;
1057 	logDebug() << "old current: " << oldCurrent << endl;
1058 	app()->selectionModel()->dumpSelectedItems();
1059     }
1060 }
1061 
1062 
mousePressEvent(QMouseEvent * event)1063 void MainWindow::mousePressEvent( QMouseEvent * event )
1064 {
1065     if ( event )
1066     {
1067         QAction * action = 0;
1068 
1069         switch ( event->button() )
1070         {
1071             // Handle the back / forward buttons on the mouse to act like the
1072             // history back / forward buttons in the tool bar
1073 
1074             case Qt::BackButton:
1075                 // logDebug() << "BackButton" << endl;
1076                 action = _ui->actionGoBack;
1077                 break;
1078 
1079             case Qt::ForwardButton:
1080                 // logDebug() << "ForwardButton" << endl;
1081                 action = _ui->actionGoForward;
1082                 break;
1083 
1084             default:
1085                 QMainWindow::mousePressEvent( event );
1086                 break;
1087         }
1088 
1089         if ( action )
1090         {
1091             if ( action->isEnabled() )
1092                 action->trigger();
1093         }
1094     }
1095 }
1096 
1097 
1098 
1099 
1100 //---------------------------------------------------------------------------
1101 //			       Debugging Helpers
1102 //---------------------------------------------------------------------------
1103 
1104 
toggleVerboseSelection()1105 void MainWindow::toggleVerboseSelection()
1106 {
1107     // Verbose selection is toggled with Shift-F7
1108 
1109     _verboseSelection = _ui->actionVerboseSelection->isChecked();
1110 
1111     if ( app()->selectionModel() )
1112 	app()->selectionModel()->setVerbose( _verboseSelection );
1113 
1114     logInfo() << "Verbose selection is now " << ( _verboseSelection ? "on" : "off" )
1115 	      << ". Change this with Shift-F7." << endl;
1116 }
1117 
1118 
itemClicked(const QModelIndex & index)1119 void MainWindow::itemClicked( const QModelIndex & index )
1120 {
1121     if ( ! _verboseSelection )
1122 	return;
1123 
1124     if ( index.isValid() )
1125     {
1126 	FileInfo * item = static_cast<FileInfo *>( index.internalPointer() );
1127 
1128 	logDebug() << "Clicked row " << index.row()
1129 		   << " col " << index.column()
1130 		   << " (" << QDirStat::DataColumns::fromViewCol( index.column() ) << ")"
1131 		   << "\t" << item
1132 		   << endl;
1133 	// << " data(0): " << index.model()->data( index, 0 ).toString()
1134 	// logDebug() << "Ancestors: " << Debug::modelTreeAncestors( index ).join( " -> " ) << endl;
1135     }
1136     else
1137     {
1138 	logDebug() << "Invalid model index" << endl;
1139     }
1140 
1141     // app()->dirTreeModel()->dumpPersistentIndexList();
1142 }
1143 
1144 
1145 // For more MainWindow:: methods, See also:
1146 //
1147 //   - MainWindowHelp.cpp
1148 //   - MainWindowLayout.cpp
1149 //   - MainWindowMenus.cpp
1150 //   - MainWindowUnpkg.cpp
1151 
1152