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 | |
32 | static constexpr int s_maxRecentDirs = 10; // Hardcoded max size |
33 | |
34 | 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 | |
45 | KFileCopyToMenu::(QWidget *parentWidget) |
46 | : QObject(parentWidget) |
47 | , d(new KFileCopyToMenuPrivate(this, parentWidget)) |
48 | { |
49 | } |
50 | |
51 | KFileCopyToMenu::() = default; |
52 | |
53 | void KFileCopyToMenu::(const QList<QUrl> &urls) |
54 | { |
55 | d->m_urls = urls; |
56 | } |
57 | |
58 | void KFileCopyToMenu::(bool ro) |
59 | { |
60 | d->m_readOnly = ro; |
61 | } |
62 | |
63 | void KFileCopyToMenu::setAutoErrorHandlingEnabled(bool b) |
64 | { |
65 | d->m_autoErrorHandling = b; |
66 | } |
67 | |
68 | void KFileCopyToMenu::(QMenu *) 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 | |
85 | KFileCopyToMainMenu::KFileCopyToMainMenu(QMenu *parent, KFileCopyToMenuPrivate *_d, 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 | |
96 | void KFileCopyToMainMenu::slotAboutToShow() |
97 | { |
98 | clear(); |
99 | KFileCopyToDirectoryMenu *; |
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 | |
166 | void 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 | |
174 | void KFileCopyToMainMenu::slotTriggered(QAction *action) |
175 | { |
176 | const QUrl url = action->data().toUrl(); |
177 | Q_ASSERT(!url.isEmpty()); |
178 | copyOrMoveTo(dest: url); |
179 | } |
180 | |
181 | void 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 | |
214 | KFileCopyToDirectoryMenu::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 | |
222 | void KFileCopyToDirectoryMenu::() |
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 * = new KFileCopyToDirectoryMenu(this, m_mainMenu, subPath); |
244 | QString (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 | |