]> cloud.milkyroute.net Git - dolphin.git/blob - src/dolphincontextmenu.cpp
Reuse KFileItemActions instance during lifetime of dolphin
[dolphin.git] / src / dolphincontextmenu.cpp
1 /*
2 * SPDX-FileCopyrightText: 2006 Peter Penz (peter.penz@gmx.at) and Cvetoslav Ludmiloff
3 *
4 * SPDX-License-Identifier: GPL-2.0-or-later
5 */
6
7 #include "dolphincontextmenu.h"
8
9 #include "dolphin_generalsettings.h"
10 #include "dolphin_contextmenusettings.h"
11 #include "dolphinmainwindow.h"
12 #include "dolphinnewfilemenu.h"
13 #include "dolphinplacesmodelsingleton.h"
14 #include "dolphinremoveaction.h"
15 #include "dolphinviewcontainer.h"
16 #include "panels/places/placesitem.h"
17 #include "panels/places/placesitemmodel.h"
18 #include "trash/dolphintrash.h"
19 #include "views/dolphinview.h"
20 #include "views/viewmodecontroller.h"
21
22 #include <KActionCollection>
23 #include <KFileItemActions>
24 #include <KFileItemListProperties>
25 #include <KHamburgerMenu>
26 #include <KIO/EmptyTrashJob>
27 #include <KIO/JobUiDelegate>
28 #include <KIO/Paste>
29 #include <KIO/RestoreJob>
30 #include <KJobWidgets>
31 #include <KLocalizedString>
32 #include <KNewFileMenu>
33 #include <KPluginMetaData>
34 #include <KStandardAction>
35 #include <KToolBar>
36
37 #include <QApplication>
38 #include <QClipboard>
39 #include <QKeyEvent>
40 #include <QMenuBar>
41 #include <QMimeDatabase>
42
43 DolphinContextMenu::DolphinContextMenu(DolphinMainWindow* parent,
44 const QPoint& pos,
45 const KFileItem& fileInfo,
46 const QUrl& baseUrl,
47 KFileItemActions *fileItemActions) :
48 QMenu(parent),
49 m_pos(pos),
50 m_mainWindow(parent),
51 m_fileInfo(fileInfo),
52 m_baseUrl(baseUrl),
53 m_baseFileItem(nullptr),
54 m_selectedItems(),
55 m_selectedItemsProperties(nullptr),
56 m_context(NoContext),
57 m_copyToMenu(parent),
58 m_customActions(),
59 m_command(None),
60 m_removeAction(nullptr),
61 m_fileItemActions(fileItemActions)
62 {
63 // The context menu either accesses the URLs of the selected items
64 // or the items itself. To increase the performance both lists are cached.
65 const DolphinView* view = m_mainWindow->activeViewContainer()->view();
66 m_selectedItems = view->selectedItems();
67
68 QApplication::instance()->installEventFilter(this);
69
70 static_cast<KHamburgerMenu *>(m_mainWindow->actionCollection()->
71 action(QStringLiteral("hamburger_menu")))->addToMenu(this);
72 }
73
74 DolphinContextMenu::~DolphinContextMenu()
75 {
76 delete m_baseFileItem;
77 m_baseFileItem = nullptr;
78 delete m_selectedItemsProperties;
79 m_selectedItemsProperties = nullptr;
80 }
81
82 void DolphinContextMenu::setCustomActions(const QList<QAction*>& actions)
83 {
84 m_customActions = actions;
85 }
86
87 DolphinContextMenu::Command DolphinContextMenu::open()
88 {
89 // get the context information
90 const auto scheme = m_baseUrl.scheme();
91 if (scheme == QLatin1String("trash")) {
92 m_context |= TrashContext;
93 } else if (scheme.contains(QLatin1String("search"))) {
94 m_context |= SearchContext;
95 } else if (scheme.contains(QLatin1String("timeline"))) {
96 m_context |= TimelineContext;
97 }
98
99 if (!m_fileInfo.isNull() && !m_selectedItems.isEmpty()) {
100 m_context |= ItemContext;
101 // TODO: handle other use cases like devices + desktop files
102 }
103
104 // open the corresponding popup for the context
105 if (m_context & TrashContext) {
106 if (m_context & ItemContext) {
107 openTrashItemContextMenu();
108 } else {
109 openTrashContextMenu();
110 }
111 } else if (m_context & ItemContext) {
112 openItemContextMenu();
113 } else {
114 openViewportContextMenu();
115 }
116
117 return m_command;
118 }
119
120 bool DolphinContextMenu::eventFilter(QObject* object, QEvent* event)
121 {
122 Q_UNUSED(object)
123
124 if(event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease) {
125 QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
126
127 if (m_removeAction && keyEvent->key() == Qt::Key_Shift) {
128 if (event->type() == QEvent::KeyPress) {
129 m_removeAction->update(DolphinRemoveAction::ShiftState::Pressed);
130 } else {
131 m_removeAction->update(DolphinRemoveAction::ShiftState::Released);
132 }
133 }
134 }
135
136 return false;
137 }
138
139 void DolphinContextMenu::openTrashContextMenu()
140 {
141 Q_ASSERT(m_context & TrashContext);
142
143 QAction* emptyTrashAction = new QAction(QIcon::fromTheme(QStringLiteral("trash-empty")), i18nc("@action:inmenu", "Empty Trash"), this);
144 emptyTrashAction->setEnabled(!Trash::isEmpty());
145 addAction(emptyTrashAction);
146
147 addCustomActions();
148
149 QAction* propertiesAction = m_mainWindow->actionCollection()->action(QStringLiteral("properties"));
150 addAction(propertiesAction);
151
152 if (exec(m_pos) == emptyTrashAction) {
153 Trash::empty(m_mainWindow);
154 }
155 }
156
157 void DolphinContextMenu::openTrashItemContextMenu()
158 {
159 Q_ASSERT(m_context & TrashContext);
160 Q_ASSERT(m_context & ItemContext);
161
162 QAction* restoreAction = new QAction(QIcon::fromTheme("restoration"), i18nc("@action:inmenu", "Restore"), m_mainWindow);
163 addAction(restoreAction);
164
165 QAction* deleteAction = m_mainWindow->actionCollection()->action(KStandardAction::name(KStandardAction::DeleteFile));
166 addAction(deleteAction);
167
168 QAction* propertiesAction = m_mainWindow->actionCollection()->action(QStringLiteral("properties"));
169 addAction(propertiesAction);
170
171 if (exec(m_pos) == restoreAction) {
172 QList<QUrl> selectedUrls;
173 selectedUrls.reserve(m_selectedItems.count());
174 for (const KFileItem &item : qAsConst(m_selectedItems)) {
175 selectedUrls.append(item.url());
176 }
177
178 KIO::RestoreJob *job = KIO::restoreFromTrash(selectedUrls);
179 KJobWidgets::setWindow(job, m_mainWindow);
180 job->uiDelegate()->setAutoErrorHandlingEnabled(true);
181 }
182 }
183
184 void DolphinContextMenu::addDirectoryItemContextMenu()
185 {
186 // insert 'Open in new window' and 'Open in new tab' entries
187 const KFileItemListProperties& selectedItemsProps = selectedItemsProperties();
188 if (ContextMenuSettings::showOpenInNewTab()) {
189 addAction(m_mainWindow->actionCollection()->action(QStringLiteral("open_in_new_tab")));
190 }
191 if (ContextMenuSettings::showOpenInNewWindow()) {
192 addAction(m_mainWindow->actionCollection()->action(QStringLiteral("open_in_new_window")));
193 }
194
195 // Insert 'Open With' entries
196 addOpenWithActions();
197
198 // set up 'Create New' menu
199 DolphinNewFileMenu* newFileMenu = new DolphinNewFileMenu(m_mainWindow->actionCollection(), m_mainWindow);
200 const DolphinView* view = m_mainWindow->activeViewContainer()->view();
201 newFileMenu->setViewShowsHiddenFiles(view->hiddenFilesShown());
202 newFileMenu->checkUpToDate();
203 newFileMenu->setPopupFiles(QList<QUrl>() << m_fileInfo.url());
204 newFileMenu->setEnabled(selectedItemsProps.supportsWriting());
205 connect(newFileMenu, &DolphinNewFileMenu::fileCreated, newFileMenu, &DolphinNewFileMenu::deleteLater);
206 connect(newFileMenu, &DolphinNewFileMenu::directoryCreated, newFileMenu, &DolphinNewFileMenu::deleteLater);
207
208 QMenu* menu = newFileMenu->menu();
209 menu->setTitle(i18nc("@title:menu Create new folder, file, link, etc.", "Create New"));
210 menu->setIcon(QIcon::fromTheme(QStringLiteral("list-add")));
211 addMenu(menu);
212
213 addSeparator();
214 }
215
216 void DolphinContextMenu::openItemContextMenu()
217 {
218 Q_ASSERT(!m_fileInfo.isNull());
219
220 QAction* openParentAction = nullptr;
221 QAction* openParentInNewWindowAction = nullptr;
222 QAction* openParentInNewTabAction = nullptr;
223 const KFileItemListProperties& selectedItemsProps = selectedItemsProperties();
224
225
226 m_fileItemActions->setItemListProperties(selectedItemsProps);
227
228 if (m_selectedItems.count() == 1) {
229 // single files
230 if (m_fileInfo.isDir()) {
231 addDirectoryItemContextMenu();
232 } else if (m_context & TimelineContext || m_context & SearchContext) {
233 addOpenWithActions();
234
235 openParentAction = new QAction(QIcon::fromTheme(QStringLiteral("document-open-folder")),
236 i18nc("@action:inmenu",
237 "Open Path"),
238 this);
239 addAction(openParentAction);
240
241 openParentInNewWindowAction = new QAction(QIcon::fromTheme(QStringLiteral("window-new")),
242 i18nc("@action:inmenu",
243 "Open Path in New Window"),
244 this);
245 addAction(openParentInNewWindowAction);
246
247 openParentInNewTabAction = new QAction(QIcon::fromTheme(QStringLiteral("tab-new")),
248 i18nc("@action:inmenu",
249 "Open Path in New Tab"),
250 this);
251 addAction(openParentInNewTabAction);
252
253 addSeparator();
254 } else {
255 // Insert 'Open With" entries
256 addOpenWithActions();
257 }
258 if (m_fileInfo.isLink()) {
259 addAction(m_mainWindow->actionCollection()->action(QStringLiteral("show_target")));
260 addSeparator();
261 }
262 } else {
263 // multiple files
264 bool selectionHasOnlyDirs = true;
265 for (const auto &item : qAsConst(m_selectedItems)) {
266 const QUrl& url = DolphinView::openItemAsFolderUrl(item);
267 if (url.isEmpty()) {
268 selectionHasOnlyDirs = false;
269 break;
270 }
271 }
272
273 if (selectionHasOnlyDirs && ContextMenuSettings::showOpenInNewTab()) {
274 // insert 'Open in new tab' entry
275 addAction(m_mainWindow->actionCollection()->action(QStringLiteral("open_in_new_tabs")));
276 }
277 // Insert 'Open With" entries
278 addOpenWithActions();
279 }
280
281 insertDefaultItemActions(selectedItemsProps);
282
283 addAdditionalActions(selectedItemsProps);
284
285 // insert 'Copy To' and 'Move To' sub menus
286 if (ContextMenuSettings::showCopyMoveMenu()) {
287 m_copyToMenu.setUrls(m_selectedItems.urlList());
288 m_copyToMenu.setReadOnly(!selectedItemsProps.supportsWriting());
289 m_copyToMenu.setAutoErrorHandlingEnabled(true);
290 m_copyToMenu.addActionsTo(this);
291 }
292
293 // insert 'Properties...' entry
294 addSeparator();
295 QAction* propertiesAction = m_mainWindow->actionCollection()->action(QStringLiteral("properties"));
296 addAction(propertiesAction);
297
298 QAction* activatedAction = exec(m_pos);
299 if (activatedAction) {
300 if (activatedAction == openParentAction) {
301 m_command = OpenParentFolder;
302 } else if (activatedAction == openParentInNewWindowAction) {
303 m_command = OpenParentFolderInNewWindow;
304 } else if (activatedAction == openParentInNewTabAction) {
305 m_command = OpenParentFolderInNewTab;
306 }
307 }
308 }
309
310 void DolphinContextMenu::openViewportContextMenu()
311 {
312 const DolphinView* view = m_mainWindow->activeViewContainer()->view();
313
314 const KFileItemListProperties baseUrlProperties(KFileItemList() << baseFileItem());
315 m_fileItemActions->setItemListProperties(baseUrlProperties);
316
317 // Set up and insert 'Create New' menu
318 KNewFileMenu* newFileMenu = m_mainWindow->newFileMenu();
319 newFileMenu->setViewShowsHiddenFiles(view->hiddenFilesShown());
320 newFileMenu->checkUpToDate();
321 newFileMenu->setPopupFiles(QList<QUrl>() << m_baseUrl);
322 addMenu(newFileMenu->menu());
323
324 // Show "open with" menu items even if the dir is empty, because there are legitimate
325 // use cases for this, such as opening an empty dir in Kate or VSCode or something
326 addOpenWithActions();
327
328 QAction* pasteAction = createPasteAction();
329 if (pasteAction) {
330 addAction(pasteAction);
331 }
332
333 // Insert 'Add to Places' entry if it's not already in the places panel
334 if (ContextMenuSettings::showAddToPlaces() &&
335 !placeExists(m_mainWindow->activeViewContainer()->url())) {
336 addAction(m_mainWindow->actionCollection()->action(QStringLiteral("add_to_places")));
337 }
338 addSeparator();
339
340 // Insert 'Sort By' and 'View Mode'
341 if (ContextMenuSettings::showSortBy()) {
342 addAction(m_mainWindow->actionCollection()->action(QStringLiteral("sort")));
343 }
344 if (ContextMenuSettings::showViewMode()) {
345 addAction(m_mainWindow->actionCollection()->action(QStringLiteral("view_mode")));
346 }
347 if (ContextMenuSettings::showSortBy() || ContextMenuSettings::showViewMode()) {
348 addSeparator();
349 }
350
351 addAdditionalActions(baseUrlProperties);
352 addCustomActions();
353
354 addSeparator();
355
356 QAction* propertiesAction = m_mainWindow->actionCollection()->action(QStringLiteral("properties"));
357 addAction(propertiesAction);
358
359 exec(m_pos);
360 }
361
362 void DolphinContextMenu::insertDefaultItemActions(const KFileItemListProperties& properties)
363 {
364 const KActionCollection* collection = m_mainWindow->actionCollection();
365
366 // Insert 'Cut', 'Copy', 'Copy Location' and 'Paste'
367 addAction(collection->action(KStandardAction::name(KStandardAction::Cut)));
368 addAction(collection->action(KStandardAction::name(KStandardAction::Copy)));
369 if (ContextMenuSettings::showCopyLocation()) {
370 QAction* copyPathAction = collection->action(QString("copy_location"));
371 copyPathAction->setEnabled(m_selectedItems.size() == 1);
372 addAction(copyPathAction);
373 }
374 QAction* pasteAction = createPasteAction();
375 if (pasteAction) {
376 addAction(pasteAction);
377 }
378
379 // Insert 'Duplicate Here'
380 if (ContextMenuSettings::showDuplicateHere()) {
381 addAction(m_mainWindow->actionCollection()->action(QStringLiteral("duplicate")));
382 }
383
384 // Insert 'Rename'
385 addAction(collection->action(KStandardAction::name(KStandardAction::RenameFile)));
386
387 // Insert 'Add to Places' entry if appropriate
388 if (ContextMenuSettings::showAddToPlaces() &&
389 m_selectedItems.count() == 1 &&
390 m_fileInfo.isDir() &&
391 !placeExists(m_fileInfo.url())) {
392 addAction(m_mainWindow->actionCollection()->action(QStringLiteral("add_to_places")));
393 }
394
395 addSeparator();
396
397 // Insert 'Move to Trash' and/or 'Delete'
398 const bool showDeleteAction = (KSharedConfig::openConfig()->group("KDE").readEntry("ShowDeleteCommand", false) ||
399 !properties.isLocal());
400 const bool showMoveToTrashAction = (properties.isLocal() &&
401 properties.supportsMoving());
402
403 if (showDeleteAction && showMoveToTrashAction) {
404 delete m_removeAction;
405 m_removeAction = nullptr;
406 addAction(m_mainWindow->actionCollection()->action(KStandardAction::name(KStandardAction::MoveToTrash)));
407 addAction(m_mainWindow->actionCollection()->action(KStandardAction::name(KStandardAction::DeleteFile)));
408 } else if (showDeleteAction && !showMoveToTrashAction) {
409 addAction(m_mainWindow->actionCollection()->action(KStandardAction::name(KStandardAction::DeleteFile)));
410 } else {
411 if (!m_removeAction) {
412 m_removeAction = new DolphinRemoveAction(this, m_mainWindow->actionCollection());
413 }
414 addAction(m_removeAction);
415 m_removeAction->update();
416 }
417 }
418
419 bool DolphinContextMenu::placeExists(const QUrl& url) const
420 {
421 const KFilePlacesModel* placesModel = DolphinPlacesModelSingleton::instance().placesModel();
422
423 const auto& matchedPlaces = placesModel->match(placesModel->index(0,0), KFilePlacesModel::UrlRole, url, 1, Qt::MatchExactly);
424
425 return !matchedPlaces.isEmpty();
426 }
427
428 QAction* DolphinContextMenu::createPasteAction()
429 {
430 QAction* action = nullptr;
431 KFileItem destItem;
432 if (!m_fileInfo.isNull() && m_selectedItems.count() <= 1) {
433 destItem = m_fileInfo;
434 } else {
435 destItem = baseFileItem();
436 }
437
438 if (!destItem.isNull() && destItem.isDir()) {
439 const QMimeData *mimeData = QApplication::clipboard()->mimeData();
440 bool canPaste;
441 const QString text = KIO::pasteActionText(mimeData, &canPaste, destItem);
442 if (canPaste) {
443 if (destItem == m_fileInfo) {
444 // if paste destination is a selected folder
445 action = new QAction(QIcon::fromTheme(QStringLiteral("edit-paste")), text, this);
446 connect(action, &QAction::triggered, m_mainWindow, &DolphinMainWindow::pasteIntoFolder);
447 } else {
448 action = m_mainWindow->actionCollection()->action(KStandardAction::name(KStandardAction::Paste));
449 }
450 }
451 }
452
453 return action;
454 }
455
456 KFileItemListProperties& DolphinContextMenu::selectedItemsProperties() const
457 {
458 if (!m_selectedItemsProperties) {
459 m_selectedItemsProperties = new KFileItemListProperties(m_selectedItems);
460 }
461 return *m_selectedItemsProperties;
462 }
463
464 KFileItem DolphinContextMenu::baseFileItem()
465 {
466 if (!m_baseFileItem) {
467 const DolphinView* view = m_mainWindow->activeViewContainer()->view();
468 KFileItem baseItem = view->rootItem();
469 if (baseItem.isNull() || baseItem.url() != m_baseUrl) {
470 m_baseFileItem = new KFileItem(m_baseUrl);
471 } else {
472 m_baseFileItem = new KFileItem(baseItem);
473 }
474 }
475 return *m_baseFileItem;
476 }
477
478 void DolphinContextMenu::addOpenWithActions()
479 {
480 // insert 'Open With...' action or sub menu
481 m_fileItemActions->addOpenWithActionsTo(this, QStringLiteral("DesktopEntryName != '%1'").arg(qApp->desktopFileName()));
482 }
483
484 void DolphinContextMenu::addCustomActions()
485 {
486 addActions(m_customActions);
487 }
488
489 void DolphinContextMenu::addAdditionalActions(const KFileItemListProperties &props)
490 {
491 addSeparator();
492
493 QList<QAction *> additionalActions;
494 if (props.isDirectory() && props.isLocal() && ContextMenuSettings::showOpenTerminal()) {
495 additionalActions << m_mainWindow->actionCollection()->action(QStringLiteral("open_terminal"));
496 }
497 m_fileItemActions->addActionsTo(this, KFileItemActions::MenuActionSource::All, additionalActions);
498
499 const DolphinView* view = m_mainWindow->activeViewContainer()->view();
500 const QList<QAction*> versionControlActions = view->versionControlActions(m_selectedItems);
501 if (!versionControlActions.isEmpty()) {
502 addActions(versionControlActions);
503 addSeparator();
504 }
505 }
506