]> cloud.milkyroute.net Git - dolphin.git/commitdiff
DolphinMainWindow: show a banner when the user presses the shortcut of a disabled...
authorJin Liu <m.liu.jin@gmail.com>
Wed, 3 Apr 2024 12:34:50 +0000 (12:34 +0000)
committerMéven Car <meven.car@kdemail.net>
Wed, 3 Apr 2024 12:34:50 +0000 (12:34 +0000)
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
src/disabledactionnotifier.cpp [new file with mode: 0644]
src/disabledactionnotifier.h [new file with mode: 0644]
src/dolphinmainwindow.cpp
src/dolphinmainwindow.h

index 4c2c3cb6cb170f9110aba25ddd4e80082deb0c6e..ab288a563e880d8d11245c133abaabadf90397dd 100644 (file)
@@ -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 (file)
index 0000000..844e662
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Jin Liu <m.liu.jin@gmail.com>
+ *
+ * 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 (file)
index 0000000..535e993
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Jin Liu <m.liu.jin@gmail.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#pragma once
+
+#include <QAction>
+#include <QHash>
+#include <QShortcut>
+
+/**
+ * @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<QAction *, QShortcut *> m_shortcuts;
+};
index 406edd37c0aaa19ac3a3e6faa145bd05d469eab9..4cba465540cb4f8b43991e2682145ce2cf4bcbbe 100644 (file)
@@ -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<bool, QString> 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."));
+        }
     }
 }
 
index 62f8ceb6e489422cbce04f5902f160c6aced8d3f..9a1582c1fef903b949f9a84ec614e016db4c22e2 100644 (file)
@@ -10,6 +10,7 @@
 #define DOLPHIN_MAINWINDOW_H
 
 #include "config-dolphin.h"
+#include "disabledactionnotifier.h"
 #include "dolphintabwidget.h"
 #include "selectionmode/bottombar.h"
 #include <KActionMenu>
@@ -727,6 +728,7 @@ private:
     QPointer<DolphinSettingsDialog> m_settingsDialog;
     DolphinBookmarkHandler *m_bookmarkHandler;
     SelectionMode::ActionTextHelper *m_actionTextHelper;
+    DisabledActionNotifier *m_disabledActionNotifier;
 
     KIO::OpenUrlJob *m_lastHandleUrlOpenJob;