]> cloud.milkyroute.net Git - dolphin.git/commitdiff
Add a SetFolderIcon ItemAction plugin
authorMéven Car <meven@kde.org>
Mon, 9 Jun 2025 12:16:36 +0000 (12:16 +0000)
committerMéven Car <meven@kde.org>
Mon, 9 Jun 2025 12:16:36 +0000 (12:16 +0000)
To allow to change folder icon from the context menu.

CCBUG: 467221

src/CMakeLists.txt
src/dolphincontextmenu.cpp
src/itemactions/CMakeLists.txt [new file with mode: 0644]
src/itemactions/setfoldericonitemaction.cpp [new file with mode: 0644]
src/itemactions/setfoldericonitemaction.h [new file with mode: 0644]
src/itemactions/setfoldericonitemaction.json [new file with mode: 0644]

index 3bf64454e26e9d28e6a93d2817bc2329cfda3e16..5b02a0f76ebf925253788f632668bafdbdf8546f 100644 (file)
@@ -672,14 +672,4 @@ if(BUILD_TESTING)
     add_subdirectory(tests)
 endif()
 
-# movetonewfolderitemaction plugin
-
-kcoreaddons_add_plugin(movetonewfolderitemaction
-    SOURCES itemactions/movetonewfolderitemaction.cpp itemactions/movetonewfolderitemaction.h
-    INSTALL_NAMESPACE "kf6/kfileitemaction")
-
-target_link_libraries(movetonewfolderitemaction
-    KF6::I18n
-    KF6::KIOCore
-    KF6::KIOWidgets
-    KF6::KIOFileWidgets)
+add_subdirectory(itemactions)
index 8372060aade9a5d30fc884eccecaafc36a770ff5..e1c67aad1fc53821f1d5b667fbd48e34940a0f11 100644 (file)
@@ -122,7 +122,7 @@ void DolphinContextMenu::addTrashContextMenu()
 {
     Q_ASSERT(m_context & TrashContext);
 
-    QAction *emptyTrashAction = addAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18nc("@action:inmenu", "Empty Trash"), [this]() {
+    QAction *emptyTrashAction = addAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18nc("@action:inmenu", "Empty Trash"), this, [this]() {
         Trash::empty(m_mainWindow);
     });
     emptyTrashAction->setEnabled(!Trash::isEmpty());
@@ -154,6 +154,7 @@ void DolphinContextMenu::addTrashItemContextMenu()
                      "Restore to Former Location",
                      "Restore to Former Locations",
                      m_selectedItems.count()),
+              this,
               [this]() {
                   QList<QUrl> selectedUrls;
                   selectedUrls.reserve(m_selectedItems.count());
@@ -202,7 +203,7 @@ void DolphinContextMenu::addDirectoryItemContextMenu()
     // set up 'Create New' menu
     QAction *newDirAction = m_mainWindow->actionCollection()->action(QStringLiteral("create_dir"));
     QAction *newFileAction = m_mainWindow->actionCollection()->action(QStringLiteral("create_file"));
-    DolphinNewFileMenu *newFileMenu = new DolphinNewFileMenu(newDirAction, newFileAction, m_mainWindow);
+    DolphinNewFileMenu *newFileMenu = new DolphinNewFileMenu(newDirAction, newFileAction, this);
     newFileMenu->checkUpToDate();
     newFileMenu->setWorkingDirectory(m_fileInfo.url());
     newFileMenu->setEnabled(selectedItemsProps.supportsWriting());
diff --git a/src/itemactions/CMakeLists.txt b/src/itemactions/CMakeLists.txt
new file mode 100644 (file)
index 0000000..6610b0e
--- /dev/null
@@ -0,0 +1,26 @@
+
+# movetonewfolderitemaction plugin
+
+kcoreaddons_add_plugin(movetonewfolderitemaction
+    SOURCES movetonewfolderitemaction.cpp movetonewfolderitemaction.h
+    INSTALL_NAMESPACE "kf6/kfileitemaction")
+
+target_link_libraries(movetonewfolderitemaction
+    KF6::I18n
+    KF6::KIOCore
+    KF6::KIOWidgets
+    KF6::KIOFileWidgets)
+
+
+if(NOT WIN32)
+    # setfoldericon plugin
+
+    kcoreaddons_add_plugin(setfoldericonitemaction
+        SOURCES setfoldericonitemaction.cpp setfoldericonitemaction.h ../dolphindebug.h ../dolphindebug.cpp
+        INSTALL_NAMESPACE "kf6/kfileitemaction")
+
+    target_link_libraries(setfoldericonitemaction
+        KF6::I18n
+        KF6::KIOCore
+        KF6::KIOWidgets)
+endif()
diff --git a/src/itemactions/setfoldericonitemaction.cpp b/src/itemactions/setfoldericonitemaction.cpp
new file mode 100644 (file)
index 0000000..744283f
--- /dev/null
@@ -0,0 +1,253 @@
+/*
+ * SPDX-FileCopyrightText: 2025 Méven Car <meven@kde.org>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "setfoldericonitemaction.h"
+#include "../dolphindebug.h"
+
+#include <KConfigGroup>
+#include <KDesktopFile>
+#include <KFileItem>
+#include <KLocalizedString>
+#include <KPluginFactory>
+#ifdef QT_DBUS_LIB
+#include <KDirNotify>
+#endif
+
+#include <QActionGroup>
+#include <QBoxLayout>
+#include <QEvent>
+#include <QFocusEvent>
+#include <QKeyEvent>
+#include <QMenu>
+#include <QPushButton>
+#include <QUrl>
+#include <QWidgetAction>
+
+K_PLUGIN_CLASS_WITH_JSON(SetFolderIconItemAction, "setfoldericonitemaction.json")
+
+namespace
+{
+bool isDefaultFolderIcon(const QString &iconName)
+{
+    return iconName.isEmpty() || iconName == QLatin1String("folder") || iconName == QLatin1String("inode-directory");
+}
+}
+
+SetFolderIconItemAction::SetFolderIconItemAction(QObject *parent)
+    : KAbstractFileItemActionPlugin(parent)
+{
+}
+
+void SetFolderIconItemAction::setFolderIcon(bool check)
+{
+    QAction *action = qobject_cast<QAction *>(sender());
+    Q_ASSERT(action);
+
+    action->setChecked(check);
+
+    auto iconName = action->icon().name();
+
+    // Apply custom folder icon, if applicable.
+    const QString fileName = m_localUrl.toLocalFile() + QLatin1String("/.directory");
+    KDesktopFile desktopFile{fileName};
+
+    if (check && !isDefaultFolderIcon(iconName)) {
+        desktopFile.desktopGroup().writeEntry(QStringLiteral("Icon"), iconName);
+    } else {
+        desktopFile.desktopGroup().deleteEntry(QStringLiteral("Icon"));
+        if (desktopFile.desktopGroup().entryMap().isEmpty() && QFile::exists(fileName)) {
+            // clean file
+            QFile::remove(fileName);
+        }
+    }
+
+#ifdef QT_DBUS_LIB
+    org::kde::KDirNotify::emitFilesChanged({m_url});
+#endif
+}
+
+class ButtonsWithSubMenuWidgetAction : public QWidgetAction
+{
+public:
+    ButtonsWithSubMenuWidgetAction(QMenu *subMenu, QWidget *parentWidget)
+        : QWidgetAction(parentWidget)
+        , m_subMenu(subMenu)
+    {
+    }
+
+    void setActions(const QList<QAction *> actions)
+    {
+        m_actions = actions;
+    }
+
+    bool eventFilter(QObject *object, QEvent *event) override
+    {
+        if (event->type() == QEvent::KeyPress) {
+            const QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
+            auto widget = qobject_cast<QWidget *>(object);
+
+            if (keyEvent->keyCombination() == QKeyCombination(Qt::Modifier::SHIFT, Qt::Key_Backtab) || keyEvent->key() == Qt::Key_Left
+                || keyEvent->key() == Qt::Key_Up) {
+                auto previous = widget->previousInFocusChain();
+                if (previous == widget->parentWidget()) {
+                    // the next object is the parent, let the focus bubble up
+                    return false;
+                }
+
+                previous->setFocus(Qt::BacktabFocusReason);
+                event->accept();
+                return true;
+            }
+
+            if (keyEvent->keyCombination() == QKeyCombination(Qt::Key_Tab) || keyEvent->key() == Qt::Key_Right || keyEvent->key() == Qt::Key_Down) {
+                auto next = widget->nextInFocusChain();
+                if (next->parentWidget() != widget->parentWidget()) {
+                    // the next object is not a sibling, let the focus bubble up
+                    return false;
+                }
+
+                next->setFocus(Qt::TabFocusReason);
+                event->accept();
+                return true;
+            }
+        }
+
+        // TODO implement proper SHIFT+TAB
+        // See https://bugreports.qt.io/browse/QTBUG-137298
+
+        return false;
+    }
+
+    QWidget *createWidget(QWidget *parent) override
+    {
+        QWidget *widget = new QWidget(parent);
+        auto layout = new QHBoxLayout(widget);
+
+        bool firstAction = false;
+        for (const auto action : std::as_const(m_actions)) {
+            action->setParent(widget);
+
+            auto p = new QPushButton(widget);
+
+            p->setIcon(action->icon());
+            p->setCheckable(true);
+            p->setChecked(action->isChecked());
+            p->setToolTip(action->toolTip());
+            p->installEventFilter(this);
+
+            connect(p, &QPushButton::clicked, action, &QAction::triggered);
+            connect(action, &QAction::toggled, p, &QPushButton::setChecked);
+
+            layout->addWidget(p);
+
+            if (!firstAction) {
+                widget->setFocusProxy(p);
+                firstAction = true;
+            }
+        }
+
+        auto p = new QPushButton(widget);
+        p->setText(i18nc("@action open a submeun with additional entries", "Other"));
+        p->setToolTip(i18nc("@label", "Other folder icon options"));
+        p->setMenu(m_subMenu);
+        layout->addWidget(p);
+        p->installEventFilter(this);
+
+        widget->setFocusPolicy(Qt::StrongFocus);
+
+        return widget;
+    }
+
+    QList<QAction *> m_actions;
+    QMenu *m_subMenu;
+};
+
+QList<QAction *> SetFolderIconItemAction::actions(const KFileItemListProperties &fileItemInfos, QWidget *parentWidget)
+{
+    if (fileItemInfos.items().count() != 1) {
+        return {};
+    }
+
+    auto fileItem = fileItemInfos.items().at(0);
+    m_url = fileItem.url();
+
+    bool local;
+    m_localUrl = fileItem.mostLocalUrl(&local);
+    if (!local || !fileItemInfos.supportsWriting() || !fileItem.isWritable()) {
+        return {};
+    }
+    const short s_numberOfEntriesVisible = 5;
+
+    using StringPair = QPair<KLocalizedString, QString>;
+    // keep in sync with kio/src/filewidgets/knewfilemenu.cpp
+    // default folder icon goes here.
+    const QList<StringPair> icons = {// colors.
+                                     StringPair{ki18nc("@label as in default folder color", "Red"), QStringLiteral("folder-red")},
+                                     StringPair{ki18nc("@label as in default folder color", "Yellow"), QStringLiteral("folder-yellow")},
+                                     StringPair{ki18nc("@label as in default folder color", "Orange"), QStringLiteral("folder-orange")},
+                                     StringPair{ki18nc("@label as in default folder color", "Green"), QStringLiteral("folder-green")},
+                                     StringPair{ki18nc("@label as in default folder color", "Cyan"), QStringLiteral("folder-cyan")},
+                                     // must match s_numberOfEntriesVisible
+                                     StringPair{ki18nc("@label: as in default folder icon", "Default"), QStringLiteral("inode-directory")},
+
+                                     StringPair{ki18nc("@label as in default folder color", "Blue"), QStringLiteral("folder-blue")},
+                                     StringPair{ki18nc("@label as in default folder color", "Violet"), QStringLiteral("folder-violet")},
+                                     StringPair{ki18nc("@label as in default folder color", "Brown"), QStringLiteral("folder-brown")},
+                                     StringPair{ki18nc("@label as in default folder color", "Grey"), QStringLiteral("folder-grey")},
+
+                                     // emblems.
+                                     StringPair{ki18nc("@label as in default folder color", "Bookmark"), QStringLiteral("folder-bookmark")},
+                                     StringPair{ki18nc("@label as in default folder color", "Cloud"), QStringLiteral("folder-cloud")},
+                                     StringPair{ki18nc("@label as in default folder color", "Development"), QStringLiteral("folder-development")},
+                                     StringPair{ki18nc("@label as in default folder color", "Games"), QStringLiteral("folder-games")},
+                                     StringPair{ki18nc("@label as in default folder color", "Mail"), QStringLiteral("folder-mail")},
+                                     StringPair{ki18nc("@label as in default folder color", "Music"), QStringLiteral("folder-music")},
+                                     StringPair{ki18nc("@label as in default folder color", "Print"), QStringLiteral("folder-print")},
+                                     StringPair{ki18nc("@label as in default folder color", "Compressed"), QStringLiteral("folder-tar")},
+                                     StringPair{ki18nc("@label as in default folder color", "Temporary"), QStringLiteral("folder-temp")},
+                                     StringPair{ki18nc("@label as in default folder color", "Important"), QStringLiteral("folder-important")}};
+
+    QActionGroup *actiongroup = new QActionGroup(this);
+    actiongroup->setExclusionPolicy(QActionGroup::ExclusionPolicy::ExclusiveOptional);
+
+    QMenu *subMenu = new QMenu();
+    auto action = new ButtonsWithSubMenuWidgetAction(subMenu, parentWidget);
+
+    int i = 0;
+    QList<QAction *> actions;
+    const auto fileIconName = fileItem.iconName();
+    for (const auto &[name, iconName] : icons) {
+        auto icon = QIcon::fromTheme(iconName);
+        if (icon.isNull()) {
+            qCWarning(DolphinDebug) << "SetFolderIconItemAction Missing icon:" << iconName;
+            continue;
+        }
+
+        QAction *folderIconAction = new QAction(KLocalizedString(name).toString(), parentWidget);
+        folderIconAction->setIcon(icon);
+        folderIconAction->setCheckable(true);
+        folderIconAction->setChecked(fileIconName == iconName);
+        folderIconAction->setToolTip(i18nc("@label %1 is a folder icon name (Red, Music...) etc", "Set folder icon to %1", folderIconAction->iconText()));
+        actiongroup->addAction(folderIconAction);
+
+        connect(folderIconAction, &QAction::triggered, this, &SetFolderIconItemAction::setFolderIcon);
+        connect(folderIconAction, &QAction::triggered, action, &QAction::triggered);
+
+        ++i;
+        if (i < s_numberOfEntriesVisible + 1) {
+            actions.append(folderIconAction);
+        } else {
+            folderIconAction->setParent(subMenu);
+            subMenu->addAction(folderIconAction);
+        }
+    }
+    action->setActions(actions);
+
+    return {action};
+}
+
+#include "moc_setfoldericonitemaction.cpp"
+#include "setfoldericonitemaction.moc"
diff --git a/src/itemactions/setfoldericonitemaction.h b/src/itemactions/setfoldericonitemaction.h
new file mode 100644 (file)
index 0000000..c2ebbd5
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * SPDX-FileCopyrightText: 2025 Méven Car <meven@kde.org>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#pragma once
+
+#include <KAbstractFileItemActionPlugin>
+#include <KFileItemListProperties>
+
+class SetFolderIconItemAction : public KAbstractFileItemActionPlugin
+{
+    Q_OBJECT
+
+public:
+    SetFolderIconItemAction(QObject *parent);
+
+    QList<QAction *> actions(const KFileItemListProperties &fileItemInfos, QWidget *parentWidget) override;
+
+private:
+    void setFolderIcon(bool check);
+    QUrl m_url;
+    QUrl m_localUrl;
+};
diff --git a/src/itemactions/setfoldericonitemaction.json b/src/itemactions/setfoldericonitemaction.json
new file mode 100644 (file)
index 0000000..bca39ac
--- /dev/null
@@ -0,0 +1,11 @@
+{
+    "KPlugin": {
+        "Icon": "folder-new",
+        "MimeTypes": [
+            "inode/directory"
+        ],
+        "Name": "Set Folder Icon"
+    },
+    "X-KDE-Require": "Write",
+    "X-KDE-Show-In-Submenu": "true"
+}