#include <KLocalizedString>
#include <KUrlMimeData>
+#ifndef QT_NO_ACCESSIBILITY
+#include <QAccessible>
+#endif
#include <QElapsedTimer>
#include <QIcon>
#include <QMimeData>
, m_sortDirsFirst(true)
, m_sortHiddenLast(false)
, m_sortRole(NameRole)
+ , m_groupRole(NoRole)
, m_sortingProgressPercent(-1)
, m_roles()
, m_itemData()
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 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;
}
}
return rolesInfo;
}
+QList<KFileItemModel::RoleInfo> KFileItemModel::extraGroupingInformation()
+{
+ static QList<RoleInfo> rolesInfo{
+ {QByteArray("none"), kli18nc("@label", "No grouping").toString(), nullptr, nullptr, false, false},
+ {QByteArray("followSort"), kli18nc("@label", "Follow sorting").toString(), nullptr, nullptr, false, false}
+ };
+ return rolesInfo;
+}
+
void KFileItemModel::onGroupedSortingChanged(bool current)
{
Q_UNUSED(current)
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());
KFileItemModel::ItemGroupInfo KFileItemModel::nameRoleGroup(const ItemData *itemData, bool withString) const
{
+ static bool oldWithString;
static ItemGroupInfo oldGroupInfo;
static QChar oldFirstChar;
ItemGroupInfo groupInfo;
// Use the first character of the name as group indication
firstChar = name.at(0).toUpper();
-
- if (firstChar == oldFirstChar) {
+
+ if (firstChar == oldFirstChar && withString == oldWithString) {
return oldGroupInfo;
}
if (firstChar == QLatin1Char('~') && name.length() > 1) {
}
groupInfo.comparable = (int)'.';
}
+ oldWithString = withString;
oldFirstChar = firstChar;
oldGroupInfo = groupInfo;
return groupInfo;
KFileItemModel::ItemGroupInfo KFileItemModel::sizeRoleGroup(const ItemData *itemData, bool withString) const
{
- static ItemGroupInfo oldGroupInfo;
- static KIO::filesize_t oldFileSize;
ItemGroupInfo groupInfo;
KIO::filesize_t fileSize;
fileSize = itemData->values.value("size").toULongLong();
}
}
- if (fileSize == oldFileSize) {
- return oldGroupInfo;
- }
if (groupInfo.comparable < 0) {
if (fileSize < 5 * 1024 * 1024) { // < 5 MB
groupInfo.comparable = 1; // Small
char const *groupNames[] = {"Folders", "Small", "Medium", "Big"};
groupInfo.text = i18nc("@title:group Size", groupNames[groupInfo.comparable]);
}
- oldFileSize = fileSize;
- oldGroupInfo = groupInfo;
return groupInfo;
}
KFileItemModel::ItemGroupInfo
KFileItemModel::timeRoleGroup(const std::function<QDateTime(const ItemData *)> &fileTimeCb, const ItemData *itemData, bool withString) const
{
+ static bool oldWithString;
static ItemGroupInfo oldGroupInfo;
static QDate oldFileDate;
ItemGroupInfo groupInfo;
const QDate currentDate = QDate::currentDate();
- const QDateTime fileTime = fileTimeCb(itemData);
- const QDate fileDate = fileTime.date();
- const int daysDistance = fileDate.daysTo(currentDate);
+ QDate previousFileDate;
+ QString groupValue;
+ for (int i = 0; i <= maxIndex; ++i) {
+ if (isChildItem(i)) {
+ continue;
+ }
+
+ const QLocale locale;
+ const QDateTime fileTime = fileTimeCb(m_itemData.at(i));
+ const QDate fileDate = fileTime.date();
+ if (fileDate == previousFileDate) {
+ // The current item is in the same group as the previous item
+ continue;
+ }
+ previousFileDate = fileDate;
+
+ const int daysDistance = fileDate.daysTo(currentDate);
+
+ if (fileDate == oldFileDate && withString == oldWithString) {
+ return oldGroupInfo;
+ }
// Simplified grouping algorithm, preserving dates
// but not taking "pretty printing" into account
if (currentDate.year() == fileDate.year() && currentDate.month() == fileDate.month()) {
groupInfo.text = i18nc("@title:group Date", "Yesterday");
break;
default:
- groupInfo.text = fileTime.toString(i18nc("@title:group Date: The week day name: dddd", "dddd"));
- groupInfo.text = i18nc(
+ 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",
"%1",
"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) {
- groupInfo.text = fileTime.toString(translatedFormat);
- groupInfo.text = i18nc(
+ 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",
"%1",
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")});
- groupInfo.text = fileTime.toString(untranslatedFormat);
+ newGroupValue = locale.toString(fileTime, untranslatedFormat);
}
- } else if (daysDistance < 7) {
- groupInfo.text =
- 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)"));
- groupInfo.text = i18nc(
+ } else if (daysDistance <= 7) {
+ 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",
"%1",
"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) {
- groupInfo.text = fileTime.toString(translatedFormat);
- groupInfo.text = i18nc(
+ 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",
"%1",
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")});
- groupInfo.text = 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) {
- groupInfo.text = fileTime.toString(translatedFormat);
- groupInfo.text = i18nc(
+ 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",
"%1",
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")});
- groupInfo.text = 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) {
- groupInfo.text = fileTime.toString(translatedFormat);
- groupInfo.text = i18nc(
+ 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",
"%1",
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")});
- groupInfo.text = 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) {
- groupInfo.text = fileTime.toString(translatedFormat);
- groupInfo.text = i18nc(
+ 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",
"%1",
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")});
- groupInfo.text = fileTime.toString(untranslatedFormat);
+ newGroupValue = locale.toString(fileTime, untranslatedFormat);
}
}
} else {
- groupInfo.text =
- 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"));
- groupInfo.text = i18nc(
+ 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",
"%1",
}
}
}
+ oldWithString = withString;
oldFileDate = fileDate;
oldGroupInfo = groupInfo;
return groupInfo;
KFileItemModel::ItemGroupInfo KFileItemModel::permissionRoleGroup(const ItemData *itemData, bool withString) const
{
+ static bool oldWithString;
static ItemGroupInfo oldGroupInfo;
static QFileDevice::Permissions oldPermissions;
ItemGroupInfo groupInfo;
const QFileInfo info(itemData->item.url().toLocalFile());
const QFileDevice::Permissions permissions = info.permissions();
- if (permissions == oldPermissions) {
+ if (permissions == oldPermissions && withString == oldWithString) {
return oldGroupInfo;
}
groupInfo.comparable = (int)permissions;
others = others.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : others.mid(0, others.length() - 2);
groupInfo.text = i18nc("@title:group Files and folders by permissions", "User: %1 | Group: %2 | Others: %3", user, group, others);
}
+ oldWithString = withString;
oldPermissions = permissions;
oldGroupInfo = groupInfo;
return groupInfo;
if (withString) {
// Dolphin does not currently use string representation of star rating
// as stars are rendered as graphics in group headers.
- groupInfo.text = i18nc("@item:intext Rated N (stars)", "Rated ") + QString::number(groupInfo.comparable);
+ groupInfo.text = i18nc("@item:intext Rated N (stars)", "Rated %i", QString::number(groupInfo.comparable));
}
return groupInfo;
}
{ "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);
}
}