From: Zakhar Afonin Date: Sat, 28 Sep 2024 08:17:27 +0000 (+0300) Subject: Merge remote-tracking branch 'upstream/master' into work/zakharafoniam/useful-groups X-Git-Url: https://cloud.milkyroute.net/gitweb/dolphin.git/commitdiff_plain/6e752f507a1dd82a40d4bd140457203842fc0c80?hp=-c Merge remote-tracking branch 'upstream/master' into work/zakharafoniam/useful-groups --- 6e752f507a1dd82a40d4bd140457203842fc0c80 diff --combined src/dolphincontextmenu.cpp index bc00af7cc,3ce1d1d51..15c65ee56 --- a/src/dolphincontextmenu.cpp +++ b/src/dolphincontextmenu.cpp @@@ -7,6 -7,7 +7,7 @@@ #include "dolphincontextmenu.h" #include "dolphin_contextmenusettings.h" + #include "dolphin_generalsettings.h" #include "dolphinmainwindow.h" #include "dolphinnewfilemenu.h" #include "dolphinplacesmodelsingleton.h" @@@ -305,13 -306,10 +306,13 @@@ void DolphinContextMenu::addViewportCon } 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"))); } @@@ -383,9 -381,8 +384,8 @@@ bool DolphinContextMenu::placeExists(co { const KFilePlacesModel *placesModel = DolphinPlacesModelSingleton::instance().placesModel(); - const auto &matchedPlaces = placesModel->match(placesModel->index(0, 0), KFilePlacesModel::UrlRole, url, 1, Qt::MatchExactly); - - return !matchedPlaces.isEmpty(); + QModelIndex url_index = placesModel->closestItem(url); + return url_index.isValid() && placesModel->url(url_index).matches(url, QUrl::StripTrailingSlash); } QAction *DolphinContextMenu::createPasteAction() @@@ -444,7 -441,9 +444,9 @@@ void DolphinContextMenu::addOpenWithAct m_fileItemActions->insertOpenWithActionsTo(nullptr, this, QStringList{qApp->desktopFileName()}); // For a single file, hint in "Open with" menu that middle-clicking would open it in the secondary app. - if (m_selectedItems.count() == 1 && !m_fileInfo.isDir()) { + // (Unless middle-clicking would open it as a folder in a new tab (e.g. archives).) + const QUrl &url = DolphinView::openItemAsFolderUrl(m_fileInfo, GeneralSettings::browseThroughArchives()); + if (m_selectedItems.count() == 1 && url.isEmpty()) { if (QAction *openWithSubMenu = findChild(QStringLiteral("openWith_submenu"))) { Q_ASSERT(openWithSubMenu->menu()); Q_ASSERT(!openWithSubMenu->menu()->isEmpty()); diff --combined src/dolphinmainwindow.cpp index cb94e8657,54cd3bf71..17396dabd --- a/src/dolphinmainwindow.cpp +++ b/src/dolphinmainwindow.cpp @@@ -53,6 -53,7 +53,7 @@@ #include #include #include + #include #include #include #include @@@ -184,7 -185,7 +185,7 @@@ DolphinMainWindow::DolphinMainWindow( m_disabledActionNotifier = new DisabledActionNotifier(this); connect(m_disabledActionNotifier, &DisabledActionNotifier::disabledActionTriggered, this, [this](const QAction *, QString reason) { - m_activeViewContainer->showMessage(reason, DolphinViewContainer::Warning); + m_activeViewContainer->showMessage(reason, KMessageWidget::Warning); }); setupDockWidgets(); @@@ -240,6 -241,13 +241,13 @@@ DolphinMainWindow::~DolphinMainWindow( { // This fixes a crash on Wayland when closing the mainwindow while another dialog is open. disconnect(QGuiApplication::clipboard(), &QClipboard::dataChanged, this, &DolphinMainWindow::updatePasteAction); + + // This fixes a crash in dolphinmainwindowtest where the connection below fires even though the KMainWindow destructor of this object is already running. + Q_CHECK_PTR(qobject_cast(m_placesPanel->parent())); + disconnect(static_cast(m_placesPanel->parent()), + &DolphinDockWidget::visibilityChanged, + this, + &DolphinMainWindow::slotPlacesPanelVisibilityChanged); } QVector DolphinMainWindow::viewContainers() const @@@ -365,6 -373,9 +373,9 @@@ void DolphinMainWindow::changeUrl(cons updateViewActions(); updateGoActions(); + // will signal used urls to activities manager, too + m_recentFiles->addUrl(url); + Q_EMIT urlChanged(url); } @@@ -562,7 -573,7 +573,7 @@@ void DolphinMainWindow::showTarget( KIO::StatJob *statJob = static_cast(job); if (statJob->error()) { - m_activeViewContainer->showMessage(job->errorString(), DolphinViewContainer::Error); + m_activeViewContainer->showMessage(job->errorString(), KMessageWidget::Error); } else { KIO::highlightInFileManager({destinationUrl}); } @@@ -586,7 -597,7 +597,7 @@@ void DolphinMainWindow::showEvent(QShow { KXmlGuiWindow::showEvent(event); - if (!event->spontaneous()) { + if (!event->spontaneous() && m_activeViewContainer) { m_activeViewContainer->view()->setFocus(); } } @@@ -801,7 -812,7 +812,7 @@@ void DolphinMainWindow::quit( void DolphinMainWindow::showErrorMessage(const QString &message) { - m_activeViewContainer->showMessage(message, DolphinViewContainer::Error); + m_activeViewContainer->showMessage(message, KMessageWidget::Error); } void DolphinMainWindow::slotUndoAvailable(bool available) @@@ -1132,11 -1143,21 +1143,21 @@@ void DolphinMainWindow::togglePanelLock GeneralSettings::setLockPanels(newLockState); } - void DolphinMainWindow::slotTerminalPanelVisibilityChanged() + void DolphinMainWindow::slotTerminalPanelVisibilityChanged(bool visible) + { + if (!visible && m_activeViewContainer) { + m_activeViewContainer->view()->setFocus(); + } + // Putting focus to the Terminal is not handled here but in TerminalPanel::showEvent(). + } + + void DolphinMainWindow::slotPlacesPanelVisibilityChanged(bool visible) { - if (m_terminalPanel->isHiddenInVisibleWindow() && m_activeViewContainer) { + if (!visible && m_activeViewContainer) { m_activeViewContainer->view()->setFocus(); + return; } + m_placesPanel->setFocus(); } void DolphinMainWindow::goBack() @@@ -1496,7 -1517,6 +1517,7 @@@ void DolphinMainWindow::updateHamburger } 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"))); @@@ -2043,14 -2063,6 +2064,6 @@@ void DolphinMainWindow::setupActions( openTerminalHere->setIcon(QIcon::fromTheme(QStringLiteral("utilities-terminal"))); actionCollection()->setDefaultShortcut(openTerminalHere, Qt::SHIFT | Qt::ALT | Qt::Key_F4); connect(openTerminalHere, &QAction::triggered, this, &DolphinMainWindow::openTerminalHere); - - #if HAVE_TERMINAL - QAction *focusTerminalPanel = actionCollection()->addAction(QStringLiteral("focus_terminal_panel")); - focusTerminalPanel->setText(i18nc("@action:inmenu Tools", "Focus Terminal Panel")); - focusTerminalPanel->setIcon(QIcon::fromTheme(QStringLiteral("swap-panels"))); - actionCollection()->setDefaultShortcut(focusTerminalPanel, Qt::CTRL | Qt::SHIFT | Qt::Key_F4); - connect(focusTerminalPanel, &QAction::triggered, this, &DolphinMainWindow::focusTerminalPanel); - #endif } // setup 'Bookmarks' menu @@@ -2158,6 -2170,8 +2171,8 @@@ connect(openInSplitViewAction, &QAction::triggered, this, [this]() { openInSplitView(QUrl()); }); + + m_recentFiles = new KRecentFilesAction(this); } void DolphinMainWindow::setupDockWidgets() @@@ -2192,7 -2206,7 +2207,7 @@@ connect(infoPanel, &InformationPanel::urlActivated, this, &DolphinMainWindow::handleUrl); infoDock->setWidget(infoPanel); - createPanelAction(QIcon::fromTheme(QStringLiteral("dialog-information")), Qt::Key_F11, infoDock, QStringLiteral("show_information_panel")); + createPanelAction(QIcon::fromTheme(QStringLiteral("documentinfo")), Qt::Key_F11, infoDock, QStringLiteral("show_information_panel")); addDockWidget(Qt::RightDockWidgetArea, infoDock); connect(this, &DolphinMainWindow::urlChanged, infoPanel, &InformationPanel::setUrl); @@@ -2304,8 -2318,15 +2319,15 @@@ "advanced tasks. To learn more about terminals use the help features in a " "standalone terminal application like Konsole.") + panelWhatsThis); - } - #endif + + QAction *focusTerminalPanel = actionCollection()->addAction(QStringLiteral("focus_terminal_panel")); + focusTerminalPanel->setText(i18nc("@action:inmenu Tools", "Focus Terminal Panel")); + focusTerminalPanel->setToolTip(i18nc("@info:tooltip", "Move keyboard focus to and from the Terminal panel.")); + focusTerminalPanel->setIcon(QIcon::fromTheme(QStringLiteral("swap-panels"))); + actionCollection()->setDefaultShortcut(focusTerminalPanel, Qt::CTRL | Qt::SHIFT | Qt::Key_F4); + connect(focusTerminalPanel, &QAction::triggered, this, &DolphinMainWindow::toggleTerminalPanelFocus); + } // endif "shell_access" allowed + #endif // HAVE_TERMINAL if (GeneralSettings::version() < 200) { infoDock->hide(); @@@ -2317,7 -2338,7 +2339,7 @@@ 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); @@@ -2335,6 -2356,7 +2357,7 @@@ connect(m_placesPanel, &PlacesPanel::errorMessage, this, &DolphinMainWindow::showErrorMessage); connect(this, &DolphinMainWindow::urlChanged, m_placesPanel, &PlacesPanel::setUrl); connect(placesDock, &DolphinDockWidget::visibilityChanged, &DolphinUrlNavigatorsController::slotPlacesPanelVisibilityChanged); + connect(placesDock, &DolphinDockWidget::visibilityChanged, this, &DolphinMainWindow::slotPlacesPanelVisibilityChanged); connect(this, &DolphinMainWindow::settingsChanged, m_placesPanel, &PlacesPanel::readSettings); connect(m_placesPanel, &PlacesPanel::storageTearDownRequested, this, &DolphinMainWindow::slotStorageTearDownFromPlacesRequested); connect(m_placesPanel, &PlacesPanel::storageTearDownExternallyRequested, this, &DolphinMainWindow::slotStorageTearDownExternallyRequested); @@@ -2376,6 -2398,13 +2399,13 @@@ " to display it again.") + panelWhatsThis); + QAction *focusPlacesPanel = actionCollection()->addAction(QStringLiteral("focus_places_panel")); + focusPlacesPanel->setText(i18nc("@action:inmenu View", "Focus Places Panel")); + focusPlacesPanel->setToolTip(i18nc("@info:tooltip", "Move keyboard focus to and from the Places panel.")); + focusPlacesPanel->setIcon(QIcon::fromTheme(QStringLiteral("swap-panels"))); + actionCollection()->setDefaultShortcut(focusPlacesPanel, Qt::CTRL | Qt::Key_P); + connect(focusPlacesPanel, &QAction::triggered, this, &DolphinMainWindow::togglePlacesPanelFocus); + // Add actions into the "Panels" menu KActionMenu *panelsMenu = new KActionMenu(i18nc("@action:inmenu View", "Show Panels"), this); actionCollection()->addAction(QStringLiteral("panels"), panelsMenu); @@@ -2389,8 -2418,11 +2419,11 @@@ panelsMenu->addAction(ac->action(QStringLiteral("show_folders_panel"))); panelsMenu->addAction(ac->action(QStringLiteral("show_terminal_panel"))); panelsMenu->addSeparator(); - panelsMenu->addAction(actionShowAllPlaces); panelsMenu->addAction(lockLayoutAction); + panelsMenu->addSeparator(); + panelsMenu->addAction(actionShowAllPlaces); + panelsMenu->addAction(focusPlacesPanel); + panelsMenu->addAction(ac->action(QStringLiteral("focus_terminal_panel"))); connect(panelsMenu->menu(), &QMenu::aboutToShow, this, [actionShowAllPlaces] { actionShowAllPlaces->setEnabled(DolphinPlacesModelSingleton::instance().placesModel()->hiddenCount()); @@@ -2869,20 -2901,40 +2902,40 @@@ void DolphinMainWindow::saveNewToolbarC (static_cast(actionCollection()->action(KStandardAction::name(KStandardAction::HamburgerMenu))))->hideActionsOf(toolBar()); } - void DolphinMainWindow::focusTerminalPanel() + void DolphinMainWindow::toggleTerminalPanelFocus() { - if (m_terminalPanel->isVisible()) { - if (m_terminalPanel->terminalHasFocus()) { - m_activeViewContainer->view()->setFocus(Qt::FocusReason::ShortcutFocusReason); - actionCollection()->action(QStringLiteral("focus_terminal_panel"))->setText(i18nc("@action:inmenu Tools", "Focus Terminal Panel")); - } else { - m_terminalPanel->setFocus(Qt::FocusReason::ShortcutFocusReason); - actionCollection()->action(QStringLiteral("focus_terminal_panel"))->setText(i18nc("@action:inmenu Tools", "Defocus Terminal Panel")); - } - } else { - actionCollection()->action(QStringLiteral("show_terminal_panel"))->trigger(); + if (!m_terminalPanel->isVisible()) { + actionCollection()->action(QStringLiteral("show_terminal_panel"))->trigger(); // Also moves focus to the panel. actionCollection()->action(QStringLiteral("focus_terminal_panel"))->setText(i18nc("@action:inmenu Tools", "Defocus Terminal Panel")); + return; + } + + if (m_terminalPanel->terminalHasFocus()) { + m_activeViewContainer->view()->setFocus(Qt::FocusReason::ShortcutFocusReason); + actionCollection()->action(QStringLiteral("focus_terminal_panel"))->setText(i18nc("@action:inmenu Tools", "Focus Terminal Panel")); + return; } + + m_terminalPanel->setFocus(Qt::FocusReason::ShortcutFocusReason); + actionCollection()->action(QStringLiteral("focus_terminal_panel"))->setText(i18nc("@action:inmenu Tools", "Defocus Terminal Panel")); + } + + void DolphinMainWindow::togglePlacesPanelFocus() + { + if (!m_placesPanel->isVisible()) { + actionCollection()->action(QStringLiteral("show_places_panel"))->trigger(); // Also moves focus to the panel. + actionCollection()->action(QStringLiteral("focus_places_panel"))->setText(i18nc("@action:inmenu View", "Defocus Terminal Panel")); + return; + } + + if (m_placesPanel->hasFocus()) { + m_activeViewContainer->view()->setFocus(Qt::FocusReason::ShortcutFocusReason); + actionCollection()->action(QStringLiteral("focus_places_panel"))->setText(i18nc("@action:inmenu View", "Focus Places Panel")); + return; + } + + m_placesPanel->setFocus(Qt::FocusReason::ShortcutFocusReason); + actionCollection()->action(QStringLiteral("focus_places_panel"))->setText(i18nc("@action:inmenu View", "Defocus Places Panel")); } DolphinMainWindow::UndoUiInterface::UndoUiInterface() @@@ -2899,7 -2951,7 +2952,7 @@@ void DolphinMainWindow::UndoUiInterface DolphinMainWindow *mainWin = qobject_cast(parentWidget()); if (mainWin) { DolphinViewContainer *container = mainWin->activeViewContainer(); - container->showMessage(job->errorString(), DolphinViewContainer::Error); + container->showMessage(job->errorString(), KMessageWidget::Error); } else { KIO::FileUndoManager::UiInterface::jobError(job); } @@@ -2917,7 -2969,10 +2970,10 @@@ bool DolphinMainWindow::isItemVisibleIn void DolphinMainWindow::slotDoubleClickViewBackground(Qt::MouseButton button) { - Q_UNUSED(button) // might be of use later + if (button == Qt::MouseButton::LeftButton) { + // only handle left mouse button for now + return; + } GeneralSettings *settings = GeneralSettings::self(); QString clickAction = settings->doubleClickViewAction(); diff --combined src/dolphinui.rc index d884fe300,5c9afa03d..bbbfb967f --- a/src/dolphinui.rc +++ b/src/dolphinui.rc @@@ -1,6 -1,6 +1,6 @@@ - + @@@ -43,7 -43,6 +43,7 @@@ + @@@ -74,7 -73,6 +74,6 @@@ - diff --combined src/kitemviews/kfileitemmodel.cpp index 5b7b781a8,3e4a8c663..a6f90b9f5 --- a/src/kitemviews/kfileitemmodel.cpp +++ b/src/kitemviews/kfileitemmodel.cpp @@@ -34,12 -34,11 +34,12 @@@ Q_GLOBAL_STATIC(QRecursiveMutex, s_coll // #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() @@@ -388,17 -387,7 +388,17 @@@ QList> KFileItemMo 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; @@@ -432,7 -421,7 +432,7 @@@ m_groups = ratingRoleGroups(); break; default: - m_groups = genericStringRoleGroups(sortRole()); + m_groups = genericStringRoleGroups(role); break; } @@@ -897,39 -886,6 +897,39 @@@ void KFileItemModel::removeFilteredChil } } +KFileItemModel::RoleInfo KFileItemModel::roleInformation(const QByteArray &role) +{ + static QHash 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::rolesInformation() { static QList rolesInfo; @@@ -938,7 -894,24 +938,7 @@@ 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); } } @@@ -947,15 -920,6 +947,15 @@@ return rolesInfo; } +QList KFileItemModel::extraGroupingInformation() +{ + static QList 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) @@@ -966,13 -930,6 +966,13 @@@ void KFileItemModel::onSortRoleChanged( { 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 newRoles = m_roles; @@@ -992,36 -949,6 +992,36 @@@ void KFileItemModel::onSortOrderChanged 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 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; @@@ -1044,7 -971,6 +1044,7 @@@ // 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() @@@ -1110,8 -1036,7 +1110,8 @@@ } 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> oldGroups = m_groups; m_groups.clear(); @@@ -1696,7 -1621,7 +1696,7 @@@ void KFileItemModel::removeItems(const QList 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). @@@ -1720,9 -1645,9 +1720,9 @@@ return itemDataList; } -void KFileItemModel::prepareItemsForSorting(QList &itemDataList) +void KFileItemModel::prepareItemsWithRole(QList &itemDataList, RoleType roleType) { - switch (m_sortRole) { + switch (roleType) { case ExtensionRole: case PermissionsRole: case OwnerRole: @@@ -1761,12 -1686,6 +1761,12 @@@ } } +void KFileItemModel::prepareItemsForSorting(QList &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" @@@ -2100,33 -2019,31 +2100,33 @@@ bool KFileItemModel::lessThan(const Ite } } - // 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::iterator &begin, const QList::iterator &end) const @@@ -2317,108 -2234,6 +2317,108 @@@ int KFileItemModel::sortRoleCompare(con 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()); @@@ -2438,196 -2253,181 +2438,211 @@@ return QString::compare(a, b, Qt::CaseSensitive); } -QList> KFileItemModel::nameRoleGroups() const +KFileItemModel::ItemGroupInfo KFileItemModel::nameRoleGroup(const ItemData *itemData, bool withString) const { - Q_ASSERT(!m_itemData.isEmpty()); - - const int maxIndex = count() - 1; - QList> 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(); + const QString name = itemData->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. - - // Try to find a matching group in the range 'A' to 'Z'. - static std::vector 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 lettersAtoZ; + lettersAtoZ.reserve('Z' - 'A' + 1); + if (lettersAtoZ.empty()) { + for (char c = 'A'; c <= 'Z'; ++c) { + lettersAtoZ.push_back(QLatin1Char(c)); + } + } - std::vector::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::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(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> 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> 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(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> KFileItemModel::timeRoleGroups(const std::function &fileTimeCb) const +KFileItemModel::ItemGroupInfo +KFileItemModel::timeRoleGroup(const std::function &fileTimeCb, const ItemData *itemData, bool withString) const { - Q_ASSERT(!m_itemData.isEmpty()); - - const int maxIndex = count() - 1; - QList> groups; + 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); - 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: - 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", - 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); @@@ -2644,30 -2444,30 +2659,30 @@@ "'Yesterday' (MMMM, yyyy)"); const QString translatedFormat = format.toString(); if (translatedFormat.count(QLatin1Char('\'')) == 2) { - groupInfo.text = fileTime.toString(translatedFormat); - groupInfo.text = i18nc( + newGroupValue = locale.toString(fileTime, translatedFormat); + newGroupValue = i18nc( "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"; 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", - 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 " @@@ -2676,19 -2476,19 +2691,19 @@@ "'One Week Ago' (MMMM, yyyy)"); const QString translatedFormat = format.toString(); if (translatedFormat.count(QLatin1Char('\'')) == 2) { - groupInfo.text = fileTime.toString(translatedFormat); - groupInfo.text = i18nc( + 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", - 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")}); - groupInfo.text = fileTime.toString(untranslatedFormat); + 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 " @@@ -2697,19 -2497,19 +2712,19 @@@ "'Two Weeks Ago' (MMMM, yyyy)"); const QString translatedFormat = format.toString(); if (translatedFormat.count(QLatin1Char('\'')) == 2) { - groupInfo.text = fileTime.toString(translatedFormat); - groupInfo.text = i18nc( + 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", - 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")}); - groupInfo.text = fileTime.toString(untranslatedFormat); + 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 " @@@ -2718,17 -2518,17 +2733,17 @@@ "'Three Weeks Ago' (MMMM, yyyy)"); const QString translatedFormat = format.toString(); if (translatedFormat.count(QLatin1Char('\'')) == 2) { - groupInfo.text = fileTime.toString(translatedFormat); - groupInfo.text = i18nc( + 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", - 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")}); - groupInfo.text = fileTime.toString(untranslatedFormat); + newGroupValue = locale.toString(fileTime, untranslatedFormat); } } else { const KLocalizedString format = ki18nc( @@@ -2739,268 -2539,157 +2754,268 @@@ "'Earlier on' MMMM, yyyy"); const QString translatedFormat = format.toString(); if (translatedFormat.count(QLatin1Char('\'')) == 2) { - groupInfo.text = fileTime.toString(translatedFormat); - groupInfo.text = i18nc( + newGroupValue = locale.toString(fileTime, translatedFormat); + newGroupValue = i18nc( "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"; 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", - newGroupValue); + groupInfo.text); } } - - if (newGroupValue != groupValue) { - groupValue = newGroupValue; - groups.append(QPair(i, newGroupValue)); - } } - - return groups; + oldWithString = withString; + oldFileDate = fileDate; + oldGroupInfo = groupInfo; + return groupInfo; } -QList> KFileItemModel::permissionRoleGroups() const +KFileItemModel::ItemGroupInfo KFileItemModel::permissionRoleGroup(const ItemData *itemData, bool withString) const { - Q_ASSERT(!m_itemData.isEmpty()); + static bool oldWithString; + static ItemGroupInfo oldGroupInfo; + static QFileDevice::Permissions oldPermissions; + ItemGroupInfo groupInfo; - const int maxIndex = count() - 1; - QList> groups; - - QString permissionsString; - QString groupValue; - for (int i = 0; i <= maxIndex; ++i) { - if (isChildItem(i)) { - continue; - } - - 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; +} + +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> KFileItemModel::nameRoleGroups() const +{ + Q_ASSERT(!m_itemData.isEmpty()); + + const int maxIndex = count() - 1; + QList> groups; + + ItemGroupInfo groupInfo; + for (int i = 0; i <= maxIndex; ++i) { + if (isChildItem(i)) { + continue; + } + + ItemGroupInfo newGroupInfo = nameRoleGroup(m_itemData.at(i)); - 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(i, newGroupValue)); + if (newGroupInfo != groupInfo) { + groupInfo = newGroupInfo; + groups.append(QPair(i, newGroupInfo.text)); } } + return groups; +} + +QList> KFileItemModel::sizeRoleGroups() const +{ + Q_ASSERT(!m_itemData.isEmpty()); + + const int maxIndex = count() - 1; + QList> 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(i, newGroupInfo.text)); + } + } return groups; } -QList> 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> KFileItemModel::timeRoleGroups(const std::function &fileTimeCb) const { Q_ASSERT(!m_itemData.isEmpty()); const int maxIndex = count() - 1; QList> 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(i, newGroupValue)); + + ItemGroupInfo newGroupInfo = timeRoleGroup(fileTimeCb, m_itemData.at(i)); + + if (newGroupInfo != groupInfo) { + groupInfo = newGroupInfo; + groups.append(QPair(i, newGroupInfo.text)); } } + return groups; +} +QList> KFileItemModel::permissionRoleGroups() const +{ + Q_ASSERT(!m_itemData.isEmpty()); + + const int maxIndex = count() - 1; + QList> 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(i, newGroupInfo.text)); + } + } return groups; } -QList> KFileItemModel::genericStringRoleGroups(const QByteArray &role) const +QList> KFileItemModel::ratingRoleGroups() const { Q_ASSERT(!m_itemData.isEmpty()); const int maxIndex = count() - 1; QList> 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(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(i, newGroupInfo.comparable)); } } + return groups; +} + +QList> KFileItemModel::genericStringRoleGroups(const QByteArray &role) const +{ + Q_ASSERT(!m_itemData.isEmpty()); + + const int maxIndex = count() - 1; + QList> 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(i, newGroupInfo.text)); + } + } return groups; } @@@ -3037,7 -2726,7 +3052,7 @@@ const KFileItemModel::RoleInfoMap *KFil 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 }, @@@ -3168,13 -2857,14 +3183,14 @@@ bool KFileItemModel::isConsistent() con 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(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); } } diff --combined src/kitemviews/kfileitemmodel.h index 001c84701,5662d4fa8..980efe66b --- a/src/kitemviews/kfileitemmodel.h +++ b/src/kitemviews/kfileitemmodel.h @@@ -196,13 -196,6 +196,13 @@@ public 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 @@@ -211,13 -204,6 +211,13 @@@ */ static QList rolesInformation(); + /** + * @return Provides static information for all available grouping + * behaviors supported by KFileItemModel but not directly + * mapped to roles of KFileItemModel. + */ + static QList extraGroupingInformation(); + /** set to true to hide application/x-trash files */ void setShowTrashMime(bool show); @@@ -272,7 -258,7 +272,7 @@@ Q_SIGNALS * Is emitted if an error message (e.g. "Unknown location") * should be shown. */ - void errorMessage(const QString &message); + void errorMessage(const QString &message, const int kioErrorCode); /** * Is emitted if a redirection from the current URL \a oldUrl @@@ -301,13 -287,11 +301,13 @@@ protected 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(); @@@ -381,15 -365,6 +381,15 @@@ private ItemData *parent; }; + 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 &items); @@@ -403,12 -378,6 +403,12 @@@ */ QList 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 &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 @@@ -476,29 -445,13 +476,29 @@@ */ 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 &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> nameRoleGroups() const; QList> sizeRoleGroups() const; QList> timeRoleGroups(const std::function &fileTimeCb) const; QList> permissionRoleGroups() const; QList> ratingRoleGroups() const; + QList> typeRoleGroups() const; QList> genericStringRoleGroups(const QByteArray &typeForRole) const; /** @@@ -589,10 -542,6 +589,10 @@@ private bool m_sortHiddenLast; RoleType m_sortRole; + RoleType m_groupRole; + QByteArray m_sortExtraInfo; + QByteArray m_groupExtraInfo; + int m_sortingProgressPercent; // Value of directorySortingProgress() signal QSet m_roles; @@@ -651,14 -600,4 +651,14 @@@ inline bool KFileItemModel::isChildItem } } +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 diff --combined src/kitemviews/kfileitemmodelrolesupdater.cpp index 3828f0979,ac14ed795..318936e8a --- a/src/kitemviews/kfileitemmodelrolesupdater.cpp +++ b/src/kitemviews/kfileitemmodelrolesupdater.cpp @@@ -16,6 -16,7 +16,7 @@@ #include #include #include + #include #include #include #include @@@ -368,7 -369,7 +369,7 @@@ void KFileItemModelRolesUpdater::slotIt timer.start(); // Determine the sort role synchronously for as many items as possible. - if (m_resolvableRoles.contains(m_model->sortRole())) { + if (m_resolvableRoles.contains(m_model->sortRole()) || m_resolvableRoles.contains(m_model->groupRole())) { int insertedCount = 0; for (const KItemRange &range : itemRanges) { const int lastIndex = insertedCount + range.index + range.count - 1; @@@ -557,15 -558,13 +558,13 @@@ void KFileItemModelRolesUpdater::slotGo const QStringList overlays = data["iconOverlays"].toStringList(); // Strangely KFileItem::overlays() returns empty string-values, so // we need to check first whether an overlay must be drawn at all. - // It is more efficient to do it here, as KIconLoader::drawOverlays() - // assumes that an overlay will be drawn and has some additional - // setup time. if (!scaledPixmap.isNull()) { for (const QString &overlay : overlays) { if (!overlay.isEmpty()) { // There is at least one overlay, draw all overlays above m_pixmap // and cancel the check - KIconLoader::global()->drawOverlays(overlays, scaledPixmap, KIconLoader::Desktop); + const QSize size = scaledPixmap.size(); + scaledPixmap = KIconUtils::addOverlays(scaledPixmap, overlays).pixmap(size); break; } } @@@ -1218,13 -1217,13 +1217,13 @@@ void KFileItemModelRolesUpdater::applyS QHash 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 { diff --combined src/settings/contextmenu/contextmenusettingspage.cpp index 64b78d2bd,b3585e3fa..6046de4c6 --- a/src/settings/contextmenu/contextmenusettingspage.cpp +++ b/src/settings/contextmenu/contextmenusettingspage.cpp @@@ -110,8 -110,6 +110,8 @@@ bool ContextMenuSettingsPage::entryVisi 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") { @@@ -140,8 -138,6 +140,8 @@@ void ContextMenuSettingsPage::setEntryV 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") { @@@ -310,7 -306,6 +310,6 @@@ void ContextMenuSettingsPage::loadServi } m_sortModel->sort(Qt::DisplayRole); - m_searchLineEdit->setFocus(Qt::OtherFocusReason); } void ContextMenuSettingsPage::loadVersionControlSystems() diff --combined src/settings/dolphinsettingsdialog.cpp index 0fd432805,782a03ae9..577dd23e2 --- a/src/settings/dolphinsettingsdialog.cpp +++ b/src/settings/dolphinsettingsdialog.cpp @@@ -39,14 -39,6 +39,6 @@@ DolphinSettingsDialog::DolphinSettingsD setFaceType(List); setWindowTitle(i18nc("@title:window", "Configure")); - QDialogButtonBox *box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Apply | QDialogButtonBox::Cancel | QDialogButtonBox::RestoreDefaults); - box->button(QDialogButtonBox::Apply)->setEnabled(false); - box->button(QDialogButtonBox::Ok)->setDefault(true); - setButtonBox(box); - - connect(box->button(QDialogButtonBox::Ok), &QAbstractButton::clicked, this, &DolphinSettingsDialog::applySettings); - connect(box->button(QDialogButtonBox::Apply), &QAbstractButton::clicked, this, &DolphinSettingsDialog::applySettings); - connect(box->button(QDialogButtonBox::RestoreDefaults), &QAbstractButton::clicked, this, &DolphinSettingsDialog::restoreDefaults); // Interface InterfaceSettingsPage *interfaceSettingsPage = new InterfaceSettingsPage(this); @@@ -65,7 -57,6 +57,7 @@@ actions, {QStringLiteral("add_to_places"), QStringLiteral("sort"), + QStringLiteral("group"), QStringLiteral("view_mode"), QStringLiteral("open_in_new_tab"), QStringLiteral("open_in_new_window"), @@@ -113,6 -104,16 +105,16 @@@ } #endif + // Create the buttons last so they are also last in the keyboard Tab focus order. + QDialogButtonBox *box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Apply | QDialogButtonBox::Cancel | QDialogButtonBox::RestoreDefaults); + box->button(QDialogButtonBox::Apply)->setEnabled(false); + box->button(QDialogButtonBox::Ok)->setDefault(true); + setButtonBox(box); + + connect(box->button(QDialogButtonBox::Ok), &QAbstractButton::clicked, this, &DolphinSettingsDialog::applySettings); + connect(box->button(QDialogButtonBox::Apply), &QAbstractButton::clicked, this, &DolphinSettingsDialog::applySettings); + connect(box->button(QDialogButtonBox::RestoreDefaults), &QAbstractButton::clicked, this, &DolphinSettingsDialog::restoreDefaults); + const KConfigGroup dialogConfig(KSharedConfig::openStateConfig(), QStringLiteral("SettingsDialog")); KWindowConfig::restoreWindowSize(windowHandle(), dialogConfig); } diff --combined src/views/dolphinview.cpp index 9dedb9661,11c0423be..abaed9c7c --- a/src/views/dolphinview.cpp +++ b/src/views/dolphinview.cpp @@@ -231,7 -231,9 +231,9 @@@ DolphinView::DolphinView(const QUrl &ur m_versionControlObserver->setView(this); m_versionControlObserver->setModel(m_model); connect(m_versionControlObserver, &VersionControlObserver::infoMessage, this, &DolphinView::infoMessage); - connect(m_versionControlObserver, &VersionControlObserver::errorMessage, this, &DolphinView::errorMessage); + connect(m_versionControlObserver, &VersionControlObserver::errorMessage, this, [this](const QString &message) { + Q_EMIT errorMessage(message, KIO::ERR_UNKNOWN); + }); connect(m_versionControlObserver, &VersionControlObserver::operationCompletedMessage, this, &DolphinView::operationCompletedMessage); m_twoClicksRenamingTimer = new QTimer(this); @@@ -503,42 -505,6 +505,42 @@@ Qt::SortOrder DolphinView::sortOrder() 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) { @@@ -1154,7 -1120,7 +1156,7 @@@ void DolphinView::slotItemsActivated(co void DolphinView::slotItemMiddleClicked(int index) { const KFileItem &item = m_model->fileItem(index); - const QUrl &url = openItemAsFolderUrl(item); + const QUrl &url = openItemAsFolderUrl(item, GeneralSettings::browseThroughArchives()); const auto modifiers = QGuiApplication::keyboardModifiers(); if (!url.isEmpty()) { // keep in sync with KUrlNavigator::slotNavigatorButtonClicked @@@ -1489,7 -1455,7 +1491,7 @@@ void DolphinView::onDirectoryLoadingCom void DolphinView::slotJobResult(KJob *job) { if (job->error() && job->error() != KIO::ERR_USER_CANCELED) { - Q_EMIT errorMessage(job->errorString()); + Q_EMIT errorMessage(job->errorString(), job->error()); } if (!m_selectJobCreatedItems) { m_selectedUrls.clear(); @@@ -1862,7 -1828,7 +1864,7 @@@ void DolphinView::slotTrashFileFinished selectNextItem(); // Fixes BUG: 419914 via selecting next item Q_EMIT operationCompletedMessage(i18nc("@info:status", "Trash operation completed.")); } else if (job->error() != KIO::ERR_USER_CANCELED) { - Q_EMIT errorMessage(job->errorString()); + Q_EMIT errorMessage(job->errorString(), job->error()); } } @@@ -1872,7 -1838,7 +1874,7 @@@ void DolphinView::slotDeleteFileFinishe selectNextItem(); // Fixes BUG: 419914 via selecting next item Q_EMIT operationCompletedMessage(i18nc("@info:status", "Delete operation completed.")); } else if (job->error() != KIO::ERR_USER_CANCELED) { - Q_EMIT errorMessage(job->errorString()); + Q_EMIT errorMessage(job->errorString(), job->error()); } } @@@ -2084,9 -2050,9 +2086,9 @@@ void DolphinView::loadDirectory(const Q if (!url.isValid()) { const QString location(url.toDisplayString(QUrl::PreferLocalFile)); if (location.isEmpty()) { - Q_EMIT errorMessage(i18nc("@info:status", "The location is empty.")); + Q_EMIT errorMessage(i18nc("@info:status", "The location is empty."), KIO::ERR_UNKNOWN); } else { - Q_EMIT errorMessage(i18nc("@info:status", "The location '%1' is invalid.", location)); + Q_EMIT errorMessage(i18nc("@info:status", "The location '%1' is invalid.", location), KIO::ERR_UNKNOWN); } return; } @@@ -2150,18 -2116,6 +2152,18 @@@ void DolphinView::applyViewProperties(c 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); diff --combined src/views/dolphinview.h index f3c0189bf,c985f4eb9..2c772ad90 --- a/src/views/dolphinview.h +++ b/src/views/dolphinview.h @@@ -51,8 -51,6 +51,8 @@@ class QRegularExpression * - show hidden files * - show previews * - enable grouping + * - grouping order + * - grouping type */ class DOLPHIN_EXPORT DolphinView : public QWidget { @@@ -221,20 -219,6 +221,20 @@@ public 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; @@@ -538,12 -522,6 +538,12 @@@ Q_SIGNALS /** 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. @@@ -589,7 -567,7 +589,7 @@@ * Is emitted if an error message with the content \a msg * should be shown. */ - void errorMessage(const QString &msg); + void errorMessage(const QString &message, const int kioErrorCode); /** * Is emitted if an "operation completed" message with the content \a msg diff --combined src/views/viewproperties.cpp index c5afe663d,d65572f21..60c643062 --- a/src/views/viewproperties.cpp +++ b/src/views/viewproperties.cpp @@@ -120,14 -120,16 +120,16 @@@ ViewProperties::ViewProperties(const QU setViewMode(DolphinView::DetailsView); setVisibleRoles({"text", "path", "deletiontime"}); } else if (useRecentDocumentsView || useDownloadsView) { - setSortRole(QByteArrayLiteral("modificationtime")); setSortOrder(Qt::DescendingOrder); setSortFoldersFirst(false); setGroupedSorting(true); if (useRecentDocumentsView) { + setSortRole(QByteArrayLiteral("accesstime")); setViewMode(DolphinView::DetailsView); - setVisibleRoles({"text", "path", "modificationtime"}); + setVisibleRoles({"text", "path", "accesstime"}); + } else { + setSortRole(QByteArrayLiteral("modificationtime")); } } else { // The global view-properties act as default for directories without @@@ -253,32 -255,6 +255,32 @@@ Qt::SortOrder ViewProperties::sortOrder return static_cast(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(m_node->groupOrder()); +} + void ViewProperties::setSortFoldersFirst(bool foldersFirst) { if (m_node->sortFoldersFirst() != foldersFirst) { @@@ -410,8 -386,6 +412,8 @@@ void ViewProperties::setDirProperties(c setGroupedSorting(props.groupedSorting()); setSortRole(props.sortRole()); setSortOrder(props.sortOrder()); + setGroupRole(props.groupRole()); + setGroupOrder(props.groupOrder()); setSortFoldersFirst(props.sortFoldersFirst()); setSortHiddenLast(props.sortHiddenLast()); setVisibleRoles(props.visibleRoles());