diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8128a2c534..504e3729c3 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -209,6 +209,7 @@ target_sources(dolphinstatic PRIVATE dolphinurlnavigatorscontroller.cpp trash/dolphintrash.cpp filterbar/filterbar.cpp + kitemviews/kfileitemlisttostring.cpp panels/places/placespanel.cpp panels/panel.cpp panels/folders/foldersitemlistwidget.cpp @@ -218,6 +219,10 @@ target_sources(dolphinstatic PRIVATE search/dolphinfacetswidget.cpp search/dolphinquery.cpp search/dolphinsearchbox.cpp + selectionmode/actionwithwidget.cpp + selectionmode/backgroundcolorhelper.cpp + selectionmode/selectionmodebottombar.cpp + selectionmode/selectionmodetopbar.cpp settings/general/behaviorsettingspage.cpp settings/general/configurepreviewplugindialog.cpp settings/general/confirmationssettingspage.cpp diff --git a/src/dolphinmainwindow.cpp b/src/dolphinmainwindow.cpp index 39b8203877..aea0d31ad5 100644 --- a/src/dolphinmainwindow.cpp +++ b/src/dolphinmainwindow.cpp @@ -83,6 +83,8 @@ #include #include +#include + namespace { // Used for GeneralSettings::version() to determine whether // an updated version of Dolphin is running, so as to migrate @@ -124,7 +126,7 @@ DolphinMainWindow::DolphinMainWindow() : setComponentName(QStringLiteral("dolphin"), QGuiApplication::applicationDisplayName()); setObjectName(QStringLiteral("Dolphin#")); - setStateConfigGroup("State"); + //setStateConfigGroup("State"); // TODO: Don't leave this as a comment. connect(&DolphinNewFileMenuObserver::instance(), &DolphinNewFileMenuObserver::errorMessage, this, &DolphinMainWindow::showErrorMessage); @@ -165,6 +167,7 @@ DolphinMainWindow::DolphinMainWindow() : m_actionHandler = new DolphinViewActionHandler(actionCollection(), this); connect(m_actionHandler, &DolphinViewActionHandler::actionBeingHandled, this, &DolphinMainWindow::clearStatusBar); connect(m_actionHandler, &DolphinViewActionHandler::createDirectoryTriggered, this, &DolphinMainWindow::createDirectory); + connect(m_actionHandler, &DolphinViewActionHandler::setSelectionMode, this, &DolphinMainWindow::slotSetSelectionMode); m_remoteEncoding = new DolphinRemoteEncoding(this, m_actionHandler); connect(this, &DolphinMainWindow::urlChanged, @@ -192,6 +195,7 @@ DolphinMainWindow::DolphinMainWindow() : auto hamburgerMenu = static_cast(actionCollection()->action( KStandardAction::name(KStandardAction::HamburgerMenu))); + hamburgerMenu->icon(); hamburgerMenu->setMenuBar(menuBar()); hamburgerMenu->setShowMenuBarAction(showMenuBarAction); connect(hamburgerMenu, &KHamburgerMenu::aboutToShowMenu, @@ -713,12 +717,22 @@ void DolphinMainWindow::undo() void DolphinMainWindow::cut() { - m_activeViewContainer->view()->cutSelectedItemsToClipboard(); + if (m_activeViewContainer->view()->selectedItems().isEmpty()) { + m_activeViewContainer->setSelectionModeEnabled(true, actionCollection(), SelectionModeBottomBar::Contents::CutContents); + } else { + m_activeViewContainer->view()->cutSelectedItemsToClipboard(); + m_activeViewContainer->setSelectionModeEnabled(false); + } } void DolphinMainWindow::copy() { - m_activeViewContainer->view()->copySelectedItemsToClipboard(); + if (m_activeViewContainer->view()->selectedItems().isEmpty()) { + m_activeViewContainer->setSelectionModeEnabled(true, actionCollection(), SelectionModeBottomBar::Contents::CopyContents); + } else { + m_activeViewContainer->view()->copySelectedItemsToClipboard(); + m_activeViewContainer->setSelectionModeEnabled(false); + } } void DolphinMainWindow::paste() @@ -845,6 +859,11 @@ void DolphinMainWindow::slotGoForward(QAction* action) } } +void DolphinMainWindow::slotSetSelectionMode(bool enabled, SelectionModeBottomBar::Contents bottomBarContents) +{ + m_activeViewContainer->setSelectionModeEnabled(enabled, actionCollection(), bottomBarContents); +} + void DolphinMainWindow::selectAll() { clearStatusBar(); @@ -884,6 +903,26 @@ void DolphinMainWindow::toggleSplitStash() tabPage->setSplitViewEnabled(true, WithAnimation, QUrl("stash:/")); } +void DolphinMainWindow::copyToInactiveSplitView() +{ + if (m_activeViewContainer->view()->selectedItems().isEmpty()) { + m_activeViewContainer->setSelectionModeEnabled(true, actionCollection(), SelectionModeBottomBar::Contents::CopyToOtherViewContents); + } else { + m_tabWidget->copyToInactiveSplitView(); + m_activeViewContainer->setSelectionModeEnabled(false); + } +} + +void DolphinMainWindow::moveToInactiveSplitView() +{ + if (m_activeViewContainer->view()->selectedItems().isEmpty()) { + m_activeViewContainer->setSelectionModeEnabled(true, actionCollection(), SelectionModeBottomBar::Contents::MoveToOtherViewContents); + } else { + m_tabWidget->moveToInactiveSplitView(); + m_activeViewContainer->setSelectionModeEnabled(false); + } +} + void DolphinMainWindow::reloadView() { clearStatusBar(); @@ -906,6 +945,14 @@ void DolphinMainWindow::disableStopAction() actionCollection()->action(QStringLiteral("stop"))->setEnabled(false); } +void DolphinMainWindow::toggleSelectionMode() +{ + const bool checked = !m_activeViewContainer->isSelectionModeEnabled(); + + m_activeViewContainer->setSelectionModeEnabled(checked, actionCollection(), SelectionModeBottomBar::Contents::GeneralContents); + actionCollection()->action(QStringLiteral("toggle_selection_mode"))->setChecked(checked); +} + void DolphinMainWindow::showFilterBar() { m_activeViewContainer->setFilterBarVisible(true); @@ -1283,6 +1330,11 @@ void DolphinMainWindow::updateHamburgerMenu() menu->addAction(ac->action(QStringLiteral("go_forward"))); menu->addMenu(m_newFileMenu->menu()); + if (!toolBar()->isVisible() + || !toolbarActions.contains(ac->action(QStringLiteral("toggle_selection_mode_with_popup"))) + ) { + menu->addAction(ac->action(QStringLiteral("toggle_selection_mode"))); + } menu->addAction(ac->action(QStringLiteral("basic_actions"))); menu->addAction(ac->action(KStandardAction::name(KStandardAction::Undo))); if (!toolBar()->isVisible() @@ -1562,7 +1614,7 @@ void DolphinMainWindow::setupActions() copyToOtherViewAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy"))); copyToOtherViewAction->setIconText(i18nc("@action:inmenu Edit", "Copy to Inactive Split View")); actionCollection()->setDefaultShortcut(copyToOtherViewAction, Qt::SHIFT | Qt::Key_F5 ); - connect(copyToOtherViewAction, &QAction::triggered, m_tabWidget, &DolphinTabWidget::copyToInactiveSplitView); + connect(copyToOtherViewAction, &QAction::triggered, this, &DolphinMainWindow::copyToInactiveSplitView); QAction* moveToOtherViewAction = actionCollection()->addAction(QStringLiteral("move_to_inactive_split_view")); moveToOtherViewAction->setText(i18nc("@action:inmenu", "Move to Inactive Split View")); @@ -1571,7 +1623,7 @@ void DolphinMainWindow::setupActions() moveToOtherViewAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-cut"))); moveToOtherViewAction->setIconText(i18nc("@action:inmenu Edit", "Move to Inactive Split View")); actionCollection()->setDefaultShortcut(moveToOtherViewAction, Qt::SHIFT | Qt::Key_F6 ); - connect(moveToOtherViewAction, &QAction::triggered, m_tabWidget, &DolphinTabWidget::moveToInactiveSplitView); + connect(moveToOtherViewAction, &QAction::triggered, this, &DolphinMainWindow::moveToInactiveSplitView); QAction* showFilterBar = actionCollection()->addAction(QStringLiteral("show_filter_bar")); showFilterBar->setText(i18nc("@action:inmenu Tools", "Filter...")); @@ -1617,6 +1669,33 @@ void DolphinMainWindow::setupActions() toggleSearchAction->setWhatsThis(searchAction->whatsThis()); toggleSearchAction->setCheckable(true); + QAction *toggleSelectionModeAction = actionCollection()->addAction(QStringLiteral("toggle_selection_mode")); + // i18n: This action toggles a selection mode. + toggleSelectionModeAction->setText(i18nc("@action:inmenu", "Select Files and Folders")); + // i18n: Opens a selection mode for selecting files/folders and later selecting an action that acts on them. + // So in a way "Select" here is used to mean both "Select files" and also "Select what to do" but mostly the first. + // The text is kept so unspecific because it will be shown on the toolbar where space is at a premium. + toggleSelectionModeAction->setIconText(i18nc("@action:intoolbar", "Select")); + toggleSelectionModeAction->setWhatsThis(xi18nc("@info:whatsthis", "This application doesn't know which files or folders should be acted on, " + "unless they are selected first. Press this to toggle the Selection Mode which makes selecting and deselecting as " + "easy as pressing an item once.While in this mode, a quick access bar at the bottom shows all the available actions for the current " + "selection of items.")); + toggleSelectionModeAction->setIcon(QIcon::fromTheme(QStringLiteral("quickwizard"))); + toggleSelectionModeAction->setCheckable(true); + actionCollection()->setDefaultShortcut(toggleSelectionModeAction, Qt::Key_Space ); + connect(toggleSelectionModeAction, &QAction::triggered, this, &DolphinMainWindow::toggleSelectionMode); + + // A special version of the toggleSelectionModeAction for the toolbar that also contains a menu + // with the selectAllAction and invertSelectionAction. + auto *toggleSelectionModeToolBarAction = new KToolBarPopupAction(toggleSelectionModeAction->icon(), toggleSelectionModeAction->iconText(), actionCollection()); + toggleSelectionModeToolBarAction->setToolTip(toggleSelectionModeAction->text()); + toggleSelectionModeToolBarAction->setWhatsThis(toggleSelectionModeAction->whatsThis()); + actionCollection()->addAction(QStringLiteral("toggle_selection_mode_with_popup"), toggleSelectionModeToolBarAction); + toggleSelectionModeToolBarAction->setCheckable(true); + toggleSelectionModeToolBarAction->setPopupMode(QToolButton::DelayedPopup); + connect(toggleSelectionModeToolBarAction, &QAction::triggered, toggleSelectionModeAction, &QAction::trigger); + connect(toggleSelectionModeAction, &QAction::toggled, toggleSelectionModeToolBarAction, &QAction::setChecked); + QAction* selectAllAction = KStandardAction::selectAll(this, &DolphinMainWindow::selectAll, actionCollection()); selectAllAction->setWhatsThis(xi18nc("@info:whatsthis", "This selects all " "files and folders in the current location.")); @@ -1629,6 +1708,11 @@ void DolphinMainWindow::setupActions() actionCollection()->setDefaultShortcut(invertSelection, Qt::CTRL | Qt::SHIFT | Qt::Key_A); connect(invertSelection, &QAction::triggered, this, &DolphinMainWindow::invertSelection); + QMenu *toggleSelectionModeActionMenu = new QMenu(this); + toggleSelectionModeActionMenu->addAction(selectAllAction); + toggleSelectionModeActionMenu->addAction(invertSelection); + toggleSelectionModeToolBarAction->setMenu(toggleSelectionModeActionMenu); + // setup 'View' menu // (note that most of it is set up in DolphinViewActionHandler) @@ -2143,6 +2227,11 @@ void DolphinMainWindow::updateFileAndEditActions() const KActionCollection* col = actionCollection(); KFileItemListProperties capabilitiesSource(list); + QAction* renameAction = col->action(KStandardAction::name(KStandardAction::RenameFile)); + QAction* moveToTrashAction = col->action(KStandardAction::name(KStandardAction::MoveToTrash)); + QAction* deleteAction = col->action(KStandardAction::name(KStandardAction::DeleteFile)); + QAction* cutAction = col->action(KStandardAction::name(KStandardAction::Cut)); + QAction* duplicateAction = col->action(QStringLiteral("duplicate")); // see DolphinViewActionHandler 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")); @@ -2151,20 +2240,19 @@ void DolphinMainWindow::updateFileAndEditActions() if (list.isEmpty()) { stateChanged(QStringLiteral("has_no_selection")); + // All actions that need a selection to function can be enabled because they should trigger selection mode. + renameAction->setEnabled(true); + moveToTrashAction->setEnabled(true); + deleteAction->setEnabled(true); + cutAction->setEnabled(true); + duplicateAction->setEnabled(true); addToPlacesAction->setEnabled(true); - copyToOtherViewAction->setEnabled(false); - moveToOtherViewAction->setEnabled(false); - copyLocation->setEnabled(false); + copyLocation->setEnabled(true); } else { stateChanged(QStringLiteral("has_selection")); - QAction* renameAction = col->action(KStandardAction::name(KStandardAction::RenameFile)); - QAction* moveToTrashAction = col->action(KStandardAction::name(KStandardAction::MoveToTrash)); - QAction* deleteAction = col->action(KStandardAction::name(KStandardAction::DeleteFile)); - QAction* cutAction = col->action(KStandardAction::name(KStandardAction::Cut)); QAction* deleteWithTrashShortcut = col->action(QStringLiteral("delete_shortcut")); // see DolphinViewActionHandler QAction* showTarget = col->action(QStringLiteral("show_target")); - QAction* duplicateAction = col->action(QStringLiteral("duplicate")); // see DolphinViewActionHandler if (list.length() == 1 && list.first().isDir()) { addToPlacesAction->setEnabled(true); @@ -2172,23 +2260,6 @@ void DolphinMainWindow::updateFileAndEditActions() addToPlacesAction->setEnabled(false); } - if (m_tabWidget->currentTabPage()->splitViewEnabled()) { - DolphinTabPage* tabPage = m_tabWidget->currentTabPage(); - KFileItem capabilitiesDestination; - - if (tabPage->primaryViewActive()) { - capabilitiesDestination = tabPage->secondaryViewContainer()->url(); - } else { - capabilitiesDestination = tabPage->primaryViewContainer()->url(); - } - - copyToOtherViewAction->setEnabled(capabilitiesDestination.isWritable()); - moveToOtherViewAction->setEnabled(capabilitiesSource.supportsMoving() && capabilitiesDestination.isWritable()); - } else { - copyToOtherViewAction->setEnabled(false); - moveToOtherViewAction->setEnabled(false); - } - const bool enableMoveToTrash = capabilitiesSource.isLocal() && capabilitiesSource.supportsMoving(); renameAction->setEnabled(capabilitiesSource.supportsMoving()); @@ -2200,12 +2271,36 @@ void DolphinMainWindow::updateFileAndEditActions() showTarget->setEnabled(list.length() == 1 && list.at(0).isLink()); duplicateAction->setEnabled(capabilitiesSource.supportsWriting()); } + + if (m_tabWidget->currentTabPage()->splitViewEnabled()) { + DolphinTabPage* tabPage = m_tabWidget->currentTabPage(); + KFileItem capabilitiesDestination; + + if (tabPage->primaryViewActive()) { + capabilitiesDestination = tabPage->secondaryViewContainer()->url(); + } else { + capabilitiesDestination = tabPage->primaryViewContainer()->url(); + } + + copyToOtherViewAction->setEnabled(capabilitiesDestination.isWritable()); + moveToOtherViewAction->setEnabled((list.isEmpty() || capabilitiesSource.supportsMoving()) && capabilitiesDestination.isWritable()); + } else { + copyToOtherViewAction->setEnabled(false); + moveToOtherViewAction->setEnabled(false); + } } void DolphinMainWindow::updateViewActions() { m_actionHandler->updateViewActions(); + QAction *toggleSelectionModeAction = actionCollection()->action(QStringLiteral("toggle_selection_mode")); + disconnect(nullptr, &DolphinViewContainer::selectionModeChanged, + toggleSelectionModeAction, &QAction::setChecked); + toggleSelectionModeAction->setChecked(m_activeViewContainer->isSelectionModeEnabled()); + connect(m_activeViewContainer, &DolphinViewContainer::selectionModeChanged, + toggleSelectionModeAction, &QAction::setChecked); + QAction* toggleFilterBarAction = actionCollection()->action(QStringLiteral("toggle_filter")); toggleFilterBarAction->setChecked(m_activeViewContainer->isFilterBarVisible()); diff --git a/src/dolphinmainwindow.h b/src/dolphinmainwindow.h index b86b6b4f32..0c9c762d63 100644 --- a/src/dolphinmainwindow.h +++ b/src/dolphinmainwindow.h @@ -10,6 +10,7 @@ #define DOLPHIN_MAINWINDOW_H #include "dolphintabwidget.h" +#include "selectionmode/selectionmodebottombar.h" #include "config-dolphin.h" #include #include @@ -313,6 +314,9 @@ private Q_SLOTS: */ void updatePasteAction(); + /** Calls DolphinViewContainer::setSelectionMode() for m_activeViewContainer. */ + void slotSetSelectionMode(bool enabled, SelectionModeBottomBar::Contents bottomBarContents); + /** Selects all items from the active view. */ void selectAll(); @@ -333,6 +337,12 @@ private Q_SLOTS: /** Dedicated action to open the stash:/ ioslave in split view. */ void toggleSplitStash(); + /** Copies all selected items to the inactive view. */ + void copyToInactiveSplitView(); + + /** Moves all selected items to the inactive view. */ + void moveToInactiveSplitView(); + /** Reloads the currently active view. */ void reloadView(); @@ -342,6 +352,8 @@ private Q_SLOTS: void enableStopAction(); void disableStopAction(); + void toggleSelectionMode(); + void showFilterBar(); void toggleFilterBar(); diff --git a/src/dolphintabpage.cpp b/src/dolphintabpage.cpp index 771bbe9cd6..9fde4071a3 100644 --- a/src/dolphintabpage.cpp +++ b/src/dolphintabpage.cpp @@ -9,7 +9,6 @@ #include "dolphin_generalsettings.h" #include "dolphinviewcontainer.h" -#include "global.h" #include #include @@ -152,6 +151,8 @@ void DolphinTabPage::setSplitViewEnabled(bool enabled, Animated animated, const view->setDisabled(true); startExpandViewAnimation(m_primaryViewContainer); } + + m_primaryViewContainer->slotSplitTabDisabled(); } } } diff --git a/src/dolphintabpage.h b/src/dolphintabpage.h index a8c1ba3110..abe65843a3 100644 --- a/src/dolphintabpage.h +++ b/src/dolphintabpage.h @@ -8,6 +8,8 @@ #ifndef DOLPHIN_TAB_PAGE_H #define DOLPHIN_TAB_PAGE_H +#include "global.h" + #include #include #include @@ -19,11 +21,6 @@ class QVariantAnimation; class KFileItemList; class DolphinTabPageSplitter; -enum Animated { - WithAnimation, - WithoutAnimation -}; - class DolphinTabPage : public QWidget { Q_OBJECT diff --git a/src/dolphinui.rc b/src/dolphinui.rc index b6afe2abb1..a23912608a 100644 --- a/src/dolphinui.rc +++ b/src/dolphinui.rc @@ -1,6 +1,6 @@ - + @@ -31,6 +31,7 @@ + @@ -80,7 +81,6 @@ - @@ -92,23 +92,11 @@ - - - - - - - - - - - - @@ -125,6 +113,7 @@ + diff --git a/src/dolphinviewcontainer.cpp b/src/dolphinviewcontainer.cpp index 5d177c93a1..dd72a6d667 100644 --- a/src/dolphinviewcontainer.cpp +++ b/src/dolphinviewcontainer.cpp @@ -12,11 +12,13 @@ #include "filterbar/filterbar.h" #include "global.h" #include "search/dolphinsearchbox.h" +#include "selectionmode/selectionmodetopbar.h" #include "statusbar/dolphinstatusbar.h" #include "views/viewmodecontroller.h" #include "views/viewproperties.h" #include "dolphin_detailsmodesettings.h" +#include #if HAVE_KACTIVITIES #include #endif @@ -32,14 +34,28 @@ #include #include +#include #include #include #include #include #include -#include #include +#include + +// An overview of the widgets contained by this ViewContainer +struct LayoutStructure { + int searchBox = 0; + int messageWidget = 1; + int selectionModeTopBar = 2; + int view = 3; + int selectionModeBottomBar = 4; + int filterBar = 5; + int statusBar = 6; +}; +constexpr LayoutStructure positionFor; + DolphinViewContainer::DolphinViewContainer(const QUrl& url, QWidget* parent) : QWidget(parent), m_topLayout(nullptr), @@ -48,8 +64,10 @@ DolphinViewContainer::DolphinViewContainer(const QUrl& url, QWidget* parent) : m_searchBox(nullptr), m_searchModeEnabled(false), m_messageWidget(nullptr), + m_selectionModeTopBar{nullptr}, m_view(nullptr), m_filterBar(nullptr), + m_selectionModeBottomBar{nullptr}, m_statusBar(nullptr), m_statusBarTimer(nullptr), m_statusBarTimestamp(), @@ -60,7 +78,7 @@ DolphinViewContainer::DolphinViewContainer(const QUrl& url, QWidget* parent) : { hide(); - m_topLayout = new QVBoxLayout(this); + m_topLayout = new QGridLayout(this); m_topLayout->setSpacing(0); m_topLayout->setContentsMargins(0, 0, 0, 0); @@ -187,16 +205,16 @@ DolphinViewContainer::DolphinViewContainer(const QUrl& url, QWidget* parent) : connect(undoManager, &KIO::FileUndoManager::jobRecordingFinished, this, &DolphinViewContainer::delayedStatusBarUpdate); - m_topLayout->addWidget(m_searchBox); - m_topLayout->addWidget(m_messageWidget); - m_topLayout->addWidget(m_view); - m_topLayout->addWidget(m_filterBar); - m_topLayout->addWidget(m_statusBar); + m_topLayout->addWidget(m_searchBox, positionFor.searchBox, 0); + m_topLayout->addWidget(m_messageWidget, positionFor.messageWidget, 0); + m_topLayout->addWidget(m_view, positionFor.view, 0); + m_topLayout->addWidget(m_filterBar, positionFor.filterBar, 0); + m_topLayout->addWidget(m_statusBar, positionFor.statusBar, 0); setSearchModeEnabled(isSearchUrl(url)); connect(DetailsModeSettings::self(), &KCoreConfigSkeleton::configChanged, this, [=]() { - if (view()->mode() == DolphinView::Mode::DetailsView) { + if (view()->viewMode() == DolphinView::Mode::DetailsView) { view()->reload(); } }); @@ -357,6 +375,78 @@ void DolphinViewContainer::disconnectUrlNavigator() m_urlNavigatorConnected = nullptr; } +void DolphinViewContainer::setSelectionModeEnabled(bool enabled, KActionCollection *actionCollection, SelectionModeBottomBar::Contents bottomBarContents) +{ + std::cout << "DolphinViewContainer::setSelectionModeEnabled(" << enabled << ", " << bottomBarContents << ")\n"; + const bool wasEnabled = m_view->selectionMode(); + m_view->setSelectionMode(enabled); + + if (!enabled) { + Q_CHECK_PTR(m_selectionModeTopBar); // there is no point in disabling selectionMode when it wasn't even enabled once. + Q_CHECK_PTR(m_selectionModeBottomBar); + m_selectionModeTopBar->setVisible(false, WithAnimation); + m_selectionModeBottomBar->setVisible(false, WithAnimation); + if (wasEnabled) { + Q_EMIT selectionModeChanged(false); + } + return; + } + + if (!m_selectionModeTopBar) { + // Changing the location will disable selection mode. + connect(m_urlNavigator.get(), &DolphinUrlNavigator::urlChanged, this, [this]() { + setSelectionModeEnabled(false); + }); + + m_selectionModeTopBar = new SelectionModeTopBar(this); // will be created hidden + connect(m_selectionModeTopBar, &SelectionModeTopBar::leaveSelectionModeRequested, this, [this]() { + setSelectionModeEnabled(false); + }); + m_topLayout->addWidget(m_selectionModeTopBar, positionFor.selectionModeTopBar, 0); + } + + if (!m_selectionModeBottomBar) { + m_selectionModeBottomBar = new SelectionModeBottomBar(actionCollection, this); + connect(m_view, &DolphinView::selectionChanged, this, [this](const KFileItemList &selection) { + m_selectionModeBottomBar->slotSelectionChanged(selection, m_view->url()); + }); + + connect(m_selectionModeBottomBar, &SelectionModeBottomBar::error, this, [this](const QString &errorMessage) { + showErrorMessage(errorMessage); + }); + connect(m_selectionModeBottomBar, &SelectionModeBottomBar::leaveSelectionModeRequested, this, [this]() { + setSelectionModeEnabled(false); + }); + m_topLayout->addWidget(m_selectionModeBottomBar, positionFor.selectionModeBottomBar, 0); + } + m_selectionModeBottomBar->resetContents(bottomBarContents); + if (bottomBarContents == SelectionModeBottomBar::GeneralContents) { + m_selectionModeBottomBar->slotSelectionChanged(m_view->selectedItems(), m_view->url()); + } + + if (!wasEnabled) { + m_selectionModeTopBar->setVisible(true, WithAnimation); + m_selectionModeBottomBar->setVisible(true, WithAnimation); + Q_EMIT selectionModeChanged(true); + } +} + +bool DolphinViewContainer::isSelectionModeEnabled() const +{ + const bool isEnabled = m_view->selectionMode(); + Q_ASSERT( !isEnabled // We cannot assert the invisibility of the bars because of the hide animation. + || ( isEnabled && m_selectionModeTopBar && m_selectionModeTopBar->isVisible() && m_selectionModeBottomBar && m_selectionModeBottomBar->isVisible())); + return isEnabled; +} + +void DolphinViewContainer::slotSplitTabDisabled() +{ + if (m_selectionModeBottomBar) { + m_selectionModeBottomBar->slotSplitTabDisabled(); + } +} + + void DolphinViewContainer::showMessage(const QString& msg, MessageType type) { if (msg.isEmpty()) { diff --git a/src/dolphinviewcontainer.h b/src/dolphinviewcontainer.h index 057e471495..a0936efd3d 100644 --- a/src/dolphinviewcontainer.h +++ b/src/dolphinviewcontainer.h @@ -9,6 +9,7 @@ #include "config-dolphin.h" #include "dolphinurlnavigator.h" +#include "selectionmode/selectionmodebottombar.h" #include "views/dolphinview.h" #include @@ -27,9 +28,12 @@ namespace KActivities { class FilterBar; class KMessageWidget; +class QAction; +class QGridLayout; class QUrl; class DolphinSearchBox; class DolphinStatusBar; +class SelectionModeTopBar; /** * @short Represents a view for the directory content @@ -131,6 +135,9 @@ public: */ void disconnectUrlNavigator(); + void setSelectionModeEnabled(bool enabled, KActionCollection *actionCollection = nullptr, SelectionModeBottomBar::Contents bottomBarContents = SelectionModeBottomBar::Contents::GeneralContents); + bool isSelectionModeEnabled() const; + /** * Shows the message \msg with the given type non-modal above * the view-content. @@ -206,6 +213,9 @@ public Q_SLOTS: */ void setSearchModeEnabled(bool enabled); + /** Used to notify the m_selectionModeBottomBar that there is no other ViewContainer in the tab. */ + void slotSplitTabDisabled(); + Q_SIGNALS: /** * Is emitted whenever the filter bar has changed its visibility state. @@ -216,6 +226,8 @@ Q_SIGNALS: */ void searchModeEnabledChanged(bool enabled); + void selectionModeChanged(bool enabled); + /** * Is emitted when the write state of the folder has been changed. The application * should disable all actions like "Create New..." that depend on the write @@ -395,7 +407,7 @@ private: void tryRestoreViewState(); private: - QVBoxLayout* m_topLayout; + QGridLayout *m_topLayout; /** * The internal UrlNavigator which is never visible to the user. @@ -410,14 +422,22 @@ private: * Otherwise it's one of the UrlNavigators visible in the toolbar. */ QPointer m_urlNavigatorConnected; + DolphinSearchBox* m_searchBox; bool m_searchModeEnabled; + KMessageWidget* m_messageWidget; + /// A bar shown at the top of the view to signify that selection mode is currently active. + SelectionModeTopBar *m_selectionModeTopBar; + DolphinView* m_view; FilterBar* m_filterBar; + /// A bar shown at the bottom of the view whose contents depend on what the user is currently doing. + SelectionModeBottomBar *m_selectionModeBottomBar; + DolphinStatusBar* m_statusBar; QTimer* m_statusBarTimer; // Triggers a delayed update QElapsedTimer m_statusBarTimestamp; // Time in ms since last update diff --git a/src/global.h b/src/global.h index 4f4eb890cd..ee9a7ec271 100644 --- a/src/global.h +++ b/src/global.h @@ -55,6 +55,11 @@ namespace Dolphin { const int LAYOUT_SPACING_SMALL = 2; } +enum Animated { + WithAnimation, + WithoutAnimation +}; + class GlobalConfig : public QObject { Q_OBJECT diff --git a/src/kitemviews/kitemlistcontroller.cpp b/src/kitemviews/kitemlistcontroller.cpp index 3d83bc9143..03ee5cfe62 100644 --- a/src/kitemviews/kitemlistcontroller.cpp +++ b/src/kitemviews/kitemlistcontroller.cpp @@ -27,12 +27,14 @@ #include #include #include +#include #include #include KItemListController::KItemListController(KItemModelBase* model, KItemListView* view, QObject* parent) : QObject(parent), m_singleClickActivationEnforced(false), + m_selectionMode(false), m_selectionTogglePressed(false), m_clearSelectionIfItemsAreNotDragged(false), m_isSwipeGesture(false), @@ -51,6 +53,7 @@ KItemListController::KItemListController(KItemModelBase* model, KItemListView* v m_pressedIndex(std::nullopt), m_pressedMousePos(), m_autoActivationTimer(nullptr), + m_longPressDetectionTimer(nullptr), m_swipeGesture(Qt::CustomGesture), m_twoFingerTapGesture(Qt::CustomGesture), m_oldSelection(), @@ -69,6 +72,15 @@ KItemListController::KItemListController(KItemModelBase* model, KItemListView* v m_autoActivationTimer->setInterval(-1); connect(m_autoActivationTimer, &QTimer::timeout, this, &KItemListController::slotAutoActivationTimeout); + m_longPressDetectionTimer = new QTimer(this); + m_longPressDetectionTimer->setSingleShot(true); + m_longPressDetectionTimer->setInterval(QGuiApplication::styleHints()->mousePressAndHoldInterval()); + connect(m_longPressDetectionTimer, &QTimer::timeout, this, [this]() { + if (!m_selectionMode) { + Q_EMIT selectionModeRequested(); + } + }); + setModel(model); setView(view); @@ -220,6 +232,16 @@ bool KItemListController::singleClickActivationEnforced() const return m_singleClickActivationEnforced; } +void KItemListController::setSelectionMode(bool enabled) +{ + m_selectionMode = enabled; +} + +bool KItemListController::selectionMode() const +{ + return m_selectionMode; +} + bool KItemListController::keyPressEvent(QKeyEvent* event) { int index = m_selectionManager->currentItem(); @@ -576,10 +598,14 @@ bool KItemListController::mouseMoveEvent(QGraphicsSceneMouseEvent* event, const return false; } + const QPointF pos = transform.map(event->pos()); + if ((pos - m_pressedMousePos).manhattanLength() >= QApplication::startDragDistance()) { + m_longPressDetectionTimer->stop(); + } + if (m_pressedIndex.has_value() && !m_view->rubberBand()->isActive()) { // Check whether a dragging should be started if (event->buttons() & Qt::LeftButton) { - const QPointF pos = transform.map(event->pos()); if ((pos - m_pressedMousePos).manhattanLength() >= QApplication::startDragDistance()) { if (!m_selectionManager->isSelected(m_pressedIndex.value())) { // Always assure that the dragged item gets selected. Usually this is already @@ -639,6 +665,8 @@ bool KItemListController::mouseReleaseEvent(QGraphicsSceneMouseEvent* event, con m_view->m_tapAndHoldIndicator->setActive(false); } + m_longPressDetectionTimer->stop(); + KItemListRubberBand* rubberBand = m_view->rubberBand(); if (event->source() == Qt::MouseEventSynthesizedByQt && !rubberBand->isActive() && m_isTouchEvent) { return false; @@ -1247,7 +1275,7 @@ void KItemListController::slotRubberBandChanged() // been activated in case if no Shift- or Control-key are pressed const bool shiftOrControlPressed = QApplication::keyboardModifiers() & Qt::ShiftModifier || QApplication::keyboardModifiers() & Qt::ControlModifier; - if (!shiftOrControlPressed) { + if (!shiftOrControlPressed && !m_selectionMode) { m_oldSelection.clear(); } } @@ -1296,7 +1324,7 @@ void KItemListController::slotRubberBandChanged() } } while (!selectionFinished); - if (QApplication::keyboardModifiers() & Qt::ControlModifier) { + if ((QApplication::keyboardModifiers() & Qt::ControlModifier) || m_selectionMode) { // If Control is pressed, the selection state of all items in the rubberband is toggled. // Therefore, the new selection contains: // 1. All previously selected items which are not inside the rubberband, and @@ -1518,10 +1546,14 @@ bool KItemListController::onPress(const QPoint& screenPos, const QPointF& pos, c } const bool shiftPressed = modifiers & Qt::ShiftModifier; - const bool controlPressed = modifiers & Qt::ControlModifier; + const bool controlPressed = (modifiers & Qt::ControlModifier) || m_selectionMode; const bool leftClick = buttons & Qt::LeftButton; const bool rightClick = buttons & Qt::RightButton; + if (leftClick) { + m_longPressDetectionTimer->start(); + } + // The previous selection is cleared if either // 1. The selection mode is SingleSelection, or // 2. the selection mode is MultiSelection, and *none* of the following conditions are met: @@ -1565,7 +1597,7 @@ bool KItemListController::onPress(const QPoint& screenPos, const QPointF& pos, c return false; } } - } else if (pressedItemAlreadySelected && !shiftOrControlPressed && (buttons & Qt::LeftButton)) { + } else if (pressedItemAlreadySelected && !shiftOrControlPressed && leftClick) { // The user might want to start dragging multiple items, but if he clicks the item // in order to trigger it instead, the other selected items must be deselected. // However, we do not know yet what the user is going to do. diff --git a/src/kitemviews/kitemlistcontroller.h b/src/kitemviews/kitemlistcontroller.h index d2e56eb64b..a3d952de55 100644 --- a/src/kitemviews/kitemlistcontroller.h +++ b/src/kitemviews/kitemlistcontroller.h @@ -126,6 +126,9 @@ public: void setSingleClickActivationEnforced(bool singleClick); bool singleClickActivationEnforced() const; + void setSelectionMode(bool enabled); + bool selectionMode() const; + bool processEvent(QEvent* event, const QTransform& transform); Q_SIGNALS: @@ -209,6 +212,14 @@ Q_SIGNALS: */ void escapePressed(); + /** + * Is emitted if left click is pressed down for a long time without moving the cursor too much. + * Moving the cursor would either trigger an item drag if the click was initiated on top of an item + * or a selection rectangle if the click was not initiated on top of an item. + * So long press is only emitted if there wasn't a lot of cursor movement. + */ + void selectionModeRequested(); + void modelChanged(KItemModelBase* current, KItemModelBase* previous); void viewChanged(KItemListView* current, KItemListView* previous); @@ -325,6 +336,7 @@ private: private: bool m_singleClickActivationEnforced; + bool m_selectionMode; bool m_selectionTogglePressed; bool m_clearSelectionIfItemsAreNotDragged; bool m_isSwipeGesture; @@ -344,6 +356,7 @@ private: QPointF m_pressedMousePos; QTimer* m_autoActivationTimer; + QTimer* m_longPressDetectionTimer; Qt::GestureType m_swipeGesture; Qt::GestureType m_twoFingerTapGesture; diff --git a/src/selectionmode/actionwithwidget.cpp b/src/selectionmode/actionwithwidget.cpp new file mode 100644 index 0000000000..8e82a37bf1 --- /dev/null +++ b/src/selectionmode/actionwithwidget.cpp @@ -0,0 +1,76 @@ +/* + This file is part of the KDE project + SPDX-FileCopyrightText: 2022 Felix Ernst + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#include "actionwithwidget.h" + +#include +#include +#include +#include + +ActionWithWidget::ActionWithWidget(QAction *action) : + m_action{action} +{ } + +ActionWithWidget::ActionWithWidget(QAction *action, QAbstractButton *button) : + m_action{action}, + m_widget{button} +{ + copyActionDataToButton(button, action); +} + +QWidget *ActionWithWidget::newWidget(QWidget *parent) +{ + Q_CHECK_PTR(m_action); + Q_ASSERT(!m_widget); + + if (m_action->isSeparator()) { + auto line = new QFrame(parent); + line->setFrameShape(QFrame::VLine); + line->setFrameShadow(QFrame::Sunken); + + m_widget = line; + } else { + m_widget = newButtonForAction(m_action, parent); + } + return m_widget; +} + +QAbstractButton *newButtonForAction(QAction *action, QWidget *parent) +{ + Q_CHECK_PTR(action); + Q_ASSERT(!action->isSeparator()); + + if (action->priority() == QAction::LowPriority) { + // We don't want the low priority actions to be displayed icon-only so we need trickery. + auto button = new QPushButton(parent); + copyActionDataToButton(static_cast(button), action); + button->setMinimumWidth(0); + return button; + } + + auto *toolButton = new QToolButton(parent); + toolButton->setToolButtonStyle(Qt::ToolButtonStyle::ToolButtonTextBesideIcon); + toolButton->setDefaultAction(action); + toolButton->setPopupMode(QToolButton::ToolButtonPopupMode::InstantPopup); + toolButton->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); + toolButton->setMinimumWidth(0); + return toolButton; +} + +void copyActionDataToButton(QAbstractButton *button, QAction *action) +{ + button->setText(action->text()); + button->setIcon(action->icon()); + button->setToolTip(action->toolTip()); + button->setWhatsThis(action->whatsThis()); + + button->setVisible(action->isVisible()); + button->setEnabled(action->isEnabled()); + + QObject::connect(button, &QAbstractButton::clicked, action, &QAction::trigger); +} diff --git a/src/selectionmode/actionwithwidget.h b/src/selectionmode/actionwithwidget.h new file mode 100644 index 0000000000..722fdf2843 --- /dev/null +++ b/src/selectionmode/actionwithwidget.h @@ -0,0 +1,86 @@ +/* + This file is part of the KDE project + SPDX-FileCopyrightText: 2022 Felix Ernst + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#ifndef ACTIONWITHWIDGET_H +#define ACTIONWITHWIDGET_H + +#include +#include +#include + +class QAbstractButton; + +/** + * @brief Small wrapper/helper class that contains an action and its widget. + * + * This class takes neither the responsibility for deleting its action() nor its widget(). + */ +class ActionWithWidget +{ +public: + ActionWithWidget(QAction *action); + + /** + * Connect @p action and @p button using copyActionDataToButton() and the + * wraps the two together in the ActionWithWidget object. + * ActionWithWidget doesn't take any ownership. + * + * @see copyActionDataToButton() + * + * @param button the button to be styled and used to fit the @p action. + */ + ActionWithWidget(QAction *action, QAbstractButton *button); + + /** @returns the action of this object. Crashes if that action has been deleted elsewhere in the meantime. */ + inline QAction *action() { + Q_CHECK_PTR(m_action); + return m_action; + }; + + /** @returns the widget of this object. */ + inline QWidget *widget() { + return m_widget; + } + + /** + * @returns a widget with parent @p parent for the action() of this object. + * + * For most actions some sort of button will be returned. For separators a vertical line will be returned. + * If this ActionWithWidget already has a widget(), this method will crash. + */ + QWidget *newWidget(QWidget *parent); + + /** returns true if the widget exists and is visible. false otherwise. */ + inline bool isWidgetVisible() const { + return m_widget && m_widget->isVisible(); + }; + +private: + QPointer m_action; + QPointer m_widget; +}; + +/** + * A small helper method. + * @return a button with the correct styling for the general mode of the SelectionModeBottomBar which can be added to its layout. + */ +QAbstractButton *newButtonForAction(QAction *action, QWidget *parent); + +/** + * Normally, if one wants a button that represents a QAction one would use a QToolButton + * and simply call QToolButton::setDefaultAction(action). However if one does this, all + * control over the style, text, etc. of the button is forfeited. One can't for example + * have text on the button then, if the action has a low QAction::priority(). + * + * This method styles the @p button based on the @p action without using QToolButton::setDefaultAction(). + * + * Another reason why this is necessary is because the actions have application-wide scope while + * these buttons belong to one ViewContainer. + */ +void copyActionDataToButton(QAbstractButton *button, QAction *action); + +#endif // ACTIONWITHWIDGET_H diff --git a/src/selectionmode/backgroundcolorhelper.cpp b/src/selectionmode/backgroundcolorhelper.cpp new file mode 100644 index 0000000000..8a7d69758b --- /dev/null +++ b/src/selectionmode/backgroundcolorhelper.cpp @@ -0,0 +1,90 @@ +/* + This file is part of the KDE project + SPDX-FileCopyrightText: 2022 Felix Ernst + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#include "backgroundcolorhelper.h" + +#include + +#include +#include +#include +#include + +BackgroundColorHelper *BackgroundColorHelper::instance() +{ + if (!s_instance) { + s_instance = new BackgroundColorHelper; + } + return s_instance; +} + + +void setBackgroundColorForWidget(QWidget *widget, QColor color) +{ + QPalette palette; + palette.setBrush(QPalette::Active, QPalette::Window, color); + palette.setBrush(QPalette::Inactive, QPalette::Window, color); + palette.setBrush(QPalette::Disabled, QPalette::Window, color); + widget->setAutoFillBackground(true); + widget->setPalette(palette); +} + +void BackgroundColorHelper::controlBackgroundColor(QWidget *widget) +{ + setBackgroundColorForWidget(widget, m_backgroundColor); + + Q_ASSERT_X(std::find(m_colorControlledWidgets.begin(), m_colorControlledWidgets.end(), widget) == m_colorControlledWidgets.end(), "controlBackgroundColor", + "Duplicate insertion is not necessary because the background color should already automatically update itself on paletteChanged"); + m_colorControlledWidgets.emplace_back(widget); +} + +BackgroundColorHelper::BackgroundColorHelper() +{ + updateBackgroundColor(); + QObject::connect(qApp, &QGuiApplication::paletteChanged, [=](){ slotPaletteChanged(); }); +} + +void BackgroundColorHelper::slotPaletteChanged() +{ + updateBackgroundColor(); + for (auto i = m_colorControlledWidgets.begin(); i != m_colorControlledWidgets.end(); ++i) { + if (!*i) { + i = m_colorControlledWidgets.erase(i); + continue; + } + setBackgroundColorForWidget(*i, m_backgroundColor); + } +} + +void BackgroundColorHelper::updateBackgroundColor() +{ + // We use colors from the color scheme for mixing so it fits the theme. + const auto colorScheme = KColorScheme(QPalette::Normal, KColorScheme::Window); + const auto activeBackgroundColor = colorScheme.background(KColorScheme::BackgroundRole::ActiveBackground).color(); + // We use the positive color for mixing so the end product doesn't look like a warning or error. + const auto positiveBackgroundColor = colorScheme.background(KColorScheme::BackgroundRole::PositiveBackground).color(); + + // Make sure the new background color has a meaningfully different hue than the activeBackgroundColor. + const int hueDifference = positiveBackgroundColor.hue() - activeBackgroundColor.hue(); + int newHue; + if (std::abs(hueDifference) > 80) { + newHue = (activeBackgroundColor.hue() + positiveBackgroundColor.hue()) / 2; + } else { + newHue = hueDifference > 0 ? + activeBackgroundColor.hue() + 40 : + activeBackgroundColor.hue() - 40; + newHue %= 360; // hue needs to be between 0 and 359 per Qt documentation. + } + + m_backgroundColor = QColor::fromHsv(newHue, + // Saturation should be closer to the active color because otherwise the selection mode color might overpower it. + .7 * activeBackgroundColor.saturation() + .3 * positiveBackgroundColor.saturation(), + (activeBackgroundColor.value() + positiveBackgroundColor.value()) / 2, + (activeBackgroundColor.alpha() + positiveBackgroundColor.alpha()) / 2); +} + +BackgroundColorHelper *BackgroundColorHelper::s_instance = nullptr; diff --git a/src/selectionmode/backgroundcolorhelper.h b/src/selectionmode/backgroundcolorhelper.h new file mode 100644 index 0000000000..0e8a61b348 --- /dev/null +++ b/src/selectionmode/backgroundcolorhelper.h @@ -0,0 +1,45 @@ +/* + This file is part of the KDE project + SPDX-FileCopyrightText: 2022 Felix Ernst + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#ifndef BACKGROUNDCOLORHELPER_H +#define BACKGROUNDCOLORHELPER_H + +#include +#include + +#include + +class QWidget; + +/** + * @brief A Singleton class for managing the colors of selection mode widgets. + */ +class BackgroundColorHelper +{ +public: + static BackgroundColorHelper *instance(); + + /** + * Changes the background color of @p widget to a distinct color scheme matching color which makes it clear that the widget belongs to the selection mode. + */ + void controlBackgroundColor(QWidget *widget); + +private: + BackgroundColorHelper(); + + void slotPaletteChanged(); + + void updateBackgroundColor(); + +private: + std::vector> m_colorControlledWidgets; + QColor m_backgroundColor; + + static BackgroundColorHelper *s_instance; +}; + +#endif // BACKGROUNDCOLORHELPER_H diff --git a/src/selectionmode/selectionmodebottombar.cpp b/src/selectionmode/selectionmodebottombar.cpp new file mode 100644 index 0000000000..5bf19295a4 --- /dev/null +++ b/src/selectionmode/selectionmodebottombar.cpp @@ -0,0 +1,705 @@ +/* + This file is part of the KDE project + SPDX-FileCopyrightText: 2022 Felix Ernst + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#include "selectionmodebottombar.h" + +#include "backgroundcolorhelper.h" +#include "dolphin_generalsettings.h" +#include "dolphincontextmenu.h" +#include "dolphinmainwindow.h" +#include "dolphinremoveaction.h" +#include "global.h" +#include "kitemviews/kfileitemlisttostring.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +SelectionModeBottomBar::SelectionModeBottomBar(KActionCollection *actionCollection, QWidget *parent) : + QWidget{parent}, + m_actionCollection{actionCollection} +{ + // Showing of this widget is normally animated. We hide it for now and make it small. + hide(); + setMaximumHeight(0); + + setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); + setMinimumWidth(0); + + auto fillParentLayout = new QGridLayout(this); + fillParentLayout->setContentsMargins(0, 0, 0, 0); + + // Put the contents into a QScrollArea. This prevents increasing the view width + // in case that not enough width for the contents is available. (this trick is also used in dolphinsearchbox.cpp.) + auto scrollArea = new QScrollArea(this); + fillParentLayout->addWidget(scrollArea); + scrollArea->setFrameShape(QFrame::NoFrame); + scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + scrollArea->setWidgetResizable(true); + + auto contentsContainer = new QWidget(scrollArea); + scrollArea->setWidget(contentsContainer); + contentsContainer->installEventFilter(this); // Adjusts the height of this bar to the height of the contentsContainer + + BackgroundColorHelper::instance()->controlBackgroundColor(this); + + // We will mostly interact with m_layout when changing the contents and not care about the other internal hierarchy. + m_layout = new QHBoxLayout(contentsContainer); +} + +void SelectionModeBottomBar::setVisible(bool visible, Animated animated) +{ + Q_ASSERT_X(animated == WithAnimation, "SelectionModeBottomBar::setVisible", "This wasn't implemented."); + + if (!visible && m_contents == PasteContents) { + return; // The bar with PasteContents should not be hidden or users might not know how to paste what they just copied. + // Set m_contents to anything else to circumvent this prevention mechanism. + } + + if (!m_heightAnimation) { + m_heightAnimation = new QPropertyAnimation(this, "maximumHeight"); + } + disconnect(m_heightAnimation, &QAbstractAnimation::finished, + this, nullptr); + m_heightAnimation->setDuration(2 * + style()->styleHint(QStyle::SH_Widget_Animation_Duration, nullptr, this) * + GlobalConfig::animationDurationFactor()); + + if (visible) { + show(); + m_heightAnimation->setStartValue(0); + m_heightAnimation->setEndValue(sizeHint().height()); + m_heightAnimation->setEasingCurve(QEasingCurve::OutCubic); + connect(m_heightAnimation, &QAbstractAnimation::finished, + this, [this](){ setMaximumHeight(sizeHint().height()); }); + } else { + m_heightAnimation->setStartValue(height()); + m_heightAnimation->setEndValue(0); + m_heightAnimation->setEasingCurve(QEasingCurve::OutCubic); + connect(m_heightAnimation, &QAbstractAnimation::finished, + this, &QWidget::hide); + } + + m_heightAnimation->start(); +} + +QSize SelectionModeBottomBar::sizeHint() const +{ + // 1 as width because this widget should never be the reason the DolphinViewContainer is made wider. + return QSize{1, m_layout->parentWidget()->sizeHint().height()}; +} + +void SelectionModeBottomBar::slotSelectionChanged(const KFileItemList &selection, const QUrl &baseUrl) +{ + if (m_contents == GeneralContents) { + auto contextActions = contextActionsFor(selection, baseUrl); + m_generalBarActions.clear(); + for (auto i = contextActions.begin(); i != contextActions.end(); ++i) { + m_generalBarActions.emplace_back(ActionWithWidget{*i}); + } + resetContents(GeneralContents); + } + updateMainActionButton(selection); +} + +void SelectionModeBottomBar::slotSplitTabDisabled() +{ + switch (m_contents) { + case CopyToOtherViewContents: + case MoveToOtherViewContents: + Q_EMIT leaveSelectionModeRequested(); + default: + return; + } +} + +void SelectionModeBottomBar::resetContents(SelectionModeBottomBar::Contents contents) +{ + emptyBarContents(); + + // A label is added in many of the methods below. We only know its size a bit later and if it should be hidden. + QTimer::singleShot(10, this, [this](){ updateExplanatoryLabelVisibility(); }); + + Q_CHECK_PTR(m_actionCollection); + m_contents = contents; + switch (contents) { + case CopyContents: + return addCopyContents(); + case CopyLocationContents: + return addCopyLocationContents(); + case CopyToOtherViewContents: + return addCopyToOtherViewContents(); + case CutContents: + return addCutContents(); + case DeleteContents: + return addDeleteContents(); + case DuplicateContents: + return addDuplicateContents(); + case GeneralContents: + return addGeneralContents(); + case PasteContents: + return addPasteContents(); + case MoveToOtherViewContents: + return addMoveToOtherViewContents(); + case MoveToTrashContents: + return addMoveToTrashContents(); + case RenameContents: + return addRenameContents(); + } +} + +bool SelectionModeBottomBar::eventFilter(QObject *watched, QEvent *event) +{ + Q_ASSERT(qobject_cast(watched)); // This evenfFilter is only implemented for QWidgets. + + switch (event->type()) { + case QEvent::ChildAdded: + case QEvent::ChildRemoved: + QTimer::singleShot(0, this, [this](){ setMaximumHeight(sizeHint().height()); }); + // Fall through. + default: + return false; + } +} + +void SelectionModeBottomBar::resizeEvent(QResizeEvent *resizeEvent) +{ + if (resizeEvent->oldSize().width() == resizeEvent->size().width()) { + // The width() didn't change so our custom override isn't needed. + return QWidget::resizeEvent(resizeEvent); + } + m_layout->parentWidget()->setFixedWidth(resizeEvent->size().width()); + + if (m_contents == GeneralContents) { + Q_ASSERT(m_overflowButton); + if (unusedSpace() < 0) { + // The bottom bar is overflowing! We need to hide some of the widgets. + for (auto i = m_generalBarActions.rbegin(); i != m_generalBarActions.rend(); ++i) { + if (!i->isWidgetVisible()) { + continue; + } + i->widget()->setVisible(false); + + // Add the action to the overflow. + std::cout << "An Action is added to the m_overflowButton because of a resize: " << qPrintable(i->action()->text()) << "\n"; + auto overflowMenu = m_overflowButton->menu(); + if (overflowMenu->actions().isEmpty()) { + overflowMenu->addAction(i->action()); + } else { + overflowMenu->insertAction(overflowMenu->actions().at(0), i->action()); + } + std::cout << "The number of actions in the menu is now " << m_overflowButton->menu()->actions().count() << "\n."; + m_overflowButton->setVisible(true); + if (unusedSpace() >= 0) { + break; // All widgets fit now. + } + } + } else { + // We have some unusedSpace(). Let's check if we can maybe add more of the contextual action's widgets. + for (auto i = m_generalBarActions.begin(); i != m_generalBarActions.end(); ++i) { + if (i->isWidgetVisible()) { + continue; + } + if (!i->widget()) { + i->newWidget(this); + i->widget()->setVisible(false); + m_layout->insertWidget(m_layout->count() - 1, i->widget()); // Insert before m_overflowButton + } + if (unusedSpace() < i->widget()->sizeHint().width()) { + // It doesn't fit. We keep it invisible. + break; + } + i->widget()->setVisible(true); + + // Remove the action from the overflow. + std::cout << "An Action is removed from the m_overflowButton because of a resize: " << qPrintable(i->action()->text()) << "\n"; + auto overflowMenu = m_overflowButton->menu(); + overflowMenu->removeAction(i->action()); + std::cout << "The number of actions in the menu is now " << m_overflowButton->menu()->actions().count() << "\n."; + if (overflowMenu->isEmpty()) { + m_overflowButton->setVisible(false); + } + } + } + } + + // Hide the leading explanation if it doesn't fit. The buttons are labeled clear enough that this shouldn't be a big UX problem. + updateExplanatoryLabelVisibility(); + return QWidget::resizeEvent(resizeEvent); +} + +void SelectionModeBottomBar::addCopyContents() +{ + m_explanatoryLabel = new QLabel(i18nc("@info explaining the next step in a process", "Select the files and folders that should be copied."), this); + m_explanatoryLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); + m_explanatoryLabel->setWordWrap(true); + m_layout->addWidget(m_explanatoryLabel); + + // i18n: Aborts the current step-by-step process to copy files by leaving the selection mode. + auto *cancelButton = new QPushButton(i18nc("@action:button", "Abort Copying"), this); + connect(cancelButton, &QAbstractButton::clicked, this, &SelectionModeBottomBar::leaveSelectionModeRequested); + m_layout->addWidget(cancelButton); + + auto *copyButton = new QPushButton(this); + // We claim to have PasteContents already so triggering the copy action next won't instantly hide the bottom bar. + connect(copyButton, &QAbstractButton::clicked, [this]() { + if (GeneralSettings::showPasteBarAfterCopying()) { + m_contents = Contents::PasteContents; + } + }); + // Connect the copy action as a second step. + m_mainAction = ActionWithWidget(m_actionCollection->action(KStandardAction::name(KStandardAction::Copy)), copyButton); + // Finally connect the lambda that actually changes the contents to the PasteContents. + connect(copyButton, &QAbstractButton::clicked, [this]() { + if (GeneralSettings::showPasteBarAfterCopying()) { + resetContents(Contents::PasteContents); // resetContents() needs to be connected last because + // it instantly deletes the button and then the other slots won't be called. + } + Q_EMIT leaveSelectionModeRequested(); + }); + updateMainActionButton(KFileItemList()); + m_layout->addWidget(copyButton); +} + +void SelectionModeBottomBar::addCopyLocationContents() +{ + m_explanatoryLabel = new QLabel(i18nc("@info explaining the next step in a process", "Select one file or folder whose location should be copied."), this); + m_explanatoryLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); + m_explanatoryLabel->setWordWrap(true); + m_layout->addWidget(m_explanatoryLabel); + + // i18n: Aborts the current step-by-step process to copy the location of files by leaving the selection mode. + auto *cancelButton = new QPushButton(i18nc("@action:button", "Abort Copying"), this); + connect(cancelButton, &QAbstractButton::clicked, this, &SelectionModeBottomBar::leaveSelectionModeRequested); + m_layout->addWidget(cancelButton); + + auto *copyLocationButton = new QPushButton(this); + m_mainAction = ActionWithWidget(m_actionCollection->action(QStringLiteral("copy_location")), copyLocationButton); + updateMainActionButton(KFileItemList()); + m_layout->addWidget(copyLocationButton); +} + +void SelectionModeBottomBar::addCopyToOtherViewContents() +{ + // i18n: "Copy over" refers to copying to the other split view area that is currently visible to the user. + m_explanatoryLabel = new QLabel(i18nc("@info explaining the next step in a process", "Select the files and folders that should be copied over."), this); + m_explanatoryLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); + m_explanatoryLabel->setWordWrap(true); + m_layout->addWidget(m_explanatoryLabel); + + // i18n: Aborts the current step-by-step process to copy the location of files by leaving the selection mode. + auto *cancelButton = new QPushButton(i18nc("@action:button", "Abort Copying"), this); + connect(cancelButton, &QAbstractButton::clicked, this, &SelectionModeBottomBar::leaveSelectionModeRequested); + m_layout->addWidget(cancelButton); + + auto *copyToOtherViewButton = new QPushButton(this); + m_mainAction = ActionWithWidget(m_actionCollection->action(QStringLiteral("copy_to_inactive_split_view")), copyToOtherViewButton); + updateMainActionButton(KFileItemList()); + m_layout->addWidget(copyToOtherViewButton); +} + +void SelectionModeBottomBar::addCutContents() +{ + m_explanatoryLabel = new QLabel(i18nc("@info explaining the next step in a process", "Select the files and folders that should be cut."), this); + m_explanatoryLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); + m_explanatoryLabel->setWordWrap(true); + m_layout->addWidget(m_explanatoryLabel); + + // i18n: Aborts the current step-by-step process to cut files by leaving the selection mode. + auto *cancelButton = new QPushButton(i18nc("@action:button", "Abort Cutting"), this); + connect(cancelButton, &QAbstractButton::clicked, this, &SelectionModeBottomBar::leaveSelectionModeRequested); + m_layout->addWidget(cancelButton); + + auto *cutButton = new QPushButton(this); + // We claim to have PasteContents already so triggering the cut action next won't instantly hide the bottom bar. + connect(cutButton, &QAbstractButton::clicked, [this]() { + if (GeneralSettings::showPasteBarAfterCopying()) { + m_contents = Contents::PasteContents; + } + }); + // Connect the cut action as a second step. + m_mainAction = ActionWithWidget(m_actionCollection->action(KStandardAction::name(KStandardAction::Cut)), cutButton); + // Finally connect the lambda that actually changes the contents to the PasteContents. + connect(cutButton, &QAbstractButton::clicked, [this](){ + if (GeneralSettings::showPasteBarAfterCopying()) { + resetContents(Contents::PasteContents); // resetContents() needs to be connected last because + // it instantly deletes the button and then the other slots won't be called. + } + Q_EMIT leaveSelectionModeRequested(); + }); + updateMainActionButton(KFileItemList()); + m_layout->addWidget(cutButton); +} + +void SelectionModeBottomBar::addDeleteContents() +{ + m_explanatoryLabel = new QLabel(i18nc("@info explaining the next step in a process", "Select the files and folders that should be permanently deleted."), this); + m_explanatoryLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); + m_explanatoryLabel->setWordWrap(true); + m_layout->addWidget(m_explanatoryLabel); + + // i18n: Aborts the current step-by-step process to delete files by leaving the selection mode. + auto *cancelButton = new QPushButton(i18nc("@action:button", "Abort"), this); + connect(cancelButton, &QAbstractButton::clicked, this, &SelectionModeBottomBar::leaveSelectionModeRequested); + m_layout->addWidget(cancelButton); + + auto *deleteButton = new QPushButton(this); + m_mainAction = ActionWithWidget(m_actionCollection->action(KStandardAction::name(KStandardAction::DeleteFile)), deleteButton); + updateMainActionButton(KFileItemList()); + m_layout->addWidget(deleteButton); +} + +void SelectionModeBottomBar::addDuplicateContents() +{ + m_explanatoryLabel = new QLabel(i18nc("@info explaining the next step in a process", "Select the files and folders that should be duplicated here."), this); + m_explanatoryLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); + m_explanatoryLabel->setWordWrap(true); + m_layout->addWidget(m_explanatoryLabel); + + // i18n: Aborts the current step-by-step process to duplicate files by leaving the selection mode. + auto *cancelButton = new QPushButton(i18nc("@action:button", "Abort Duplicating"), this); + connect(cancelButton, &QAbstractButton::clicked, this, &SelectionModeBottomBar::leaveSelectionModeRequested); + m_layout->addWidget(cancelButton); + + auto *duplicateButton = new QPushButton(this); + m_mainAction = ActionWithWidget(m_actionCollection->action(QStringLiteral("duplicate")), duplicateButton); + updateMainActionButton(KFileItemList()); + m_layout->addWidget(duplicateButton); +} + +void SelectionModeBottomBar::addGeneralContents() +{ + if (!m_overflowButton) { + m_overflowButton = new QToolButton{this}; + // i18n: This button appears in a bar if there isn't enough horizontal space to fit all the other buttons. + // The small icon-only button opens a menu that contains the actions that didn't fit on the bar. + // Since this is an icon-only button this text will only appear as a tooltip and as accessibility text. + m_overflowButton->setToolTip(i18nc("@action", "More")); + m_overflowButton->setAccessibleName(m_overflowButton->toolTip()); + m_overflowButton->setIcon(QIcon::fromTheme(QStringLiteral("view-more-horizontal-symbolic"))); + m_overflowButton->setMenu(new QMenu{m_overflowButton}); + m_overflowButton->setPopupMode(QToolButton::ToolButtonPopupMode::InstantPopup); + m_overflowButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::MinimumExpanding); // Makes sure it has the same height as the labeled buttons. + m_layout->addWidget(m_overflowButton); + } else { + m_overflowButton->menu()->actions().clear(); + // The overflowButton should be part of the calculation for needed space so we set it visible in regards to unusedSpace(). + m_overflowButton->setVisible(true); + } + + // We first add all the m_generalBarActions to the bar until the bar is full. + auto i = m_generalBarActions.begin(); + for (; i != m_generalBarActions.end(); ++i) { + if (i->action()->isVisible()) { + if (i->widget()) { + i->widget()->setEnabled(i->action()->isEnabled()); + } else { + i->newWidget(this); + i->widget()->setVisible(false); + m_layout->insertWidget(m_layout->count() - 1, i->widget()); // Insert before m_overflowButton + } + if (unusedSpace() < i->widget()->sizeHint().width()) { + std::cout << "The " << unusedSpace() << " is smaller than the button->sizeHint().width() of " << i->widget()->sizeHint().width() << " plus the m_layout->spacing() of " << m_layout->spacing() << " so the action " << qPrintable(i->action()->text()) << " doesn't get its own button.\n"; + break; // The bar is too full already. We keep it invisible. + } else { + std::cout << "The " << unusedSpace() << " is bigger than the button->sizeHint().width() of " << i->widget()->sizeHint().width() << " plus the m_layout->spacing() of " << m_layout->spacing() << " so the action " << qPrintable(i->action()->text()) << " was added as its own button/widget.\n"; + i->widget()->setVisible(true); + } + } + } + // We are done adding widgets to the bar so either we were able to fit all the actions in there + m_overflowButton->setVisible(false); + // …or there are more actions left which need to be put into m_overflowButton. + for (; i != m_generalBarActions.end(); ++i) { + m_overflowButton->menu()->addAction(i->action()); + + // The overflowButton is set visible if there is actually an action in it. + if (!m_overflowButton->isVisible() && i->action()->isVisible() && !i->action()->isSeparator()) { + m_overflowButton->setVisible(true); + } + } +} + +void SelectionModeBottomBar::addMoveToOtherViewContents() +{ + // i18n: "Move over" refers to moving to the other split view area that is currently visible to the user. + m_explanatoryLabel = new QLabel(i18nc("@info explaining the next step in a process", "Select the files and folders that should be moved over."), this); + m_explanatoryLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); + m_explanatoryLabel->setWordWrap(true); + m_layout->addWidget(m_explanatoryLabel); + + // i18n: Aborts the current step-by-step process to copy the location of files by leaving the selection mode. + auto *cancelButton = new QPushButton(i18nc("@action:button", "Abort Moving"), this); + connect(cancelButton, &QAbstractButton::clicked, this, &SelectionModeBottomBar::leaveSelectionModeRequested); + m_layout->addWidget(cancelButton); + + auto *moveToOtherViewButton = new QPushButton(this); + m_mainAction = ActionWithWidget(m_actionCollection->action(QStringLiteral("move_to_inactive_split_view")), moveToOtherViewButton); + updateMainActionButton(KFileItemList()); + m_layout->addWidget(moveToOtherViewButton); +} + +void SelectionModeBottomBar::addMoveToTrashContents() +{ + m_explanatoryLabel = new QLabel(i18nc("@info explaining the next step in a process", "Select the files and folders that should be moved to the Trash."), this); + m_explanatoryLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); + m_explanatoryLabel->setWordWrap(true); + m_layout->addWidget(m_explanatoryLabel); + + // i18n: Aborts the current step-by-step process of moving files to the trash by leaving the selection mode. + auto *cancelButton = new QPushButton(i18nc("@action:button", "Abort"), this); + connect(cancelButton, &QAbstractButton::clicked, this, &SelectionModeBottomBar::leaveSelectionModeRequested); + m_layout->addWidget(cancelButton); + + auto *moveToTrashButton = new QPushButton(this); + m_mainAction = ActionWithWidget(m_actionCollection->action(KStandardAction::name(KStandardAction::MoveToTrash)), moveToTrashButton); + updateMainActionButton(KFileItemList()); + m_layout->addWidget(moveToTrashButton); +} + +void SelectionModeBottomBar::addPasteContents() +{ + m_explanatoryLabel = new QLabel(xi18n("The selected files and folders were added to the Clipboard. " + "Now the Paste action can be used to transfer them from the Clipboard " + "to any other location. They can even be transferred to other applications by using their " + "respective Paste actions."), this); + m_explanatoryLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); + m_explanatoryLabel->setWordWrap(true); + m_layout->addWidget(m_explanatoryLabel); + + auto *vBoxLayout = new QVBoxLayout(this); + m_layout->addLayout(vBoxLayout); + + /** We are in "PasteContents" mode which means hiding the bottom bar is impossible. + * So we first have to claim that we have different contents before requesting to leave selection mode. */ + auto actuallyLeaveSelectionMode = [this]() { + m_contents = Contents::CopyLocationContents; + Q_EMIT leaveSelectionModeRequested(); + }; + + auto *pasteButton = new QPushButton(this); + copyActionDataToButton(pasteButton, m_actionCollection->action(KStandardAction::name(KStandardAction::Paste))); + pasteButton->setText(i18nc("@action A more elaborate and clearly worded version of the Paste action", "Paste from Clipboard")); + connect(pasteButton, &QAbstractButton::clicked, this, actuallyLeaveSelectionMode); + vBoxLayout->addWidget(pasteButton); + + auto *dismissButton = new QToolButton(this); + dismissButton->setText(i18nc("@action Dismisses a bar explaining how to use the Paste action", "Dismiss this Reminder")); + connect(dismissButton, &QAbstractButton::clicked, this, actuallyLeaveSelectionMode); + auto *dontRemindAgainAction = new QAction(i18nc("@action Dismisses an explanatory area and never shows it again", "Don't remind me again"), this); + connect(dontRemindAgainAction, &QAction::triggered, this, []() { + GeneralSettings::setShowPasteBarAfterCopying(false); + }); + connect(dontRemindAgainAction, &QAction::triggered, this, actuallyLeaveSelectionMode); + auto *dismissButtonMenu = new QMenu(dismissButton); + dismissButtonMenu->addAction(dontRemindAgainAction); + dismissButton->setMenu(dismissButtonMenu); + dismissButton->setPopupMode(QToolButton::MenuButtonPopup); + vBoxLayout->addWidget(dismissButton); + + m_explanatoryLabel->setMaximumHeight(pasteButton->sizeHint().height() + dismissButton->sizeHint().height() + m_explanatoryLabel->fontMetrics().height()); +} + +void SelectionModeBottomBar::addRenameContents() +{ + m_explanatoryLabel = new QLabel(i18nc("@info explains the next step in a process", "Select the file or folder that should be renamed.\nBulk renaming is possible when multiple items are selected."), this); + m_explanatoryLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); + m_explanatoryLabel->setWordWrap(true); + m_layout->addWidget(m_explanatoryLabel); + + // i18n: Aborts the current step-by-step process to delete files by leaving the selection mode. + auto *cancelButton = new QPushButton(i18nc("@action:button", "Stop Renaming"), this); + connect(cancelButton, &QAbstractButton::clicked, this, &SelectionModeBottomBar::leaveSelectionModeRequested); + m_layout->addWidget(cancelButton); + + auto *renameButton = new QPushButton(this); + m_mainAction = ActionWithWidget(m_actionCollection->action(KStandardAction::name(KStandardAction::RenameFile)), renameButton); + updateMainActionButton(KFileItemList()); + m_layout->addWidget(renameButton); +} + +void SelectionModeBottomBar::emptyBarContents() +{ + QLayoutItem *child; + while ((child = m_layout->takeAt(0)) != nullptr) { + if (auto *childLayout = child->layout()) { + QLayoutItem *grandChild; + while ((grandChild = childLayout->takeAt(0)) != nullptr) { + delete grandChild->widget(); // delete the widget + delete grandChild; // delete the layout item + } + } + delete child->widget(); // delete the widget + delete child; // delete the layout item + } +} + +std::vector SelectionModeBottomBar::contextActionsFor(const KFileItemList& selectedItems, const QUrl& baseUrl) +{ + std::vector contextActions; + contextActions.emplace_back(m_actionCollection->action(KStandardAction::name(KStandardAction::Copy))); + contextActions.emplace_back(m_actionCollection->action(KStandardAction::name(KStandardAction::Cut))); + contextActions.emplace_back(m_actionCollection->action(KStandardAction::name(KStandardAction::RenameFile))); + contextActions.emplace_back(m_actionCollection->action(KStandardAction::name(KStandardAction::MoveToTrash))); + + if (!selectedItems.isEmpty()) { + // We are going to add the actions from the right-click context menu for the selected items. + auto *dolphinMainWindow = qobject_cast(window()); + Q_CHECK_PTR(dolphinMainWindow); + if (!m_fileItemActions) { + m_fileItemActions = new KFileItemActions(this); + m_fileItemActions->setParentWidget(dolphinMainWindow); + connect(m_fileItemActions, &KFileItemActions::error, this, &SelectionModeBottomBar::error); + } + m_internalContextMenu = std::make_unique(dolphinMainWindow, selectedItems.constFirst(), selectedItems, baseUrl, m_fileItemActions); + auto internalContextMenuActions = m_internalContextMenu->actions(); + + // There are some actions which we wouldn't want to add. We remember them in the actionsThatShouldntBeAdded set. + // We don't want to add the four basic actions again which were already added to the top. + std::unordered_set actionsThatShouldntBeAdded{contextActions.begin(), contextActions.end()}; + // "Delete" isn't really necessary to add because we have "Move to Trash" already. It is also more dangerous so let's exclude it. + actionsThatShouldntBeAdded.insert(m_actionCollection->action(KStandardAction::name(KStandardAction::DeleteFile))); + // "Open Terminal" isn't really context dependent and can therefore be opened from elsewhere instead. + actionsThatShouldntBeAdded.insert(m_actionCollection->action(QStringLiteral("open_terminal"))); + + // KHamburgerMenu would only be visible if there is no menu available anywhere on the user interface. This might be useful for recovery from + // such a situation in theory but a bar with context dependent actions doesn't really seem like the right place for it. + Q_ASSERT(internalContextMenuActions.first()->icon().name() == m_actionCollection->action(KStandardAction::name(KStandardAction::HamburgerMenu))->icon().name()); + internalContextMenuActions.removeFirst(); + + for (auto it = internalContextMenuActions.constBegin(); it != internalContextMenuActions.constEnd(); ++it) { + if (actionsThatShouldntBeAdded.count(*it)) { + continue; // Skip this action. + } + if (!qobject_cast(*it)) { // We already have a "Move to Trash" action so we don't want a DolphinRemoveAction. + // We filter duplicate separators here so we won't have to deal with them later. + if (!contextActions.back()->isSeparator() || !(*it)->isSeparator()) { + contextActions.emplace_back((*it)); + } + } + } + } + return contextActions; +} + +int SelectionModeBottomBar::unusedSpace() const +{ + int sumOfPreferredWidths = m_layout->contentsMargins().left() + m_layout->contentsMargins().right(); + if (m_overflowButton) { + sumOfPreferredWidths += m_overflowButton->sizeHint().width(); + } + std::cout << "These layout items should have sane width: "; + for (int i = 0; i < m_layout->count(); ++i) { + auto widget = m_layout->itemAt(i)->widget(); + if (widget && !widget->isVisibleTo(widget->parentWidget())) { + continue; // We don't count invisible widgets. + } + std::cout << m_layout->itemAt(i)->sizeHint().width() << ", "; + if (m_layout->itemAt(i)->sizeHint().width() == 0) { + // One of the items reports an invalid width. We can't work with this so we report an unused space of 0 which should lead to as few changes to the + // layout as possible until the next resize event happens at a later point in time. + //return 0; + } + sumOfPreferredWidths += m_layout->itemAt(i)->sizeHint().width() + m_layout->spacing(); + } + std::cout << "leads to unusedSpace = " << width() << " - " << sumOfPreferredWidths - 20 << " = " << width() - sumOfPreferredWidths - 20 << "\n"; + return width() - sumOfPreferredWidths - 20; // We consider all space used when there are only 20 pixels left + // so there is some room to breath and not too much wonkyness while resizing. +} + +void SelectionModeBottomBar::updateExplanatoryLabelVisibility() +{ + if (!m_explanatoryLabel) { + return; + } + std::cout << "label minimumSizeHint compared to width() :" << m_explanatoryLabel->sizeHint().width() << "/" << m_explanatoryLabel->width() << "; unusedSpace: " << unusedSpace() << "\n"; + if (m_explanatoryLabel->isVisible()) { + m_explanatoryLabel->setVisible(unusedSpace() > 0); + } else { + // We only want to re-show the label when it fits comfortably so the computation below adds another "+20". + m_explanatoryLabel->setVisible(unusedSpace() > m_explanatoryLabel->sizeHint().width() + 20); + } +} + +void SelectionModeBottomBar::updateMainActionButton(const KFileItemList& selection) +{ + if (!m_mainAction.widget()) { + return; + } + Q_ASSERT(qobject_cast(m_mainAction.widget())); + + // Users are nudged towards selecting items by having the button disabled when nothing is selected. + m_mainAction.widget()->setEnabled(selection.count() > 0 && m_mainAction.action()->isEnabled()); + QFontMetrics fontMetrics = m_mainAction.widget()->fontMetrics(); + + QString buttonText; + switch (m_contents) { + case CopyContents: + buttonText = i18ncp("@action A more elaborate and clearly worded version of the Copy action", + "Copy %2 to the Clipboard", "Copy %2 to the Clipboard", selection.count(), + fileItemListToString(selection, fontMetrics.averageCharWidth() * 20, fontMetrics)); + break; + case CopyLocationContents: + buttonText = i18ncp("@action A more elaborate and clearly worded version of the Copy Location action", + "Copy the Location of %2 to the Clipboard", "Copy the Location of %2 to the Clipboard", selection.count(), + fileItemListToString(selection, fontMetrics.averageCharWidth() * 20, fontMetrics)); + break; + case CutContents: + buttonText = i18ncp("@action A more elaborate and clearly worded version of the Cut action", + "Cut %2 to the Clipboard", "Cut %2 to the Clipboard", selection.count(), + fileItemListToString(selection, fontMetrics.averageCharWidth() * 20, fontMetrics)); + break; + case DeleteContents: + buttonText = i18ncp("@action A more elaborate and clearly worded version of the Delete action", + "Permanently Delete %2", "Permanently Delete %2", selection.count(), + fileItemListToString(selection, fontMetrics.averageCharWidth() * 20, fontMetrics)); + break; + case DuplicateContents: + buttonText = i18ncp("@action A more elaborate and clearly worded version of the Duplicate action", + "Duplicate %2", "Duplicate %2", selection.count(), + fileItemListToString(selection, fontMetrics.averageCharWidth() * 20, fontMetrics)); + break; + case MoveToTrashContents: + buttonText = i18ncp("@action A more elaborate and clearly worded version of the Trash action", + "Move %2 to the Trash", "Move %2 to the Trash", selection.count(), + fileItemListToString(selection, fontMetrics.averageCharWidth() * 20, fontMetrics)); + break; + case RenameContents: + buttonText = i18ncp("@action A more elaborate and clearly worded version of the Rename action", + "Rename %2", "Rename %2", selection.count(), + fileItemListToString(selection, fontMetrics.averageCharWidth() * 20, fontMetrics)); + break; + default: + return; + } + if (buttonText != QStringLiteral("NULL")) { + static_cast(m_mainAction.widget())->setText(buttonText); + + // The width of the button has changed. We might want to hide the label so the full button text fits on the bar. + updateExplanatoryLabelVisibility(); + } +} diff --git a/src/selectionmode/selectionmodebottombar.h b/src/selectionmode/selectionmodebottombar.h new file mode 100644 index 0000000000..61cb903344 --- /dev/null +++ b/src/selectionmode/selectionmodebottombar.h @@ -0,0 +1,181 @@ +/* + This file is part of the KDE project + SPDX-FileCopyrightText: 2022 Felix Ernst + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#ifndef SELECTIONMODEBOTTOMBAR_H +#define SELECTIONMODEBOTTOMBAR_H + +#include "actionwithwidget.h" +#include "global.h" + +#include +#include +#include +#include + +#include + +class DolphinContextMenu; +class KActionCollection; +class KFileItemActions; +class KFileItemList; +class QAbstractButton; +class QAction; +class QFontMetrics; +class QHBoxLayout; +class QLabel; +class QPushButton; +class QResizeEvent; +class QToolButton; +class QUrl; + +/** + * A bar mainly used in selection mode that serves various purposes depending on what the user is currently trying to do. + * + * The Contents enum below gives a rough idea about the different states this bar might have. + * The bar is notified of various changes that make changing or updating the content worthwhile. + */ +class SelectionModeBottomBar : public QWidget +{ + Q_OBJECT + +public: + /** The different contents this bar can have. */ + enum Contents{ + CopyContents, + CopyLocationContents, + CopyToOtherViewContents, + CutContents, + DeleteContents, + DuplicateContents, + GeneralContents, + MoveToOtherViewContents, + MoveToTrashContents, + PasteContents, + RenameContents + }; + + /** + * Default constructor + */ + explicit SelectionModeBottomBar(KActionCollection *actionCollection, QWidget *parent); + + /** + * Plays a show or hide animation while changing visibility. + * Therefore, if this method is used to hide this widget, the actual hiding will be postponed until the animation finished. + * @see QWidget::setVisible() + */ + void setVisible(bool visible, Animated animated); + using QWidget::setVisible; // Makes sure that the setVisible() declaration above doesn't hide the one from QWidget. + + void resetContents(Contents contents); + inline Contents contents() const + { + return m_contents; + }; + + QSize sizeHint() const override; + +public Q_SLOTS: + void slotSelectionChanged(const KFileItemList &selection, const QUrl &baseUrl); + + /** Used to notify the m_selectionModeBottomBar that there is no other ViewContainer in the tab. */ + void slotSplitTabDisabled(); + +Q_SIGNALS: + /** + * Forwards the errors from the KFileItemAction::error() used for contextual actions. + */ + void error(const QString &errorMessage); + + void leaveSelectionModeRequested(); + +protected: + /** Is installed on an internal widget to make sure that the height of the bar is adjusted to its contents. */ + bool eventFilter(QObject *watched, QEvent *event) override; + void resizeEvent(QResizeEvent *resizeEvent) override; + +private: + void addCopyContents(); + void addCopyLocationContents(); + void addCopyToOtherViewContents(); + void addCutContents(); + void addDeleteContents(); + void addDuplicateContents(); + /** + * Adds the actions of m_generalBarActions as buttons to the bar. An overflow menu button is + * created to make sure any amount of actions can be accessed. + */ + void addGeneralContents(); + void addMoveToOtherViewContents(); + void addMoveToTrashContents(); + void addPasteContents(); + void addRenameContents(); + + /** + * Deletes all visible widgets and layouts from the bar. + */ + void emptyBarContents(); + + /** + * @returns A vector containing contextual actions for the given \a selection in the \a baseUrl. + * Cut, Copy, Rename and MoveToTrash are always added. Any further contextual actions depend on + * \a selection and \a baseUrl. \a selection and \a baseUrl can be empty/default constructed if + * no item- or view-specific actions should be added aside from Cut, Copy, Rename, MoveToTrash. + * @param selectedItems The selected items for which contextual actions should be displayed. + * @param baseUrl Base URL of the viewport the contextual actions apply to. + */ + std::vector contextActionsFor(const KFileItemList &selectedItems, const QUrl &baseUrl); + + /** + * @returns the amount of pixels that can be spared to add more widgets. A negative value might + * be returned which signifies that some widgets should be hidden or removed from this bar to + * make sure that this SelectionModeBottomBar won't stretch the width of its parent. + */ + int unusedSpace() const; + + /** + * The label isn't that important. This method hides it if there isn't enough room on the bar or + * shows it if there is. + */ + void updateExplanatoryLabelVisibility(); + + /** + * Changes the text and enabled state of the main action button + * based on the amount of currently selected items and the state of the current m_mainAction. + * The current main action depends on the current barContents. + * @param selection the currently selected fileItems. + */ + void updateMainActionButton(const KFileItemList &selection); + +private: + /// All the actions that should be available from this bar when in general mode. + std::vector m_generalBarActions; + /// The context menu used to retrieve all the actions that are relevant for the current selection. + std::unique_ptr m_internalContextMenu; + /// An object that is necessary to keep around for m_internalContextMenu. + KFileItemActions *m_fileItemActions = nullptr; + + /// @see updateMainActionButtonText + ActionWithWidget m_mainAction = ActionWithWidget(nullptr); + /// The button containing all the actions that don't currently fit into the bar. + QPointer m_overflowButton; + /// The actionCollection from which the actions for this bar are retrieved. + KActionCollection *m_actionCollection; + /// Describes the current contents of the bar. + Contents m_contents; + /** The layout all the buttons and labels are added to. + * Do not confuse this with layout() because we do have a QScrollView in between this widget and m_layout. */ + QHBoxLayout *m_layout; + + /// @see SelectionModeBottomBar::setVisible() + QPointer m_heightAnimation; + + /// The info label used for some of the BarContents. Is hidden for narrow widths. + QPointer m_explanatoryLabel; +}; + +#endif // SELECTIONMODEBOTTOMBAR_H diff --git a/src/selectionmode/selectionmodetopbar.cpp b/src/selectionmode/selectionmodetopbar.cpp new file mode 100644 index 0000000000..89a4aa03a0 --- /dev/null +++ b/src/selectionmode/selectionmodetopbar.cpp @@ -0,0 +1,131 @@ +/* + This file is part of the KDE project + SPDX-FileCopyrightText: 2022 Felix Ernst + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#include "selectionmodetopbar.h" + +#include "backgroundcolorhelper.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +SelectionModeTopBar::SelectionModeTopBar(QWidget *parent) : + QWidget{parent} +{ + // Showing of this widget is normally animated. We hide it for now and make it small. + hide(); + setMaximumHeight(0); + + setToolTip(KToolTipHelper::whatsThisHintOnly()); + setWhatsThis(xi18nc("@info:whatsthis", "Selection ModeSelect files or folders to manage or manipulate them." + "Press on a file or folder to select it.Press on an already selected file or folder to deselect it." + "Pressing an empty area does not clear the selection." + "Selection rectangles (created by dragging from an empty area) invert the selection status of items within." + "The available action buttons at the bottom change depending on the current selection.")); + + auto fillParentLayout = new QGridLayout(this); + fillParentLayout->setContentsMargins(0, 0, 0, 0); + + // Put the contents into a QScrollArea. This prevents increasing the view width + // in case that not enough width for the contents is available. (this trick is also used in selectionmodebottombar.cpp.) + auto scrollArea = new QScrollArea(this); + fillParentLayout->addWidget(scrollArea); + scrollArea->setFrameShape(QFrame::NoFrame); + scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + scrollArea->setWidgetResizable(true); + + auto contentsContainer = new QWidget(scrollArea); + scrollArea->setWidget(contentsContainer); + + BackgroundColorHelper::instance()->controlBackgroundColor(this); + + setMinimumWidth(0); + + m_fullLabelString = i18nc("@info label above the view explaining the state", + "Selection Mode: Click on files or folders to select or deselect them."); + m_shortLabelString = i18nc("@info label above the view explaining the state", + "Selection Mode"); + m_label = new QLabel(contentsContainer); + m_label->setMinimumWidth(0); + BackgroundColorHelper::instance()->controlBackgroundColor(m_label); + + m_closeButton = new QPushButton(QIcon::fromTheme(QStringLiteral("window-close-symbolic")), "", contentsContainer); + m_closeButton->setToolTip(i18nc("@action:button", "Exit Selection Mode")); + m_closeButton->setAccessibleName(m_closeButton->toolTip()); + m_closeButton->setFlat(true); + connect(m_closeButton, &QAbstractButton::pressed, + this, &SelectionModeTopBar::leaveSelectionModeRequested); + + QHBoxLayout *layout = new QHBoxLayout(contentsContainer); + auto contentsMargins = layout->contentsMargins(); + m_preferredHeight = contentsMargins.top() + m_label->sizeHint().height() + contentsMargins.bottom(); + scrollArea->setMaximumHeight(m_preferredHeight); + m_closeButton->setFixedHeight(m_preferredHeight); + layout->setContentsMargins(0, 0, 0, 0); + + layout->addStretch(); + layout->addWidget(m_label); + layout->addStretch(); + layout->addWidget(m_closeButton); +} + +void SelectionModeTopBar::setVisible(bool visible, Animated animated) +{ + Q_ASSERT_X(animated == WithAnimation, "SelectionModeTopBar::setVisible", "This wasn't implemented."); + + if (!m_heightAnimation) { + m_heightAnimation = new QPropertyAnimation(this, "maximumHeight"); + } + disconnect(m_heightAnimation, &QAbstractAnimation::finished, + this, &QWidget::hide); + m_heightAnimation->setDuration(2 * + style()->styleHint(QStyle::SH_Widget_Animation_Duration, nullptr, this) * + GlobalConfig::animationDurationFactor()); + + if (visible) { + show(); + m_heightAnimation->setStartValue(0); + m_heightAnimation->setEndValue(m_preferredHeight); + m_heightAnimation->setEasingCurve(QEasingCurve::OutCubic); + } else { + m_heightAnimation->setStartValue(height()); + m_heightAnimation->setEndValue(0); + m_heightAnimation->setEasingCurve(QEasingCurve::OutCubic); + connect(m_heightAnimation, &QAbstractAnimation::finished, + this, &QWidget::hide); + } + + m_heightAnimation->start(); +} + +void SelectionModeTopBar::resizeEvent(QResizeEvent */* resizeEvent */) +{ + updateLabelString(); +} + +void SelectionModeTopBar::updateLabelString() +{ + QFontMetrics fontMetrics = m_label->fontMetrics(); + if (fontMetrics.horizontalAdvance(m_fullLabelString) + m_closeButton->sizeHint().width() + style()->pixelMetric(QStyle::PM_LayoutLeftMargin) * 2 + style()->pixelMetric(QStyle::PM_LayoutRightMargin) * 2 < width()) { + m_label->setText(m_fullLabelString); + } else { + m_label->setText(m_shortLabelString); + } +} diff --git a/src/selectionmode/selectionmodetopbar.h b/src/selectionmode/selectionmodetopbar.h new file mode 100644 index 0000000000..fa829aef52 --- /dev/null +++ b/src/selectionmode/selectionmodetopbar.h @@ -0,0 +1,66 @@ +/* + This file is part of the KDE project + SPDX-FileCopyrightText: 2022 Felix Ernst + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#ifndef SELECTIONMODETOPBAR_H +#define SELECTIONMODETOPBAR_H + +#include "global.h" + +#include +#include +#include +#include + +class QHideEvent; +class QLabel; +class QPushButton; +class QResizeEvent; +class QShowEvent; + +/** + * @todo write docs + */ +class SelectionModeTopBar : public QWidget +{ + Q_OBJECT + +public: + SelectionModeTopBar(QWidget *parent); + + /** + * Plays a show or hide animation while changing visibility. + * Therefore, if this method is used to hide this widget, the actual hiding will be postponed until the animation finished. + * @see QWidget::setVisible() + */ + void setVisible(bool visible, Animated animated); + using QWidget::setVisible; // Makes sure that the setVisible() declaration above doesn't hide the one from QWidget. + +Q_SIGNALS: + void leaveSelectionModeRequested(); + +protected: + void resizeEvent(QResizeEvent */* resizeEvent */) override; + +private: + /** Decides whether the m_fullLabelString or m_shortLabelString should be used based on available width. */ + void updateLabelString(); + +private: + QLabel *m_label; + QPushButton *m_closeButton; + + /** @see updateLabelString() */ + QString m_fullLabelString; + /** @see updateLabelString() */ + QString m_shortLabelString; + + int m_preferredHeight; + + QPointer m_heightAnimation; +}; + +#endif // SELECTIONMODETOPBAR_H diff --git a/src/selectionmode/singleclickselectionproxystyle.h b/src/selectionmode/singleclickselectionproxystyle.h new file mode 100644 index 0000000000..9c185a85a2 --- /dev/null +++ b/src/selectionmode/singleclickselectionproxystyle.h @@ -0,0 +1,29 @@ +/* + This file is part of the KDE project + SPDX-FileCopyrightText: 2020 Felix Ernst + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#ifndef SINGLECLICKSELECTIONPROXYSTYLE_H +#define SINGLECLICKSELECTIONPROXYSTYLE_H + +#include + +/** + * @todo write docs + */ +class SingleClickSelectionProxyStyle : public QProxyStyle +{ +public: + inline int styleHint(StyleHint hint, const QStyleOption *option = nullptr, + const QWidget *widget = nullptr, QStyleHintReturn *returnData = nullptr) const override + { + if (hint == QStyle::SH_ItemView_ActivateItemOnSingleClick) { + return 0; + } + return QProxyStyle::styleHint(hint, option, widget, returnData); + } +}; + +#endif // SINGLECLICKSELECTIONPROXYSTYLE_H diff --git a/src/settings/dolphin_generalsettings.kcfg b/src/settings/dolphin_generalsettings.kcfg index 0267dcb758..ee8192f36a 100644 --- a/src/settings/dolphin_generalsettings.kcfg +++ b/src/settings/dolphin_generalsettings.kcfg @@ -83,6 +83,10 @@ true + + + true + false diff --git a/src/settings/viewpropertiesdialog.cpp b/src/settings/viewpropertiesdialog.cpp index 0f182512c8..87a3acb142 100644 --- a/src/settings/viewpropertiesdialog.cpp +++ b/src/settings/viewpropertiesdialog.cpp @@ -395,7 +395,7 @@ void ViewPropertiesDialog::applyViewProperties() settings->save(); } - m_dolphinView->setMode(m_viewProps->viewMode()); + m_dolphinView->setViewMode(m_viewProps->viewMode()); m_dolphinView->setSortRole(m_viewProps->sortRole()); m_dolphinView->setSortOrder(m_viewProps->sortOrder()); m_dolphinView->setSortFoldersFirst(m_viewProps->sortFoldersFirst()); diff --git a/src/views/dolphinview.cpp b/src/views/dolphinview.cpp index 0e97b85614..56867dd13b 100644 --- a/src/views/dolphinview.cpp +++ b/src/views/dolphinview.cpp @@ -20,6 +20,7 @@ #include "kitemviews/kitemlistselectionmanager.h" #include "kitemviews/private/kitemlistroleeditor.h" #include "settings/viewmodes/viewmodesettings.h" +#include "selectionmode/singleclickselectionproxystyle.h" #include "versioncontrol/versioncontrolobserver.h" #include "viewproperties.h" #include "views/tooltips/tooltipmanager.h" @@ -172,6 +173,7 @@ DolphinView::DolphinView(const QUrl& url, QWidget* parent) : connect(controller, &KItemListController::increaseZoom, this, &DolphinView::slotIncreaseZoom); connect(controller, &KItemListController::decreaseZoom, this, &DolphinView::slotDecreaseZoom); connect(controller, &KItemListController::swipeUp, this, &DolphinView::slotSwipeUp); + connect(controller, &KItemListController::selectionModeRequested, this, &DolphinView::selectionModeRequested); connect(m_model, &KFileItemModel::directoryLoadingStarted, this, &DolphinView::slotDirectoryLoadingStarted); connect(m_model, &KFileItemModel::directoryLoadingCompleted, this, &DolphinView::slotDirectoryLoadingCompleted); @@ -262,7 +264,7 @@ bool DolphinView::isActive() const return m_active; } -void DolphinView::setMode(Mode mode) +void DolphinView::setViewMode(Mode mode) { if (mode != m_mode) { ViewProperties props(viewPropertiesUrl()); @@ -276,11 +278,30 @@ void DolphinView::setMode(Mode mode) } } -DolphinView::Mode DolphinView::mode() const +DolphinView::Mode DolphinView::viewMode() const { return m_mode; } +void DolphinView::setSelectionMode(const bool enabled) +{ + if (enabled) { + m_proxyStyle = std::make_unique(); + setStyle(m_proxyStyle.get()); + m_view->setStyle(m_proxyStyle.get()); + } else { + setStyle(QApplication::style()); + m_view->setStyle(QApplication::style()); + } + m_container->controller()->setSelectionMode(enabled); +} + +bool DolphinView::selectionMode() const +{ + return m_container->controller()->selectionMode(); +} + + void DolphinView::setPreviewsShown(bool show) { if (previewsShown() == show) { diff --git a/src/views/dolphinview.h b/src/views/dolphinview.h index d1ecf74ba7..37af971379 100644 --- a/src/views/dolphinview.h +++ b/src/views/dolphinview.h @@ -23,6 +23,8 @@ #include #include +#include + typedef KIO::FileUndoManager::CommandType CommandType; class QVBoxLayout; class DolphinItemListView; @@ -36,6 +38,7 @@ class ViewProperties; class QLabel; class QGraphicsSceneDragDropEvent; class QHelpEvent; +class QProxyStyle; class QRegularExpression; /** @@ -106,8 +109,11 @@ public: * (GeneralSettings::globalViewProps() returns false), then the * changed view mode will be stored automatically. */ - void setMode(Mode mode); - Mode mode() const; + void setViewMode(Mode mode); + Mode viewMode() const; + + void setSelectionMode(bool enabled); + bool selectionMode() const; /** * Turns on the file preview for the all files of the current directory, @@ -599,6 +605,13 @@ Q_SIGNALS: */ void goForwardRequested(); + /** + * Is emitted when the selection mode is requested for the current view. + * This typically happens on press and hold. + * @see KItemListController::longPress() + */ + void selectionModeRequested(); + /** * Is emitted when the user wants to move the focus to another view. */ @@ -916,6 +929,9 @@ private: QLabel* m_placeholderLabel; QTimer* m_showLoadingPlaceholderTimer; + /// Used for selection mode. @see setSelectionMode() + std::unique_ptr m_proxyStyle; + // For unit tests friend class TestBase; friend class DolphinDetailsViewTest; diff --git a/src/views/dolphinviewactionhandler.cpp b/src/views/dolphinviewactionhandler.cpp index f59daab424..a66d1f6dda 100644 --- a/src/views/dolphinviewactionhandler.cpp +++ b/src/views/dolphinviewactionhandler.cpp @@ -29,6 +29,8 @@ #include #include +#include + DolphinViewActionHandler::DolphinViewActionHandler(KActionCollection* collection, QObject* parent) : QObject(parent), m_actionCollection(collection), @@ -72,6 +74,8 @@ void DolphinViewActionHandler::setCurrentView(DolphinView* view) this, &DolphinViewActionHandler::slotZoomLevelChanged); connect(view, &DolphinView::writeStateChanged, this, &DolphinViewActionHandler::slotWriteStateChanged); + connect(view, &DolphinView::selectionModeRequested, + this, [this]() { Q_EMIT setSelectionMode(true); }); connect(view, &DolphinView::selectionChanged, this, &DolphinViewActionHandler::slotSelectionChanged); slotSelectionChanged(m_currentView->selectedItems()); @@ -415,7 +419,7 @@ QActionGroup* DolphinViewActionHandler::createFileItemRolesActionGroup(const QSt void DolphinViewActionHandler::slotViewModeActionTriggered(QAction* action) { const DolphinView::Mode mode = action->data().value(); - m_currentView->setMode(mode); + m_currentView->setViewMode(mode); QAction* viewModeMenu = m_actionCollection->action(QStringLiteral("view_mode")); viewModeMenu->setIcon(action->icon()); @@ -423,20 +427,34 @@ void DolphinViewActionHandler::slotViewModeActionTriggered(QAction* action) void DolphinViewActionHandler::slotRename() { - Q_EMIT actionBeingHandled(); - m_currentView->renameSelectedItems(); + if (m_currentView->selectedItemsCount() == 0) { + Q_EMIT setSelectionMode(true, SelectionModeBottomBar::Contents::RenameContents); + } else { + Q_EMIT actionBeingHandled(); + m_currentView->renameSelectedItems(); + } } void DolphinViewActionHandler::slotTrashActivated() { - Q_EMIT actionBeingHandled(); - m_currentView->trashSelectedItems(); + if (m_currentView->selectedItemsCount() == 0) { + Q_EMIT setSelectionMode(true, SelectionModeBottomBar::Contents::MoveToTrashContents); + } else { + Q_EMIT actionBeingHandled(); + m_currentView->trashSelectedItems(); + Q_EMIT setSelectionMode(false); + } } void DolphinViewActionHandler::slotDeleteItems() { - Q_EMIT actionBeingHandled(); - m_currentView->deleteSelectedItems(); + if (m_currentView->selectedItemsCount() == 0) { + Q_EMIT setSelectionMode(true, SelectionModeBottomBar::Contents::DeleteContents); + } else { + Q_EMIT actionBeingHandled(); + m_currentView->deleteSelectedItems(); + Q_EMIT setSelectionMode(false); + } } void DolphinViewActionHandler::togglePreview(bool show) @@ -455,7 +473,7 @@ void DolphinViewActionHandler::slotPreviewsShownChanged(bool shown) QString DolphinViewActionHandler::currentViewModeActionName() const { - switch (m_currentView->mode()) { + switch (m_currentView->viewMode()) { case DolphinView::IconsView: return QStringLiteral("icons"); case DolphinView::DetailsView: @@ -735,8 +753,13 @@ void DolphinViewActionHandler::slotAdjustViewProperties() void DolphinViewActionHandler::slotDuplicate() { - Q_EMIT actionBeingHandled(); - m_currentView->duplicateSelectedItems(); + if (m_currentView->selectedItemsCount() == 0) { + Q_EMIT setSelectionMode(true, SelectionModeBottomBar::Contents::DuplicateContents); + } else { + Q_EMIT actionBeingHandled(); + m_currentView->duplicateSelectedItems(); + Q_EMIT setSelectionMode(false); + } } void DolphinViewActionHandler::slotProperties() @@ -758,7 +781,12 @@ void DolphinViewActionHandler::slotProperties() void DolphinViewActionHandler::slotCopyPath() { - m_currentView->copyPathToClipboard(); + if (m_currentView->selectedItemsCount() == 0) { + Q_EMIT setSelectionMode(true, SelectionModeBottomBar::Contents::CopyLocationContents); + } else { + m_currentView->copyPathToClipboard(); + Q_EMIT setSelectionMode(false); + } } void DolphinViewActionHandler::slotSelectionChanged(const KFileItemList& selection) diff --git a/src/views/dolphinviewactionhandler.h b/src/views/dolphinviewactionhandler.h index 6e9b4a432b..f35512a5ff 100644 --- a/src/views/dolphinviewactionhandler.h +++ b/src/views/dolphinviewactionhandler.h @@ -10,6 +10,7 @@ #define DOLPHINVIEWACTIONHANDLER_H #include "dolphin_export.h" +#include "selectionmode/selectionmodebottombar.h" #include "views/dolphinview.h" #include @@ -83,6 +84,9 @@ Q_SIGNALS: */ void createDirectoryTriggered(); + /** Used to request selection mode */ + void setSelectionMode(bool enabled, SelectionModeBottomBar::Contents bottomBarContents = SelectionModeBottomBar::Contents::GeneralContents); + private Q_SLOTS: /** * Emitted when the user requested a change of view mode