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