[system/dolphin/release/26.08] src: dolphinview,dolphinviewactionhandler: split create folder into two actions

Méven Car <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit 705e366aaf6ea76c2583107c708c7bdd6dc4a08a by Méven Car.
Committed on 08/08/2026 at 13:54.
Pushed by meven into branch 'release/26.08'.

dolphinview,dolphinviewactionhandler: split create folder into two actions

Ctrl+Shift+N and the context menu shared a single action, so the menu
advertised a shortcut that did something else: the shortcut creates in the
viewed directory, the menu in the folder that was clicked. Add a second
action for the selected folder, so the shortcut is no longer shown in the
menu and can be bound separately. It gets no default shortcut, matching
the existing paste-into-folder entry.

A newly created folder is now also selected regardless of the previous
selection. Before it was selected only when nothing was selected, so the
common "select something, then create a folder next to it" flow left it
unselected and it had to be looked up by hand.

Implements the approach described in
https://invent.kde.org/system/dolphin/-/merge_requests/1137#note_1400212

CCBUG: 508196
CCBUG: 512020


(cherry picked from commit 2ae4985411d56807119287714f7fc4924564c65d)

Co-authored-by: Gergő Gelóczi <[email protected]>

M  +1    -1    src/dolphincontextmenu.cpp
M  +4    -2    src/dolphinmainwindow.cpp
M  +1    -1    src/dolphinmainwindow.h
M  +9    -2    src/dolphinpart.cpp
M  +5    -0    src/dolphinpart.h
M  +153  -0    src/tests/dolphinmainwindowtest.cpp
M  +8    -10   src/views/dolphinview.cpp
M  +35   -1    src/views/dolphinviewactionhandler.cpp
M  +13   -2    src/views/dolphinviewactionhandler.h

https://invent.kde.org/system/dolphin/-/commit/705e366aaf6ea76c2583107c708c7bdd6dc4a08a

diff --git a/src/dolphincontextmenu.cpp b/src/dolphincontextmenu.cpp
index 796c723edb..b9b6edd8db 100644
--- a/src/dolphincontextmenu.cpp
+++ b/src/dolphincontextmenu.cpp
@@ -217,7 +217,7 @@ void DolphinContextMenu::addDirectoryItemContextMenu()
     addOpenWithActions();
 
     // set up 'Create New' menu
-    QAction *newDirAction = m_mainWindow->actionCollection()->action(QStringLiteral("create_dir"));
+    QAction *newDirAction = m_mainWindow->actionCollection()->action(QStringLiteral("create_subdir"));
     QAction *newFileAction = m_mainWindow->actionCollection()->action(QStringLiteral("create_file"));
     // Do not parent this to the menu, it has to outlive it. It is deleted manually below once a file has been created.
     DolphinNewFileMenu *newFileMenu = new DolphinNewFileMenu(newDirAction, newFileAction, m_mainWindow);
diff --git a/src/dolphinmainwindow.cpp b/src/dolphinmainwindow.cpp
index 317b2992c7..a29fe0df39 100644
--- a/src/dolphinmainwindow.cpp
+++ b/src/dolphinmainwindow.cpp
@@ -826,14 +826,14 @@ void DolphinMainWindow::updateNewMenu()
     m_newFileMenu->setWorkingDirectory(activeViewContainer()->url());
 }
 
-void DolphinMainWindow::createDirectory()
+void DolphinMainWindow::createDirectory(const QUrl &parent)
 {
     // When creating directory, namejob is being run. In network folders,
     // this job can take long time, so instead of starting multiple namejobs,
     // just check if we are already running one. This prevents opening multiple
     // dialogs. BUG:481401
     if (!m_newFileMenu->isCreateDirectoryRunning()) {
-        m_newFileMenu->setWorkingDirectory(activeViewContainer()->url());
+        m_newFileMenu->setWorkingDirectory(parent);
         m_newFileMenu->createDirectory();
     }
 }
@@ -1515,6 +1515,8 @@ void DolphinMainWindow::slotWriteStateChanged(bool isFolderWritable)
                                                     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."));
+        m_disabledActionNotifier->setDisabledReason(actionCollection()->action(QStringLiteral("create_subdir")),
+                                                    i18nc("@info", "Cannot create new subfolder: Select a single folder you can write to."));
     });
 }
 
diff --git a/src/dolphinmainwindow.h b/src/dolphinmainwindow.h
index f847ff5098..50e944fa77 100644
--- a/src/dolphinmainwindow.h
+++ b/src/dolphinmainwindow.h
@@ -304,7 +304,7 @@ private Q_SLOTS:
     /** Updates the 'Create New...' sub menu. */
     void updateNewMenu();
 
-    void createDirectory();
+    void createDirectory(const QUrl &parent);
     void createFile();
 
     /** Shows the error message in a non-modal message box above the active view. */
diff --git a/src/dolphinpart.cpp b/src/dolphinpart.cpp
index 6fa4b6228f..939d08b1bb 100644
--- a/src/dolphinpart.cpp
+++ b/src/dolphinpart.cpp
@@ -97,7 +97,10 @@ DolphinPart::DolphinPart(QWidget *parentWidget, QObject *parent, const KPluginMe
 
     m_actionHandler = new DolphinViewActionHandler(actionCollection(), nullptr, this);
     m_actionHandler->setCurrentView(m_view);
-    connect(m_actionHandler, &DolphinViewActionHandler::createDirectoryTriggered, this, &DolphinPart::createDirectory);
+    connect(m_actionHandler, &DolphinViewActionHandler::createDirectoryTriggered, this, [this](const QUrl &parent) {
+        setNewFileMenuWorkingDirectory(parent);
+        createDirectory();
+    });
 
     m_remoteEncoding = new DolphinRemoteEncoding(this, m_actionHandler);
     connect(this, &DolphinPart::aboutToOpenURL, m_remoteEncoding, &DolphinRemoteEncoding::slotAboutToOpenUrl);
@@ -549,9 +552,13 @@ void DolphinPart::updateProgress(int percent)
     Q_EMIT m_extension->loadingProgress(percent);
 }
 
+void DolphinPart::setNewFileMenuWorkingDirectory(const QUrl &directory)
+{
+    m_newFileMenu->setWorkingDirectory(directory);
+}
+
 void DolphinPart::createDirectory()
 {
-    m_newFileMenu->setWorkingDirectory(url());
     m_newFileMenu->createDirectory();
 }
 
diff --git a/src/dolphinpart.h b/src/dolphinpart.h
index aec4277531..6dc72b0900 100644
--- a/src/dolphinpart.h
+++ b/src/dolphinpart.h
@@ -216,6 +216,11 @@ private Q_SLOTS:
      */
     void updateProgress(int percent);
 
+    /**
+     * Sets the directory the 'Create New...' menu creates in.
+     */
+    void setNewFileMenuWorkingDirectory(const QUrl &directory);
+
     void createDirectory();
 
     /**
diff --git a/src/tests/dolphinmainwindowtest.cpp b/src/tests/dolphinmainwindowtest.cpp
index ad2543a999..869f4305ab 100644
--- a/src/tests/dolphinmainwindowtest.cpp
+++ b/src/tests/dolphinmainwindowtest.cpp
@@ -5,6 +5,7 @@
  */
 
 #include "dolphinmainwindow.h"
+#include "dolphin_detailsmodesettings.h"
 #include "dolphin_generalsettings.h"
 #include "dolphinnewfilemenu.h"
 #include "dolphintabpage.h"
@@ -33,6 +34,7 @@
 #include <QFileSystemWatcher>
 #include <QKeySequence>
 #include <QPixmap>
+#include <QScopeGuard>
 #include <QScopedPointer>
 #include <QSignalSpy>
 #include <QStandardPaths>
@@ -59,6 +61,9 @@ private Q_SLOTS:
     void testOpenInNewTabTitle();
     void testNewFileMenuEnabled_data();
     void testNewFileMenuEnabled();
+    void testCreateDirectoryFocus_data();
+    void testCreateDirectoryFocus();
+    void testCreateSubdirectory();
     void testCreateFileAction();
     void testCreateFileActionRequiresWritePermission();
     void testWindowTitle_data();
@@ -79,6 +84,8 @@ private Q_SLOTS:
     void cleanupTestCase();
 
 private:
+    bool createDirectory(QAction *action, const QString &name);
+
     QScopedPointer<DolphinMainWindow> m_mainWindow;
 };
 
@@ -432,6 +439,152 @@ void DolphinMainWindowTest::testNewFileMenuEnabled()
     QTRY_COMPARE(newFileMenu->isEnabled(), expectedEnabled);
 }
 
+bool DolphinMainWindowTest::createDirectory(QAction *action, const QString &name)
+{
+    if (!QTest::qWaitFor([this, action] {
+            return action->isEnabled() && !m_mainWindow->m_newFileMenu->isCreateDirectoryRunning();
+        })) {
+        return false;
+    }
+    action->trigger();
+    if (!QTest::qWaitFor([] {
+            return QApplication::activeModalWidget() != nullptr;
+        })) {
+        return false;
+    }
+    QWidget *dialog = QApplication::activeModalWidget()->focusWidget();
+    if (!dialog) {
+        return false;
+    }
+    QTest::keyClicks(dialog, name);
+    QTest::keyClick(dialog, Qt::Key_Enter);
+    return QTest::qWaitFor([] {
+        return QApplication::activeModalWidget() == nullptr;
+    });
+}
+
+void DolphinMainWindowTest::testCreateDirectoryFocus_data()
+{
+    QTest::addColumn<DolphinView::Mode>("viewMode");
+
+    QTest::newRow("icons") << DolphinView::IconsView;
+    QTest::newRow("expandable details") << DolphinView::DetailsView;
+}
+
+/**
+ * A new directory gets selected and focused in every view mode, even if something else was selected before.
+ */
+void DolphinMainWindowTest::testCreateDirectoryFocus()
+{
+    QFETCH(DolphinView::Mode, viewMode);
+
+    QScopedPointer<TestDir> testDir{new TestDir()};
+    testDir->createFile("selected-file");
+    const QUrl selectedFileUrl = QUrl::fromLocalFile(testDir->url().toLocalFile() + "/selected-file");
+    // sorts before "selected-file", so inserting it shifts the index of the pre-selected item
+    const QUrl newDirectoryUrl = QUrl::fromLocalFile(testDir->url().toLocalFile() + "/new-directory");
+
+    // this setting is global and persisted, so restore it even when the test aborts early
+    const bool expandableFoldersBefore = DetailsModeSettings::expandableFolders();
+    auto restoreExpandableFolders = qScopeGuard([expandableFoldersBefore] {
+        DetailsModeSettings::setExpandableFolders(expandableFoldersBefore);
+        DetailsModeSettings::self()->save();
+    });
+    DetailsModeSettings::setExpandableFolders(true);
+    DetailsModeSettings::self()->save();
+
+    m_mainWindow->openDirectories({QDir::cleanPath(testDir->url().toString())}, false);
+    m_mainWindow->show();
+#ifdef Q_OS_WIN
+    if (!QTest::qWaitForWindowExposed(m_mainWindow.data())) {
+        QSKIP("Window not exposed on Windows, probably running in a headless CI environment.");
+    }
+#else
+    QVERIFY(QTest::qWaitForWindowExposed(m_mainWindow.data()));
+#endif
+    QVERIFY(m_mainWindow->isVisible());
+
+    DolphinView *view = m_mainWindow->m_activeViewContainer->view();
+    view->setViewMode(viewMode);
+    QCOMPARE(view->m_view->supportsItemExpanding(), viewMode == DolphinView::DetailsView);
+
+    QTRY_COMPARE(view->items().count(), 1);
+
+    KItemListSelectionManager *selectionManager = view->m_container->controller()->selectionManager();
+    const auto currentItemUrl = [view, selectionManager]() {
+        return view->m_model->fileItem(selectionManager->currentItem()).url();
+    };
+
+    // a pre-existing selection used to prevent the new directory from being selected
+    view->forceUrlsSelection(selectedFileUrl, {selectedFileUrl});
+    view->updateViewState();
+    QCOMPARE(view->selectedItems().urlList(), QList<QUrl>{selectedFileUrl});
+    QCOMPARE(currentItemUrl(), selectedFileUrl);
+
+    QAction *createDirectoryAction = m_mainWindow->actionCollection()->action(QStringLiteral("create_dir"));
+    QVERIFY(createDirectoryAction);
+    QVERIFY(createDirectory(createDirectoryAction, QStringLiteral("new-directory")));
+
+    QTRY_COMPARE(view->items().count(), 2);
+
+    QTRY_COMPARE(view->selectedItems().urlList(), QList<QUrl>{newDirectoryUrl});
+    QTRY_COMPARE(currentItemUrl(), newDirectoryUrl);
+}
+
+void DolphinMainWindowTest::testCreateSubdirectory()
+{
+    QScopedPointer<TestDir> testDir{new TestDir()};
+    testDir->createDir("parent");
+    testDir->createFile("a-file");
+    const QUrl parentUrl = QUrl::fromLocalFile(testDir->url().toLocalFile() + "/parent");
+    const QUrl fileUrl = QUrl::fromLocalFile(testDir->url().toLocalFile() + "/a-file");
+
+    m_mainWindow->openDirectories({QDir::cleanPath(testDir->url().toString())}, false);
+    m_mainWindow->show();
+#ifdef Q_OS_WIN
+    if (!QTest::qWaitForWindowExposed(m_mainWindow.data())) {
+        QSKIP("Window not exposed on Windows, probably running in a headless CI environment.");
+    }
+#else
+    QVERIFY(QTest::qWaitForWindowExposed(m_mainWindow.data()));
+#endif
+
+    DolphinView *view = m_mainWindow->m_activeViewContainer->view();
+    QTRY_COMPARE(view->items().count(), 2);
+
+    QAction *createSubdir = m_mainWindow->actionCollection()->action(QStringLiteral("create_subdir"));
+    QVERIFY(createSubdir);
+    QAction *createInView = m_mainWindow->actionCollection()->action(QStringLiteral("create_dir"));
+    QVERIFY(createInView);
+
+    QVERIFY(createSubdir->shortcut().isEmpty());
+
+    // With nothing selected there is no subfolder to create in, so this acts like create_dir.
+    QTRY_VERIFY(createSubdir->isEnabled());
+    QVERIFY(createDirectory(createSubdir, QStringLiteral("no-selection")));
+    QTRY_VERIFY(QFileInfo::exists(testDir->url().toLocalFile() + QStringLiteral("/no-selection")));
+
+    view->forceUrlsSelection(fileUrl, {fileUrl});
+    view->updateViewState();
+    QTRY_COMPARE(view->selectedItems().urlList(), QList<QUrl>{fileUrl});
+    QTRY_VERIFY(!createSubdir->isEnabled());
+
+    view->forceUrlsSelection(parentUrl, {parentUrl});
+    view->updateViewState();
+    QTRY_COMPARE(view->selectedItems().urlList(), QList<QUrl>{parentUrl});
+    QTRY_VERIFY(createSubdir->isEnabled());
+
+    QVERIFY(createDirectory(createSubdir, QStringLiteral("child")));
+    QTRY_VERIFY(QFileInfo::exists(parentUrl.toLocalFile() + QStringLiteral("/child")));
+
+    QTRY_VERIFY(!view->selectedItems().isEmpty());
+
+    QVERIFY(createDirectory(createInView, QStringLiteral("sibling")));
+    QTRY_VERIFY(QFileInfo::exists(testDir->url().toLocalFile() + QStringLiteral("/sibling")));
+    QVERIFY(!QFileInfo::exists(parentUrl.toLocalFile() + QStringLiteral("/sibling")));
+    QVERIFY(!QFileInfo::exists(parentUrl.toLocalFile() + QStringLiteral("/child/sibling")));
+}
+
 void DolphinMainWindowTest::testCreateFileAction()
 {
     QScopedPointer<TestDir> testDir{new TestDir()};
diff --git a/src/views/dolphinview.cpp b/src/views/dolphinview.cpp
index ac69a408ee..b2f3c44afc 100644
--- a/src/views/dolphinview.cpp
+++ b/src/views/dolphinview.cpp
@@ -1903,8 +1903,9 @@ void DolphinView::selectFileOnceAvailable(const QUrl &url, const std::function<b
                 break;
             }
         }
-        // check whether the selection should be changed
-        if (condition()) {
+        // check whether the selection should be changed, but only once the item really is there:
+        // forceUrlsSelection() clears the current selection, so calling it early would drop it
+        if (found && condition()) {
             forceUrlsSelection(url, {url});
         }
         if (found) {
@@ -1920,11 +1921,6 @@ void DolphinView::observeCreatedDirectory(const QUrl &newDirectoryUrl)
         return;
     }
 
-    // if there was no selection but a new directory was created
-    if (m_container->controller()->selectionManager()->hasSelection()) {
-        return;
-    }
-
     // select the new directory
     if (!m_model->fileItem(newDirectoryUrl).isNull()) {
         forceUrlsSelection(newDirectoryUrl, {newDirectoryUrl});
@@ -1936,9 +1932,11 @@ void DolphinView::observeCreatedDirectory(const QUrl &newDirectoryUrl)
         return;
     }
 
-    // since this is async make sure the selection state hasn't change in the meantime
-    std::function<bool()> condition([this]() {
-        return !m_container->controller()->selectionManager()->hasSelection();
+    // since this is async make sure the selection hasn't changed in the meantime.
+    // Compare URLs, not indexes: those shift when items are inserted.
+    const QList<QUrl> selectedUrls = selectedItems().urlList();
+    std::function<bool()> condition([this, selectedUrls]() {
+        return selectedItems().urlList() == selectedUrls;
     });
 
     // in case, a new hiercachy was created, select the first folder in its parent path
diff --git a/src/views/dolphinviewactionhandler.cpp b/src/views/dolphinviewactionhandler.cpp
index 25d89ee901..6fd5d6b88f 100644
--- a/src/views/dolphinviewactionhandler.cpp
+++ b/src/views/dolphinviewactionhandler.cpp
@@ -87,7 +87,17 @@ void DolphinViewActionHandler::createActions(SelectionMode::ActionTextHelper *ac
     m_actionCollection->setDefaultShortcuts(newDirAction, KStandardShortcut::createFolder());
     newDirAction->setIcon(QIcon::fromTheme(QStringLiteral("folder-new")));
     newDirAction->setEnabled(false); // Will be enabled in slotWriteStateChanged(bool) if the current URL is writable
-    connect(newDirAction, &QAction::triggered, this, &DolphinViewActionHandler::createDirectoryTriggered);
+    connect(newDirAction, &QAction::triggered, this, [this] {
+        Q_EMIT createDirectoryTriggered(m_currentView->url());
+    });
+
+    QAction *newSubdirAction = m_actionCollection->addAction(QStringLiteral("create_subdir"));
+    newSubdirAction->setText(i18nc("@action", "Create Subfolder…"));
+    newSubdirAction->setIcon(QIcon::fromTheme(QStringLiteral("folder-new")));
+    newSubdirAction->setEnabled(false);
+    connect(newSubdirAction, &QAction::triggered, this, [this] {
+        Q_EMIT createDirectoryTriggered(subdirectoryParent());
+    });
 
     QAction *newFileAction = m_actionCollection->addAction(QStringLiteral("create_file"));
     newFileAction->setText(i18nc("@action", "Create File…"));
@@ -762,6 +772,7 @@ void DolphinViewActionHandler::slotWriteStateChanged(bool isFolderWritable)
     const bool supportsMakeDir = KProtocolManager::supportsMakeDir(currentView()->url());
     m_actionCollection->action(QStringLiteral("create_dir"))->setEnabled(isFolderWritable && supportsMakeDir);
     m_actionCollection->action(QStringLiteral("create_file"))->setEnabled(isFolderWritable);
+    updateCreateSubdirectoryAction();
 }
 
 KToggleAction *DolphinViewActionHandler::iconsModeAction()
@@ -960,6 +971,29 @@ void DolphinViewActionHandler::slotSelectionChanged(const KFileItemList &selecti
             basicActionsMenu->menu()->addAction(m_actionCollection->action(QStringLiteral("add_to_places")));
         }
     }
+
+    updateCreateSubdirectoryAction();
+}
+
+QUrl DolphinViewActionHandler::subdirectoryParent() const
+{
+    const KFileItemList selection = m_currentView->selectedItems();
+    if (selection.count() == 1 && selection.first().isDir()) {
+        return selection.first().url();
+    }
+    return m_currentView->url();
+}
+
+void DolphinViewActionHandler::updateCreateSubdirectoryAction()
+{
+    const KFileItemList selection = m_currentView->selectedItems();
+    bool enabled;
+    if (selection.isEmpty()) {
+        enabled = m_actionCollection->action(QStringLiteral("create_dir"))->isEnabled();
+    } else {
+        enabled = selection.count() == 1 && selection.first().isDir() && KFileItemListProperties(selection).supportsWriting();
+    }
+    m_actionCollection->action(QStringLiteral("create_subdir"))->setEnabled(enabled);
 }
 
 void DolphinViewActionHandler::restoreViewSettingsToDefaults()
diff --git a/src/views/dolphinviewactionhandler.h b/src/views/dolphinviewactionhandler.h
index 41fc994d10..20aa6896e6 100644
--- a/src/views/dolphinviewactionhandler.h
+++ b/src/views/dolphinviewactionhandler.h
@@ -81,11 +81,11 @@ Q_SIGNALS:
     void actionBeingHandled();
 
     /**
-     * Emitted if the user requested creating a new directory by the F10 key.
+     * Emitted if the user requested creating a new directory in @p parent.
      * The receiver of the signal (DolphinMainWindow or DolphinPart) invokes
      * the method createDirectory of their KNewFileMenu instance.
      */
-    void createDirectoryTriggered();
+    void createDirectoryTriggered(const QUrl &parent);
 
     /**
      * Emitted if the user requested creating a new file.
@@ -245,6 +245,17 @@ private:
      */
     void createActions(SelectionMode::ActionTextHelper *actionTextHelper);
 
+    /**
+     * Returns the directory the 'Create Subfolder...' action creates in: the selected
+     * folder, or the viewed directory if nothing is selected.
+     */
+    QUrl subdirectoryParent() const;
+
+    /**
+     * Updates the state of the 'Create Subfolder...' action.
+     */
+    void updateCreateSubdirectoryAction();
+
     /**
      * Creates an action-group out of all roles from KFileItemModel.
      * Dependent on the group-prefix either a radiobutton-group is
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.