--- /dev/null
+/*
+ * 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"