1/*
2 SPDX-FileCopyrightText: 2008, 2009, 2015 David Faure <faure@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
5*/
6
7#include "kfilecopytomenu.h"
8#include "kfilecopytomenu_p.h"
9
10#include <QAction>
11#include <QDir>
12#include <QFileDialog>
13#include <QIcon>
14#include <QMimeDatabase>
15#include <QMimeType>
16
17#include "../utils_p.h"
18
19#include <KIO/CopyJob>
20#include <KIO/FileUndoManager>
21#include <KIO/JobUiDelegate>
22
23#include <KJobWidgets>
24#include <KLocalizedString>
25#include <KSharedConfig>
26#include <KStringHandler>
27
28#ifdef Q_OS_WIN
29#include "windows.h"
30#endif
31
32static constexpr int s_maxRecentDirs = 10; // Hardcoded max size
33
34KFileCopyToMenuPrivate::KFileCopyToMenuPrivate(KFileCopyToMenu *qq, QWidget *parentWidget)
35 : q(qq)
36 , m_urls()
37 , m_parentWidget(parentWidget)
38 , m_readOnly(false)
39 , m_autoErrorHandling(false)
40{
41}
42
43////
44
45KFileCopyToMenu::KFileCopyToMenu(QWidget *parentWidget)
46 : QObject(parentWidget)
47 , d(new KFileCopyToMenuPrivate(this, parentWidget))
48{
49}
50
51KFileCopyToMenu::~KFileCopyToMenu() = default;
52
53void KFileCopyToMenu::setUrls(const QList<QUrl> &urls)
54{
55 d->m_urls = urls;
56}
57
58void KFileCopyToMenu::setReadOnly(bool ro)
59{
60 d->m_readOnly = ro;
61}
62
63void KFileCopyToMenu::setAutoErrorHandlingEnabled(bool b)
64{
65 d->m_autoErrorHandling = b;
66}
67
68void KFileCopyToMenu::addActionsTo(QMenu *menu) const
69{
70 QMenu *mainCopyMenu = new KFileCopyToMainMenu(menu, d.get(), Copy);
71 mainCopyMenu->setTitle(i18nc("@title:menu", "Copy To"));
72 mainCopyMenu->menuAction()->setObjectName(QStringLiteral("copyTo_submenu")); // for the unittest
73 menu->addMenu(menu: mainCopyMenu);
74
75 if (!d->m_readOnly) {
76 QMenu *mainMoveMenu = new KFileCopyToMainMenu(menu, d.get(), Move);
77 mainMoveMenu->setTitle(i18nc("@title:menu", "Move To"));
78 mainMoveMenu->menuAction()->setObjectName(QStringLiteral("moveTo_submenu")); // for the unittest
79 menu->addMenu(menu: mainMoveMenu);
80 }
81}
82
83////
84
85KFileCopyToMainMenu::KFileCopyToMainMenu(QMenu *parent, KFileCopyToMenuPrivate *_d, MenuType menuType)
86 : QMenu(parent)
87 , m_menuType(menuType)
88 , m_actionGroup(static_cast<QWidget *>(nullptr))
89 , d(_d)
90 , m_recentDirsGroup(KSharedConfig::openConfig(), m_menuType == Copy ? QStringLiteral("kuick-copy") : QStringLiteral("kuick-move"))
91{
92 connect(sender: this, signal: &KFileCopyToMainMenu::aboutToShow, context: this, slot: &KFileCopyToMainMenu::slotAboutToShow);
93 connect(sender: &m_actionGroup, signal: &QActionGroup::triggered, context: this, slot: &KFileCopyToMainMenu::slotTriggered);
94}
95
96void KFileCopyToMainMenu::slotAboutToShow()
97{
98 clear();
99 KFileCopyToDirectoryMenu *subMenu;
100 // Home Folder
101 subMenu = new KFileCopyToDirectoryMenu(this, this, QDir::homePath());
102 subMenu->setTitle(i18nc("@title:menu", "Home Folder"));
103 subMenu->setIcon(QIcon::fromTheme(QStringLiteral("go-home")));
104 QAction *act = addMenu(menu: subMenu);
105 act->setObjectName(QStringLiteral("home"));
106
107 // Root Folder
108#ifndef Q_OS_WIN
109 subMenu = new KFileCopyToDirectoryMenu(this, this, QDir::rootPath());
110 subMenu->setTitle(i18nc("@title:menu", "Root Folder"));
111 subMenu->setIcon(QIcon::fromTheme(QStringLiteral("folder-red")));
112 act = addMenu(menu: subMenu);
113 act->setObjectName(QStringLiteral("root"));
114#else
115 const QFileInfoList drives = QDir::drives();
116 for (const QFileInfo &info : drives) {
117 QString driveIcon = QStringLiteral("drive-harddisk");
118 const uint type = GetDriveTypeW((wchar_t *)info.absoluteFilePath().utf16());
119 switch (type) {
120 case DRIVE_REMOVABLE:
121 driveIcon = QStringLiteral("drive-removable-media");
122 break;
123 case DRIVE_FIXED:
124 driveIcon = QStringLiteral("drive-harddisk");
125 break;
126 case DRIVE_REMOTE:
127 driveIcon = QStringLiteral("network-server");
128 break;
129 case DRIVE_CDROM:
130 driveIcon = QStringLiteral("drive-optical");
131 break;
132 case DRIVE_RAMDISK:
133 case DRIVE_UNKNOWN:
134 case DRIVE_NO_ROOT_DIR:
135 default:
136 driveIcon = QStringLiteral("drive-harddisk");
137 }
138 subMenu = new KFileCopyToDirectoryMenu(this, this, info.absoluteFilePath());
139 subMenu->setTitle(info.absoluteFilePath());
140 subMenu->setIcon(QIcon::fromTheme(driveIcon));
141 addMenu(subMenu);
142 }
143#endif
144
145 // Browse... action, shows a file dialog
146 QAction *browseAction = new QAction(i18nc("@title:menu in Copy To or Move To submenu", "Browse..."), this);
147 browseAction->setObjectName(QStringLiteral("browse"));
148 connect(sender: browseAction, signal: &QAction::triggered, context: this, slot: &KFileCopyToMainMenu::slotBrowse);
149 addAction(action: browseAction);
150
151 addSeparator(); // Qt handles removing it automatically if it's last in the menu, nice.
152
153 // Recent Destinations
154 const QStringList recentDirs = m_recentDirsGroup.readPathEntry(key: "Paths", aDefault: QStringList());
155 for (const QString &recentDir : recentDirs) {
156 const QUrl url = QUrl::fromLocalFile(localfile: recentDir);
157 const QString text = KStringHandler::csqueeze(str: url.toDisplayString(options: QUrl::PreferLocalFile), maxlen: 60); // shorten very long paths (#61386)
158 QAction *act = new QAction(text, this);
159 act->setObjectName(recentDir);
160 act->setData(url);
161 m_actionGroup.addAction(a: act);
162 addAction(action: act);
163 }
164}
165
166void KFileCopyToMainMenu::slotBrowse()
167{
168 const QUrl dest = QFileDialog::getExistingDirectoryUrl(parent: d->m_parentWidget ? d->m_parentWidget : this);
169 if (!dest.isEmpty()) {
170 copyOrMoveTo(dest);
171 }
172}
173
174void KFileCopyToMainMenu::slotTriggered(QAction *action)
175{
176 const QUrl url = action->data().toUrl();
177 Q_ASSERT(!url.isEmpty());
178 copyOrMoveTo(dest: url);
179}
180
181void KFileCopyToMainMenu::copyOrMoveTo(const QUrl &dest)
182{
183 // Insert into the recent destinations list
184 QStringList recentDirs = m_recentDirsGroup.readPathEntry(key: "Paths", aDefault: QStringList());
185 const QString niceDest = dest.toDisplayString(options: QUrl::PreferLocalFile);
186 if (!recentDirs.contains(str: niceDest)) { // don't change position if already there, moving stuff is bad usability
187 recentDirs.prepend(t: niceDest);
188 if (recentDirs.size() > s_maxRecentDirs) {
189 recentDirs.erase(abegin: recentDirs.begin() + s_maxRecentDirs, aend: recentDirs.end());
190 }
191 m_recentDirsGroup.writePathEntry(key: "Paths", value: recentDirs);
192 }
193
194 // #199549: add a trailing slash to avoid unexpected results when the
195 // dest doesn't exist anymore: it was creating a file with the name of
196 // the now non-existing dest.
197 QUrl dirDest = dest;
198 Utils::appendSlashToPath(url&: dirDest);
199
200 // And now let's do the copy or move -- with undo/redo support.
201 KIO::CopyJob *job = m_menuType == Copy ? KIO::copy(src: d->m_urls, dest: dirDest) : KIO::move(src: d->m_urls, dest: dirDest);
202 KIO::FileUndoManager::self()->recordCopyJob(copyJob: job);
203 KJobWidgets::setWindow(job, widget: d->m_parentWidget ? d->m_parentWidget : this);
204 if (job->uiDelegate()) {
205 job->uiDelegate()->setAutoErrorHandlingEnabled(d->m_autoErrorHandling);
206 }
207 connect(sender: job, signal: &KIO::CopyJob::result, context: this, slot: [this](KJob *job) {
208 Q_EMIT d->q->error(errorCode: job->error(), message: job->errorString());
209 });
210}
211
212////
213
214KFileCopyToDirectoryMenu::KFileCopyToDirectoryMenu(QMenu *parent, KFileCopyToMainMenu *mainMenu, const QString &path)
215 : QMenu(parent)
216 , m_mainMenu(mainMenu)
217 , m_path(Utils::slashAppended(s: path))
218{
219 connect(sender: this, signal: &KFileCopyToDirectoryMenu::aboutToShow, context: this, slot: &KFileCopyToDirectoryMenu::slotAboutToShow);
220}
221
222void KFileCopyToDirectoryMenu::slotAboutToShow()
223{
224 clear();
225 QAction *act = new QAction(m_mainMenu->menuType() == Copy ? i18nc("@title:menu", "Copy Here") : i18nc("@title:menu", "Move Here"), this);
226 act->setData(QUrl::fromLocalFile(localfile: m_path));
227 act->setEnabled(QFileInfo(m_path).isWritable());
228 m_mainMenu->actionGroup().addAction(a: act);
229 addAction(action: act);
230
231 addSeparator(); // Qt handles removing it automatically if it's last in the menu, nice.
232
233 // List directory
234 // All we need is sub folder names, their permissions, their icon.
235 // KDirLister or KIO::listDir would fetch much more info, and would be async,
236 // and we only care about local directories so we use QDir directly.
237 QDir dir(m_path);
238 const QStringList entries = dir.entryList(filters: QDir::Dirs | QDir::NoDotAndDotDot, sort: QDir::LocaleAware);
239 const QMimeDatabase db;
240 const QMimeType dirMime = db.mimeTypeForName(QStringLiteral("inode/directory"));
241 for (const QString &subDir : entries) {
242 QString subPath = m_path + subDir;
243 KFileCopyToDirectoryMenu *subMenu = new KFileCopyToDirectoryMenu(this, m_mainMenu, subPath);
244 QString menuTitle(subDir);
245 // Replace '&' by "&&" to make sure that '&' inside the directory name is displayed
246 // correctly and not misinterpreted as an indicator for a keyboard shortcut
247 subMenu->setTitle(menuTitle.replace(c: QLatin1Char('&'), after: QLatin1String("&&")));
248 const QString iconName = dirMime.iconName();
249 subMenu->setIcon(QIcon::fromTheme(name: iconName));
250 if (QFileInfo(subPath).isSymLink()) {
251 QFont font = subMenu->menuAction()->font();
252 font.setItalic(true);
253 subMenu->menuAction()->setFont(font);
254 }
255 addMenu(menu: subMenu);
256 }
257}
258
259#include "moc_kfilecopytomenu.cpp"
260#include "moc_kfilecopytomenu_p.cpp"
261

source code of kio/src/filewidgets/kfilecopytomenu.cpp