diff --git a/src/mainWindow.cpp b/src/mainWindow.cpp index 9c82250..6cf80e9 100644 --- a/src/mainWindow.cpp +++ b/src/mainWindow.cpp @@ -1,552 +1,551 @@ /*********************************************************************** * Copyright 2003-2004 Max Howell * Copyright 2008-2009 Martin Sandsmark * Copyright 2017 Harald Sitter * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . ***********************************************************************/ #include "mainWindow.h" #include "historyAction.h" #include "Config.h" #include "define.h" #include "fileTree.h" #include "progressBox.h" #include "radialMap/widget.h" #include "scan.h" #include "settingsDialog.h" #include "summaryWidget.h" #include //std::exit() #include #include #include #include //for editToolbar dialog #include #include // upUrl #include #include //::start() #include #include #include #include //locationbar #include //setupActions() #include #include #include #include #include namespace Filelight { MainWindow::MainWindow() : KXmlGuiWindow() , m_histories(nullptr) , m_summary(nullptr) , m_map(nullptr) , m_started(false) { Config::read(); QScrollArea *scrollArea = new QScrollArea(this); scrollArea->setWidgetResizable(true); setCentralWidget(scrollArea); QWidget *partWidget = new QWidget(scrollArea); scrollArea->setWidget(partWidget); partWidget->setBackgroundRole(QPalette::Base); partWidget->setAutoFillBackground(true); m_layout = new QGridLayout(); partWidget->setLayout(m_layout); m_manager = new ScanManager(partWidget); m_map = new RadialMap::Widget(partWidget); m_layout->addWidget(m_map); // FIXME: drop stupid nullptr argument m_stateWidget = new ProgressBox(statusBar(), this, m_manager); m_layout->addWidget(m_stateWidget); m_stateWidget->hide(); m_numberOfFiles = new QLabel(); statusBar()->addPermanentWidget(m_numberOfFiles); KStandardAction::zoomIn(m_map, &RadialMap::Widget::zoomIn, actionCollection()); KStandardAction::zoomOut(m_map, &RadialMap::Widget::zoomOut, actionCollection()); KStandardAction::preferences(this, &MainWindow::configFilelight, actionCollection()); connect(m_map, &RadialMap::Widget::folderCreated, this, &MainWindow::completed); connect(m_map, &RadialMap::Widget::folderCreated, this, &MainWindow::mapChanged); connect(m_map, &RadialMap::Widget::activated, this, &MainWindow::updateURL); // TODO make better system connect(m_map, &RadialMap::Widget::giveMeTreeFor, this, &MainWindow::updateURL); connect(m_map, &RadialMap::Widget::giveMeTreeFor, this, &MainWindow::openUrl); connect(m_manager, &ScanManager::completed, this, &MainWindow::folderScanCompleted); connect(m_manager, &ScanManager::aboutToEmptyCache, m_map, &RadialMap::Widget::invalidate); setStandardToolBarMenuEnabled(true); setupActions(); createGUI(QStringLiteral("filelightui.rc")); stateChanged(QStringLiteral("scan_failed")); //bah! doesn't affect the parts' actions, should I add them to the actionCollection here? connect(this, &MainWindow::started, this, &MainWindow::scanStarted); connect(this, &MainWindow::completed, this, &MainWindow::scanCompleted); connect(this, &MainWindow::canceled, this, &MainWindow::scanFailed); connect(this, &MainWindow::canceled, m_histories, &HistoryCollection::stop); const KConfigGroup config = KSharedConfig::openConfig()->group("general"); m_combo->setHistoryItems(config.readPathEntry("comboHistory", QStringList())); setAutoSaveSettings(QStringLiteral("window")); QTimer::singleShot(0, this, &MainWindow::postInit); } void MainWindow::scan(const QUrl &u) { slotScanUrl(u); } void MainWindow::setupActions() //singleton function { KActionCollection *const ac = actionCollection(); m_combo = new KHistoryComboBox(this); m_combo->setCompletionObject(new KUrlCompletion(KUrlCompletion::DirCompletion)); m_combo->setAutoDeleteCompletionObject(true); m_combo->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed)); m_combo->setDuplicatesEnabled(false); KStandardAction::open(this, SLOT(slotScanFolder()), ac); KStandardAction::quit(this, SLOT(close()), ac); KStandardAction::up(this, SLOT(slotUp()), ac); KStandardAction::configureToolbars(this, SLOT(configToolbars()), ac); KStandardAction::keyBindings(this, SLOT(configKeys()), ac); QAction *action; action = ac->addAction(QStringLiteral("scan_home"), this, &MainWindow::slotScanHomeFolder); action->setText(i18n("Scan &Home Folder")); action->setIcon(QIcon::fromTheme(QStringLiteral("user-home"))); ac->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_Home)); action = ac->addAction(QStringLiteral("scan_root"), this, &MainWindow::slotScanRootFolder); action->setText(i18n("Scan &Root Folder")); action->setIcon(QIcon::fromTheme(QStringLiteral("folder-red"))); action = ac->addAction(QStringLiteral("scan_rescan"), this, &MainWindow::rescan); action->setText(i18n("Rescan")); action->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh"))); ac->setDefaultShortcut(action, QKeySequence::Refresh); action = ac->addAction(QStringLiteral("scan_stop"), this, &MainWindow::slotAbortScan); action->setText(i18n("Stop")); action->setIcon(QIcon::fromTheme(QStringLiteral("process-stop"))); ac->setDefaultShortcut(action, Qt::Key_Escape); action = ac->addAction(QStringLiteral("go"), m_combo, static_cast(&KHistoryComboBox::returnPressed)); action->setText(i18n("Go")); action->setIcon(QIcon::fromTheme(QStringLiteral("go-jump-locationbar"))); action = ac->addAction(QStringLiteral("scan_folder"), this, &MainWindow::slotScanFolder); action->setText(i18n("Scan Folder")); action->setIcon(QIcon::fromTheme(QStringLiteral("folder"))); QWidgetAction *locationAction = ac->add(QStringLiteral("location_bar"), nullptr, nullptr); locationAction->setText(i18n("Location Bar")); locationAction->setDefaultWidget(m_combo); m_recentScans = new KRecentFilesAction(i18n("&Recent Scans"), ac); m_recentScans->setMaxItems(8); m_histories = new HistoryCollection(ac, this); m_recentScans->loadEntries(KSharedConfig::openConfig()->group("general")); connect(m_recentScans, &KRecentFilesAction::urlSelected, this, &MainWindow::slotScanUrl); connect(m_combo, static_cast(&KHistoryComboBox::returnPressed), this, &MainWindow::slotComboScan); connect(m_histories, &HistoryCollection::activated, this, &MainWindow::slotScanUrl); } void MainWindow::closeEvent(QCloseEvent *event) { KConfigGroup config = KSharedConfig::openConfig()->group("general"); m_recentScans->saveEntries(config); config.writePathEntry("comboHistory", m_combo->historyItems()); config.sync(); KXmlGuiWindow::closeEvent(event); } void MainWindow::configToolbars() //slot { KEditToolBar dialog(factory(), this); if (dialog.exec()) //krazy:exclude=crashy { createGUI(QStringLiteral("filelightui.rc")); applyMainWindowSettings(KSharedConfig::openConfig()->group("window")); } } void MainWindow::configKeys() //slot { KShortcutsDialog::configure(actionCollection(), KShortcutsEditor::LetterShortcutsAllowed, this, true); } void MainWindow::slotScanFolder() { slotScanUrl(QFileDialog::getExistingDirectoryUrl(this, i18n("Select Folder to Scan"), url())); } void MainWindow::slotScanHomeFolder() { slotScanPath(QDir::homePath()); } void MainWindow::slotScanRootFolder() { slotScanPath(QDir::rootPath()); } void MainWindow::slotUp() { slotScanUrl(KIO::upUrl(url())); } void MainWindow::slotComboScan() { QString path = m_combo->lineEdit()->text(); QUrl url = QUrl::fromUserInput(path); if (url.isRelative()) path = QLatin1String("~/") + path; // KUrlCompletion completes relative to ~, not CWD path = KShell::tildeExpand(path); if (slotScanPath(path)) m_combo->addToHistory(path); } bool MainWindow::slotScanPath(const QString &path) { return slotScanUrl(QUrl::fromUserInput(path)); } bool MainWindow::slotScanUrl(const QUrl &url) { const QUrl oldUrl = this->url(); if (openUrl(url)) { m_histories->push(oldUrl); return true; } else return false; } void MainWindow::slotAbortScan() { if (closeUrl()) action("scan_stop")->setEnabled(false); } void MainWindow::scanStarted() { stateChanged(QStringLiteral("scan_started")); m_combo->clearFocus(); } void MainWindow::scanFailed() { stateChanged(QStringLiteral("scan_failed")); action("go_up")->setStatusTip(QString()); action("go_up")->setToolTip(QString()); m_combo->lineEdit()->clear(); } void MainWindow::scanCompleted() { const QUrl url = this->url(); stateChanged(QStringLiteral("scan_complete")); m_combo->lineEdit()->setText(prettyUrl()); if (url.toLocalFile() == QLatin1String("/")) { action("go_up")->setEnabled(false); action("go_up")->setStatusTip(QString()); action("go_up")->setToolTip(QString()); } else { action("go_up")->setStatusTip(KIO::upUrl(url).path()); action("go_up")->setToolTip(KIO::upUrl(url).path()); } m_recentScans->addUrl(url); //FIXME doesn't set the tick } void MainWindow::urlAboutToChange() { //called when part's URL is about to change internally //the part will then create the Map and emit completed() m_histories->push(url()); } /********************************************** SESSION MANAGEMENT **********************************************/ void MainWindow::saveProperties(KConfigGroup &configgroup) //virtual { if (!m_histories) return; m_histories->save(configgroup); configgroup.writeEntry("currentMap", url().path()); } void MainWindow::readProperties(const KConfigGroup &configgroup) //virtual { m_histories->restore(configgroup); slotScanPath(configgroup.group("General").readEntry("currentMap", QString())); } void MainWindow::postInit() { if (url().isEmpty()) //if url is not empty openUrl() has been called immediately after ctor, which happens { m_map->hide(); showSummary(); //FIXME KXMLGUI is b0rked, it should allow us to set this //BEFORE createGUI is called but it doesn't stateChanged(QStringLiteral("scan_failed")); } } bool MainWindow::openUrl(const QUrl &u) { //TODO everyone hates dialogs, instead render the text in big fonts on the Map //TODO should have an empty QUrl until scan is confirmed successful //TODO probably should set caption to QString::null while map is unusable #define KMSG(s) KMessageBox::information(widget(), s) QUrl uri = u.adjusted(QUrl::NormalizePathSegments); const QString localPath = uri.toLocalFile(); const bool isLocal = uri.isLocalFile(); if (uri.isEmpty()) { //do nothing, chances are the user accidentally pressed ENTER } else if (!uri.isValid()) { KMSG(i18n("The entered URL cannot be parsed; it is invalid.")); } else if (isLocal && !QDir::isAbsolutePath(localPath)) { KMSG(i18n("Filelight only accepts absolute paths, eg. /%1", localPath)); } else if (isLocal && !QDir(localPath).exists()) { KMSG(i18n("Folder not found: %1", localPath)); } else if (isLocal && !QDir(localPath).isReadable()) { KMSG(i18n("Unable to enter: %1\nYou do not have access rights to this location.", localPath)); } else { //we don't want to be using the summary screen anymore if (m_summary != nullptr) m_summary->hide(); m_stateWidget->show(); m_layout->addWidget(m_stateWidget); return start(uri); } return false; } bool MainWindow::closeUrl() { if (m_manager->abort()) statusBar()->showMessage(i18n("Aborting Scan...")); m_map->hide(); m_stateWidget->hide(); showSummary(); return true; } QString MainWindow::prettyUrl() const { return url().isLocalFile() ? QDir::toNativeSeparators(url().toLocalFile()) : url().toString(); } void MainWindow::updateURL(const QUrl &u) { if (m_manager->running()) m_manager->abort(); if (u == url()) - m_manager->emptyCache(); //same as rescan() + m_manager->invalidateCacheFor(u); //same as rescan() //do this last, or it breaks Konqi location bar setUrl(u); } QUrl MainWindow::url() const { return m_url; } void MainWindow::setUrl(const QUrl &url) { m_url = url; } void MainWindow::configFilelight() { SettingsDialog *dialog = new SettingsDialog(widget()); connect(dialog, &SettingsDialog::canvasIsDirty, m_map, &RadialMap::Widget::refresh); connect(dialog, &SettingsDialog::mapIsInvalid, m_manager, &ScanManager::emptyCache); dialog->show(); //deletes itself } bool MainWindow::start(const QUrl &url) { if (!m_started) { connect(m_map, &RadialMap::Widget::mouseHover, [&](const QString &msg) { statusBar()->showMessage(msg); }); connect(m_map, &RadialMap::Widget::folderCreated, statusBar(), &QStatusBar::clearMessage); m_started = true; } if (m_manager->running()) m_manager->abort(); m_numberOfFiles->setText(QString()); if (m_manager->start(url)) { setUrl(url); const QString s = i18n("Scanning: %1", prettyUrl()); stateChanged(QStringLiteral("scan_started")); emit started(); //as a MainWindow, we have to do this emit setWindowCaption(s); statusBar()->showMessage(s); m_map->hide(); m_map->invalidate(); //to maintain ui consistency return true; } return false; } void MainWindow::rescan() { if (m_summary && !m_summary->isHidden()) { delete m_summary; m_summary = nullptr; showSummary(); return; } - //FIXME we have to empty the cache because otherwise rescan picks up the old tree.. - m_manager->emptyCache(); //causes canvas to invalidate + m_manager->invalidateCacheFor(url()); //causes canvas to invalidate m_map->hide(); m_stateWidget->show(); start(url()); } void MainWindow::folderScanCompleted(Folder *tree) { if (tree) { statusBar()->showMessage(i18n("Scan completed, generating map...")); m_stateWidget->hide(); m_map->show(); m_map->create(tree); stateChanged(QStringLiteral("scan_complete")); } else { stateChanged(QStringLiteral("scan_failed")); emit canceled(i18n("Scan failed: %1", prettyUrl())); emit setWindowCaption(QString()); statusBar()->clearMessage(); m_map->hide(); m_stateWidget->hide(); showSummary(); setUrl(QUrl()); } } void MainWindow::mapChanged(const Folder *tree) { //IMPORTANT -> url() has already been set emit setWindowCaption(prettyUrl()); const int fileCount = tree->children(); const QString text = (fileCount == 0) ? i18n("No files.") : i18np("1 file", "%1 files",fileCount); m_numberOfFiles->setText(text); } void MainWindow::showSummary() { if (m_summary == nullptr) { m_summary = new SummaryWidget(widget()); m_summary->setObjectName(QStringLiteral("summaryWidget")); connect(m_summary, &SummaryWidget::activated, this, &MainWindow::openUrl); m_summary->show(); m_layout->addWidget(m_summary); } else m_summary->show(); } } //namespace Filelight diff --git a/src/scan.cpp b/src/scan.cpp index 5c128a4..45c9860 100644 --- a/src/scan.cpp +++ b/src/scan.cpp @@ -1,229 +1,286 @@ /*********************************************************************** * Copyright 2003-2004 Max Howell * Copyright 2008-2009 Martin Sandsmark * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . ***********************************************************************/ #include "scan.h" #include "remoteLister.h" #include "fileTree.h" #include "localLister.h" #include "filelight_debug.h" #include #include #include #include namespace Filelight { ScanManager::ScanManager(QObject *parent) : QObject(parent) , m_abort(false) , m_files(0) , m_mutex() , m_thread(nullptr) { Filelight::LocalLister::readMounts(); connect(this, &ScanManager::branchCacheHit, this, &ScanManager::foundCached, Qt::QueuedConnection); } ScanManager::~ScanManager() { if (m_thread) { qCDebug(FILELIGHT_LOG) << "Attempting to abort scan operation..."; m_abort = true; m_thread->wait(); } qDeleteAll(m_cache); //RemoteListers are QObjects and get automatically deleted } bool ScanManager::running() const { return m_thread && m_thread->isRunning(); } bool ScanManager::start(const QUrl &url) { QMutexLocker locker(&m_mutex); // The m_mutex gets released once locker is destroyed (goes out of scope). //url is guaranteed clean and safe qCDebug(FILELIGHT_LOG) << "Scan requested for: " << url; if (running()) { qCWarning(FILELIGHT_LOG) << "Tried to launch two concurrent scans, aborting old one..."; abort(); } m_files = 0; m_abort = false; if (!url.isLocalFile()) { QGuiApplication::changeOverrideCursor(Qt::BusyCursor); //will start listing straight away Filelight::RemoteLister *remoteLister = new Filelight::RemoteLister(url, (QWidget*)parent(), this); connect(remoteLister, &Filelight::RemoteLister::branchCompleted, this, &ScanManager::cacheTree, Qt::QueuedConnection); remoteLister->setParent(this); remoteLister->setObjectName(QStringLiteral( "remote_lister" )); remoteLister->openUrl(url); return true; } QString path = url.toLocalFile(); if (!path.endsWith(QDir::separator())) path += QDir::separator(); QList *trees = new QList; /* CHECK CACHE * user wants: /usr/local/ * cached: /usr/ * * user wants: /usr/ * cached: /usr/local/, /usr/include/ */ QMutableListIterator it(m_cache); while (it.hasNext()) { Folder *folder = it.next(); QString cachePath = folder->decodedName(); if (path.startsWith(cachePath)) { //then whole tree already scanned //find a pointer to the requested branch qCDebug(FILELIGHT_LOG) << "Cache-(a)hit: " << cachePath; QVector split = path.midRef(cachePath.length()).split(QLatin1Char('/')); Folder *d = folder; while (!split.isEmpty() && d != nullptr) { //if NULL we have got lost so abort!! if (split.first().isEmpty()) { //found the dir break; } QString s = split.first() % QLatin1Char('/'); // % is the string concatenation operator for QStringBuilder QListIterator it(d->files); d = nullptr; while (it.hasNext()) { File *subfolder = it.next(); if (s == subfolder->decodedName()) { d = (Folder*)subfolder; break; } } split.pop_front(); } if (d) { delete trees; //we found a completed tree, thus no need to scan qCDebug(FILELIGHT_LOG) << "Found cache-handle, generating map.."; emit branchCacheHit(d); return true; } else { //something went wrong, we couldn't find the folder we were expecting qCWarning(FILELIGHT_LOG) << "Didn't find " << path << " in the cache!\n"; it.remove(); emit aboutToEmptyCache(); delete folder; break; //do a full scan } } else if (cachePath.startsWith(path)) { //then part of the requested tree is already scanned qCDebug(FILELIGHT_LOG) << "Cache-(b)hit: " << cachePath; it.remove(); trees->append(folder); } } QGuiApplication::changeOverrideCursor(QCursor(Qt::BusyCursor)); //starts listing by itself m_thread = new Filelight::LocalLister(path, trees, this); connect(m_thread, &LocalLister::branchCompleted, this, &ScanManager::cacheTree, Qt::QueuedConnection); m_thread->start(); return true; } bool ScanManager::abort() { m_abort = true; delete findChild(QStringLiteral( "remote_lister" )); return m_thread && m_thread->wait(); } +void ScanManager::invalidateCacheFor(const QUrl &url) +{ + m_abort = true; + + if (m_thread && m_thread->isRunning()) { + m_thread->wait(); + } + + if (!url.isLocalFile()) { + qWarning() << "Remote cache clearing not implemented"; + return; + } + + QString path = url.toLocalFile(); + if (!path.endsWith(QDir::separator())) path += QDir::separator(); + + emit aboutToEmptyCache(); + + QMutableListIterator it(m_cache); + while (it.hasNext()) { + Folder *folder = it.next(); + QString cachePath = folder->decodedName(); + + if (!path.startsWith(cachePath)) { + continue; + } + + QVector split = path.midRef(cachePath.length()).split(QLatin1Char('/')); + Folder *d = folder; + + while (!split.isEmpty() && d != nullptr) { //if NULL we have got lost so abort!! + if (split.first().isEmpty()) { //found the dir + break; + } + QString s = split.first() % QLatin1Char('/'); // % is the string concatenation operator for QStringBuilder + + QListIterator it(d->files); + d = nullptr; + while (it.hasNext()) { + File *subfolder = it.next(); + if (s == subfolder->decodedName()) { + d = (Folder*)subfolder; + break; + } + } + + split.pop_front(); + } + + if (!d || !d->parent()) { + continue; + } + d->parent()->remove(d); + } +} + void ScanManager::emptyCache() { m_abort = true; if (m_thread && m_thread->isRunning()) { m_thread->wait(); } emit aboutToEmptyCache(); qDeleteAll(m_cache); m_cache.clear(); } void ScanManager::cacheTree(Folder *tree) { QMutexLocker locker(&m_mutex); // This gets released once it is destroyed. if (m_thread) { qCDebug(FILELIGHT_LOG) << "Waiting for thread to terminate ..."; m_thread->wait(); qCDebug(FILELIGHT_LOG) << "Thread terminated!"; delete m_thread; //note the lister deletes itself m_thread = nullptr; } emit completed(tree); if (tree) { //we don't cache foreign stuff //we don't recache stuff (thus only type 1000 events) + //we always just have one tree cached, so we really don't need a list.. m_cache.append(tree); } else { //scan failed qDeleteAll(m_cache); m_cache.clear(); } QGuiApplication::restoreOverrideCursor(); } void ScanManager::foundCached(Folder *tree) { emit completed(tree); QGuiApplication::restoreOverrideCursor(); } } diff --git a/src/scan.h b/src/scan.h index 3b1c8af..4af468e 100644 --- a/src/scan.h +++ b/src/scan.h @@ -1,75 +1,77 @@ /*********************************************************************** * Copyright 2003-2004 Max Howell * Copyright 2008-2009 Martin Sandsmark * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . ***********************************************************************/ #ifndef SCAN_H #define SCAN_H #include #include #include class Folder; namespace Filelight { class LocalLister; class ScanManager : public QObject { Q_OBJECT friend class LocalLister; friend class RemoteLister; public: explicit ScanManager(QObject *parent); ~ScanManager() override; bool start(const QUrl& path); bool running() const; uint files() const { return m_files; } + void invalidateCacheFor(const QUrl &url); + public Q_SLOTS: bool abort(); void emptyCache(); void cacheTree(Folder*); void foundCached(Folder*); Q_SIGNALS: void completed(Folder*); void aboutToEmptyCache(); void branchCacheHit(Folder* tree); private: bool m_abort; uint m_files; QMutex m_mutex; LocalLister *m_thread; QList m_cache; }; } #endif