From 697d58e9727e229abb81956d27a05d1f02d8c775 Mon Sep 17 00:00:00 2001 From: =?utf8?q?M=C3=A9ven=20Car?= Date: Mon, 9 Jun 2025 12:16:36 +0000 Subject: [PATCH] Add a SetFolderIcon ItemAction plugin To allow to change folder icon from the context menu. CCBUG: 467221 --- src/CMakeLists.txt | 12 +- src/dolphincontextmenu.cpp | 5 +- src/itemactions/CMakeLists.txt | 26 ++ src/itemactions/setfoldericonitemaction.cpp | 253 +++++++++++++++++++ src/itemactions/setfoldericonitemaction.h | 25 ++ src/itemactions/setfoldericonitemaction.json | 11 + 6 files changed, 319 insertions(+), 13 deletions(-) create mode 100644 src/itemactions/CMakeLists.txt create mode 100644 src/itemactions/setfoldericonitemaction.cpp create mode 100644 src/itemactions/setfoldericonitemaction.h create mode 100644 src/itemactions/setfoldericonitemaction.json diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3bf64454e..5b02a0f76 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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) diff --git a/src/dolphincontextmenu.cpp b/src/dolphincontextmenu.cpp index 8372060aa..e1c67aad1 100644 --- a/src/dolphincontextmenu.cpp +++ b/src/dolphincontextmenu.cpp @@ -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 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 index 000000000..6610b0e4a --- /dev/null +++ b/src/itemactions/CMakeLists.txt @@ -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 index 000000000..744283f2f --- /dev/null +++ b/src/itemactions/setfoldericonitemaction.cpp @@ -0,0 +1,253 @@ +/* + * SPDX-FileCopyrightText: 2025 Méven Car + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "setfoldericonitemaction.h" +#include "../dolphindebug.h" + +#include +#include +#include +#include +#include +#ifdef QT_DBUS_LIB +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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(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 actions) + { + m_actions = actions; + } + + bool eventFilter(QObject *object, QEvent *event) override + { + if (event->type() == QEvent::KeyPress) { + const QKeyEvent *keyEvent = static_cast(event); + auto widget = qobject_cast(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 m_actions; + QMenu *m_subMenu; +}; + +QList 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; + // keep in sync with kio/src/filewidgets/knewfilemenu.cpp + // default folder icon goes here. + const QList 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 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 index 000000000..c2ebbd521 --- /dev/null +++ b/src/itemactions/setfoldericonitemaction.h @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: 2025 Méven Car + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#pragma once + +#include +#include + +class SetFolderIconItemAction : public KAbstractFileItemActionPlugin +{ + Q_OBJECT + +public: + SetFolderIconItemAction(QObject *parent); + + QList 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 index 000000000..bca39ac78 --- /dev/null +++ b/src/itemactions/setfoldericonitemaction.json @@ -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" +} -- 2.47.3