From 7d49cb567b595570f59156965332428c9490a04d Mon Sep 17 00:00:00 2001 From: Jin Liu Date: Wed, 3 Apr 2024 12:34:50 +0000 Subject: [PATCH] DolphinMainWindow: show a banner when the user presses the shortcut of a disabled action Currently, there's no feedback when the user presses a shortcut of a disabled action, e.g. cut / paste in a read-only directory. This patch shows a banner in that case. It's implemented by enabling a QShortcut for each disabled action. the QShortcut is deleted when the action is enabled again. The following actions are included: cut paste rename moveToTrash deleteWithTrashShortcut deleted createDir copyToOtherView moveToOtherView --- src/CMakeLists.txt | 1 + src/disabledactionnotifier.cpp | 51 ++++++++++++++++++++++ src/disabledactionnotifier.h | 60 ++++++++++++++++++++++++++ src/dolphinmainwindow.cpp | 77 ++++++++++++++++++++++++++++++---- src/dolphinmainwindow.h | 2 + 5 files changed, 182 insertions(+), 9 deletions(-) create mode 100644 src/disabledactionnotifier.cpp create mode 100644 src/disabledactionnotifier.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4c2c3cb6c..ab288a563 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -262,6 +262,7 @@ add_library(dolphinstatic STATIC) target_sources(dolphinstatic PRIVATE animatedheightwidget.cpp + disabledactionnotifier.cpp dolphinbookmarkhandler.cpp dolphindockwidget.cpp dolphinmainwindow.cpp diff --git a/src/disabledactionnotifier.cpp b/src/disabledactionnotifier.cpp new file mode 100644 index 000000000..844e66228 --- /dev/null +++ b/src/disabledactionnotifier.cpp @@ -0,0 +1,51 @@ +/* + * SPDX-FileCopyrightText: 2024 Jin Liu + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "disabledactionnotifier.h" + +DisabledActionNotifier::DisabledActionNotifier(QObject *parent) + : QObject(parent) +{ +} + +void DisabledActionNotifier::setDisabledReason(QAction *action, QStringView reason) +{ + if (action->isEnabled()) { + return; + } + + if (m_shortcuts.contains(action)) { + m_shortcuts.take(action)->deleteLater(); + } + + QShortcut *shortcut = new QShortcut(action->shortcut(), parent()); + m_shortcuts.insert(action, shortcut); + + connect(action, &QAction::enabledChanged, this, [this, action](bool enabled) { + if (enabled) { + m_shortcuts.take(action)->deleteLater(); + } + }); + + // Don't capture QStringView, as it may reference a temporary QString + QString reasonString = reason.toString(); + connect(shortcut, &QShortcut::activated, this, [this, action, reasonString]() { + Q_EMIT disabledActionTriggered(action, reasonString); + }); +} + +void DisabledActionNotifier::clearDisabledReason(QAction *action) +{ + if (action->isEnabled()) { + return; + } + + if (m_shortcuts.contains(action)) { + m_shortcuts.take(action)->deleteLater(); + } +} + +#include "moc_disabledactionnotifier.cpp" diff --git a/src/disabledactionnotifier.h b/src/disabledactionnotifier.h new file mode 100644 index 000000000..535e9932b --- /dev/null +++ b/src/disabledactionnotifier.h @@ -0,0 +1,60 @@ +/* + * SPDX-FileCopyrightText: 2024 Jin Liu + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#pragma once + +#include +#include +#include + +/** + * @brief A helper class to display a notification when the user presses the shortcut of a disabled action. + */ +class DisabledActionNotifier : public QObject +{ + Q_OBJECT + +public: + DisabledActionNotifier(QObject *parent = nullptr); + + /** + * Set the reason why the action is disabled. + * + * If the action is enabled, this function does nothing. + * + * Otherwise, it registers a shortcut, so when the user presses the shortcut for the + * disabled action, it emits the disabledActionTriggered() signal with the disabled + * action and the reason. + * + * If a reason has already been set, it will be replaced. + */ + void setDisabledReason(QAction *action, QStringView reason); + + /** + * Clear the reason if it's set before by setDisabledReason(). + * + * If the action is enabled, this function does nothing. + * + * Otherwise, it unregisters any shortcut set by setDisabledReason() on the same action. + * + * When an action is disabled in two cases, but only case A needs to show the reason, + * then case B should call this function. Otherwise, the reason set by case A might be + * shown for case B. + */ + void clearDisabledReason(QAction *action); + +Q_SIGNALS: + /** + * Emitted when the user presses the shortcut of a disabled action. + * + * @param action The disabled action. + * @param reason The reason set in setDisabledReason(). + */ + void disabledActionTriggered(const QAction *action, QString reason); + +private: + QHash m_shortcuts; +}; diff --git a/src/dolphinmainwindow.cpp b/src/dolphinmainwindow.cpp index 406edd37c..4cba46554 100644 --- a/src/dolphinmainwindow.cpp +++ b/src/dolphinmainwindow.cpp @@ -110,6 +110,7 @@ DolphinMainWindow::DolphinMainWindow() , m_remoteEncoding(nullptr) , m_settingsDialog() , m_bookmarkHandler(nullptr) + , m_disabledActionNotifier(nullptr) , m_lastHandleUrlOpenJob(nullptr) , m_terminalPanel(nullptr) , m_placesPanel(nullptr) @@ -178,6 +179,11 @@ DolphinMainWindow::DolphinMainWindow() m_remoteEncoding = new DolphinRemoteEncoding(this, m_actionHandler); connect(this, &DolphinMainWindow::urlChanged, m_remoteEncoding, &DolphinRemoteEncoding::slotAboutToOpenUrl); + m_disabledActionNotifier = new DisabledActionNotifier(this); + connect(m_disabledActionNotifier, &DisabledActionNotifier::disabledActionTriggered, this, [this](const QAction *, QString reason) { + m_activeViewContainer->showMessage(reason, DolphinViewContainer::Warning); + }); + setupDockWidgets(); setupGUI(Save | Create | ToolBar); @@ -853,6 +859,10 @@ void DolphinMainWindow::updatePasteAction() QAction *pasteAction = actionCollection()->action(KStandardAction::name(KStandardAction::Paste)); QPair pasteInfo = m_activeViewContainer->view()->pasteInfo(); pasteAction->setEnabled(pasteInfo.first); + m_disabledActionNotifier->setDisabledReason(pasteAction, + m_activeViewContainer->rootItem().isWritable() + ? i18nc("@info", "Could not paste: The clipboard is empty.") + : i18nc("@info", "Could not paste: You do not have permission to write into this folder.")); pasteAction->setText(pasteInfo.second); } @@ -1383,6 +1393,11 @@ void DolphinMainWindow::slotWriteStateChanged(bool isFolderWritable) // trash:/ is writable but we don't want to create new items in it. // TODO: remove the trash check once https://phabricator.kde.org/T8234 is implemented newFileMenu()->setEnabled(isFolderWritable && m_activeViewContainer->url().scheme() != QLatin1String("trash")); + // When the menu is disabled, actions in it are disabled later in the event loop, and we need to set the disabled reason after that. + QTimer::singleShot(0, this, [this]() { + m_disabledActionNotifier->setDisabledReason(actionCollection()->action(QStringLiteral("create_dir")), + i18nc("@info", "Could not create new folder: You do not have permission to create items in this folder.")); + }); } void DolphinMainWindow::openContextMenu(const QPoint &pos, const KFileItem &item, const KFileItemList &selectedItems, const QUrl &url) @@ -2416,16 +2431,43 @@ void DolphinMainWindow::updateFileAndEditActions() const bool enableMoveToTrash = capabilitiesSource.isLocal() && capabilitiesSource.supportsMoving(); renameAction->setEnabled(capabilitiesSource.supportsMoving()); - moveToTrashAction->setEnabled(enableMoveToTrash); + m_disabledActionNotifier->setDisabledReason(renameAction, + i18nc("@info", "Could not rename: You do not have permission to rename items in this folder.")); deleteAction->setEnabled(capabilitiesSource.supportsDeleting()); - deleteWithTrashShortcut->setEnabled(capabilitiesSource.supportsDeleting() && !enableMoveToTrash); + m_disabledActionNotifier->setDisabledReason(deleteAction, + i18nc("@info", "Could not delete: You do not have permission to remove items from this folder.")); cutAction->setEnabled(capabilitiesSource.supportsMoving()); + m_disabledActionNotifier->setDisabledReason(cutAction, i18nc("@info", "Could not cut: You do not have permission to move items from this folder.")); copyLocation->setEnabled(list.length() == 1); showTarget->setEnabled(list.length() == 1 && list.at(0).isLink()); duplicateAction->setEnabled(capabilitiesSource.supportsWriting()); + m_disabledActionNotifier->setDisabledReason(duplicateAction, + i18nc("@info", "Could not duplicate here: You do not have permission to create items in this folder.")); + + if (enableMoveToTrash) { + moveToTrashAction->setEnabled(true); + deleteWithTrashShortcut->setEnabled(false); + m_disabledActionNotifier->clearDisabledReason(deleteWithTrashShortcut); + } else { + moveToTrashAction->setEnabled(false); + deleteWithTrashShortcut->setEnabled(capabilitiesSource.supportsDeleting()); + m_disabledActionNotifier->setDisabledReason(deleteWithTrashShortcut, + i18nc("@info", "Could not delete: You do not have permission to remove items from this folder.")); + } } - if (m_tabWidget->currentTabPage()->splitViewEnabled() && !list.isEmpty()) { + if (!m_tabWidget->currentTabPage()->splitViewEnabled()) { + // No need to set the disabled reason here, as it's obvious to the user that the reason is the split view being disabled. + copyToOtherViewAction->setEnabled(false); + m_disabledActionNotifier->clearDisabledReason(copyToOtherViewAction); + moveToOtherViewAction->setEnabled(false); + m_disabledActionNotifier->clearDisabledReason(moveToOtherViewAction); + } else if (list.isEmpty()) { + copyToOtherViewAction->setEnabled(false); + m_disabledActionNotifier->setDisabledReason(copyToOtherViewAction, i18nc("@info", "Could not copy to other view: No files selected.")); + moveToOtherViewAction->setEnabled(false); + m_disabledActionNotifier->setDisabledReason(moveToOtherViewAction, i18nc("@info", "Could not move to other view: No files selected.")); + } else { DolphinTabPage *tabPage = m_tabWidget->currentTabPage(); KFileItem capabilitiesDestination; @@ -2440,12 +2482,29 @@ void DolphinMainWindow::updateFileAndEditActions() return item.url().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash) != destUrl; }); - copyToOtherViewAction->setEnabled(capabilitiesDestination.isWritable() && allNotTargetOrigin); - moveToOtherViewAction->setEnabled((list.isEmpty() || capabilitiesSource.supportsMoving()) && capabilitiesDestination.isWritable() - && allNotTargetOrigin); - } else { - copyToOtherViewAction->setEnabled(false); - moveToOtherViewAction->setEnabled(false); + if (!allNotTargetOrigin) { + copyToOtherViewAction->setEnabled(false); + m_disabledActionNotifier->setDisabledReason(copyToOtherViewAction, + i18nc("@info", "Could not copy to other view: The other view already contains these items.")); + moveToOtherViewAction->setEnabled(false); + m_disabledActionNotifier->setDisabledReason(moveToOtherViewAction, + i18nc("@info", "Could not move to other view: The other view already contains these items.")); + } else if (!capabilitiesDestination.isWritable()) { + copyToOtherViewAction->setEnabled(false); + m_disabledActionNotifier->setDisabledReason( + copyToOtherViewAction, + i18nc("@info", "Could not copy to other view: You do not have permission to write into the destination folder.")); + moveToOtherViewAction->setEnabled(false); + m_disabledActionNotifier->setDisabledReason( + moveToOtherViewAction, + i18nc("@info", "Could not move to other view: You do not have permission to write into the destination folder.")); + } else { + copyToOtherViewAction->setEnabled(true); + moveToOtherViewAction->setEnabled(capabilitiesSource.supportsMoving()); + m_disabledActionNotifier->setDisabledReason( + moveToOtherViewAction, + i18nc("@info", "Could not move to other view: You do not have permission to move items from this folder.")); + } } } diff --git a/src/dolphinmainwindow.h b/src/dolphinmainwindow.h index 62f8ceb6e..9a1582c1f 100644 --- a/src/dolphinmainwindow.h +++ b/src/dolphinmainwindow.h @@ -10,6 +10,7 @@ #define DOLPHIN_MAINWINDOW_H #include "config-dolphin.h" +#include "disabledactionnotifier.h" #include "dolphintabwidget.h" #include "selectionmode/bottombar.h" #include @@ -727,6 +728,7 @@ private: QPointer m_settingsDialog; DolphinBookmarkHandler *m_bookmarkHandler; SelectionMode::ActionTextHelper *m_actionTextHelper; + DisabledActionNotifier *m_disabledActionNotifier; KIO::OpenUrlJob *m_lastHandleUrlOpenJob; -- 2.47.3