]> cloud.milkyroute.net Git - dolphin.git/blob - src/panels/places/placespanel.cpp
Merge branch 'release/21.04'
[dolphin.git] / src / panels / places / placespanel.cpp
1 /*
2 * SPDX-FileCopyrightText: 2008-2012 Peter Penz <peter.penz19@gmail.com>
3 *
4 * Based on KFilePlacesView from kdelibs:
5 * SPDX-FileCopyrightText: 2007 Kevin Ottens <ervin@kde.org>
6 * SPDX-FileCopyrightText: 2007 David Faure <faure@kde.org>
7 *
8 * SPDX-License-Identifier: GPL-2.0-or-later
9 */
10
11 #include "placespanel.h"
12
13 #include "dolphin_generalsettings.h"
14 #include "global.h"
15 #include "kitemviews/kitemlistcontainer.h"
16 #include "kitemviews/kitemlistcontroller.h"
17 #include "kitemviews/kitemlistselectionmanager.h"
18 #include "kitemviews/kstandarditem.h"
19 #include "placesitem.h"
20 #include "placesitemlistgroupheader.h"
21 #include "placesitemlistwidget.h"
22 #include "placesitemmodel.h"
23 #include "placesview.h"
24 #include "trash/dolphintrash.h"
25 #include "views/draganddrophelper.h"
26 #include "settings/dolphinsettingsdialog.h"
27
28 #include <KFilePlaceEditDialog>
29 #include <KFilePlacesModel>
30 #include <KIO/DropJob>
31 #include <KIO/EmptyTrashJob>
32 #include <KIO/Job>
33 #include <KIconLoader>
34 #include <KLocalizedString>
35 #include <KMountPoint>
36 #include <KPropertiesDialog>
37
38 #include <QActionGroup>
39 #include <QGraphicsSceneDragDropEvent>
40 #include <QIcon>
41 #include <QMenu>
42 #include <QMimeData>
43 #include <QVBoxLayout>
44 #include <QToolTip>
45 #include <QTimer>
46
47 PlacesPanel::PlacesPanel(QWidget* parent) :
48 Panel(parent),
49 m_controller(nullptr),
50 m_model(nullptr),
51 m_view(nullptr),
52 m_storageSetupFailedUrl(),
53 m_triggerStorageSetupButton(),
54 m_itemDropEventIndex(-1),
55 m_itemDropEventMimeData(nullptr),
56 m_itemDropEvent(nullptr),
57 m_tooltipTimer()
58 {
59 m_tooltipTimer.setInterval(500);
60 m_tooltipTimer.setSingleShot(true);
61 connect(&m_tooltipTimer, &QTimer::timeout, this, &PlacesPanel::slotShowTooltip);
62 }
63
64 PlacesPanel::~PlacesPanel()
65 {
66 }
67
68 void PlacesPanel::proceedWithTearDown()
69 {
70 m_model->proceedWithTearDown();
71 }
72
73 bool PlacesPanel::urlChanged()
74 {
75 if (!url().isValid() || url().scheme().contains(QLatin1String("search"))) {
76 // Skip results shown by a search, as possible identical
77 // directory names are useless without parent-path information.
78 return false;
79 }
80
81 if (m_controller) {
82 selectItem();
83 }
84
85 return true;
86 }
87
88 void PlacesPanel::readSettings()
89 {
90 if (m_controller) {
91 const int delay = GeneralSettings::autoExpandFolders() ? 750 : -1;
92 m_controller->setAutoActivationDelay(delay);
93 }
94 }
95
96 void PlacesPanel::showEvent(QShowEvent* event)
97 {
98 if (event->spontaneous()) {
99 Panel::showEvent(event);
100 return;
101 }
102
103 if (!m_controller) {
104 // Postpone the creating of the controller to the first show event.
105 // This assures that no performance and memory overhead is given when the folders panel is not
106 // used at all and stays invisible.
107 m_model = new PlacesItemModel(this);
108 m_model->setGroupedSorting(true);
109 connect(m_model, &PlacesItemModel::errorMessage,
110 this, &PlacesPanel::errorMessage);
111 connect(m_model, &PlacesItemModel::storageTearDownRequested,
112 this, &PlacesPanel::storageTearDownRequested);
113 connect(m_model, &PlacesItemModel::storageTearDownExternallyRequested,
114 this, &PlacesPanel::storageTearDownExternallyRequested);
115 connect(m_model, &PlacesItemModel::storageTearDownSuccessful,
116 this, &PlacesPanel::storageTearDownSuccessful);
117
118 m_view = new PlacesView();
119 m_view->setWidgetCreator(new KItemListWidgetCreator<PlacesItemListWidget>());
120 m_view->setGroupHeaderCreator(new KItemListGroupHeaderCreator<PlacesItemListGroupHeader>());
121
122 installEventFilter(this);
123
124 m_controller = new KItemListController(m_model, m_view, this);
125 m_controller->setSelectionBehavior(KItemListController::SingleSelection);
126 m_controller->setSingleClickActivationEnforced(true);
127
128 readSettings();
129
130 connect(m_controller, &KItemListController::itemActivated, this, &PlacesPanel::slotItemActivated);
131 connect(m_controller, &KItemListController::itemMiddleClicked, this, &PlacesPanel::slotItemMiddleClicked);
132 connect(m_controller, &KItemListController::itemContextMenuRequested, this, &PlacesPanel::slotItemContextMenuRequested);
133 connect(m_controller, &KItemListController::viewContextMenuRequested, this, &PlacesPanel::slotViewContextMenuRequested);
134 connect(m_controller, &KItemListController::itemDropEvent, this, &PlacesPanel::slotItemDropEvent);
135 connect(m_controller, &KItemListController::aboveItemDropEvent, this, &PlacesPanel::slotAboveItemDropEvent);
136
137 KItemListContainer* container = new KItemListContainer(m_controller, this);
138 container->setEnabledFrame(false);
139
140 QVBoxLayout* layout = new QVBoxLayout(this);
141 layout->setContentsMargins(0, 0, 0, 0);
142 layout->addWidget(container);
143
144 selectItem();
145 }
146
147 Panel::showEvent(event);
148 }
149
150 bool PlacesPanel::eventFilter(QObject * /* obj */, QEvent *event)
151 {
152 if (event->type() == QEvent::ToolTip) {
153
154 QHelpEvent *hoverEvent = reinterpret_cast<QHelpEvent *>(event);
155
156 m_hoveredIndex = m_view->itemAt(hoverEvent->pos());
157 m_hoverPos = mapToGlobal(hoverEvent->pos());
158
159 m_tooltipTimer.start();
160 return true;
161 }
162 return false;
163 }
164
165 void PlacesPanel::slotItemActivated(int index)
166 {
167 triggerItem(index, Qt::LeftButton);
168 }
169
170 void PlacesPanel::slotItemMiddleClicked(int index)
171 {
172 triggerItem(index, Qt::MiddleButton);
173 }
174
175 void PlacesPanel::slotItemContextMenuRequested(int index, const QPointF& pos)
176 {
177 PlacesItem* item = m_model->placesItem(index);
178 if (!item) {
179 return;
180 }
181
182 QMenu menu(this);
183
184 QAction* emptyTrashAction = nullptr;
185 QAction* configureTrashAction = nullptr;
186 QAction* editAction = nullptr;
187 QAction* teardownAction = nullptr;
188 QAction* ejectAction = nullptr;
189 QAction* mountAction = nullptr;
190
191 const bool isDevice = !item->udi().isEmpty();
192 const bool isTrash = (item->url().scheme() == QLatin1String("trash"));
193 if (isTrash) {
194 emptyTrashAction = menu.addAction(QIcon::fromTheme(QStringLiteral("trash-empty")), i18nc("@action:inmenu", "Empty Trash"));
195 emptyTrashAction->setEnabled(item->icon() == QLatin1String("user-trash-full"));
196 menu.addSeparator();
197 }
198
199 QAction* openInNewTabAction = menu.addAction(QIcon::fromTheme(QStringLiteral("tab-new")), i18nc("@item:inmenu", "Open in New Tab"));
200 QAction* openInNewWindowAction = menu.addAction(QIcon::fromTheme(QStringLiteral("window-new")), i18nc("@item:inmenu", "Open in New Window"));
201 QAction* propertiesAction = nullptr;
202 if (item->url().isLocalFile()) {
203 propertiesAction = menu.addAction(QIcon::fromTheme(QStringLiteral("document-properties")), i18nc("@action:inmenu", "Properties"));
204 }
205 if (!isDevice) {
206 menu.addSeparator();
207 }
208
209 if (isDevice) {
210 ejectAction = m_model->ejectAction(index);
211 if (ejectAction) {
212 ejectAction->setParent(&menu);
213 menu.addAction(ejectAction);
214 }
215
216 teardownAction = m_model->teardownAction(index);
217 if (teardownAction) {
218 // Disable teardown option for root and home partitions
219 bool teardownEnabled = item->url() != QUrl::fromLocalFile(QDir::rootPath());
220 if (teardownEnabled) {
221 KMountPoint::Ptr mountPoint = KMountPoint::currentMountPoints().findByPath(QDir::homePath());
222 if (mountPoint && item->url() == QUrl::fromLocalFile(mountPoint->mountPoint())) {
223 teardownEnabled = false;
224 }
225 }
226 teardownAction->setEnabled(teardownEnabled);
227
228 teardownAction->setParent(&menu);
229 menu.addAction(teardownAction);
230 }
231
232 if (item->storageSetupNeeded()) {
233 mountAction = menu.addAction(QIcon::fromTheme(QStringLiteral("media-mount")), i18nc("@action:inmenu", "Mount"));
234 }
235
236 if (teardownAction || ejectAction || mountAction) {
237 menu.addSeparator();
238 }
239 }
240
241 if (isTrash) {
242 configureTrashAction = menu.addAction(QIcon::fromTheme(QStringLiteral("configure")), i18nc("@action:inmenu", "Configure Trash..."));
243 }
244
245 if (!isDevice) {
246 editAction = menu.addAction(QIcon::fromTheme(QStringLiteral("edit-entry")), i18nc("@item:inmenu", "Edit..."));
247 }
248
249 QAction* removeAction = nullptr;
250 if (!isDevice && !item->isSystemItem()) {
251 removeAction = menu.addAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18nc("@item:inmenu", "Remove"));
252 }
253
254 QAction* hideAction = menu.addAction(QIcon::fromTheme(QStringLiteral("view-hidden")), i18nc("@item:inmenu", "Hide"));
255 hideAction->setCheckable(true);
256 hideAction->setChecked(item->isHidden());
257
258 buildGroupContextMenu(&menu, index);
259
260 QAction* action = menu.exec(pos.toPoint());
261 if (action) {
262 if (action == emptyTrashAction) {
263 Trash::empty(this);
264 } else if (action == configureTrashAction) {
265 DolphinSettingsDialog* settingsDialog = new DolphinSettingsDialog(item->url(), this);
266 settingsDialog->setCurrentPage(settingsDialog->trashSettings);
267 settingsDialog->setAttribute(Qt::WA_DeleteOnClose);
268 settingsDialog->show();
269 } else {
270 // The index might have changed if devices were added/removed while
271 // the context menu was open.
272 index = m_model->index(item);
273 if (index < 0) {
274 // The item is not in the model any more, probably because it was an
275 // external device that has been removed while the context menu was open.
276 return;
277 }
278
279 if (action == editAction) {
280 editEntry(index);
281 } else if (action == removeAction) {
282 m_model->deleteItem(index);
283 } else if (action == hideAction) {
284 item->setHidden(hideAction->isChecked());
285 if (!m_model->hiddenCount()) {
286 showHiddenEntries(false);
287 }
288 } else if (action == openInNewWindowAction) {
289 Dolphin::openNewWindow({KFilePlacesModel::convertedUrl(m_model->data(index).value("url").toUrl())}, this);
290 } else if (action == openInNewTabAction) {
291 // TriggerItem does set up the storage first and then it will
292 // emit the slotItemMiddleClicked signal, because of Qt::MiddleButton.
293 triggerItem(index, Qt::MiddleButton);
294 } else if (action == mountAction) {
295 m_model->requestStorageSetup(index);
296 } else if (action == teardownAction) {
297 m_model->requestTearDown(index);
298 } else if (action == ejectAction) {
299 m_model->requestEject(index);
300 } else if (action == propertiesAction) {
301 KPropertiesDialog* dialog = new KPropertiesDialog(item->url(), this);
302 dialog->setAttribute(Qt::WA_DeleteOnClose);
303 dialog->show();
304 }
305 }
306 }
307
308 selectItem();
309 }
310
311 void PlacesPanel::slotViewContextMenuRequested(const QPointF& pos)
312 {
313 QMenu menu(this);
314
315 QAction* addAction = menu.addAction(QIcon::fromTheme(QStringLiteral("document-new")), i18nc("@item:inmenu", "Add Entry..."));
316
317 QAction* showAllAction = menu.addAction(i18nc("@item:inmenu", "Show Hidden Places"));
318 showAllAction->setCheckable(true);
319 showAllAction->setChecked(m_model->hiddenItemsShown());
320 showAllAction->setIcon(QIcon::fromTheme(m_model->hiddenItemsShown() ? QStringLiteral("view-visible") : QStringLiteral("view-hidden")));
321 showAllAction->setEnabled(m_model->hiddenCount());
322
323 buildGroupContextMenu(&menu, m_controller->indexCloseToMousePressedPosition());
324
325 QMenu* iconSizeSubMenu = new QMenu(i18nc("@item:inmenu", "Icon Size"), &menu);
326
327 struct IconSizeInfo
328 {
329 int size;
330 const char* context;
331 const char* text;
332 };
333
334 const int iconSizeCount = 4;
335 static const IconSizeInfo iconSizes[iconSizeCount] = {
336 {KIconLoader::SizeSmall, I18NC_NOOP("Small icon size", "Small (%1x%2)")},
337 {KIconLoader::SizeSmallMedium, I18NC_NOOP("Medium icon size", "Medium (%1x%2)")},
338 {KIconLoader::SizeMedium, I18NC_NOOP("Large icon size", "Large (%1x%2)")},
339 {KIconLoader::SizeLarge, I18NC_NOOP("Huge icon size", "Huge (%1x%2)")}
340 };
341
342 QHash<QAction*, int> iconSizeActionMap;
343 QActionGroup* iconSizeGroup = new QActionGroup(iconSizeSubMenu);
344
345 for (int i = 0; i < iconSizeCount; ++i) {
346 const int size = iconSizes[i].size;
347 const QString text = i18nc(iconSizes[i].context, iconSizes[i].text,
348 size, size);
349
350 QAction* action = iconSizeSubMenu->addAction(text);
351 iconSizeActionMap.insert(action, size);
352 action->setActionGroup(iconSizeGroup);
353 action->setCheckable(true);
354 action->setChecked(m_view->iconSize() == size);
355 }
356
357 menu.addMenu(iconSizeSubMenu);
358
359 menu.addSeparator();
360 const auto actions = customContextMenuActions();
361 for (QAction* action : actions) {
362 menu.addAction(action);
363 }
364
365 QAction* action = menu.exec(pos.toPoint());
366 if (action) {
367 if (action == addAction) {
368 addEntry();
369 } else if (action == showAllAction) {
370 showHiddenEntries(showAllAction->isChecked());
371 } else if (iconSizeActionMap.contains(action)) {
372 m_view->setIconSize(iconSizeActionMap.value(action));
373 }
374 }
375
376 selectItem();
377 }
378
379 QAction *PlacesPanel::buildGroupContextMenu(QMenu *menu, int index)
380 {
381 if (index == -1) {
382 return nullptr;
383 }
384
385 KFilePlacesModel::GroupType groupType = m_model->groupType(index);
386 QAction *hideGroupAction = menu->addAction(QIcon::fromTheme(QStringLiteral("view-hidden")), i18nc("@item:inmenu", "Hide Section '%1'", m_model->item(index)->group()));
387 hideGroupAction->setCheckable(true);
388 hideGroupAction->setChecked(m_model->isGroupHidden(groupType));
389
390 connect(hideGroupAction, &QAction::triggered, this, [this, groupType, hideGroupAction]{
391 m_model->setGroupHidden(groupType, hideGroupAction->isChecked());
392 if (!m_model->hiddenCount()) {
393 showHiddenEntries(false);
394 }
395 });
396
397 return hideGroupAction;
398 }
399
400 void PlacesPanel::slotItemDropEvent(int index, QGraphicsSceneDragDropEvent* event)
401 {
402 if (index < 0) {
403 return;
404 }
405
406 const PlacesItem* destItem = m_model->placesItem(index);
407
408 if (destItem->isSearchOrTimelineUrl()) {
409 return;
410 }
411
412 if (m_model->storageSetupNeeded(index)) {
413 connect(m_model, &PlacesItemModel::storageSetupDone,
414 this, &PlacesPanel::slotItemDropEventStorageSetupDone);
415
416 m_itemDropEventIndex = index;
417
418 // Make a full copy of the Mime-Data
419 m_itemDropEventMimeData = new QMimeData;
420 m_itemDropEventMimeData->setText(event->mimeData()->text());
421 m_itemDropEventMimeData->setHtml(event->mimeData()->html());
422 m_itemDropEventMimeData->setUrls(event->mimeData()->urls());
423 m_itemDropEventMimeData->setImageData(event->mimeData()->imageData());
424 m_itemDropEventMimeData->setColorData(event->mimeData()->colorData());
425
426 m_itemDropEvent = new QDropEvent(event->pos().toPoint(),
427 event->possibleActions(),
428 m_itemDropEventMimeData,
429 event->buttons(),
430 event->modifiers());
431
432 m_model->requestStorageSetup(index);
433 return;
434 }
435
436 QUrl destUrl = destItem->url();
437 QDropEvent dropEvent(event->pos().toPoint(),
438 event->possibleActions(),
439 event->mimeData(),
440 event->buttons(),
441 event->modifiers());
442
443 slotUrlsDropped(destUrl, &dropEvent, this);
444 }
445
446 void PlacesPanel::slotItemDropEventStorageSetupDone(int index, bool success)
447 {
448 disconnect(m_model, &PlacesItemModel::storageSetupDone,
449 this, &PlacesPanel::slotItemDropEventStorageSetupDone);
450
451 if ((index == m_itemDropEventIndex) && m_itemDropEvent && m_itemDropEventMimeData) {
452 if (success) {
453 QUrl destUrl = m_model->placesItem(index)->url();
454 slotUrlsDropped(destUrl, m_itemDropEvent, this);
455 }
456
457 delete m_itemDropEventMimeData;
458 delete m_itemDropEvent;
459
460 m_itemDropEventIndex = -1;
461 m_itemDropEventMimeData = nullptr;
462 m_itemDropEvent = nullptr;
463 }
464 }
465
466 void PlacesPanel::slotAboveItemDropEvent(int index, QGraphicsSceneDragDropEvent* event)
467 {
468 m_model->dropMimeDataBefore(index, event->mimeData());
469 }
470
471 void PlacesPanel::slotUrlsDropped(const QUrl& dest, QDropEvent* event, QWidget* parent)
472 {
473 KIO::DropJob *job = DragAndDropHelper::dropUrls(dest, event, parent);
474 if (job) {
475 connect(job, &KIO::DropJob::result, this, [this](KJob *job) { if (job->error()) Q_EMIT errorMessage(job->errorString()); });
476 }
477 }
478
479 void PlacesPanel::slotStorageSetupDone(int index, bool success)
480 {
481 disconnect(m_model, &PlacesItemModel::storageSetupDone,
482 this, &PlacesPanel::slotStorageSetupDone);
483
484 if (m_triggerStorageSetupButton == Qt::NoButton) {
485 return;
486 }
487
488 if (success) {
489 Q_ASSERT(!m_model->storageSetupNeeded(index));
490 triggerItem(index, m_triggerStorageSetupButton);
491 m_triggerStorageSetupButton = Qt::NoButton;
492 } else {
493 setUrl(m_storageSetupFailedUrl);
494 m_storageSetupFailedUrl = QUrl();
495 }
496 }
497
498 void PlacesPanel::slotShowTooltip()
499 {
500 const QUrl url = m_model->data(m_hoveredIndex).value("url").value<QUrl>();
501 const QString text = url.toDisplayString(QUrl::PreferLocalFile);
502 QToolTip::showText(m_hoverPos, text);
503 }
504
505 void PlacesPanel::addEntry()
506 {
507 const int index = m_controller->selectionManager()->currentItem();
508 const QUrl url = m_model->data(index).value("url").toUrl();
509 const QString text = url.fileName().isEmpty() ? url.toDisplayString(QUrl::PreferLocalFile) : url.fileName();
510
511 QPointer<KFilePlaceEditDialog> dialog = new KFilePlaceEditDialog(true, url, text, QString(), true, false, KIconLoader::SizeMedium, this);
512 if (dialog->exec() == QDialog::Accepted) {
513 const QString appName = dialog->applicationLocal() ? QCoreApplication::applicationName() : QString();
514 m_model->createPlacesItem(dialog->label(), dialog->url(), dialog->icon(), appName);
515 }
516
517 delete dialog;
518 }
519
520 void PlacesPanel::editEntry(int index)
521 {
522 QHash<QByteArray, QVariant> data = m_model->data(index);
523 const QUrl url = data.value("url").toUrl();
524 const QString text = data.value("text").toString();
525 const QString iconName = data.value("iconName").toString();
526 const bool applicationLocal = !data.value("applicationName").toString().isEmpty();
527
528 QPointer<KFilePlaceEditDialog> dialog = new KFilePlaceEditDialog(true, url, text, iconName, true, applicationLocal, KIconLoader::SizeMedium, this);
529 if (dialog->exec() == QDialog::Accepted) {
530 PlacesItem* oldItem = m_model->placesItem(index);
531 if (oldItem) {
532 const QString appName = dialog->applicationLocal() ? QCoreApplication::applicationName() : QString();
533 oldItem->setApplicationName(appName);
534 oldItem->setText(dialog->label());
535 oldItem->setUrl(dialog->url());
536 oldItem->setIcon(dialog->icon());
537 m_model->refresh();
538 }
539 }
540
541 delete dialog;
542 }
543
544 void PlacesPanel::selectItem()
545 {
546 const int index = m_model->closestItem(url());
547 KItemListSelectionManager* selectionManager = m_controller->selectionManager();
548 selectionManager->setCurrentItem(index);
549 selectionManager->clearSelection();
550
551 const QUrl closestUrl = m_model->url(index);
552 if (!closestUrl.path().isEmpty() && url() == closestUrl) {
553 selectionManager->setSelected(index);
554 }
555 }
556
557 void PlacesPanel::triggerItem(int index, Qt::MouseButton button)
558 {
559 const PlacesItem* item = m_model->placesItem(index);
560 if (!item) {
561 return;
562 }
563
564 if (m_model->storageSetupNeeded(index)) {
565 m_triggerStorageSetupButton = button;
566 m_storageSetupFailedUrl = url();
567
568 connect(m_model, &PlacesItemModel::storageSetupDone,
569 this, &PlacesPanel::slotStorageSetupDone);
570
571 m_model->requestStorageSetup(index);
572 } else {
573 m_triggerStorageSetupButton = Qt::NoButton;
574
575 const QUrl url = m_model->data(index).value("url").toUrl();
576 if (!url.isEmpty()) {
577 if (button == Qt::MiddleButton) {
578 Q_EMIT placeMiddleClicked(KFilePlacesModel::convertedUrl(url));
579 } else {
580 Q_EMIT placeActivated(KFilePlacesModel::convertedUrl(url));
581 }
582 }
583 }
584 }
585
586 void PlacesPanel::showHiddenEntries(bool shown)
587 {
588 m_model->setHiddenItemsShown(shown);
589 Q_EMIT showHiddenEntriesChanged(shown);
590 }
591
592 int PlacesPanel::hiddenListCount()
593 {
594 if(!m_model) {
595 return 0;
596 }
597 return m_model->hiddenCount();
598 }