#include "dolphin_generalsettings.h"
#include "dolphindebug.h"
#include "private/kfileitemmodelsortalgorithm.h"
+#include "views/draganddrophelper.h"
#include <KDirLister>
#include <KIO/Job>
+#include <KIO/ListJob>
#include <KLocalizedString>
#include <KUrlMimeData>
+#ifndef QT_NO_ACCESSIBILITY
+#include <QAccessible>
+#endif
#include <QElapsedTimer>
#include <QIcon>
#include <QMimeData>
#include <QRecursiveMutex>
#include <QTimer>
#include <QWidget>
-#include <algorithm>
#include <klazylocalizedstring.h>
Q_GLOBAL_STATIC(QRecursiveMutex, s_collatorMutex)
// for a lot of items within a quite small timeslot. To prevent expensive resortings the
// resorting is postponed until the timer has been exceeded.
m_resortAllItemsTimer = new QTimer(this);
- m_resortAllItemsTimer->setInterval(500);
+ m_resortAllItemsTimer->setInterval(100); // 100 is a middle ground between sorting too frequently which makes the view unreadable
+ // and sorting too infrequently which leads to users seeing an outdated sort order.
m_resortAllItemsTimer->setSingleShot(true);
connect(m_resortAllItemsTimer, &QTimer::timeout, this, &KFileItemModel::resortAllItems);
connect(GeneralSettings::self(), &GeneralSettings::sortingChoiceChanged, this, &KFileItemModel::slotSortingChoiceChanged);
- setShowTrashMime(m_dirLister->showHiddenFiles());
+ setShowTrashMime(m_dirLister->showHiddenFiles() || !GeneralSettings::hideXTrashFile());
}
KFileItemModel::~KFileItemModel()
return false;
}
- m_itemData[index]->values = currentValues;
if (changedRoles.contains("text")) {
QUrl url = m_itemData[index]->item.url();
+ m_items.remove(url);
url = url.adjusted(QUrl::RemoveFilename);
url.setPath(url.path() + currentValues["text"].toString());
m_itemData[index]->item.setUrl(url);
+ m_items.insert(url, index);
+
+ if (!changedRoles.contains("url")) {
+ changedRoles.insert("url");
+ currentValues["url"] = url;
+ }
}
+ m_itemData[index]->values = currentValues;
emitItemsChangedAndTriggerResorting(KItemRangeList() << KItemRange(index, 1), changedRoles);
return m_sortHiddenLast;
}
-void KFileItemModel::setShowTrashMime(bool show)
+void KFileItemModel::setShowTrashMime(bool showTrashMime)
{
const auto trashMime = QStringLiteral("application/x-trash");
QStringList excludeFilter = m_filter.excludeMimeTypes();
- bool wasShown = !excludeFilter.contains(trashMime);
- if (show) {
- if (!wasShown) {
- excludeFilter.removeAll(trashMime);
- }
- } else {
- if (wasShown) {
- excludeFilter.append(trashMime);
- }
+ if (showTrashMime) {
+ excludeFilter.removeAll(trashMime);
+ } else if (!excludeFilter.contains(trashMime)) {
+ excludeFilter.append(trashMime);
}
- if (wasShown != show) {
- setExcludeMimeTypeFilter(excludeFilter);
+ setExcludeMimeTypeFilter(excludeFilter);
+}
+
+void KFileItemModel::scheduleResortAllItems()
+{
+ if (!m_resortAllItemsTimer->isActive()) {
+ m_resortAllItemsTimer->start();
}
}
void KFileItemModel::setShowHiddenFiles(bool show)
{
m_dirLister->setShowHiddenFiles(show);
- setShowTrashMime(show);
+ setShowTrashMime(show || !GeneralSettings::hideXTrashFile());
m_dirLister->emitChanges();
if (show) {
dispatchPendingItemsToInsert();
return data;
}
+namespace
+{
+QString removeMarks(const QString &original)
+{
+ const auto normalized = original.normalized(QString::NormalizationForm_D);
+ QString res;
+ for (auto ch : normalized) {
+ if (!ch.isMark()) {
+ res.append(ch);
+ }
+ }
+ return res;
+}
+}
+
int KFileItemModel::indexForKeyboardSearch(const QString &text, int startFromIndex) const
{
+ const auto noMarkText = removeMarks(text);
startFromIndex = qMax(0, startFromIndex);
for (int i = startFromIndex; i < count(); ++i) {
- if (fileItem(i).text().startsWith(text, Qt::CaseInsensitive)) {
+ if (removeMarks(fileItem(i).text()).startsWith(noMarkText, Qt::CaseInsensitive)) {
return i;
}
}
for (int i = 0; i < startFromIndex; ++i) {
- if (fileItem(i).text().startsWith(text, Qt::CaseInsensitive)) {
+ if (removeMarks(fileItem(i).text()).startsWith(noMarkText, Qt::CaseInsensitive)) {
return i;
}
}
} else {
item = fileItem(index);
}
- return !item.isNull() && ((item.isDir() && item.isWritable()) || item.isDesktopFile());
+ return !item.isNull() && DragAndDropHelper::supportsDropping(item);
+}
+
+bool KFileItemModel::canEnterOnHover(int index) const
+{
+ KFileItem item;
+ if (index == -1) {
+ item = rootItem();
+ } else {
+ item = fileItem(index);
+ }
+ return !item.isNull() && (item.isDir() || item.isDesktopFile());
}
QString KFileItemModel::roleDescription(const QByteArray &role) const
for (const KFileItem &item : items) {
if (item.url() == currentDir) {
- // #473377: Delay emitting currentDirectoryRemoved() to avoid modifying KCoreDirLister
- // before KCoreDirListerCache::deleteDir() returns.
- QTimer::singleShot(0, this, [this] {
- Q_EMIT currentDirectoryRemoved();
- });
-
+ Q_EMIT currentDirectoryRemoved();
return;
}
// Trigger a resorting if necessary. Note that this can happen even if the sort
// role has not changed at all because the file name can be used as a fallback.
- if (changedRoles.contains(sortRole()) || changedRoles.contains(roleForType(NameRole))) {
+ if (changedRoles.contains(sortRole()) || changedRoles.contains(roleForType(NameRole))
+ || (changedRoles.contains("count") && sortRole() == "size")) { // "count" is used in the "size" sort role, so this might require a resorting.
for (const KItemRange &range : itemRanges) {
bool needsResorting = false;
}
if (needsResorting) {
- m_resortAllItemsTimer->start();
+ scheduleResortAllItems();
return;
}
}
}
}
- if (m_sortDirsFirst || (ContentDisplaySettings::directorySizeCount() && m_sortRole == SizeRole)) {
+ if (m_sortDirsFirst
+ || (ContentDisplaySettings::directorySizeMode() == ContentDisplaySettings::EnumDirectorySizeMode::ContentCount && m_sortRole == SizeRole)) {
const bool isDirA = a->item.isDir();
const bool isDirB = b->item.isDir();
if (isDirA && !isDirB) {
break;
case SizeRole: {
- if (ContentDisplaySettings::directorySizeCount() && itemA.isDir()) {
+ if (ContentDisplaySettings::directorySizeMode() == ContentDisplaySettings::EnumDirectorySizeMode::ContentCount && itemA.isDir()) {
// folders first then
// items A and B are folders thanks to lessThan checks
auto valueA = a->values.value("count");
QMutexLocker collatorLock(s_collatorMutex());
if (m_naturalSorting) {
- return collator.compare(a, b);
+ // Split extension, taking into account it can be empty
+ constexpr QString::SectionFlags flags = QString::SectionSkipEmpty | QString::SectionIncludeLeadingSep;
+
+ // Sort by baseName first
+ const QString aBaseName = a.section('.', 0, 0, flags);
+ const QString bBaseName = b.section('.', 0, 0, flags);
+
+ const int res = collator.compare(aBaseName, bBaseName);
+ if (res != 0 || (aBaseName.length() == a.length() && bBaseName.length() == b.length())) {
+ return res;
+ }
+
+ // sliced() has undefined behavior when pos < 0 or pos > size().
+ Q_ASSERT(aBaseName.length() <= a.length() && aBaseName.length() >= 0);
+ Q_ASSERT(bBaseName.length() <= b.length() && bBaseName.length() >= 0);
+
+ // baseNames were equal, sort by extension
+ return collator.compare(a.sliced(aBaseName.length()), b.sliced(bBaseName.length()));
}
const int result = QString::compare(a, b, collator.caseSensitivity());
KIO::filesize_t fileSize = !item.isNull() ? item.size() : ~0U;
QString newGroupValue;
if (!item.isNull() && item.isDir()) {
- if (ContentDisplaySettings::directorySizeCount() || m_sortDirsFirst) {
+ if (ContentDisplaySettings::directorySizeMode() == ContentDisplaySettings::EnumDirectorySizeMode::ContentCount || m_sortDirsFirst) {
newGroupValue = i18nc("@title:group Size", "Folders");
} else {
fileSize = m_itemData.at(i)->values.value("size").toULongLong();
continue;
}
+ const QLocale locale;
const QDateTime fileTime = fileTimeCb(m_itemData.at(i));
const QDate fileDate = fileTime.date();
if (fileDate == previousFileDate) {
newGroupValue = i18nc("@title:group Date", "Yesterday");
break;
default:
- newGroupValue = fileTime.toString(i18nc("@title:group Date: The week day name: dddd", "dddd"));
+ newGroupValue = locale.toString(fileTime, i18nc("@title:group Date: The week day name: dddd", "dddd"));
newGroupValue = i18nc(
"Can be used to script translation of \"dddd\""
"with context @title:group Date",
"part of the text that should not be formatted as a date",
"'Yesterday' (MMMM, yyyy)");
const QString translatedFormat = format.toString();
- if (translatedFormat.count(QLatin1Char('\'')) == 2) {
- newGroupValue = fileTime.toString(translatedFormat);
+ if (const int count = translatedFormat.count(QLatin1Char('\'')); count >= 2 && count % 2 == 0) {
+ newGroupValue = locale.toString(fileTime, translatedFormat);
newGroupValue = i18nc(
"Can be used to script translation of "
"\"'Yesterday' (MMMM, yyyy)\" with context @title:group Date",
qCWarning(DolphinDebug).nospace()
<< "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org";
const QString untranslatedFormat = format.toString({QLatin1String("en_US")});
- newGroupValue = fileTime.toString(untranslatedFormat);
+ newGroupValue = locale.toString(fileTime, untranslatedFormat);
}
} else if (daysDistance <= 7) {
- newGroupValue =
- fileTime.toString(i18nc("@title:group Date: "
- "The week day name: dddd, MMMM is full month name "
- "in current locale, and yyyy is full year number.",
- "dddd (MMMM, yyyy)"));
+ newGroupValue = locale.toString(fileTime,
+ i18nc("@title:group Date: "
+ "The week day name: dddd, MMMM is full month name "
+ "in current locale, and yyyy is full year number.",
+ "dddd (MMMM, yyyy)"));
newGroupValue = i18nc(
"Can be used to script translation of "
"\"dddd (MMMM, yyyy)\" with context @title:group Date",
"part of the text that should not be formatted as a date",
"'One Week Ago' (MMMM, yyyy)");
const QString translatedFormat = format.toString();
- if (translatedFormat.count(QLatin1Char('\'')) == 2) {
- newGroupValue = fileTime.toString(translatedFormat);
+ if (const int count = translatedFormat.count(QLatin1Char('\'')); count >= 2 && count % 2 == 0) {
+ newGroupValue = locale.toString(fileTime, translatedFormat);
newGroupValue = i18nc(
"Can be used to script translation of "
"\"'One Week Ago' (MMMM, yyyy)\" with context @title:group Date",
qCWarning(DolphinDebug).nospace()
<< "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org";
const QString untranslatedFormat = format.toString({QLatin1String("en_US")});
- newGroupValue = fileTime.toString(untranslatedFormat);
+ newGroupValue = locale.toString(fileTime, untranslatedFormat);
}
} else if (daysDistance <= 7 * 3) {
const KLocalizedString format = ki18nc(
"part of the text that should not be formatted as a date",
"'Two Weeks Ago' (MMMM, yyyy)");
const QString translatedFormat = format.toString();
- if (translatedFormat.count(QLatin1Char('\'')) == 2) {
- newGroupValue = fileTime.toString(translatedFormat);
+ if (const int count = translatedFormat.count(QLatin1Char('\'')); count >= 2 && count % 2 == 0) {
+ newGroupValue = locale.toString(fileTime, translatedFormat);
newGroupValue = i18nc(
"Can be used to script translation of "
"\"'Two Weeks Ago' (MMMM, yyyy)\" with context @title:group Date",
qCWarning(DolphinDebug).nospace()
<< "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org";
const QString untranslatedFormat = format.toString({QLatin1String("en_US")});
- newGroupValue = fileTime.toString(untranslatedFormat);
+ newGroupValue = locale.toString(fileTime, untranslatedFormat);
}
} else if (daysDistance <= 7 * 4) {
const KLocalizedString format = ki18nc(
"part of the text that should not be formatted as a date",
"'Three Weeks Ago' (MMMM, yyyy)");
const QString translatedFormat = format.toString();
- if (translatedFormat.count(QLatin1Char('\'')) == 2) {
- newGroupValue = fileTime.toString(translatedFormat);
+ if (const int count = translatedFormat.count(QLatin1Char('\'')); count >= 2 && count % 2 == 0) {
+ newGroupValue = locale.toString(fileTime, translatedFormat);
newGroupValue = i18nc(
"Can be used to script translation of "
"\"'Three Weeks Ago' (MMMM, yyyy)\" with context @title:group Date",
qCWarning(DolphinDebug).nospace()
<< "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org";
const QString untranslatedFormat = format.toString({QLatin1String("en_US")});
- newGroupValue = fileTime.toString(untranslatedFormat);
+ newGroupValue = locale.toString(fileTime, untranslatedFormat);
}
} else {
const KLocalizedString format = ki18nc(
"part of the text that should not be formatted as a date",
"'Earlier on' MMMM, yyyy");
const QString translatedFormat = format.toString();
- if (translatedFormat.count(QLatin1Char('\'')) == 2) {
- newGroupValue = fileTime.toString(translatedFormat);
+ if (const int count = translatedFormat.count(QLatin1Char('\'')); count >= 2 && count % 2 == 0) {
+ newGroupValue = locale.toString(fileTime, translatedFormat);
newGroupValue = i18nc(
"Can be used to script translation of "
"\"'Earlier on' MMMM, yyyy\" with context @title:group Date",
qCWarning(DolphinDebug).nospace()
<< "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org";
const QString untranslatedFormat = format.toString({QLatin1String("en_US")});
- newGroupValue = fileTime.toString(untranslatedFormat);
+ newGroupValue = locale.toString(fileTime, untranslatedFormat);
}
}
} else {
- newGroupValue =
- fileTime.toString(i18nc("@title:group "
- "The month and year: MMMM is full month name in current locale, "
- "and yyyy is full year number",
- "MMMM, yyyy"));
+ newGroupValue = locale.toString(fileTime,
+ i18nc("@title:group "
+ "The month and year: MMMM is full month name in current locale, "
+ "and yyyy is full year number",
+ "MMMM, yyyy"));
newGroupValue = i18nc(
"Can be used to script translation of "
"\"MMMM, yyyy\" with context @title:group Date",
{ "releaseYear", ReleaseYearRole, kli18nc("@label", "Release Year"), kli18nc("@label", "Audio"), KLazyLocalizedString(), true, true },
{ "aspectRatio", AspectRatioRole, kli18nc("@label", "Aspect Ratio"), kli18nc("@label", "Video"), KLazyLocalizedString(), true, true },
{ "frameRate", FrameRateRole, kli18nc("@label", "Frame Rate"), kli18nc("@label", "Video"), KLazyLocalizedString(), true, true },
+ { "duration", DurationRole, kli18nc("@label", "Duration"), kli18nc("@label", "Video"), KLazyLocalizedString(), true, true },
{ "path", PathRole, kli18nc("@label", "Path"), kli18nc("@label", "Other"), KLazyLocalizedString(), false, false },
{ "extension", ExtensionRole, kli18nc("@label", "File Extension"), kli18nc("@label", "Other"), KLazyLocalizedString(), false, false },
{ "deletiontime", DeletionTimeRole, kli18nc("@label", "Deletion Time"), kli18nc("@label", "Other"), KLazyLocalizedString(), false, false },
void KFileItemModel::slotListerError(KIO::Job *job)
{
- if (job->error() == KIO::ERR_IS_FILE) {
+ const int jobError = job->error();
+ if (jobError == KIO::ERR_IS_FILE) {
if (auto *listJob = qobject_cast<KIO::ListJob *>(job)) {
Q_EMIT urlIsFileError(listJob->url());
}
} else {
const QString errorString = job->errorString();
- Q_EMIT errorMessage(!errorString.isEmpty() ? errorString : i18nc("@info:status", "Unknown error."));
+ Q_EMIT errorMessage(!errorString.isEmpty() ? errorString : i18nc("@info:status", "Unknown error."), jobError);
}
}