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