[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