[system/dolphin] src: dolphinview,dolphinviewactionhandler: split create folder into two actions
Gergő Gelóczi <[email protected]>
| Newsgroups | gmane.comp.kde.cvs |
|---|---|
| Message-ID | <[email protected]> |
Git commit 2ae4985411d56807119287714f7fc4924564c65d by Gergő Gelóczi.
Committed on 28/07/2026 at 11:04.
Pushed by meven into branch 'master'.
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
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/2ae4985411d56807119287714f7fc4924564c65d
diff --git a/src/dolphincontextmenu.cpp b/src/dolphincontextmenu.cpp
index 2a120662a4..caf2e2ba01 100644
--- a/src/dolphincontextmenu.cpp
+++ b/src/dolphincontextmenu.cpp
@@ -223,7 +223,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 51fec82475..f41be37abd 100644
--- a/src/dolphinmainwindow.cpp
+++ b/src/dolphinmainwindow.cpp
@@ -822,14 +822,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();
}
}
@@ -1511,6 +1511,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 50559444b8..bc1c5ea2cc 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;
};
@@ -441,6 +448,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 68f9e0bc15..a6231db83f 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