]> cloud.milkyroute.net Git - dolphin.git/blobdiff - src/kitemviews/kfileitemmodel.cpp
Merge remote-tracking branch 'fork/work/zakharafoniam/useful-groups'
[dolphin.git] / src / kitemviews / kfileitemmodel.cpp
index 960847ea7f8aaca66beebd36912c53e75559c4ce..603c16e0d37ff816488aa69b9122308335a36d45 100644 (file)
@@ -20,6 +20,9 @@
 #include <KLocalizedString>
 #include <KUrlMimeData>
 
+#ifndef QT_NO_ACCESSIBILITY
+#include <QAccessible>
+#endif
 #include <QElapsedTimer>
 #include <QIcon>
 #include <QMimeData>
@@ -39,6 +42,7 @@ KFileItemModel::KFileItemModel(QObject *parent)
     , m_sortDirsFirst(true)
     , m_sortHiddenLast(false)
     , m_sortRole(NameRole)
+    , m_groupRole(NoRole)
     , m_sortingProgressPercent(-1)
     , m_roles()
     , m_itemData()
@@ -202,13 +206,20 @@ bool KFileItemModel::setData(int index, const QHash<QByteArray, QVariant> &value
         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);
 
@@ -325,16 +336,32 @@ QMimeData *KFileItemModel::createMimeData(const KItemSet &indexes) const
     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;
         }
     }
@@ -946,6 +973,15 @@ QList<KFileItemModel::RoleInfo> KFileItemModel::rolesInformation()
     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)
@@ -2414,7 +2450,24 @@ int KFileItemModel::stringCompare(const QString &a, const QString &b, const QCol
     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());
@@ -2430,6 +2483,7 @@ int KFileItemModel::stringCompare(const QString &a, const QString &b, const QCol
 
 KFileItemModel::ItemGroupInfo KFileItemModel::nameRoleGroup(const ItemData *itemData, bool withString) const
 {
+    static bool oldWithString;
     static ItemGroupInfo oldGroupInfo;
     static QChar oldFirstChar;
     ItemGroupInfo groupInfo;
@@ -2441,8 +2495,8 @@ KFileItemModel::ItemGroupInfo KFileItemModel::nameRoleGroup(const ItemData *item
 
     // 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) {
@@ -2497,6 +2551,7 @@ KFileItemModel::ItemGroupInfo KFileItemModel::nameRoleGroup(const ItemData *item
         }
         groupInfo.comparable = (int)'.';
     }
+    oldWithString = withString;
     oldFirstChar = firstChar;
     oldGroupInfo = groupInfo;
     return groupInfo;
@@ -2504,8 +2559,6 @@ KFileItemModel::ItemGroupInfo KFileItemModel::nameRoleGroup(const ItemData *item
 
 KFileItemModel::ItemGroupInfo KFileItemModel::sizeRoleGroup(const ItemData *itemData, bool withString) const
 {
-    static ItemGroupInfo oldGroupInfo;
-    static KIO::filesize_t oldFileSize;
     ItemGroupInfo groupInfo;
     KIO::filesize_t fileSize;
 
@@ -2520,9 +2573,6 @@ KFileItemModel::ItemGroupInfo KFileItemModel::sizeRoleGroup(const ItemData *item
             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
@@ -2537,23 +2587,40 @@ KFileItemModel::ItemGroupInfo KFileItemModel::sizeRoleGroup(const ItemData *item
         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()) {
@@ -2599,8 +2666,8 @@ KFileItemModel::timeRoleGroup(const std::function<QDateTime(const ItemData *)> &
                     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",
@@ -2634,9 +2701,9 @@ KFileItemModel::timeRoleGroup(const std::function<QDateTime(const ItemData *)> &
                         "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",
@@ -2645,15 +2712,15 @@ KFileItemModel::timeRoleGroup(const std::function<QDateTime(const ItemData *)> &
                         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",
@@ -2666,9 +2733,9 @@ KFileItemModel::timeRoleGroup(const std::function<QDateTime(const ItemData *)> &
                         "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",
@@ -2677,7 +2744,7 @@ KFileItemModel::timeRoleGroup(const std::function<QDateTime(const ItemData *)> &
                         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(
@@ -2687,9 +2754,9 @@ KFileItemModel::timeRoleGroup(const std::function<QDateTime(const ItemData *)> &
                         "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",
@@ -2698,7 +2765,7 @@ KFileItemModel::timeRoleGroup(const std::function<QDateTime(const ItemData *)> &
                         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(
@@ -2708,9 +2775,9 @@ KFileItemModel::timeRoleGroup(const std::function<QDateTime(const ItemData *)> &
                         "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",
@@ -2719,7 +2786,7 @@ KFileItemModel::timeRoleGroup(const std::function<QDateTime(const ItemData *)> &
                         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(
@@ -2729,9 +2796,9 @@ KFileItemModel::timeRoleGroup(const std::function<QDateTime(const ItemData *)> &
                         "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",
@@ -2740,16 +2807,16 @@ KFileItemModel::timeRoleGroup(const std::function<QDateTime(const ItemData *)> &
                         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",
@@ -2757,6 +2824,7 @@ KFileItemModel::timeRoleGroup(const std::function<QDateTime(const ItemData *)> &
             }
         }
     }
+    oldWithString = withString;
     oldFileDate = fileDate;
     oldGroupInfo = groupInfo;
     return groupInfo;
@@ -2764,13 +2832,14 @@ KFileItemModel::timeRoleGroup(const std::function<QDateTime(const ItemData *)> &
 
 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;
@@ -2816,6 +2885,7 @@ KFileItemModel::ItemGroupInfo KFileItemModel::permissionRoleGroup(const ItemData
         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;
@@ -2828,7 +2898,7 @@ KFileItemModel::ItemGroupInfo KFileItemModel::ratingRoleGroup(const ItemData *it
     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;
 }
@@ -3055,6 +3125,7 @@ const KFileItemModel::RoleInfoMap *KFileItemModel::rolesInfoMap(int &count)
         { "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 },
@@ -3156,13 +3227,14 @@ bool KFileItemModel::isConsistent() const
 
 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);
     }
 }