Commit e6ea3ab4 authored by Méven Car's avatar Méven Car
Browse files

[Details mode] Allow to fill the column size of directories with actual size

Summary:
Allow to compute the recursive size of directories to fill the details view size column.
A setting allow to set a limit to the recursive level, allowing the user to have some power over the setting.

When sorting by size and the feature is on, we get progressive ordering as the directory size are gathered.

KDirectoryContentsCounter uses a cache internally to keep results so that it can display directory size faster, but counts the dir size of directories each time it is asked to count the size a directory nevertheless and when the size has changed, it is updated.
KDirectoryContentsCounter uses one worker per instance only, meaning one process per view makes the disk spin.

FIXED-IN: 20.08
BUG: 190580
BUG: 158090

Test Plan:
With some recursion allowed:
{F8267580}

Without any recursion allowed (default):
{F8267581}

Reviewers: elvisangelaccio, ngraham, #dolphin

Reviewed By: elvisangelaccio, ngraham, #dolphin

Subscribers: feverfew, anthonyfieroni, iasensio, kfm-devel

Tags: #dolphin

Differential Revision: https://phabricator.kde.org/D25335
parent d34559d1
......@@ -29,6 +29,8 @@
#include "trash/dolphintrash.h"
#include "views/viewmodecontroller.h"
#include "views/viewproperties.h"
#include "dolphin_detailsmodesettings.h"
#include "views/dolphinview.h"
#ifdef HAVE_KACTIVITIES
#include <KActivities/ResourceInstance>
......@@ -249,6 +251,12 @@ DolphinViewContainer::DolphinViewContainer(const QUrl& url, QWidget* parent) :
setSearchModeEnabled(isSearchUrl(url));
connect(DetailsModeSettings::self(), &KCoreConfigSkeleton::configChanged, this, [=]() {
if (view()->mode() == DolphinView::Mode::DetailsView) {
view()->reload();
}
});
// Initialize kactivities resource instance
#ifdef HAVE_KACTIVITIES
......
......@@ -21,6 +21,8 @@
#include "kfileitemmodel.h"
#include "kitemlistview.h"
#include "dolphin_detailsmodesettings.h"
#include <KFormat>
#include <KLocalizedString>
......@@ -64,14 +66,24 @@ QString KFileItemListWidgetInformant::roleText(const QByteArray& role,
if (role == "size") {
if (values.value("isDir").toBool()) {
// The item represents a directory. Show the number of sub directories
// instead of the file size of the directory.
// The item represents a directory.
if (!roleValue.isNull()) {
const int count = roleValue.toInt();
const int count = values.value("count").toInt();
if (count < 0) {
text = i18nc("@item:intable", "Unknown");
} else {
text = i18ncp("@item:intable", "%1 item", "%1 items", count);
if (DetailsModeSettings::directorySizeCount()) {
// Show the number of sub directories instead of the file size of the directory.
text = i18ncp("@item:intable", "%1 item", "%1 items", count);
} else {
// if we have directory size available
if (roleValue == -1) {
text = i18nc("@item:intable", "Unknown");
} else {
const KIO::filesize_t size = roleValue.value<KIO::filesize_t>();
text = KFormat().formatByteSize(size);
}
}
}
}
} else {
......
......@@ -22,6 +22,7 @@
#include "kfileitemmodel.h"
#include "dolphin_generalsettings.h"
#include "dolphin_detailsmodesettings.h"
#include "dolphindebug.h"
#include "private/kfileitemmodeldirlister.h"
#include "private/kfileitemmodelsortalgorithm.h"
......@@ -1767,8 +1768,15 @@ int KFileItemModel::sortRoleCompare(const ItemData* a, const ItemData* b, const
// See "if (m_sortFoldersFirst || m_sortRole == SizeRole)" in KFileItemModel::lessThan():
Q_ASSERT(itemB.isDir());
const QVariant valueA = a->values.value("size");
const QVariant valueB = b->values.value("size");
QVariant valueA, valueB;
if (DetailsModeSettings::directorySizeCount()) {
// use dir size then
valueA = a->values.value("size");
valueB = b->values.value("size");
} else {
valueA = a->values.value("count");
valueB = b->values.value("count");
}
if (valueA.isNull() && valueB.isNull()) {
result = 0;
} else if (valueA.isNull()) {
......@@ -1776,7 +1784,11 @@ int KFileItemModel::sortRoleCompare(const ItemData* a, const ItemData* b, const
} else if (valueB.isNull()) {
result = +1;
} else {
result = valueA.toInt() - valueB.toInt();
if (valueA < valueB) {
return -1;
} else {
return +1;
}
}
} else {
// See "if (m_sortFoldersFirst || m_sortRole == SizeRole)" in KFileItemModel::lessThan():
......
......@@ -44,7 +44,6 @@
#include <QElapsedTimer>
#include <QTimer>
// #define KFILEITEMMODELROLESUPDATER_DEBUG
namespace {
......@@ -108,9 +107,9 @@ KFileItemModelRolesUpdater::KFileItemModelRolesUpdater(KFileItemModel* model, QO
this, &KFileItemModelRolesUpdater::slotSortRoleChanged);
// Use a timer to prevent that each call of slotItemsChanged() results in a synchronous
// resolving of the roles. Postpone the resolving until no update has been done for 1 second.
// resolving of the roles. Postpone the resolving until no update has been done for 100 ms.
m_recentlyChangedItemsTimer = new QTimer(this);
m_recentlyChangedItemsTimer->setInterval(1000);
m_recentlyChangedItemsTimer->setInterval(100);
m_recentlyChangedItemsTimer->setSingleShot(true);
connect(m_recentlyChangedItemsTimer, &QTimer::timeout, this, &KFileItemModelRolesUpdater::resolveRecentlyChangedItems);
......@@ -750,7 +749,7 @@ void KFileItemModelRolesUpdater::applyChangedBalooRolesForItem(const KFileItem &
#endif
}
void KFileItemModelRolesUpdater::slotDirectoryContentsCountReceived(const QString& path, int count)
void KFileItemModelRolesUpdater::slotDirectoryContentsCountReceived(const QString& path, int count, long size)
{
const bool getSizeRole = m_roles.contains("size");
const bool getIsExpandableRole = m_roles.contains("isExpandable");
......@@ -761,17 +760,16 @@ void KFileItemModelRolesUpdater::slotDirectoryContentsCountReceived(const QStrin
QHash<QByteArray, QVariant> data;
if (getSizeRole) {
data.insert("size", count);
data.insert("count", count);
if (size != -1) {
data.insert("size", QVariant::fromValue(size));
}
}
if (getIsExpandableRole) {
data.insert("isExpandable", count > 0);
}
disconnect(m_model, &KFileItemModel::itemsChanged,
this, &KFileItemModelRolesUpdater::slotItemsChanged);
m_model->setData(index, data);
connect(m_model, &KFileItemModel::itemsChanged,
this, &KFileItemModelRolesUpdater::slotItemsChanged);
}
}
}
......@@ -997,7 +995,7 @@ void KFileItemModelRolesUpdater::applySortRole(int index)
data.insert("type", item.mimeComment());
} else if (m_model->sortRole() == "size" && item.isLocalFile() && item.isDir()) {
const QString path = item.localPath();
data.insert("size", m_directoryContentsCounter->countDirectoryContentsSynchronously(path));
m_directoryContentsCounter->scanDirectory(path);
} else {
// Probably the sort role is a baloo role - just determine all roles.
data = rolesData(item);
......@@ -1070,7 +1068,7 @@ QHash<QByteArray, QVariant> KFileItemModelRolesUpdater::rolesData(const KFileIte
// Tell m_directoryContentsCounter that we want to count the items
// inside the directory. The result will be received in slotDirectoryContentsCountReceived.
const QString path = item.localPath();
m_directoryContentsCounter->addDirectory(path);
m_directoryContentsCounter->scanDirectory(path);
} else if (getSizeRole) {
data.insert("size", -1); // -1 indicates an unknown number of items
}
......
......@@ -212,7 +212,7 @@ private slots:
void applyChangedBalooRoles(const QString& file);
void applyChangedBalooRolesForItem(const KFileItem& file);
void slotDirectoryContentsCountReceived(const QString& path, int count);
void slotDirectoryContentsCountReceived(const QString& path, int count, long size);
private:
/**
......
......@@ -24,8 +24,14 @@
#include <KDirWatch>
#include <QFileInfo>
#include <QDir>
#include <QThread>
namespace {
/// cache of directory counting result
static QHash<QString, QPair<int, long>> *s_cache;
}
KDirectoryContentsCounter::KDirectoryContentsCounter(KFileItemModel* model, QObject* parent) :
QObject(parent),
m_model(model),
......@@ -43,9 +49,12 @@ KDirectoryContentsCounter::KDirectoryContentsCounter(KFileItemModel* model, QObj
m_workerThread->start();
}
if (s_cache == nullptr) {
s_cache = new QHash<QString, QPair<int, long>>();
}
m_worker = new KDirectoryContentsCounterWorker();
m_worker->moveToThread(m_workerThread);
++m_workersCount;
connect(this, &KDirectoryContentsCounter::requestDirectoryContentsCount,
m_worker, &KDirectoryContentsCounterWorker::countDirectoryContents);
......@@ -58,9 +67,7 @@ KDirectoryContentsCounter::KDirectoryContentsCounter(KFileItemModel* model, QObj
KDirectoryContentsCounter::~KDirectoryContentsCounter()
{
--m_workersCount;
if (m_workersCount > 0) {
if (m_workerThread->isRunning()) {
// The worker thread will continue running. It could even be running
// a method of m_worker at the moment, so we delete it using
// deleteLater() to prevent a crash.
......@@ -79,38 +86,17 @@ KDirectoryContentsCounter::~KDirectoryContentsCounter()
}
}
void KDirectoryContentsCounter::addDirectory(const QString& path)
void KDirectoryContentsCounter::scanDirectory(const QString& path)
{
startWorker(path);
}
int KDirectoryContentsCounter::countDirectoryContentsSynchronously(const QString& path)
{
const QString resolvedPath = QFileInfo(path).canonicalFilePath();
if (!m_dirWatcher->contains(resolvedPath)) {
m_dirWatcher->addDir(resolvedPath);
m_watchedDirs.insert(resolvedPath);
}
KDirectoryContentsCounterWorker::Options options;
if (m_model->showHiddenFiles()) {
options |= KDirectoryContentsCounterWorker::CountHiddenFiles;
}
if (m_model->showDirectoriesOnly()) {
options |= KDirectoryContentsCounterWorker::CountDirectoriesOnly;
}
return KDirectoryContentsCounterWorker::subItemsCount(path, options);
}
void KDirectoryContentsCounter::slotResult(const QString& path, int count)
void KDirectoryContentsCounter::slotResult(const QString& path, int count, long size)
{
m_workerIsBusy = false;
const QString resolvedPath = QFileInfo(path).canonicalFilePath();
const QFileInfo info = QFileInfo(path);
const QString resolvedPath = info.canonicalFilePath();
if (!m_dirWatcher->contains(resolvedPath)) {
m_dirWatcher->addDir(resolvedPath);
......@@ -121,7 +107,22 @@ void KDirectoryContentsCounter::slotResult(const QString& path, int count)
startWorker(m_queue.dequeue());
}
emit result(path, count);
if (s_cache->contains(resolvedPath)) {
const auto pair = s_cache->value(resolvedPath);
if (pair.first == count && pair.second == size) {
// no change no need to send another result event
return;
}
}
if (info.dir().path() == m_model->rootItem().url().path()) {
// update cache or overwrite value
// when path is a direct children of the current model root
s_cache->insert(resolvedPath, QPair<int, long>(count, size));
}
// sends the results
emit result(resolvedPath, count, size);
}
void KDirectoryContentsCounter::slotDirWatchDirty(const QString& path)
......@@ -146,7 +147,7 @@ void KDirectoryContentsCounter::slotItemsRemoved()
if (!m_watchedDirs.isEmpty()) {
// Don't let KDirWatch watch for removed items
if (allItemsRemoved) {
foreach (const QString& path, m_watchedDirs) {
for (const QString& path : qAsConst(m_watchedDirs)) {
m_dirWatcher->removeDir(path);
}
m_watchedDirs.clear();
......@@ -166,6 +167,13 @@ void KDirectoryContentsCounter::slotItemsRemoved()
void KDirectoryContentsCounter::startWorker(const QString& path)
{
if (s_cache->contains(path)) {
// fast path when in cache
// will be updated later if result has changed
const auto pair = s_cache->value(path);
emit result(path, pair.first, pair.second);
}
if (m_workerIsBusy) {
m_queue.enqueue(path);
} else {
......@@ -185,4 +193,3 @@ void KDirectoryContentsCounter::startWorker(const QString& path)
}
QThread* KDirectoryContentsCounter::m_workerThread = nullptr;
int KDirectoryContentsCounter::m_workersCount = 0;
......@@ -25,6 +25,7 @@
#include <QQueue>
#include <QSet>
#include <QHash>
class KDirWatch;
class KFileItemModel;
......@@ -45,28 +46,23 @@ public:
*
* The directory \a path is watched for changes, and the signal is emitted
* again if a change occurs.
*/
void addDirectory(const QString& path);
/**
* In contrast to \a addDirectory, this function counts the items inside
* the directory \a path synchronously and returns the result.
*
* The directory is watched for changes, and the signal \a result is
* emitted if a change occurs.
* Uses a cache internally to speed up first result,
* but emit again result when the cache was updated
*/
int countDirectoryContentsSynchronously(const QString& path);
void scanDirectory(const QString& path);
signals:
/**
* Signals that the directory \a path contains \a count items.
* Signals that the directory \a path contains \a count items of size \a
* Size calculation depends on parameter DetailsModeSettings::recursiveDirectorySizeLimit
*/
void result(const QString& path, int count);
void result(const QString& path, int count, long size);
void requestDirectoryContentsCount(const QString& path, KDirectoryContentsCounterWorker::Options options);
private slots:
void slotResult(const QString& path, int count);
void slotResult(const QString& path, int count, long size);
void slotDirWatchDirty(const QString& path);
void slotItemsRemoved();
......@@ -79,7 +75,6 @@ private:
QQueue<QString> m_queue;
static QThread* m_workerThread;
static int m_workersCount;
KDirectoryContentsCounterWorker* m_worker;
bool m_workerIsBusy;
......
......@@ -22,44 +22,33 @@
// Required includes for subItemsCount():
#ifdef Q_OS_WIN
#include <QDir>
#include <QDir>
#else
#include <QFile>
#include <qplatformdefs.h>
#include <QFile>
#include <qplatformdefs.h>
#endif
#include "dolphin_detailsmodesettings.h"
KDirectoryContentsCounterWorker::KDirectoryContentsCounterWorker(QObject* parent) :
QObject(parent)
{
qRegisterMetaType<KDirectoryContentsCounterWorker::Options>();
}
int KDirectoryContentsCounterWorker::subItemsCount(const QString& path, Options options)
KDirectoryContentsCounterWorker::CountResult walkDir(const QString &dirPath,
const bool countHiddenFiles,
const bool countDirectoriesOnly,
QT_DIRENT *dirEntry,
const uint allowedRecursiveLevel)
{
const bool countHiddenFiles = options & CountHiddenFiles;
const bool countDirectoriesOnly = options & CountDirectoriesOnly;
#ifdef Q_OS_WIN
QDir dir(path);
QDir::Filters filters = QDir::NoDotAndDotDot | QDir::System;
if (countHiddenFiles) {
filters |= QDir::Hidden;
}
if (countDirectoriesOnly) {
filters |= QDir::Dirs;
} else {
filters |= QDir::AllEntries;
}
return dir.entryList(filters).count();
#else
// Taken from kio/src/widgets/kdirmodel.cpp
// Copyright (C) 2006 David Faure <faure@kde.org>
int count = -1;
auto dir = QT_OPENDIR(QFile::encodeName(path));
long size = -1;
auto dir = QT_OPENDIR(QFile::encodeName(dirPath));
if (dir) {
count = 0;
QT_DIRENT *dirEntry = nullptr;
QT_STATBUF buf;
while ((dirEntry = QT_READDIR(dir))) {
if (dirEntry->d_name[0] == '.') {
if (dirEntry->d_name[1] == '\0' || !countHiddenFiles) {
......@@ -76,20 +65,69 @@ int KDirectoryContentsCounterWorker::subItemsCount(const QString& path, Options
// as directory instead of trying to do an expensive stat()
// (see bugs 292642 and 299997).
const bool countEntry = !countDirectoriesOnly ||
dirEntry->d_type == DT_DIR ||
dirEntry->d_type == DT_LNK ||
dirEntry->d_type == DT_UNKNOWN;
dirEntry->d_type == DT_DIR ||
dirEntry->d_type == DT_LNK ||
dirEntry->d_type == DT_UNKNOWN;
if (countEntry) {
++count;
}
if (allowedRecursiveLevel > 0) {
bool linkFound = false;
QString nameBuf = QStringLiteral("%1/%2").arg(dirPath, dirEntry->d_name);
if (dirEntry->d_type == DT_REG || dirEntry->d_type == DT_LNK) {
if (QT_STAT(nameBuf.toLocal8Bit(), &buf) == 0) {
if (S_ISDIR(buf.st_mode)) {
// was a dir link, recurse
linkFound = true;
}
size += buf.st_size;
}
}
if (dirEntry->d_type == DT_DIR || linkFound) {
// recursion for dirs and dir links
size += walkDir(nameBuf, countHiddenFiles, countDirectoriesOnly, dirEntry, allowedRecursiveLevel - 1).size;
}
}
}
QT_CLOSEDIR(dir);
}
return count;
return KDirectoryContentsCounterWorker::CountResult{count, size};
}
KDirectoryContentsCounterWorker::CountResult KDirectoryContentsCounterWorker::subItemsCount(const QString& path, Options options)
{
const bool countHiddenFiles = options & CountHiddenFiles;
const bool countDirectoriesOnly = options & CountDirectoriesOnly;
#ifdef Q_OS_WIN
QDir dir(path);
QDir::Filters filters = QDir::NoDotAndDotDot | QDir::System;
if (countHiddenFiles) {
filters |= QDir::Hidden;
}
if (countDirectoriesOnly) {
filters |= QDir::Dirs;
} else {
filters |= QDir::AllEntries;
}
return {dir.entryList(filters).count(), 0};
#else
const uint maxRecursiveLevel = DetailsModeSettings::directorySizeCount() ? 1 : DetailsModeSettings::recursiveDirectorySizeLimit();
QT_DIRENT *dirEntry = nullptr;
auto res = walkDir(QFile::encodeName(path), countHiddenFiles, countDirectoriesOnly, dirEntry, maxRecursiveLevel);
return res;
#endif
}
void KDirectoryContentsCounterWorker::countDirectoryContents(const QString& path, Options options)
{
emit result(path, subItemsCount(path, options));
auto res = subItemsCount(path, options);
emit result(path, res.count, res.size);
}
......@@ -37,6 +37,14 @@ public:
};
Q_DECLARE_FLAGS(Options, Option)
struct CountResult {
/// number of elements in the directory
int count;
/// Recursive sum of the size of the directory content files and folders
/// Calculation depends on DetailsModeSettings::recursiveDirectorySizeLimit
long size;
};
explicit KDirectoryContentsCounterWorker(QObject* parent = nullptr);
/**
......@@ -45,13 +53,13 @@ public:
*
* @return The number of items.
*/
static int subItemsCount(const QString& path, Options options);
static CountResult subItemsCount(const QString& path, Options options);
signals:
/**
* Signals that the directory \a path contains \a count items.
* Signals that the directory \a path contains \a count items and optionally the size of its content.
*/
void result(const QString& path, int count);
void result(const QString& path, int count, long size);
public slots:
/**
......
......@@ -44,5 +44,13 @@
<label>Expandable folders</label>
<default>true</default>
</entry>
<entry name="DirectorySizeCount" type="Bool">
<label>Whether or not content count is use as directory size</label>
<default>true</default>
</entry>
<entry name="RecursiveDirectorySizeLimit" type="UInt">
<label>Recursive directory size limit</label>
<default>10</default>
</entry>
</group>
</kcfg>
......@@ -33,6 +33,10 @@
#include <QComboBox>
#include <QHelpEvent>
#include <QFormLayout>
#include <QSpinBox>
#include <QRadioButton>
#include <QButtonGroup>
#include <QLabel>
ViewSettingsTab::ViewSettingsTab(Mode mode, QWidget* parent) :
QWidget(parent),
......@@ -42,11 +46,11 @@ ViewSettingsTab::ViewSettingsTab(Mode mode, QWidget* parent) :
m_fontRequester(nullptr),
m_widthBox(nullptr),
m_maxLinesBox(nullptr),
m_expandableFolders(nullptr)
m_expandableFolders(nullptr),
m_recursiveDirectorySizeLimit(nullptr)
{
QFormLayout* topLayout = new QFormLayout(this);
// Create "Icon Size" section
const int minRange = ZoomLevelInfo::minimumLevel();
const int maxRange = ZoomLevelInfo::maximumLevel();
......@@ -75,7 +79,6 @@ ViewSettingsTab::ViewSettingsTab(Mode mode, QWidget* parent) :
m_fontRequester = new DolphinFontRequester(this);
topLayout->addRow(i18nc("@label:listbox", "Label font:"), m_fontRequester);
switch (m_mode) {
case IconsMode: {
m_widthBox = new QComboBox();
......@@ -107,8 +110,30 @@ ViewSettingsTab::ViewSettingsTab(Mode mode, QWidget* parent) :
case DetailsMode:
m_expandableFolders = new QCheckBox(i18nc("@option:check", "Expandable"));
topLayout->addRow(i18nc("@label:checkbox", "Folders:"), m_expandableFolders);
break;
default:
#ifndef Q_OS_WIN
// Sorting properties
m_numberOfItems = new QRadioButton(i18nc("option:radio", "Number of items"));
m_sizeOfContents = new QRadioButton(i18nc("option:radio", "Size of contents, up to "));
QButtonGroup* sortingModeGroup = new QButtonGroup(this);
sortingModeGroup->addButton(m_numberOfItems);
sortingModeGroup->addButton(m_sizeOfContents);
m_recursiveDirectorySizeLimit = new QSpinBox();
connect(m_recursiveDirectorySizeLimit, QOverload<int>::of(&QSpinBox::valueChanged), this, [this](int value) {
m_recursiveDirectorySizeLimit->setSuffix(i18np(" level deep", " levels deep", value));
});
m_recursiveDirectorySizeLimit->setRange(1, 20);
m_recursiveDirectorySizeLimit->setSingleStep(1);
QHBoxLayout *contentsSizeLayout = new QHBoxLayout();
contentsSizeLayout->addWidget(m_sizeOfContents);
contentsSizeLayout->addWidget(m_recursiveDirectorySizeLimit);