}
addSeparator();
- // Insert 'Sort By' and 'View Mode'
+ // Insert 'Sort By', 'Group By' and 'View Mode'
if (ContextMenuSettings::showSortBy()) {
addAction(m_mainWindow->actionCollection()->action(QStringLiteral("sort")));
}
+ if (ContextMenuSettings::showGroupBy()) {
+ addAction(m_mainWindow->actionCollection()->action(QStringLiteral("group")));
+ }
if (ContextMenuSettings::showViewMode()) {
addAction(m_mainWindow->actionCollection()->action(QStringLiteral("view_mode")));
}
menu->addAction(ac->action(QStringLiteral("view_mode")));
}
if (!toolBar()->isVisible() || !toolbarActions.contains(ac->action(QStringLiteral("view_settings")))) {
- menu->addAction(ac->action(QStringLiteral("show_hidden_files")));
- menu->addAction(ac->action(QStringLiteral("sort")));
- menu->addAction(ac->action(QStringLiteral("additional_info")));
- if (!GeneralSettings::showStatusBar() || !GeneralSettings::showZoomSlider()) {
- menu->addAction(ac->action(QStringLiteral("zoom")));
- }
+ menu->addAction(ac->action(QStringLiteral("show_hidden_files")));
+ menu->addAction(ac->action(QStringLiteral("sort")));
+ menu->addAction(ac->action(QStringLiteral("group")));
+ menu->addAction(ac->action(QStringLiteral("additional_info")));
+ if (!GeneralSettings::showStatusBar() || !GeneralSettings::showZoomSlider()) {
+ menu->addAction(ac->action(QStringLiteral("zoom")));
+ }
}
menu->addAction(ac->action(QStringLiteral("panels")));
placesDock->setLocked(lock);
placesDock->setObjectName(QStringLiteral("placesDock"));
placesDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
-
+
m_placesPanel = new PlacesPanel(placesDock);
m_placesPanel->setCustomContextMenuActions({lockLayoutAction});
placesDock->setWidget(m_placesPanel);
</Menu>
<Menu name="view"><text>&View</text>
<Action name="sort" />
+ <Action name="group" />
<Action name="additional_info" />
<Action name="show_preview" />
<Action name="show_in_groups" />
<Action name="view_zoom_out"/>
<Separator/>
<Action name="sort" />
+ <Action name="group" />
<Action name="view_mode" />
<Action name="additional_info" />
<Action name="show_preview" />
roles.insert("expandedParentsCount");
}
- // Assure that the role that is used for sorting will be determined
+ // Assure that the roles used for sorting and grouping will be determined
roles.insert(fileItemModel->sortRole());
+ roles.insert(fileItemModel->groupRole());
fileItemModel->setRoles(roles);
m_modelRolesUpdater->setRoles(roles);
// #define KFILEITEMMODEL_DEBUG
KFileItemModel::KFileItemModel(QObject *parent)
- : KItemModelBase("text", parent)
+ : KItemModelBase("text", "none", parent)
, m_dirLister(nullptr)
, m_sortDirsFirst(true)
, m_sortHiddenLast(false)
, m_sortRole(NameRole)
+ , m_groupRole(NoRole)
, m_sortingProgressPercent(-1)
, m_roles()
, m_itemData()
QElapsedTimer timer;
timer.start();
#endif
- switch (typeForRole(sortRole())) {
+ QByteArray role = groupRole();
+ if (typeForRole(role) == NoRole) {
+ // Handle extra grouping information
+ if (m_groupExtraInfo == "followSort") {
+ role = sortRole();
+ }
+ }
+ switch (typeForRole(role)) {
+ case NoRole:
+ m_groups.clear();
+ break;
case NameRole:
m_groups = nameRoleGroups();
break;
m_groups = ratingRoleGroups();
break;
default:
- m_groups = genericStringRoleGroups(sortRole());
+ m_groups = genericStringRoleGroups(role);
break;
}
}
}
+KFileItemModel::RoleInfo KFileItemModel::roleInformation(const QByteArray &role)
+{
+ static QHash<QByteArray, RoleInfo> information;
+ if (information.isEmpty()) {
+ int count = 0;
+ const RoleInfoMap *map = rolesInfoMap(count);
+ for (int i = 0; i < count; ++i) {
+ RoleInfo info;
+ info.role = map[i].role;
+ info.translation = map[i].roleTranslation.toString();
+ if (!map[i].groupTranslation.isEmpty()) {
+ info.group = map[i].groupTranslation.toString();
+ } else {
+ // For top level roles, groupTranslation is 0. We must make sure that
+ // info.group is an empty string then because the code that generates
+ // menus tries to put the actions into sub menus otherwise.
+ info.group = QString();
+ }
+ info.requiresBaloo = map[i].requiresBaloo;
+ info.requiresIndexer = map[i].requiresIndexer;
+ if (!map[i].tooltipTranslation.isEmpty()) {
+ info.tooltip = map[i].tooltipTranslation.toString();
+ } else {
+ info.tooltip = QString();
+ }
+
+ information.insert(map[i].role, info);
+ }
+ }
+
+ return information.value(role);
+}
+
QList<KFileItemModel::RoleInfo> KFileItemModel::rolesInformation()
{
static QList<RoleInfo> rolesInfo;
const RoleInfoMap *map = rolesInfoMap(count);
for (int i = 0; i < count; ++i) {
if (map[i].roleType != NoRole) {
- RoleInfo info;
- info.role = map[i].role;
- info.translation = map[i].roleTranslation.toString();
- if (!map[i].groupTranslation.isEmpty()) {
- info.group = map[i].groupTranslation.toString();
- } else {
- // For top level roles, groupTranslation is 0. We must make sure that
- // info.group is an empty string then because the code that generates
- // menus tries to put the actions into sub menus otherwise.
- info.group = QString();
- }
- info.requiresBaloo = map[i].requiresBaloo;
- info.requiresIndexer = map[i].requiresIndexer;
- if (!map[i].tooltipTranslation.isEmpty()) {
- info.tooltip = map[i].tooltipTranslation.toString();
- } else {
- info.tooltip = QString();
- }
+ RoleInfo info = roleInformation(map[i].role);
rolesInfo.append(info);
}
}
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)
{
Q_UNUSED(previous)
m_sortRole = typeForRole(current);
+ if (m_sortRole == NoRole) {
+ // Requested role not in list of roles. This could
+ // be used for indicating non-trivial sorting behavior
+ m_sortExtraInfo = current;
+ } else {
+ m_sortExtraInfo.clear();
+ }
if (!m_requestRole[m_sortRole]) {
QSet<QByteArray> newRoles = m_roles;
resortAllItems();
}
+void KFileItemModel::onGroupRoleChanged(const QByteArray ¤t, const QByteArray &previous, bool resortItems)
+{
+ Q_UNUSED(previous)
+ m_groupRole = typeForRole(current);
+ if (m_groupRole == NoRole) {
+ // Requested role not in list of roles. This could
+ // be used for indicating non-trivial grouping behavior
+ m_groupExtraInfo = current;
+ } else {
+ m_groupExtraInfo.clear();
+ }
+
+ if (!m_requestRole[m_groupRole]) {
+ QSet<QByteArray> newRoles = m_roles;
+ newRoles << current;
+ setRoles(newRoles);
+ }
+
+ if (resortItems) {
+ resortAllItems();
+ }
+}
+
+void KFileItemModel::onGroupOrderChanged(Qt::SortOrder current, Qt::SortOrder previous)
+{
+ Q_UNUSED(current)
+ Q_UNUSED(previous)
+ resortAllItems();
+}
+
void KFileItemModel::loadSortingSettings()
{
using Choice = GeneralSettings::EnumSortingChoice;
// Workaround for bug https://bugreports.qt.io/browse/QTBUG-69361
// Force the clean state of QCollator in single thread to avoid thread safety problems in sort
m_collator.compare(QString(), QString());
+ ContentDisplaySettings::self();
}
void KFileItemModel::resortAllItems()
}
Q_EMIT itemsMoved(KItemRange(firstMovedIndex, movedItemsCount), movedToIndexes);
- } else if (groupedSorting()) {
+ }
+ if (groupedSorting()) {
// The groups might have changed even if the order of the items has not.
const QList<QPair<int, QVariant>> oldGroups = m_groups;
m_groups.clear();
QList<KFileItemModel::ItemData *> KFileItemModel::createItemDataList(const QUrl &parentUrl, const KFileItemList &items) const
{
- if (m_sortRole == TypeRole) {
+ if (m_sortRole == TypeRole || m_groupRole == TypeRole) {
// Try to resolve the MIME-types synchronously to prevent a reordering of
// the items when sorting by type (per default MIME-types are resolved
// asynchronously by KFileItemModelRolesUpdater).
return itemDataList;
}
-void KFileItemModel::prepareItemsForSorting(QList<ItemData *> &itemDataList)
+void KFileItemModel::prepareItemsWithRole(QList<ItemData *> &itemDataList, RoleType roleType)
{
- switch (m_sortRole) {
+ switch (roleType) {
case ExtensionRole:
case PermissionsRole:
case OwnerRole:
}
}
+void KFileItemModel::prepareItemsForSorting(QList<ItemData *> &itemDataList)
+{
+ prepareItemsWithRole(itemDataList, m_sortRole);
+ prepareItemsWithRole(itemDataList, m_groupRole);
+}
+
int KFileItemModel::expandedParentsCount(const ItemData *data)
{
// The hash 'values' is only guaranteed to contain the key "expandedParentsCount"
}
}
- // Show hidden files and folders last
- if (m_sortHiddenLast) {
- const bool isHiddenA = a->item.isHidden();
- const bool isHiddenB = b->item.isHidden();
- if (isHiddenA && !isHiddenB) {
- return false;
- } else if (!isHiddenA && isHiddenB) {
- return true;
+ result = groupRoleCompare(a, b, collator);
+ if (result == 0) {
+ // Show hidden files and folders last
+ if (m_sortHiddenLast) {
+ const bool isHiddenA = a->item.isHidden();
+ const bool isHiddenB = b->item.isHidden();
+ if (isHiddenA && !isHiddenB) {
+ return false;
+ } else if (!isHiddenA && isHiddenB) {
+ return true;
+ }
}
- }
-
- 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) {
- return true;
- } else if (!isDirA && isDirB) {
- return false;
+ 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) {
+ return true;
+ } else if (!isDirA && isDirB) {
+ return false;
+ }
}
+ result = sortRoleCompare(a, b, collator);
+ result = (sortOrder() == Qt::AscendingOrder) ? result < 0 : result > 0;
+ } else {
+ result = (groupOrder() == Qt::AscendingOrder) ? result < 0 : result > 0;
}
-
- result = sortRoleCompare(a, b, collator);
-
- return (sortOrder() == Qt::AscendingOrder) ? result < 0 : result > 0;
+ return result;
}
void KFileItemModel::sort(const QList<KFileItemModel::ItemData *>::iterator &begin, const QList<KFileItemModel::ItemData *>::iterator &end) const
return QString::compare(itemA.url().url(), itemB.url().url(), Qt::CaseSensitive);
}
+int KFileItemModel::groupRoleCompare(const ItemData *a, const ItemData *b, const QCollator &collator) const
+{
+ // Unlike sortRoleCompare, this function can and often will return 0.
+ int result = 0;
+
+ ItemGroupInfo groupA, groupB;
+ switch (m_groupRole) {
+ case NoRole:
+ // Non-trivial grouping behavior might be handled there in the future.
+ return 0;
+ case NameRole:
+ groupA = nameRoleGroup(a, false);
+ groupB = nameRoleGroup(b, false);
+ break;
+ case SizeRole:
+ groupA = sizeRoleGroup(a, false);
+ groupB = sizeRoleGroup(b, false);
+ break;
+ case ModificationTimeRole:
+ groupA = timeRoleGroup(
+ [](const ItemData *item) {
+ return item->item.time(KFileItem::ModificationTime);
+ },
+ a,
+ false);
+ groupB = timeRoleGroup(
+ [](const ItemData *item) {
+ return item->item.time(KFileItem::ModificationTime);
+ },
+ b,
+ false);
+ break;
+ case CreationTimeRole:
+ groupA = timeRoleGroup(
+ [](const ItemData *item) {
+ return item->item.time(KFileItem::CreationTime);
+ },
+ a,
+ false);
+ groupB = timeRoleGroup(
+ [](const ItemData *item) {
+ return item->item.time(KFileItem::CreationTime);
+ },
+ b,
+ false);
+ break;
+ case AccessTimeRole:
+ groupA = timeRoleGroup(
+ [](const ItemData *item) {
+ return item->item.time(KFileItem::AccessTime);
+ },
+ a,
+ false);
+ groupB = timeRoleGroup(
+ [](const ItemData *item) {
+ return item->item.time(KFileItem::AccessTime);
+ },
+ b,
+ false);
+ break;
+ case DeletionTimeRole:
+ groupA = timeRoleGroup(
+ [](const ItemData *item) {
+ return item->values.value("deletiontime").toDateTime();
+ },
+ a,
+ false);
+ groupB = timeRoleGroup(
+ [](const ItemData *item) {
+ return item->values.value("deletiontime").toDateTime();
+ },
+ b,
+ false);
+ break;
+ case PermissionsRole:
+ groupA = permissionRoleGroup(a, false);
+ groupB = permissionRoleGroup(b, false);
+ break;
+ case RatingRole:
+ groupA = ratingRoleGroup(a, false);
+ groupB = ratingRoleGroup(b, false);
+ break;
+ case TypeRole:
+ groupA = typeRoleGroup(a);
+ groupB = typeRoleGroup(b);
+ break;
+ default: {
+ groupA = genericStringRoleGroup(groupRole(), a);
+ groupB = genericStringRoleGroup(groupRole(), b);
+ break;
+ }
+ }
+ if (groupA.comparable < groupB.comparable) {
+ result = -1;
+ } else if (groupA.comparable > groupB.comparable) {
+ result = 1;
+ } else {
+ result = stringCompare(groupA.text, groupB.text, collator);
+ }
+ return result;
+}
+
int KFileItemModel::stringCompare(const QString &a, const QString &b, const QCollator &collator) const
{
QMutexLocker collatorLock(s_collatorMutex());
return QString::compare(a, b, Qt::CaseSensitive);
}
-QList<QPair<int, QVariant>> KFileItemModel::nameRoleGroups() const
+KFileItemModel::ItemGroupInfo KFileItemModel::nameRoleGroup(const ItemData *itemData, bool withString) const
{
- Q_ASSERT(!m_itemData.isEmpty());
-
- const int maxIndex = count() - 1;
- QList<QPair<int, QVariant>> groups;
-
- QString groupValue;
+ static bool oldWithString;
+ static ItemGroupInfo oldGroupInfo;
+ static QChar oldFirstChar;
+ ItemGroupInfo groupInfo;
QChar firstChar;
- for (int i = 0; i <= maxIndex; ++i) {
- if (isChildItem(i)) {
- continue;
- }
-
- const QString name = m_itemData.at(i)->item.text();
-
- // Use the first character of the name as group indication
- QChar newFirstChar = name.at(0).toUpper();
- if (newFirstChar == QLatin1Char('~') && name.length() > 1) {
- newFirstChar = name.at(1).toUpper();
- }
- if (firstChar != newFirstChar) {
- QString newGroupValue;
- if (newFirstChar.isLetter()) {
- if (m_collator.compare(newFirstChar, QChar(QLatin1Char('A'))) >= 0 && m_collator.compare(newFirstChar, QChar(QLatin1Char('Z'))) <= 0) {
- // WARNING! Symbols based on latin 'Z' like 'Z' with acute are treated wrong as non Latin and put in a new group.
+ const QString name = itemData->item.text();
- // Try to find a matching group in the range 'A' to 'Z'.
- static std::vector<QChar> lettersAtoZ;
- lettersAtoZ.reserve('Z' - 'A' + 1);
- if (lettersAtoZ.empty()) {
- for (char c = 'A'; c <= 'Z'; ++c) {
- lettersAtoZ.push_back(QLatin1Char(c));
- }
- }
+ QMutexLocker collatorLock(s_collatorMutex());
- auto localeAwareLessThan = [this](QChar c1, QChar c2) -> bool {
- return m_collator.compare(c1, c2) < 0;
- };
+ // Use the first character of the name as group indication
+ firstChar = name.at(0).toUpper();
+
+ if (firstChar == oldFirstChar && withString == oldWithString) {
+ return oldGroupInfo;
+ }
+ if (firstChar == QLatin1Char('~') && name.length() > 1) {
+ firstChar = name.at(1).toUpper();
+ }
+ if (firstChar.isLetter()) {
+ if (m_collator.compare(firstChar, QChar(QLatin1Char('A'))) >= 0 && m_collator.compare(firstChar, QChar(QLatin1Char('Z'))) <= 0) {
+ // WARNING! Symbols based on latin 'Z' like 'Z' with acute are treated wrong as non Latin and put in a new group.
+
+ // Try to find a matching group in the range 'A' to 'Z'.
+ static std::vector<QChar> lettersAtoZ;
+ lettersAtoZ.reserve('Z' - 'A' + 1);
+ if (lettersAtoZ.empty()) {
+ for (char c = 'A'; c <= 'Z'; ++c) {
+ lettersAtoZ.push_back(QLatin1Char(c));
+ }
+ }
- std::vector<QChar>::iterator it = std::lower_bound(lettersAtoZ.begin(), lettersAtoZ.end(), newFirstChar, localeAwareLessThan);
- if (it != lettersAtoZ.end()) {
- if (localeAwareLessThan(newFirstChar, *it)) {
- // newFirstChar belongs to the group preceding *it.
- // Example: for an umlaut 'A' in the German locale, *it would be 'B' now.
- --it;
- }
- newGroupValue = *it;
- }
+ auto localeAwareLessThan = [this](QChar c1, QChar c2) -> bool {
+ return m_collator.compare(c1, c2) < 0;
+ };
- } else {
- // Symbols from non Latin-based scripts
- newGroupValue = newFirstChar;
+ std::vector<QChar>::iterator it = std::lower_bound(lettersAtoZ.begin(), lettersAtoZ.end(), firstChar, localeAwareLessThan);
+ if (it != lettersAtoZ.end()) {
+ if (localeAwareLessThan(firstChar, *it)) {
+ // newFirstChar belongs to the group preceding *it.
+ // Example: for an umlaut 'A' in the German locale, *it would be 'B' now.
+ --it;
}
- } else if (newFirstChar >= QLatin1Char('0') && newFirstChar <= QLatin1Char('9')) {
- // Apply group '0 - 9' for any name that starts with a digit
- newGroupValue = i18nc("@title:group Groups that start with a digit", "0 - 9");
- } else {
- newGroupValue = i18nc("@title:group", "Others");
+ if (withString) {
+ groupInfo.text = *it;
+ }
+ groupInfo.comparable = (*it).unicode();
}
- if (newGroupValue != groupValue) {
- groupValue = newGroupValue;
- groups.append(QPair<int, QVariant>(i, newGroupValue));
+ } else {
+ // Symbols from non Latin-based scripts
+ if (withString) {
+ groupInfo.text = firstChar;
}
-
- firstChar = newFirstChar;
+ groupInfo.comparable = firstChar.unicode();
}
+ } else if (firstChar >= QLatin1Char('0') && firstChar <= QLatin1Char('9')) {
+ // Apply group '0 - 9' for any name that starts with a digit
+ if (withString) {
+ groupInfo.text = i18nc("@title:group Groups that start with a digit", "0 - 9");
+ }
+ groupInfo.comparable = (int)'0';
+ } else {
+ if (withString) {
+ groupInfo.text = i18nc("@title:group", "Others");
+ }
+ groupInfo.comparable = (int)'.';
}
- return groups;
+ oldWithString = withString;
+ oldFirstChar = firstChar;
+ oldGroupInfo = groupInfo;
+ return groupInfo;
}
-QList<QPair<int, QVariant>> KFileItemModel::sizeRoleGroups() const
+KFileItemModel::ItemGroupInfo KFileItemModel::sizeRoleGroup(const ItemData *itemData, bool withString) const
{
- Q_ASSERT(!m_itemData.isEmpty());
+ ItemGroupInfo groupInfo;
+ KIO::filesize_t fileSize;
- const int maxIndex = count() - 1;
- QList<QPair<int, QVariant>> groups;
+ const KFileItem item = itemData->item;
+ fileSize = !item.isNull() ? item.size() : ~0U;
- QString groupValue;
- for (int i = 0; i <= maxIndex; ++i) {
- if (isChildItem(i)) {
- continue;
- }
-
- const KFileItem &item = m_itemData.at(i)->item;
- KIO::filesize_t fileSize = !item.isNull() ? item.size() : ~0U;
- QString newGroupValue;
- if (!item.isNull() && item.isDir()) {
- if (ContentDisplaySettings::directorySizeMode() == ContentDisplaySettings::EnumDirectorySizeMode::ContentCount || m_sortDirsFirst) {
- newGroupValue = i18nc("@title:group Size", "Folders");
- } else {
- fileSize = m_itemData.at(i)->values.value("size").toULongLong();
- }
- }
-
- if (newGroupValue.isEmpty()) {
- if (fileSize < 5 * 1024 * 1024) { // < 5 MB
- newGroupValue = i18nc("@title:group Size", "Small");
- } else if (fileSize < 10 * 1024 * 1024) { // < 10 MB
- newGroupValue = i18nc("@title:group Size", "Medium");
- } else {
- newGroupValue = i18nc("@title:group Size", "Big");
- }
+ groupInfo.comparable = -1; // None
+ if (!item.isNull() && item.isDir()) {
+ if (ContentDisplaySettings::directorySizeMode() != ContentDisplaySettings::EnumDirectorySizeMode::ContentSize) {
+ groupInfo.comparable = 0; // Folders
+ } else {
+ fileSize = itemData->values.value("size").toULongLong();
}
-
- if (newGroupValue != groupValue) {
- groupValue = newGroupValue;
- groups.append(QPair<int, QVariant>(i, newGroupValue));
+ }
+ if (groupInfo.comparable < 0) {
+ if (fileSize < 5 * 1024 * 1024) { // < 5 MB
+ groupInfo.comparable = 1; // Small
+ } else if (fileSize < 10 * 1024 * 1024) { // < 10 MB
+ groupInfo.comparable = 2; // Medium
+ } else {
+ groupInfo.comparable = 3; // Big
}
}
- return groups;
+ if (withString) {
+ char const *groupNames[] = {"Folders", "Small", "Medium", "Big"};
+ groupInfo.text = i18nc("@title:group Size", groupNames[groupInfo.comparable]);
+ }
+ return groupInfo;
}
-QList<QPair<int, QVariant>> KFileItemModel::timeRoleGroups(const std::function<QDateTime(const ItemData *)> &fileTimeCb) const
+KFileItemModel::ItemGroupInfo
+KFileItemModel::timeRoleGroup(const std::function<QDateTime(const ItemData *)> &fileTimeCb, const ItemData *itemData, bool withString) const
{
- Q_ASSERT(!m_itemData.isEmpty());
-
- const int maxIndex = count() - 1;
- QList<QPair<int, QVariant>> groups;
+ static bool oldWithString;
+ static ItemGroupInfo oldGroupInfo;
+ static QDate oldFileDate;
+ ItemGroupInfo groupInfo;
const QDate currentDate = QDate::currentDate();
const int daysDistance = fileDate.daysTo(currentDate);
- QString newGroupValue;
+ 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()) {
+ if (daysDistance < 7) {
+ groupInfo.comparable = daysDistance; // Today, Yesterday and week days
+ } else if (daysDistance < 14) {
+ groupInfo.comparable = 10; // One Week Ago
+ } else if (daysDistance < 21) {
+ groupInfo.comparable = 20; // Two Weeks Ago
+ } else if (daysDistance < 28) {
+ groupInfo.comparable = 30; // Three Weeks Ago
+ } else {
+ groupInfo.comparable = 40; // Earlier This Month
+ }
+ } else {
+ const QDate lastMonthDate = currentDate.addMonths(-1);
+ if (lastMonthDate.year() == fileDate.year() && lastMonthDate.month() == fileDate.month()) {
+ if (daysDistance < 7) {
+ groupInfo.comparable = daysDistance; // Today, Yesterday and week days (Month, Year)
+ } else if (daysDistance < 14) {
+ groupInfo.comparable = 11; // One Week Ago (Month, Year)
+ } else if (daysDistance < 21) {
+ groupInfo.comparable = 21; // Two Weeks Ago (Month, Year)
+ } else if (daysDistance < 28) {
+ groupInfo.comparable = 31; // Three Weeks Ago (Month, Year)
+ } else {
+ groupInfo.comparable = 41; // Earlier on Month, Year
+ }
+ } else {
+ // The trick will fail for dates past April, 178956967 or before 1 AD.
+ groupInfo.comparable = 2147483647 - (fileDate.year() * 12 + fileDate.month() - 1); // Month, Year; newer < older
+ }
+ }
+ if (withString) {
if (currentDate.year() == fileDate.year() && currentDate.month() == fileDate.month()) {
switch (daysDistance / 7) {
case 0:
switch (daysDistance) {
case 0:
- newGroupValue = i18nc("@title:group Date", "Today");
+ groupInfo.text = i18nc("@title:group Date", "Today");
break;
case 1:
- newGroupValue = i18nc("@title:group Date", "Yesterday");
+ groupInfo.text = i18nc("@title:group Date", "Yesterday");
break;
default:
newGroupValue = locale.toString(fileTime, i18nc("@title:group Date: The week day name: dddd", "dddd"));
"Can be used to script translation of \"dddd\""
"with context @title:group Date",
"%1",
- newGroupValue);
+ groupInfo.text);
}
break;
case 1:
- newGroupValue = i18nc("@title:group Date", "One Week Ago");
+ groupInfo.text = i18nc("@title:group Date", "One Week Ago");
break;
case 2:
- newGroupValue = i18nc("@title:group Date", "Two Weeks Ago");
+ groupInfo.text = i18nc("@title:group Date", "Two Weeks Ago");
break;
case 3:
- newGroupValue = i18nc("@title:group Date", "Three Weeks Ago");
+ groupInfo.text = i18nc("@title:group Date", "Three Weeks Ago");
break;
case 4:
case 5:
- newGroupValue = i18nc("@title:group Date", "Earlier this Month");
+ groupInfo.text = i18nc("@title:group Date", "Earlier this Month");
break;
default:
Q_ASSERT(false);
"Can be used to script translation of "
"\"'Yesterday' (MMMM, yyyy)\" with context @title:group Date",
"%1",
- newGroupValue);
+ groupInfo.text);
} else {
qCWarning(DolphinDebug).nospace()
<< "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org";
"Can be used to script translation of "
"\"dddd (MMMM, yyyy)\" with context @title:group Date",
"%1",
- newGroupValue);
- } else if (daysDistance <= 7 * 2) {
+ groupInfo.text);
+ } else if (daysDistance < 7 * 2) {
const KLocalizedString format = ki18nc(
"@title:group Date: "
"MMMM is full month name in current locale, and yyyy is "
"Can be used to script translation of "
"\"'One Week Ago' (MMMM, yyyy)\" with context @title:group Date",
"%1",
- newGroupValue);
+ groupInfo.text);
} else {
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 = locale.toString(fileTime, untranslatedFormat);
}
- } else if (daysDistance <= 7 * 3) {
+ } else if (daysDistance < 7 * 3) {
const KLocalizedString format = ki18nc(
"@title:group Date: "
"MMMM is full month name in current locale, and yyyy is "
"Can be used to script translation of "
"\"'Two Weeks Ago' (MMMM, yyyy)\" with context @title:group Date",
"%1",
- newGroupValue);
+ groupInfo.text);
} else {
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 = locale.toString(fileTime, untranslatedFormat);
}
- } else if (daysDistance <= 7 * 4) {
+ } else if (daysDistance < 7 * 4) {
const KLocalizedString format = ki18nc(
"@title:group Date: "
"MMMM is full month name in current locale, and yyyy is "
"Can be used to script translation of "
"\"'Three Weeks Ago' (MMMM, yyyy)\" with context @title:group Date",
"%1",
- newGroupValue);
+ groupInfo.text);
} else {
qCWarning(DolphinDebug).nospace()
<< "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org";
"Can be used to script translation of "
"\"'Earlier on' MMMM, yyyy\" with context @title:group Date",
"%1",
- newGroupValue);
+ groupInfo.text);
} else {
qCWarning(DolphinDebug).nospace()
<< "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org";
"Can be used to script translation of "
"\"MMMM, yyyy\" with context @title:group Date",
"%1",
- newGroupValue);
+ groupInfo.text);
}
}
-
- if (newGroupValue != groupValue) {
- groupValue = newGroupValue;
- groups.append(QPair<int, QVariant>(i, newGroupValue));
- }
}
-
- return groups;
+ oldWithString = withString;
+ oldFileDate = fileDate;
+ oldGroupInfo = groupInfo;
+ return groupInfo;
}
-QList<QPair<int, QVariant>> KFileItemModel::permissionRoleGroups() const
+KFileItemModel::ItemGroupInfo KFileItemModel::permissionRoleGroup(const ItemData *itemData, bool withString) const
{
- Q_ASSERT(!m_itemData.isEmpty());
-
- const int maxIndex = count() - 1;
- QList<QPair<int, QVariant>> groups;
-
- QString permissionsString;
- QString groupValue;
- for (int i = 0; i <= maxIndex; ++i) {
- if (isChildItem(i)) {
- continue;
- }
+ static bool oldWithString;
+ static ItemGroupInfo oldGroupInfo;
+ static QFileDevice::Permissions oldPermissions;
+ ItemGroupInfo groupInfo;
- const ItemData *itemData = m_itemData.at(i);
- const QString newPermissionsString = itemData->values.value("permissions").toString();
- if (newPermissionsString == permissionsString) {
- continue;
- }
- permissionsString = newPermissionsString;
-
- const QFileInfo info(itemData->item.url().toLocalFile());
+ const QFileInfo info(itemData->item.url().toLocalFile());
+ const QFileDevice::Permissions permissions = info.permissions();
+ if (permissions == oldPermissions && withString == oldWithString) {
+ return oldGroupInfo;
+ }
+ groupInfo.comparable = (int)permissions;
+ if (withString) {
// Set user string
QString user;
- if (info.permission(QFile::ReadUser)) {
+ if (permissions & QFile::ReadUser) {
user = i18nc("@item:intext Access permission, concatenated", "Read, ");
}
- if (info.permission(QFile::WriteUser)) {
+ if (permissions & QFile::WriteUser) {
user += i18nc("@item:intext Access permission, concatenated", "Write, ");
}
- if (info.permission(QFile::ExeUser)) {
+ if (permissions & QFile::ExeUser) {
user += i18nc("@item:intext Access permission, concatenated", "Execute, ");
}
user = user.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : user.mid(0, user.length() - 2);
// Set group string
QString group;
- if (info.permission(QFile::ReadGroup)) {
+ if (permissions & QFile::ReadGroup) {
group = i18nc("@item:intext Access permission, concatenated", "Read, ");
}
- if (info.permission(QFile::WriteGroup)) {
+ if (permissions & QFile::WriteGroup) {
group += i18nc("@item:intext Access permission, concatenated", "Write, ");
}
- if (info.permission(QFile::ExeGroup)) {
+ if (permissions & QFile::ExeGroup) {
group += i18nc("@item:intext Access permission, concatenated", "Execute, ");
}
group = group.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : group.mid(0, group.length() - 2);
// Set others string
QString others;
- if (info.permission(QFile::ReadOther)) {
+ if (permissions & QFile::ReadOther) {
others = i18nc("@item:intext Access permission, concatenated", "Read, ");
}
- if (info.permission(QFile::WriteOther)) {
+ if (permissions & QFile::WriteOther) {
others += i18nc("@item:intext Access permission, concatenated", "Write, ");
}
- if (info.permission(QFile::ExeOther)) {
+ if (permissions & QFile::ExeOther) {
others += i18nc("@item:intext Access permission, concatenated", "Execute, ");
}
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;
+}
- const QString newGroupValue = i18nc("@title:group Files and folders by permissions", "User: %1 | Group: %2 | Others: %3", user, group, others);
- if (newGroupValue != groupValue) {
- groupValue = newGroupValue;
- groups.append(QPair<int, QVariant>(i, newGroupValue));
+KFileItemModel::ItemGroupInfo KFileItemModel::ratingRoleGroup(const ItemData *itemData, bool withString) const
+{
+ ItemGroupInfo groupInfo;
+ groupInfo.comparable = itemData->values.value("rating", 0).toInt();
+ 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 %i", QString::number(groupInfo.comparable));
+ }
+ return groupInfo;
+}
+
+KFileItemModel::ItemGroupInfo KFileItemModel::genericStringRoleGroup(const QByteArray &role, const ItemData *itemData) const
+{
+ return {0, itemData->values.value(role).toString()};
+}
+
+QList<QPair<int, QVariant>> KFileItemModel::nameRoleGroups() const
+{
+ Q_ASSERT(!m_itemData.isEmpty());
+
+ const int maxIndex = count() - 1;
+ QList<QPair<int, QVariant>> groups;
+
+ ItemGroupInfo groupInfo;
+ for (int i = 0; i <= maxIndex; ++i) {
+ if (isChildItem(i)) {
+ continue;
+ }
+
+ ItemGroupInfo newGroupInfo = nameRoleGroup(m_itemData.at(i));
+
+ if (newGroupInfo != groupInfo) {
+ groupInfo = newGroupInfo;
+ groups.append(QPair<int, QVariant>(i, newGroupInfo.text));
}
}
+ return groups;
+}
+
+QList<QPair<int, QVariant>> KFileItemModel::sizeRoleGroups() const
+{
+ Q_ASSERT(!m_itemData.isEmpty());
+
+ const int maxIndex = count() - 1;
+ QList<QPair<int, QVariant>> groups;
+
+ ItemGroupInfo groupInfo;
+ for (int i = 0; i <= maxIndex; ++i) {
+ if (isChildItem(i)) {
+ continue;
+ }
+
+ ItemGroupInfo newGroupInfo = sizeRoleGroup(m_itemData.at(i));
+ if (newGroupInfo != groupInfo) {
+ groupInfo = newGroupInfo;
+ groups.append(QPair<int, QVariant>(i, newGroupInfo.text));
+ }
+ }
return groups;
}
-QList<QPair<int, QVariant>> KFileItemModel::ratingRoleGroups() const
+KFileItemModel::ItemGroupInfo KFileItemModel::typeRoleGroup(const ItemData *itemData) const
+{
+ int priority = 0;
+ if (itemData->item.isDir() && m_sortDirsFirst) {
+ // Ensure folders stay first regardless of grouping order
+ if (groupOrder() == Qt::AscendingOrder) {
+ priority = -1;
+ } else {
+ priority = 1;
+ }
+ }
+ return {priority, itemData->values.value("type").toString()};
+}
+
+QList<QPair<int, QVariant>> KFileItemModel::timeRoleGroups(const std::function<QDateTime(const ItemData *)> &fileTimeCb) const
{
Q_ASSERT(!m_itemData.isEmpty());
const int maxIndex = count() - 1;
QList<QPair<int, QVariant>> groups;
- int groupValue = -1;
+ ItemGroupInfo groupInfo;
for (int i = 0; i <= maxIndex; ++i) {
if (isChildItem(i)) {
continue;
}
- const int newGroupValue = m_itemData.at(i)->values.value("rating", 0).toInt();
- if (newGroupValue != groupValue) {
- groupValue = newGroupValue;
- groups.append(QPair<int, QVariant>(i, newGroupValue));
+
+ ItemGroupInfo newGroupInfo = timeRoleGroup(fileTimeCb, m_itemData.at(i));
+
+ if (newGroupInfo != groupInfo) {
+ groupInfo = newGroupInfo;
+ groups.append(QPair<int, QVariant>(i, newGroupInfo.text));
}
}
+ return groups;
+}
+QList<QPair<int, QVariant>> KFileItemModel::permissionRoleGroups() const
+{
+ Q_ASSERT(!m_itemData.isEmpty());
+
+ const int maxIndex = count() - 1;
+ QList<QPair<int, QVariant>> groups;
+
+ ItemGroupInfo groupInfo;
+ for (int i = 0; i <= maxIndex; ++i) {
+ if (isChildItem(i)) {
+ continue;
+ }
+
+ ItemGroupInfo newGroupInfo = permissionRoleGroup(m_itemData.at(i));
+
+ if (newGroupInfo != groupInfo) {
+ groupInfo = newGroupInfo;
+ groups.append(QPair<int, QVariant>(i, newGroupInfo.text));
+ }
+ }
return groups;
}
-QList<QPair<int, QVariant>> KFileItemModel::genericStringRoleGroups(const QByteArray &role) const
+QList<QPair<int, QVariant>> KFileItemModel::ratingRoleGroups() const
{
Q_ASSERT(!m_itemData.isEmpty());
const int maxIndex = count() - 1;
QList<QPair<int, QVariant>> groups;
- bool isFirstGroupValue = true;
- QString groupValue;
+ ItemGroupInfo groupInfo;
for (int i = 0; i <= maxIndex; ++i) {
if (isChildItem(i)) {
continue;
}
- const QString newGroupValue = m_itemData.at(i)->values.value(role).toString();
- if (newGroupValue != groupValue || isFirstGroupValue) {
- groupValue = newGroupValue;
- groups.append(QPair<int, QVariant>(i, newGroupValue));
- isFirstGroupValue = false;
+
+ ItemGroupInfo newGroupInfo = ratingRoleGroup(m_itemData.at(i));
+
+ if (newGroupInfo != groupInfo) {
+ groupInfo = newGroupInfo;
+ // Using the numeric representation because Dolphin has a special
+ // case for drawing stars.
+ groups.append(QPair<int, QVariant>(i, newGroupInfo.comparable));
}
}
+ return groups;
+}
+
+QList<QPair<int, QVariant>> KFileItemModel::genericStringRoleGroups(const QByteArray &role) const
+{
+ Q_ASSERT(!m_itemData.isEmpty());
+
+ const int maxIndex = count() - 1;
+ QList<QPair<int, QVariant>> groups;
+ ItemGroupInfo groupInfo;
+ for (int i = 0; i <= maxIndex; ++i) {
+ if (isChildItem(i)) {
+ continue;
+ }
+
+ ItemGroupInfo newGroupInfo = genericStringRoleGroup(role, m_itemData.at(i));
+
+ if (newGroupInfo != groupInfo) {
+ groupInfo = newGroupInfo;
+ groups.append(QPair<int, QVariant>(i, newGroupInfo.text));
+ }
+ }
return groups;
}
static const RoleInfoMap rolesInfoMap[] = {
// clang-format off
// | role | roleType | role translation | group translation | requires Baloo | requires indexer
- { nullptr, NoRole, KLazyLocalizedString(), KLazyLocalizedString(), KLazyLocalizedString(), false, false },
+ { nullptr, NoRole, kli18nc("@label", "None"), KLazyLocalizedString(), KLazyLocalizedString(), false, false },
{ "text", NameRole, kli18nc("@label", "Name"), KLazyLocalizedString(), KLazyLocalizedString(), false, false },
{ "size", SizeRole, kli18nc("@label", "Size"), KLazyLocalizedString(), KLazyLocalizedString(), false, false },
{ "modificationtime", ModificationTimeRole, kli18nc("@label", "Modified"), KLazyLocalizedString(), kli18nc("@tooltip", "The date format can be selected in settings."), false, false },
bool requiresIndexer;
};
+ /**
+ * @return Provides static information for a role that is supported
+ * by KFileItemModel. Some roles can only be determined if
+ * Baloo is enabled and/or the Baloo indexing is enabled.
+ */
+ static RoleInfo roleInformation(const QByteArray &role);
+
/**
* @return Provides static information for all available roles that
* are supported by KFileItemModel. Some roles can only be
*/
static QList<RoleInfo> rolesInformation();
+ /**
+ * @return Provides static information for all available grouping
+ * behaviors supported by KFileItemModel but not directly
+ * mapped to roles of KFileItemModel.
+ */
+ static QList<RoleInfo> extraGroupingInformation();
+
/** set to true to hide application/x-trash files */
void setShowTrashMime(bool show);
void onGroupedSortingChanged(bool current) override;
void onSortRoleChanged(const QByteArray ¤t, const QByteArray &previous, bool resortItems = true) override;
void onSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous) override;
+ void onGroupRoleChanged(const QByteArray ¤t, const QByteArray &previous, bool resortItems = true) override;
+ void onGroupOrderChanged(Qt::SortOrder current, Qt::SortOrder previous) override;
private Q_SLOTS:
/**
- * Resorts all items dependent on the set sortRole(), sortOrder()
- * and foldersFirst() settings.
+ * Resorts all items dependent on the set sortRole(), sortOrder(),
+ * groupRole(), groupOrder() and foldersFirst() settings.
*/
void resortAllItems();
QHash<QByteArray, QVariant> values;
ItemData *parent;
};
-
- enum RemoveItemsBehavior {
- KeepItemData,
- DeleteItemData,
- DeleteItemDataIfUnfiltered
+
+ struct ItemGroupInfo {
+ int comparable;
+ QString text;
+
+ bool operator==(const ItemGroupInfo &other) const;
+ bool operator!=(const ItemGroupInfo &other) const;
+ bool operator<(const ItemGroupInfo &other) const;
};
+ enum RemoveItemsBehavior { KeepItemData, DeleteItemData, DeleteItemDataIfUnfiltered };
+
void insertItems(QList<ItemData *> &items);
void removeItems(const KItemRangeList &itemRanges, RemoveItemsBehavior behavior);
*/
QList<ItemData *> createItemDataList(const QUrl &parentUrl, const KFileItemList &items) const;
+ /**
+ * Helper method for prepareItemsForSorting().
+ * For a set role, fills 'values' of ItemData non-lazily.
+ */
+ void prepareItemsWithRole(QList<ItemData *> &itemDataList, RoleType roleType);
+
/**
* Prepares the items for sorting. Normally, the hash 'values' in ItemData is filled
* lazily to save time and memory, but for some sort roles, it is expected that the
*/
int sortRoleCompare(const ItemData *a, const ItemData *b, const QCollator &collator) const;
+ /**
+ * Helper method for lessThan() and expandedParentsCountCompare(): Compares
+ * the passed item-data using m_groupRole as criteria. Both items must
+ * have the same parent item, otherwise the comparison will be wrong.
+ */
+ int groupRoleCompare(const ItemData *a, const ItemData *b, const QCollator &collator) const;
+
int stringCompare(const QString &a, const QString &b, const QCollator &collator) const;
+ ItemGroupInfo nameRoleGroup(const ItemData *itemData, bool withString = true) const;
+ ItemGroupInfo sizeRoleGroup(const ItemData *itemData, bool withString = true) const;
+ ItemGroupInfo timeRoleGroup(const std::function<QDateTime(const ItemData *)> &fileTimeCb, const ItemData *itemData, bool withString = true) const;
+ ItemGroupInfo permissionRoleGroup(const ItemData *itemData, bool withString = true) const;
+ ItemGroupInfo ratingRoleGroup(const ItemData *itemData, bool withString = true) const;
+ ItemGroupInfo typeRoleGroup(const ItemData *itemData) const;
+ ItemGroupInfo genericStringRoleGroup(const QByteArray &role, const ItemData *itemData) const;
+
QList<QPair<int, QVariant>> nameRoleGroups() const;
QList<QPair<int, QVariant>> sizeRoleGroups() const;
QList<QPair<int, QVariant>> timeRoleGroups(const std::function<QDateTime(const ItemData *)> &fileTimeCb) const;
QList<QPair<int, QVariant>> permissionRoleGroups() const;
QList<QPair<int, QVariant>> ratingRoleGroups() const;
+ QList<QPair<int, QVariant>> typeRoleGroups() const;
QList<QPair<int, QVariant>> genericStringRoleGroups(const QByteArray &typeForRole) const;
/**
bool m_sortHiddenLast;
RoleType m_sortRole;
+ RoleType m_groupRole;
+ QByteArray m_sortExtraInfo;
+ QByteArray m_groupExtraInfo;
+
int m_sortingProgressPercent; // Value of directorySortingProgress() signal
QSet<QByteArray> m_roles;
}
}
+inline bool KFileItemModel::ItemGroupInfo::operator==(const ItemGroupInfo &other) const
+{
+ return comparable == other.comparable && text == other.text;
+}
+
+inline bool KFileItemModel::ItemGroupInfo::operator!=(const ItemGroupInfo &other) const
+{
+ return comparable != other.comparable || text != other.text;
+}
+
#endif
timer.start();
// Determine the sort role synchronously for as many items as possible.
- if (m_resolvableRoles.contains(m_model->sortRole())) {
- QList<QUrl> dirsWithAddedItems;
-
+ if (m_resolvableRoles.contains(m_model->sortRole()) || m_resolvableRoles.contains(m_model->groupRole())) {
+ QList<QUrl> dirsWithAddedItems;
int insertedCount = 0;
for (const KItemRange &range : itemRanges) {
const int lastIndex = insertedCount + range.index + range.count - 1;
QHash<QByteArray, QVariant> data;
const KFileItem item = m_model->fileItem(index);
- if (m_model->sortRole() == "type") {
+ if (m_model->sortRole() == "type" || m_model->groupRole() == "type") {
if (!item.isMimeTypeKnown()) {
item.determineMimeType();
}
data.insert("type", item.mimeComment());
- } else if (m_model->sortRole() == "size" && item.isLocalFile() && item.isDir()) {
+ } else if ((m_model->sortRole() == "size" || m_model->groupRole() == "size") && item.isLocalFile() && item.isDir()) {
startDirectorySizeCounting(item, index);
return;
} else {
/**
* Resolves the sort role of the item and applies it to the model.
+ * Despite the name, this handles both sorting and grouping, as
+ * regrouping never happens without resorting at the same time.
*/
void applySortRole(int index);
}
}
+void KItemListView::slotGroupOrderChanged(Qt::SortOrder current, Qt::SortOrder previous)
+{
+ Q_UNUSED(current)
+ Q_UNUSED(previous)
+ if (m_grouped) {
+ updateVisibleGroupHeaders();
+ doLayout(NoAnimation);
+ }
+}
+
+void KItemListView::slotGroupRoleChanged(const QByteArray ¤t, const QByteArray &previous)
+{
+ Q_UNUSED(current)
+ Q_UNUSED(previous)
+ if (m_grouped) {
+ updateVisibleGroupHeaders();
+ doLayout(NoAnimation);
+ }
+}
+
void KItemListView::slotCurrentChanged(int current, int previous)
{
// In SingleSelection mode (e.g., in the Places Panel), the current item is
disconnect(m_model, &KItemModelBase::groupedSortingChanged, this, &KItemListView::slotGroupedSortingChanged);
disconnect(m_model, &KItemModelBase::sortOrderChanged, this, &KItemListView::slotSortOrderChanged);
disconnect(m_model, &KItemModelBase::sortRoleChanged, this, &KItemListView::slotSortRoleChanged);
+ disconnect(m_model, &KItemModelBase::groupOrderChanged, this, &KItemListView::slotGroupOrderChanged);
+ disconnect(m_model, &KItemModelBase::groupRoleChanged, this, &KItemListView::slotGroupRoleChanged);
m_sizeHintResolver->itemsRemoved(KItemRangeList() << KItemRange(0, m_model->count()));
}
const int groupIndex = groupIndexForItem(index);
Q_ASSERT(groupIndex >= 0);
groupHeader->setData(groups.at(groupIndex).second);
- groupHeader->setRole(model()->sortRole());
+ groupHeader->setRole(model()->groupRole());
groupHeader->setStyleOption(m_styleOption);
groupHeader->setScrollOrientation(scrollOrientation());
groupHeader->setItemIndex(index);
virtual void slotGroupedSortingChanged(bool current);
virtual void slotSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous);
virtual void slotSortRoleChanged(const QByteArray ¤t, const QByteArray &previous);
+ virtual void slotGroupOrderChanged(Qt::SortOrder current, Qt::SortOrder previous);
+ virtual void slotGroupRoleChanged(const QByteArray ¤t, const QByteArray &previous);
virtual void slotCurrentChanged(int current, int previous);
virtual void slotSelectionChanged(const KItemSet ¤t, const KItemSet &previous);
void recycleGroupHeaderForWidget(KItemListWidget *widget);
/**
- * Helper method for slotGroupedSortingChanged(), slotSortOrderChanged()
- * and slotSortRoleChanged(): Iterates through all visible items and updates
+ * Helper method for slotGroupedSortingChanged(), slotSortOrderChanged(),
+ * slotSortRoleChanged(), slotGroupOrderChanged() and slotGroupRoleChanged():
+ * Iterates through all visible items and updates
* the group-header widgets.
*/
void updateVisibleGroupHeaders();
KItemModelBase::KItemModelBase(QObject *parent)
: QObject(parent)
- , m_groupedSorting(false)
+ , m_groupedSorting(true)
, m_sortRole()
, m_sortOrder(Qt::AscendingOrder)
+ , m_groupRole()
+ , m_groupOrder(Qt::AscendingOrder)
{
}
-KItemModelBase::KItemModelBase(const QByteArray &sortRole, QObject *parent)
+KItemModelBase::KItemModelBase(const QByteArray &sortRole, const QByteArray &groupRole, QObject *parent)
: QObject(parent)
- , m_groupedSorting(false)
+ , m_groupedSorting(true)
, m_sortRole(sortRole)
, m_sortOrder(Qt::AscendingOrder)
+ , m_groupRole(groupRole)
+ , m_groupOrder(Qt::AscendingOrder)
{
}
}
}
+void KItemModelBase::setGroupRole(const QByteArray &role, bool regroupItems)
+{
+ if (role != m_groupRole) {
+ const QByteArray previous = m_groupRole;
+ m_groupRole = role;
+ onGroupRoleChanged(role, previous, regroupItems);
+ Q_EMIT groupRoleChanged(role, previous);
+ }
+}
+
+QByteArray KItemModelBase::groupRole() const
+{
+ return m_groupRole;
+}
+
+void KItemModelBase::setGroupOrder(Qt::SortOrder order)
+{
+ if (order != m_groupOrder) {
+ const Qt::SortOrder previous = m_groupOrder;
+ m_groupOrder = order;
+ onGroupOrderChanged(order, previous);
+ Q_EMIT groupOrderChanged(order, previous);
+ }
+}
+
QString KItemModelBase::roleDescription(const QByteArray &role) const
{
return QString::fromLatin1(role);
Q_UNUSED(previous)
}
+void KItemModelBase::onGroupRoleChanged(const QByteArray ¤t, const QByteArray &previous, bool resortItems)
+{
+ Q_UNUSED(current)
+ Q_UNUSED(previous)
+ Q_UNUSED(resortItems)
+}
+
+void KItemModelBase::onGroupOrderChanged(Qt::SortOrder current, Qt::SortOrder previous)
+{
+ Q_UNUSED(current)
+ Q_UNUSED(previous)
+}
+
QUrl KItemModelBase::url(int index) const
{
return data(index).value("url").toUrl();
public:
explicit KItemModelBase(QObject *parent = nullptr);
- explicit KItemModelBase(const QByteArray &sortRole, QObject *parent = nullptr);
+ explicit KItemModelBase(const QByteArray &sortRole, const QByteArray &groupRole, QObject *parent = nullptr);
~KItemModelBase() override;
/** @return The number of items. */
void setSortOrder(Qt::SortOrder order);
Qt::SortOrder sortOrder() const;
+ /**
+ * Sets the group-role to \a role. The method KItemModelBase::onGroupRoleChanged() will be
+ * called so that model-implementations can react on the group-role change. Afterwards the
+ * signal groupRoleChanged() will be emitted.
+ * The implementation should regroup only if \a regroupItems is true.
+ */
+ void setGroupRole(const QByteArray &role, bool regroupItems = true);
+ QByteArray groupRole() const;
+
+ /**
+ * Sets the group order to \a order. The method KItemModelBase::onGroupOrderChanged() will be
+ * called so that model-implementations can react on the group order change. Afterwards the
+ * signal groupOrderChanged() will be emitted.
+ */
+ void setGroupOrder(Qt::SortOrder order);
+ Qt::SortOrder groupOrder() const;
+
/**
* @return Translated description for the \p role. The description is e.g. used
* for the header in KItemListView.
void groupedSortingChanged(bool current);
void sortRoleChanged(const QByteArray ¤t, const QByteArray &previous);
void sortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous);
+ void groupRoleChanged(const QByteArray ¤t, const QByteArray &previous);
+ void groupOrderChanged(Qt::SortOrder current, Qt::SortOrder previous);
protected:
/**
*/
virtual void onSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous);
+ /**
+ * Is invoked if the sort role has been changed by KItemModelBase::setSortRole(). Allows
+ * to react on the changed sort role before the signal sortRoleChanged() will be emitted.
+ * The implementation must assure that the items are sorted by the role given by \a current.
+ * Usually the most efficient way is to emit a
+ * itemsRemoved() signal for all items, reorder the items internally and to emit a
+ * itemsInserted() signal afterwards.
+ * The implementation should resort only if \a regroupItems is true.
+ */
+ virtual void onGroupRoleChanged(const QByteArray ¤t, const QByteArray &previous, bool regroupItems = true);
+
+ /**
+ * Is invoked if the sort order has been changed by KItemModelBase::setSortOrder(). Allows
+ * to react on the changed sort order before the signal sortOrderChanged() will be emitted.
+ * The implementation must assure that the items are sorted by the order given by \a current.
+ * Usually the most efficient way is to emit a
+ * itemsRemoved() signal for all items, reorder the items internally and to emit a
+ * itemsInserted() signal afterwards.
+ */
+ virtual void onGroupOrderChanged(Qt::SortOrder current, Qt::SortOrder previous);
+
private:
bool m_groupedSorting;
QByteArray m_sortRole;
Qt::SortOrder m_sortOrder;
+ QByteArray m_groupRole;
+ Qt::SortOrder m_groupOrder;
};
inline Qt::SortOrder KItemModelBase::sortOrder() const
return m_sortOrder;
}
+inline Qt::SortOrder KItemModelBase::groupOrder() const
+{
+ return m_groupOrder;
+}
+
#endif
m_viewProps->setHiddenFilesShown(viewProps.hiddenFilesShown());
m_viewProps->setSortRole(viewProps.sortRole());
m_viewProps->setSortOrder(viewProps.sortOrder());
+ m_viewProps->setGroupRole(viewProps.groupRole());
+ m_viewProps->setGroupOrder(viewProps.groupOrder());
KIO::ListJob *listJob = KIO::listRecursive(dir, KIO::HideProgressInfo);
connect(listJob, &KIO::ListJob::entries, this, &ApplyViewPropsJob::slotEntries);
return ContextMenuSettings::showAddToPlaces();
} else if (id == "sort") {
return ContextMenuSettings::showSortBy();
+ } else if (id == "group") {
+ return ContextMenuSettings::showGroupBy();
} else if (id == "view_mode") {
return ContextMenuSettings::showViewMode();
} else if (id == "open_in_new_tab") {
ContextMenuSettings::setShowAddToPlaces(visible);
} else if (id == "sort") {
ContextMenuSettings::setShowSortBy(visible);
+ } else if (id == "group") {
+ ContextMenuSettings::setShowGroupBy(visible);
} else if (id == "view_mode") {
ContextMenuSettings::setShowViewMode(visible);
} else if (id == "open_in_new_tab") {
<label>Show 'Sort By' in context menu.</label>
<default>true</default>
</entry>
+ <entry name="ShowGroupBy" type="Bool">
+ <label>Show 'Group By' in context menu.</label>
+ <default>true</default>
+ </entry>
<entry name="ShowViewMode" type="Bool">
<label>Show 'View Mode' in context menu.</label>
<default>true</default>
<entry name="GroupedSorting" type="Bool" >
<label context="@label">Grouped Sorting</label>
- <whatsthis context="@info:whatsthis">When this option is enabled, the sorted items are categorized into groups.</whatsthis>
- <default>false</default>
+ <whatsthis context="@info:whatsthis">When this option is enabled, the items are categorized into groups.</whatsthis>
+ <default>true</default>
</entry>
<entry name="SortRole" type="String" >
<max code="true">Qt::DescendingOrder</max>
</entry>
+ <entry name="GroupRole" type="String" >
+ <label context="@label">Group files by</label>
+ <whatsthis context="@info:whatsthis">This option defines which attribute (text, size, date, etc.) grouping is performed on.</whatsthis>
+ <default>none</default>
+ </entry>
+
+ <entry name="GroupOrder" type="Int" >
+ <label context="@label">Order in which to group files</label>
+ <default code="true">Qt::AscendingOrder</default>
+ <min code="true">Qt::AscendingOrder</min>
+ <max code="true">Qt::DescendingOrder</max>
+ </entry>
+
<entry name="SortFoldersFirst" type="Bool" >
<label context="@label">Show folders first when sorting files and folders</label>
<default>true</default>
</entry>
</group>
</kcfg>
-
-
actions,
{QStringLiteral("add_to_places"),
QStringLiteral("sort"),
+ QStringLiteral("group"),
QStringLiteral("view_mode"),
QStringLiteral("open_in_new_tab"),
QStringLiteral("open_in_new_window"),
, m_viewMode(nullptr)
, m_sortOrder(nullptr)
, m_sorting(nullptr)
+ , m_groupOrder(nullptr)
+ , m_grouping(nullptr)
, m_sortFoldersFirst(nullptr)
, m_sortHiddenLast(nullptr)
, m_previewsShown(nullptr)
// Otherwise the dialog won't resize when we collapse the KCollapsibleGroupBox.
layout->setSizeConstraint(QLayout::SetFixedSize);
- // create 'Properties' group containing view mode, sorting, sort order and show hidden files
+ // create 'Properties' group containing view mode, sorting/grouping, sort/group order and show hidden files
m_viewMode = new QComboBox();
m_viewMode->addItem(QIcon::fromTheme(QStringLiteral("view-list-icons")), i18nc("@item:inlistbox", "Icons"), DolphinView::IconsView);
m_viewMode->addItem(QIcon::fromTheme(QStringLiteral("view-list-details")), i18nc("@item:inlistbox", "Compact"), DolphinView::CompactView);
m_sorting->addItem(info.translation, info.role);
}
+ m_groupOrder = new QComboBox();
+ m_groupOrder->addItem(i18nc("@item:inlistbox Group", "Ascending"));
+ m_groupOrder->addItem(i18nc("@item:inlistbox Group", "Descending"));
+
+ m_grouping = new QComboBox();
+ const QList<KFileItemModel::RoleInfo> combinedGroupingInfo = rolesInfo + KFileItemModel::extraGroupingInformation();
+ for (const KFileItemModel::RoleInfo &info : combinedGroupingInfo) {
+ m_grouping->addItem(info.translation, info.role);
+ }
+
m_sortFoldersFirst = new QCheckBox(i18nc("@option:check", "Show folders first"));
m_sortHiddenLast = new QCheckBox(i18nc("@option:check", "Show hidden files last"));
m_previewsShown = new QCheckBox(i18nc("@option:check", "Show preview"));
sortingLayout->addWidget(m_sortOrder);
sortingLayout->addWidget(m_sorting);
+ QHBoxLayout *groupingLayout = new QHBoxLayout();
+ groupingLayout->setContentsMargins(0, 0, 0, 0);
+ groupingLayout->addWidget(m_groupOrder);
+ groupingLayout->addWidget(m_grouping);
+
layout->addRow(i18nc("@label:listbox", "View mode:"), m_viewMode);
layout->addRow(i18nc("@label:listbox", "Sorting:"), sortingLayout);
+ layout->addRow(i18nc("@label:listbox", "Grouping:"), groupingLayout);
layout->addItem(new QSpacerItem(0, Dolphin::VERTICAL_SPACER_HEIGHT, QSizePolicy::Fixed, QSizePolicy::Fixed));
connect(m_viewMode, &QComboBox::currentIndexChanged, this, &ViewPropertiesDialog::slotViewModeChanged);
connect(m_sorting, &QComboBox::currentIndexChanged, this, &ViewPropertiesDialog::slotSortingChanged);
connect(m_sortOrder, &QComboBox::currentIndexChanged, this, &ViewPropertiesDialog::slotSortOrderChanged);
+ connect(m_grouping, &QComboBox::currentIndexChanged, this, &ViewPropertiesDialog::slotGroupingChanged);
+ connect(m_groupOrder, &QComboBox::currentIndexChanged, this, &ViewPropertiesDialog::slotGroupOrderChanged);
connect(m_sortFoldersFirst, &QCheckBox::clicked, this, &ViewPropertiesDialog::slotSortFoldersFirstChanged);
connect(m_sortHiddenLast, &QCheckBox::clicked, this, &ViewPropertiesDialog::slotSortHiddenLastChanged);
connect(m_previewsShown, &QCheckBox::clicked, this, &ViewPropertiesDialog::slotShowPreviewChanged);
markAsDirty(true);
}
+void ViewPropertiesDialog::slotGroupingChanged(int index)
+{
+ const QByteArray role = m_grouping->itemData(index).toByteArray();
+ m_viewProps->setGroupRole(role);
+ markAsDirty(true);
+}
+
+void ViewPropertiesDialog::slotGroupOrderChanged(int index)
+{
+ const Qt::SortOrder groupOrder = (index == 0) ? Qt::AscendingOrder : Qt::DescendingOrder;
+ m_viewProps->setGroupOrder(groupOrder);
+ markAsDirty(true);
+}
+
void ViewPropertiesDialog::slotGroupedSortingChanged()
{
m_viewProps->setGroupedSorting(m_showInGroups->isChecked());
m_dolphinView->setViewMode(m_viewProps->viewMode());
m_dolphinView->setSortRole(m_viewProps->sortRole());
m_dolphinView->setSortOrder(m_viewProps->sortOrder());
+ m_dolphinView->setGroupRole(m_viewProps->groupRole());
+ m_dolphinView->setGroupOrder(m_viewProps->groupOrder());
m_dolphinView->setSortFoldersFirst(m_viewProps->sortFoldersFirst());
m_dolphinView->setSortHiddenLast(m_viewProps->sortHiddenLast());
m_dolphinView->setGroupedSorting(m_viewProps->groupedSorting());
m_sortFoldersFirst->setChecked(m_viewProps->sortFoldersFirst());
m_sortHiddenLast->setChecked(m_viewProps->sortHiddenLast());
+ // Load group order and sorting
+ const int groupOrderIndex = (m_viewProps->groupOrder() == Qt::AscendingOrder) ? 0 : 1;
+ m_groupOrder->setCurrentIndex(groupOrderIndex);
+
+ const QList<KFileItemModel::RoleInfo> combinedGroupingInfo = rolesInfo + KFileItemModel::extraGroupingInformation();
+ int groupRoleIndex = 0;
+ for (int i = 0; i < combinedGroupingInfo.count(); ++i) {
+ if (combinedGroupingInfo[i].role == m_viewProps->groupRole()) {
+ groupRoleIndex = i;
+ break;
+ }
+ }
+ m_grouping->setCurrentIndex(groupRoleIndex);
+
// Load show preview, show in groups and show hidden files settings
m_previewsShown->setChecked(m_viewProps->previewsShown());
m_showInGroups->setChecked(m_viewProps->groupedSorting());
void slotViewModeChanged(int index);
void slotSortingChanged(int index);
void slotSortOrderChanged(int index);
+ void slotGroupingChanged(int index);
+ void slotGroupOrderChanged(int index);
void slotGroupedSortingChanged();
void slotSortFoldersFirstChanged();
void slotSortHiddenLastChanged();
QComboBox *m_viewMode;
QComboBox *m_sortOrder;
QComboBox *m_sorting;
+ QComboBox *m_groupOrder;
+ QComboBox *m_grouping;
QCheckBox *m_sortFoldersFirst;
QCheckBox *m_sortHiddenLast;
QCheckBox *m_previewsShown;
void KFileItemModelTest::testDefaultGroupedSorting()
{
- QCOMPARE(m_model->groupedSorting(), false);
+ QCOMPARE(m_model->groupedSorting(), true);
}
void KFileItemModelTest::testNewItems()
m_testDir->createFiles({"b.txt", "c.txt", "d.txt", "e.txt"});
m_model->setGroupedSorting(true);
+ m_model->setGroupRole("text");
m_model->loadDirectory(m_testDir->url());
QVERIFY(itemsInsertedSpy.wait());
QCOMPARE(itemsInModel(),
m_testDir->createFiles({"a/b.txt", "a/c.txt", "d/e.txt", "d/f.txt"});
m_model->setGroupedSorting(true);
+ m_model->setGroupRole("text");
+
m_model->loadDirectory(m_testDir->url());
QVERIFY(itemsInsertedSpy.wait());
QCOMPARE(itemsInModel(),
QList<QPair<int, QVariant>> expectedGroups;
expectedGroups << QPair<int, QVariant>(0, QLatin1String("A"));
expectedGroups << QPair<int, QVariant>(1, QLatin1String("D"));
+
QCOMPARE(m_model->groups(), expectedGroups);
// Verify that expanding "a" and "d" will not change the groups (except for the index of "D").
return m_model->sortOrder();
}
+void DolphinView::setGroupRole(const QByteArray &role)
+{
+ if (role != groupRole()) {
+ ViewProperties props(viewPropertiesUrl());
+ props.setGroupRole(role);
+
+ KItemModelBase *model = m_container->controller()->model();
+ model->setGroupRole(role);
+
+ Q_EMIT groupRoleChanged(role);
+ }
+}
+
+QByteArray DolphinView::groupRole() const
+{
+ const KItemModelBase *model = m_container->controller()->model();
+ return model->groupRole();
+}
+
+void DolphinView::setGroupOrder(Qt::SortOrder order)
+{
+ if (groupOrder() != order) {
+ ViewProperties props(viewPropertiesUrl());
+ props.setGroupOrder(order);
+
+ m_model->setGroupOrder(order);
+
+ Q_EMIT groupOrderChanged(order);
+ }
+}
+
+Qt::SortOrder DolphinView::groupOrder() const
+{
+ return m_model->groupOrder();
+}
+
void DolphinView::setSortFoldersFirst(bool foldersFirst)
{
if (sortFoldersFirst() != foldersFirst) {
Q_EMIT sortOrderChanged(sortOrder);
}
+ const QByteArray groupRole = props.groupRole();
+ if (groupRole != m_model->groupRole()) {
+ m_model->setGroupRole(groupRole);
+ Q_EMIT groupRoleChanged(groupRole);
+ }
+
+ const Qt::SortOrder groupOrder = props.groupOrder();
+ if (groupOrder != m_model->groupOrder()) {
+ m_model->setGroupOrder(groupOrder);
+ Q_EMIT groupOrderChanged(groupOrder);
+ }
+
const bool sortFoldersFirst = props.sortFoldersFirst();
if (sortFoldersFirst != m_model->sortDirectoriesFirst()) {
m_model->setSortDirectoriesFirst(sortFoldersFirst);
* - show hidden files
* - show previews
* - enable grouping
+ * - grouping order
+ * - grouping type
*/
class DOLPHIN_EXPORT DolphinView : public QWidget
{
void setSortOrder(Qt::SortOrder order);
Qt::SortOrder sortOrder() const;
+ /**
+ * Updates the view properties of the current URL to the
+ * grouping given by \a role.
+ */
+ void setGroupRole(const QByteArray &role);
+ QByteArray groupRole() const;
+
+ /**
+ * Updates the view properties of the current URL to the
+ * sort order given by \a order.
+ */
+ void setGroupOrder(Qt::SortOrder order);
+ Qt::SortOrder groupOrder() const;
+
/** Sets a separate sorting with folders first (true) or a mixed sorting of files and folders (false). */
void setSortFoldersFirst(bool foldersFirst);
bool sortFoldersFirst() const;
/** Is emitted if the sort order (ascending or descending) has been changed. */
void sortOrderChanged(Qt::SortOrder order);
+ /** Is emitted if the grouping by name, size or date has been changed. */
+ void groupRoleChanged(const QByteArray &role);
+
+ /** Is emitted if the group order (ascending or descending) has been changed. */
+ void groupOrderChanged(Qt::SortOrder order);
+
/**
* Is emitted if the sorting of files and folders (separate with folders
* first or mixed) has been changed.
, m_actionCollection(collection)
, m_currentView(nullptr)
, m_sortByActions()
+ , m_groupByActions()
, m_visibleRoles()
{
Q_ASSERT(m_actionCollection);
connect(view, &DolphinView::groupedSortingChanged, this, &DolphinViewActionHandler::slotGroupedSortingChanged);
connect(view, &DolphinView::hiddenFilesShownChanged, this, &DolphinViewActionHandler::slotHiddenFilesShownChanged);
connect(view, &DolphinView::sortRoleChanged, this, &DolphinViewActionHandler::slotSortRoleChanged);
+ connect(view, &DolphinView::groupRoleChanged, this, &DolphinViewActionHandler::slotGroupRoleChanged);
+ connect(view, &DolphinView::groupOrderChanged, this, &DolphinViewActionHandler::slotGroupOrderChanged);
connect(view, &DolphinView::zoomLevelChanged, this, &DolphinViewActionHandler::slotZoomLevelChanged);
connect(view, &DolphinView::writeStateChanged, this, &DolphinViewActionHandler::slotWriteStateChanged);
slotWriteStateChanged(view->isFolderWritable());
sortByActionMenu->addSeparator();
- QActionGroup *group = new QActionGroup(sortByActionMenu);
- group->setExclusive(true);
+ QActionGroup *groupForSort = new QActionGroup(sortByActionMenu);
+ groupForSort->setExclusive(true);
- KToggleAction *ascendingAction = m_actionCollection->add<KToggleAction>(QStringLiteral("ascending"));
- ascendingAction->setActionGroup(group);
- connect(ascendingAction, &QAction::triggered, this, [this] {
+ KToggleAction *sortAscendingAction = m_actionCollection->add<KToggleAction>(QStringLiteral("sort_ascending"));
+ sortAscendingAction->setActionGroup(groupForSort);
+ connect(sortAscendingAction, &QAction::triggered, this, [this] {
m_currentView->setSortOrder(Qt::AscendingOrder);
});
- KToggleAction *descendingAction = m_actionCollection->add<KToggleAction>(QStringLiteral("descending"));
- descendingAction->setActionGroup(group);
- connect(descendingAction, &QAction::triggered, this, [this] {
+ KToggleAction *sortDescendingAction = m_actionCollection->add<KToggleAction>(QStringLiteral("sort_descending"));
+ sortDescendingAction->setActionGroup(groupForSort);
+ connect(sortDescendingAction, &QAction::triggered, this, [this] {
m_currentView->setSortOrder(Qt::DescendingOrder);
});
- sortByActionMenu->addAction(ascendingAction);
- sortByActionMenu->addAction(descendingAction);
+ sortByActionMenu->addAction(sortAscendingAction);
+ sortByActionMenu->addAction(sortDescendingAction);
sortByActionMenu->addSeparator();
sortByActionMenu->addAction(sortFoldersFirst);
sortByActionMenu->addAction(sortHiddenLast);
+ // View -> Group By
+ QActionGroup *groupByActionGroup = createFileItemRolesActionGroup(QStringLiteral("group_by_"));
+
+ KActionMenu *groupByActionMenu = m_actionCollection->add<KActionMenu>(QStringLiteral("group"));
+ groupByActionMenu->setIcon(QIcon::fromTheme(QStringLiteral("view-group")));
+ groupByActionMenu->setText(i18nc("@action:inmenu View", "Group By"));
+ groupByActionMenu->setPopupMode(QToolButton::InstantPopup);
+
+ const auto groupByActionGroupActions = groupByActionGroup->actions();
+ for (QAction *action : groupByActionGroupActions) {
+ groupByActionMenu->addAction(action);
+ }
+
+ groupByActionMenu->addSeparator();
+
+ QActionGroup *groupForGroup = new QActionGroup(groupByActionMenu);
+ groupForGroup->setExclusive(true);
+
+ KToggleAction *groupAscendingAction = m_actionCollection->add<KToggleAction>(QStringLiteral("group_ascending"));
+ groupAscendingAction->setActionGroup(groupForGroup);
+ connect(groupAscendingAction, &QAction::triggered, this, [this] {
+ m_currentView->setGroupOrder(Qt::AscendingOrder);
+ });
+
+ KToggleAction *groupDescendingAction = m_actionCollection->add<KToggleAction>(QStringLiteral("group_descending"));
+ groupDescendingAction->setActionGroup(groupForGroup);
+ connect(groupDescendingAction, &QAction::triggered, this, [this] {
+ m_currentView->setGroupOrder(Qt::DescendingOrder);
+ });
+
+ groupByActionMenu->addAction(groupAscendingAction);
+ groupByActionMenu->addAction(groupDescendingAction);
+
// View -> Additional Information
QActionGroup *visibleRolesGroup = createFileItemRolesActionGroup(QStringLiteral("show_"));
QActionGroup *DolphinViewActionHandler::createFileItemRolesActionGroup(const QString &groupPrefix)
{
const bool isSortGroup = (groupPrefix == QLatin1String("sort_by_"));
- Q_ASSERT(isSortGroup || groupPrefix == QLatin1String("show_"));
+ const bool isGroupGroup = (groupPrefix == QLatin1String("group_by_"));
+ Q_ASSERT(isSortGroup || isGroupGroup || groupPrefix == QLatin1String("show_"));
QActionGroup *rolesActionGroup = new QActionGroup(m_actionCollection);
- rolesActionGroup->setExclusive(isSortGroup);
+ rolesActionGroup->setExclusive(isSortGroup || isGroupGroup);
if (isSortGroup) {
connect(rolesActionGroup, &QActionGroup::triggered, this, &DolphinViewActionHandler::slotSortTriggered);
+ } else if (isGroupGroup) {
+ connect(rolesActionGroup, &QActionGroup::triggered, this, &DolphinViewActionHandler::slotGroupTriggered);
} else {
connect(rolesActionGroup, &QActionGroup::triggered, this, &DolphinViewActionHandler::toggleVisibleRole);
}
indexingEnabled = config.fileIndexingEnabled();
#endif
- const QList<KFileItemModel::RoleInfo> rolesInfo = KFileItemModel::rolesInformation();
+ QList<KFileItemModel::RoleInfo> rolesInfo = KFileItemModel::rolesInformation();
+ if (isGroupGroup) {
+ rolesInfo += KFileItemModel::extraGroupingInformation();
+ }
+
for (const KFileItemModel::RoleInfo &info : rolesInfo) {
- if (!isSortGroup && info.role == "text") {
+ if (!isSortGroup && !isGroupGroup && info.role == "text") {
// It should not be possible to hide the "text" role
continue;
}
groupMenu->setActionGroup(rolesActionGroup);
groupMenuGroup = new QActionGroup(groupMenu);
- groupMenuGroup->setExclusive(isSortGroup);
+ groupMenuGroup->setExclusive(isSortGroup || isGroupGroup);
if (isSortGroup) {
connect(groupMenuGroup, &QActionGroup::triggered, this, &DolphinViewActionHandler::slotSortTriggered);
+ } else if (isGroupGroup) {
+ connect(groupMenuGroup, &QActionGroup::triggered, this, &DolphinViewActionHandler::slotGroupTriggered);
} else {
connect(groupMenuGroup, &QActionGroup::triggered, this, &DolphinViewActionHandler::toggleVisibleRole);
}
if (isSortGroup) {
m_sortByActions.insert(info.role, action);
+ } else if (isGroupGroup) {
+ m_groupByActions.insert(info.role, action);
} else {
m_visibleRoles.insert(info.role, action);
}
slotVisibleRolesChanged(m_currentView->visibleRoles(), QList<QByteArray>());
slotGroupedSortingChanged(m_currentView->groupedSorting());
slotSortRoleChanged(m_currentView->sortRole());
+ slotGroupRoleChanged(m_currentView->groupRole());
+ slotGroupOrderChanged(m_currentView->groupOrder());
slotZoomLevelChanged(m_currentView->zoomLevel(), -1);
// Updates the "show_hidden_files" action state and icon
void DolphinViewActionHandler::slotSortOrderChanged(Qt::SortOrder order)
{
- QAction *descending = m_actionCollection->action(QStringLiteral("descending"));
- QAction *ascending = m_actionCollection->action(QStringLiteral("ascending"));
+ QAction *descending = m_actionCollection->action(QStringLiteral("sort_descending"));
+ QAction *ascending = m_actionCollection->action(QStringLiteral("sort_ascending"));
const bool sortDescending = (order == Qt::DescendingOrder);
descending->setChecked(sortDescending);
ascending->setChecked(!sortDescending);
}
+void DolphinViewActionHandler::slotGroupOrderChanged(Qt::SortOrder order)
+{
+ QAction *descending = m_actionCollection->action(QStringLiteral("group_descending"));
+ QAction *ascending = m_actionCollection->action(QStringLiteral("group_ascending"));
+ const bool groupDescending = (order == Qt::DescendingOrder);
+ descending->setChecked(groupDescending);
+ ascending->setChecked(!groupDescending);
+}
+
void DolphinViewActionHandler::slotSortFoldersFirstChanged(bool foldersFirst)
{
m_actionCollection->action(QStringLiteral("folders_first"))->setChecked(foldersFirst);
}
}
- QAction *descending = m_actionCollection->action(QStringLiteral("descending"));
- QAction *ascending = m_actionCollection->action(QStringLiteral("ascending"));
+ QAction *descending = m_actionCollection->action(QStringLiteral("sort_descending"));
+ QAction *ascending = m_actionCollection->action(QStringLiteral("sort_ascending"));
if (role == "text" || role == "type" || role == "extension" || role == "tags" || role == "comment") {
descending->setText(i18nc("Sort descending", "Z-A"));
slotSortOrderChanged(m_currentView->sortOrder());
}
+void DolphinViewActionHandler::slotGroupRoleChanged(const QByteArray &role)
+{
+ KToggleAction *action = m_groupByActions.value(role);
+ if (action) {
+ action->setChecked(true);
+
+ if (!action->icon().isNull()) {
+ QAction *groupByMenu = m_actionCollection->action(QStringLiteral("group"));
+ groupByMenu->setIcon(action->icon());
+ }
+ }
+
+ QAction *descending = m_actionCollection->action(QStringLiteral("group_descending"));
+ QAction *ascending = m_actionCollection->action(QStringLiteral("group_ascending"));
+
+ if (role == "text" || role == "type" || role == "extension" || role == "tags" || role == "comment") {
+ descending->setText(i18nc("Group descending", "Z-A"));
+ ascending->setText(i18nc("Group ascending", "A-Z"));
+ } else if (role == "size") {
+ descending->setText(i18nc("Group descending", "Largest First"));
+ ascending->setText(i18nc("Group ascending", "Smallest First"));
+ } else if (role == "modificationtime" || role == "creationtime" || role == "accesstime") {
+ descending->setText(i18nc("Group descending", "Newest First"));
+ ascending->setText(i18nc("Group ascending", "Oldest First"));
+ } else if (role == "rating") {
+ descending->setText(i18nc("Group descending", "Highest First"));
+ ascending->setText(i18nc("Group ascending", "Lowest First"));
+ } else {
+ descending->setText(i18nc("Group descending", "Descending"));
+ ascending->setText(i18nc("Group ascending", "Ascending"));
+ }
+
+ // Disable group order selector if grouping behavior does not support it
+ if (role == "none" || role == "followSort") {
+ descending->setEnabled(false);
+ ascending->setEnabled(false);
+ } else {
+ descending->setEnabled(true);
+ ascending->setEnabled(true);
+ }
+
+ slotGroupOrderChanged(m_currentView->groupOrder());
+}
+
void DolphinViewActionHandler::slotZoomLevelChanged(int current, int previous)
{
Q_UNUSED(previous)
m_currentView->setSortRole(role);
}
+void DolphinViewActionHandler::slotGroupTriggered(QAction *action)
+{
+ // The radiobuttons of the "Group By"-menu are split between the main-menu
+ // and several sub-menus. Because of this they don't have a common
+ // action-group that assures an exclusive toggle-state between the main-menu
+ // actions and the sub-menu-actions. If an action gets checked, it must
+ // be assured that all other actions get unchecked, except the ascending/
+ // descending actions
+ for (QAction *groupAction : std::as_const(m_groupByActions)) {
+ KActionMenu *actionMenu = qobject_cast<KActionMenu *>(groupAction);
+ if (actionMenu) {
+ const auto actions = actionMenu->menu()->actions();
+ for (QAction *subAction : actions) {
+ subAction->setChecked(false);
+ }
+ } else if (groupAction->actionGroup()) {
+ groupAction->setChecked(false);
+ }
+ }
+ action->setChecked(true);
+
+ // Apply the activated sort-role to the view
+ const QByteArray role = action->data().toByteArray();
+ m_currentView->setGroupRole(role);
+}
+
void DolphinViewActionHandler::slotAdjustViewProperties()
{
Q_EMIT actionBeingHandled();
*/
void slotSortRoleChanged(const QByteArray &role);
+ /**
+ * Updates the state of the 'Group Ascending/Descending' action.
+ */
+ void slotGroupOrderChanged(Qt::SortOrder order);
+
+ /**
+ * Updates the state of the 'Group by' actions.
+ */
+ void slotGroupRoleChanged(const QByteArray &role);
+
/**
* Updates the state of the 'Zoom In' and 'Zoom Out' actions.
*/
*/
void slotVisibleRolesChanged(const QList<QByteArray> ¤t, const QList<QByteArray> &previous);
+ /**
+ * Changes the grouping of the current view.
+ */
+ void slotGroupTriggered(QAction *);
+
/**
* Switches between sorting by groups or not.
*/
DolphinView *m_currentView;
QHash<QByteArray, KToggleAction *> m_sortByActions;
+ QHash<QByteArray, KToggleAction *> m_groupByActions;
QHash<QByteArray, KToggleAction *> m_visibleRoles;
};
return static_cast<Qt::SortOrder>(m_node->sortOrder());
}
+void ViewProperties::setGroupRole(const QByteArray &role)
+{
+ if (m_node->groupRole() != role) {
+ m_node->setGroupRole(role);
+ update();
+ }
+}
+
+QByteArray ViewProperties::groupRole() const
+{
+ return m_node->groupRole().toLatin1();
+}
+
+void ViewProperties::setGroupOrder(Qt::SortOrder groupOrder)
+{
+ if (m_node->groupOrder() != groupOrder) {
+ m_node->setGroupOrder(groupOrder);
+ update();
+ }
+}
+
+Qt::SortOrder ViewProperties::groupOrder() const
+{
+ return static_cast<Qt::SortOrder>(m_node->groupOrder());
+}
+
void ViewProperties::setSortFoldersFirst(bool foldersFirst)
{
if (m_node->sortFoldersFirst() != foldersFirst) {
setGroupedSorting(props.groupedSorting());
setSortRole(props.sortRole());
setSortOrder(props.sortOrder());
+ setGroupRole(props.groupRole());
+ setGroupOrder(props.groupOrder());
setSortFoldersFirst(props.sortFoldersFirst());
setSortHiddenLast(props.sortHiddenLast());
setVisibleRoles(props.visibleRoles());
void setSortOrder(Qt::SortOrder sortOrder);
Qt::SortOrder sortOrder() const;
+ void setGroupRole(const QByteArray &role);
+ QByteArray groupRole() const;
+
+ void setGroupOrder(Qt::SortOrder groupOrder);
+ Qt::SortOrder groupOrder() const;
+
void setSortFoldersFirst(bool foldersFirst);
bool sortFoldersFirst() const;