]> cloud.milkyroute.net Git - dolphin.git/commitdiff
Merge remote-tracking branch 'fork/work/zakharafoniam/useful-groups' master
authormilkyroute <milkyroute@milkyroute.net>
Sun, 10 Aug 2025 11:06:17 +0000 (13:06 +0200)
committermilkyroute <milkyroute@milkyroute.net>
Sun, 10 Aug 2025 11:06:17 +0000 (13:06 +0200)
19 files changed:
1  2 
src/dolphincontextmenu.cpp
src/dolphinmainwindow.cpp
src/dolphinpart.rc
src/dolphinui.rc
src/kitemviews/kfileitemmodel.cpp
src/kitemviews/kfileitemmodel.h
src/kitemviews/kfileitemmodelrolesupdater.cpp
src/kitemviews/kfileitemmodelrolesupdater.h
src/kitemviews/kitemlistview.cpp
src/kitemviews/kitemlistview.h
src/settings/dolphin_directoryviewpropertysettings.kcfg
src/settings/dolphinsettingsdialog.cpp
src/tests/kfileitemmodeltest.cpp
src/views/dolphinview.cpp
src/views/dolphinview.h
src/views/dolphinviewactionhandler.cpp
src/views/dolphinviewactionhandler.h
src/views/viewproperties.cpp
src/views/viewproperties.h

index e1c67aad1fc53821f1d5b667fbd48e34940a0f11,15c65ee565946308a6b4eb6453614d623a004cbc..b6c70b48ab24fd60427690324c5f5b36d762d2b8
@@@ -32,7 -32,6 +32,7 @@@
  #include <QApplication>
  #include <QClipboard>
  #include <QKeyEvent>
 +#include <QAction>
  
  DolphinContextMenu::DolphinContextMenu(DolphinMainWindow *parent,
                                         const KFileItem &fileInfo,
@@@ -122,23 -121,11 +122,23 @@@ void DolphinContextMenu::addTrashContex
  {
      Q_ASSERT(m_context & TrashContext);
  
 -    QAction *emptyTrashAction = addAction(QIcon::fromTheme(QStringLiteral("trash-empty")), i18nc("@action:inmenu", "Empty Trash"), [this]() {
 +    QAction *emptyTrashAction = addAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18nc("@action:inmenu", "Empty Trash"), this, [this]() {
          Trash::empty(m_mainWindow);
      });
      emptyTrashAction->setEnabled(!Trash::isEmpty());
  
 +    // Insert 'Sort By' and 'View Mode'
 +    if (ContextMenuSettings::showSortBy() || ContextMenuSettings::showViewMode()) {
 +        addSeparator();
 +    }
 +    if (ContextMenuSettings::showSortBy()) {
 +        addAction(m_mainWindow->actionCollection()->action(QStringLiteral("sort")));
 +    }
 +    if (ContextMenuSettings::showViewMode()) {
 +        addAction(m_mainWindow->actionCollection()->action(QStringLiteral("view_mode")));
 +    }
 +
 +    addSeparator();
      QAction *propertiesAction = m_mainWindow->actionCollection()->action(QStringLiteral("properties"));
      addAction(propertiesAction);
  }
@@@ -148,38 -135,23 +148,38 @@@ void DolphinContextMenu::addTrashItemCo
      Q_ASSERT(m_context & TrashContext);
      Q_ASSERT(m_context & ItemContext);
  
 -    addAction(QIcon::fromTheme("restoration"), i18nc("@action:inmenu", "Restore"), [this]() {
 -        QList<QUrl> selectedUrls;
 -        selectedUrls.reserve(m_selectedItems.count());
 -        for (const KFileItem &item : std::as_const(m_selectedItems)) {
 -            selectedUrls.append(item.url());
 -        }
 +    addAction(QIcon::fromTheme(QStringLiteral("edit-reset")),
 +              i18ncp("@action:inmenu Restore the selected files that are in the trash to the place they lived at the moment they were trashed. Minimize the "
 +                     "length of this string if possible.",
 +                     "Restore to Former Location",
 +                     "Restore to Former Locations",
 +                     m_selectedItems.count()),
 +              this,
 +              [this]() {
 +                  QList<QUrl> selectedUrls;
 +                  selectedUrls.reserve(m_selectedItems.count());
 +                  for (const KFileItem &item : std::as_const(m_selectedItems)) {
 +                      selectedUrls.append(item.url());
 +                  }
 +
 +                  KIO::RestoreJob *job = KIO::restoreFromTrash(selectedUrls);
 +                  KJobWidgets::setWindow(job, m_mainWindow);
 +                  job->uiDelegate()->setAutoErrorHandlingEnabled(true);
 +              });
  
 -        KIO::RestoreJob *job = KIO::restoreFromTrash(selectedUrls);
 -        KJobWidgets::setWindow(job, m_mainWindow);
 -        job->uiDelegate()->setAutoErrorHandlingEnabled(true);
 -    });
 +    addSeparator();
 +
 +    addAction(m_mainWindow->actionCollection()->action(KStandardAction::name(KStandardAction::Cut)));
 +    addAction(m_mainWindow->actionCollection()->action(KStandardAction::name(KStandardAction::Copy)));
 +
 +    addSeparator();
  
      QAction *deleteAction = m_mainWindow->actionCollection()->action(KStandardAction::name(KStandardAction::DeleteFile));
      addAction(deleteAction);
  
 -    QAction *propertiesAction = m_mainWindow->actionCollection()->action(QStringLiteral("properties"));
 -    addAction(propertiesAction);
 +    addSeparator();
 +
 +    addAction(m_mainWindow->actionCollection()->action(QStringLiteral("properties")));
  }
  
  void DolphinContextMenu::addDirectoryItemContextMenu()
      addOpenWithActions();
  
      // set up 'Create New' menu
 -    DolphinNewFileMenu *newFileMenu = new DolphinNewFileMenu(m_mainWindow->actionCollection()->action(QStringLiteral("create_dir")), m_mainWindow);
 +    QAction *newDirAction = m_mainWindow->actionCollection()->action(QStringLiteral("create_dir"));
 +    QAction *newFileAction = m_mainWindow->actionCollection()->action(QStringLiteral("create_file"));
 +    DolphinNewFileMenu *newFileMenu = new DolphinNewFileMenu(newDirAction, newFileAction, this);
      newFileMenu->checkUpToDate();
      newFileMenu->setWorkingDirectory(m_fileInfo.url());
      newFileMenu->setEnabled(selectedItemsProps.supportsWriting());
@@@ -229,11 -199,7 +229,11 @@@ void DolphinContextMenu::addOpenParentF
      });
  
      addAction(QIcon::fromTheme(QStringLiteral("tab-new")), i18nc("@action:inmenu", "Open Path in New Tab"), [this]() {
 -        m_mainWindow->openNewTab(KIO::upUrl(m_fileInfo.targetUrl()));
 +        const QUrl url = m_fileInfo.targetUrl();
 +        const QUrl parentUrl = KIO::upUrl(url);
 +        DolphinTabPage *tabPage = m_mainWindow->openNewTab(parentUrl);
 +        tabPage->activeViewContainer()->view()->markUrlsAsSelected({url});
 +        tabPage->activeViewContainer()->view()->markUrlAsCurrent(url);
      });
  
      addAction(QIcon::fromTheme(QStringLiteral("window-new")), i18nc("@action:inmenu", "Open Path in New Window"), [this]() {
@@@ -340,10 -306,13 +340,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")));
      }
@@@ -367,7 -336,7 +370,7 @@@ void DolphinContextMenu::insertDefaultI
      addAction(collection->action(KStandardAction::name(KStandardAction::Cut)));
      addAction(collection->action(KStandardAction::name(KStandardAction::Copy)));
      if (ContextMenuSettings::showCopyLocation()) {
 -        QAction *copyPathAction = collection->action(QString("copy_location"));
 +        QAction *copyPathAction = collection->action(QStringLiteral("copy_location"));
          copyPathAction->setEnabled(m_selectedItems.size() == 1);
          addAction(copyPathAction);
      }
index 51772eac28b1abb7897abe3d58387f32f9298472,17396dabd90d966b701a7bb581c916360ec22b5c..d08d6f6b396fe75bc9dda6cb48d67eabe354e41d
  #include "panels/folders/folderspanel.h"
  #include "panels/places/placespanel.h"
  #include "panels/terminal/terminalpanel.h"
 +#include "search/dolphinquery.h"
  #include "selectionmode/actiontexthelper.h"
  #include "settings/dolphinsettingsdialog.h"
 +#include "statusbar/diskspaceusagemenu.h"
  #include "statusbar/dolphinstatusbar.h"
  #include "views/dolphinnewfilemenuobserver.h"
  #include "views/dolphinremoteencoding.h"
@@@ -43,7 -41,6 +43,7 @@@
  #include <KColorSchemeManager>
  #include <KConfig>
  #include <KConfigGui>
 +#include <KDesktopFile>
  #include <KDialogJobUiDelegate>
  #include <KDualAction>
  #include <KFileItemListProperties>
@@@ -57,7 -54,6 +57,7 @@@
  #include <KProtocolInfo>
  #include <KProtocolManager>
  #include <KRecentFilesAction>
 +#include <KRuntimePlatform>
  #include <KShell>
  #include <KShortcutsDialog>
  #include <KStandardAction>
@@@ -127,7 -123,6 +127,7 @@@ DolphinMainWindow::DolphinMainWindow(
      , m_forwardAction(nullptr)
      , m_splitViewAction(nullptr)
      , m_splitViewMenuAction(nullptr)
 +    , m_diskSpaceUsageMenu(nullptr)
      , m_sessionSaveTimer(nullptr)
      , m_sessionSaveWatcher(nullptr)
      , m_sessionSaveScheduled(false)
      m_actionHandler = new DolphinViewActionHandler(actionCollection(), m_actionTextHelper, this);
      connect(m_actionHandler, &DolphinViewActionHandler::actionBeingHandled, this, &DolphinMainWindow::clearStatusBar);
      connect(m_actionHandler, &DolphinViewActionHandler::createDirectoryTriggered, this, &DolphinMainWindow::createDirectory);
 +    connect(m_actionHandler, &DolphinViewActionHandler::createFileTriggered, this, &DolphinMainWindow::createFile);
      connect(m_actionHandler, &DolphinViewActionHandler::selectionModeChangeTriggered, this, &DolphinMainWindow::slotSetSelectionMode);
  
 -    Q_CHECK_PTR(actionCollection()->action(QStringLiteral("create_dir")));
 -    m_newFileMenu->setNewFolderShortcutAction(actionCollection()->action(QStringLiteral("create_dir")));
 +    QAction *newDirAction = actionCollection()->action(QStringLiteral("create_dir"));
 +    Q_CHECK_PTR(newDirAction);
 +    m_newFileMenu->setNewFolderShortcutAction(newDirAction);
 +
 +    QAction *newFileAction = actionCollection()->action(QStringLiteral("create_file"));
 +    Q_CHECK_PTR(newFileAction);
 +    m_newFileMenu->setNewFileShortcutAction(newFileAction);
  
      m_remoteEncoding = new DolphinRemoteEncoding(this, m_actionHandler);
      connect(this, &DolphinMainWindow::urlChanged, m_remoteEncoding, &DolphinRemoteEncoding::slotAboutToOpenUrl);
  
      setupDockWidgets();
  
 -    setupGUI(Save | Create | ToolBar);
 +    const bool usePhoneUi{KRuntimePlatform::runtimePlatform().contains(QLatin1String("phone"))};
 +    setupGUI(Save | Create | ToolBar, usePhoneUi ? QStringLiteral("dolphinuiforphones.rc") : QString() /* load the default dolphinui.rc file */);
      stateChanged(QStringLiteral("new_file"));
  
      QClipboard *clipboard = QApplication::clipboard();
  
      if (firstRun) {
          menuBar()->setVisible(false);
 +
 +        if (usePhoneUi) {
 +            Q_ASSERT(qobject_cast<QDockWidget *>(m_placesPanel->parent()));
 +            m_placesPanel->parentWidget()->hide();
 +            auto settings = GeneralSettings::self();
 +            settings->setShowZoomSlider(false); // Zooming can be done with pinch gestures instead and we are short on horizontal space.
 +            settings->setRenameInline(false); // This works around inline renaming currently not working well with virtual keyboards.
 +            settings->save(); // Otherwise the RenameInline setting is not picked up for the first time Dolphin is used.
 +        }
      }
  
      const bool showMenu = !menuBar()->isHidden();
      }
  
      updateAllowedToolbarAreas();
 +    updateNavigatorsBackground();
  
      // enable middle-click on back/forward/up to open in a new tab
      auto *middleClickEventFilter = new MiddleClickActionEventFilter(this);
@@@ -394,10 -372,9 +394,10 @@@ void DolphinMainWindow::changeUrl(cons
      updatePasteAction();
      updateViewActions();
      updateGoActions();
 +    m_diskSpaceUsageMenu->setUrl(url);
  
      // will signal used urls to activities manager, too
 -    m_recentFiles->addUrl(url);
 +    m_recentFiles->addUrl(url, QString(), "inode/directory");
  
      Q_EMIT urlChanged(url);
  }
@@@ -409,9 -386,9 +409,9 @@@ void DolphinMainWindow::slotTerminalDir
          m_tearDownFromPlacesRequested = false;
      }
  
 -    m_activeViewContainer->setAutoGrabFocus(false);
 +    m_activeViewContainer->setGrabFocusOnUrlChange(false);
      changeUrl(url);
 -    m_activeViewContainer->setAutoGrabFocus(true);
 +    m_activeViewContainer->setGrabFocusOnUrlChange(true);
  }
  
  void DolphinMainWindow::slotEditableStateChanged(bool editable)
@@@ -492,7 -469,7 +492,7 @@@ void DolphinMainWindow::addToPlaces(
      }
      if (url.isValid()) {
          QString icon;
 -        if (m_activeViewContainer->isSearchModeEnabled()) {
 +        if (isSearchUrl(url)) {
              icon = QStringLiteral("folder-saved-search-symbolic");
          } else {
              icon = KIO::iconNameForUrl(url);
      }
  }
  
 -void DolphinMainWindow::openNewTab(const QUrl &url)
 +DolphinTabPage *DolphinMainWindow::openNewTab(const QUrl &url)
  {
 -    m_tabWidget->openNewTab(url, QUrl());
 +    return m_tabWidget->openNewTab(url, QUrl());
  }
  
  void DolphinMainWindow::openNewTabAndActivate(const QUrl &url)
@@@ -763,8 -740,6 +763,8 @@@ void DolphinMainWindow::slotSaveSession
          KConfig *config = KConfigGui::sessionConfig();
          saveGlobalProperties(config);
          savePropertiesInternal(config, 1);
 +        KConfigGroup group = config->group(QStringLiteral("Number"));
 +        group.writeEntry("NumberOfWindows", 1); // Makes session restore aware that there is a window to restore.
  
          auto future = QtConcurrent::run([config]() {
              config->sync();
@@@ -830,15 -805,6 +830,15 @@@ void DolphinMainWindow::createDirectory
      }
  }
  
 +void DolphinMainWindow::createFile()
 +{
 +    // Use the same logic as in createDirectory()
 +    if (!m_newFileMenu->isCreateFileRunning()) {
 +        m_newFileMenu->setWorkingDirectory(activeViewContainer()->url());
 +        m_newFileMenu->createFile();
 +    }
 +}
 +
  void DolphinMainWindow::quit()
  {
      close();
@@@ -899,14 -865,13 +899,14 @@@ void DolphinMainWindow::paste(
  
  void DolphinMainWindow::find()
  {
 -    m_activeViewContainer->setSearchModeEnabled(true);
 +    m_activeViewContainer->setSearchBarVisible(true);
 +    m_activeViewContainer->setFocusToSearchBar();
  }
  
  void DolphinMainWindow::updateSearchAction()
  {
      QAction *toggleSearchAction = actionCollection()->action(QStringLiteral("toggle_search"));
 -    toggleSearchAction->setChecked(m_activeViewContainer->isSearchModeEnabled());
 +    toggleSearchAction->setChecked(m_activeViewContainer->isSearchBarVisible());
  }
  
  void DolphinMainWindow::updatePasteAction()
@@@ -943,13 -908,9 +943,13 @@@ QAction *DolphinMainWindow::urlNavigato
  {
      const QUrl url = urlNavigator->locationUrl(historyIndex);
  
 -    QString text = url.toDisplayString(QUrl::PreferLocalFile);
 +    QString text;
  
 -    if (!urlNavigator->showFullPath()) {
 +    if (isSearchUrl(url)) {
 +        text = Search::DolphinQuery(url, QUrl{}).title();
 +    } else if (urlNavigator->showFullPath()) {
 +        text = url.toDisplayString(QUrl::PreferLocalFile);
 +    } else {
          const KFilePlacesModel *placesModel = DolphinPlacesModelSingleton::instance().placesModel();
  
          const QModelIndex closestIdx = placesModel->closestItem(url);
@@@ -1009,7 -970,6 +1009,7 @@@ void DolphinMainWindow::slotAboutToShow
      const KUrlNavigator *urlNavigator = m_activeViewContainer->urlNavigatorInternalWithHistory();
      int entries = 0;
      QMenu *menu = m_forwardAction->popupMenu();
 +    menu->clear();
      for (int i = urlNavigator->historyIndex() - 1; i >= 0 && entries < MaxNumberOfNavigationentries; --i, ++entries) {
          QAction *action = urlNavigatorHistoryAction(urlNavigator, i, menu);
          menu->addAction(action);
@@@ -1055,15 -1015,8 +1055,15 @@@ void DolphinMainWindow::invertSelection
  
  void DolphinMainWindow::toggleSplitView()
  {
 +    QUrl newSplitViewUrl;
 +    const KFileItemList list = m_activeViewContainer->view()->selectedItems();
 +    if (list.count() == 1) {
 +        const KFileItem &item = list.first();
 +        newSplitViewUrl = DolphinView::openItemAsFolderUrl(item);
 +    }
 +
      DolphinTabPage *tabPage = m_tabWidget->currentTabPage();
 -    tabPage->setSplitViewEnabled(!tabPage->splitViewEnabled(), WithAnimation);
 +    tabPage->setSplitViewEnabled(!tabPage->splitViewEnabled(), WithAnimation, newSplitViewUrl);
      m_tabWidget->updateTabName(m_tabWidget->indexOf(tabPage));
      updateViewActions();
  }
@@@ -1165,10 -1118,8 +1165,10 @@@ void DolphinMainWindow::replaceLocation
  
      // If the text field currently has focus and everything is selected,
      // pressing the keyboard shortcut returns the whole thing to breadcrumb mode
 +    // and goes back to the view, just like how it was before this action was triggered the first time.
      if (navigator->isUrlEditable() && lineEdit->hasFocus() && lineEdit->selectedText() == lineEdit->text()) {
          navigator->setUrlEditable(false);
 +        m_activeViewContainer->view()->setFocus();
      } else {
          navigator->setUrlEditable(true);
          navigator->setFocus();
@@@ -1474,8 -1425,6 +1474,8 @@@ void DolphinMainWindow::slotWriteStateC
      newFileMenu()->setEnabled(isFolderWritable && m_activeViewContainer->url().scheme() != QLatin1String("trash"));
      // When the menu is disabled, actions in it are disabled later in the event loop, and we need to set the disabled reason after that.
      QTimer::singleShot(0, this, [this]() {
 +        m_disabledActionNotifier->setDisabledReason(actionCollection()->action(QStringLiteral("create_file")),
 +                                                    i18nc("@info", "Cannot create new file: You do not have permission to create items in this folder."));
          m_disabledActionNotifier->setDisabledReason(actionCollection()->action(QStringLiteral("create_dir")),
                                                      i18nc("@info", "Cannot create new folder: You do not have permission to create items in this folder."));
      });
@@@ -1511,7 -1460,7 +1511,7 @@@ void DolphinMainWindow::updateHamburger
          menu = new QMenu(this);
          hamburgerMenu->setMenu(menu);
          hamburgerMenu->hideActionsOf(ac->action(QStringLiteral("basic_actions"))->menu());
 -        hamburgerMenu->hideActionsOf(ac->action(QStringLiteral("zoom"))->menu());
 +        hamburgerMenu->hideActionsOf(qobject_cast<KToolBarPopupAction *>(ac->action(QStringLiteral("zoom")))->popupMenu());
      } else {
          menu->clear();
      }
      // The third group contains actions to change what one sees in the view
      // and to change the more general UI.
      if (!toolBar()->isVisible()
 -        || (!toolbarActions.contains(ac->action(QStringLiteral("icons"))) && !toolbarActions.contains(ac->action(QStringLiteral("compact")))
 -            && !toolbarActions.contains(ac->action(QStringLiteral("details"))) && !toolbarActions.contains(ac->action(QStringLiteral("view_mode"))))) {
 +        || ((!toolbarActions.contains(ac->action(QStringLiteral("icons"))) && !toolbarActions.contains(ac->action(QStringLiteral("compact")))
 +             && !toolbarActions.contains(ac->action(QStringLiteral("details"))) && !toolbarActions.contains(ac->action(QStringLiteral("view_mode"))))
 +            && !toolbarActions.contains(ac->action(QStringLiteral("view_settings"))))) {
          menu->addAction(ac->action(QStringLiteral("view_mode")));
      }
 -    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")));
 +    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")));
  
@@@ -1597,8 -1544,6 +1598,8 @@@ void DolphinMainWindow::slotPlaceActiva
          // We can end up here if the user clicked a device in the Places Panel
          // which had been unmounted earlier, see https://bugs.kde.org/show_bug.cgi?id=161385.
          reloadView();
 +
 +        m_activeViewContainer->view()->setFocus(); // We always want the focus on the view after activating a place.
      } else {
          view->disableUrlNavigatorSelectionRequests();
          changeUrl(url);
@@@ -1619,6 -1564,9 +1620,6 @@@ void DolphinMainWindow::activeViewChang
      m_activeViewContainer = viewContainer;
  
      if (oldViewContainer) {
 -        const QAction *toggleSearchAction = actionCollection()->action(QStringLiteral("toggle_search"));
 -        toggleSearchAction->disconnect(oldViewContainer);
 -
          // Disconnect all signals between the old view container (container,
          // view and url navigator) and main window.
          oldViewContainer->disconnect(this);
          if (auto secondaryUrlNavigator = navigators->secondaryUrlNavigator()) {
              secondaryUrlNavigator->disconnect(this);
          }
 +        oldViewContainer->disconnect(m_diskSpaceUsageMenu);
  
          // except the requestItemInfo so that on hover the information panel can still be updated
          connect(oldViewContainer->view(), &DolphinView::requestItemInfo, this, &DolphinMainWindow::requestItemInfo);
      updateViewActions();
      updateGoActions();
      updateSearchAction();
 +    connect(m_diskSpaceUsageMenu,
 +            &DiskSpaceUsageMenu::showMessage,
 +            viewContainer,
 +            [viewContainer](const QString &message, KMessageWidget::MessageType messageType) {
 +                viewContainer->showMessage(message, messageType);
 +            });
 +    connect(m_diskSpaceUsageMenu, &DiskSpaceUsageMenu::showInstallationProgress, viewContainer, &DolphinViewContainer::showProgress);
  
      const QUrl url = viewContainer->url();
      Q_EMIT urlChanged(url);
 +    Q_EMIT selectionChanged(m_activeViewContainer->view()->selectedItems());
  }
  
  void DolphinMainWindow::tabCountChanged(int count)
@@@ -1727,11 -1666,7 +1728,11 @@@ void DolphinMainWindow::setViewsToHomeI
  {
      const QVector<DolphinViewContainer *> theViewContainers = viewContainers();
      for (DolphinViewContainer *viewContainer : theViewContainers) {
 -        if (viewContainer && viewContainer->url().toLocalFile().startsWith(mountPath)) {
 +        if (!viewContainer) {
 +            continue;
 +        }
 +        const auto viewPath = viewContainer->url().toLocalFile();
 +        if (viewPath.startsWith(mountPath + QLatin1String("/")) || viewPath == mountPath) {
              viewContainer->setUrl(QUrl::fromLocalFile(QDir::homePath()));
          }
      }
@@@ -1743,7 -1678,7 +1744,7 @@@ void DolphinMainWindow::setupActions(
      auto hamburgerMenuAction = KStandardAction::hamburgerMenu(nullptr, nullptr, actionCollection());
  
      // setup 'File' menu
 -    m_newFileMenu = new DolphinNewFileMenu(nullptr, this);
 +    m_newFileMenu = new DolphinNewFileMenu(nullptr, nullptr, this);
      actionCollection()->addAction(QStringLiteral("new_menu"), m_newFileMenu);
      QMenu *menu = m_newFileMenu->menu();
      menu->setTitle(i18nc("@title:menu Create new folder, file, link, etc.", "Create New"));
                                        "<para>This helps you "
                                        "find files and folders by opening a <emphasis>search bar</emphasis>. "
                                        "There you can enter search terms and specify settings to find the "
 -                                      "items you are looking for.</para><para>Use this help again on "
 -                                      "the search bar so we can have a look at it while the settings are "
 -                                      "explained.</para>"));
 +                                      "items you are looking for.</para>"));
  
      // toggle_search acts as a copy of the main searchAction to be used mainly
      // in the toolbar, with no default shortcut attached, to avoid messing with
      toggleSearchAction->setToolTip(searchAction->toolTip());
      toggleSearchAction->setWhatsThis(searchAction->whatsThis());
      toggleSearchAction->setCheckable(true);
 +    connect(toggleSearchAction, &QAction::triggered, this, [this](bool checked) {
 +        if (checked) {
 +            find();
 +        } else {
 +            m_activeViewContainer->setSearchBarVisible(false);
 +        }
 +    });
  
      QAction *toggleSelectionModeAction = actionCollection()->addAction(QStringLiteral("toggle_selection_mode"));
      // i18n: This action toggles a selection mode.
      compareFiles->setEnabled(false);
      connect(compareFiles, &QAction::triggered, this, &DolphinMainWindow::compareFiles);
  
 +    QAction *manageDiskSpaceUsage = actionCollection()->addAction(QStringLiteral("manage_disk_space"));
 +    manageDiskSpaceUsage->setText(i18nc("@action:inmenu Tools", "Manage Disk Space Usage"));
 +    manageDiskSpaceUsage->setIcon(QIcon::fromTheme(QStringLiteral("filelight")));
 +    m_diskSpaceUsageMenu = new DiskSpaceUsageMenu{this};
 +    manageDiskSpaceUsage->setMenu(m_diskSpaceUsageMenu);
 +
      QAction *openPreferredSearchTool = actionCollection()->addAction(QStringLiteral("open_preferred_search_tool"));
      openPreferredSearchTool->setText(i18nc("@action:inmenu Tools", "Open Preferred Search Tool"));
      openPreferredSearchTool->setWhatsThis(xi18nc("@info:whatsthis",
      connect(openPreferredSearchTool, &QAction::triggered, this, &DolphinMainWindow::openPreferredSearchTool);
  
      if (KAuthorized::authorize(QStringLiteral("shell_access"))) {
 +        // Get icon of user default terminal emulator application
 +        const KConfigGroup group(KSharedConfig::openConfig(QStringLiteral("kdeglobals"), KConfig::SimpleConfig), QStringLiteral("General"));
 +        const QString terminalDesktopFilename = group.readEntry("TerminalService");
 +        // Use utilities-terminal icon from theme if readEntry() has failed
 +        const QString terminalIcon = terminalDesktopFilename.isEmpty() ? "utilities-terminal" : KDesktopFile(terminalDesktopFilename).readIcon();
 +
          QAction *openTerminal = actionCollection()->addAction(QStringLiteral("open_terminal"));
          openTerminal->setText(i18nc("@action:inmenu Tools", "Open Terminal"));
          openTerminal->setWhatsThis(xi18nc("@info:whatsthis",
                                            "<para>This opens a <emphasis>terminal</emphasis> application for the viewed location.</para>"
                                            "<para>To learn more about terminals use the help features in the terminal application.</para>"));
 -        openTerminal->setIcon(QIcon::fromTheme(QStringLiteral("utilities-terminal")));
 +        openTerminal->setIcon(QIcon::fromTheme(terminalIcon));
          actionCollection()->setDefaultShortcut(openTerminal, Qt::SHIFT | Qt::Key_F4);
          connect(openTerminal, &QAction::triggered, this, &DolphinMainWindow::openTerminal);
  
          openTerminalHere->setWhatsThis(xi18nc("@info:whatsthis",
                                                "<para>This opens <emphasis>terminal</emphasis> applications for the selected items' locations.</para>"
                                                "<para>To learn more about terminals use the help features in the terminal application.</para>"));
 -        openTerminalHere->setIcon(QIcon::fromTheme(QStringLiteral("utilities-terminal")));
 +        openTerminalHere->setIcon(QIcon::fromTheme(terminalIcon));
          actionCollection()->setDefaultShortcut(openTerminalHere, Qt::SHIFT | Qt::ALT | Qt::Key_F4);
          connect(openTerminalHere, &QAction::triggered, this, &DolphinMainWindow::openTerminalHere);
      }
              &DolphinMainWindow::toggleShowMenuBar,
              Qt::QueuedConnection);
  
 -    KToggleAction *showStatusBar = KStandardAction::showStatusbar(nullptr, nullptr, actionCollection());
 -    showStatusBar->setChecked(GeneralSettings::showStatusBar());
 -    connect(GeneralSettings::self(), &GeneralSettings::showStatusBarChanged, showStatusBar, &KToggleAction::setChecked);
 -    connect(showStatusBar, &KToggleAction::triggered, this, [this](bool checked) {
 -        GeneralSettings::setShowStatusBar(checked);
 -        refreshViews();
 -    });
 -
      KStandardAction::keyBindings(this, &DolphinMainWindow::slotKeyBindings, actionCollection());
      KStandardAction::preferences(this, &DolphinMainWindow::editSettings, actionCollection());
  
  
      QAction *openInSplitViewAction = actionCollection()->addAction(QStringLiteral("open_in_split_view"));
      openInSplitViewAction->setText(i18nc("@action:inmenu", "Open in Split View"));
 -    openInSplitViewAction->setIcon(QIcon::fromTheme(QStringLiteral("view-right-new")));
 +    openInSplitViewAction->setIcon(QIcon::fromTheme(QStringLiteral("view-split-left-right")));
      connect(openInSplitViewAction, &QAction::triggered, this, [this]() {
          openInSplitView(QUrl());
      });
@@@ -2413,7 -2339,7 +2414,7 @@@ void DolphinMainWindow::setupDockWidget
      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);
@@@ -2518,7 -2444,7 +2519,7 @@@ void DolphinMainWindow::updateFileAndEd
      QAction *addToPlacesAction = col->action(QStringLiteral("add_to_places"));
      QAction *copyToOtherViewAction = col->action(QStringLiteral("copy_to_inactive_split_view"));
      QAction *moveToOtherViewAction = col->action(QStringLiteral("move_to_inactive_split_view"));
 -    QAction *copyLocation = col->action(QString("copy_location"));
 +    QAction *copyLocation = col->action(QStringLiteral("copy_location"));
  
      if (list.isEmpty()) {
          stateChanged(QStringLiteral("has_no_selection"));
@@@ -2677,11 -2603,14 +2678,11 @@@ void DolphinMainWindow::connectViewSign
      connect(container, &DolphinViewContainer::showFilterBarChanged, this, &DolphinMainWindow::updateFilterBarAction);
      connect(container, &DolphinViewContainer::writeStateChanged, this, &DolphinMainWindow::slotWriteStateChanged);
      slotWriteStateChanged(container->view()->isFolderWritable());
 -    connect(container, &DolphinViewContainer::searchModeEnabledChanged, this, &DolphinMainWindow::updateSearchAction);
 +    connect(container, &DolphinViewContainer::searchBarVisibilityChanged, this, &DolphinMainWindow::updateSearchAction);
      connect(container, &DolphinViewContainer::captionChanged, this, &DolphinMainWindow::updateWindowTitle);
      connect(container, &DolphinViewContainer::tabRequested, this, &DolphinMainWindow::openNewTab);
      connect(container, &DolphinViewContainer::activeTabRequested, this, &DolphinMainWindow::openNewTabAndActivate);
  
 -    const QAction *toggleSearchAction = actionCollection()->action(QStringLiteral("toggle_search"));
 -    connect(toggleSearchAction, &QAction::triggered, container, &DolphinViewContainer::setSearchModeEnabled);
 -
      // Make the toggled state of the selection mode actions visually follow the selection mode state of the view.
      auto toggleSelectionModeAction = actionCollection()->action(QStringLiteral("toggle_selection_mode"));
      toggleSelectionModeAction->setChecked(m_activeViewContainer->isSelectionModeEnabled());
@@@ -2736,16 -2665,12 +2737,16 @@@ void DolphinMainWindow::updateSplitActi
              m_splitViewAction->setText(i18nc("@action:intoolbar Close left view", "Close"));
              m_splitViewAction->setToolTip(i18nc("@info", "Close left view"));
              m_splitViewAction->setIcon(QIcon::fromTheme(QStringLiteral("view-left-close")));
 +            m_splitViewMenuAction->setText(i18nc("@action:inmenu Close left view", "Close Left View"));
 +
              popoutSplitAction->setText(i18nc("@action:intoolbar Move left view to a new window", "Pop out Left View"));
              popoutSplitAction->setToolTip(i18nc("@info", "Move left view to a new window"));
          } else {
              m_splitViewAction->setText(i18nc("@action:intoolbar Close right view", "Close"));
              m_splitViewAction->setToolTip(i18nc("@info", "Close right view"));
              m_splitViewAction->setIcon(QIcon::fromTheme(QStringLiteral("view-right-close")));
 +            m_splitViewMenuAction->setText(i18nc("@action:inmenu Close left view", "Close Right View"));
 +
              popoutSplitAction->setText(i18nc("@action:intoolbar Move right view to a new window", "Pop out Right View"));
              popoutSplitAction->setToolTip(i18nc("@info", "Move right view to a new window"));
          }
          }
      } else {
          m_splitViewAction->setText(i18nc("@action:intoolbar Split view", "Split"));
 +        m_splitViewMenuAction->setText(m_splitViewAction->text());
          m_splitViewAction->setToolTip(i18nc("@info", "Split view"));
 -        m_splitViewAction->setIcon(QIcon::fromTheme(QStringLiteral("view-right-new")));
 +        m_splitViewAction->setIcon(QIcon::fromTheme(QStringLiteral("view-split-left-right")));
          popoutSplitAction->setText(i18nc("@action:intoolbar Move view in focus to a new window", "Pop out"));
          popoutSplitAction->setEnabled(false);
          if (m_splitViewAction->menu()) {
      }
  
      // Update state from toolbar action
 -    m_splitViewMenuAction->setText(m_splitViewAction->text());
      m_splitViewMenuAction->setToolTip(m_splitViewAction->toolTip());
      m_splitViewMenuAction->setIcon(m_splitViewAction->icon());
  }
@@@ -2788,12 -2713,6 +2789,12 @@@ void DolphinMainWindow::updateAllowedTo
      }
  }
  
 +void DolphinMainWindow::updateNavigatorsBackground()
 +{
 +    auto navigators = static_cast<DolphinNavigatorsWidgetAction *>(actionCollection()->action(QStringLiteral("url_navigators")));
 +    navigators->setBackgroundEnabled(navigators->isInToolbar());
 +}
 +
  bool DolphinMainWindow::isKompareInstalled() const
  {
      static bool initialized = false;
@@@ -2980,7 -2899,6 +2981,7 @@@ void DolphinMainWindow::saveNewToolbarC
          m_tabWidget->currentTabPage()->insertNavigatorsWidget(navigators);
      }
      updateAllowedToolbarAreas();
 +    updateNavigatorsBackground();
      (static_cast<KHamburgerMenu *>(actionCollection()->action(KStandardAction::name(KStandardAction::HamburgerMenu))))->hideActionsOf(toolBar());
  }
  
@@@ -3052,7 -2970,7 +3053,7 @@@ bool DolphinMainWindow::isItemVisibleIn
  
  void DolphinMainWindow::slotDoubleClickViewBackground(Qt::MouseButton button)
  {
 -    if (button == Qt::MouseButton::LeftButton) {
 +    if (button != Qt::MouseButton::LeftButton) {
          // only handle left mouse button for now
          return;
      }
diff --combined src/dolphinpart.rc
index 5f57f586ab568a79d89bfcc5873331fe99aa1975,13f0f2172bfb3e1d3ac9bbae42fc4db3c92fa55f..5dacc2b81382656cf88480354571582834a2b1d9
@@@ -1,6 -1,6 +1,6 @@@
  <?xml version="1.0"?>
  <!DOCTYPE gui SYSTEM "kpartgui.dtd">
 -<gui name="dolphinpart" version="16" translationDomain="dolphin">
 +<gui name="dolphinpart" version="18" translationDomain="dolphin">
   <MenuBar>
    <Menu name="edit"><text>&amp;Edit</text>
     <Action name="new_menu"/>
@@@ -23,6 -23,7 +23,7 @@@
    </Menu>
    <Menu name="view"><text>&amp;View</text>
     <Action name="sort" />
+    <Action name="group" />
     <Action name="additional_info" />
     <Action name="show_preview" />
     <Action name="show_in_groups" />
diff --combined src/dolphinui.rc
index 3e6e4c463034e4ec100188a0f623eedc7da619f3,bbbfb967fbf282331012d3f70d1295e497df0645..b611b91703d885f96b30ac851feda68732335d4c
@@@ -1,6 -1,6 +1,6 @@@
  <?xml version="1.0"?>
  <!DOCTYPE gui SYSTEM "kpartgui.dtd">
 -<gui name="dolphin" version="41">
 +<gui name="dolphin" version="46">
      <MenuBar>
          <Menu name="file">
              <Action name="new_menu" />
@@@ -43,6 -43,7 +43,7 @@@
              <Action name="view_zoom_out"/>
              <Separator/>
              <Action name="sort" />
+             <Action name="group" />
              <Action name="view_mode" />
              <Action name="additional_info" />
              <Action name="show_preview" />
@@@ -73,7 -74,6 +74,7 @@@
              <Action name="open_preferred_search_tool" />
              <Action name="open_terminal" />
              <Action name="open_terminal_here" />
 +            <Action name="manage_disk_space" />
              <Action name="compare_files" />
              <Action name="change_remote_encoding" />
          </Menu>
          <text context="@title:menu">Main Toolbar</text>
          <Action name="go_back" />
          <Action name="go_forward" />
 -        <Separator name="separator_1" />
 -        <Action name="icons" />
 -        <Action name="compact" />
 -        <Action name="details" />
 +        <Action name="view_settings" />
          <Action name="url_navigators" />
          <Action name="split_view" />
          <Action name="split_stash" />
          <Action priority="0" name="icons"/>
          <Action priority="0" name="compact"/>
          <Action priority="0" name="details"/>
 +        <Action priority="0" name="view_mode" />
 +        <Action priority="0" name="view_settings" />
          <Action priority="0" name="view_zoom_in"/>
          <Action priority="0" name="view_zoom_reset"/>
          <Action priority="0" name="view_zoom_out"/>
index 4386bca16b6ada60a808a9764ff83ed8d74826ed,a6f90b9f5adcf4ad0158ffe43c92b293eb456720..603c16e0d37ff816488aa69b9122308335a36d45
@@@ -20,9 -20,6 +20,9 @@@
  #include <KLocalizedString>
  #include <KUrlMimeData>
  
 +#ifndef QT_NO_ACCESSIBILITY
 +#include <QAccessible>
 +#endif
  #include <QElapsedTimer>
  #include <QIcon>
  #include <QMimeData>
@@@ -37,11 -34,12 +37,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()
@@@ -205,20 -203,13 +206,20 @@@ bool KFileItemModel::setData(int index
          return false;
      }
  
 -    m_itemData[index]->values = currentValues;
      if (changedRoles.contains("text")) {
          QUrl url = m_itemData[index]->item.url();
 +        m_items.remove(url);
          url = url.adjusted(QUrl::RemoveFilename);
          url.setPath(url.path() + currentValues["text"].toString());
          m_itemData[index]->item.setUrl(url);
 +        m_items.insert(url, index);
 +
 +        if (!changedRoles.contains("url")) {
 +            changedRoles.insert("url");
 +            currentValues["url"] = url;
 +        }
      }
 +    m_itemData[index]->values = currentValues;
  
      emitItemsChangedAndTriggerResorting(KItemRangeList() << KItemRange(index, 1), changedRoles);
  
@@@ -335,32 -326,16 +336,32 @@@ QMimeData *KFileItemModel::createMimeDa
      return data;
  }
  
 +namespace
 +{
 +QString removeMarks(const QString &original)
 +{
 +    const auto normalized = original.normalized(QString::NormalizationForm_D);
 +    QString res;
 +    for (auto ch : normalized) {
 +        if (!ch.isMark()) {
 +            res.append(ch);
 +        }
 +    }
 +    return res;
 +}
 +}
 +
  int KFileItemModel::indexForKeyboardSearch(const QString &text, int startFromIndex) const
  {
 +    const auto noMarkText = removeMarks(text);
      startFromIndex = qMax(0, startFromIndex);
      for (int i = startFromIndex; i < count(); ++i) {
 -        if (fileItem(i).text().startsWith(text, Qt::CaseInsensitive)) {
 +        if (removeMarks(fileItem(i).text()).startsWith(noMarkText, Qt::CaseInsensitive)) {
              return i;
          }
      }
      for (int i = 0; i < startFromIndex; ++i) {
 -        if (fileItem(i).text().startsWith(text, Qt::CaseInsensitive)) {
 +        if (removeMarks(fileItem(i).text()).startsWith(noMarkText, Qt::CaseInsensitive)) {
              return i;
          }
      }
@@@ -413,7 -388,17 +414,17 @@@ QList<QPair<int, QVariant>> 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;
              m_groups = ratingRoleGroups();
              break;
          default:
-             m_groups = genericStringRoleGroups(sortRole());
+             m_groups = genericStringRoleGroups(role);
              break;
          }
  
@@@ -912,6 -897,39 +923,39 @@@ void KFileItemModel::removeFilteredChil
      }
  }
  
+ 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)
@@@ -956,6 -966,13 +992,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<QByteArray> newRoles = m_roles;
@@@ -975,6 -992,36 +1018,36 @@@ void KFileItemModel::onSortOrderChanged
      resortAllItems();
  }
  
+ void KFileItemModel::onGroupRoleChanged(const QByteArray &current, 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();
@@@ -1647,7 -1696,7 +1722,7 @@@ void KFileItemModel::removeItems(const 
  
  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"
@@@ -2045,31 -2100,33 +2126,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<KFileItemModel::ItemData *>::iterator &begin, const QList<KFileItemModel::ItemData *>::iterator &end) const
@@@ -2260,29 -2317,114 +2343,131 @@@ 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());
  
      if (m_naturalSorting) {
 -        return collator.compare(a, b);
 +        // Split extension, taking into account it can be empty
 +        constexpr QString::SectionFlags flags = QString::SectionSkipEmpty | QString::SectionIncludeLeadingSep;
 +
 +        // Sort by baseName first
 +        const QString aBaseName = a.section('.', 0, 0, flags);
 +        const QString bBaseName = b.section('.', 0, 0, flags);
 +
 +        const int res = collator.compare(aBaseName, bBaseName);
 +        if (res != 0 || (aBaseName.length() == a.length() && bBaseName.length() == b.length())) {
 +            return res;
 +        }
 +
 +        // sliced() has undefined behavior when pos < 0 or pos > size().
 +        Q_ASSERT(aBaseName.length() <= a.length() && aBaseName.length() >= 0);
 +        Q_ASSERT(bBaseName.length() <= b.length() && bBaseName.length() >= 0);
 +
 +        // baseNames were equal, sort by extension
 +        return collator.compare(a.sliced(aBaseName.length()), b.sliced(bBaseName.length()));
      }
  
      const int result = QString::compare(a, b, collator.caseSensitivity());
      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();
+     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<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);
                          "part of the text that should not be formatted as a date",
                          "'Yesterday' (MMMM, yyyy)");
                      const QString translatedFormat = format.toString();
 -                    if (translatedFormat.count(QLatin1Char('\'')) == 2) {
 +                    if (const int count = translatedFormat.count(QLatin1Char('\'')); count >= 2 && count % 2 == 0) {
                          newGroupValue = locale.toString(fileTime, translatedFormat);
                          newGroupValue = i18nc(
                              "Can be used to script translation of "
                              "\"'Yesterday' (MMMM, yyyy)\" with context @title:group Date",
                              "%1",
-                             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 "
                          "part of the text that should not be formatted as a date",
                          "'One Week Ago' (MMMM, yyyy)");
                      const QString translatedFormat = format.toString();
 -                    if (translatedFormat.count(QLatin1Char('\'')) == 2) {
 +                    if (const int count = translatedFormat.count(QLatin1Char('\'')); count >= 2 && count % 2 == 0) {
                          newGroupValue = locale.toString(fileTime, translatedFormat);
                          newGroupValue = i18nc(
                              "Can be used to script translation of "
                              "\"'One Week Ago' (MMMM, yyyy)\" with context @title:group Date",
                              "%1",
-                             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 "
                          "part of the text that should not be formatted as a date",
                          "'Two Weeks Ago' (MMMM, yyyy)");
                      const QString translatedFormat = format.toString();
 -                    if (translatedFormat.count(QLatin1Char('\'')) == 2) {
 +                    if (const int count = translatedFormat.count(QLatin1Char('\'')); count >= 2 && count % 2 == 0) {
                          newGroupValue = locale.toString(fileTime, translatedFormat);
                          newGroupValue = i18nc(
                              "Can be used to script translation of "
                              "\"'Two Weeks Ago' (MMMM, yyyy)\" with context @title:group Date",
                              "%1",
-                             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 "
                          "part of the text that should not be formatted as a date",
                          "'Three Weeks Ago' (MMMM, yyyy)");
                      const QString translatedFormat = format.toString();
 -                    if (translatedFormat.count(QLatin1Char('\'')) == 2) {
 +                    if (const int count = translatedFormat.count(QLatin1Char('\'')); count >= 2 && count % 2 == 0) {
                          newGroupValue = locale.toString(fileTime, translatedFormat);
                          newGroupValue = i18nc(
                              "Can be used to script translation of "
                              "\"'Three Weeks Ago' (MMMM, yyyy)\" with context @title:group Date",
                              "%1",
-                             newGroupValue);
+                             groupInfo.text);
                      } else {
                          qCWarning(DolphinDebug).nospace()
                              << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org";
                          "part of the text that should not be formatted as a date",
                          "'Earlier on' MMMM, yyyy");
                      const QString translatedFormat = format.toString();
 -                    if (translatedFormat.count(QLatin1Char('\'')) == 2) {
 +                    if (const int count = translatedFormat.count(QLatin1Char('\'')); count >= 2 && count % 2 == 0) {
                          newGroupValue = locale.toString(fileTime, translatedFormat);
                          newGroupValue = i18nc(
                              "Can be used to script translation of "
                              "\"'Earlier on' MMMM, yyyy\" with context @title:group Date",
                              "%1",
-                             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());
+     static bool oldWithString;
+     static ItemGroupInfo oldGroupInfo;
+     static QFileDevice::Permissions oldPermissions;
+     ItemGroupInfo groupInfo;
  
-     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;
-         }
-         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<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));
  
-         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));
+         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;
  }
  
@@@ -2769,7 -3052,7 +3095,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 },
          { "releaseYear",         ReleaseYearRole,         kli18nc("@label", "Release Year"),         kli18nc("@label", "Audio"),    KLazyLocalizedString(),                    true,            true  },
          { "aspectRatio",         AspectRatioRole,         kli18nc("@label", "Aspect Ratio"),         kli18nc("@label", "Video"),    KLazyLocalizedString(),                    true,            true  },
          { "frameRate",           FrameRateRole,           kli18nc("@label", "Frame Rate"),           kli18nc("@label", "Video"),    KLazyLocalizedString(),                    true,            true  },
 +        { "duration",            DurationRole,            kli18nc("@label", "Duration"),             kli18nc("@label", "Video"),    KLazyLocalizedString(),                    true,            true  },
          { "path",                PathRole,                kli18nc("@label", "Path"),                 kli18nc("@label", "Other"),    KLazyLocalizedString(),                    false,           false },
          { "extension",           ExtensionRole,           kli18nc("@label", "File Extension"),       kli18nc("@label", "Other"),    KLazyLocalizedString(),                    false,           false },
          { "deletiontime",        DeletionTimeRole,        kli18nc("@label", "Deletion Time"),        kli18nc("@label", "Other"),    KLazyLocalizedString(),                    false,           false },
index 10be27128832d3bb8e02ff87bdf55a23ec2d0de4,980efe66b26c1108a06996c36683d4d75ed83a50..13554d8c7fa1999dc62e8fd355c71967096ca9e5
@@@ -196,6 -196,13 +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
       */
      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);
  
@@@ -287,11 -301,13 +301,13 @@@ protected
      void onGroupedSortingChanged(bool current) override;
      void onSortRoleChanged(const QByteArray &current, const QByteArray &previous, bool resortItems = true) override;
      void onSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous) override;
+     void onGroupRoleChanged(const QByteArray &current, 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();
  
@@@ -364,13 -380,18 +380,18 @@@ private
          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;
  
      /**
@@@ -546,6 -589,10 +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<QByteArray> m_roles;
  
@@@ -592,9 -639,7 +639,9 @@@ inline bool KFileItemModel::isRoleValue
  
  inline bool KFileItemModel::nameLessThan(const ItemData *a, const ItemData *b)
  {
 -    return a->item.text() < b->item.text();
 +    // Split extension, taking into account it can be empty
 +    constexpr QString::SectionFlags flags = QString::SectionSkipEmpty | QString::SectionIncludeLeadingSep;
 +    return a->item.text().section('.', 0, 0, flags) < b->item.text().section('.', 0, 0, flags);
  }
  
  inline bool KFileItemModel::isChildItem(int index) const
      }
  }
  
+ 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
index 497c13b525c6da3708d597057ea792585fdcec37,318936e8a69ed6fe98cfb1c88f2b9b210dc2edff..44165dedcb0ed3a903cd09d688a10acab412b4d5
@@@ -369,29 -369,20 +369,28 @@@ void KFileItemModelRolesUpdater::slotIt
      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;
              for (int i = insertedCount + range.index; i <= lastIndex; ++i) {
 +                const auto fileItem = m_model->fileItem(i);
 +                const auto fileItemParentFolderUrl = fileItem.url().adjusted(QUrl::RemoveFilename);
 +                if (!dirsWithAddedItems.contains(fileItemParentFolderUrl)) {
 +                    dirsWithAddedItems.append(fileItemParentFolderUrl);
 +                }
                  if (timer.elapsed() < MaxBlockTimeout) {
                      applySortRole(i);
                  } else {
 -                    m_pendingSortRoleItems.insert(m_model->fileItem(i));
 +                    m_pendingSortRoleItems.insert(fileItem);
                  }
              }
              insertedCount += range.count;
          }
  
 +        recountDirectoryItems(dirsWithAddedItems);
 +
          applySortProgressToModel();
  
          // If there are still items whose sort role is unknown, check if the
@@@ -448,25 -439,17 +447,25 @@@ void KFileItemModelRolesUpdater::slotIt
              m_directoryContentsCounter->stopWorker();
          }
      } else {
 +        QList<QUrl> dirsWithDeletedItems;
          // Only remove the items from m_finishedItems. They will be removed
          // from the other sets later on.
          QSet<KFileItem>::iterator it = m_finishedItems.begin();
          while (it != m_finishedItems.end()) {
              if (m_model->index(*it) < 0) {
 +                // Get the folder path of the file.
 +                const auto folderUrl = it->url().adjusted(QUrl::RemoveFilename);
 +                if (!dirsWithDeletedItems.contains(folderUrl)) {
 +                    dirsWithDeletedItems.append(folderUrl);
 +                }
                  it = m_finishedItems.erase(it);
              } else {
                  ++it;
              }
          }
  
 +        recountDirectoryItems(dirsWithDeletedItems);
 +
          // Removed items won't have hover previews loaded anymore.
          for (const KItemRange &itemRange : itemRanges) {
              int index = itemRange.index;
@@@ -568,14 -551,30 +567,14 @@@ void KFileItemModelRolesUpdater::slotGo
          return;
      }
  
 -    QPixmap scaledPixmap = transformPreviewPixmap(pixmap);
 -
      QHash<QByteArray, QVariant> data = rolesData(item, index);
 -
 -    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.
 -    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
 -                const QSize size = scaledPixmap.size();
 -                scaledPixmap = KIconUtils::addOverlays(scaledPixmap, overlays).pixmap(size);
 -                break;
 -            }
 -        }
 -    }
 -
 -    data.insert("iconPixmap", scaledPixmap);
 +    data.insert("iconPixmap", transformPreviewPixmap(pixmap));
 +    data.insert("supportsSequencing", m_previewJob->handlesSequences());
  
      disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
      m_model->setData(index, data);
      connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
 +    Q_EMIT previewJobFinished(); // For unit testing
  
      m_finishedItems.insert(item);
  }
@@@ -977,6 -976,13 +976,6 @@@ void KFileItemModelRolesUpdater::startP
          return;
      }
  
 -    // PreviewJob internally caches items always with the size of
 -    // 128 x 128 pixels or 256 x 256 pixels. A (slow) downscaling is done
 -    // by PreviewJob if a smaller size is requested. For images KFileItemModelRolesUpdater must
 -    // do a downscaling anyhow because of the frame, so in this case only the provided
 -    // cache sizes are requested.
 -    const QSize cacheSize = (m_iconSize.width() > 128) || (m_iconSize.height() > 128) ? QSize(256, 256) : QSize(128, 128);
 -
      // KIO::filePreview() will request the MIME-type of all passed items, which (in the
      // worst case) might block the application for several seconds. To prevent such
      // a blocking, we only pass items with known mime type to the preview job.
          } while (!m_pendingPreviewItems.isEmpty() && timer.elapsed() < MaxBlockTimeout);
      }
  
 -    KIO::PreviewJob *job = new KIO::PreviewJob(itemSubSet, cacheSize, &m_enabledPlugins);
 +    KIO::PreviewJob *job = new KIO::PreviewJob(itemSubSet, cacheSize(), &m_enabledPlugins);
      job->setDevicePixelRatio(m_devicePixelRatio);
      job->setIgnoreMaximumSize(itemSubSet.first().isLocalFile() && !itemSubSet.first().isSlow() && m_localFileSizePreviewLimit <= 0);
      if (job->uiDelegate()) {
@@@ -1023,11 -1029,7 +1022,11 @@@ QPixmap KFileItemModelRolesUpdater::tra
  {
      QPixmap scaledPixmap = pixmap;
  
 -    if (!pixmap.hasAlpha() && !pixmap.isNull() && m_iconSize.width() > KIconLoader::SizeSmallMedium && m_iconSize.height() > KIconLoader::SizeSmallMedium) {
 +    if (pixmap.isNull()) {
 +        return scaledPixmap;
 +    }
 +
 +    if (!pixmap.hasAlpha() && m_iconSize.width() > KIconLoader::SizeSmallMedium && m_iconSize.height() > KIconLoader::SizeSmallMedium) {
          if (m_enlargeSmallPreviews) {
              KPixmapModifier::applyFrame(scaledPixmap, m_iconSize);
          } else {
                  KPixmapModifier::applyFrame(scaledPixmap, m_iconSize);
              }
          }
 -    } else if (!pixmap.isNull()) {
 +    } else {
          KPixmapModifier::scale(scaledPixmap, m_iconSize * m_devicePixelRatio);
          scaledPixmap.setDevicePixelRatio(m_devicePixelRatio);
      }
      return scaledPixmap;
  }
  
 +QSize KFileItemModelRolesUpdater::cacheSize()
 +{
 +    // PreviewJob internally caches items always with the size of
 +    // 128 x 128 pixels or 256 x 256 pixels. A (slow) downscaling is done
 +    // by PreviewJob if a smaller size is requested. For images KFileItemModelRolesUpdater must
 +    // do a downscaling anyhow because of the frame, so in this case only the provided
 +    // cache sizes are requested.
 +    return (m_iconSize.width() > 128) || (m_iconSize.height() > 128) ? QSize(256, 256) : QSize(128, 128);
 +}
 +
  void KFileItemModelRolesUpdater::loadNextHoverSequencePreview()
  {
      if (m_hoverSequenceItem.isNull() || m_hoverSequencePreviewJob) {
          return;
      }
  
 -    // PreviewJob internally caches items always with the size of
 -    // 128 x 128 pixels or 256 x 256 pixels. A (slow) downscaling is done
 -    // by PreviewJob if a smaller size is requested. For images KFileItemModelRolesUpdater must
 -    // do a downscaling anyhow because of the frame, so in this case only the provided
 -    // cache sizes are requested.
 -    const QSize cacheSize = (m_iconSize.width() > 128) || (m_iconSize.height() > 128) ? QSize(256, 256) : QSize(128, 128);
 -
 -    KIO::PreviewJob *job = new KIO::PreviewJob({m_hoverSequenceItem}, cacheSize, &m_enabledPlugins);
 +    KIO::PreviewJob *job = new KIO::PreviewJob({m_hoverSequenceItem}, cacheSize(), &m_enabledPlugins);
  
      job->setSequenceIndex(loadSeqIdx);
      job->setIgnoreMaximumSize(m_hoverSequenceItem.isLocalFile() && !m_hoverSequenceItem.isSlow() && m_localFileSizePreviewLimit <= 0);
@@@ -1218,13 -1217,13 +1217,13 @@@ void KFileItemModelRolesUpdater::applyS
      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 {
@@@ -1288,20 -1287,13 +1287,20 @@@ bool KFileItemModelRolesUpdater::applyR
  
  void KFileItemModelRolesUpdater::startDirectorySizeCounting(const KFileItem &item, int index)
  {
 -    if (ContentDisplaySettings::directorySizeMode() == ContentDisplaySettings::EnumDirectorySizeMode::None || !item.isLocalFile()) {
 +    if (ContentDisplaySettings::directorySizeMode() == ContentDisplaySettings::EnumDirectorySizeMode::None) {
 +        return;
 +    }
 +
 +    // Set any remote files to unknown size (-1).
 +    if (!item.isLocalFile()) {
 +        resetSizeData(index, -1);
          return;
 +    } else {
 +        resetSizeData(index);
      }
  
      if (ContentDisplaySettings::directorySizeMode() == ContentDisplaySettings::EnumDirectorySizeMode::ContentCount || item.isSlow()) {
          // fastpath no recursion necessary
 -
          auto data = m_model->data(index);
          if (data.value("size") == -2) {
              // means job already started
          connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
  
          auto listJob = KIO::listDir(url, KIO::HideProgressInfo);
 -        QObject::connect(listJob, &KIO::ListJob::entries, this, [this, item](const KJob * /*job*/, const KIO::UDSEntryList &list) {
 +
 +        QObject::connect(listJob, &KIO::ListJob::entries, this, [this, item](const KJob *job, const KIO::UDSEntryList &list) {
              int index = m_model->index(item);
              if (index < 0) {
                  return;
              }
              auto data = m_model->data(index);
              int origCount = data.value("count").toInt();
 -            int entryCount = origCount;
 +            // Get the amount of processed items...
 +            int entryCount = job->processedAmount(KJob::Bytes);
  
 +            // ...and then remove the unwanted items from the amount.
              for (const KIO::UDSEntry &entry : list) {
                  const auto name = entry.stringValue(KIO::UDSEntry::UDS_NAME);
  
                  if (name == QStringLiteral("..") || name == QStringLiteral(".")) {
 +                    --entryCount;
                      continue;
                  }
                  if (!m_model->showHiddenFiles() && name.startsWith(QLatin1Char('.'))) {
 +                    --entryCount;
                      continue;
                  }
                  if (m_model->showDirectoriesOnly() && !entry.isDir()) {
 +                    --entryCount;
                      continue;
                  }
 -                ++entryCount;
              }
  
              QHash<QByteArray, QVariant> newData;
              }
  
              if (origCount != entryCount) {
 -                // count has changed
                  newData.insert("count", entryCount);
              }
  
@@@ -1540,27 -1528,4 +1539,27 @@@ void KFileItemModelRolesUpdater::trimHo
      }
  }
  
 +void KFileItemModelRolesUpdater::resetSizeData(const int index, const int size)
 +{
 +    disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
 +    auto data = m_model->data(index);
 +    data.insert("size", size);
 +    m_model->setData(index, data);
 +    connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
 +}
 +
 +void KFileItemModelRolesUpdater::recountDirectoryItems(const QList<QUrl> directories)
 +{
 +    for (const auto &dir : directories) {
 +        auto index = m_model->index(dir);
 +        if (index < 0) {
 +            continue;
 +        }
 +        auto item = m_model->fileItem(index);
 +        if (item.isDir()) {
 +            startDirectorySizeCounting(item, index);
 +        }
 +    }
 +}
 +
  #include "moc_kfileitemmodelrolesupdater.cpp"
index 56e28ce72587dbdae53096c1b7cbc3da57df9079,df3a9422622b5a3c68d166de675b30e6aee648f2..73e42cf17f2e76eaba9173c0bd3ad5199e2ec8e6
@@@ -178,9 -178,6 +178,9 @@@ public
       */
      void setHoverSequenceState(const QUrl &itemUrl, int seqIdx);
  
 +Q_SIGNALS:
 +    void previewJobFinished(); // For unit testing
 +
  private Q_SLOTS:
      void slotItemsInserted(const KItemRangeList &itemRanges);
      void slotItemsRemoved(const KItemRangeList &itemRanges);
@@@ -296,7 -293,6 +296,7 @@@ private
       * Transforms a raw preview image, applying scale and frame.
       *
       * @param pixmap A raw preview image from a PreviewJob.
 +     * @param overlays the overlays to add to the pixmap
       * @return The scaled and decorated preview image.
       */
      QPixmap transformPreviewPixmap(const QPixmap &pixmap);
  
      /**
       * 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 trimHoverSequenceLoadedItems();
  
 +    void resetSizeData(const int index, const int size = 0);
 +
 +    void recountDirectoryItems(const QList<QUrl> directories);
 +
  private:
 +    QSize cacheSize();
      /**
       * enqueue directory size counting for KFileItem item at index
       */
index 75d85be358953e7897936800d303142b183ef9f5,45f5851bf5d6c2e06bd592c09350058fea35a2a8..82325cb1978813f38747d2203f84189461932b66
@@@ -8,16 -8,12 +8,16 @@@
  
  #include "kitemlistview.h"
  
 +#ifndef QT_NO_ACCESSIBILITY
 +#include "accessibility/kitemlistcontaineraccessible.h"
 +#include "accessibility/kitemlistdelegateaccessible.h"
 +#include "accessibility/kitemlistviewaccessible.h"
 +#endif
  #include "dolphindebug.h"
  #include "kitemlistcontainer.h"
  #include "kitemlistcontroller.h"
  #include "kitemlistheader.h"
  #include "kitemlistselectionmanager.h"
 -#include "kitemlistviewaccessible.h"
  #include "kstandarditemlistwidget.h"
  
  #include "private/kitemlistheaderwidget.h"
@@@ -25,8 -21,6 +25,8 @@@
  #include "private/kitemlistsizehintresolver.h"
  #include "private/kitemlistviewlayouter.h"
  
 +#include <optional>
 +
  #include <QElapsedTimer>
  #include <QGraphicsSceneMouseEvent>
  #include <QGraphicsView>
@@@ -106,7 -100,6 +106,7 @@@ KItemListView::KItemListView(QGraphicsW
      , m_header(nullptr)
      , m_headerWidget(nullptr)
      , m_indicatorAnimation(nullptr)
 +    , m_statusBarOffset(0)
      , m_dropIndicator()
      , m_sizeHintResolver(nullptr)
  {
@@@ -193,7 -186,7 +193,7 @@@ qreal KItemListView::scrollOffset() con
  
  qreal KItemListView::maximumScrollOffset() const
  {
 -    return m_layouter->maximumScrollOffset();
 +    return m_layouter->maximumScrollOffset() + m_statusBarOffset;
  }
  
  void KItemListView::setItemOffset(qreal offset)
@@@ -386,7 -379,7 +386,7 @@@ void KItemListView::setGeometry(const Q
          if (m_headerWidget->automaticColumnResizing()) {
              applyAutomaticColumnWidths();
          } else {
 -            const qreal requiredWidth = columnWidthsSum() + 2 * m_headerWidget->sidePadding();
 +            const qreal requiredWidth = m_headerWidget->leftPadding() + columnWidthsSum() + m_headerWidget->rightPadding();
              const QSizeF dynamicItemSize(qMax(newSize.width(), requiredWidth), m_itemSize.height());
              m_layouter->setItemSize(dynamicItemSize);
          }
      doLayout(NoAnimation);
  }
  
 +qreal KItemListView::scrollSingleStep() const
 +{
 +    const QFontMetrics metrics(font());
 +    return metrics.height();
 +}
 +
  qreal KItemListView::verticalPageStep() const
  {
      qreal headerHeight = 0;
@@@ -425,7 -412,7 +425,7 @@@ std::optional<int> KItemListView::itemA
  
          const KItemListWidget *widget = it.value();
          const QPointF mappedPos = widget->mapFromItem(this, pos);
 -        if (widget->contains(mappedPos) || widget->selectionRect().contains(mappedPos)) {
 +        if (widget->contains(mappedPos)) {
              return it.key();
          }
      }
@@@ -542,7 -529,7 +542,7 @@@ QRectF KItemListView::itemContextRect(i
  
      const KItemListWidget *widget = m_visibleItems.value(index);
      if (widget) {
 -        contextRect = widget->iconRect() | widget->textRect();
 +        contextRect = widget->selectionRectCore();
          contextRect.translate(itemRect(index).topLeft());
      }
  
@@@ -561,9 -548,6 +561,9 @@@ void KItemListView::scrollToItem(int in
          const qreal headerHeight = m_headerWidget->size().height();
          viewGeometry.adjust(0, headerHeight, 0, 0);
      }
 +    if (m_statusBarOffset != 0) {
 +        viewGeometry.adjust(0, 0, 0, -m_statusBarOffset);
 +    }
      QRectF currentRect = itemRect(index);
  
      if (layoutDirection() == Qt::RightToLeft && scrollOrientation() == Qt::Horizontal) {
@@@ -750,16 -734,11 +750,16 @@@ QPixmap KItemListView::createDragPixmap
  void KItemListView::editRole(int index, const QByteArray &role)
  {
      KStandardItemListWidget *widget = qobject_cast<KStandardItemListWidget *>(m_visibleItems.value(index));
 -    if (!widget || m_editingRole) {
 +    if (!widget) {
 +        return;
 +    }
 +    if (m_editingRole || m_animation->isStarted(widget)) {
 +        Q_EMIT widget->roleEditingCanceled(index, role, QVariant());
          return;
      }
  
      m_editingRole = true;
 +    m_controller->selectionManager()->setCurrentItem(index);
      widget->setEditedRole(role);
  
      connect(widget, &KItemListWidget::roleEditingCanceled, this, &KItemListView::slotRoleEditingCanceled);
@@@ -843,16 -822,6 +843,16 @@@ void KItemListView::paint(QPainter *pai
      }
  }
  
 +void KItemListView::setStatusBarOffset(int offset)
 +{
 +    if (m_statusBarOffset != offset) {
 +        m_statusBarOffset = offset;
 +        if (m_layouter) {
 +            m_layouter->setStatusBarOffset(offset);
 +        }
 +    }
 +}
 +
  QVariant KItemListView::itemChange(GraphicsItemChange change, const QVariant &value)
  {
      if (change == QGraphicsItem::ItemSceneHasChanged && scene()) {
@@@ -1403,20 -1372,9 +1403,20 @@@ void KItemListView::slotItemsMoved(cons
      const int firstVisibleMovedIndex = qMax(firstVisibleIndex(), itemRange.index);
      const int lastVisibleMovedIndex = qMin(lastVisibleIndex(), itemRange.index + itemRange.count - 1);
  
 +    /// Represents an item that was moved while being edited.
 +    struct MovedEditedItem {
 +        int movedToIndex;
 +        QByteArray editedRole;
 +    };
 +    std::optional<MovedEditedItem> movedEditedItem;
      for (int index = firstVisibleMovedIndex; index <= lastVisibleMovedIndex; ++index) {
          KItemListWidget *widget = m_visibleItems.value(index);
          if (widget) {
 +            if (m_editingRole && !widget->editedRole().isEmpty()) {
 +                movedEditedItem = {movedToIndexes[index - itemRange.index], widget->editedRole()};
 +                disconnectRoleEditingSignals(index);
 +                m_editingRole = false;
 +            }
              updateWidgetProperties(widget, index);
              initializeItemListWidget(widget);
          }
  
      doLayout(NoAnimation);
      updateSiblingsInformation();
 +
 +    if (movedEditedItem) {
 +        editRole(movedEditedItem->movedToIndex, movedEditedItem->editedRole);
 +    }
  }
  
  void KItemListView::slotItemsChanged(const KItemRangeList &itemRanges, const QSet<QByteArray> &roles)
              updateVisibleGroupHeaders();
              doLayout(NoAnimation);
          }
 -
 -        QAccessibleTableModelChangeEvent ev(this, QAccessibleTableModelChangeEvent::DataChanged);
 -        ev.setFirstRow(itemRange.index);
 -        ev.setLastRow(itemRange.index + itemRange.count);
 -        QAccessible::updateAccessibility(&ev);
      }
 -
      doLayout(NoAnimation);
  }
  
@@@ -1521,8 -1481,30 +1521,28 @@@ void KItemListView::slotSortRoleChanged
      }
  }
  
+ 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 &current, const QByteArray &previous)
+ {
+     Q_UNUSED(current)
+     Q_UNUSED(previous)
+     if (m_grouped) {
+         updateVisibleGroupHeaders();
+         doLayout(NoAnimation);
+     }
+ }
  void KItemListView::slotCurrentChanged(int current, int previous)
  {
 -    Q_UNUSED(previous)
 -
      // In SingleSelection mode (e.g., in the Places Panel), the current item is
      // always the selected item. It is not necessary to highlight the current item then.
      if (m_controller->selectionBehavior() != KItemListController::SingleSelection) {
              currentWidget->setCurrent(true);
          }
      }
 -
 -    QAccessibleEvent ev(this, QAccessible::Focus);
 -    ev.setChild(current);
 -    QAccessible::updateAccessibility(&ev);
 +#ifndef QT_NO_ACCESSIBILITY
 +    if (current != previous && QAccessible::isActive()) {
 +        static_cast<KItemListViewAccessible *>(QAccessible::queryAccessibleInterface(this))->announceCurrentItem();
 +    }
 +#endif
  }
  
  void KItemListView::slotSelectionChanged(const KItemSet &current, const KItemSet &previous)
  {
 -    Q_UNUSED(previous)
 -
      QHashIterator<int, KItemListWidget *> it(m_visibleItems);
      while (it.hasNext()) {
          it.next();
          const int index = it.key();
          KItemListWidget *widget = it.value();
 -        widget->setSelected(current.contains(index));
 +        const bool isSelected(current.contains(index));
 +        widget->setSelected(isSelected);
 +
 +#ifndef QT_NO_ACCESSIBILITY
 +        if (!QAccessible::isActive()) {
 +            continue;
 +        }
 +        // Let the screen reader announce "selected" or "not selected" for the active item.
 +        const bool wasSelected(previous.contains(index));
 +        if (isSelected != wasSelected) {
 +            QAccessibleEvent accessibleSelectionChangedEvent(this, QAccessible::SelectionAdd);
 +            accessibleSelectionChangedEvent.setChild(index);
 +            QAccessible::updateAccessibility(&accessibleSelectionChangedEvent);
 +        }
      }
 +#else
 +    }
 +    Q_UNUSED(previous)
 +#endif
  }
  
  void KItemListView::slotAnimationFinished(QGraphicsWidget *widget, KItemListViewAnimation::AnimationType type)
@@@ -1621,10 -1587,10 +1641,10 @@@ void KItemListView::slotRubberBandActiv
          curve.addCubicBezierSegment(QPointF(0.4, 0.0), QPointF(1.0, 1.0), QPointF(1.0, 1.0));
          animation->setEasingCurve(curve);
  
 -        connect(animation, &QVariantAnimation::valueChanged, this, [=](const QVariant &) {
 +        connect(animation, &QVariantAnimation::valueChanged, this, [=, this](const QVariant &) {
              update();
          });
 -        connect(animation, &QVariantAnimation::finished, this, [=]() {
 +        connect(animation, &QVariantAnimation::finished, this, [=, this]() {
              m_rubberBandAnimations.removeAll(animation);
              delete animation;
          });
@@@ -1797,6 -1763,8 +1817,8 @@@ void KItemListView::setModel(KItemModel
          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()));
      }
@@@ -1920,9 -1888,6 +1942,9 @@@ void KItemListView::doLayout(LayoutAnim
  
          if (animate) {
              if (m_animation->isStarted(widget, KItemListViewAnimation::MovingAnimation)) {
 +                if (m_editingRole) {
 +                    Q_EMIT widget->roleEditingCanceled(widget->index(), QByteArray(), QVariant());
 +                }
                  m_animation->start(widget, KItemListViewAnimation::MovingAnimation, newPos);
                  applyNewPos = false;
              }
@@@ -2242,7 -2207,7 +2264,7 @@@ void KItemListView::updateGroupHeaderFo
      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);
@@@ -2416,7 -2381,7 +2438,7 @@@ QHash<QByteArray, qreal> KItemListView:
  void KItemListView::applyColumnWidthsFromHeader()
  {
      // Apply the new size to the layouter
 -    const qreal requiredWidth = columnWidthsSum() + 2 * m_headerWidget->sidePadding();
 +    const qreal requiredWidth = m_headerWidget->leftPadding() + columnWidthsSum() + m_headerWidget->rightPadding();
      const QSizeF dynamicItemSize(qMax(size().width(), requiredWidth), m_itemSize.height());
      m_layouter->setItemSize(dynamicItemSize);
  
@@@ -2433,7 -2398,7 +2455,7 @@@ void KItemListView::updateWidgetColumnW
      for (const QByteArray &role : std::as_const(m_visibleRoles)) {
          widget->setColumnWidth(role, m_headerWidget->columnWidth(role));
      }
 -    widget->setSidePadding(m_headerWidget->sidePadding());
 +    widget->setSidePadding(m_headerWidget->leftPadding(), m_headerWidget->rightPadding());
  }
  
  void KItemListView::updatePreferredColumnWidths(const KItemRangeList &itemRanges)
@@@ -2511,9 -2476,9 +2533,9 @@@ void KItemListView::applyAutomaticColum
      qreal firstColumnWidth = m_headerWidget->columnWidth(firstRole);
      QSizeF dynamicItemSize = m_itemSize;
  
 -    qreal requiredWidth = columnWidthsSum() + 2 * m_headerWidget->sidePadding(); // Adding the padding a second time so we have the same padding
 -    // symmetrically on both sides of the view. This improves UX, looks better and increases the chances of users figuring out that the padding
 -    // area can be used for deselecting and dropping files.
 +    qreal requiredWidth = m_headerWidget->leftPadding() + columnWidthsSum() + m_headerWidget->rightPadding();
 +    // By default we want the same padding symmetrically on both sides of the view. This improves UX, looks better and increases the chances of users figuring
 +    // out that the padding area can be used for deselecting and dropping files.
      const qreal availableWidth = size().width();
      if (requiredWidth < availableWidth) {
          // Stretch the first column to use the whole remaining width
index 1a4ff0df1c7bf9b280574f35810545c6d762121e,04d48bd4777f4bee788ba4ddb1cd3d00a1bc8eb9..1382405d058b89e87fe218cd6a6bd12cc20e8e89
@@@ -175,11 -175,6 +175,11 @@@ public
       */
      qreal verticalPageStep() const;
  
 +    /**
 +     * @return The line step which should be used for the scroll by mouse wheel.
 +     */
 +    virtual qreal scrollSingleStep() const;
 +
      /**
       * @return Index of the item that is below the point \a pos.
       *         The position is relative to the upper right of
  
      void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) override;
  
 +    /**
 +     * Set the bottom offset for moving the view so that the small overlayed statusbar
 +     * won't cover any items by accident.
 +     */
 +    void setStatusBarOffset(int offset);
 +
  Q_SIGNALS:
      void scrollOrientationChanged(Qt::Orientation current, Qt::Orientation previous);
      void scrollOffsetChanged(qreal current, qreal previous);
@@@ -441,6 -430,8 +441,8 @@@ protected Q_SLOTS
      virtual void slotGroupedSortingChanged(bool current);
      virtual void slotSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous);
      virtual void slotSortRoleChanged(const QByteArray &current, const QByteArray &previous);
+     virtual void slotGroupOrderChanged(Qt::SortOrder current, Qt::SortOrder previous);
+     virtual void slotGroupRoleChanged(const QByteArray &current, const QByteArray &previous);
      virtual void slotCurrentChanged(int current, int previous);
      virtual void slotSelectionChanged(const KItemSet &current, const KItemSet &previous);
  
@@@ -563,8 -554,9 +565,9 @@@ private
      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();
@@@ -787,8 -779,6 +790,8 @@@ private
  
      QPropertyAnimation *m_indicatorAnimation;
  
 +    int m_statusBarOffset;
 +
      // When dragging items into the view where the sort-role of the model
      // is empty, a visual indicator should be shown during dragging where
      // the dropping will happen. This indicator is specified by an index
      friend class KItemListController;
      friend class KItemListControllerTest;
      friend class KItemListViewAccessible;
 -    friend class KItemListAccessibleCell;
 +    friend class KItemListDelegateAccessible;
 +
 +    friend class DolphinMainWindowTest;
  };
  
  /**
index bae1f409fff62970168a5080a375f3364cbe557d,7f34f709322bee495e831c2ca041a11e2bc1608a..0044170a7e78f2f61cd01b6e4e23e9f8a32de6c1
@@@ -24,7 -24,7 +24,7 @@@
          <entry name="ViewMode" type="Int" >
              <label context="@label">View Mode</label>
              <whatsthis context="@info:whatsthis">This option controls the style of the view. Currently supported values include icons (0), details (1) and column (2) views.</whatsthis>
 -            <default>DolphinView::IconsView</default>
 +            <default code="true">DolphinView::IconsView</default>
          </entry>
  
          <entry name="PreviewsShown" type="Bool" >
@@@ -35,8 -35,8 +35,8 @@@
  
          <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>
              <whatsthis context="@info:whatsthis">The last time these properties were changed by the user.</whatsthis>
          </entry>
  
 +        <entry name="DynamicViewPassed" type="Bool">
 +            <label context="@label">View mode changed once by dynamic view</label>
 +            <default>false</default>
 +        </entry>
 +
          <!-- Obsolete - replaced by VisibleRoles -->
          <entry name="AdditionalInfo" type="StringList">
              <label context="@label">Additional Information</label>
          </entry>
      </group>
  </kcfg>
index 6410d9655fe6b24360dd4ebc172378e83c703464,577dd23e2b703e940806820ceb9ffcdfdd0e0cca..1c8178651abdbdee5ff7505e7cc8e38d6c6d3cd2
@@@ -21,7 -21,6 +21,7 @@@
  #include <KAuthorized>
  #include <KLocalizedString>
  #include <KMessageBox>
 +#include <KRuntimePlatform>
  #include <KWindowConfig>
  
  #include <kwidgetsaddons_version.h>
@@@ -38,13 -37,13 +38,13 @@@ DolphinSettingsDialog::DolphinSettingsD
      const QSize minSize = minimumSize();
      setMinimumSize(QSize(540, minSize.height()));
  
 -    setFaceType(List);
 +    setFaceType(KRuntimePlatform::runtimePlatform().contains(QLatin1String("phone")) ? Tabbed : List);
      setWindowTitle(i18nc("@title:window", "Configure"));
  
      // Interface
      InterfaceSettingsPage *interfaceSettingsPage = new InterfaceSettingsPage(this);
      KPageWidgetItem *interfaceSettingsFrame = addPage(interfaceSettingsPage, i18nc("@title:group Interface settings", "Interface"));
 -    interfaceSettingsFrame->setIcon(QIcon::fromTheme(QStringLiteral("system-file-manager")));
 +    interfaceSettingsFrame->setIcon(QIcon::fromTheme(QStringLiteral("org.kde.dolphin")));
      connect(interfaceSettingsPage, &InterfaceSettingsPage::changed, this, &DolphinSettingsDialog::enableApply);
  
      // View
@@@ -58,6 -57,7 +58,7 @@@
                                                                 actions,
                                                                 {QStringLiteral("add_to_places"),
                                                                  QStringLiteral("sort"),
+                                                                 QStringLiteral("group"),
                                                                  QStringLiteral("view_mode"),
                                                                  QStringLiteral("open_in_new_tab"),
                                                                  QStringLiteral("open_in_new_window"),
index 4c6db590d8c78bfb40b1d42a6f8d7b9a1a24823c,ea05f840b742f1009664391f489c4e94ed8cbe65..34ae2ba8721c02479005e5e7011053a4dfbc4b63
@@@ -69,7 -69,6 +69,7 @@@ private Q_SLOTS
      void testMakeExpandedItemHidden();
      void testRemoveFilteredExpandedItems();
      void testSorting();
 +    void testNaturalSorting();
      void testIndexForKeyboardSearch();
      void testNameFilter();
      void testEmptyPath();
@@@ -158,14 -157,14 +158,14 @@@ void KFileItemModelTest::testDefaultSor
      QVERIFY(itemsInsertedSpy.wait());
  
      QCOMPARE(m_model->count(), 3);
 -    QCOMPARE(m_model->data(0).value("text").toString(), QString("a.txt"));
 -    QCOMPARE(m_model->data(1).value("text").toString(), QString("b.txt"));
 -    QCOMPARE(m_model->data(2).value("text").toString(), QString("c.txt"));
 +    QCOMPARE(m_model->data(0).value("text").toString(), QStringLiteral("a.txt"));
 +    QCOMPARE(m_model->data(1).value("text").toString(), QStringLiteral("b.txt"));
 +    QCOMPARE(m_model->data(2).value("text").toString(), QStringLiteral("c.txt"));
  }
  
  void KFileItemModelTest::testDefaultGroupedSorting()
  {
-     QCOMPARE(m_model->groupedSorting(), false);
+     QCOMPARE(m_model->groupedSorting(), true);
  }
  
  void KFileItemModelTest::testNewItems()
@@@ -263,8 -262,8 +263,8 @@@ void KFileItemModelTest::testSetData(
      QCOMPARE(itemsChangedSpy.count(), 1);
  
      values = m_model->data(0);
 -    QCOMPARE(values.value("customRole1").toString(), QString("Test1"));
 -    QCOMPARE(values.value("customRole2").toString(), QString("Test2"));
 +    QCOMPARE(values.value("customRole1").toString(), QStringLiteral("Test1"));
 +    QCOMPARE(values.value("customRole2").toString(), QStringLiteral("Test2"));
      QVERIFY(m_model->isConsistent());
  }
  
@@@ -1276,88 -1275,11 +1276,88 @@@ void KFileItemModelTest::testSorting(
      QCOMPARE(itemsMovedSpy.takeFirst().at(1).value<QList<int>>(), QList<int>() << 2 << 4 << 5 << 3 << 0 << 1 << 6 << 7 << 9 << 8);
  }
  
 +void KFileItemModelTest::testNaturalSorting()
 +{
 +    QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted);
 +    QSignalSpy itemsMovedSpy(m_model, &KFileItemModel::itemsMoved);
 +    QVERIFY(itemsMovedSpy.isValid());
 +
 +    m_model->setSortRole("text");
 +    m_model->setShowHiddenFiles(true);
 +
 +    m_testDir->createFiles({".a", "a.txt", "b.txt", "a 1.txt", "a 2.txt", "a 10.txt", "a.tar", "b.tar"});
 +
 +    m_model->loadDirectory(m_testDir->url());
 +    QVERIFY(itemsInsertedSpy.wait());
 +
 +    // Sort by Name, ascending, natural sorting enabled
 +    QCOMPARE(m_model->sortRole(), QByteArray("text"));
 +    QCOMPARE(m_model->sortOrder(), Qt::AscendingOrder);
 +    QCOMPARE(itemsInModel(),
 +             QStringList() << ".a"
 +                           << "a.tar"
 +                           << "a.txt"
 +                           << "a 1.txt"
 +                           << "a 2.txt"
 +                           << "a 10.txt"
 +                           << "b.tar"
 +                           << "b.txt");
 +
 +    // Sort by Extension
 +    m_model->setSortRole("extension");
 +    QCOMPARE(m_model->sortRole(), QByteArray("extension"));
 +    QCOMPARE(itemsInModel(),
 +             QStringList() << ".a"
 +                           << "a.tar"
 +                           << "b.tar"
 +                           << "a.txt"
 +                           << "a 1.txt"
 +                           << "a 2.txt"
 +                           << "a 10.txt"
 +                           << "b.txt");
 +    QCOMPARE(itemsMovedSpy.count(), 1);
 +    QCOMPARE(itemsMovedSpy.first().at(0).value<KItemRange>(), KItemRange(2, 5));
 +    QCOMPARE(itemsMovedSpy.takeFirst().at(1).value<QList<int>>(), QList<int>() << 3 << 4 << 5 << 6 << 2);
 +
 +    // Disable natural sorting, refresh directory for the change to take effect
 +    m_model->m_naturalSorting = false;
 +    m_model->refreshDirectory(m_model->directory());
 +    QVERIFY(itemsInsertedSpy.wait());
 +
 +    // Sort by Extension, ascending, natural sorting disabled
 +    QCOMPARE(m_model->sortRole(), QByteArray("extension"));
 +    QCOMPARE(itemsInModel(),
 +             QStringList() << ".a"
 +                           << "a.tar"
 +                           << "b.tar"
 +                           << "a 1.txt"
 +                           << "a 10.txt"
 +                           << "a 2.txt"
 +                           << "a.txt"
 +                           << "b.txt");
 +
 +    // Sort by Name
 +    m_model->setSortRole("text");
 +    QCOMPARE(m_model->sortRole(), QByteArray("text"));
 +    QCOMPARE(itemsInModel(),
 +             QStringList() << ".a"
 +                           << "a 1.txt"
 +                           << "a 10.txt"
 +                           << "a 2.txt"
 +                           << "a.tar"
 +                           << "a.txt"
 +                           << "b.tar"
 +                           << "b.txt");
 +    QCOMPARE(itemsMovedSpy.count(), 1);
 +    QCOMPARE(itemsMovedSpy.first().at(0).value<KItemRange>(), KItemRange(1, 6));
 +    QCOMPARE(itemsMovedSpy.takeFirst().at(1).value<QList<int>>(), QList<int>() << 4 << 6 << 1 << 2 << 3 << 5);
 +}
 +
  void KFileItemModelTest::testIndexForKeyboardSearch()
  {
      QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted);
  
 -    m_testDir->createFiles({"a", "aa", "Image.jpg", "Image.png", "Text", "Text1", "Text2", "Text11"});
 +    m_testDir->createFiles({"a", "aa", "Image.jpg", "Image.png", "Text", "Text1", "Text2", "Text11", "U", "Ü", "Üu", "Ž"});
  
      m_model->loadDirectory(m_testDir->url());
      QVERIFY(itemsInsertedSpy.wait());
      QCOMPARE(m_model->indexForKeyboardSearch("text1", 0), 5);
      QCOMPARE(m_model->indexForKeyboardSearch("text2", 0), 6);
      QCOMPARE(m_model->indexForKeyboardSearch("text11", 0), 7);
 +    QCOMPARE(m_model->indexForKeyboardSearch("u", 0), 8);
 +    QCOMPARE(m_model->indexForKeyboardSearch("üu", 0), 10);
 +    QCOMPARE(m_model->indexForKeyboardSearch("ž", 0), 11);
  
      // Start a search somewhere in the middle
      QCOMPARE(m_model->indexForKeyboardSearch("a", 1), 1);
      QCOMPARE(m_model->indexForKeyboardSearch("TexT", 5), 5);
      QCOMPARE(m_model->indexForKeyboardSearch("IMAGE", 4), 2);
  
 +    // Test searches that match items with marks
 +    QCOMPARE(m_model->indexForKeyboardSearch("u", 9), 9);
 +    QCOMPARE(m_model->indexForKeyboardSearch("u", 10), 10);
 +    QCOMPARE(m_model->indexForKeyboardSearch("uu", 0), 10);
 +    QCOMPARE(m_model->indexForKeyboardSearch("z", 0), 11);
 +
      // TODO: Maybe we should also test keyboard searches in directories which are not sorted by Name?
  }
  
@@@ -2094,6 -2007,7 +2094,7 @@@ void KFileItemModelTest::testNameRoleGr
      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(),
@@@ -2180,6 -2094,8 +2181,8 @@@ void KFileItemModelTest::testNameRoleGr
      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").
index b702f5b13bf49d72c9789310fd48e77183bcd396,abaed9c7c5b910bb7db5c2c2c15a30a163de37db..5c961b47b594c18d0710a3f408dd78668b5a7a64
@@@ -12,9 -12,6 +12,9 @@@
  #include "dolphinitemlistview.h"
  #include "dolphinnewfilemenuobserver.h"
  #include "draganddrophelper.h"
 +#ifndef QT_NO_ACCESSIBILITY
 +#include "kitemviews/accessibility/kitemlistviewaccessible.h"
 +#endif
  #include "kitemviews/kfileitemlistview.h"
  #include "kitemviews/kfileitemmodel.h"
  #include "kitemviews/kitemlistcontainer.h"
@@@ -44,7 -41,6 +44,7 @@@
  #include <KIO/Paste>
  #include <KIO/PasteJob>
  #include <KIO/RenameFileDialog>
 +#include <KJob>
  #include <KJobWidgets>
  #include <KLocalizedString>
  #include <KMessageBox>
@@@ -54,9 -50,6 +54,9 @@@
  #include <kwidgetsaddons_version.h>
  
  #include <QAbstractItemView>
 +#ifndef QT_NO_ACCESSIBILITY
 +#include <QAccessible>
 +#endif
  #include <QActionGroup>
  #include <QApplication>
  #include <QClipboard>
@@@ -124,7 -117,8 +124,7 @@@ DolphinView::DolphinView(const QUrl &ur
      applyModeToView();
  
      KItemListController *controller = new KItemListController(m_model, m_view, this);
 -    const int delay = GeneralSettings::autoExpandFolders() ? 750 : -1;
 -    controller->setAutoActivationDelay(delay);
 +    controller->setAutoActivationEnabled(GeneralSettings::autoExpandFolders());
      connect(controller, &KItemListController::doubleClickViewBackground, this, &DolphinView::doubleClickViewBackground);
  
      // The EnlargeSmallPreviews setting can only be changed after the model
      m_view->setAccessibleParentsObject(m_container);
  #endif
      setFocusProxy(m_container);
 -    connect(m_container->horizontalScrollBar(), &QScrollBar::valueChanged, this, [=] {
 +    connect(m_container->horizontalScrollBar(), &QScrollBar::valueChanged, this, [this] {
          hideToolTip();
      });
 -    connect(m_container->verticalScrollBar(), &QScrollBar::valueChanged, this, [=] {
 +    connect(m_container->verticalScrollBar(), &QScrollBar::valueChanged, this, [this] {
          hideToolTip();
      });
  
      connect(m_model, &KFileItemModel::directoryRedirection, this, &DolphinView::slotDirectoryRedirection);
      connect(m_model, &KFileItemModel::urlIsFileError, this, &DolphinView::urlIsFileError);
      connect(m_model, &KFileItemModel::fileItemsChanged, this, &DolphinView::fileItemsChanged);
 -    // #473377: Use a QueuedConnection to avoid modifying KCoreDirLister before KCoreDirListerCache::deleteDir() returns.
 -    connect(m_model, &KFileItemModel::currentDirectoryRemoved, this, &DolphinView::currentDirectoryRemoved, Qt::QueuedConnection);
 +    connect(m_model, &KFileItemModel::currentDirectoryRemoved, this, &DolphinView::currentDirectoryRemoved);
  
      connect(this, &DolphinView::itemCountChanged, this, &DolphinView::updatePlaceholderLabel);
  
@@@ -320,12 -315,6 +320,12 @@@ void DolphinView::setSelectionModeEnabl
          m_view->setEnabledSelectionToggles(DolphinItemListView::SelectionTogglesEnabled::FollowSetting);
      }
      m_container->controller()->setSelectionModeEnabled(enabled);
 +#ifndef QT_NO_ACCESSIBILITY
 +    if (QAccessible::isActive()) {
 +        auto accessibleViewInterface = static_cast<KItemListViewAccessible *>(QAccessible::queryAccessibleInterface(m_view));
 +        accessibleViewInterface->announceSelectionModeEnabled(enabled);
 +    }
 +#endif
  }
  
  bool DolphinView::selectionMode() const
@@@ -387,8 -376,9 +387,8 @@@ void DolphinView::setGroupedSorting(boo
  
      ViewProperties props(viewPropertiesUrl());
      props.setGroupedSorting(grouped);
 -    props.save();
  
 -    m_container->controller()->model()->setGroupedSorting(grouped);
 +    m_model->setGroupedSorting(grouped);
  
      Q_EMIT groupedSortingChanged(grouped);
  }
@@@ -515,6 -505,42 +515,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) {
@@@ -578,7 -604,8 +614,7 @@@ void DolphinView::readSettings(
      m_view->readSettings();
      applyViewProperties();
  
 -    const int delay = GeneralSettings::autoExpandFolders() ? 750 : -1;
 -    m_container->controller()->setAutoActivationDelay(delay);
 +    m_container->controller()->setAutoActivationEnabled(GeneralSettings::autoExpandFolders());
  
      const int newZoomLevel = m_view->zoomLevel();
      if (newZoomLevel != oldZoomLevel) {
@@@ -767,23 -794,7 +803,23 @@@ void DolphinView::renameSelectedItems(
  
      } else {
          KIO::RenameFileDialog *dialog = new KIO::RenameFileDialog(items, this);
 -        connect(dialog, &KIO::RenameFileDialog::renamingFinished, this, &DolphinView::slotRenameDialogRenamingFinished);
 +        connect(dialog, &KIO::RenameFileDialog::renamingFinished, this, [this, items](const QList<QUrl> &urls) {
 +            // The model may have already been updated, so it's possible that we don't find the old items.
 +            for (int i = 0; i < items.count(); ++i) {
 +                const int index = m_model->index(items[i]);
 +                if (index >= 0) {
 +                    QHash<QByteArray, QVariant> data;
 +                    data.insert("text", urls[i].fileName());
 +                    m_model->setData(index, data);
 +                }
 +            }
 +
 +            forceUrlsSelection(urls.first(), urls);
 +            updateSelectionState();
 +        });
 +        connect(dialog, &KIO::RenameFileDialog::error, this, [this](KJob *job) {
 +            KMessageBox::error(this, job->errorString());
 +        });
  
          dialog->open();
      }
@@@ -811,7 -822,7 +847,7 @@@ void DolphinView::deleteSelectedItems(
  
      using Iface = KIO::AskUserActionInterface;
      auto *trashJob = new KIO::DeleteOrTrashJob(list, Iface::Delete, Iface::DefaultConfirmation, this);
 -    connect(trashJob, &KJob::result, this, &DolphinView::slotTrashFileFinished);
 +    connect(trashJob, &KJob::result, this, &DolphinView::slotDeleteFileFinished);
      m_selectNextItem = true;
      trashJob->start();
  }
@@@ -844,12 -855,9 +880,12 @@@ void DolphinView::copySelectedItems(con
      KIO::CopyJob *job = KIO::copy(selection.urlList(), destinationUrl, KIO::DefaultFlags);
      KJobWidgets::setWindow(job, this);
  
 -    connect(job, &KIO::DropJob::result, this, &DolphinView::slotJobResult);
 +    connect(job, &KIO::CopyJob::result, this, &DolphinView::slotJobResult);
      connect(job, &KIO::CopyJob::copying, this, &DolphinView::slotItemCreatedFromJob);
      connect(job, &KIO::CopyJob::copyingDone, this, &DolphinView::slotItemCreatedFromJob);
 +    connect(job, &KIO::CopyJob::warning, this, [this](KJob *job, const QString & /* warning */) {
 +        Q_EMIT errorMessage(job->errorString(), job->error());
 +    });
      KIO::FileUndoManager::self()->recordCopyJob(job);
  }
  
@@@ -866,12 -874,9 +902,12 @@@ void DolphinView::moveSelectedItems(con
      KIO::CopyJob *job = KIO::move(selection.urlList(), destinationUrl, KIO::DefaultFlags);
      KJobWidgets::setWindow(job, this);
  
 -    connect(job, &KIO::DropJob::result, this, &DolphinView::slotJobResult);
 +    connect(job, &KIO::CopyJob::result, this, &DolphinView::slotJobResult);
      connect(job, &KIO::CopyJob::moving, this, &DolphinView::slotItemCreatedFromJob);
      connect(job, &KIO::CopyJob::copyingDone, this, &DolphinView::slotItemCreatedFromJob);
 +    connect(job, &KIO::CopyJob::warning, this, [this](KJob *job, const QString & /*warning */) {
 +        Q_EMIT errorMessage(job->errorString(), job->error());
 +    });
      KIO::FileUndoManager::self()->recordCopyJob(job);
  }
  
@@@ -933,9 -938,6 +969,9 @@@ void DolphinView::duplicateSelectedItem
          connect(job, &KIO::CopyJob::result, this, &DolphinView::slotJobResult);
          connect(job, &KIO::CopyJob::copyingDone, this, &DolphinView::slotItemCreatedFromJob);
          connect(job, &KIO::CopyJob::copyingLinkDone, this, &DolphinView::slotItemLinkCreatedFromJob);
 +        connect(job, &KIO::CopyJob::warning, this, [this](KJob *job, const QString & /*warning*/) {
 +            Q_EMIT errorMessage(job->errorString(), job->error());
 +        });
          KIO::FileUndoManager::self()->recordCopyJob(job);
      }
  }
@@@ -1182,9 -1184,6 +1218,9 @@@ void DolphinView::slotItemContextMenuRe
      if (m_selectionChangedTimer->isActive()) {
          emitSelectionChangedSignal();
      }
 +    if (m_twoClicksRenamingTimer->isActive()) {
 +        abortTwoClicksRenaming();
 +    }
  
      const KFileItem item = m_model->fileItem(index);
      Q_EMIT requestContextMenu(pos.toPoint(), item, selectedItems(), url());
@@@ -1250,7 -1249,7 +1286,7 @@@ void DolphinView::slotHeaderContextMenu
  
      QAction *toggleSidePaddingAction = menu->addAction(i18nc("@action:inmenu", "Side Padding"));
      toggleSidePaddingAction->setCheckable(true);
 -    toggleSidePaddingAction->setChecked(view->header()->sidePadding() > 0);
 +    toggleSidePaddingAction->setChecked(layoutDirection() == Qt::LeftToRight ? view->header()->leftPadding() > 0 : view->header()->rightPadding() > 0);
  
      QAction *autoAdjustWidthsAction = menu->addAction(i18nc("@action:inmenu", "Automatic Column Widths"));
      autoAdjustWidthsAction->setCheckable(true);
              props.setHeaderColumnWidths(columnWidths);
              header->setAutomaticColumnResizing(false);
          } else if (action == toggleSidePaddingAction) {
 -            header->setSidePadding(toggleSidePaddingAction->isChecked() ? 20 : 0);
 +            if (toggleSidePaddingAction->isChecked()) {
 +                header->setSidePadding(20, 20);
 +            } else {
 +                header->setSidePadding(0, 0);
 +            }
          } else {
              // Show or hide the selected role
              const QByteArray selectedRole = action->data().toByteArray();
@@@ -1340,11 -1335,10 +1376,11 @@@ void DolphinView::slotHeaderColumnWidth
      props.setHeaderColumnWidths(columnWidths);
  }
  
 -void DolphinView::slotSidePaddingWidthChanged(qreal width)
 +void DolphinView::slotSidePaddingWidthChanged(qreal leftPaddingWidth, qreal rightPaddingWidth)
  {
      ViewProperties props(viewPropertiesUrl());
 -    DetailsModeSettings::setSidePadding(int(width));
 +    DetailsModeSettings::setLeftPadding(int(leftPaddingWidth));
 +    DetailsModeSettings::setRightPadding(int(rightPaddingWidth));
      m_view->writeSettings();
  }
  
@@@ -1411,9 -1405,6 +1447,9 @@@ void DolphinView::dropUrls(const QUrl &
              connect(job, &KIO::DropJob::copyJobStarted, this, [this](const KIO::CopyJob *copyJob) {
                  connect(copyJob, &KIO::CopyJob::copying, this, &DolphinView::slotItemCreatedFromJob);
                  connect(copyJob, &KIO::CopyJob::moving, this, &DolphinView::slotItemCreatedFromJob);
 +                connect(copyJob, &KIO::CopyJob::warning, this, [this](KJob *job, const QString & /*warning*/) {
 +                    Q_EMIT errorMessage(job->errorString(), job->error());
 +                });
                  connect(copyJob, &KIO::CopyJob::linking, this, [this](KIO::Job *job, const QString &src, const QUrl &dest) {
                      Q_UNUSED(job)
                      Q_UNUSED(src)
@@@ -1787,6 -1778,7 +1823,6 @@@ void DolphinView::updateSelectionState(
              if (!selectedItems.isEmpty()) {
                  selectionManager->beginAnchoredSelection(selectionManager->currentItem());
                  selectionManager->setSelectedItems(selectedItems);
 -                selectionManager->endAnchoredSelection();
                  if (shouldScrollToCurrentItem) {
                      m_view->scrollToItem(selectedItems.first());
                  }
@@@ -1894,7 -1886,7 +1930,7 @@@ void DolphinView::selectNextItem(
              Q_ASSERT_X(false, "DolphinView", "Selecting the next item failed.");
              return;
          }
 -        const auto lastSelectedIndex = m_model->index(selectedItems().last());
 +        const auto lastSelectedIndex = m_model->index(selectedItems().constLast());
          if (lastSelectedIndex < 0) {
              Q_ASSERT_X(false, "DolphinView", "Selecting the next item failed.");
              return;
          }
          if (nextItem >= 0) {
              selectionManager->setSelected(nextItem, 1);
 +            selectionManager->beginAnchoredSelection(nextItem);
          }
          m_selectNextItem = false;
      }
  
  void DolphinView::slotRenamingResult(KJob *job)
  {
 -    if (job->error()) {
 +    // Change model data after renaming has succeeded. On failure we do nothing.
 +    // If there is already an item with the newUrl, the copyjob will open a dialog for it, and
 +    // KFileItemModel will update the data when the dir lister signals that the file name has changed.
 +    if (!job->error()) {
          KIO::CopyJob *copyJob = qobject_cast<KIO::CopyJob *>(job);
          Q_ASSERT(copyJob);
          const QUrl newUrl = copyJob->destUrl();
 +        const QUrl oldUrl = copyJob->srcUrls().at(0);
          const int index = m_model->index(newUrl);
 -        if (index >= 0) {
 +        if (m_model->index(oldUrl) == index) {
              QHash<QByteArray, QVariant> data;
 -            const QUrl oldUrl = copyJob->srcUrls().at(0);
 -            data.insert("text", oldUrl.fileName());
 +            data.insert("text", newUrl.fileName());
              m_model->setData(index, data);
          }
      }
@@@ -1958,7 -1946,6 +1994,7 @@@ void DolphinView::slotDirectoryLoadingC
  
      Q_EMIT directoryLoadingCompleted();
  
 +    applyDynamicView();
      updatePlaceholderLabel();
      updateWritableState();
  }
@@@ -2060,14 -2047,25 +2096,14 @@@ void DolphinView::slotRoleEditingFinish
              }
  #endif
  
 -            const bool newNameExistsAlready = (m_model->index(newUrl) >= 0);
 -            if (!newNameExistsAlready && m_model->index(oldUrl) == index) {
 -                // Only change the data in the model if no item with the new name
 -                // is in the model yet. If there is an item with the new name
 -                // already, calling KIO::CopyJob will open a dialog
 -                // asking for a new name, and KFileItemModel will update the
 -                // data when the dir lister signals that the file name has changed.
 -                QHash<QByteArray, QVariant> data;
 -                data.insert(role, retVal.newName);
 -                m_model->setData(index, data);
 -            }
 -
              KIO::Job *job = KIO::moveAs(oldUrl, newUrl);
              KJobWidgets::setWindow(job, this);
              KIO::FileUndoManager::self()->recordJob(KIO::FileUndoManager::Rename, {oldUrl}, newUrl, job);
              job->uiDelegate()->setAutoErrorHandlingEnabled(true);
  
 -            if (!newNameExistsAlready) {
 +            if (m_model->index(newUrl) < 0) {
                  forceUrlsSelection(newUrl, {newUrl});
 +                updateSelectionState();
  
                  // Only connect the result signal if there is no item with the new name
                  // in the model yet, see bug 328262.
@@@ -2154,6 -2152,18 +2190,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);
          } else {
              header->setAutomaticColumnResizing(true);
          }
 -        header->setSidePadding(DetailsModeSettings::sidePadding());
 +        header->setSidePadding(DetailsModeSettings::leftPadding(), DetailsModeSettings::rightPadding());
      }
  
      m_view->endTransaction();
@@@ -2227,51 -2237,6 +2275,51 @@@ void DolphinView::applyModeToView(
      }
  }
  
 +void DolphinView::applyDynamicView()
 +{
 +    ViewProperties props(viewPropertiesUrl());
 +    /* return early if:
 +     * - dynamic view is not enabled
 +     * - the current view mode is already Icon View
 +     * - dynamic view has previously changed the view mode
 +     */
 +    if (!GeneralSettings::dynamicView() || m_mode == IconsView || props.dynamicViewPassed()) {
 +        return;
 +    }
 +
 +    uint imageAndVideoCount = 0;
 +    uint checkedItems = 0;
 +    const uint totalItems = itemsCount();
 +    const KFileItemList itemList = items();
 +    bool applyDynamicView = false;
 +
 +    for (const auto &file : itemList) {
 +        ++checkedItems;
 +        const QString type = file.mimetype().slice(0, 5);
 +
 +        if (type == "image" || type == "video") {
 +            ++imageAndVideoCount;
 +            // if 2/3 or more of the items are images/videos, dynamic view should be applied
 +            applyDynamicView = imageAndVideoCount >= (totalItems * 2 / 3);
 +            if (applyDynamicView) {
 +                break;
 +            }
 +        } else if (checkedItems - imageAndVideoCount > totalItems / 3) {
 +            // if more than a third of the checked files are not media files, return
 +            return;
 +        }
 +    }
 +
 +    if (!applyDynamicView) {
 +        return;
 +    }
 +
 +    props.setAutoSaveEnabled(!GeneralSettings::globalViewProps());
 +    props.setDynamicViewPassed(true);
 +    props.setViewMode(IconsView);
 +    applyViewProperties(props);
 +}
 +
  void DolphinView::pasteToUrl(const QUrl &url)
  {
      KIO::PasteJob *job = KIO::paste(QApplication::clipboard()->mimeData(), url);
      connect(job, &KIO::PasteJob::copyJobStarted, this, [this](const KIO::CopyJob *copyJob) {
          connect(copyJob, &KIO::CopyJob::copying, this, &DolphinView::slotItemCreatedFromJob);
          connect(copyJob, &KIO::CopyJob::moving, this, &DolphinView::slotItemCreatedFromJob);
 +        connect(copyJob, &KIO::CopyJob::warning, this, [this](KJob *job, const QString & /*warning*/) {
 +            Q_EMIT errorMessage(job->errorString(), job->error());
 +        });
          connect(copyJob, &KIO::CopyJob::linking, this, [this](KIO::Job *job, const QString &src, const QUrl &dest) {
              Q_UNUSED(job)
              Q_UNUSED(src)
@@@ -2347,22 -2309,6 +2395,22 @@@ bool DolphinView::isFolderWritable() co
      return m_isFolderWritable;
  }
  
 +int DolphinView::horizontalScrollBarHeight() const
 +{
 +    if (m_container && m_container->horizontalScrollBar() && m_container->horizontalScrollBar()->isVisible()) {
 +        return m_container->horizontalScrollBar()->height();
 +    }
 +    return 0;
 +}
 +
 +void DolphinView::setStatusBarOffset(int offset)
 +{
 +    KItemListView *view = m_container->controller()->view();
 +    if (view) {
 +        view->setStatusBarOffset(offset);
 +    }
 +}
 +
  QUrl DolphinView::viewPropertiesUrl() const
  {
      if (m_viewPropertiesContext.isEmpty()) {
      return url;
  }
  
 -void DolphinView::slotRenameDialogRenamingFinished(const QList<QUrl> &urls)
 -{
 -    forceUrlsSelection(urls.first(), urls);
 -}
 -
  void DolphinView::forceUrlsSelection(const QUrl &current, const QList<QUrl> &selected)
  {
      clearSelection();
@@@ -2420,22 -2371,12 +2468,22 @@@ void DolphinView::showLoadingPlaceholde
  {
      m_placeholderLabel->setText(i18n("Loading…"));
      m_placeholderLabel->setVisible(true);
 +#ifndef QT_NO_ACCESSIBILITY
 +    if (QAccessible::isActive()) {
 +        static_cast<KItemListViewAccessible *>(QAccessible::queryAccessibleInterface(m_view))->announceNewlyLoadedLocation(m_placeholderLabel->text());
 +    }
 +#endif
  }
  
  void DolphinView::updatePlaceholderLabel()
  {
      m_showLoadingPlaceholderTimer->stop();
      if (itemsCount() > 0) {
 +#ifndef QT_NO_ACCESSIBILITY
 +        if (QAccessible::isActive()) {
 +            static_cast<KItemListViewAccessible *>(QAccessible::queryAccessibleInterface(m_view))->announceNewlyLoadedLocation(QString());
 +        }
 +#endif
          m_placeholderLabel->setVisible(false);
          return;
      }
      }
  
      m_placeholderLabel->setVisible(true);
 +#ifndef QT_NO_ACCESSIBILITY
 +    if (QAccessible::isActive()) {
 +        static_cast<KItemListViewAccessible *>(QAccessible::queryAccessibleInterface(m_view))->announceNewlyLoadedLocation(m_placeholderLabel->text());
 +    }
 +#endif
  }
  
  bool DolphinView::tryShowNameToolTip(QHelpEvent *event)
              const KFileItem item = m_model->fileItem(index.value());
              const QString text = item.text();
              const QPoint pos = mapToGlobal(event->pos());
 -            QToolTip::showText(pos, text);
 +            QToolTip::showText(pos, text, this);
              return true;
          }
      }
diff --combined src/views/dolphinview.h
index f491b6dd563ad9f79b07d27051d1eccdf6bb5d1f,2c772ad9034bcc32347b1e2df88243f53df1cabd..d50c932df409b67cd7460732864879dfe4d0aa7d
@@@ -51,6 -51,8 +51,8 @@@ class QRegularExpression
   * - show hidden files
   * - show previews
   * - enable grouping
+  * - grouping order
+  * - grouping type
   */
  class DOLPHIN_EXPORT DolphinView : public QWidget
  {
@@@ -219,6 -221,20 +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;
       */
      bool isFolderWritable() const;
  
 +    /**
 +     * @returns the height of the scrollbar at the bottom of the view or zero if no such scroll bar is visible.
 +     */
 +    int horizontalScrollBarHeight() const;
 +
 +    /**
 +     * Set the offset for any view items that small statusbar would otherwise
 +     * cover. For example, in compact view this is used to make sure no
 +     * item is covered by statusbar.
 +     */
 +    void setStatusBarOffset(int offset);
 +
  public Q_SLOTS:
  
      void reload();
@@@ -534,6 -538,12 +550,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.
@@@ -704,12 -714,13 +726,12 @@@ private Q_SLOTS
      void slotViewContextMenuRequested(const QPointF &pos);
      void slotHeaderContextMenuRequested(const QPointF &pos);
      void slotHeaderColumnWidthChangeFinished(const QByteArray &role, qreal current);
 -    void slotSidePaddingWidthChanged(qreal width);
 +    void slotSidePaddingWidthChanged(qreal leftPaddingWidth, qreal rightPaddingWidth);
      void slotItemHovered(int index);
      void slotItemUnhovered(int index);
      void slotItemDropEvent(int index, QGraphicsSceneDragDropEvent *event);
      void slotModelChanged(KItemModelBase *current, KItemModelBase *previous);
      void slotMouseButtonPressed(int itemIndex, Qt::MouseButtons buttons);
 -    void slotRenameDialogRenamingFinished(const QList<QUrl> &urls);
      void slotSelectedItemTextPressed(int index);
      void slotItemCreatedFromJob(KIO::Job *, const QUrl &, const QUrl &to);
      void slotItemLinkCreatedFromJob(KIO::Job *, const QUrl &, const QString &, const QUrl &to);
@@@ -867,11 -878,6 +889,11 @@@ private
       */
      void applyModeToView();
  
 +    /**
 +     * Changes the current view based on the content of the directory.
 +     */
 +    void applyDynamicView();
 +
      enum Selection { HasSelection, NoSelection };
      /**
       * Helper method for DolphinView::requestStatusBarText().
index e504fd831ab5effbf81c392be6fad2b825213f48,815b0f63e685c7c525de99fb0bd1a154caf030a7..14600c133af2187647146d5022b7f7a122ff8281
@@@ -12,7 -12,6 +12,7 @@@
  #include "selectionmode/actiontexthelper.h"
  #include "settings/viewpropertiesdialog.h"
  #include "views/zoomlevelinfo.h"
 +#include "views/zoomwidgetaction.h"
  
  #if HAVE_BALOO
  #include <Baloo/IndexerConfig>
@@@ -34,6 -33,7 +34,7 @@@ DolphinViewActionHandler::DolphinViewAc
      , m_actionCollection(collection)
      , m_currentView(nullptr)
      , m_sortByActions()
+     , m_groupByActions()
      , m_visibleRoles()
  {
      Q_ASSERT(m_actionCollection);
@@@ -59,6 -59,8 +60,8 @@@ void DolphinViewActionHandler::setCurre
      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());
@@@ -85,12 -87,6 +88,12 @@@ void DolphinViewActionHandler::createAc
      newDirAction->setEnabled(false); // Will be enabled in slotWriteStateChanged(bool) if the current URL is writable
      connect(newDirAction, &QAction::triggered, this, &DolphinViewActionHandler::createDirectoryTriggered);
  
 +    QAction *newFileAction = m_actionCollection->addAction(QStringLiteral("create_file"));
 +    newFileAction->setText(i18nc("@action", "Create File…"));
 +    newFileAction->setIcon(QIcon::fromTheme(QStringLiteral("document-new")));
 +    newFileAction->setEnabled(false); // Will be enabled in slotWriteStateChanged(bool) if the current URL is writable
 +    connect(newFileAction, &QAction::triggered, this, &DolphinViewActionHandler::createFileTriggered);
 +
      // File menu
  
      auto renameAction = KStandardAction::renameFile(this, &DolphinViewActionHandler::slotRename, m_actionCollection);
                                         "view the contents of multiple folders in the same list.</para>"));
  
      KSelectAction *viewModeActions = m_actionCollection->add<KSelectAction>(QStringLiteral("view_mode"));
 -    viewModeActions->setText(i18nc("@action:intoolbar", "View Mode"));
 +    viewModeActions->setText(i18nc("@action:intoolbar", "Change View Mode"));
 +    viewModeActions->setWhatsThis(xi18nc("@info:whatsthis View Mode Toolbutton", "This cycles through all view modes."));
      viewModeActions->addAction(iconsAction);
      viewModeActions->addAction(compactAction);
      viewModeActions->addAction(detailsAction);
      viewModeActions->setToolBarMode(KSelectAction::MenuMode);
 +    viewModeActions->setToolButtonPopupMode(QToolButton::ToolButtonPopupMode::MenuButtonPopup);
      connect(viewModeActions, &KSelectAction::actionTriggered, this, &DolphinViewActionHandler::slotViewModeActionTriggered);
 +    connect(viewModeActions, &KSelectAction::triggered, this, [this, viewModeActions, iconsAction, compactAction, detailsAction]() {
 +        // Loop through the actions when button is clicked
 +        const auto currentAction = viewModeActions->currentAction();
 +        if (currentAction == iconsAction) {
 +            slotViewModeActionTriggered(compactAction);
 +        } else if (currentAction == compactAction) {
 +            slotViewModeActionTriggered(detailsAction);
 +        } else if (currentAction == detailsAction) {
 +            slotViewModeActionTriggered(iconsAction);
 +        }
 +    });
  
      QAction *zoomInAction = KStandardAction::zoomIn(this, &DolphinViewActionHandler::zoomIn, m_actionCollection);
      zoomInAction->setWhatsThis(i18nc("@info:whatsthis zoom in", "This increases the icon size."));
      QAction *zoomOutAction = KStandardAction::zoomOut(this, &DolphinViewActionHandler::zoomOut, m_actionCollection);
      zoomOutAction->setWhatsThis(i18nc("@info:whatsthis zoom out", "This reduces the icon size."));
  
 -    KActionMenu *zoomMenu = m_actionCollection->add<KActionMenu>(QStringLiteral("zoom"));
 -    zoomMenu->setText(i18nc("@action:inmenu menu of zoom actions", "Zoom"));
 -    zoomMenu->setIcon(QIcon::fromTheme(QStringLiteral("zoom")));
 -    zoomMenu->setPopupMode(QToolButton::InstantPopup);
 -    zoomMenu->addAction(zoomInAction);
 -    zoomMenu->addAction(zoomResetAction);
 -    zoomMenu->addAction(zoomOutAction);
 +    ZoomWidgetAction *zoomWidgetAction = new ZoomWidgetAction(zoomInAction, zoomResetAction, zoomOutAction, m_actionCollection);
 +    m_actionCollection->addAction(QStringLiteral("zoom"), zoomWidgetAction);
  
      KToggleAction *showPreview = m_actionCollection->add<KToggleAction>(QStringLiteral("show_preview"));
      showPreview->setText(i18nc("@action:intoolbar", "Show Previews"));
  
      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_"));
  
                                          "This opens a window "
                                          "in which all folder view properties can be adjusted."));
      connect(adjustViewProps, &QAction::triggered, this, &DolphinViewActionHandler::slotAdjustViewProperties);
 +
 +    // View settings: the dropdown menu contains various view-related actions
 +    KActionMenu *viewSettings = m_actionCollection->add<KActionMenu>(QStringLiteral("view_settings"));
 +    viewSettings->setText(i18nc("@action:intoolbar", "View Settings"));
 +    viewSettings->setWhatsThis(
 +        xi18nc("@info:whatsthis View Settings Toolbutton", "This cycles through all view modes. The dropdown menu contains various view-related actions."));
 +    const auto actions = viewModeActions->actions();
 +    for (QAction *action : actions) {
 +        viewSettings->addAction(action);
 +    }
 +    viewSettings->addSeparator();
 +    viewSettings->addAction(zoomWidgetAction);
 +    viewSettings->addAction(sortByActionMenu);
 +    viewSettings->addAction(visibleRolesMenu);
 +    viewSettings->addAction(showPreview);
 +    viewSettings->addAction(showInGroups);
 +    viewSettings->addAction(showHiddenFiles);
 +    viewSettings->addAction(adjustViewProps);
 +    viewSettings->setPopupMode(QToolButton::ToolButtonPopupMode::MenuButtonPopup);
 +    connect(viewSettings, &KActionMenu::triggered, viewModeActions, &KSelectAction::triggered);
  }
  
  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);
          }
@@@ -454,9 -466,6 +501,9 @@@ void DolphinViewActionHandler::slotView
  
      QAction *viewModeMenu = m_actionCollection->action(QStringLiteral("view_mode"));
      viewModeMenu->setIcon(action->icon());
 +
 +    QAction *viewSettingsAction = m_actionCollection->action(QStringLiteral("view_settings"));
 +    viewSettingsAction->setIcon(action->icon());
  }
  
  void DolphinViewActionHandler::slotRename()
@@@ -535,9 -544,6 +582,9 @@@ void DolphinViewActionHandler::updateVi
  
          QAction *viewModeMenu = m_actionCollection->action(QStringLiteral("view_mode"));
          viewModeMenu->setIcon(viewModeAction->icon());
 +
 +        QAction *viewSettingsAction = m_actionCollection->action(QStringLiteral("view_settings"));
 +        viewSettingsAction->setIcon(viewModeAction->icon());
      }
  
      QAction *showPreviewAction = m_actionCollection->action(QStringLiteral("show_preview"));
      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
@@@ -589,13 -597,22 +638,22 @@@ void DolphinViewActionHandler::toggleSo
  
  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);
@@@ -667,9 -684,7 +725,9 @@@ void DolphinViewActionHandler::slotHidd
  
  void DolphinViewActionHandler::slotWriteStateChanged(bool isFolderWritable)
  {
 -    m_actionCollection->action(QStringLiteral("create_dir"))->setEnabled(isFolderWritable && KProtocolManager::supportsMakeDir(currentView()->url()));
 +    const bool supportsMakeDir = KProtocolManager::supportsMakeDir(currentView()->url());
 +    m_actionCollection->action(QStringLiteral("create_dir"))->setEnabled(isFolderWritable && supportsMakeDir);
 +    m_actionCollection->action(QStringLiteral("create_file"))->setEnabled(isFolderWritable);
  }
  
  KToggleAction *DolphinViewActionHandler::iconsModeAction()
@@@ -717,8 -732,8 +775,8 @@@ void DolphinViewActionHandler::slotSort
          }
      }
  
-     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)
@@@ -781,6 -840,32 +883,32 @@@ void DolphinViewActionHandler::slotSort
      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();
index 20d62cf64a0e61601495734f354315d9b222e10a,93ed10e2beec2dd134495715fa9b5a931ca9a769..a589be9eaac7eb8e41efd7bd20b49fe75552a1b4
@@@ -87,13 -87,6 +87,13 @@@ Q_SIGNALS
       */
      void createDirectoryTriggered();
  
 +    /**
 +     * Emitted if the user requested creating a new file.
 +     * The receiver of the signal (DolphinMainWindow or DolphinPart) invokes
 +     * the method createFile of their KNewFileMenu instance.
 +     */
 +    void createFileTriggered();
 +
      /** Used to request either entering or leaving of selection mode */
      void selectionModeChangeTriggered(bool enabled, SelectionMode::BottomBar::Contents bottomBarContents = SelectionMode::BottomBar::Contents::GeneralContents);
  
@@@ -165,6 -158,16 +165,16 @@@ private Q_SLOTS
       */
      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> &current, const QList<QByteArray> &previous);
  
+     /**
+      * Changes the grouping of the current view.
+      */
+     void slotGroupTriggered(QAction *);
+     
      /**
       * Switches between sorting by groups or not.
       */
@@@ -281,6 -289,7 +296,7 @@@ private
      DolphinView *m_currentView;
  
      QHash<QByteArray, KToggleAction *> m_sortByActions;
+     QHash<QByteArray, KToggleAction *> m_groupByActions;
      QHash<QByteArray, KToggleAction *> m_visibleRoles;
  };
  
index 8bf3b2531290a68d8be66928e310ada3820e5575,60c64306266d0f015cb70cb7d5f0c26b26cce889..9c03cf598087b203d3882dbc3fea305b301c365c
  #include "dolphindebug.h"
  
  #include <QCryptographicHash>
 +#include <QTemporaryFile>
  
  #include <KFileItem>
 +#include <KFileMetaData/UserMetaData>
  
  namespace
  {
@@@ -33,74 -31,6 +33,74 @@@ const char CustomizedDetailsString[] = 
  const char ViewPropertiesFileName[] = ".directory";
  }
  
 +ViewPropertySettings *ViewProperties::loadProperties(const QString &folderPath) const
 +{
 +    const QString settingsFile = folderPath + QDir::separator() + ViewPropertiesFileName;
 +
 +    KFileMetaData::UserMetaData metadata(folderPath);
 +    if (!metadata.isSupported()) {
 +        return new ViewPropertySettings(KSharedConfig::openConfig(settingsFile, KConfig::SimpleConfig));
 +    }
 +
 +    std::unique_ptr<QTemporaryFile> tempFile(new QTemporaryFile());
 +    tempFile->setAutoRemove(false);
 +    if (!tempFile->open()) {
 +        qCWarning(DolphinDebug) << "Could not open temp file";
 +        return nullptr;
 +    }
 +    if (QFile::exists(settingsFile)) {
 +        // copy settings to tempfile to load them separately
 +        QFile::remove(tempFile->fileName());
 +        QFile::copy(settingsFile, tempFile->fileName());
 +
 +        auto config = KConfig(tempFile->fileName(), KConfig::SimpleConfig);
 +        // ignore settings that are outside of dolphin scope
 +        if (config.hasGroup("Dolphin") || config.hasGroup("Settings")) {
 +            const auto groupList = config.groupList();
 +            for (const auto &group : groupList) {
 +                if (group != QStringLiteral("Dolphin") && group != QStringLiteral("Settings")) {
 +                    config.deleteGroup(group);
 +                }
 +            }
 +            return new ViewPropertySettings(KSharedConfig::openConfig(tempFile->fileName(), KConfig::SimpleConfig));
 +
 +        } else if (!config.groupList().isEmpty()) {
 +            // clear temp file content
 +            QFile::remove(tempFile->fileName());
 +        }
 +    }
 +
 +    // load from metadata
 +    const QString viewPropertiesString = metadata.attribute(QStringLiteral("kde.fm.viewproperties#1"));
 +    if (viewPropertiesString.isEmpty()) {
 +        return nullptr;
 +    }
 +    // load view properties from xattr to temp file then loads into ViewPropertySettings
 +    QFile outputFile(tempFile->fileName());
 +    outputFile.open(QIODevice::WriteOnly);
 +    outputFile.write(viewPropertiesString.toUtf8());
 +    outputFile.close();
 +    return new ViewPropertySettings(KSharedConfig::openConfig(tempFile->fileName(), KConfig::SimpleConfig));
 +}
 +
 +ViewPropertySettings *ViewProperties::defaultProperties() const
 +{
 +    auto props = loadProperties(destinationDir(QStringLiteral("global")));
 +    if (props == nullptr) {
 +        qCWarning(DolphinDebug) << "Could not load default global viewproperties";
 +        QTemporaryFile tempFile;
 +        tempFile.setAutoRemove(false);
 +        if (!tempFile.open()) {
 +            qCWarning(DolphinDebug) << "Could not open temp file";
 +            props = new ViewPropertySettings;
 +        } else {
 +            props = new ViewPropertySettings(KSharedConfig::openConfig(tempFile.fileName(), KConfig::SimpleConfig));
 +        }
 +    }
 +
 +    return props;
 +}
 +
  ViewProperties::ViewProperties(const QUrl &url)
      : m_changedProps(false)
      , m_autoSave(true)
          m_filePath = destinationDir(QStringLiteral("remote")) + m_filePath;
      }
  
 -    const QString file = m_filePath + QDir::separator() + ViewPropertiesFileName;
 -    m_node = new ViewPropertySettings(KSharedConfig::openConfig(file));
 +    m_node = loadProperties(m_filePath);
 +
 +    bool useDefaultSettings = useGlobalViewProps ||
 +        // If the props timestamp is too old,
 +        // use default values instead.
 +        (m_node != nullptr && (!useGlobalViewProps || useSearchView || useTrashView || useRecentDocumentsView || useDownloadsView)
 +         && m_node->timestamp() < settings->viewPropsTimestamp());
 +
 +    if (m_node == nullptr) {
 +        // no settings found for m_filepath, load defaults
 +        m_node = defaultProperties();
 +        useDefaultSettings = true;
 +    }
  
 -    // If the .directory file does not exist or the timestamp is too old,
 -    // use default values instead.
 -    const bool useDefaultProps = (!useGlobalViewProps || useSearchView || useTrashView || useRecentDocumentsView || useDownloadsView)
 -        && (!QFile::exists(file) || (m_node->timestamp() < settings->viewPropsTimestamp()));
 -    if (useDefaultProps) {
 +    // default values for special directories
 +    if (useDefaultSettings) {
          if (useSearchView) {
              const QString path = url.path();
  
                  setSortRole(QByteArrayLiteral("modificationtime"));
              }
          } else {
 -            // The global view-properties act as default for directories without
 -            // any view-property configuration. Constructing a ViewProperties
 -            // instance for an empty QUrl ensures that the global view-properties
 -            // are loaded.
 -            QUrl emptyUrl;
 -            ViewProperties defaultProps(emptyUrl);
 -            setDirProperties(defaultProps);
 -
              m_changedProps = false;
          }
      }
@@@ -242,11 -172,6 +242,11 @@@ ViewProperties::~ViewProperties(
          save();
      }
  
 +    if (!m_node->config()->name().endsWith(ViewPropertiesFileName)) {
 +        // remove temp file
 +        QFile::remove(m_node->config()->name());
 +    }
 +
      delete m_node;
      m_node = nullptr;
  }
@@@ -330,6 -255,32 +330,32 @@@ Qt::SortOrder ViewProperties::sortOrder
      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) {
@@@ -356,19 -307,6 +382,19 @@@ bool ViewProperties::sortHiddenLast() c
      return m_node->sortHiddenLast();
  }
  
 +void ViewProperties::setDynamicViewPassed(bool dynamicViewPassed)
 +{
 +    if (m_node->dynamicViewPassed() != dynamicViewPassed) {
 +        m_node->setDynamicViewPassed(dynamicViewPassed);
 +        update();
 +    }
 +}
 +
 +bool ViewProperties::dynamicViewPassed() const
 +{
 +    return m_node->dynamicViewPassed();
 +}
 +
  void ViewProperties::setVisibleRoles(const QList<QByteArray> &roles)
  {
      if (roles == visibleRoles()) {
@@@ -474,6 -412,8 +500,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());
@@@ -500,112 -440,17 +528,112 @@@ void ViewProperties::update(
  void ViewProperties::save()
  {
      qCDebug(DolphinDebug) << "Saving view-properties to" << m_filePath;
 +
 +    auto cleanDotDirectoryFile = [this]() {
 +        const QString settingsFile = m_filePath + QDir::separator() + ViewPropertiesFileName;
 +        if (QFile::exists(settingsFile)) {
 +            qCDebug(DolphinDebug) << "cleaning .directory" << settingsFile;
 +            KConfig cfg(settingsFile, KConfig::OpenFlag::SimpleConfig);
 +            const auto groupList = cfg.groupList();
 +            for (const auto &group : groupList) {
 +                if (group == QStringLiteral("Dolphin") || group == QStringLiteral("Settings")) {
 +                    cfg.deleteGroup(group);
 +                }
 +            }
 +            if (cfg.groupList().isEmpty()) {
 +                QFile::remove(settingsFile);
 +            } else if (cfg.isDirty()) {
 +                cfg.sync();
 +            }
 +        }
 +    };
 +
 +    // ensures the destination dir exists, in case we don't write metadata directly on the folder
 +    QDir destinationDir(m_filePath);
 +    if (!destinationDir.exists() && !destinationDir.mkpath(m_filePath)) {
 +        qCWarning(DolphinDebug) << "Could not create fake directory to store metadata";
 +    }
 +
 +    KFileMetaData::UserMetaData metaData(m_filePath);
 +    if (metaData.isSupported()) {
 +        const auto metaDataKey = QStringLiteral("kde.fm.viewproperties#1");
 +
 +        const auto items = m_node->items();
 +        const auto defaultConfig = defaultProperties();
 +        bool allDefault = true;
 +        for (const auto item : items) {
 +            if (item->name() == "Timestamp") {
 +                continue;
 +            }
 +            if (item->name() == "Version") {
 +                if (m_node->version() != CurrentViewPropertiesVersion) {
 +                    allDefault = false;
 +                    break;
 +                } else {
 +                    continue;
 +                }
 +            }
 +            auto defaultItem = defaultConfig->findItem(item->name());
 +            if (!defaultItem || defaultItem->property() != item->property()) {
 +                allDefault = false;
 +                break;
 +            }
 +        }
 +
 +        if (allDefault) {
 +            if (metaData.hasAttribute(metaDataKey)) {
 +                qCDebug(DolphinDebug) << "clearing extended attributes for " << m_filePath;
 +                const auto result = metaData.setAttribute(metaDataKey, QString());
 +                if (result != KFileMetaData::UserMetaData::NoError) {
 +                    qCWarning(DolphinDebug) << "could not clear extended attributes for " << m_filePath << "error:" << result;
 +                }
 +            }
 +            cleanDotDirectoryFile();
 +            return;
 +        }
 +
 +        // save config to disk
 +        if (!m_node->save()) {
 +            qCWarning(DolphinDebug) << "could not save viewproperties" << m_node->config()->name();
 +            return;
 +        }
 +
 +        QFile configFile(m_node->config()->name());
 +        if (!configFile.open(QIODevice::ReadOnly)) {
 +            qCWarning(DolphinDebug) << "Could not open readonly config file" << m_node->config()->name();
 +        } else {
 +            // load config from disk
 +            const QString viewPropertiesString = configFile.readAll();
 +
 +            // save to xattr
 +            const auto result = metaData.setAttribute(metaDataKey, viewPropertiesString);
 +            if (result != KFileMetaData::UserMetaData::NoError) {
 +                if (result == KFileMetaData::UserMetaData::NoSpace) {
 +                    // copy settings to dotDirectory file as fallback
 +                    if (!configFile.copy(m_filePath + QDir::separator() + ViewPropertiesFileName)) {
 +                        qCWarning(DolphinDebug) << "could not write viewproperties to .directory for dir " << m_filePath;
 +                    }
 +                    // free the space used by viewproperties from the file metadata
 +                    metaData.setAttribute(metaDataKey, "");
 +                } else {
 +                    qCWarning(DolphinDebug) << "could not save viewproperties to extended attributes for dir " << m_filePath << "error:" << result;
 +                }
 +                // keep .directory file
 +                return;
 +            }
 +            cleanDotDirectoryFile();
 +        }
 +
 +        m_changedProps = false;
 +        return;
 +    }
 +
      QDir dir;
      dir.mkpath(m_filePath);
      m_node->setVersion(CurrentViewPropertiesVersion);
      m_node->save();
 -    m_changedProps = false;
 -}
  
 -bool ViewProperties::exist() const
 -{
 -    const QString file = m_filePath + QDir::separator() + ViewPropertiesFileName;
 -    return QFile::exists(file);
 +    m_changedProps = false;
  }
  
  QString ViewProperties::destinationDir(const QString &subDir) const
index bee1e7330f3bf00161c6371d05829a76d17039a8,0c0452d7aea0fe11be16afda688151322d0a08ef..ac9ee4bb6082917a72215069a951a45c4c5806d5
@@@ -59,15 -59,18 +59,21 @@@ public
      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;
  
      void setSortHiddenLast(bool hiddenLast);
      bool sortHiddenLast() const;
  
 +    void setDynamicViewPassed(bool dynamicViewPassed);
 +    bool dynamicViewPassed() const;
 +
      /**
       * Sets the additional information for the current set view-mode.
       * Note that the additional-info property is the only property where
       */
      void save();
  
 -    /**
 -     * @return True if properties for the given URL exist:
 -     *         As soon as the properties for an URL have been saved with
 -     *         ViewProperties::save(), true will be returned. If false is
 -     *         returned, the default view-properties are used.
 -     */
 -    bool exist() const;
 -
 -private:
      /**
       * Returns the destination directory path where the view
       * properties are stored. \a subDir specifies the used sub
       */
      QString destinationDir(const QString &subDir) const;
  
 +private:
      /**
       * Returns the view-mode prefix when storing additional properties for
       * a view-mode.
       */
      static QString directoryHashForUrl(const QUrl &url);
  
 +    /** @returns a ViewPropertySettings object with properties loaded for the directory at @param filePath. Ownership is returned to the caller. */
 +    ViewPropertySettings *loadProperties(const QString &folderPath) const;
 +    /** @returns a ViewPropertySettings object with the globally configured default values. Ownership is returned to the caller. */
 +    ViewPropertySettings *defaultProperties() const;
 +
      Q_DISABLE_COPY(ViewProperties)
  
  private: