From 8e55f2c2409fd6ca9ebc66a6568f4d3bcbef7576 Mon Sep 17 00:00:00 2001 From: Felix Ernst Date: Sun, 24 Apr 2022 13:18:30 +0200 Subject: Better separation of classes Make obvious when actions trigger selection mode. --- src/CMakeLists.txt | 7 +- src/dolphinmainwindow.cpp | 28 +- src/dolphinmainwindow.h | 8 +- src/dolphinpart.cpp | 2 +- src/dolphinviewcontainer.cpp | 20 +- src/dolphinviewcontainer.h | 12 +- src/selectionmode/actiontexthelper.cpp | 41 ++ src/selectionmode/actiontexthelper.h | 71 ++ src/selectionmode/actionwithwidget.cpp | 6 +- src/selectionmode/actionwithwidget.h | 5 + src/selectionmode/backgroundcolorhelper.cpp | 2 + src/selectionmode/backgroundcolorhelper.h | 5 + src/selectionmode/bottombar.cpp | 172 +++++ src/selectionmode/bottombar.h | 128 ++++ src/selectionmode/bottombarcontentscontainer.cpp | 595 +++++++++++++++++ src/selectionmode/bottombarcontentscontainer.h | 154 +++++ src/selectionmode/selectionmodebottombar.cpp | 730 --------------------- src/selectionmode/selectionmodebottombar.h | 198 ------ src/selectionmode/selectionmodetopbar.cpp | 125 ---- src/selectionmode/selectionmodetopbar.h | 66 -- src/selectionmode/singleclickselectionproxystyle.h | 9 +- src/selectionmode/topbar.cpp | 127 ++++ src/selectionmode/topbar.h | 71 ++ src/views/dolphinview.cpp | 2 +- src/views/dolphinviewactionhandler.cpp | 24 +- src/views/dolphinviewactionhandler.h | 11 +- 26 files changed, 1455 insertions(+), 1164 deletions(-) create mode 100644 src/selectionmode/actiontexthelper.cpp create mode 100644 src/selectionmode/actiontexthelper.h create mode 100644 src/selectionmode/bottombar.cpp create mode 100644 src/selectionmode/bottombar.h create mode 100644 src/selectionmode/bottombarcontentscontainer.cpp create mode 100644 src/selectionmode/bottombarcontentscontainer.h delete mode 100644 src/selectionmode/selectionmodebottombar.cpp delete mode 100644 src/selectionmode/selectionmodebottombar.h delete mode 100644 src/selectionmode/selectionmodetopbar.cpp delete mode 100644 src/selectionmode/selectionmodetopbar.h create mode 100644 src/selectionmode/topbar.cpp create mode 100644 src/selectionmode/topbar.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 504e3729c..4b4b5d87b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -87,6 +87,7 @@ target_sources(dolphinprivate PRIVATE settings/viewmodes/viewmodesettings.cpp settings/viewpropertiesdialog.cpp settings/viewpropsprogressinfo.cpp + selectionmode/actiontexthelper.cpp views/dolphinfileitemlistwidget.cpp views/dolphinitemlistview.cpp views/dolphinnewfilemenuobserver.cpp @@ -219,10 +220,12 @@ target_sources(dolphinstatic PRIVATE search/dolphinfacetswidget.cpp search/dolphinquery.cpp search/dolphinsearchbox.cpp + selectionmode/actiontexthelper.cpp selectionmode/actionwithwidget.cpp selectionmode/backgroundcolorhelper.cpp - selectionmode/selectionmodebottombar.cpp - selectionmode/selectionmodetopbar.cpp + selectionmode/bottombar.cpp + selectionmode/bottombarcontentscontainer.cpp + selectionmode/topbar.cpp settings/general/behaviorsettingspage.cpp settings/general/configurepreviewplugindialog.cpp settings/general/confirmationssettingspage.cpp diff --git a/src/dolphinmainwindow.cpp b/src/dolphinmainwindow.cpp index fae30761e..d8d304483 100644 --- a/src/dolphinmainwindow.cpp +++ b/src/dolphinmainwindow.cpp @@ -24,6 +24,7 @@ #include "panels/folders/folderspanel.h" #include "panels/places/placespanel.h" #include "panels/terminal/terminalpanel.h" +#include "selectionmode/actiontexthelper.h" #include "settings/dolphinsettingsdialog.h" #include "statusbar/dolphinstatusbar.h" #include "views/dolphinviewactionhandler.h" @@ -124,7 +125,7 @@ DolphinMainWindow::DolphinMainWindow() : setComponentName(QStringLiteral("dolphin"), QGuiApplication::applicationDisplayName()); setObjectName(QStringLiteral("Dolphin#")); - setStateConfigGroup("State"); + // setStateConfigGroup("State"); connect(&DolphinNewFileMenuObserver::instance(), &DolphinNewFileMenuObserver::errorMessage, this, &DolphinMainWindow::showErrorMessage); @@ -160,9 +161,10 @@ DolphinMainWindow::DolphinMainWindow() : this, &DolphinMainWindow::updateWindowTitle); setCentralWidget(m_tabWidget); + m_actionTextHelper = new SelectionMode::ActionTextHelper(this); setupActions(); - m_actionHandler = new DolphinViewActionHandler(actionCollection(), this); + m_actionHandler = new DolphinViewActionHandler(actionCollection(), m_actionTextHelper, this); connect(m_actionHandler, &DolphinViewActionHandler::actionBeingHandled, this, &DolphinMainWindow::clearStatusBar); connect(m_actionHandler, &DolphinViewActionHandler::createDirectoryTriggered, this, &DolphinMainWindow::createDirectory); connect(m_actionHandler, &DolphinViewActionHandler::setSelectionMode, this, &DolphinMainWindow::slotSetSelectionMode); @@ -715,7 +717,7 @@ void DolphinMainWindow::undo() void DolphinMainWindow::cut() { if (m_activeViewContainer->view()->selectedItems().isEmpty()) { - m_activeViewContainer->setSelectionModeEnabled(true, actionCollection(), SelectionModeBottomBar::Contents::CutContents); + m_activeViewContainer->setSelectionModeEnabled(true, actionCollection(), SelectionMode::BottomBar::Contents::CutContents); } else { m_activeViewContainer->view()->cutSelectedItemsToClipboard(); m_activeViewContainer->setSelectionModeEnabled(false); @@ -725,7 +727,7 @@ void DolphinMainWindow::cut() void DolphinMainWindow::copy() { if (m_activeViewContainer->view()->selectedItems().isEmpty()) { - m_activeViewContainer->setSelectionModeEnabled(true, actionCollection(), SelectionModeBottomBar::Contents::CopyContents); + m_activeViewContainer->setSelectionModeEnabled(true, actionCollection(), SelectionMode::BottomBar::Contents::CopyContents); } else { m_activeViewContainer->view()->copySelectedItemsToClipboard(); m_activeViewContainer->setSelectionModeEnabled(false); @@ -856,7 +858,7 @@ void DolphinMainWindow::slotGoForward(QAction* action) } } -void DolphinMainWindow::slotSetSelectionMode(bool enabled, SelectionModeBottomBar::Contents bottomBarContents) +void DolphinMainWindow::slotSetSelectionMode(bool enabled, SelectionMode::BottomBar::Contents bottomBarContents) { m_activeViewContainer->setSelectionModeEnabled(enabled, actionCollection(), bottomBarContents); } @@ -903,7 +905,7 @@ void DolphinMainWindow::toggleSplitStash() void DolphinMainWindow::copyToInactiveSplitView() { if (m_activeViewContainer->view()->selectedItems().isEmpty()) { - m_activeViewContainer->setSelectionModeEnabled(true, actionCollection(), SelectionModeBottomBar::Contents::CopyToOtherViewContents); + m_activeViewContainer->setSelectionModeEnabled(true, actionCollection(), SelectionMode::BottomBar::Contents::CopyToOtherViewContents); } else { m_tabWidget->copyToInactiveSplitView(); m_activeViewContainer->setSelectionModeEnabled(false); @@ -913,7 +915,7 @@ void DolphinMainWindow::copyToInactiveSplitView() void DolphinMainWindow::moveToInactiveSplitView() { if (m_activeViewContainer->view()->selectedItems().isEmpty()) { - m_activeViewContainer->setSelectionModeEnabled(true, actionCollection(), SelectionModeBottomBar::Contents::MoveToOtherViewContents); + m_activeViewContainer->setSelectionModeEnabled(true, actionCollection(), SelectionMode::BottomBar::Contents::MoveToOtherViewContents); } else { m_tabWidget->moveToInactiveSplitView(); m_activeViewContainer->setSelectionModeEnabled(false); @@ -946,7 +948,7 @@ void DolphinMainWindow::toggleSelectionMode() { const bool checked = !m_activeViewContainer->isSelectionModeEnabled(); - m_activeViewContainer->setSelectionModeEnabled(checked, actionCollection(), SelectionModeBottomBar::Contents::GeneralContents); + m_activeViewContainer->setSelectionModeEnabled(checked, actionCollection(), SelectionMode::BottomBar::Contents::GeneralContents); actionCollection()->action(QStringLiteral("toggle_selection_mode"))->setChecked(checked); } @@ -1584,12 +1586,14 @@ void DolphinMainWindow::setupActions() "next to each other on the keyboard: Ctrl+X, " "Ctrl+C and Ctrl+V."); QAction* cutAction = KStandardAction::cut(this, &DolphinMainWindow::cut, actionCollection()); + m_actionTextHelper->registerTextWhenNothingIsSelected(cutAction, i18nc("@action", "Cut…")); cutAction->setWhatsThis(xi18nc("@info:whatsthis cut", "This copies the items " "in your current selection to the clipboard." "Use the Paste action afterwards to copy them from " "the clipboard to a new location. The items will be removed from their " "initial location.") + cutCopyPastePara); QAction* copyAction = KStandardAction::copy(this, &DolphinMainWindow::copy, actionCollection()); + m_actionTextHelper->registerTextWhenNothingIsSelected(copyAction, i18nc("@action", "Copy…")); copyAction->setWhatsThis(xi18nc("@info:whatsthis copy", "This copies the " "items in your current selection to the clipboard." "Use the Paste action afterwards to copy them " @@ -1606,6 +1610,7 @@ void DolphinMainWindow::setupActions() QAction* copyToOtherViewAction = actionCollection()->addAction(QStringLiteral("copy_to_inactive_split_view")); copyToOtherViewAction->setText(i18nc("@action:inmenu", "Copy to Inactive Split View")); + m_actionTextHelper->registerTextWhenNothingIsSelected(copyToOtherViewAction, i18nc("@action:inmenu", "Copy to Inactive Split View…")); copyToOtherViewAction->setWhatsThis(xi18nc("@info:whatsthis Copy", "This copies the selected items from " "the active view to the inactive split view.")); copyToOtherViewAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy"))); @@ -1615,6 +1620,7 @@ void DolphinMainWindow::setupActions() QAction* moveToOtherViewAction = actionCollection()->addAction(QStringLiteral("move_to_inactive_split_view")); moveToOtherViewAction->setText(i18nc("@action:inmenu", "Move to Inactive Split View")); + m_actionTextHelper->registerTextWhenNothingIsSelected(moveToOtherViewAction, i18nc("@action:inmenu", "Move to Inactive Split View…")); moveToOtherViewAction->setWhatsThis(xi18nc("@info:whatsthis Move", "This moves the selected items from " "the active view to the inactive split view.")); moveToOtherViewAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-cut"))); @@ -2229,7 +2235,7 @@ void DolphinMainWindow::updateFileAndEditActions() 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* addToPlacesAction = col->action(QStringLiteral("add_to_places")); QAction* copyToOtherViewAction = col->action(QStringLiteral("copy_to_inactive_split_view")); QAction* moveToOtherViewAction = col->action(QStringLiteral("move_to_inactive_split_view")); QAction* copyLocation = col->action(QString("copy_location")); @@ -2245,7 +2251,11 @@ void DolphinMainWindow::updateFileAndEditActions() duplicateAction->setEnabled(true); addToPlacesAction->setEnabled(true); copyLocation->setEnabled(true); + // Them triggering selection mode and not directly acting on selected items is signified by adding "…" to their text. + m_actionTextHelper->textsWhenNothingIsSelectedEnabled(true); + } else { + m_actionTextHelper->textsWhenNothingIsSelectedEnabled(false); stateChanged(QStringLiteral("has_selection")); QAction* deleteWithTrashShortcut = col->action(QStringLiteral("delete_shortcut")); // see DolphinViewActionHandler diff --git a/src/dolphinmainwindow.h b/src/dolphinmainwindow.h index 0c9c762d6..fe844ad1b 100644 --- a/src/dolphinmainwindow.h +++ b/src/dolphinmainwindow.h @@ -10,7 +10,7 @@ #define DOLPHIN_MAINWINDOW_H #include "dolphintabwidget.h" -#include "selectionmode/selectionmodebottombar.h" +#include "selectionmode/bottombar.h" #include "config-dolphin.h" #include #include @@ -48,6 +48,9 @@ class TerminalPanel; namespace KIO { class OpenUrlJob; } +namespace SelectionMode { + class ActionTextHelper; +} /** * @short Main window for Dolphin. @@ -315,7 +318,7 @@ private Q_SLOTS: void updatePasteAction(); /** Calls DolphinViewContainer::setSelectionMode() for m_activeViewContainer. */ - void slotSetSelectionMode(bool enabled, SelectionModeBottomBar::Contents bottomBarContents); + void slotSetSelectionMode(bool enabled, SelectionMode::BottomBar::Contents bottomBarContents); /** Selects all items from the active view. */ void selectAll(); @@ -705,6 +708,7 @@ private: DolphinRemoteEncoding* m_remoteEncoding; QPointer m_settingsDialog; DolphinBookmarkHandler* m_bookmarkHandler; + SelectionMode::ActionTextHelper* m_actionTextHelper; // Members for the toolbar menu that is shown when the menubar is hidden: QToolButton* m_controlButton; diff --git a/src/dolphinpart.cpp b/src/dolphinpart.cpp index 8f2279d45..575c30417 100644 --- a/src/dolphinpart.cpp +++ b/src/dolphinpart.cpp @@ -110,7 +110,7 @@ DolphinPart::DolphinPart(QWidget* parentWidget, QObject* parent, connect(m_view, &DolphinView::itemCountChanged, this, &DolphinPart::updateStatusBar); connect(m_view, &DolphinView::selectionChanged, this, &DolphinPart::updateStatusBar); - m_actionHandler = new DolphinViewActionHandler(actionCollection(), this); + m_actionHandler = new DolphinViewActionHandler(actionCollection(), nullptr, this); m_actionHandler->setCurrentView(m_view); connect(m_actionHandler, &DolphinViewActionHandler::createDirectoryTriggered, this, &DolphinPart::createDirectory); diff --git a/src/dolphinviewcontainer.cpp b/src/dolphinviewcontainer.cpp index d45096d0b..c801d095d 100644 --- a/src/dolphinviewcontainer.cpp +++ b/src/dolphinviewcontainer.cpp @@ -12,7 +12,7 @@ #include "filterbar/filterbar.h" #include "global.h" #include "search/dolphinsearchbox.h" -#include "selectionmode/selectionmodetopbar.h" +#include "selectionmode/topbar.h" #include "statusbar/dolphinstatusbar.h" #include "views/viewmodecontroller.h" #include "views/viewproperties.h" @@ -373,7 +373,7 @@ void DolphinViewContainer::disconnectUrlNavigator() m_urlNavigatorConnected = nullptr; } -void DolphinViewContainer::setSelectionModeEnabled(bool enabled, KActionCollection *actionCollection, SelectionModeBottomBar::Contents bottomBarContents) +void DolphinViewContainer::setSelectionModeEnabled(bool enabled, KActionCollection *actionCollection, SelectionMode::BottomBar::Contents bottomBarContents) { const bool wasEnabled = m_view->selectionMode(); m_view->setSelectionMode(enabled); @@ -395,28 +395,28 @@ void DolphinViewContainer::setSelectionModeEnabled(bool enabled, KActionCollecti setSelectionModeEnabled(false); }); - m_selectionModeTopBar = new SelectionModeTopBar(this); // will be created hidden - connect(m_selectionModeTopBar, &SelectionModeTopBar::leaveSelectionModeRequested, this, [this]() { + m_selectionModeTopBar = new SelectionMode::TopBar(this); // will be created hidden + connect(m_selectionModeTopBar, &SelectionMode::TopBar::leaveSelectionModeRequested, this, [this]() { setSelectionModeEnabled(false); }); m_topLayout->addWidget(m_selectionModeTopBar, positionFor.selectionModeTopBar, 0); } if (!m_selectionModeBottomBar) { - m_selectionModeBottomBar = new SelectionModeBottomBar(actionCollection, this); + m_selectionModeBottomBar = new SelectionMode::BottomBar(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) { + connect(m_selectionModeBottomBar, &SelectionMode::BottomBar::error, this, [this](const QString &errorMessage) { showErrorMessage(errorMessage); }); - connect(m_selectionModeBottomBar, &SelectionModeBottomBar::leaveSelectionModeRequested, this, [this]() { + connect(m_selectionModeBottomBar, &SelectionMode::BottomBar::leaveSelectionModeRequested, this, [this]() { setSelectionModeEnabled(false); }); m_topLayout->addWidget(m_selectionModeBottomBar, positionFor.selectionModeBottomBar, 0); } m_selectionModeBottomBar->resetContents(bottomBarContents); - if (bottomBarContents == SelectionModeBottomBar::GeneralContents) { + if (bottomBarContents == SelectionMode::BottomBar::GeneralContents) { m_selectionModeBottomBar->slotSelectionChanged(m_view->selectedItems(), m_view->url()); } @@ -433,12 +433,12 @@ bool DolphinViewContainer::isSelectionModeEnabled() const Q_ASSERT((!isEnabled // We can't assert that the bars are invisible only because the selection mode is disabled because the hide animation might still be playing. && (!m_selectionModeBottomBar || !m_selectionModeBottomBar->isEnabled() || - !m_selectionModeBottomBar->isVisible() || m_selectionModeBottomBar->contents() == SelectionModeBottomBar::PasteContents)) + !m_selectionModeBottomBar->isVisible() || m_selectionModeBottomBar->contents() == SelectionMode::BottomBar::PasteContents)) || ( isEnabled && m_selectionModeTopBar && m_selectionModeTopBar->isVisible() // The bottom bar is either visible or was hidden because it has nothing to show in GeneralContents mode e.g. because no items are selected. && m_selectionModeBottomBar - && (m_selectionModeBottomBar->isVisible() || m_selectionModeBottomBar->contents() == SelectionModeBottomBar::GeneralContents))); + && (m_selectionModeBottomBar->isVisible() || m_selectionModeBottomBar->contents() == SelectionMode::BottomBar::GeneralContents))); return isEnabled; } diff --git a/src/dolphinviewcontainer.h b/src/dolphinviewcontainer.h index a0936efd3..9d5cec11f 100644 --- a/src/dolphinviewcontainer.h +++ b/src/dolphinviewcontainer.h @@ -9,7 +9,7 @@ #include "config-dolphin.h" #include "dolphinurlnavigator.h" -#include "selectionmode/selectionmodebottombar.h" +#include "selectionmode/bottombar.h" #include "views/dolphinview.h" #include @@ -33,7 +33,9 @@ class QGridLayout; class QUrl; class DolphinSearchBox; class DolphinStatusBar; -class SelectionModeTopBar; +namespace SelectionMode { + class TopBar; +} /** * @short Represents a view for the directory content @@ -135,7 +137,7 @@ public: */ void disconnectUrlNavigator(); - void setSelectionModeEnabled(bool enabled, KActionCollection *actionCollection = nullptr, SelectionModeBottomBar::Contents bottomBarContents = SelectionModeBottomBar::Contents::GeneralContents); + void setSelectionModeEnabled(bool enabled, KActionCollection *actionCollection = nullptr, SelectionMode::BottomBar::Contents bottomBarContents = SelectionMode::BottomBar::Contents::GeneralContents); bool isSelectionModeEnabled() const; /** @@ -429,14 +431,14 @@ private: KMessageWidget* m_messageWidget; /// A bar shown at the top of the view to signify that selection mode is currently active. - SelectionModeTopBar *m_selectionModeTopBar; + SelectionMode::TopBar *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; + SelectionMode::BottomBar *m_selectionModeBottomBar; DolphinStatusBar* m_statusBar; QTimer* m_statusBarTimer; // Triggers a delayed update diff --git a/src/selectionmode/actiontexthelper.cpp b/src/selectionmode/actiontexthelper.cpp new file mode 100644 index 000000000..3eb868a8c --- /dev/null +++ b/src/selectionmode/actiontexthelper.cpp @@ -0,0 +1,41 @@ +/* + 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 "actiontexthelper.h" + +using namespace SelectionMode; + +ActionTextHelper::ActionTextHelper(QObject *parent) : + QObject(parent) +{ } + +void ActionTextHelper::registerTextWhenNothingIsSelected(QAction *action, QString registeredText) +{ + Q_CHECK_PTR(action); + m_registeredActionTextChanges.emplace_back(action, registeredText, TextWhenNothingIsSelected); +} + +void ActionTextHelper::textsWhenNothingIsSelectedEnabled(bool enabled) +{ + for (auto i = m_registeredActionTextChanges.begin(); i != m_registeredActionTextChanges.end(); ++i) { + if (!i->action) { + i = m_registeredActionTextChanges.erase(i); + continue; + } + if (enabled && i->textStateOfRegisteredText == TextWhenNothingIsSelected) { + QString textWhenSomethingIsSelected = i->action->text(); + i->action->setText(i->registeredText); + i->registeredText = textWhenSomethingIsSelected; + i->textStateOfRegisteredText = TextWhenSomethingIsSelected; + } else if (!enabled && i->textStateOfRegisteredText == TextWhenSomethingIsSelected) { + QString textWhenNothingIsSelected = i->action->text(); + i->action->setText(i->registeredText); + i->registeredText = textWhenNothingIsSelected; + i->textStateOfRegisteredText = TextWhenNothingIsSelected; + } + } +} diff --git a/src/selectionmode/actiontexthelper.h b/src/selectionmode/actiontexthelper.h new file mode 100644 index 000000000..8f7501fa4 --- /dev/null +++ b/src/selectionmode/actiontexthelper.h @@ -0,0 +1,71 @@ +/* + 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 ACTIONTEXTHELPER_H +#define ACTIONTEXTHELPER_H + +#include +#include +#include + +namespace SelectionMode +{ + +/** + * @brief Helps changing the texts of actions depending on the current selection. + */ +class ActionTextHelper : QObject +{ +public: + explicit ActionTextHelper(QObject *parent); + + /** + * Changes the text of \a action to \a text whenever textsWhenNothingIsSelectedEnabled(true) is called. + * The texts can be changed back by calling textsWhenNothingIsSelectedEnabled(false) is called. + * @see textsWhenNothingIsSelectedEnabled() + */ + void registerTextWhenNothingIsSelected(QAction *action, QString registeredText); + + /** + * Changes all texts that were registered previously using registerTextWhenNothingIsSelected() to those + * registered texts if called with \a enabled == true. Otherwise resets the texts to the original one. + */ + void textsWhenNothingIsSelectedEnabled(bool enabled); + +private: + enum TextState { + TextWhenNothingIsSelected, + TextWhenSomethingIsSelected + }; + + /** + * Utility struct to allow switching back and forth between registered actions showing their + * distinct texts for when no items are selected or when items are selected. + * An example is "Copy" or "Copy…". The latter one is used when nothing is selected and signifies + * that it will trigger SelectionMode so items can be selected and then copied. + */ + struct RegisteredActionTextChange { + QPointer action; + QString registeredText; + TextState textStateOfRegisteredText; + + RegisteredActionTextChange(QAction *action, QString registeredText, TextState state) : + action{action}, + registeredText{registeredText}, + textStateOfRegisteredText{state} + { }; + }; + + /** + * @see RegisteredActionTextChange + */ + std::vector m_registeredActionTextChanges; +}; + +} + +#endif // ACTIONTEXTHELPER_H diff --git a/src/selectionmode/actionwithwidget.cpp b/src/selectionmode/actionwithwidget.cpp index e9823af7c..82ce045db 100644 --- a/src/selectionmode/actionwithwidget.cpp +++ b/src/selectionmode/actionwithwidget.cpp @@ -12,6 +12,8 @@ #include #include +using namespace SelectionMode; + ActionWithWidget::ActionWithWidget(QAction *action) : m_action{action} { } @@ -40,7 +42,7 @@ QWidget *ActionWithWidget::newWidget(QWidget *parent) return m_widget; } -QAbstractButton *newButtonForAction(QAction *action, QWidget *parent) +QAbstractButton *SelectionMode::newButtonForAction(QAction *action, QWidget *parent) { Q_CHECK_PTR(action); Q_ASSERT(!action->isSeparator()); @@ -62,7 +64,7 @@ QAbstractButton *newButtonForAction(QAction *action, QWidget *parent) return toolButton; } -void copyActionDataToButton(QAbstractButton *button, QAction *action) +void SelectionMode::copyActionDataToButton(QAbstractButton *button, QAction *action) { button->setText(action->text()); button->setIcon(action->icon()); diff --git a/src/selectionmode/actionwithwidget.h b/src/selectionmode/actionwithwidget.h index 62dad0fc1..cf7b8bc35 100644 --- a/src/selectionmode/actionwithwidget.h +++ b/src/selectionmode/actionwithwidget.h @@ -14,6 +14,9 @@ class QAbstractButton; +namespace SelectionMode +{ + /** * @brief Small wrapper/helper class that contains an action and its widget. * @@ -83,4 +86,6 @@ QAbstractButton *newButtonForAction(QAction *action, QWidget *parent); */ void copyActionDataToButton(QAbstractButton *button, QAction *action); +} + #endif // ACTIONWITHWIDGET_H diff --git a/src/selectionmode/backgroundcolorhelper.cpp b/src/selectionmode/backgroundcolorhelper.cpp index ca110e762..4477d0f2c 100644 --- a/src/selectionmode/backgroundcolorhelper.cpp +++ b/src/selectionmode/backgroundcolorhelper.cpp @@ -14,6 +14,8 @@ #include #include +using namespace SelectionMode; + BackgroundColorHelper *BackgroundColorHelper::instance() { if (!s_instance) { diff --git a/src/selectionmode/backgroundcolorhelper.h b/src/selectionmode/backgroundcolorhelper.h index 013d33685..8d2730fcf 100644 --- a/src/selectionmode/backgroundcolorhelper.h +++ b/src/selectionmode/backgroundcolorhelper.h @@ -15,6 +15,9 @@ class QWidget; +namespace SelectionMode +{ + /** * @brief A Singleton class for managing the colors of selection mode widgets. */ @@ -42,4 +45,6 @@ private: static BackgroundColorHelper *s_instance; }; +} + #endif // BACKGROUNDCOLORHELPER_H diff --git a/src/selectionmode/bottombar.cpp b/src/selectionmode/bottombar.cpp new file mode 100644 index 000000000..999b24ae4 --- /dev/null +++ b/src/selectionmode/bottombar.cpp @@ -0,0 +1,172 @@ +/* + 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 "bottombar.h" + +#include "bottombarcontentscontainer.h" +#include "backgroundcolorhelper.h" +#include "global.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +using namespace SelectionMode; + +BottomBar::BottomBar(KActionCollection *actionCollection, QWidget *parent) : + QWidget{parent} +{ + // 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.) + m_scrollArea = new QScrollArea(this); + fillParentLayout->addWidget(m_scrollArea); + m_scrollArea->setFrameShape(QFrame::NoFrame); + m_scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + m_scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + m_scrollArea->setWidgetResizable(true); + + m_contentsContainer = new BottomBarContentsContainer(actionCollection, m_scrollArea); + m_scrollArea->setWidget(m_contentsContainer); + m_contentsContainer->installEventFilter(this); // Adjusts the height of this bar to the height of the contentsContainer + connect(m_contentsContainer, &BottomBarContentsContainer::error, this, &BottomBar::error); + connect(m_contentsContainer, &BottomBarContentsContainer::barVisibilityChangeRequested, this, [this](bool visible){ + if (!m_allowedToBeVisible && visible) { + return; + } + setVisibleInternal(visible, WithAnimation); + }); + connect(m_contentsContainer, &BottomBarContentsContainer::leaveSelectionModeRequested, this, &BottomBar::leaveSelectionModeRequested); + + BackgroundColorHelper::instance()->controlBackgroundColor(this); +} + +void BottomBar::setVisible(bool visible, Animated animated) +{ + m_allowedToBeVisible = visible; + setVisibleInternal(visible, animated); +} + +void BottomBar::setVisibleInternal(bool visible, Animated animated) +{ + Q_ASSERT_X(animated == WithAnimation, "SelectionModeBottomBar::setVisible", "This wasn't implemented."); + if (!visible && contents() == PasteContents) { + return; // The bar with PasteContents should not be hidden or users might not know how to paste what they just copied. + // Set contents to anything else to circumvent this prevention mechanism. + } + if (visible && !m_contentsContainer->hasSomethingToShow()) { + return; // There is nothing on the bar that we want to show. We keep it invisible and only show it when the selection or the contents change. + } + + setEnabled(visible); + if (m_heightAnimation) { + m_heightAnimation->stop(); // deletes because of QAbstractAnimation::DeleteWhenStopped. + } + m_heightAnimation = new QPropertyAnimation(this, "maximumHeight"); + m_heightAnimation->setDuration(2 * + style()->styleHint(QStyle::SH_Widget_Animation_Duration, nullptr, this) * + GlobalConfig::animationDurationFactor()); + + m_heightAnimation->setStartValue(height()); + m_heightAnimation->setEasingCurve(QEasingCurve::OutCubic); + if (visible) { + show(); + m_heightAnimation->setEndValue(sizeHint().height()); + connect(m_heightAnimation, &QAbstractAnimation::finished, + this, [this](){ setMaximumHeight(sizeHint().height()); }); + } else { + m_heightAnimation->setEndValue(0); + connect(m_heightAnimation, &QAbstractAnimation::finished, + this, &QWidget::hide); + } + + m_heightAnimation->start(QAbstractAnimation::DeleteWhenStopped); +} + +QSize BottomBar::sizeHint() const +{ + return QSize{1, m_contentsContainer->sizeHint().height()}; + // 1 as width because this widget should never be the reason the DolphinViewContainer is made wider. +} + +void BottomBar::slotSelectionChanged(const KFileItemList &selection, const QUrl &baseUrl) +{ + m_contentsContainer->slotSelectionChanged(selection, baseUrl); +} + +void BottomBar::slotSplitTabDisabled() +{ + switch (contents()) { + case CopyToOtherViewContents: + case MoveToOtherViewContents: + Q_EMIT leaveSelectionModeRequested(); + default: + return; + } +} + +void BottomBar::resetContents(BottomBar::Contents contents) +{ + m_contentsContainer->resetContents(contents); + + if (m_allowedToBeVisible) { + setVisibleInternal(true, WithAnimation); + } +} + +BottomBar::Contents BottomBar::contents() const +{ + return m_contentsContainer->contents(); +} + +bool BottomBar::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]() { + // The necessary height might have changed because of the added/removed child so we change the height manually. + if (isVisibleTo(parentWidget()) && isEnabled() && (!m_heightAnimation || m_heightAnimation->state() != QAbstractAnimation::Running)) { + setMaximumHeight(sizeHint().height()); + } + }); + // Fall through. + default: + return false; + } +} + +void BottomBar::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_contentsContainer->updateForNewWidth(); + + return QWidget::resizeEvent(resizeEvent); +} diff --git a/src/selectionmode/bottombar.h b/src/selectionmode/bottombar.h new file mode 100644 index 000000000..ff23b6699 --- /dev/null +++ b/src/selectionmode/bottombar.h @@ -0,0 +1,128 @@ +/* + 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 BOTTOMBAR_H +#define BOTTOMBAR_H + +#include "global.h" + +#include +#include +#include +#include + +#include + +class KActionCollection; +class KFileItemList; +class QAction; +class QPushButton; +class QResizeEvent; +class QScrollArea; +class QUrl; + +namespace SelectionMode +{ + class BottomBarContentsContainer; + +/** + * A bar 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. + * + * The visible contents of the bar are managed in BottomBarContentsContainer. This class serves as a wrapper around it. + */ +class BottomBar : 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 + }; + + /** + * @param actionCollection the collection this bar retrieves its actions from + * @param parent the parent widget. Typically a DolphinViewContainer + */ + explicit BottomBar(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. + * + * This bar might also not show itself when setVisible(true), when context menu actions are supposed to be shown + * for the selected items but no items have been selected yet. In that case it will only show itself once items were selected. + * @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); + Contents contents() const; + + /** @returns a width of 1 to make sure that this bar never causes side panels to shrink. */ + 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; + + /** Adapts the way the contents of this bar are displayed based on the available width. */ + void resizeEvent(QResizeEvent *resizeEvent) override; + +private: + /** + * Identical to SelectionModeBottomBar::setVisible() but doesn't change m_allowedToBeVisible. + * @see SelectionModeBottomBar::setVisible() + * @see m_allowedToBeVisible + */ + void setVisibleInternal(bool visible, Animated animated); + +private: + /** The only direct child widget of this bar. */ + QScrollArea *m_scrollArea; + /** The only direct grandchild of this bar. */ + BottomBarContentsContainer *m_contentsContainer; + + /** Remembers if this bar was setVisible(true) or setVisible(false) the last time. + * This is necessary because this bar might have been setVisible(true) but there is no reason to show the bar currently so it was kept hidden. + * @see SelectionModeBottomBar::setVisible() */ + bool m_allowedToBeVisible = false; + /// @see SelectionModeBottomBar::setVisible() + QPointer m_heightAnimation; +}; + +} + +#endif // BOTTOMBAR_H diff --git a/src/selectionmode/bottombarcontentscontainer.cpp b/src/selectionmode/bottombarcontentscontainer.cpp new file mode 100644 index 000000000..04b52a60f --- /dev/null +++ b/src/selectionmode/bottombarcontentscontainer.cpp @@ -0,0 +1,595 @@ +/* + 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 "bottombarcontentscontainer.h" + +#include "dolphin_generalsettings.h" +#include "dolphincontextmenu.h" +#include "dolphinmainwindow.h" +#include "dolphinremoveaction.h" +#include "kitemviews/kfileitemlisttostring.h" + +#include + +#include +#include +#include +#include +#include +#include + +#include + +using namespace SelectionMode; + +BottomBarContentsContainer::BottomBarContentsContainer(KActionCollection *actionCollection, QWidget *parent) : + QWidget{parent}, + m_actionCollection{actionCollection} +{ + // We will mostly interact with m_layout when changing the contents and not care about the other internal hierarchy. + m_layout = new QHBoxLayout(this); +} + +void BottomBarContentsContainer::resetContents(BottomBar::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 BottomBar::CopyContents: + return addCopyContents(); + case BottomBar::CopyLocationContents: + return addCopyLocationContents(); + case BottomBar::CopyToOtherViewContents: + return addCopyToOtherViewContents(); + case BottomBar::CutContents: + return addCutContents(); + case BottomBar::DeleteContents: + return addDeleteContents(); + case BottomBar::DuplicateContents: + return addDuplicateContents(); + case BottomBar::GeneralContents: + return addGeneralContents(); + case BottomBar::PasteContents: + return addPasteContents(); + case BottomBar::MoveToOtherViewContents: + return addMoveToOtherViewContents(); + case BottomBar::MoveToTrashContents: + return addMoveToTrashContents(); + case BottomBar::RenameContents: + return addRenameContents(); + } +} + +void BottomBarContentsContainer::updateForNewWidth() +{ + if (m_contents == BottomBar::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. + auto overflowMenu = m_overflowButton->menu(); + if (overflowMenu->actions().isEmpty()) { + overflowMenu->addAction(i->action()); + } else { + overflowMenu->insertAction(overflowMenu->actions().at(0), i->action()); + } + 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. + auto overflowMenu = m_overflowButton->menu(); + overflowMenu->removeAction(i->action()); + 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(); +} + +void BottomBarContentsContainer::slotSelectionChanged(const KFileItemList &selection, const QUrl &baseUrl) +{ + if (m_contents == BottomBar::GeneralContents) { + auto contextActions = contextActionsFor(selection, baseUrl); + m_generalBarActions.clear(); + if (contextActions.empty()) { + Q_ASSERT(qobject_cast(parentWidget()->parentWidget()->parentWidget())); + if (isVisibleTo(parentWidget()->parentWidget()->parentWidget()->parentWidget())) { // is the bar visible + Q_EMIT barVisibilityChangeRequested(false); + } + } else { + for (auto i = contextActions.begin(); i != contextActions.end(); ++i) { + m_generalBarActions.emplace_back(ActionWithWidget{*i}); + } + resetContents(BottomBar::GeneralContents); + + Q_EMIT barVisibilityChangeRequested(true); + } + } + updateMainActionButton(selection); +} + +void BottomBarContentsContainer::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, &BottomBarContentsContainer::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 = BottomBar::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(BottomBar::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 BottomBarContentsContainer::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, &BottomBarContentsContainer::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 BottomBarContentsContainer::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, &BottomBarContentsContainer::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 BottomBarContentsContainer::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, &BottomBarContentsContainer::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 = BottomBar::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(BottomBar::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 BottomBarContentsContainer::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, &BottomBarContentsContainer::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 BottomBarContentsContainer::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, &BottomBarContentsContainer::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 BottomBarContentsContainer::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()) { + break; // The bar is too full already. We keep it invisible. + } else { + 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 BottomBarContentsContainer::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, &BottomBarContentsContainer::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 BottomBarContentsContainer::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, &BottomBarContentsContainer::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 BottomBarContentsContainer::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 = BottomBar::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 BottomBarContentsContainer::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, &BottomBarContentsContainer::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 BottomBarContentsContainer::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 BottomBarContentsContainer::contextActionsFor(const KFileItemList& selectedItems, const QUrl& baseUrl) +{ + if (selectedItems.isEmpty()) { + // There are no contextual actions to show for these items. + // We might even want to hide this bar in this case. To make this clear, we reset m_internalContextMenu. + m_internalContextMenu.release()->deleteLater(); + return std::vector{}; + } + + std::vector contextActions; + + // We always want to show the most important actions at the beginning + 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))); + + // 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, &BottomBarContentsContainer::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))); + + // 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 BottomBarContentsContainer::unusedSpace() const +{ + int sumOfPreferredWidths = m_layout->contentsMargins().left() + m_layout->contentsMargins().right(); + if (m_overflowButton) { + sumOfPreferredWidths += m_overflowButton->sizeHint().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. + } + sumOfPreferredWidths += m_layout->itemAt(i)->sizeHint().width() + m_layout->spacing(); + } + Q_ASSERT(qobject_cast(parentWidget()->parentWidget()->parentWidget())); + const int totalBarWidth = parentWidget()->parentWidget()->parentWidget()->width(); + return totalBarWidth - 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 BottomBarContentsContainer::updateExplanatoryLabelVisibility() +{ + if (!m_explanatoryLabel) { + return; + } + 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 BottomBarContentsContainer::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 BottomBar::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 BottomBar::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 BottomBar::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 BottomBar::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 BottomBar::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 BottomBar::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 BottomBar::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/bottombarcontentscontainer.h b/src/selectionmode/bottombarcontentscontainer.h new file mode 100644 index 000000000..6255ff987 --- /dev/null +++ b/src/selectionmode/bottombarcontentscontainer.h @@ -0,0 +1,154 @@ +/* + 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 BOTTOMBARCONTENTSCONTAINER_H +#define BOTTOMBARCONTENTSCONTAINER_H + +#include "actionwithwidget.h" +#include "bottombar.h" + +#include +#include +#include + +class DolphinContextMenu; +class KActionCollection; +class KFileItemActions; +class KFileItemList; +class QHBoxLayout; +class QLabel; +class QUrl; + +namespace SelectionMode +{ + +/** + * @brief An internal widget of BottomBar that controls the visible contents/widgets on it. + * + * This class should only be interacted with from the BottomBar class. + * @see BottomBar + */ +class BottomBarContentsContainer : public QWidget +{ + Q_OBJECT + +public: + /** + * @param actionCollection the collection where the actions for the contents are retrieved from + * @param parent the parent widget. Typically a ScrollView within the BottomBar + */ + explicit BottomBarContentsContainer(KActionCollection *actionCollection, QWidget *parent); + + void resetContents(BottomBar::Contents contents); + inline BottomBar::Contents contents() const + { + return m_contents; + }; + + inline bool hasSomethingToShow() { + return contents() != BottomBar::GeneralContents || m_internalContextMenu; + } + + void updateForNewWidth(); + +public Q_SLOTS: + void slotSelectionChanged(const KFileItemList &selection, const QUrl &baseUrl); + +Q_SIGNALS: + /** + * Forwards the errors from the KFileItemAction::error() used for contextual actions. + */ + void error(const QString &errorMessage); + + /** + * Sometimes the contents see no reason to be visible and request the bar to be hidden instead which emits this signal. + * This can later change e.g. because the user selected items. Then this signal is used to request showing of the bar. + */ + void barVisibilityChangeRequested(bool visible); + + void leaveSelectionModeRequested(); + +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. + BottomBar::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; + + /// The info label used for some of the BarContents. Is hidden for narrow widths. + QPointer m_explanatoryLabel; +}; + +} + +#endif // BOTTOMBARCONTENTSCONTAINER_H diff --git a/src/selectionmode/selectionmodebottombar.cpp b/src/selectionmode/selectionmodebottombar.cpp deleted file mode 100644 index d10a8581b..000000000 --- a/src/selectionmode/selectionmodebottombar.cpp +++ /dev/null @@ -1,730 +0,0 @@ -/* - 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 - -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) -{ - m_allowedToBeVisible = visible; - setVisibleInternal(visible, animated); -} - -void SelectionModeBottomBar::setVisibleInternal(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 (visible && m_contents == GeneralContents && !m_internalContextMenu) { - return; // There is nothing on the bar that we want to show. We keep it invisible and only show it when the selection or the contents change. - } - - setEnabled(visible); - if (m_heightAnimation) { - m_heightAnimation->stop(); // deletes because of QAbstractAnimation::DeleteWhenStopped. - } - m_heightAnimation = new QPropertyAnimation(this, "maximumHeight"); - m_heightAnimation->setDuration(2 * - style()->styleHint(QStyle::SH_Widget_Animation_Duration, nullptr, this) * - GlobalConfig::animationDurationFactor()); - - m_heightAnimation->setStartValue(height()); - m_heightAnimation->setEasingCurve(QEasingCurve::OutCubic); - if (visible) { - show(); - m_heightAnimation->setEndValue(sizeHint().height()); - connect(m_heightAnimation, &QAbstractAnimation::finished, - this, [this](){ setMaximumHeight(sizeHint().height()); }); - } else { - m_heightAnimation->setEndValue(0); - connect(m_heightAnimation, &QAbstractAnimation::finished, - this, &QWidget::hide); - } - - m_heightAnimation->start(QAbstractAnimation::DeleteWhenStopped); -} - -QSize SelectionModeBottomBar::sizeHint() const -{ - return QSize{1, m_layout->parentWidget()->sizeHint().height()}; - // 1 as width because this widget should never be the reason the DolphinViewContainer is made wider. -} - -void SelectionModeBottomBar::slotSelectionChanged(const KFileItemList &selection, const QUrl &baseUrl) -{ - if (m_contents == GeneralContents) { - auto contextActions = contextActionsFor(selection, baseUrl); - m_generalBarActions.clear(); - if (contextActions.empty()) { - if (isVisibleTo(parentWidget())) { - setVisibleInternal(false, WithAnimation); - } - } else { - for (auto i = contextActions.begin(); i != contextActions.end(); ++i) { - m_generalBarActions.emplace_back(ActionWithWidget{*i}); - } - resetContents(GeneralContents); - - if (m_allowedToBeVisible) { - setVisibleInternal(true, WithAnimation); - } - } - } - 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: - addCopyContents(); - break; - case CopyLocationContents: - addCopyLocationContents(); - break; - case CopyToOtherViewContents: - addCopyToOtherViewContents(); - break; - case CutContents: - addCutContents(); - break; - case DeleteContents: - addDeleteContents(); - break; - case DuplicateContents: - addDuplicateContents(); - break; - case GeneralContents: - addGeneralContents(); - break; - case PasteContents: - addPasteContents(); - break; - case MoveToOtherViewContents: - addMoveToOtherViewContents(); - break; - case MoveToTrashContents: - addMoveToTrashContents(); - break; - case RenameContents: - return addRenameContents(); - } - - if (m_allowedToBeVisible) { - setVisibleInternal(true, WithAnimation); - } -} - -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]() { - // The necessary height might have changed because of the added/removed child so we change the height manually. - if (isVisibleTo(parentWidget()) && isEnabled() && (!m_heightAnimation || m_heightAnimation->state() != QAbstractAnimation::Running)) { - 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. - auto overflowMenu = m_overflowButton->menu(); - if (overflowMenu->actions().isEmpty()) { - overflowMenu->addAction(i->action()); - } else { - overflowMenu->insertAction(overflowMenu->actions().at(0), i->action()); - } - 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. - auto overflowMenu = m_overflowButton->menu(); - overflowMenu->removeAction(i->action()); - 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()) { - break; // The bar is too full already. We keep it invisible. - } else { - 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) -{ - if (selectedItems.isEmpty()) { - // There are no contextual actions to show for these items. - // We might even want to hide this bar in this case. To make this clear, we reset m_internalContextMenu. - m_internalContextMenu.release()->deleteLater(); - return std::vector{}; - } - - std::vector contextActions; - - // We always want to show the most important actions at the beginning - 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))); - - // 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))); - - // 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(); - } - 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. - } - sumOfPreferredWidths += m_layout->itemAt(i)->sizeHint().width() + m_layout->spacing(); - } - 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; - } - 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 deleted file mode 100644 index 89fd3c3a3..000000000 --- a/src/selectionmode/selectionmodebottombar.h +++ /dev/null @@ -1,198 +0,0 @@ -/* - 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. - * - * This bar might also not show itself when setVisible(true), when context menu actions are supposed to be shown - * for the selected items but no items have been selected yet. In that case it will only show itself once items were selected. - * @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; - }; - - /** @returns a width of 1 to make sure that this bar never causes side panels to shrink. */ - 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; - - /** Adapts the way the contents of this bar are displayed based on the available width. */ - void resizeEvent(QResizeEvent *resizeEvent) override; - -private: - /** - * Identical to SelectionModeBottomBar::setVisible() but doesn't change m_allowedToBeVisible. - * @see SelectionModeBottomBar::setVisible() - * @see m_allowedToBeVisible - */ - void setVisibleInternal(bool visible, Animated animated); - - 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; - - /** Remembers if this bar was setVisible(true) or setVisible(false) the last time. - * This is necessary because this bar might have been setVisible(true) but there is no reason to show the bar currently so it was kept hidden. - * @see SelectionModeBottomBar::setVisible() */ - bool m_allowedToBeVisible = false; - /// @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 deleted file mode 100644 index 83aa8e849..000000000 --- a/src/selectionmode/selectionmodetopbar.cpp +++ /dev/null @@ -1,125 +0,0 @@ -/* - 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 - -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->stop(); // deletes because of QAbstractAnimation::DeleteWhenStopped. - } - m_heightAnimation = new QPropertyAnimation(this, "maximumHeight"); - m_heightAnimation->setDuration(2 * - style()->styleHint(QStyle::SH_Widget_Animation_Duration, nullptr, this) * - GlobalConfig::animationDurationFactor()); - - m_heightAnimation->setStartValue(height()); - m_heightAnimation->setEasingCurve(QEasingCurve::OutCubic); - if (visible) { - show(); - m_heightAnimation->setEndValue(m_preferredHeight); - } else { - m_heightAnimation->setEndValue(0); - connect(m_heightAnimation, &QAbstractAnimation::finished, - this, &QWidget::hide); - } - - m_heightAnimation->start(QAbstractAnimation::DeleteWhenStopped); -} - -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 deleted file mode 100644 index eb26a5c26..000000000 --- a/src/selectionmode/selectionmodetopbar.h +++ /dev/null @@ -1,66 +0,0 @@ -/* - 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 index 65117c56c..ece46cce6 100644 --- a/src/selectionmode/singleclickselectionproxystyle.h +++ b/src/selectionmode/singleclickselectionproxystyle.h @@ -10,8 +10,13 @@ #include +namespace SelectionMode +{ + /** - * @todo write docs + * @brief A simple proxy style to temporarily make single click select and not activate + * + * @see QProxyStyle */ class SingleClickSelectionProxyStyle : public QProxyStyle { @@ -26,4 +31,6 @@ public: } }; +} + #endif // SINGLECLICKSELECTIONPROXYSTYLE_H diff --git a/src/selectionmode/topbar.cpp b/src/selectionmode/topbar.cpp new file mode 100644 index 000000000..d783c76ca --- /dev/null +++ b/src/selectionmode/topbar.cpp @@ -0,0 +1,127 @@ +/* + 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 "topbar.h" + +#include "backgroundcolorhelper.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace SelectionMode; + +TopBar::TopBar(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, &TopBar::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 TopBar::setVisible(bool visible, Animated animated) +{ + Q_ASSERT_X(animated == WithAnimation, "SelectionModeTopBar::setVisible", "This wasn't implemented."); + + if (m_heightAnimation) { + m_heightAnimation->stop(); // deletes because of QAbstractAnimation::DeleteWhenStopped. + } + m_heightAnimation = new QPropertyAnimation(this, "maximumHeight"); + m_heightAnimation->setDuration(2 * + style()->styleHint(QStyle::SH_Widget_Animation_Duration, nullptr, this) * + GlobalConfig::animationDurationFactor()); + + m_heightAnimation->setStartValue(height()); + m_heightAnimation->setEasingCurve(QEasingCurve::OutCubic); + if (visible) { + show(); + m_heightAnimation->setEndValue(m_preferredHeight); + } else { + m_heightAnimation->setEndValue(0); + connect(m_heightAnimation, &QAbstractAnimation::finished, + this, &QWidget::hide); + } + + m_heightAnimation->start(QAbstractAnimation::DeleteWhenStopped); +} + +void TopBar::resizeEvent(QResizeEvent */* resizeEvent */) +{ + updateLabelString(); +} + +void TopBar::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/topbar.h b/src/selectionmode/topbar.h new file mode 100644 index 000000000..e0cd34935 --- /dev/null +++ b/src/selectionmode/topbar.h @@ -0,0 +1,71 @@ +/* + 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; + +namespace SelectionMode +{ + +/** + * @brief A bar appearing at the top of the view when in selection mode to make users aware of the selection mode state of the application. + */ +class TopBar : public QWidget +{ + Q_OBJECT + +public: + TopBar(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/views/dolphinview.cpp b/src/views/dolphinview.cpp index 56867dd13..f235efffe 100644 --- a/src/views/dolphinview.cpp +++ b/src/views/dolphinview.cpp @@ -286,7 +286,7 @@ DolphinView::Mode DolphinView::viewMode() const void DolphinView::setSelectionMode(const bool enabled) { if (enabled) { - m_proxyStyle = std::make_unique(); + m_proxyStyle = std::make_unique(); setStyle(m_proxyStyle.get()); m_view->setStyle(m_proxyStyle.get()); } else { diff --git a/src/views/dolphinviewactionhandler.cpp b/src/views/dolphinviewactionhandler.cpp index b2e45a5f6..ef30e91c9 100644 --- a/src/views/dolphinviewactionhandler.cpp +++ b/src/views/dolphinviewactionhandler.cpp @@ -10,6 +10,7 @@ #include "dolphindebug.h" #include "kitemviews/kfileitemlisttostring.h" #include "kitemviews/kfileitemmodel.h" +#include "selectionmode/actiontexthelper.h" #include "settings/viewpropertiesdialog.h" #include "views/zoomlevelinfo.h" #include "kconfig_version.h" @@ -29,7 +30,7 @@ #include #include -DolphinViewActionHandler::DolphinViewActionHandler(KActionCollection* collection, QObject* parent) : +DolphinViewActionHandler::DolphinViewActionHandler(KActionCollection* collection, SelectionMode::ActionTextHelper* actionTextHelper, QObject* parent) : QObject(parent), m_actionCollection(collection), m_currentView(nullptr), @@ -37,7 +38,7 @@ DolphinViewActionHandler::DolphinViewActionHandler(KActionCollection* collection m_visibleRoles() { Q_ASSERT(m_actionCollection); - createActions(); + createActions(actionTextHelper); } void DolphinViewActionHandler::setCurrentView(DolphinView* view) @@ -84,7 +85,7 @@ DolphinView* DolphinViewActionHandler::currentView() return m_currentView; } -void DolphinViewActionHandler::createActions() +void DolphinViewActionHandler::createActions(SelectionMode::ActionTextHelper *actionTextHelper) { // This action doesn't appear in the GUI, it's for the shortcut only. // KNewFileMenu takes care of the GUI stuff. @@ -164,6 +165,13 @@ void DolphinViewActionHandler::createActions() m_actionCollection->setDefaultShortcuts(copyPathAction, {Qt::CTRL | Qt::ALT | Qt::Key_C}); connect(copyPathAction, &QAction::triggered, this, &DolphinViewActionHandler::slotCopyPath); + if (actionTextHelper) { + actionTextHelper->registerTextWhenNothingIsSelected(trashAction, i18nc("@action:inmenu File", "Move to Trash…")); + actionTextHelper->registerTextWhenNothingIsSelected(deleteAction, i18nc("@action:inmenu File", "Delete…")); + actionTextHelper->registerTextWhenNothingIsSelected(duplicateAction, i18nc("@action:inmenu File", "Duplicate Here…")); + actionTextHelper->registerTextWhenNothingIsSelected(copyPathAction, i18nc("@action:incontextmenu", "Copy Location…")); + } + // This menu makes sure that users who don't know how to open a context menu and haven't // figured out how to enable the menu bar can still perform basic file manipulation. // This only works if they know how to select a file. @@ -426,7 +434,7 @@ void DolphinViewActionHandler::slotViewModeActionTriggered(QAction* action) void DolphinViewActionHandler::slotRename() { if (m_currentView->selectedItemsCount() == 0) { - Q_EMIT setSelectionMode(true, SelectionModeBottomBar::Contents::RenameContents); + Q_EMIT setSelectionMode(true, SelectionMode::BottomBar::Contents::RenameContents); } else { Q_EMIT actionBeingHandled(); m_currentView->renameSelectedItems(); @@ -436,7 +444,7 @@ void DolphinViewActionHandler::slotRename() void DolphinViewActionHandler::slotTrashActivated() { if (m_currentView->selectedItemsCount() == 0) { - Q_EMIT setSelectionMode(true, SelectionModeBottomBar::Contents::MoveToTrashContents); + Q_EMIT setSelectionMode(true, SelectionMode::BottomBar::Contents::MoveToTrashContents); } else { Q_EMIT actionBeingHandled(); m_currentView->trashSelectedItems(); @@ -447,7 +455,7 @@ void DolphinViewActionHandler::slotTrashActivated() void DolphinViewActionHandler::slotDeleteItems() { if (m_currentView->selectedItemsCount() == 0) { - Q_EMIT setSelectionMode(true, SelectionModeBottomBar::Contents::DeleteContents); + Q_EMIT setSelectionMode(true, SelectionMode::BottomBar::Contents::DeleteContents); } else { Q_EMIT actionBeingHandled(); m_currentView->deleteSelectedItems(); @@ -752,7 +760,7 @@ void DolphinViewActionHandler::slotAdjustViewProperties() void DolphinViewActionHandler::slotDuplicate() { if (m_currentView->selectedItemsCount() == 0) { - Q_EMIT setSelectionMode(true, SelectionModeBottomBar::Contents::DuplicateContents); + Q_EMIT setSelectionMode(true, SelectionMode::BottomBar::Contents::DuplicateContents); } else { Q_EMIT actionBeingHandled(); m_currentView->duplicateSelectedItems(); @@ -780,7 +788,7 @@ void DolphinViewActionHandler::slotProperties() void DolphinViewActionHandler::slotCopyPath() { if (m_currentView->selectedItemsCount() == 0) { - Q_EMIT setSelectionMode(true, SelectionModeBottomBar::Contents::CopyLocationContents); + Q_EMIT setSelectionMode(true, SelectionMode::BottomBar::Contents::CopyLocationContents); } else { m_currentView->copyPathToClipboard(); Q_EMIT setSelectionMode(false); diff --git a/src/views/dolphinviewactionhandler.h b/src/views/dolphinviewactionhandler.h index f35512a5f..5c7475fdb 100644 --- a/src/views/dolphinviewactionhandler.h +++ b/src/views/dolphinviewactionhandler.h @@ -10,7 +10,7 @@ #define DOLPHINVIEWACTIONHANDLER_H #include "dolphin_export.h" -#include "selectionmode/selectionmodebottombar.h" +#include "selectionmode/bottombar.h" #include "views/dolphinview.h" #include @@ -21,6 +21,9 @@ class QActionGroup; class DolphinView; class KActionCollection; class KFileItemList; +namespace SelectionMode { + class ActionTextHelper; +} /** * @short Handles all actions for DolphinView @@ -41,7 +44,7 @@ class DOLPHIN_EXPORT DolphinViewActionHandler : public QObject Q_OBJECT public: - explicit DolphinViewActionHandler(KActionCollection* collection, QObject* parent); + explicit DolphinViewActionHandler(KActionCollection* collection, SelectionMode::ActionTextHelper* actionTextHelper, QObject* parent); /** * Sets the view that this action handler should work on. @@ -85,7 +88,7 @@ Q_SIGNALS: void createDirectoryTriggered(); /** Used to request selection mode */ - void setSelectionMode(bool enabled, SelectionModeBottomBar::Contents bottomBarContents = SelectionModeBottomBar::Contents::GeneralContents); + void setSelectionMode(bool enabled, SelectionMode::BottomBar::Contents bottomBarContents = SelectionMode::BottomBar::Contents::GeneralContents); private Q_SLOTS: /** @@ -238,7 +241,7 @@ private: * Create all the actions. * This is called only once (by the constructor) */ - void createActions(); + void createActions(SelectionMode::ActionTextHelper *actionTextHelper); /** * Creates an action-group out of all roles from KFileItemModel. -- cgit v1.3