Add Selection Mode

The selection mode action is a checkable toggle action named
"Select Files and Folders" which has "Space" as the default
shortcut.

In selection mode a bottom bar with contextual actions is shown.
These should mostly mirror the actions which are available through
the right-click context menu aka DolphinContextMenu.

Resizing of the window might make a overflow button appear in the
bottom selection mode bar.

This commit makes press and hold in the view activate selection
mode. This behaviour is not triggered if the press and hold is
used to either start a rubberband selection or a drag operation
within a short time. The length of the short timeframe is defined
by a QStyleHint. This is currently not implemented in touch
because I can't test it.

Mix the selection mode bars' background colors using a nice
combination of colors from the current color scheme

BUG: 427202
This commit is contained in:
Felix Ernst 2021-09-12 15:33:39 +02:00
parent 9dbe481377
commit 3b7c05b385
26 changed files with 1821 additions and 80 deletions

View file

@ -209,6 +209,7 @@ target_sources(dolphinstatic PRIVATE
dolphinurlnavigatorscontroller.cpp
trash/dolphintrash.cpp
filterbar/filterbar.cpp
kitemviews/kfileitemlisttostring.cpp
panels/places/placespanel.cpp
panels/panel.cpp
panels/folders/foldersitemlistwidget.cpp
@ -218,6 +219,10 @@ target_sources(dolphinstatic PRIVATE
search/dolphinfacetswidget.cpp
search/dolphinquery.cpp
search/dolphinsearchbox.cpp
selectionmode/actionwithwidget.cpp
selectionmode/backgroundcolorhelper.cpp
selectionmode/selectionmodebottombar.cpp
selectionmode/selectionmodetopbar.cpp
settings/general/behaviorsettingspage.cpp
settings/general/configurepreviewplugindialog.cpp
settings/general/confirmationssettingspage.cpp

View file

@ -83,6 +83,8 @@
#include <QToolButton>
#include <QWhatsThisClickedEvent>
#include <iostream>
namespace {
// Used for GeneralSettings::version() to determine whether
// an updated version of Dolphin is running, so as to migrate
@ -124,7 +126,7 @@ DolphinMainWindow::DolphinMainWindow() :
setComponentName(QStringLiteral("dolphin"), QGuiApplication::applicationDisplayName());
setObjectName(QStringLiteral("Dolphin#"));
setStateConfigGroup("State");
//setStateConfigGroup("State"); // TODO: Don't leave this as a comment.
connect(&DolphinNewFileMenuObserver::instance(), &DolphinNewFileMenuObserver::errorMessage,
this, &DolphinMainWindow::showErrorMessage);
@ -165,6 +167,7 @@ DolphinMainWindow::DolphinMainWindow() :
m_actionHandler = new DolphinViewActionHandler(actionCollection(), this);
connect(m_actionHandler, &DolphinViewActionHandler::actionBeingHandled, this, &DolphinMainWindow::clearStatusBar);
connect(m_actionHandler, &DolphinViewActionHandler::createDirectoryTriggered, this, &DolphinMainWindow::createDirectory);
connect(m_actionHandler, &DolphinViewActionHandler::setSelectionMode, this, &DolphinMainWindow::slotSetSelectionMode);
m_remoteEncoding = new DolphinRemoteEncoding(this, m_actionHandler);
connect(this, &DolphinMainWindow::urlChanged,
@ -192,6 +195,7 @@ DolphinMainWindow::DolphinMainWindow() :
auto hamburgerMenu = static_cast<KHamburgerMenu *>(actionCollection()->action(
KStandardAction::name(KStandardAction::HamburgerMenu)));
hamburgerMenu->icon();
hamburgerMenu->setMenuBar(menuBar());
hamburgerMenu->setShowMenuBarAction(showMenuBarAction);
connect(hamburgerMenu, &KHamburgerMenu::aboutToShowMenu,
@ -713,12 +717,22 @@ void DolphinMainWindow::undo()
void DolphinMainWindow::cut()
{
m_activeViewContainer->view()->cutSelectedItemsToClipboard();
if (m_activeViewContainer->view()->selectedItems().isEmpty()) {
m_activeViewContainer->setSelectionModeEnabled(true, actionCollection(), SelectionModeBottomBar::Contents::CutContents);
} else {
m_activeViewContainer->view()->cutSelectedItemsToClipboard();
m_activeViewContainer->setSelectionModeEnabled(false);
}
}
void DolphinMainWindow::copy()
{
m_activeViewContainer->view()->copySelectedItemsToClipboard();
if (m_activeViewContainer->view()->selectedItems().isEmpty()) {
m_activeViewContainer->setSelectionModeEnabled(true, actionCollection(), SelectionModeBottomBar::Contents::CopyContents);
} else {
m_activeViewContainer->view()->copySelectedItemsToClipboard();
m_activeViewContainer->setSelectionModeEnabled(false);
}
}
void DolphinMainWindow::paste()
@ -845,6 +859,11 @@ void DolphinMainWindow::slotGoForward(QAction* action)
}
}
void DolphinMainWindow::slotSetSelectionMode(bool enabled, SelectionModeBottomBar::Contents bottomBarContents)
{
m_activeViewContainer->setSelectionModeEnabled(enabled, actionCollection(), bottomBarContents);
}
void DolphinMainWindow::selectAll()
{
clearStatusBar();
@ -884,6 +903,26 @@ void DolphinMainWindow::toggleSplitStash()
tabPage->setSplitViewEnabled(true, WithAnimation, QUrl("stash:/"));
}
void DolphinMainWindow::copyToInactiveSplitView()
{
if (m_activeViewContainer->view()->selectedItems().isEmpty()) {
m_activeViewContainer->setSelectionModeEnabled(true, actionCollection(), SelectionModeBottomBar::Contents::CopyToOtherViewContents);
} else {
m_tabWidget->copyToInactiveSplitView();
m_activeViewContainer->setSelectionModeEnabled(false);
}
}
void DolphinMainWindow::moveToInactiveSplitView()
{
if (m_activeViewContainer->view()->selectedItems().isEmpty()) {
m_activeViewContainer->setSelectionModeEnabled(true, actionCollection(), SelectionModeBottomBar::Contents::MoveToOtherViewContents);
} else {
m_tabWidget->moveToInactiveSplitView();
m_activeViewContainer->setSelectionModeEnabled(false);
}
}
void DolphinMainWindow::reloadView()
{
clearStatusBar();
@ -906,6 +945,14 @@ void DolphinMainWindow::disableStopAction()
actionCollection()->action(QStringLiteral("stop"))->setEnabled(false);
}
void DolphinMainWindow::toggleSelectionMode()
{
const bool checked = !m_activeViewContainer->isSelectionModeEnabled();
m_activeViewContainer->setSelectionModeEnabled(checked, actionCollection(), SelectionModeBottomBar::Contents::GeneralContents);
actionCollection()->action(QStringLiteral("toggle_selection_mode"))->setChecked(checked);
}
void DolphinMainWindow::showFilterBar()
{
m_activeViewContainer->setFilterBarVisible(true);
@ -1283,6 +1330,11 @@ void DolphinMainWindow::updateHamburgerMenu()
menu->addAction(ac->action(QStringLiteral("go_forward")));
menu->addMenu(m_newFileMenu->menu());
if (!toolBar()->isVisible()
|| !toolbarActions.contains(ac->action(QStringLiteral("toggle_selection_mode_with_popup")))
) {
menu->addAction(ac->action(QStringLiteral("toggle_selection_mode")));
}
menu->addAction(ac->action(QStringLiteral("basic_actions")));
menu->addAction(ac->action(KStandardAction::name(KStandardAction::Undo)));
if (!toolBar()->isVisible()
@ -1562,7 +1614,7 @@ void DolphinMainWindow::setupActions()
copyToOtherViewAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy")));
copyToOtherViewAction->setIconText(i18nc("@action:inmenu Edit", "Copy to Inactive Split View"));
actionCollection()->setDefaultShortcut(copyToOtherViewAction, Qt::SHIFT | Qt::Key_F5 );
connect(copyToOtherViewAction, &QAction::triggered, m_tabWidget, &DolphinTabWidget::copyToInactiveSplitView);
connect(copyToOtherViewAction, &QAction::triggered, this, &DolphinMainWindow::copyToInactiveSplitView);
QAction* moveToOtherViewAction = actionCollection()->addAction(QStringLiteral("move_to_inactive_split_view"));
moveToOtherViewAction->setText(i18nc("@action:inmenu", "Move to Inactive Split View"));
@ -1571,7 +1623,7 @@ void DolphinMainWindow::setupActions()
moveToOtherViewAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-cut")));
moveToOtherViewAction->setIconText(i18nc("@action:inmenu Edit", "Move to Inactive Split View"));
actionCollection()->setDefaultShortcut(moveToOtherViewAction, Qt::SHIFT | Qt::Key_F6 );
connect(moveToOtherViewAction, &QAction::triggered, m_tabWidget, &DolphinTabWidget::moveToInactiveSplitView);
connect(moveToOtherViewAction, &QAction::triggered, this, &DolphinMainWindow::moveToInactiveSplitView);
QAction* showFilterBar = actionCollection()->addAction(QStringLiteral("show_filter_bar"));
showFilterBar->setText(i18nc("@action:inmenu Tools", "Filter..."));
@ -1617,6 +1669,33 @@ void DolphinMainWindow::setupActions()
toggleSearchAction->setWhatsThis(searchAction->whatsThis());
toggleSearchAction->setCheckable(true);
QAction *toggleSelectionModeAction = actionCollection()->addAction(QStringLiteral("toggle_selection_mode"));
// i18n: This action toggles a selection mode.
toggleSelectionModeAction->setText(i18nc("@action:inmenu", "Select Files and Folders"));
// i18n: Opens a selection mode for selecting files/folders and later selecting an action that acts on them.
// So in a way "Select" here is used to mean both "Select files" and also "Select what to do" but mostly the first.
// The text is kept so unspecific because it will be shown on the toolbar where space is at a premium.
toggleSelectionModeAction->setIconText(i18nc("@action:intoolbar", "Select"));
toggleSelectionModeAction->setWhatsThis(xi18nc("@info:whatsthis", "<para>This application doesn't know which files or folders should be acted on, "
"unless they are <emphasis>selected</emphasis> first. Press this to toggle the <emphasis>Selection Mode</emphasis> which makes selecting and deselecting as "
"easy as pressing an item once.</para><para>While in this mode, a quick access bar at the bottom shows all the available actions for the current "
"selection of items.</para>"));
toggleSelectionModeAction->setIcon(QIcon::fromTheme(QStringLiteral("quickwizard")));
toggleSelectionModeAction->setCheckable(true);
actionCollection()->setDefaultShortcut(toggleSelectionModeAction, Qt::Key_Space );
connect(toggleSelectionModeAction, &QAction::triggered, this, &DolphinMainWindow::toggleSelectionMode);
// A special version of the toggleSelectionModeAction for the toolbar that also contains a menu
// with the selectAllAction and invertSelectionAction.
auto *toggleSelectionModeToolBarAction = new KToolBarPopupAction(toggleSelectionModeAction->icon(), toggleSelectionModeAction->iconText(), actionCollection());
toggleSelectionModeToolBarAction->setToolTip(toggleSelectionModeAction->text());
toggleSelectionModeToolBarAction->setWhatsThis(toggleSelectionModeAction->whatsThis());
actionCollection()->addAction(QStringLiteral("toggle_selection_mode_with_popup"), toggleSelectionModeToolBarAction);
toggleSelectionModeToolBarAction->setCheckable(true);
toggleSelectionModeToolBarAction->setPopupMode(QToolButton::DelayedPopup);
connect(toggleSelectionModeToolBarAction, &QAction::triggered, toggleSelectionModeAction, &QAction::trigger);
connect(toggleSelectionModeAction, &QAction::toggled, toggleSelectionModeToolBarAction, &QAction::setChecked);
QAction* selectAllAction = KStandardAction::selectAll(this, &DolphinMainWindow::selectAll, actionCollection());
selectAllAction->setWhatsThis(xi18nc("@info:whatsthis", "This selects all "
"files and folders in the current location."));
@ -1629,6 +1708,11 @@ void DolphinMainWindow::setupActions()
actionCollection()->setDefaultShortcut(invertSelection, Qt::CTRL | Qt::SHIFT | Qt::Key_A);
connect(invertSelection, &QAction::triggered, this, &DolphinMainWindow::invertSelection);
QMenu *toggleSelectionModeActionMenu = new QMenu(this);
toggleSelectionModeActionMenu->addAction(selectAllAction);
toggleSelectionModeActionMenu->addAction(invertSelection);
toggleSelectionModeToolBarAction->setMenu(toggleSelectionModeActionMenu);
// setup 'View' menu
// (note that most of it is set up in DolphinViewActionHandler)
@ -2143,6 +2227,11 @@ void DolphinMainWindow::updateFileAndEditActions()
const KActionCollection* col = actionCollection();
KFileItemListProperties capabilitiesSource(list);
QAction* renameAction = col->action(KStandardAction::name(KStandardAction::RenameFile));
QAction* moveToTrashAction = col->action(KStandardAction::name(KStandardAction::MoveToTrash));
QAction* deleteAction = col->action(KStandardAction::name(KStandardAction::DeleteFile));
QAction* cutAction = col->action(KStandardAction::name(KStandardAction::Cut));
QAction* duplicateAction = col->action(QStringLiteral("duplicate")); // see DolphinViewActionHandler
QAction* addToPlacesAction = col->action(QStringLiteral("add_to_places"));
QAction* copyToOtherViewAction = col->action(QStringLiteral("copy_to_inactive_split_view"));
QAction* moveToOtherViewAction = col->action(QStringLiteral("move_to_inactive_split_view"));
@ -2151,20 +2240,19 @@ void DolphinMainWindow::updateFileAndEditActions()
if (list.isEmpty()) {
stateChanged(QStringLiteral("has_no_selection"));
// All actions that need a selection to function can be enabled because they should trigger selection mode.
renameAction->setEnabled(true);
moveToTrashAction->setEnabled(true);
deleteAction->setEnabled(true);
cutAction->setEnabled(true);
duplicateAction->setEnabled(true);
addToPlacesAction->setEnabled(true);
copyToOtherViewAction->setEnabled(false);
moveToOtherViewAction->setEnabled(false);
copyLocation->setEnabled(false);
copyLocation->setEnabled(true);
} else {
stateChanged(QStringLiteral("has_selection"));
QAction* renameAction = col->action(KStandardAction::name(KStandardAction::RenameFile));
QAction* moveToTrashAction = col->action(KStandardAction::name(KStandardAction::MoveToTrash));
QAction* deleteAction = col->action(KStandardAction::name(KStandardAction::DeleteFile));
QAction* cutAction = col->action(KStandardAction::name(KStandardAction::Cut));
QAction* deleteWithTrashShortcut = col->action(QStringLiteral("delete_shortcut")); // see DolphinViewActionHandler
QAction* showTarget = col->action(QStringLiteral("show_target"));
QAction* duplicateAction = col->action(QStringLiteral("duplicate")); // see DolphinViewActionHandler
if (list.length() == 1 && list.first().isDir()) {
addToPlacesAction->setEnabled(true);
@ -2172,23 +2260,6 @@ void DolphinMainWindow::updateFileAndEditActions()
addToPlacesAction->setEnabled(false);
}
if (m_tabWidget->currentTabPage()->splitViewEnabled()) {
DolphinTabPage* tabPage = m_tabWidget->currentTabPage();
KFileItem capabilitiesDestination;
if (tabPage->primaryViewActive()) {
capabilitiesDestination = tabPage->secondaryViewContainer()->url();
} else {
capabilitiesDestination = tabPage->primaryViewContainer()->url();
}
copyToOtherViewAction->setEnabled(capabilitiesDestination.isWritable());
moveToOtherViewAction->setEnabled(capabilitiesSource.supportsMoving() && capabilitiesDestination.isWritable());
} else {
copyToOtherViewAction->setEnabled(false);
moveToOtherViewAction->setEnabled(false);
}
const bool enableMoveToTrash = capabilitiesSource.isLocal() && capabilitiesSource.supportsMoving();
renameAction->setEnabled(capabilitiesSource.supportsMoving());
@ -2200,12 +2271,36 @@ void DolphinMainWindow::updateFileAndEditActions()
showTarget->setEnabled(list.length() == 1 && list.at(0).isLink());
duplicateAction->setEnabled(capabilitiesSource.supportsWriting());
}
if (m_tabWidget->currentTabPage()->splitViewEnabled()) {
DolphinTabPage* tabPage = m_tabWidget->currentTabPage();
KFileItem capabilitiesDestination;
if (tabPage->primaryViewActive()) {
capabilitiesDestination = tabPage->secondaryViewContainer()->url();
} else {
capabilitiesDestination = tabPage->primaryViewContainer()->url();
}
copyToOtherViewAction->setEnabled(capabilitiesDestination.isWritable());
moveToOtherViewAction->setEnabled((list.isEmpty() || capabilitiesSource.supportsMoving()) && capabilitiesDestination.isWritable());
} else {
copyToOtherViewAction->setEnabled(false);
moveToOtherViewAction->setEnabled(false);
}
}
void DolphinMainWindow::updateViewActions()
{
m_actionHandler->updateViewActions();
QAction *toggleSelectionModeAction = actionCollection()->action(QStringLiteral("toggle_selection_mode"));
disconnect(nullptr, &DolphinViewContainer::selectionModeChanged,
toggleSelectionModeAction, &QAction::setChecked);
toggleSelectionModeAction->setChecked(m_activeViewContainer->isSelectionModeEnabled());
connect(m_activeViewContainer, &DolphinViewContainer::selectionModeChanged,
toggleSelectionModeAction, &QAction::setChecked);
QAction* toggleFilterBarAction = actionCollection()->action(QStringLiteral("toggle_filter"));
toggleFilterBarAction->setChecked(m_activeViewContainer->isFilterBarVisible());

View file

@ -10,6 +10,7 @@
#define DOLPHIN_MAINWINDOW_H
#include "dolphintabwidget.h"
#include "selectionmode/selectionmodebottombar.h"
#include "config-dolphin.h"
#include <KFileItemActions>
#include <kio/fileundomanager.h>
@ -313,6 +314,9 @@ private Q_SLOTS:
*/
void updatePasteAction();
/** Calls DolphinViewContainer::setSelectionMode() for m_activeViewContainer. */
void slotSetSelectionMode(bool enabled, SelectionModeBottomBar::Contents bottomBarContents);
/** Selects all items from the active view. */
void selectAll();
@ -333,6 +337,12 @@ private Q_SLOTS:
/** Dedicated action to open the stash:/ ioslave in split view. */
void toggleSplitStash();
/** Copies all selected items to the inactive view. */
void copyToInactiveSplitView();
/** Moves all selected items to the inactive view. */
void moveToInactiveSplitView();
/** Reloads the currently active view. */
void reloadView();
@ -342,6 +352,8 @@ private Q_SLOTS:
void enableStopAction();
void disableStopAction();
void toggleSelectionMode();
void showFilterBar();
void toggleFilterBar();

View file

@ -9,7 +9,6 @@
#include "dolphin_generalsettings.h"
#include "dolphinviewcontainer.h"
#include "global.h"
#include <QVariantAnimation>
#include <QGridLayout>
@ -152,6 +151,8 @@ void DolphinTabPage::setSplitViewEnabled(bool enabled, Animated animated, const
view->setDisabled(true);
startExpandViewAnimation(m_primaryViewContainer);
}
m_primaryViewContainer->slotSplitTabDisabled();
}
}
}

View file

@ -8,6 +8,8 @@
#ifndef DOLPHIN_TAB_PAGE_H
#define DOLPHIN_TAB_PAGE_H
#include "global.h"
#include <QPointer>
#include <QUrl>
#include <QWidget>
@ -19,11 +21,6 @@ class QVariantAnimation;
class KFileItemList;
class DolphinTabPageSplitter;
enum Animated {
WithAnimation,
WithoutAnimation
};
class DolphinTabPage : public QWidget
{
Q_OBJECT

View file

@ -1,6 +1,6 @@
<?xml version="1.0"?>
<!DOCTYPE gui SYSTEM "kpartgui.dtd">
<gui name="dolphin" version="36">
<gui name="dolphin" version="38">
<MenuBar>
<Menu name="file">
<Action name="new_menu" />
@ -31,6 +31,7 @@
<Action name="show_filter_bar" />
<Action name="edit_find" />
<Separator />
<Action name="toggle_selection_mode" />
<Action name="copy_to_inactive_split_view" />
<Action name="move_to_inactive_split_view" />
<Action name="edit_select_all" />
@ -80,7 +81,6 @@
<Action name="edit_undo" />
<Action name="edit_redo" />
<Action name="edit_cut" />
<Action name="edit_copy" />
<Action name="renamefile" />
<Action name="movetotrash" />
<Action name="deletefile" />
@ -92,23 +92,11 @@
</State>
<State name="has_selection" >
<enable>
<Action name="edit_cut" />
<Action name="edit_copy" />
<Action name="renamefile" />
<Action name="duplicate" />
<Action name="movetotrash" />
<Action name="deletefile" />
<Action name="invert_selection" />
</enable>
</State>
<State name="has_no_selection" >
<disable>
<Action name="edit_cut" />
<Action name="edit_copy" />
<Action name="renamefile" />
<Action name="duplicate" />
<Action name="movetotrash" />
<Action name="deletefile" />
<Action name="delete_shortcut" />
<Action name="invert_selection" />
</disable>
@ -125,6 +113,7 @@
<Action name="split_view" />
<Action name="split_stash" />
<Action name="toggle_search" />
<Action name="toggle_selection_mode_with_popup" />
<Action name="hamburger_menu" />
</ToolBar>
<ActionProperties scheme="Default">

View file

@ -12,11 +12,13 @@
#include "filterbar/filterbar.h"
#include "global.h"
#include "search/dolphinsearchbox.h"
#include "selectionmode/selectionmodetopbar.h"
#include "statusbar/dolphinstatusbar.h"
#include "views/viewmodecontroller.h"
#include "views/viewproperties.h"
#include "dolphin_detailsmodesettings.h"
#include <KActionCollection>
#if HAVE_KACTIVITIES
#include <KActivities/ResourceInstance>
#endif
@ -32,14 +34,28 @@
#include <KUrlComboBox>
#include <QDropEvent>
#include <QGridLayout>
#include <QGuiApplication>
#include <QLoggingCategory>
#include <QMimeData>
#include <QTimer>
#include <QUrl>
#include <QVBoxLayout>
#include <QDesktopServices>
#include <iostream>
// An overview of the widgets contained by this ViewContainer
struct LayoutStructure {
int searchBox = 0;
int messageWidget = 1;
int selectionModeTopBar = 2;
int view = 3;
int selectionModeBottomBar = 4;
int filterBar = 5;
int statusBar = 6;
};
constexpr LayoutStructure positionFor;
DolphinViewContainer::DolphinViewContainer(const QUrl& url, QWidget* parent) :
QWidget(parent),
m_topLayout(nullptr),
@ -48,8 +64,10 @@ DolphinViewContainer::DolphinViewContainer(const QUrl& url, QWidget* parent) :
m_searchBox(nullptr),
m_searchModeEnabled(false),
m_messageWidget(nullptr),
m_selectionModeTopBar{nullptr},
m_view(nullptr),
m_filterBar(nullptr),
m_selectionModeBottomBar{nullptr},
m_statusBar(nullptr),
m_statusBarTimer(nullptr),
m_statusBarTimestamp(),
@ -60,7 +78,7 @@ DolphinViewContainer::DolphinViewContainer(const QUrl& url, QWidget* parent) :
{
hide();
m_topLayout = new QVBoxLayout(this);
m_topLayout = new QGridLayout(this);
m_topLayout->setSpacing(0);
m_topLayout->setContentsMargins(0, 0, 0, 0);
@ -187,16 +205,16 @@ DolphinViewContainer::DolphinViewContainer(const QUrl& url, QWidget* parent) :
connect(undoManager, &KIO::FileUndoManager::jobRecordingFinished,
this, &DolphinViewContainer::delayedStatusBarUpdate);
m_topLayout->addWidget(m_searchBox);
m_topLayout->addWidget(m_messageWidget);
m_topLayout->addWidget(m_view);
m_topLayout->addWidget(m_filterBar);
m_topLayout->addWidget(m_statusBar);
m_topLayout->addWidget(m_searchBox, positionFor.searchBox, 0);
m_topLayout->addWidget(m_messageWidget, positionFor.messageWidget, 0);
m_topLayout->addWidget(m_view, positionFor.view, 0);
m_topLayout->addWidget(m_filterBar, positionFor.filterBar, 0);
m_topLayout->addWidget(m_statusBar, positionFor.statusBar, 0);
setSearchModeEnabled(isSearchUrl(url));
connect(DetailsModeSettings::self(), &KCoreConfigSkeleton::configChanged, this, [=]() {
if (view()->mode() == DolphinView::Mode::DetailsView) {
if (view()->viewMode() == DolphinView::Mode::DetailsView) {
view()->reload();
}
});
@ -357,6 +375,78 @@ void DolphinViewContainer::disconnectUrlNavigator()
m_urlNavigatorConnected = nullptr;
}
void DolphinViewContainer::setSelectionModeEnabled(bool enabled, KActionCollection *actionCollection, SelectionModeBottomBar::Contents bottomBarContents)
{
std::cout << "DolphinViewContainer::setSelectionModeEnabled(" << enabled << ", " << bottomBarContents << ")\n";
const bool wasEnabled = m_view->selectionMode();
m_view->setSelectionMode(enabled);
if (!enabled) {
Q_CHECK_PTR(m_selectionModeTopBar); // there is no point in disabling selectionMode when it wasn't even enabled once.
Q_CHECK_PTR(m_selectionModeBottomBar);
m_selectionModeTopBar->setVisible(false, WithAnimation);
m_selectionModeBottomBar->setVisible(false, WithAnimation);
if (wasEnabled) {
Q_EMIT selectionModeChanged(false);
}
return;
}
if (!m_selectionModeTopBar) {
// Changing the location will disable selection mode.
connect(m_urlNavigator.get(), &DolphinUrlNavigator::urlChanged, this, [this]() {
setSelectionModeEnabled(false);
});
m_selectionModeTopBar = new SelectionModeTopBar(this); // will be created hidden
connect(m_selectionModeTopBar, &SelectionModeTopBar::leaveSelectionModeRequested, this, [this]() {
setSelectionModeEnabled(false);
});
m_topLayout->addWidget(m_selectionModeTopBar, positionFor.selectionModeTopBar, 0);
}
if (!m_selectionModeBottomBar) {
m_selectionModeBottomBar = new SelectionModeBottomBar(actionCollection, this);
connect(m_view, &DolphinView::selectionChanged, this, [this](const KFileItemList &selection) {
m_selectionModeBottomBar->slotSelectionChanged(selection, m_view->url());
});
connect(m_selectionModeBottomBar, &SelectionModeBottomBar::error, this, [this](const QString &errorMessage) {
showErrorMessage(errorMessage);
});
connect(m_selectionModeBottomBar, &SelectionModeBottomBar::leaveSelectionModeRequested, this, [this]() {
setSelectionModeEnabled(false);
});
m_topLayout->addWidget(m_selectionModeBottomBar, positionFor.selectionModeBottomBar, 0);
}
m_selectionModeBottomBar->resetContents(bottomBarContents);
if (bottomBarContents == SelectionModeBottomBar::GeneralContents) {
m_selectionModeBottomBar->slotSelectionChanged(m_view->selectedItems(), m_view->url());
}
if (!wasEnabled) {
m_selectionModeTopBar->setVisible(true, WithAnimation);
m_selectionModeBottomBar->setVisible(true, WithAnimation);
Q_EMIT selectionModeChanged(true);
}
}
bool DolphinViewContainer::isSelectionModeEnabled() const
{
const bool isEnabled = m_view->selectionMode();
Q_ASSERT( !isEnabled // We cannot assert the invisibility of the bars because of the hide animation.
|| ( isEnabled && m_selectionModeTopBar && m_selectionModeTopBar->isVisible() && m_selectionModeBottomBar && m_selectionModeBottomBar->isVisible()));
return isEnabled;
}
void DolphinViewContainer::slotSplitTabDisabled()
{
if (m_selectionModeBottomBar) {
m_selectionModeBottomBar->slotSplitTabDisabled();
}
}
void DolphinViewContainer::showMessage(const QString& msg, MessageType type)
{
if (msg.isEmpty()) {

View file

@ -9,6 +9,7 @@
#include "config-dolphin.h"
#include "dolphinurlnavigator.h"
#include "selectionmode/selectionmodebottombar.h"
#include "views/dolphinview.h"
#include <KFileItem>
@ -27,9 +28,12 @@ namespace KActivities {
class FilterBar;
class KMessageWidget;
class QAction;
class QGridLayout;
class QUrl;
class DolphinSearchBox;
class DolphinStatusBar;
class SelectionModeTopBar;
/**
* @short Represents a view for the directory content
@ -131,6 +135,9 @@ public:
*/
void disconnectUrlNavigator();
void setSelectionModeEnabled(bool enabled, KActionCollection *actionCollection = nullptr, SelectionModeBottomBar::Contents bottomBarContents = SelectionModeBottomBar::Contents::GeneralContents);
bool isSelectionModeEnabled() const;
/**
* Shows the message \msg with the given type non-modal above
* the view-content.
@ -206,6 +213,9 @@ public Q_SLOTS:
*/
void setSearchModeEnabled(bool enabled);
/** Used to notify the m_selectionModeBottomBar that there is no other ViewContainer in the tab. */
void slotSplitTabDisabled();
Q_SIGNALS:
/**
* Is emitted whenever the filter bar has changed its visibility state.
@ -216,6 +226,8 @@ Q_SIGNALS:
*/
void searchModeEnabledChanged(bool enabled);
void selectionModeChanged(bool enabled);
/**
* Is emitted when the write state of the folder has been changed. The application
* should disable all actions like "Create New..." that depend on the write
@ -395,7 +407,7 @@ private:
void tryRestoreViewState();
private:
QVBoxLayout* m_topLayout;
QGridLayout *m_topLayout;
/**
* The internal UrlNavigator which is never visible to the user.
@ -410,14 +422,22 @@ private:
* Otherwise it's one of the UrlNavigators visible in the toolbar.
*/
QPointer<DolphinUrlNavigator> m_urlNavigatorConnected;
DolphinSearchBox* m_searchBox;
bool m_searchModeEnabled;
KMessageWidget* m_messageWidget;
/// A bar shown at the top of the view to signify that selection mode is currently active.
SelectionModeTopBar *m_selectionModeTopBar;
DolphinView* m_view;
FilterBar* m_filterBar;
/// A bar shown at the bottom of the view whose contents depend on what the user is currently doing.
SelectionModeBottomBar *m_selectionModeBottomBar;
DolphinStatusBar* m_statusBar;
QTimer* m_statusBarTimer; // Triggers a delayed update
QElapsedTimer m_statusBarTimestamp; // Time in ms since last update

View file

@ -55,6 +55,11 @@ namespace Dolphin {
const int LAYOUT_SPACING_SMALL = 2;
}
enum Animated {
WithAnimation,
WithoutAnimation
};
class GlobalConfig : public QObject
{
Q_OBJECT

View file

@ -27,12 +27,14 @@
#include <QGraphicsSceneEvent>
#include <QGraphicsView>
#include <QMimeData>
#include <QStyleHints>
#include <QTimer>
#include <QTouchEvent>
KItemListController::KItemListController(KItemModelBase* model, KItemListView* view, QObject* parent) :
QObject(parent),
m_singleClickActivationEnforced(false),
m_selectionMode(false),
m_selectionTogglePressed(false),
m_clearSelectionIfItemsAreNotDragged(false),
m_isSwipeGesture(false),
@ -51,6 +53,7 @@ KItemListController::KItemListController(KItemModelBase* model, KItemListView* v
m_pressedIndex(std::nullopt),
m_pressedMousePos(),
m_autoActivationTimer(nullptr),
m_longPressDetectionTimer(nullptr),
m_swipeGesture(Qt::CustomGesture),
m_twoFingerTapGesture(Qt::CustomGesture),
m_oldSelection(),
@ -69,6 +72,15 @@ KItemListController::KItemListController(KItemModelBase* model, KItemListView* v
m_autoActivationTimer->setInterval(-1);
connect(m_autoActivationTimer, &QTimer::timeout, this, &KItemListController::slotAutoActivationTimeout);
m_longPressDetectionTimer = new QTimer(this);
m_longPressDetectionTimer->setSingleShot(true);
m_longPressDetectionTimer->setInterval(QGuiApplication::styleHints()->mousePressAndHoldInterval());
connect(m_longPressDetectionTimer, &QTimer::timeout, this, [this]() {
if (!m_selectionMode) {
Q_EMIT selectionModeRequested();
}
});
setModel(model);
setView(view);
@ -220,6 +232,16 @@ bool KItemListController::singleClickActivationEnforced() const
return m_singleClickActivationEnforced;
}
void KItemListController::setSelectionMode(bool enabled)
{
m_selectionMode = enabled;
}
bool KItemListController::selectionMode() const
{
return m_selectionMode;
}
bool KItemListController::keyPressEvent(QKeyEvent* event)
{
int index = m_selectionManager->currentItem();
@ -576,10 +598,14 @@ bool KItemListController::mouseMoveEvent(QGraphicsSceneMouseEvent* event, const
return false;
}
const QPointF pos = transform.map(event->pos());
if ((pos - m_pressedMousePos).manhattanLength() >= QApplication::startDragDistance()) {
m_longPressDetectionTimer->stop();
}
if (m_pressedIndex.has_value() && !m_view->rubberBand()->isActive()) {
// Check whether a dragging should be started
if (event->buttons() & Qt::LeftButton) {
const QPointF pos = transform.map(event->pos());
if ((pos - m_pressedMousePos).manhattanLength() >= QApplication::startDragDistance()) {
if (!m_selectionManager->isSelected(m_pressedIndex.value())) {
// Always assure that the dragged item gets selected. Usually this is already
@ -639,6 +665,8 @@ bool KItemListController::mouseReleaseEvent(QGraphicsSceneMouseEvent* event, con
m_view->m_tapAndHoldIndicator->setActive(false);
}
m_longPressDetectionTimer->stop();
KItemListRubberBand* rubberBand = m_view->rubberBand();
if (event->source() == Qt::MouseEventSynthesizedByQt && !rubberBand->isActive() && m_isTouchEvent) {
return false;
@ -1247,7 +1275,7 @@ void KItemListController::slotRubberBandChanged()
// been activated in case if no Shift- or Control-key are pressed
const bool shiftOrControlPressed = QApplication::keyboardModifiers() & Qt::ShiftModifier ||
QApplication::keyboardModifiers() & Qt::ControlModifier;
if (!shiftOrControlPressed) {
if (!shiftOrControlPressed && !m_selectionMode) {
m_oldSelection.clear();
}
}
@ -1296,7 +1324,7 @@ void KItemListController::slotRubberBandChanged()
}
} while (!selectionFinished);
if (QApplication::keyboardModifiers() & Qt::ControlModifier) {
if ((QApplication::keyboardModifiers() & Qt::ControlModifier) || m_selectionMode) {
// If Control is pressed, the selection state of all items in the rubberband is toggled.
// Therefore, the new selection contains:
// 1. All previously selected items which are not inside the rubberband, and
@ -1518,10 +1546,14 @@ bool KItemListController::onPress(const QPoint& screenPos, const QPointF& pos, c
}
const bool shiftPressed = modifiers & Qt::ShiftModifier;
const bool controlPressed = modifiers & Qt::ControlModifier;
const bool controlPressed = (modifiers & Qt::ControlModifier) || m_selectionMode;
const bool leftClick = buttons & Qt::LeftButton;
const bool rightClick = buttons & Qt::RightButton;
if (leftClick) {
m_longPressDetectionTimer->start();
}
// The previous selection is cleared if either
// 1. The selection mode is SingleSelection, or
// 2. the selection mode is MultiSelection, and *none* of the following conditions are met:
@ -1565,7 +1597,7 @@ bool KItemListController::onPress(const QPoint& screenPos, const QPointF& pos, c
return false;
}
}
} else if (pressedItemAlreadySelected && !shiftOrControlPressed && (buttons & Qt::LeftButton)) {
} else if (pressedItemAlreadySelected && !shiftOrControlPressed && leftClick) {
// The user might want to start dragging multiple items, but if he clicks the item
// in order to trigger it instead, the other selected items must be deselected.
// However, we do not know yet what the user is going to do.

View file

@ -126,6 +126,9 @@ public:
void setSingleClickActivationEnforced(bool singleClick);
bool singleClickActivationEnforced() const;
void setSelectionMode(bool enabled);
bool selectionMode() const;
bool processEvent(QEvent* event, const QTransform& transform);
Q_SIGNALS:
@ -209,6 +212,14 @@ Q_SIGNALS:
*/
void escapePressed();
/**
* Is emitted if left click is pressed down for a long time without moving the cursor too much.
* Moving the cursor would either trigger an item drag if the click was initiated on top of an item
* or a selection rectangle if the click was not initiated on top of an item.
* So long press is only emitted if there wasn't a lot of cursor movement.
*/
void selectionModeRequested();
void modelChanged(KItemModelBase* current, KItemModelBase* previous);
void viewChanged(KItemListView* current, KItemListView* previous);
@ -325,6 +336,7 @@ private:
private:
bool m_singleClickActivationEnforced;
bool m_selectionMode;
bool m_selectionTogglePressed;
bool m_clearSelectionIfItemsAreNotDragged;
bool m_isSwipeGesture;
@ -344,6 +356,7 @@ private:
QPointF m_pressedMousePos;
QTimer* m_autoActivationTimer;
QTimer* m_longPressDetectionTimer;
Qt::GestureType m_swipeGesture;
Qt::GestureType m_twoFingerTapGesture;

View file

@ -0,0 +1,76 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2022 Felix Ernst <fe.a.ernst@gmail.com>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "actionwithwidget.h"
#include <QAbstractButton>
#include <QFrame>
#include <QPushButton>
#include <QToolButton>
ActionWithWidget::ActionWithWidget(QAction *action) :
m_action{action}
{ }
ActionWithWidget::ActionWithWidget(QAction *action, QAbstractButton *button) :
m_action{action},
m_widget{button}
{
copyActionDataToButton(button, action);
}
QWidget *ActionWithWidget::newWidget(QWidget *parent)
{
Q_CHECK_PTR(m_action);
Q_ASSERT(!m_widget);
if (m_action->isSeparator()) {
auto line = new QFrame(parent);
line->setFrameShape(QFrame::VLine);
line->setFrameShadow(QFrame::Sunken);
m_widget = line;
} else {
m_widget = newButtonForAction(m_action, parent);
}
return m_widget;
}
QAbstractButton *newButtonForAction(QAction *action, QWidget *parent)
{
Q_CHECK_PTR(action);
Q_ASSERT(!action->isSeparator());
if (action->priority() == QAction::LowPriority) {
// We don't want the low priority actions to be displayed icon-only so we need trickery.
auto button = new QPushButton(parent);
copyActionDataToButton(static_cast<QAbstractButton *>(button), action);
button->setMinimumWidth(0);
return button;
}
auto *toolButton = new QToolButton(parent);
toolButton->setToolButtonStyle(Qt::ToolButtonStyle::ToolButtonTextBesideIcon);
toolButton->setDefaultAction(action);
toolButton->setPopupMode(QToolButton::ToolButtonPopupMode::InstantPopup);
toolButton->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
toolButton->setMinimumWidth(0);
return toolButton;
}
void copyActionDataToButton(QAbstractButton *button, QAction *action)
{
button->setText(action->text());
button->setIcon(action->icon());
button->setToolTip(action->toolTip());
button->setWhatsThis(action->whatsThis());
button->setVisible(action->isVisible());
button->setEnabled(action->isEnabled());
QObject::connect(button, &QAbstractButton::clicked, action, &QAction::trigger);
}

View file

@ -0,0 +1,86 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2022 Felix Ernst <fe.a.ernst@gmail.com>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#ifndef ACTIONWITHWIDGET_H
#define ACTIONWITHWIDGET_H
#include <QAction>
#include <QPointer>
#include <QWidget>
class QAbstractButton;
/**
* @brief Small wrapper/helper class that contains an action and its widget.
*
* This class takes neither the responsibility for deleting its action() nor its widget().
*/
class ActionWithWidget
{
public:
ActionWithWidget(QAction *action);
/**
* Connect @p action and @p button using copyActionDataToButton() and the
* wraps the two together in the ActionWithWidget object.
* ActionWithWidget doesn't take any ownership.
*
* @see copyActionDataToButton()
*
* @param button the button to be styled and used to fit the @p action.
*/
ActionWithWidget(QAction *action, QAbstractButton *button);
/** @returns the action of this object. Crashes if that action has been deleted elsewhere in the meantime. */
inline QAction *action() {
Q_CHECK_PTR(m_action);
return m_action;
};
/** @returns the widget of this object. */
inline QWidget *widget() {
return m_widget;
}
/**
* @returns a widget with parent @p parent for the action() of this object.
*
* For most actions some sort of button will be returned. For separators a vertical line will be returned.
* If this ActionWithWidget already has a widget(), this method will crash.
*/
QWidget *newWidget(QWidget *parent);
/** returns true if the widget exists and is visible. false otherwise. */
inline bool isWidgetVisible() const {
return m_widget && m_widget->isVisible();
};
private:
QPointer<QAction> m_action;
QPointer<QWidget> m_widget;
};
/**
* A small helper method.
* @return a button with the correct styling for the general mode of the SelectionModeBottomBar which can be added to its layout.
*/
QAbstractButton *newButtonForAction(QAction *action, QWidget *parent);
/**
* Normally, if one wants a button that represents a QAction one would use a QToolButton
* and simply call QToolButton::setDefaultAction(action). However if one does this, all
* control over the style, text, etc. of the button is forfeited. One can't for example
* have text on the button then, if the action has a low QAction::priority().
*
* This method styles the @p button based on the @p action without using QToolButton::setDefaultAction().
*
* Another reason why this is necessary is because the actions have application-wide scope while
* these buttons belong to one ViewContainer.
*/
void copyActionDataToButton(QAbstractButton *button, QAction *action);
#endif // ACTIONWITHWIDGET_H

View file

@ -0,0 +1,90 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2022 Felix Ernst <fe.a.ernst@gmail.com>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "backgroundcolorhelper.h"
#include <KColorScheme>
#include <QGuiApplication>
#include <QPalette>
#include <QtGlobal>
#include <QWidget>
BackgroundColorHelper *BackgroundColorHelper::instance()
{
if (!s_instance) {
s_instance = new BackgroundColorHelper;
}
return s_instance;
}
void setBackgroundColorForWidget(QWidget *widget, QColor color)
{
QPalette palette;
palette.setBrush(QPalette::Active, QPalette::Window, color);
palette.setBrush(QPalette::Inactive, QPalette::Window, color);
palette.setBrush(QPalette::Disabled, QPalette::Window, color);
widget->setAutoFillBackground(true);
widget->setPalette(palette);
}
void BackgroundColorHelper::controlBackgroundColor(QWidget *widget)
{
setBackgroundColorForWidget(widget, m_backgroundColor);
Q_ASSERT_X(std::find(m_colorControlledWidgets.begin(), m_colorControlledWidgets.end(), widget) == m_colorControlledWidgets.end(), "controlBackgroundColor",
"Duplicate insertion is not necessary because the background color should already automatically update itself on paletteChanged");
m_colorControlledWidgets.emplace_back(widget);
}
BackgroundColorHelper::BackgroundColorHelper()
{
updateBackgroundColor();
QObject::connect(qApp, &QGuiApplication::paletteChanged, [=](){ slotPaletteChanged(); });
}
void BackgroundColorHelper::slotPaletteChanged()
{
updateBackgroundColor();
for (auto i = m_colorControlledWidgets.begin(); i != m_colorControlledWidgets.end(); ++i) {
if (!*i) {
i = m_colorControlledWidgets.erase(i);
continue;
}
setBackgroundColorForWidget(*i, m_backgroundColor);
}
}
void BackgroundColorHelper::updateBackgroundColor()
{
// We use colors from the color scheme for mixing so it fits the theme.
const auto colorScheme = KColorScheme(QPalette::Normal, KColorScheme::Window);
const auto activeBackgroundColor = colorScheme.background(KColorScheme::BackgroundRole::ActiveBackground).color();
// We use the positive color for mixing so the end product doesn't look like a warning or error.
const auto positiveBackgroundColor = colorScheme.background(KColorScheme::BackgroundRole::PositiveBackground).color();
// Make sure the new background color has a meaningfully different hue than the activeBackgroundColor.
const int hueDifference = positiveBackgroundColor.hue() - activeBackgroundColor.hue();
int newHue;
if (std::abs(hueDifference) > 80) {
newHue = (activeBackgroundColor.hue() + positiveBackgroundColor.hue()) / 2;
} else {
newHue = hueDifference > 0 ?
activeBackgroundColor.hue() + 40 :
activeBackgroundColor.hue() - 40;
newHue %= 360; // hue needs to be between 0 and 359 per Qt documentation.
}
m_backgroundColor = QColor::fromHsv(newHue,
// Saturation should be closer to the active color because otherwise the selection mode color might overpower it.
.7 * activeBackgroundColor.saturation() + .3 * positiveBackgroundColor.saturation(),
(activeBackgroundColor.value() + positiveBackgroundColor.value()) / 2,
(activeBackgroundColor.alpha() + positiveBackgroundColor.alpha()) / 2);
}
BackgroundColorHelper *BackgroundColorHelper::s_instance = nullptr;

View file

@ -0,0 +1,45 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2022 Felix Ernst <fe.a.ernst@gmail.com>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#ifndef BACKGROUNDCOLORHELPER_H
#define BACKGROUNDCOLORHELPER_H
#include <QColor>
#include <QPointer>
#include <memory>
class QWidget;
/**
* @brief A Singleton class for managing the colors of selection mode widgets.
*/
class BackgroundColorHelper
{
public:
static BackgroundColorHelper *instance();
/**
* Changes the background color of @p widget to a distinct color scheme matching color which makes it clear that the widget belongs to the selection mode.
*/
void controlBackgroundColor(QWidget *widget);
private:
BackgroundColorHelper();
void slotPaletteChanged();
void updateBackgroundColor();
private:
std::vector<QPointer<QWidget>> m_colorControlledWidgets;
QColor m_backgroundColor;
static BackgroundColorHelper *s_instance;
};
#endif // BACKGROUNDCOLORHELPER_H

View file

@ -0,0 +1,705 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2022 Felix Ernst <fe.a.ernst@gmail.com>
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 <KActionCollection>
#include <KColorScheme>
#include <KFileItem>
#include <KFileItemListProperties>
#include <KLocalizedString>
#include <KStandardAction>
#include <QFontMetrics>
#include <QGuiApplication>
#include <QHBoxLayout>
#include <QLabel>
#include <QLayout>
#include <QMenu>
#include <QPushButton>
#include <QResizeEvent>
#include <QScrollArea>
#include <QStyle>
#include <QToolButton>
#include <QtGlobal>
#include <QVBoxLayout>
#include <unordered_set>
#include <iostream>
SelectionModeBottomBar::SelectionModeBottomBar(KActionCollection *actionCollection, QWidget *parent) :
QWidget{parent},
m_actionCollection{actionCollection}
{
// Showing of this widget is normally animated. We hide it for now and make it small.
hide();
setMaximumHeight(0);
setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed);
setMinimumWidth(0);
auto fillParentLayout = new QGridLayout(this);
fillParentLayout->setContentsMargins(0, 0, 0, 0);
// Put the contents into a QScrollArea. This prevents increasing the view width
// in case that not enough width for the contents is available. (this trick is also used in dolphinsearchbox.cpp.)
auto scrollArea = new QScrollArea(this);
fillParentLayout->addWidget(scrollArea);
scrollArea->setFrameShape(QFrame::NoFrame);
scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
scrollArea->setWidgetResizable(true);
auto contentsContainer = new QWidget(scrollArea);
scrollArea->setWidget(contentsContainer);
contentsContainer->installEventFilter(this); // Adjusts the height of this bar to the height of the contentsContainer
BackgroundColorHelper::instance()->controlBackgroundColor(this);
// We will mostly interact with m_layout when changing the contents and not care about the other internal hierarchy.
m_layout = new QHBoxLayout(contentsContainer);
}
void SelectionModeBottomBar::setVisible(bool visible, Animated animated)
{
Q_ASSERT_X(animated == WithAnimation, "SelectionModeBottomBar::setVisible", "This wasn't implemented.");
if (!visible && m_contents == PasteContents) {
return; // The bar with PasteContents should not be hidden or users might not know how to paste what they just copied.
// Set m_contents to anything else to circumvent this prevention mechanism.
}
if (!m_heightAnimation) {
m_heightAnimation = new QPropertyAnimation(this, "maximumHeight");
}
disconnect(m_heightAnimation, &QAbstractAnimation::finished,
this, nullptr);
m_heightAnimation->setDuration(2 *
style()->styleHint(QStyle::SH_Widget_Animation_Duration, nullptr, this) *
GlobalConfig::animationDurationFactor());
if (visible) {
show();
m_heightAnimation->setStartValue(0);
m_heightAnimation->setEndValue(sizeHint().height());
m_heightAnimation->setEasingCurve(QEasingCurve::OutCubic);
connect(m_heightAnimation, &QAbstractAnimation::finished,
this, [this](){ setMaximumHeight(sizeHint().height()); });
} else {
m_heightAnimation->setStartValue(height());
m_heightAnimation->setEndValue(0);
m_heightAnimation->setEasingCurve(QEasingCurve::OutCubic);
connect(m_heightAnimation, &QAbstractAnimation::finished,
this, &QWidget::hide);
}
m_heightAnimation->start();
}
QSize SelectionModeBottomBar::sizeHint() const
{
// 1 as width because this widget should never be the reason the DolphinViewContainer is made wider.
return QSize{1, m_layout->parentWidget()->sizeHint().height()};
}
void SelectionModeBottomBar::slotSelectionChanged(const KFileItemList &selection, const QUrl &baseUrl)
{
if (m_contents == GeneralContents) {
auto contextActions = contextActionsFor(selection, baseUrl);
m_generalBarActions.clear();
for (auto i = contextActions.begin(); i != contextActions.end(); ++i) {
m_generalBarActions.emplace_back(ActionWithWidget{*i});
}
resetContents(GeneralContents);
}
updateMainActionButton(selection);
}
void SelectionModeBottomBar::slotSplitTabDisabled()
{
switch (m_contents) {
case CopyToOtherViewContents:
case MoveToOtherViewContents:
Q_EMIT leaveSelectionModeRequested();
default:
return;
}
}
void SelectionModeBottomBar::resetContents(SelectionModeBottomBar::Contents contents)
{
emptyBarContents();
// A label is added in many of the methods below. We only know its size a bit later and if it should be hidden.
QTimer::singleShot(10, this, [this](){ updateExplanatoryLabelVisibility(); });
Q_CHECK_PTR(m_actionCollection);
m_contents = contents;
switch (contents) {
case CopyContents:
return addCopyContents();
case CopyLocationContents:
return addCopyLocationContents();
case CopyToOtherViewContents:
return addCopyToOtherViewContents();
case CutContents:
return addCutContents();
case DeleteContents:
return addDeleteContents();
case DuplicateContents:
return addDuplicateContents();
case GeneralContents:
return addGeneralContents();
case PasteContents:
return addPasteContents();
case MoveToOtherViewContents:
return addMoveToOtherViewContents();
case MoveToTrashContents:
return addMoveToTrashContents();
case RenameContents:
return addRenameContents();
}
}
bool SelectionModeBottomBar::eventFilter(QObject *watched, QEvent *event)
{
Q_ASSERT(qobject_cast<QWidget *>(watched)); // This evenfFilter is only implemented for QWidgets.
switch (event->type()) {
case QEvent::ChildAdded:
case QEvent::ChildRemoved:
QTimer::singleShot(0, this, [this](){ setMaximumHeight(sizeHint().height()); });
// Fall through.
default:
return false;
}
}
void SelectionModeBottomBar::resizeEvent(QResizeEvent *resizeEvent)
{
if (resizeEvent->oldSize().width() == resizeEvent->size().width()) {
// The width() didn't change so our custom override isn't needed.
return QWidget::resizeEvent(resizeEvent);
}
m_layout->parentWidget()->setFixedWidth(resizeEvent->size().width());
if (m_contents == GeneralContents) {
Q_ASSERT(m_overflowButton);
if (unusedSpace() < 0) {
// The bottom bar is overflowing! We need to hide some of the widgets.
for (auto i = m_generalBarActions.rbegin(); i != m_generalBarActions.rend(); ++i) {
if (!i->isWidgetVisible()) {
continue;
}
i->widget()->setVisible(false);
// Add the action to the overflow.
std::cout << "An Action is added to the m_overflowButton because of a resize: " << qPrintable(i->action()->text()) << "\n";
auto overflowMenu = m_overflowButton->menu();
if (overflowMenu->actions().isEmpty()) {
overflowMenu->addAction(i->action());
} else {
overflowMenu->insertAction(overflowMenu->actions().at(0), i->action());
}
std::cout << "The number of actions in the menu is now " << m_overflowButton->menu()->actions().count() << "\n.";
m_overflowButton->setVisible(true);
if (unusedSpace() >= 0) {
break; // All widgets fit now.
}
}
} else {
// We have some unusedSpace(). Let's check if we can maybe add more of the contextual action's widgets.
for (auto i = m_generalBarActions.begin(); i != m_generalBarActions.end(); ++i) {
if (i->isWidgetVisible()) {
continue;
}
if (!i->widget()) {
i->newWidget(this);
i->widget()->setVisible(false);
m_layout->insertWidget(m_layout->count() - 1, i->widget()); // Insert before m_overflowButton
}
if (unusedSpace() < i->widget()->sizeHint().width()) {
// It doesn't fit. We keep it invisible.
break;
}
i->widget()->setVisible(true);
// Remove the action from the overflow.
std::cout << "An Action is removed from the m_overflowButton because of a resize: " << qPrintable(i->action()->text()) << "\n";
auto overflowMenu = m_overflowButton->menu();
overflowMenu->removeAction(i->action());
std::cout << "The number of actions in the menu is now " << m_overflowButton->menu()->actions().count() << "\n.";
if (overflowMenu->isEmpty()) {
m_overflowButton->setVisible(false);
}
}
}
}
// Hide the leading explanation if it doesn't fit. The buttons are labeled clear enough that this shouldn't be a big UX problem.
updateExplanatoryLabelVisibility();
return QWidget::resizeEvent(resizeEvent);
}
void SelectionModeBottomBar::addCopyContents()
{
m_explanatoryLabel = new QLabel(i18nc("@info explaining the next step in a process", "Select the files and folders that should be copied."), this);
m_explanatoryLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
m_explanatoryLabel->setWordWrap(true);
m_layout->addWidget(m_explanatoryLabel);
// i18n: Aborts the current step-by-step process to copy files by leaving the selection mode.
auto *cancelButton = new QPushButton(i18nc("@action:button", "Abort Copying"), this);
connect(cancelButton, &QAbstractButton::clicked, this, &SelectionModeBottomBar::leaveSelectionModeRequested);
m_layout->addWidget(cancelButton);
auto *copyButton = new QPushButton(this);
// We claim to have PasteContents already so triggering the copy action next won't instantly hide the bottom bar.
connect(copyButton, &QAbstractButton::clicked, [this]() {
if (GeneralSettings::showPasteBarAfterCopying()) {
m_contents = Contents::PasteContents;
}
});
// Connect the copy action as a second step.
m_mainAction = ActionWithWidget(m_actionCollection->action(KStandardAction::name(KStandardAction::Copy)), copyButton);
// Finally connect the lambda that actually changes the contents to the PasteContents.
connect(copyButton, &QAbstractButton::clicked, [this]() {
if (GeneralSettings::showPasteBarAfterCopying()) {
resetContents(Contents::PasteContents); // resetContents() needs to be connected last because
// it instantly deletes the button and then the other slots won't be called.
}
Q_EMIT leaveSelectionModeRequested();
});
updateMainActionButton(KFileItemList());
m_layout->addWidget(copyButton);
}
void SelectionModeBottomBar::addCopyLocationContents()
{
m_explanatoryLabel = new QLabel(i18nc("@info explaining the next step in a process", "Select one file or folder whose location should be copied."), this);
m_explanatoryLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
m_explanatoryLabel->setWordWrap(true);
m_layout->addWidget(m_explanatoryLabel);
// i18n: Aborts the current step-by-step process to copy the location of files by leaving the selection mode.
auto *cancelButton = new QPushButton(i18nc("@action:button", "Abort Copying"), this);
connect(cancelButton, &QAbstractButton::clicked, this, &SelectionModeBottomBar::leaveSelectionModeRequested);
m_layout->addWidget(cancelButton);
auto *copyLocationButton = new QPushButton(this);
m_mainAction = ActionWithWidget(m_actionCollection->action(QStringLiteral("copy_location")), copyLocationButton);
updateMainActionButton(KFileItemList());
m_layout->addWidget(copyLocationButton);
}
void SelectionModeBottomBar::addCopyToOtherViewContents()
{
// i18n: "Copy over" refers to copying to the other split view area that is currently visible to the user.
m_explanatoryLabel = new QLabel(i18nc("@info explaining the next step in a process", "Select the files and folders that should be copied over."), this);
m_explanatoryLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
m_explanatoryLabel->setWordWrap(true);
m_layout->addWidget(m_explanatoryLabel);
// i18n: Aborts the current step-by-step process to copy the location of files by leaving the selection mode.
auto *cancelButton = new QPushButton(i18nc("@action:button", "Abort Copying"), this);
connect(cancelButton, &QAbstractButton::clicked, this, &SelectionModeBottomBar::leaveSelectionModeRequested);
m_layout->addWidget(cancelButton);
auto *copyToOtherViewButton = new QPushButton(this);
m_mainAction = ActionWithWidget(m_actionCollection->action(QStringLiteral("copy_to_inactive_split_view")), copyToOtherViewButton);
updateMainActionButton(KFileItemList());
m_layout->addWidget(copyToOtherViewButton);
}
void SelectionModeBottomBar::addCutContents()
{
m_explanatoryLabel = new QLabel(i18nc("@info explaining the next step in a process", "Select the files and folders that should be cut."), this);
m_explanatoryLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
m_explanatoryLabel->setWordWrap(true);
m_layout->addWidget(m_explanatoryLabel);
// i18n: Aborts the current step-by-step process to cut files by leaving the selection mode.
auto *cancelButton = new QPushButton(i18nc("@action:button", "Abort Cutting"), this);
connect(cancelButton, &QAbstractButton::clicked, this, &SelectionModeBottomBar::leaveSelectionModeRequested);
m_layout->addWidget(cancelButton);
auto *cutButton = new QPushButton(this);
// We claim to have PasteContents already so triggering the cut action next won't instantly hide the bottom bar.
connect(cutButton, &QAbstractButton::clicked, [this]() {
if (GeneralSettings::showPasteBarAfterCopying()) {
m_contents = Contents::PasteContents;
}
});
// Connect the cut action as a second step.
m_mainAction = ActionWithWidget(m_actionCollection->action(KStandardAction::name(KStandardAction::Cut)), cutButton);
// Finally connect the lambda that actually changes the contents to the PasteContents.
connect(cutButton, &QAbstractButton::clicked, [this](){
if (GeneralSettings::showPasteBarAfterCopying()) {
resetContents(Contents::PasteContents); // resetContents() needs to be connected last because
// it instantly deletes the button and then the other slots won't be called.
}
Q_EMIT leaveSelectionModeRequested();
});
updateMainActionButton(KFileItemList());
m_layout->addWidget(cutButton);
}
void SelectionModeBottomBar::addDeleteContents()
{
m_explanatoryLabel = new QLabel(i18nc("@info explaining the next step in a process", "Select the files and folders that should be permanently deleted."), this);
m_explanatoryLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
m_explanatoryLabel->setWordWrap(true);
m_layout->addWidget(m_explanatoryLabel);
// i18n: Aborts the current step-by-step process to delete files by leaving the selection mode.
auto *cancelButton = new QPushButton(i18nc("@action:button", "Abort"), this);
connect(cancelButton, &QAbstractButton::clicked, this, &SelectionModeBottomBar::leaveSelectionModeRequested);
m_layout->addWidget(cancelButton);
auto *deleteButton = new QPushButton(this);
m_mainAction = ActionWithWidget(m_actionCollection->action(KStandardAction::name(KStandardAction::DeleteFile)), deleteButton);
updateMainActionButton(KFileItemList());
m_layout->addWidget(deleteButton);
}
void SelectionModeBottomBar::addDuplicateContents()
{
m_explanatoryLabel = new QLabel(i18nc("@info explaining the next step in a process", "Select the files and folders that should be duplicated here."), this);
m_explanatoryLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
m_explanatoryLabel->setWordWrap(true);
m_layout->addWidget(m_explanatoryLabel);
// i18n: Aborts the current step-by-step process to duplicate files by leaving the selection mode.
auto *cancelButton = new QPushButton(i18nc("@action:button", "Abort Duplicating"), this);
connect(cancelButton, &QAbstractButton::clicked, this, &SelectionModeBottomBar::leaveSelectionModeRequested);
m_layout->addWidget(cancelButton);
auto *duplicateButton = new QPushButton(this);
m_mainAction = ActionWithWidget(m_actionCollection->action(QStringLiteral("duplicate")), duplicateButton);
updateMainActionButton(KFileItemList());
m_layout->addWidget(duplicateButton);
}
void SelectionModeBottomBar::addGeneralContents()
{
if (!m_overflowButton) {
m_overflowButton = new QToolButton{this};
// i18n: This button appears in a bar if there isn't enough horizontal space to fit all the other buttons.
// The small icon-only button opens a menu that contains the actions that didn't fit on the bar.
// Since this is an icon-only button this text will only appear as a tooltip and as accessibility text.
m_overflowButton->setToolTip(i18nc("@action", "More"));
m_overflowButton->setAccessibleName(m_overflowButton->toolTip());
m_overflowButton->setIcon(QIcon::fromTheme(QStringLiteral("view-more-horizontal-symbolic")));
m_overflowButton->setMenu(new QMenu{m_overflowButton});
m_overflowButton->setPopupMode(QToolButton::ToolButtonPopupMode::InstantPopup);
m_overflowButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::MinimumExpanding); // Makes sure it has the same height as the labeled buttons.
m_layout->addWidget(m_overflowButton);
} else {
m_overflowButton->menu()->actions().clear();
// The overflowButton should be part of the calculation for needed space so we set it visible in regards to unusedSpace().
m_overflowButton->setVisible(true);
}
// We first add all the m_generalBarActions to the bar until the bar is full.
auto i = m_generalBarActions.begin();
for (; i != m_generalBarActions.end(); ++i) {
if (i->action()->isVisible()) {
if (i->widget()) {
i->widget()->setEnabled(i->action()->isEnabled());
} else {
i->newWidget(this);
i->widget()->setVisible(false);
m_layout->insertWidget(m_layout->count() - 1, i->widget()); // Insert before m_overflowButton
}
if (unusedSpace() < i->widget()->sizeHint().width()) {
std::cout << "The " << unusedSpace() << " is smaller than the button->sizeHint().width() of " << i->widget()->sizeHint().width() << " plus the m_layout->spacing() of " << m_layout->spacing() << " so the action " << qPrintable(i->action()->text()) << " doesn't get its own button.\n";
break; // The bar is too full already. We keep it invisible.
} else {
std::cout << "The " << unusedSpace() << " is bigger than the button->sizeHint().width() of " << i->widget()->sizeHint().width() << " plus the m_layout->spacing() of " << m_layout->spacing() << " so the action " << qPrintable(i->action()->text()) << " was added as its own button/widget.\n";
i->widget()->setVisible(true);
}
}
}
// We are done adding widgets to the bar so either we were able to fit all the actions in there
m_overflowButton->setVisible(false);
// …or there are more actions left which need to be put into m_overflowButton.
for (; i != m_generalBarActions.end(); ++i) {
m_overflowButton->menu()->addAction(i->action());
// The overflowButton is set visible if there is actually an action in it.
if (!m_overflowButton->isVisible() && i->action()->isVisible() && !i->action()->isSeparator()) {
m_overflowButton->setVisible(true);
}
}
}
void SelectionModeBottomBar::addMoveToOtherViewContents()
{
// i18n: "Move over" refers to moving to the other split view area that is currently visible to the user.
m_explanatoryLabel = new QLabel(i18nc("@info explaining the next step in a process", "Select the files and folders that should be moved over."), this);
m_explanatoryLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
m_explanatoryLabel->setWordWrap(true);
m_layout->addWidget(m_explanatoryLabel);
// i18n: Aborts the current step-by-step process to copy the location of files by leaving the selection mode.
auto *cancelButton = new QPushButton(i18nc("@action:button", "Abort Moving"), this);
connect(cancelButton, &QAbstractButton::clicked, this, &SelectionModeBottomBar::leaveSelectionModeRequested);
m_layout->addWidget(cancelButton);
auto *moveToOtherViewButton = new QPushButton(this);
m_mainAction = ActionWithWidget(m_actionCollection->action(QStringLiteral("move_to_inactive_split_view")), moveToOtherViewButton);
updateMainActionButton(KFileItemList());
m_layout->addWidget(moveToOtherViewButton);
}
void SelectionModeBottomBar::addMoveToTrashContents()
{
m_explanatoryLabel = new QLabel(i18nc("@info explaining the next step in a process", "Select the files and folders that should be moved to the Trash."), this);
m_explanatoryLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
m_explanatoryLabel->setWordWrap(true);
m_layout->addWidget(m_explanatoryLabel);
// i18n: Aborts the current step-by-step process of moving files to the trash by leaving the selection mode.
auto *cancelButton = new QPushButton(i18nc("@action:button", "Abort"), this);
connect(cancelButton, &QAbstractButton::clicked, this, &SelectionModeBottomBar::leaveSelectionModeRequested);
m_layout->addWidget(cancelButton);
auto *moveToTrashButton = new QPushButton(this);
m_mainAction = ActionWithWidget(m_actionCollection->action(KStandardAction::name(KStandardAction::MoveToTrash)), moveToTrashButton);
updateMainActionButton(KFileItemList());
m_layout->addWidget(moveToTrashButton);
}
void SelectionModeBottomBar::addPasteContents()
{
m_explanatoryLabel = new QLabel(xi18n("<para>The selected files and folders were added to the Clipboard. "
"Now the <emphasis>Paste</emphasis> 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 <emphasis>Paste</emphasis> actions.</para>"), 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<QAction *> SelectionModeBottomBar::contextActionsFor(const KFileItemList& selectedItems, const QUrl& baseUrl)
{
std::vector<QAction *> contextActions;
contextActions.emplace_back(m_actionCollection->action(KStandardAction::name(KStandardAction::Copy)));
contextActions.emplace_back(m_actionCollection->action(KStandardAction::name(KStandardAction::Cut)));
contextActions.emplace_back(m_actionCollection->action(KStandardAction::name(KStandardAction::RenameFile)));
contextActions.emplace_back(m_actionCollection->action(KStandardAction::name(KStandardAction::MoveToTrash)));
if (!selectedItems.isEmpty()) {
// We are going to add the actions from the right-click context menu for the selected items.
auto *dolphinMainWindow = qobject_cast<DolphinMainWindow *>(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<DolphinContextMenu>(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<QAction *> actionsThatShouldntBeAdded{contextActions.begin(), contextActions.end()};
// "Delete" isn't really necessary to add because we have "Move to Trash" already. It is also more dangerous so let's exclude it.
actionsThatShouldntBeAdded.insert(m_actionCollection->action(KStandardAction::name(KStandardAction::DeleteFile)));
// "Open Terminal" isn't really context dependent and can therefore be opened from elsewhere instead.
actionsThatShouldntBeAdded.insert(m_actionCollection->action(QStringLiteral("open_terminal")));
// KHamburgerMenu would only be visible if there is no menu available anywhere on the user interface. This might be useful for recovery from
// such a situation in theory but a bar with context dependent actions doesn't really seem like the right place for it.
Q_ASSERT(internalContextMenuActions.first()->icon().name() == m_actionCollection->action(KStandardAction::name(KStandardAction::HamburgerMenu))->icon().name());
internalContextMenuActions.removeFirst();
for (auto it = internalContextMenuActions.constBegin(); it != internalContextMenuActions.constEnd(); ++it) {
if (actionsThatShouldntBeAdded.count(*it)) {
continue; // Skip this action.
}
if (!qobject_cast<DolphinRemoveAction *>(*it)) { // We already have a "Move to Trash" action so we don't want a DolphinRemoveAction.
// We filter duplicate separators here so we won't have to deal with them later.
if (!contextActions.back()->isSeparator() || !(*it)->isSeparator()) {
contextActions.emplace_back((*it));
}
}
}
}
return contextActions;
}
int SelectionModeBottomBar::unusedSpace() const
{
int sumOfPreferredWidths = m_layout->contentsMargins().left() + m_layout->contentsMargins().right();
if (m_overflowButton) {
sumOfPreferredWidths += m_overflowButton->sizeHint().width();
}
std::cout << "These layout items should have sane width: ";
for (int i = 0; i < m_layout->count(); ++i) {
auto widget = m_layout->itemAt(i)->widget();
if (widget && !widget->isVisibleTo(widget->parentWidget())) {
continue; // We don't count invisible widgets.
}
std::cout << m_layout->itemAt(i)->sizeHint().width() << ", ";
if (m_layout->itemAt(i)->sizeHint().width() == 0) {
// One of the items reports an invalid width. We can't work with this so we report an unused space of 0 which should lead to as few changes to the
// layout as possible until the next resize event happens at a later point in time.
//return 0;
}
sumOfPreferredWidths += m_layout->itemAt(i)->sizeHint().width() + m_layout->spacing();
}
std::cout << "leads to unusedSpace = " << width() << " - " << sumOfPreferredWidths - 20 << " = " << width() - sumOfPreferredWidths - 20 << "\n";
return width() - sumOfPreferredWidths - 20; // We consider all space used when there are only 20 pixels left
// so there is some room to breath and not too much wonkyness while resizing.
}
void SelectionModeBottomBar::updateExplanatoryLabelVisibility()
{
if (!m_explanatoryLabel) {
return;
}
std::cout << "label minimumSizeHint compared to width() :" << m_explanatoryLabel->sizeHint().width() << "/" << m_explanatoryLabel->width() << "; unusedSpace: " << unusedSpace() << "\n";
if (m_explanatoryLabel->isVisible()) {
m_explanatoryLabel->setVisible(unusedSpace() > 0);
} else {
// We only want to re-show the label when it fits comfortably so the computation below adds another "+20".
m_explanatoryLabel->setVisible(unusedSpace() > m_explanatoryLabel->sizeHint().width() + 20);
}
}
void SelectionModeBottomBar::updateMainActionButton(const KFileItemList& selection)
{
if (!m_mainAction.widget()) {
return;
}
Q_ASSERT(qobject_cast<QAbstractButton *>(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<QAbstractButton *>(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();
}
}

View file

@ -0,0 +1,181 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2022 Felix Ernst <fe.a.ernst@gmail.com>
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 <QAction>
#include <QPointer>
#include <QPropertyAnimation>
#include <QWidget>
#include <memory>
class DolphinContextMenu;
class KActionCollection;
class KFileItemActions;
class KFileItemList;
class QAbstractButton;
class QAction;
class QFontMetrics;
class QHBoxLayout;
class QLabel;
class QPushButton;
class QResizeEvent;
class QToolButton;
class QUrl;
/**
* A bar mainly used in selection mode that serves various purposes depending on what the user is currently trying to do.
*
* The Contents enum below gives a rough idea about the different states this bar might have.
* The bar is notified of various changes that make changing or updating the content worthwhile.
*/
class SelectionModeBottomBar : public QWidget
{
Q_OBJECT
public:
/** The different contents this bar can have. */
enum Contents{
CopyContents,
CopyLocationContents,
CopyToOtherViewContents,
CutContents,
DeleteContents,
DuplicateContents,
GeneralContents,
MoveToOtherViewContents,
MoveToTrashContents,
PasteContents,
RenameContents
};
/**
* Default constructor
*/
explicit SelectionModeBottomBar(KActionCollection *actionCollection, QWidget *parent);
/**
* Plays a show or hide animation while changing visibility.
* Therefore, if this method is used to hide this widget, the actual hiding will be postponed until the animation finished.
* @see QWidget::setVisible()
*/
void setVisible(bool visible, Animated animated);
using QWidget::setVisible; // Makes sure that the setVisible() declaration above doesn't hide the one from QWidget.
void resetContents(Contents contents);
inline Contents contents() const
{
return m_contents;
};
QSize sizeHint() const override;
public Q_SLOTS:
void slotSelectionChanged(const KFileItemList &selection, const QUrl &baseUrl);
/** Used to notify the m_selectionModeBottomBar that there is no other ViewContainer in the tab. */
void slotSplitTabDisabled();
Q_SIGNALS:
/**
* Forwards the errors from the KFileItemAction::error() used for contextual actions.
*/
void error(const QString &errorMessage);
void leaveSelectionModeRequested();
protected:
/** Is installed on an internal widget to make sure that the height of the bar is adjusted to its contents. */
bool eventFilter(QObject *watched, QEvent *event) override;
void resizeEvent(QResizeEvent *resizeEvent) override;
private:
void addCopyContents();
void addCopyLocationContents();
void addCopyToOtherViewContents();
void addCutContents();
void addDeleteContents();
void addDuplicateContents();
/**
* Adds the actions of m_generalBarActions as buttons to the bar. An overflow menu button is
* created to make sure any amount of actions can be accessed.
*/
void addGeneralContents();
void addMoveToOtherViewContents();
void addMoveToTrashContents();
void addPasteContents();
void addRenameContents();
/**
* Deletes all visible widgets and layouts from the bar.
*/
void emptyBarContents();
/**
* @returns A vector containing contextual actions for the given \a selection in the \a baseUrl.
* Cut, Copy, Rename and MoveToTrash are always added. Any further contextual actions depend on
* \a selection and \a baseUrl. \a selection and \a baseUrl can be empty/default constructed if
* no item- or view-specific actions should be added aside from Cut, Copy, Rename, MoveToTrash.
* @param selectedItems The selected items for which contextual actions should be displayed.
* @param baseUrl Base URL of the viewport the contextual actions apply to.
*/
std::vector<QAction *> 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<ActionWithWidget> m_generalBarActions;
/// The context menu used to retrieve all the actions that are relevant for the current selection.
std::unique_ptr<DolphinContextMenu> 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<QToolButton> m_overflowButton;
/// The actionCollection from which the actions for this bar are retrieved.
KActionCollection *m_actionCollection;
/// Describes the current contents of the bar.
Contents m_contents;
/** The layout all the buttons and labels are added to.
* Do not confuse this with layout() because we do have a QScrollView in between this widget and m_layout. */
QHBoxLayout *m_layout;
/// @see SelectionModeBottomBar::setVisible()
QPointer<QPropertyAnimation> m_heightAnimation;
/// The info label used for some of the BarContents. Is hidden for narrow widths.
QPointer<QLabel> m_explanatoryLabel;
};
#endif // SELECTIONMODEBOTTOMBAR_H

View file

@ -0,0 +1,131 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2022 Felix Ernst <fe.a.ernst@gmail.com>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "selectionmodetopbar.h"
#include "backgroundcolorhelper.h"
#include <KColorScheme>
#include <KLocalizedString>
#include <KToolTipHelper>
#include <QHBoxLayout>
#include <QLabel>
#include <QPalette>
#include <QPropertyAnimation>
#include <QPushButton>
#include <QScrollArea>
#include <QStyle>
#include <QtGlobal>
#include <type_traits>
#include <iostream>
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", "<title>Selection Mode</title><para>Select files or folders to manage or manipulate them."
"<list><item>Press on a file or folder to select it.</item><item>Press on an already selected file or folder to deselect it.</item>"
"<item>Pressing an empty area does <emphasis>not</emphasis> clear the selection.</item>"
"<item>Selection rectangles (created by dragging from an empty area) invert the selection status of items within.</item></list></para>"
"<para>The available action buttons at the bottom change depending on the current selection.</para>"));
auto fillParentLayout = new QGridLayout(this);
fillParentLayout->setContentsMargins(0, 0, 0, 0);
// Put the contents into a QScrollArea. This prevents increasing the view width
// in case that not enough width for the contents is available. (this trick is also used in selectionmodebottombar.cpp.)
auto scrollArea = new QScrollArea(this);
fillParentLayout->addWidget(scrollArea);
scrollArea->setFrameShape(QFrame::NoFrame);
scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
scrollArea->setWidgetResizable(true);
auto contentsContainer = new QWidget(scrollArea);
scrollArea->setWidget(contentsContainer);
BackgroundColorHelper::instance()->controlBackgroundColor(this);
setMinimumWidth(0);
m_fullLabelString = i18nc("@info label above the view explaining the state",
"Selection Mode: Click on files or folders to select or deselect them.");
m_shortLabelString = i18nc("@info label above the view explaining the state",
"Selection Mode");
m_label = new QLabel(contentsContainer);
m_label->setMinimumWidth(0);
BackgroundColorHelper::instance()->controlBackgroundColor(m_label);
m_closeButton = new QPushButton(QIcon::fromTheme(QStringLiteral("window-close-symbolic")), "", contentsContainer);
m_closeButton->setToolTip(i18nc("@action:button", "Exit Selection Mode"));
m_closeButton->setAccessibleName(m_closeButton->toolTip());
m_closeButton->setFlat(true);
connect(m_closeButton, &QAbstractButton::pressed,
this, &SelectionModeTopBar::leaveSelectionModeRequested);
QHBoxLayout *layout = new QHBoxLayout(contentsContainer);
auto contentsMargins = layout->contentsMargins();
m_preferredHeight = contentsMargins.top() + m_label->sizeHint().height() + contentsMargins.bottom();
scrollArea->setMaximumHeight(m_preferredHeight);
m_closeButton->setFixedHeight(m_preferredHeight);
layout->setContentsMargins(0, 0, 0, 0);
layout->addStretch();
layout->addWidget(m_label);
layout->addStretch();
layout->addWidget(m_closeButton);
}
void SelectionModeTopBar::setVisible(bool visible, Animated animated)
{
Q_ASSERT_X(animated == WithAnimation, "SelectionModeTopBar::setVisible", "This wasn't implemented.");
if (!m_heightAnimation) {
m_heightAnimation = new QPropertyAnimation(this, "maximumHeight");
}
disconnect(m_heightAnimation, &QAbstractAnimation::finished,
this, &QWidget::hide);
m_heightAnimation->setDuration(2 *
style()->styleHint(QStyle::SH_Widget_Animation_Duration, nullptr, this) *
GlobalConfig::animationDurationFactor());
if (visible) {
show();
m_heightAnimation->setStartValue(0);
m_heightAnimation->setEndValue(m_preferredHeight);
m_heightAnimation->setEasingCurve(QEasingCurve::OutCubic);
} else {
m_heightAnimation->setStartValue(height());
m_heightAnimation->setEndValue(0);
m_heightAnimation->setEasingCurve(QEasingCurve::OutCubic);
connect(m_heightAnimation, &QAbstractAnimation::finished,
this, &QWidget::hide);
}
m_heightAnimation->start();
}
void SelectionModeTopBar::resizeEvent(QResizeEvent */* resizeEvent */)
{
updateLabelString();
}
void SelectionModeTopBar::updateLabelString()
{
QFontMetrics fontMetrics = m_label->fontMetrics();
if (fontMetrics.horizontalAdvance(m_fullLabelString) + m_closeButton->sizeHint().width() + style()->pixelMetric(QStyle::PM_LayoutLeftMargin) * 2 + style()->pixelMetric(QStyle::PM_LayoutRightMargin) * 2 < width()) {
m_label->setText(m_fullLabelString);
} else {
m_label->setText(m_shortLabelString);
}
}

View file

@ -0,0 +1,66 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2022 Felix Ernst <fe.a.ernst@gmail.com>
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 <QPointer>
#include <QPropertyAnimation>
#include <QStyle>
#include <QWidget>
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<QPropertyAnimation> m_heightAnimation;
};
#endif // SELECTIONMODETOPBAR_H

View file

@ -0,0 +1,29 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2020 Felix Ernst <fe.a.ernst@gmail.com>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#ifndef SINGLECLICKSELECTIONPROXYSTYLE_H
#define SINGLECLICKSELECTIONPROXYSTYLE_H
#include <QProxyStyle>
/**
* @todo write docs
*/
class SingleClickSelectionProxyStyle : public QProxyStyle
{
public:
inline int styleHint(StyleHint hint, const QStyleOption *option = nullptr,
const QWidget *widget = nullptr, QStyleHintReturn *returnData = nullptr) const override
{
if (hint == QStyle::SH_ItemView_ActivateItemOnSingleClick) {
return 0;
}
return QProxyStyle::styleHint(hint, option, widget, returnData);
}
};
#endif // SINGLECLICKSELECTIONPROXYSTYLE_H

View file

@ -83,6 +83,10 @@
<label>Show selection toggle</label>
<default>true</default>
</entry>
<entry name="ShowPasteBarAfterCopying" type="Bool">
<label>Show a bar for easy pasting after a cut or copy was done using the selection mode bottom bar.</label>
<default>true</default>
</entry>
<entry name="UseTabForSwitchingSplitView" type="Bool">
<label>Use tab for switching between right and left split</label>
<default>false</default>

View file

@ -395,7 +395,7 @@ void ViewPropertiesDialog::applyViewProperties()
settings->save();
}
m_dolphinView->setMode(m_viewProps->viewMode());
m_dolphinView->setViewMode(m_viewProps->viewMode());
m_dolphinView->setSortRole(m_viewProps->sortRole());
m_dolphinView->setSortOrder(m_viewProps->sortOrder());
m_dolphinView->setSortFoldersFirst(m_viewProps->sortFoldersFirst());

View file

@ -20,6 +20,7 @@
#include "kitemviews/kitemlistselectionmanager.h"
#include "kitemviews/private/kitemlistroleeditor.h"
#include "settings/viewmodes/viewmodesettings.h"
#include "selectionmode/singleclickselectionproxystyle.h"
#include "versioncontrol/versioncontrolobserver.h"
#include "viewproperties.h"
#include "views/tooltips/tooltipmanager.h"
@ -172,6 +173,7 @@ DolphinView::DolphinView(const QUrl& url, QWidget* parent) :
connect(controller, &KItemListController::increaseZoom, this, &DolphinView::slotIncreaseZoom);
connect(controller, &KItemListController::decreaseZoom, this, &DolphinView::slotDecreaseZoom);
connect(controller, &KItemListController::swipeUp, this, &DolphinView::slotSwipeUp);
connect(controller, &KItemListController::selectionModeRequested, this, &DolphinView::selectionModeRequested);
connect(m_model, &KFileItemModel::directoryLoadingStarted, this, &DolphinView::slotDirectoryLoadingStarted);
connect(m_model, &KFileItemModel::directoryLoadingCompleted, this, &DolphinView::slotDirectoryLoadingCompleted);
@ -262,7 +264,7 @@ bool DolphinView::isActive() const
return m_active;
}
void DolphinView::setMode(Mode mode)
void DolphinView::setViewMode(Mode mode)
{
if (mode != m_mode) {
ViewProperties props(viewPropertiesUrl());
@ -276,11 +278,30 @@ void DolphinView::setMode(Mode mode)
}
}
DolphinView::Mode DolphinView::mode() const
DolphinView::Mode DolphinView::viewMode() const
{
return m_mode;
}
void DolphinView::setSelectionMode(const bool enabled)
{
if (enabled) {
m_proxyStyle = std::make_unique<SingleClickSelectionProxyStyle>();
setStyle(m_proxyStyle.get());
m_view->setStyle(m_proxyStyle.get());
} else {
setStyle(QApplication::style());
m_view->setStyle(QApplication::style());
}
m_container->controller()->setSelectionMode(enabled);
}
bool DolphinView::selectionMode() const
{
return m_container->controller()->selectionMode();
}
void DolphinView::setPreviewsShown(bool show)
{
if (previewsShown() == show) {

View file

@ -23,6 +23,8 @@
#include <QUrl>
#include <QWidget>
#include <memory>
typedef KIO::FileUndoManager::CommandType CommandType;
class QVBoxLayout;
class DolphinItemListView;
@ -36,6 +38,7 @@ class ViewProperties;
class QLabel;
class QGraphicsSceneDragDropEvent;
class QHelpEvent;
class QProxyStyle;
class QRegularExpression;
/**
@ -106,8 +109,11 @@ public:
* (GeneralSettings::globalViewProps() returns false), then the
* changed view mode will be stored automatically.
*/
void setMode(Mode mode);
Mode mode() const;
void setViewMode(Mode mode);
Mode viewMode() const;
void setSelectionMode(bool enabled);
bool selectionMode() const;
/**
* Turns on the file preview for the all files of the current directory,
@ -599,6 +605,13 @@ Q_SIGNALS:
*/
void goForwardRequested();
/**
* Is emitted when the selection mode is requested for the current view.
* This typically happens on press and hold.
* @see KItemListController::longPress()
*/
void selectionModeRequested();
/**
* Is emitted when the user wants to move the focus to another view.
*/
@ -916,6 +929,9 @@ private:
QLabel* m_placeholderLabel;
QTimer* m_showLoadingPlaceholderTimer;
/// Used for selection mode. @see setSelectionMode()
std::unique_ptr<QProxyStyle> m_proxyStyle;
// For unit tests
friend class TestBase;
friend class DolphinDetailsViewTest;

View file

@ -29,6 +29,8 @@
#include <QMenu>
#include <QPointer>
#include <iostream>
DolphinViewActionHandler::DolphinViewActionHandler(KActionCollection* collection, QObject* parent) :
QObject(parent),
m_actionCollection(collection),
@ -72,6 +74,8 @@ void DolphinViewActionHandler::setCurrentView(DolphinView* view)
this, &DolphinViewActionHandler::slotZoomLevelChanged);
connect(view, &DolphinView::writeStateChanged,
this, &DolphinViewActionHandler::slotWriteStateChanged);
connect(view, &DolphinView::selectionModeRequested,
this, [this]() { Q_EMIT setSelectionMode(true); });
connect(view, &DolphinView::selectionChanged,
this, &DolphinViewActionHandler::slotSelectionChanged);
slotSelectionChanged(m_currentView->selectedItems());
@ -415,7 +419,7 @@ QActionGroup* DolphinViewActionHandler::createFileItemRolesActionGroup(const QSt
void DolphinViewActionHandler::slotViewModeActionTriggered(QAction* action)
{
const DolphinView::Mode mode = action->data().value<DolphinView::Mode>();
m_currentView->setMode(mode);
m_currentView->setViewMode(mode);
QAction* viewModeMenu = m_actionCollection->action(QStringLiteral("view_mode"));
viewModeMenu->setIcon(action->icon());
@ -423,20 +427,34 @@ void DolphinViewActionHandler::slotViewModeActionTriggered(QAction* action)
void DolphinViewActionHandler::slotRename()
{
Q_EMIT actionBeingHandled();
m_currentView->renameSelectedItems();
if (m_currentView->selectedItemsCount() == 0) {
Q_EMIT setSelectionMode(true, SelectionModeBottomBar::Contents::RenameContents);
} else {
Q_EMIT actionBeingHandled();
m_currentView->renameSelectedItems();
}
}
void DolphinViewActionHandler::slotTrashActivated()
{
Q_EMIT actionBeingHandled();
m_currentView->trashSelectedItems();
if (m_currentView->selectedItemsCount() == 0) {
Q_EMIT setSelectionMode(true, SelectionModeBottomBar::Contents::MoveToTrashContents);
} else {
Q_EMIT actionBeingHandled();
m_currentView->trashSelectedItems();
Q_EMIT setSelectionMode(false);
}
}
void DolphinViewActionHandler::slotDeleteItems()
{
Q_EMIT actionBeingHandled();
m_currentView->deleteSelectedItems();
if (m_currentView->selectedItemsCount() == 0) {
Q_EMIT setSelectionMode(true, SelectionModeBottomBar::Contents::DeleteContents);
} else {
Q_EMIT actionBeingHandled();
m_currentView->deleteSelectedItems();
Q_EMIT setSelectionMode(false);
}
}
void DolphinViewActionHandler::togglePreview(bool show)
@ -455,7 +473,7 @@ void DolphinViewActionHandler::slotPreviewsShownChanged(bool shown)
QString DolphinViewActionHandler::currentViewModeActionName() const
{
switch (m_currentView->mode()) {
switch (m_currentView->viewMode()) {
case DolphinView::IconsView:
return QStringLiteral("icons");
case DolphinView::DetailsView:
@ -735,8 +753,13 @@ void DolphinViewActionHandler::slotAdjustViewProperties()
void DolphinViewActionHandler::slotDuplicate()
{
Q_EMIT actionBeingHandled();
m_currentView->duplicateSelectedItems();
if (m_currentView->selectedItemsCount() == 0) {
Q_EMIT setSelectionMode(true, SelectionModeBottomBar::Contents::DuplicateContents);
} else {
Q_EMIT actionBeingHandled();
m_currentView->duplicateSelectedItems();
Q_EMIT setSelectionMode(false);
}
}
void DolphinViewActionHandler::slotProperties()
@ -758,7 +781,12 @@ void DolphinViewActionHandler::slotProperties()
void DolphinViewActionHandler::slotCopyPath()
{
m_currentView->copyPathToClipboard();
if (m_currentView->selectedItemsCount() == 0) {
Q_EMIT setSelectionMode(true, SelectionModeBottomBar::Contents::CopyLocationContents);
} else {
m_currentView->copyPathToClipboard();
Q_EMIT setSelectionMode(false);
}
}
void DolphinViewActionHandler::slotSelectionChanged(const KFileItemList& selection)

View file

@ -10,6 +10,7 @@
#define DOLPHINVIEWACTIONHANDLER_H
#include "dolphin_export.h"
#include "selectionmode/selectionmodebottombar.h"
#include "views/dolphinview.h"
#include <QObject>
@ -83,6 +84,9 @@ Q_SIGNALS:
*/
void createDirectoryTriggered();
/** Used to request selection mode */
void setSelectionMode(bool enabled, SelectionModeBottomBar::Contents bottomBarContents = SelectionModeBottomBar::Contents::GeneralContents);
private Q_SLOTS:
/**
* Emitted when the user requested a change of view mode