]> cloud.milkyroute.net Git - dolphin.git/blob - src/panels/places/placesitemmodel.cpp
680d513b5c166dcd214984eaa3d9b5ac44da58d8
[dolphin.git] / src / panels / places / placesitemmodel.cpp
1 /***************************************************************************
2 * Copyright (C) 2012 by Peter Penz <peter.penz19@gmail.com> *
3 * *
4 * Based on KFilePlacesModel from kdelibs: *
5 * Copyright (C) 2007 Kevin Ottens <ervin@kde.org> *
6 * Copyright (C) 2007 David Faure <faure@kde.org> *
7 * *
8 * This program is free software; you can redistribute it and/or modify *
9 * it under the terms of the GNU General Public License as published by *
10 * the Free Software Foundation; either version 2 of the License, or *
11 * (at your option) any later version. *
12 * *
13 * This program is distributed in the hope that it will be useful, *
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
16 * GNU General Public License for more details. *
17 * *
18 * You should have received a copy of the GNU General Public License *
19 * along with this program; if not, write to the *
20 * Free Software Foundation, Inc., *
21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *
22 ***************************************************************************/
23
24 #include "placesitemmodel.h"
25 #include "placesitemsignalhandler.h"
26
27 #include "dolphin_generalsettings.h"
28
29 #include <KBookmark>
30 #include <KBookmarkManager>
31 #include "dolphindebug.h"
32 #include <QIcon>
33 #include <KProtocolInfo>
34 #include <KLocalizedString>
35 #include <QStandardPaths>
36 #include <KAboutData>
37 #include "placesitem.h"
38 #include <QAction>
39 #include <QDate>
40 #include <QMimeData>
41 #include <QTimer>
42 #include <KUrlMimeData>
43
44 #include <Solid/Device>
45 #include <Solid/DeviceNotifier>
46 #include <Solid/OpticalDisc>
47 #include <Solid/OpticalDrive>
48 #include <Solid/StorageAccess>
49 #include <Solid/StorageDrive>
50
51 #include <views/dolphinview.h>
52 #include <views/viewproperties.h>
53
54 #ifdef HAVE_BALOO
55 #include <Baloo/Query>
56 #include <Baloo/IndexerConfig>
57 #endif
58
59 namespace {
60 // As long as KFilePlacesView from kdelibs is available in parallel, the
61 // system-bookmarks for "Recently Saved" and "Search For" should be
62 // shown only inside the Places Panel. This is necessary as the stored
63 // URLs needs to get translated to a Baloo-search-URL on-the-fly to
64 // be independent from changes in the Baloo-search-URL-syntax.
65 // Hence a prefix to the application-name of the stored bookmarks is
66 // added, which is only read by PlacesItemModel.
67 const char AppNamePrefix[] = "-places-panel";
68 }
69
70 PlacesItemModel::PlacesItemModel(QObject* parent) :
71 KStandardItemModel(parent),
72 m_fileIndexingEnabled(false),
73 m_hiddenItemsShown(false),
74 m_availableDevices(),
75 m_predicate(),
76 m_bookmarkManager(0),
77 m_systemBookmarks(),
78 m_systemBookmarksIndexes(),
79 m_bookmarkedItems(),
80 m_hiddenItemToRemove(-1),
81 m_deviceToTearDown(0),
82 m_updateBookmarksTimer(0),
83 m_storageSetupInProgress()
84 {
85 #ifdef HAVE_BALOO
86 Baloo::IndexerConfig config;
87 m_fileIndexingEnabled = config.fileIndexingEnabled();
88 #endif
89 const QString file = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + "/user-places.xbel";
90 m_bookmarkManager = KBookmarkManager::managerForExternalFile(file);
91
92 createSystemBookmarks();
93 initializeAvailableDevices();
94 loadBookmarks();
95
96 const int syncBookmarksTimeout = 100;
97
98 m_updateBookmarksTimer = new QTimer(this);
99 m_updateBookmarksTimer->setInterval(syncBookmarksTimeout);
100 m_updateBookmarksTimer->setSingleShot(true);
101 connect(m_updateBookmarksTimer, &QTimer::timeout, this, &PlacesItemModel::updateBookmarks);
102
103 connect(m_bookmarkManager, &KBookmarkManager::changed,
104 m_updateBookmarksTimer, static_cast<void(QTimer::*)()>(&QTimer::start));
105 }
106
107 PlacesItemModel::~PlacesItemModel()
108 {
109 qDeleteAll(m_bookmarkedItems);
110 m_bookmarkedItems.clear();
111 }
112
113 PlacesItem* PlacesItemModel::createPlacesItem(const QString& text,
114 const QUrl& url,
115 const QString& iconName)
116 {
117 const KBookmark bookmark = PlacesItem::createBookmark(m_bookmarkManager, text, url, iconName);
118 return new PlacesItem(bookmark);
119 }
120
121 PlacesItem* PlacesItemModel::placesItem(int index) const
122 {
123 return dynamic_cast<PlacesItem*>(item(index));
124 }
125
126 int PlacesItemModel::hiddenCount() const
127 {
128 int modelIndex = 0;
129 int hiddenItemCount = 0;
130 foreach (const PlacesItem* item, m_bookmarkedItems) {
131 if (item) {
132 ++hiddenItemCount;
133 } else {
134 if (placesItem(modelIndex)->isHidden()) {
135 ++hiddenItemCount;
136 }
137 ++modelIndex;
138 }
139 }
140
141 return hiddenItemCount;
142 }
143
144 void PlacesItemModel::setHiddenItemsShown(bool show)
145 {
146 if (m_hiddenItemsShown == show) {
147 return;
148 }
149
150 m_hiddenItemsShown = show;
151
152 if (show) {
153 // Move all items that are part of m_bookmarkedItems to the model.
154 QList<PlacesItem*> itemsToInsert;
155 QList<int> insertPos;
156 int modelIndex = 0;
157 for (int i = 0; i < m_bookmarkedItems.count(); ++i) {
158 if (m_bookmarkedItems[i]) {
159 itemsToInsert.append(m_bookmarkedItems[i]);
160 m_bookmarkedItems[i] = 0;
161 insertPos.append(modelIndex);
162 }
163 ++modelIndex;
164 }
165
166 // Inserting the items will automatically insert an item
167 // to m_bookmarkedItems in PlacesItemModel::onItemsInserted().
168 // The items are temporary saved in itemsToInsert, so
169 // m_bookmarkedItems can be shrinked now.
170 m_bookmarkedItems.erase(m_bookmarkedItems.begin(),
171 m_bookmarkedItems.begin() + itemsToInsert.count());
172
173 for (int i = 0; i < itemsToInsert.count(); ++i) {
174 insertItem(insertPos[i], itemsToInsert[i]);
175 }
176
177 Q_ASSERT(m_bookmarkedItems.count() == count());
178 } else {
179 // Move all items of the model, where the "isHidden" property is true, to
180 // m_bookmarkedItems.
181 Q_ASSERT(m_bookmarkedItems.count() == count());
182 for (int i = count() - 1; i >= 0; --i) {
183 if (placesItem(i)->isHidden()) {
184 hideItem(i);
185 }
186 }
187 }
188
189 #ifdef PLACESITEMMODEL_DEBUG
190 qCDebug(DolphinDebug) << "Changed visibility of hidden items";
191 showModelState();
192 #endif
193 }
194
195 bool PlacesItemModel::hiddenItemsShown() const
196 {
197 return m_hiddenItemsShown;
198 }
199
200 int PlacesItemModel::closestItem(const QUrl& url) const
201 {
202 int foundIndex = -1;
203 int maxLength = 0;
204
205 for (int i = 0; i < count(); ++i) {
206 const QUrl itemUrl = placesItem(i)->url();
207 if (url == itemUrl) {
208 // We can't find a closer one, so stop here.
209 foundIndex = i;
210 break;
211 } else if (itemUrl.isParentOf(url)) {
212 const int length = itemUrl.path().length();
213 if (length > maxLength) {
214 foundIndex = i;
215 maxLength = length;
216 }
217 }
218 }
219
220 return foundIndex;
221 }
222
223 void PlacesItemModel::appendItemToGroup(PlacesItem* item)
224 {
225 if (!item) {
226 return;
227 }
228
229 int i = 0;
230 while (i < count() && placesItem(i)->group() != item->group()) {
231 ++i;
232 }
233
234 bool inserted = false;
235 while (!inserted && i < count()) {
236 if (placesItem(i)->group() != item->group()) {
237 insertItem(i, item);
238 inserted = true;
239 }
240 ++i;
241 }
242
243 if (!inserted) {
244 appendItem(item);
245 }
246 }
247
248
249 QAction* PlacesItemModel::ejectAction(int index) const
250 {
251 const PlacesItem* item = placesItem(index);
252 if (item && item->device().is<Solid::OpticalDisc>()) {
253 return new QAction(QIcon::fromTheme(QStringLiteral("media-eject")), i18nc("@item", "Eject"), 0);
254 }
255
256 return 0;
257 }
258
259 QAction* PlacesItemModel::teardownAction(int index) const
260 {
261 const PlacesItem* item = placesItem(index);
262 if (!item) {
263 return 0;
264 }
265
266 Solid::Device device = item->device();
267 const bool providesTearDown = device.is<Solid::StorageAccess>() &&
268 device.as<Solid::StorageAccess>()->isAccessible();
269 if (!providesTearDown) {
270 return 0;
271 }
272
273 Solid::StorageDrive* drive = device.as<Solid::StorageDrive>();
274 if (!drive) {
275 drive = device.parent().as<Solid::StorageDrive>();
276 }
277
278 bool hotPluggable = false;
279 bool removable = false;
280 if (drive) {
281 hotPluggable = drive->isHotpluggable();
282 removable = drive->isRemovable();
283 }
284
285 QString iconName;
286 QString text;
287 if (device.is<Solid::OpticalDisc>()) {
288 text = i18nc("@item", "Release");
289 } else if (removable || hotPluggable) {
290 text = i18nc("@item", "Safely Remove");
291 iconName = QStringLiteral("media-eject");
292 } else {
293 text = i18nc("@item", "Unmount");
294 iconName = QStringLiteral("media-eject");
295 }
296
297 if (iconName.isEmpty()) {
298 return new QAction(text, 0);
299 }
300
301 return new QAction(QIcon::fromTheme(iconName), text, 0);
302 }
303
304 void PlacesItemModel::requestEject(int index)
305 {
306 const PlacesItem* item = placesItem(index);
307 if (item) {
308 Solid::OpticalDrive* drive = item->device().parent().as<Solid::OpticalDrive>();
309 if (drive) {
310 connect(drive, &Solid::OpticalDrive::ejectDone,
311 this, &PlacesItemModel::slotStorageTearDownDone);
312 drive->eject();
313 } else {
314 const QString label = item->text();
315 const QString message = i18nc("@info", "The device '%1' is not a disk and cannot be ejected.", label);
316 emit errorMessage(message);
317 }
318 }
319 }
320
321 void PlacesItemModel::requestTearDown(int index)
322 {
323 const PlacesItem* item = placesItem(index);
324 if (item) {
325 Solid::StorageAccess *tmp = item->device().as<Solid::StorageAccess>();
326 if (tmp) {
327 m_deviceToTearDown = tmp;
328 // disconnect the Solid::StorageAccess::teardownRequested
329 // to prevent emitting PlacesItemModel::storageTearDownExternallyRequested
330 // after we have emitted PlacesItemModel::storageTearDownRequested
331 disconnect(tmp, &Solid::StorageAccess::teardownRequested,
332 item->signalHandler(), &PlacesItemSignalHandler::onTearDownRequested);
333 emit storageTearDownRequested(tmp->filePath());
334 }
335 }
336 }
337
338 bool PlacesItemModel::storageSetupNeeded(int index) const
339 {
340 const PlacesItem* item = placesItem(index);
341 return item ? item->storageSetupNeeded() : false;
342 }
343
344 void PlacesItemModel::requestStorageSetup(int index)
345 {
346 const PlacesItem* item = placesItem(index);
347 if (!item) {
348 return;
349 }
350
351 Solid::Device device = item->device();
352 const bool setup = device.is<Solid::StorageAccess>()
353 && !m_storageSetupInProgress.contains(device.as<Solid::StorageAccess>())
354 && !device.as<Solid::StorageAccess>()->isAccessible();
355 if (setup) {
356 Solid::StorageAccess* access = device.as<Solid::StorageAccess>();
357
358 m_storageSetupInProgress[access] = index;
359
360 connect(access, &Solid::StorageAccess::setupDone,
361 this, &PlacesItemModel::slotStorageSetupDone);
362
363 access->setup();
364 }
365 }
366
367 QMimeData* PlacesItemModel::createMimeData(const KItemSet& indexes) const
368 {
369 QList<QUrl> urls;
370 QByteArray itemData;
371
372 QDataStream stream(&itemData, QIODevice::WriteOnly);
373
374 for (int index : indexes) {
375 const QUrl itemUrl = placesItem(index)->url();
376 if (itemUrl.isValid()) {
377 urls << itemUrl;
378 }
379 stream << index;
380 }
381
382 QMimeData* mimeData = new QMimeData();
383 if (!urls.isEmpty()) {
384 mimeData->setUrls(urls);
385 } else {
386 // #378954: prevent itemDropEvent() drops if there isn't a source url.
387 mimeData->setData(blacklistItemDropEventMimeType(), QByteArrayLiteral("true"));
388 }
389 mimeData->setData(internalMimeType(), itemData);
390
391 return mimeData;
392 }
393
394 bool PlacesItemModel::supportsDropping(int index) const
395 {
396 return index >= 0 && index < count();
397 }
398
399 void PlacesItemModel::dropMimeDataBefore(int index, const QMimeData* mimeData)
400 {
401 if (mimeData->hasFormat(internalMimeType())) {
402 // The item has been moved inside the view
403 QByteArray itemData = mimeData->data(internalMimeType());
404 QDataStream stream(&itemData, QIODevice::ReadOnly);
405 int oldIndex;
406 stream >> oldIndex;
407 if (oldIndex == index || oldIndex == index - 1) {
408 // No moving has been done
409 return;
410 }
411
412 PlacesItem* oldItem = placesItem(oldIndex);
413 if (!oldItem) {
414 return;
415 }
416
417 PlacesItem* newItem = new PlacesItem(oldItem->bookmark());
418 removeItem(oldIndex);
419
420 if (oldIndex < index) {
421 --index;
422 }
423
424 const int dropIndex = groupedDropIndex(index, newItem);
425 insertItem(dropIndex, newItem);
426 } else if (mimeData->hasFormat(QStringLiteral("text/uri-list"))) {
427 // One or more items must be added to the model
428 const QList<QUrl> urls = KUrlMimeData::urlsFromMimeData(mimeData);
429 for (int i = urls.count() - 1; i >= 0; --i) {
430 const QUrl& url = urls[i];
431
432 QString text = url.fileName();
433 if (text.isEmpty()) {
434 text = url.host();
435 }
436
437 if ((url.isLocalFile() && !QFileInfo(url.toLocalFile()).isDir())
438 || url.scheme() == QLatin1String("trash")) {
439 // Only directories outside the trash are allowed
440 continue;
441 }
442
443 PlacesItem* newItem = createPlacesItem(text, url);
444 const int dropIndex = groupedDropIndex(index, newItem);
445 insertItem(dropIndex, newItem);
446 }
447 }
448 }
449
450 QUrl PlacesItemModel::convertedUrl(const QUrl& url)
451 {
452 QUrl newUrl = url;
453 if (url.scheme() == QLatin1String("timeline")) {
454 newUrl = createTimelineUrl(url);
455 } else if (url.scheme() == QLatin1String("search")) {
456 newUrl = createSearchUrl(url);
457 }
458
459 return newUrl;
460 }
461
462 void PlacesItemModel::onItemInserted(int index)
463 {
464 const PlacesItem* insertedItem = placesItem(index);
465 if (insertedItem) {
466 // Take care to apply the PlacesItemModel-order of the inserted item
467 // also to the bookmark-manager.
468 const KBookmark insertedBookmark = insertedItem->bookmark();
469
470 const PlacesItem* previousItem = placesItem(index - 1);
471 KBookmark previousBookmark;
472 if (previousItem) {
473 previousBookmark = previousItem->bookmark();
474 }
475
476 m_bookmarkManager->root().moveBookmark(insertedBookmark, previousBookmark);
477 }
478
479 if (index == count() - 1) {
480 // The item has been appended as last item to the list. In this
481 // case assure that it is also appended after the hidden items and
482 // not before (like done otherwise).
483 m_bookmarkedItems.append(0);
484 } else {
485
486 int modelIndex = -1;
487 int bookmarkIndex = 0;
488 while (bookmarkIndex < m_bookmarkedItems.count()) {
489 if (!m_bookmarkedItems[bookmarkIndex]) {
490 ++modelIndex;
491 if (modelIndex + 1 == index) {
492 break;
493 }
494 }
495 ++bookmarkIndex;
496 }
497 m_bookmarkedItems.insert(bookmarkIndex, 0);
498 }
499
500 #ifdef PLACESITEMMODEL_DEBUG
501 qCDebug(DolphinDebug) << "Inserted item" << index;
502 showModelState();
503 #endif
504 }
505
506 void PlacesItemModel::onItemRemoved(int index, KStandardItem* removedItem)
507 {
508 PlacesItem* placesItem = dynamic_cast<PlacesItem*>(removedItem);
509 if (placesItem) {
510 const KBookmark bookmark = placesItem->bookmark();
511 m_bookmarkManager->root().deleteBookmark(bookmark);
512 }
513
514 const int boomarkIndex = bookmarkIndex(index);
515 Q_ASSERT(!m_bookmarkedItems[boomarkIndex]);
516 m_bookmarkedItems.removeAt(boomarkIndex);
517
518 #ifdef PLACESITEMMODEL_DEBUG
519 qCDebug(DolphinDebug) << "Removed item" << index;
520 showModelState();
521 #endif
522 }
523
524 void PlacesItemModel::onItemChanged(int index, const QSet<QByteArray>& changedRoles)
525 {
526 const PlacesItem* changedItem = placesItem(index);
527 if (changedItem) {
528 // Take care to apply the PlacesItemModel-order of the changed item
529 // also to the bookmark-manager.
530 const KBookmark insertedBookmark = changedItem->bookmark();
531
532 const PlacesItem* previousItem = placesItem(index - 1);
533 KBookmark previousBookmark;
534 if (previousItem) {
535 previousBookmark = previousItem->bookmark();
536 }
537
538 m_bookmarkManager->root().moveBookmark(insertedBookmark, previousBookmark);
539 }
540
541 if (changedRoles.contains("isHidden")) {
542 if (!m_hiddenItemsShown && changedItem->isHidden()) {
543 m_hiddenItemToRemove = index;
544 QTimer::singleShot(0, this, static_cast<void (PlacesItemModel::*)()>(&PlacesItemModel::hideItem));
545 }
546 }
547 }
548
549 void PlacesItemModel::slotDeviceAdded(const QString& udi)
550 {
551 const Solid::Device device(udi);
552
553 if (!m_predicate.matches(device)) {
554 return;
555 }
556
557 m_availableDevices << udi;
558 const KBookmark bookmark = PlacesItem::createDeviceBookmark(m_bookmarkManager, udi);
559
560 PlacesItem *item = new PlacesItem(bookmark);
561 appendItem(item);
562 connect(item->signalHandler(), &PlacesItemSignalHandler::tearDownExternallyRequested,
563 this, &PlacesItemModel::storageTearDownExternallyRequested);
564 }
565
566 void PlacesItemModel::slotDeviceRemoved(const QString& udi)
567 {
568 if (!m_availableDevices.contains(udi)) {
569 return;
570 }
571
572 for (int i = 0; i < m_bookmarkedItems.count(); ++i) {
573 PlacesItem* item = m_bookmarkedItems[i];
574 if (item && item->udi() == udi) {
575 m_bookmarkedItems.removeAt(i);
576 delete item;
577 return;
578 }
579 }
580
581 for (int i = 0; i < count(); ++i) {
582 if (placesItem(i)->udi() == udi) {
583 removeItem(i);
584 return;
585 }
586 }
587 }
588
589 void PlacesItemModel::slotStorageTearDownDone(Solid::ErrorType error, const QVariant& errorData)
590 {
591 if (error && errorData.isValid()) {
592 emit errorMessage(errorData.toString());
593 }
594 m_deviceToTearDown->disconnect();
595 m_deviceToTearDown = nullptr;
596 }
597
598 void PlacesItemModel::slotStorageSetupDone(Solid::ErrorType error,
599 const QVariant& errorData,
600 const QString& udi)
601 {
602 Q_UNUSED(udi);
603
604 const int index = m_storageSetupInProgress.take(sender());
605 const PlacesItem* item = placesItem(index);
606 if (!item) {
607 return;
608 }
609
610 if (error != Solid::NoError) {
611 if (errorData.isValid()) {
612 emit errorMessage(i18nc("@info", "An error occurred while accessing '%1', the system responded: %2",
613 item->text(),
614 errorData.toString()));
615 } else {
616 emit errorMessage(i18nc("@info", "An error occurred while accessing '%1'",
617 item->text()));
618 }
619 emit storageSetupDone(index, false);
620 } else {
621 emit storageSetupDone(index, true);
622 }
623 }
624
625 void PlacesItemModel::hideItem()
626 {
627 hideItem(m_hiddenItemToRemove);
628 m_hiddenItemToRemove = -1;
629 }
630
631 void PlacesItemModel::updateBookmarks()
632 {
633 // Verify whether new bookmarks have been added or existing
634 // bookmarks have been changed.
635 KBookmarkGroup root = m_bookmarkManager->root();
636 KBookmark newBookmark = root.first();
637 while (!newBookmark.isNull()) {
638 if (acceptBookmark(newBookmark, m_availableDevices)) {
639 bool found = false;
640 int modelIndex = 0;
641 for (int i = 0; i < m_bookmarkedItems.count(); ++i) {
642 PlacesItem* item = m_bookmarkedItems[i];
643 if (!item) {
644 item = placesItem(modelIndex);
645 ++modelIndex;
646 }
647
648 const KBookmark oldBookmark = item->bookmark();
649 if (equalBookmarkIdentifiers(newBookmark, oldBookmark)) {
650 // The bookmark has been found in the model or as
651 // a hidden item. The content of the bookmark might
652 // have been changed, so an update is done.
653 found = true;
654 if (newBookmark.metaDataItem(QStringLiteral("UDI")).isEmpty()) {
655 item->setBookmark(newBookmark);
656 item->setText(i18nc("KFile System Bookmarks", newBookmark.text().toUtf8().constData()));
657 }
658 break;
659 }
660 }
661
662 if (!found) {
663 const QString udi = newBookmark.metaDataItem(QStringLiteral("UDI"));
664
665 /*
666 * See Bug 304878
667 * Only add a new places item, if the item text is not empty
668 * and if the device is available. Fixes the strange behaviour -
669 * add a places item without text in the Places section - when you
670 * remove a device (e.g. a usb stick) without unmounting.
671 */
672 if (udi.isEmpty() || Solid::Device(udi).isValid()) {
673 PlacesItem* item = new PlacesItem(newBookmark);
674 if (item->isHidden() && !m_hiddenItemsShown) {
675 m_bookmarkedItems.append(item);
676 } else {
677 appendItemToGroup(item);
678 }
679 }
680 }
681 }
682
683 newBookmark = root.next(newBookmark);
684 }
685
686 // Remove items that are not part of the bookmark-manager anymore
687 int modelIndex = 0;
688 for (int i = m_bookmarkedItems.count() - 1; i >= 0; --i) {
689 PlacesItem* item = m_bookmarkedItems[i];
690 const bool itemIsPartOfModel = (item == 0);
691 if (itemIsPartOfModel) {
692 item = placesItem(modelIndex);
693 }
694
695 bool hasBeenRemoved = true;
696 const KBookmark oldBookmark = item->bookmark();
697 KBookmark newBookmark = root.first();
698 while (!newBookmark.isNull()) {
699 if (equalBookmarkIdentifiers(newBookmark, oldBookmark)) {
700 hasBeenRemoved = false;
701 break;
702 }
703 newBookmark = root.next(newBookmark);
704 }
705
706 if (hasBeenRemoved) {
707 if (m_bookmarkedItems[i]) {
708 delete m_bookmarkedItems[i];
709 m_bookmarkedItems.removeAt(i);
710 } else {
711 removeItem(modelIndex);
712 --modelIndex;
713 }
714 }
715
716 if (itemIsPartOfModel) {
717 ++modelIndex;
718 }
719 }
720 }
721
722 void PlacesItemModel::saveBookmarks()
723 {
724 m_bookmarkManager->emitChanged(m_bookmarkManager->root());
725 }
726
727 void PlacesItemModel::loadBookmarks()
728 {
729 KBookmarkGroup root = m_bookmarkManager->root();
730 KBookmark bookmark = root.first();
731 QSet<QString> devices = m_availableDevices;
732
733 QSet<QUrl> missingSystemBookmarks;
734 foreach (const SystemBookmarkData& data, m_systemBookmarks) {
735 missingSystemBookmarks.insert(data.url);
736 }
737
738 // The bookmarks might have a mixed order of places, devices and search-groups due
739 // to the compatibility with the KFilePlacesPanel. In Dolphin's places panel the
740 // items should always be collected in one group so the items are collected first
741 // in separate lists before inserting them.
742 QList<PlacesItem*> placesItems;
743 QList<PlacesItem*> recentlySavedItems;
744 QList<PlacesItem*> searchForItems;
745 QList<PlacesItem*> devicesItems;
746
747 while (!bookmark.isNull()) {
748 if (acceptBookmark(bookmark, devices)) {
749 PlacesItem* item = new PlacesItem(bookmark);
750 if (item->groupType() == PlacesItem::DevicesType) {
751 devices.remove(item->udi());
752 devicesItems.append(item);
753 } else {
754 const QUrl url = bookmark.url();
755 if (missingSystemBookmarks.contains(url)) {
756 missingSystemBookmarks.remove(url);
757
758 // Try to retranslate the text of system bookmarks to have translated
759 // items when changing the language. In case if the user has applied a custom
760 // text, the retranslation will fail and the users custom text is still used.
761 // It is important to use "KFile System Bookmarks" as context (see
762 // createSystemBookmarks()).
763 item->setText(i18nc("KFile System Bookmarks", bookmark.text().toUtf8().constData()));
764 item->setSystemItem(true);
765 }
766
767 switch (item->groupType()) {
768 case PlacesItem::PlacesType: placesItems.append(item); break;
769 case PlacesItem::RecentlySavedType: recentlySavedItems.append(item); break;
770 case PlacesItem::SearchForType: searchForItems.append(item); break;
771 case PlacesItem::DevicesType:
772 default: Q_ASSERT(false); break;
773 }
774 }
775 }
776
777 bookmark = root.next(bookmark);
778 }
779
780 if (!missingSystemBookmarks.isEmpty()) {
781 // The current bookmarks don't contain all system-bookmarks. Add the missing
782 // bookmarks.
783 foreach (const SystemBookmarkData& data, m_systemBookmarks) {
784 if (missingSystemBookmarks.contains(data.url)) {
785 PlacesItem* item = createSystemPlacesItem(data);
786 switch (item->groupType()) {
787 case PlacesItem::PlacesType: placesItems.append(item); break;
788 case PlacesItem::RecentlySavedType: recentlySavedItems.append(item); break;
789 case PlacesItem::SearchForType: searchForItems.append(item); break;
790 case PlacesItem::DevicesType:
791 default: Q_ASSERT(false); break;
792 }
793 }
794 }
795 }
796
797 // Create items for devices that have not been stored as bookmark yet
798 devicesItems.reserve(devicesItems.count() + devices.count());
799 foreach (const QString& udi, devices) {
800 const KBookmark bookmark = PlacesItem::createDeviceBookmark(m_bookmarkManager, udi);
801 PlacesItem *item = new PlacesItem(bookmark);
802 devicesItems.append(item);
803 connect(item->signalHandler(), &PlacesItemSignalHandler::tearDownExternallyRequested,
804 this, &PlacesItemModel::storageTearDownExternallyRequested);
805 }
806
807 QList<PlacesItem*> items;
808 items.append(placesItems);
809 items.append(recentlySavedItems);
810 items.append(searchForItems);
811 items.append(devicesItems);
812
813 foreach (PlacesItem* item, items) {
814 if (!m_hiddenItemsShown && item->isHidden()) {
815 m_bookmarkedItems.append(item);
816 } else {
817 appendItem(item);
818 }
819 }
820
821 #ifdef PLACESITEMMODEL_DEBUG
822 qCDebug(DolphinDebug) << "Loaded bookmarks";
823 showModelState();
824 #endif
825 }
826
827 bool PlacesItemModel::acceptBookmark(const KBookmark& bookmark,
828 const QSet<QString>& availableDevices) const
829 {
830 const QString udi = bookmark.metaDataItem(QStringLiteral("UDI"));
831 const QUrl url = bookmark.url();
832 const QString appName = bookmark.metaDataItem(QStringLiteral("OnlyInApp"));
833 const bool deviceAvailable = availableDevices.contains(udi);
834
835 const bool allowedHere = (appName.isEmpty()
836 || appName == KAboutData::applicationData().componentName()
837 || appName == KAboutData::applicationData().componentName() + AppNamePrefix)
838 && (m_fileIndexingEnabled || (url.scheme() != QLatin1String("timeline") &&
839 url.scheme() != QLatin1String("search")));
840
841 return (udi.isEmpty() && allowedHere) || deviceAvailable;
842 }
843
844 PlacesItem* PlacesItemModel::createSystemPlacesItem(const SystemBookmarkData& data)
845 {
846 KBookmark bookmark = PlacesItem::createBookmark(m_bookmarkManager,
847 data.text,
848 data.url,
849 data.icon);
850
851 const QString protocol = data.url.scheme();
852 if (protocol == QLatin1String("timeline") || protocol == QLatin1String("search")) {
853 // As long as the KFilePlacesView from kdelibs is available, the system-bookmarks
854 // for "Recently Saved" and "Search For" should be a setting available only
855 // in the Places Panel (see description of AppNamePrefix for more details).
856 const QString appName = KAboutData::applicationData().componentName() + AppNamePrefix;
857 bookmark.setMetaDataItem(QStringLiteral("OnlyInApp"), appName);
858 }
859
860 PlacesItem* item = new PlacesItem(bookmark);
861 item->setSystemItem(true);
862
863 // Create default view-properties for all "Search For" and "Recently Saved" bookmarks
864 // in case if the user has not already created custom view-properties for a corresponding
865 // query yet.
866 const bool createDefaultViewProperties = (item->groupType() == PlacesItem::SearchForType ||
867 item->groupType() == PlacesItem::RecentlySavedType) &&
868 !GeneralSettings::self()->globalViewProps();
869 if (createDefaultViewProperties) {
870 ViewProperties props(convertedUrl(data.url));
871 if (!props.exist()) {
872 const QString path = data.url.path();
873 if (path == QLatin1String("/documents")) {
874 props.setViewMode(DolphinView::DetailsView);
875 props.setPreviewsShown(false);
876 props.setVisibleRoles({"text", "path"});
877 } else if (path == QLatin1String("/images")) {
878 props.setViewMode(DolphinView::IconsView);
879 props.setPreviewsShown(true);
880 props.setVisibleRoles({"text", "imageSize"});
881 } else if (path == QLatin1String("/audio")) {
882 props.setViewMode(DolphinView::DetailsView);
883 props.setPreviewsShown(false);
884 props.setVisibleRoles({"text", "artist", "album"});
885 } else if (path == QLatin1String("/videos")) {
886 props.setViewMode(DolphinView::IconsView);
887 props.setPreviewsShown(true);
888 props.setVisibleRoles({"text"});
889 } else if (data.url.scheme() == QLatin1String("timeline")) {
890 props.setViewMode(DolphinView::DetailsView);
891 props.setVisibleRoles({"text", "modificationtime"});
892 }
893 }
894 }
895
896 return item;
897 }
898
899 void PlacesItemModel::createSystemBookmarks()
900 {
901 Q_ASSERT(m_systemBookmarks.isEmpty());
902 Q_ASSERT(m_systemBookmarksIndexes.isEmpty());
903
904 // Note: The context of the I18N_NOOP2 must be "KFile System Bookmarks". The real
905 // i18nc call is done after reading the bookmark. The reason why the i18nc call is not
906 // done here is because otherwise switching the language would not result in retranslating the
907 // bookmarks.
908 m_systemBookmarks.append(SystemBookmarkData(QUrl::fromLocalFile(QDir::homePath()),
909 QStringLiteral("user-home"),
910 I18N_NOOP2("KFile System Bookmarks", "Home")));
911 m_systemBookmarks.append(SystemBookmarkData(QUrl(QStringLiteral("remote:/")),
912 QStringLiteral("network-workgroup"),
913 I18N_NOOP2("KFile System Bookmarks", "Network")));
914 m_systemBookmarks.append(SystemBookmarkData(QUrl::fromLocalFile(QStringLiteral("/")),
915 QStringLiteral("folder-red"),
916 I18N_NOOP2("KFile System Bookmarks", "Root")));
917 m_systemBookmarks.append(SystemBookmarkData(QUrl(QStringLiteral("trash:/")),
918 QStringLiteral("user-trash"),
919 I18N_NOOP2("KFile System Bookmarks", "Trash")));
920
921 if (m_fileIndexingEnabled) {
922 m_systemBookmarks.append(SystemBookmarkData(QUrl(QStringLiteral("timeline:/today")),
923 QStringLiteral("go-jump-today"),
924 I18N_NOOP2("KFile System Bookmarks", "Today")));
925 m_systemBookmarks.append(SystemBookmarkData(QUrl(QStringLiteral("timeline:/yesterday")),
926 QStringLiteral("view-calendar-day"),
927 I18N_NOOP2("KFile System Bookmarks", "Yesterday")));
928 m_systemBookmarks.append(SystemBookmarkData(QUrl(QStringLiteral("timeline:/thismonth")),
929 QStringLiteral("view-calendar-month"),
930 I18N_NOOP2("KFile System Bookmarks", "This Month")));
931 m_systemBookmarks.append(SystemBookmarkData(QUrl(QStringLiteral("timeline:/lastmonth")),
932 QStringLiteral("view-calendar-month"),
933 I18N_NOOP2("KFile System Bookmarks", "Last Month")));
934 m_systemBookmarks.append(SystemBookmarkData(QUrl(QStringLiteral("search:/documents")),
935 QStringLiteral("folder-text"),
936 I18N_NOOP2("KFile System Bookmarks", "Documents")));
937 m_systemBookmarks.append(SystemBookmarkData(QUrl(QStringLiteral("search:/images")),
938 QStringLiteral("folder-images"),
939 I18N_NOOP2("KFile System Bookmarks", "Images")));
940 m_systemBookmarks.append(SystemBookmarkData(QUrl(QStringLiteral("search:/audio")),
941 QStringLiteral("folder-sound"),
942 I18N_NOOP2("KFile System Bookmarks", "Audio Files")));
943 m_systemBookmarks.append(SystemBookmarkData(QUrl(QStringLiteral("search:/videos")),
944 QStringLiteral("folder-videos"),
945 I18N_NOOP2("KFile System Bookmarks", "Videos")));
946 }
947
948 for (int i = 0; i < m_systemBookmarks.count(); ++i) {
949 m_systemBookmarksIndexes.insert(m_systemBookmarks[i].url, i);
950 }
951 }
952
953 void PlacesItemModel::clear() {
954 m_bookmarkedItems.clear();
955 KStandardItemModel::clear();
956 }
957
958 void PlacesItemModel::proceedWithTearDown()
959 {
960 Q_ASSERT(m_deviceToTearDown);
961
962 connect(m_deviceToTearDown, &Solid::StorageAccess::teardownDone,
963 this, &PlacesItemModel::slotStorageTearDownDone);
964 m_deviceToTearDown->teardown();
965 }
966
967 void PlacesItemModel::initializeAvailableDevices()
968 {
969 QString predicate(QStringLiteral("[[[[ StorageVolume.ignored == false AND [ StorageVolume.usage == 'FileSystem' OR StorageVolume.usage == 'Encrypted' ]]"
970 " OR "
971 "[ IS StorageAccess AND StorageDrive.driveType == 'Floppy' ]]"
972 " OR "
973 "OpticalDisc.availableContent & 'Audio' ]"
974 " OR "
975 "StorageAccess.ignored == false ]"));
976
977
978 if (KProtocolInfo::isKnownProtocol(QStringLiteral("mtp"))) {
979 predicate.prepend("[");
980 predicate.append(" OR PortableMediaPlayer.supportedProtocols == 'mtp']");
981 }
982
983 m_predicate = Solid::Predicate::fromString(predicate);
984 Q_ASSERT(m_predicate.isValid());
985
986 Solid::DeviceNotifier* notifier = Solid::DeviceNotifier::instance();
987 connect(notifier, &Solid::DeviceNotifier::deviceAdded, this, &PlacesItemModel::slotDeviceAdded);
988 connect(notifier, &Solid::DeviceNotifier::deviceRemoved, this, &PlacesItemModel::slotDeviceRemoved);
989
990 const QList<Solid::Device>& deviceList = Solid::Device::listFromQuery(m_predicate);
991 foreach (const Solid::Device& device, deviceList) {
992 m_availableDevices << device.udi();
993 }
994 }
995
996 int PlacesItemModel::bookmarkIndex(int index) const
997 {
998 int bookmarkIndex = 0;
999 int modelIndex = 0;
1000 while (bookmarkIndex < m_bookmarkedItems.count()) {
1001 if (!m_bookmarkedItems[bookmarkIndex]) {
1002 if (modelIndex == index) {
1003 break;
1004 }
1005 ++modelIndex;
1006 }
1007 ++bookmarkIndex;
1008 }
1009
1010 return bookmarkIndex >= m_bookmarkedItems.count() ? -1 : bookmarkIndex;
1011 }
1012
1013 void PlacesItemModel::hideItem(int index)
1014 {
1015 PlacesItem* shownItem = placesItem(index);
1016 if (!shownItem) {
1017 return;
1018 }
1019
1020 shownItem->setHidden(true);
1021 if (m_hiddenItemsShown) {
1022 // Removing items from the model is not allowed if all hidden
1023 // items should be shown.
1024 return;
1025 }
1026
1027 const int newIndex = bookmarkIndex(index);
1028 if (newIndex >= 0) {
1029 const KBookmark hiddenBookmark = shownItem->bookmark();
1030 PlacesItem* hiddenItem = new PlacesItem(hiddenBookmark);
1031
1032 const PlacesItem* previousItem = placesItem(index - 1);
1033 KBookmark previousBookmark;
1034 if (previousItem) {
1035 previousBookmark = previousItem->bookmark();
1036 }
1037
1038 const bool updateBookmark = (m_bookmarkManager->root().indexOf(hiddenBookmark) >= 0);
1039 removeItem(index);
1040
1041 if (updateBookmark) {
1042 // removeItem() also removed the bookmark from m_bookmarkManager in
1043 // PlacesItemModel::onItemRemoved(). However for hidden items the
1044 // bookmark should still be remembered, so readd it again:
1045 m_bookmarkManager->root().addBookmark(hiddenBookmark);
1046 m_bookmarkManager->root().moveBookmark(hiddenBookmark, previousBookmark);
1047 }
1048
1049 m_bookmarkedItems.insert(newIndex, hiddenItem);
1050 }
1051 }
1052
1053 QString PlacesItemModel::internalMimeType() const
1054 {
1055 return "application/x-dolphinplacesmodel-" +
1056 QString::number((qptrdiff)this);
1057 }
1058
1059 int PlacesItemModel::groupedDropIndex(int index, const PlacesItem* item) const
1060 {
1061 Q_ASSERT(item);
1062
1063 int dropIndex = index;
1064 const PlacesItem::GroupType type = item->groupType();
1065
1066 const int itemCount = count();
1067 if (index < 0) {
1068 dropIndex = itemCount;
1069 }
1070
1071 // Search nearest previous item with the same group
1072 int previousIndex = -1;
1073 for (int i = dropIndex - 1; i >= 0; --i) {
1074 if (placesItem(i)->groupType() == type) {
1075 previousIndex = i;
1076 break;
1077 }
1078 }
1079
1080 // Search nearest next item with the same group
1081 int nextIndex = -1;
1082 for (int i = dropIndex; i < count(); ++i) {
1083 if (placesItem(i)->groupType() == type) {
1084 nextIndex = i;
1085 break;
1086 }
1087 }
1088
1089 // Adjust the drop-index to be inserted to the
1090 // nearest item with the same group.
1091 if (previousIndex >= 0 && nextIndex >= 0) {
1092 dropIndex = (dropIndex - previousIndex < nextIndex - dropIndex) ?
1093 previousIndex + 1 : nextIndex;
1094 } else if (previousIndex >= 0) {
1095 dropIndex = previousIndex + 1;
1096 } else if (nextIndex >= 0) {
1097 dropIndex = nextIndex;
1098 }
1099
1100 return dropIndex;
1101 }
1102
1103 bool PlacesItemModel::equalBookmarkIdentifiers(const KBookmark& b1, const KBookmark& b2)
1104 {
1105 const QString udi1 = b1.metaDataItem(QStringLiteral("UDI"));
1106 const QString udi2 = b2.metaDataItem(QStringLiteral("UDI"));
1107 if (!udi1.isEmpty() && !udi2.isEmpty()) {
1108 return udi1 == udi2;
1109 } else {
1110 return b1.metaDataItem(QStringLiteral("ID")) == b2.metaDataItem(QStringLiteral("ID"));
1111 }
1112 }
1113
1114 QUrl PlacesItemModel::createTimelineUrl(const QUrl& url)
1115 {
1116 // TODO: Clarify with the Baloo-team whether it makes sense
1117 // provide default-timeline-URLs like 'yesterday', 'this month'
1118 // and 'last month'.
1119 QUrl timelineUrl;
1120
1121 const QString path = url.toDisplayString(QUrl::PreferLocalFile);
1122 if (path.endsWith(QLatin1String("yesterday"))) {
1123 const QDate date = QDate::currentDate().addDays(-1);
1124 const int year = date.year();
1125 const int month = date.month();
1126 const int day = date.day();
1127 timelineUrl = QUrl("timeline:/" + timelineDateString(year, month) +
1128 '/' + timelineDateString(year, month, day));
1129 } else if (path.endsWith(QLatin1String("thismonth"))) {
1130 const QDate date = QDate::currentDate();
1131 timelineUrl = QUrl("timeline:/" + timelineDateString(date.year(), date.month()));
1132 } else if (path.endsWith(QLatin1String("lastmonth"))) {
1133 const QDate date = QDate::currentDate().addMonths(-1);
1134 timelineUrl = QUrl("timeline:/" + timelineDateString(date.year(), date.month()));
1135 } else {
1136 Q_ASSERT(path.endsWith(QLatin1String("today")));
1137 timelineUrl = url;
1138 }
1139
1140 return timelineUrl;
1141 }
1142
1143 QString PlacesItemModel::timelineDateString(int year, int month, int day)
1144 {
1145 QString date = QString::number(year) + '-';
1146 if (month < 10) {
1147 date += '0';
1148 }
1149 date += QString::number(month);
1150
1151 if (day >= 1) {
1152 date += '-';
1153 if (day < 10) {
1154 date += '0';
1155 }
1156 date += QString::number(day);
1157 }
1158
1159 return date;
1160 }
1161
1162 bool PlacesItemModel::isDir(int index) const
1163 {
1164 Q_UNUSED(index);
1165 return true;
1166 }
1167
1168 QUrl PlacesItemModel::createSearchUrl(const QUrl& url)
1169 {
1170 QUrl searchUrl;
1171
1172 #ifdef HAVE_BALOO
1173 const QString path = url.toDisplayString(QUrl::PreferLocalFile);
1174 if (path.endsWith(QLatin1String("documents"))) {
1175 searchUrl = searchUrlForType(QStringLiteral("Document"));
1176 } else if (path.endsWith(QLatin1String("images"))) {
1177 searchUrl = searchUrlForType(QStringLiteral("Image"));
1178 } else if (path.endsWith(QLatin1String("audio"))) {
1179 searchUrl = searchUrlForType(QStringLiteral("Audio"));
1180 } else if (path.endsWith(QLatin1String("videos"))) {
1181 searchUrl = searchUrlForType(QStringLiteral("Video"));
1182 } else {
1183 Q_ASSERT(false);
1184 }
1185 #else
1186 Q_UNUSED(url);
1187 #endif
1188
1189 return searchUrl;
1190 }
1191
1192 #ifdef HAVE_BALOO
1193 QUrl PlacesItemModel::searchUrlForType(const QString& type)
1194 {
1195 Baloo::Query query;
1196 query.addType(type);
1197
1198 return query.toSearchUrl();
1199 }
1200 #endif
1201
1202 #ifdef PLACESITEMMODEL_DEBUG
1203 void PlacesItemModel::showModelState()
1204 {
1205 qCDebug(DolphinDebug) << "=================================";
1206 qCDebug(DolphinDebug) << "Model:";
1207 qCDebug(DolphinDebug) << "hidden-index model-index text";
1208 int modelIndex = 0;
1209 for (int i = 0; i < m_bookmarkedItems.count(); ++i) {
1210 if (m_bookmarkedItems[i]) {
1211 qCDebug(DolphinDebug) << i << "(Hidden) " << " " << m_bookmarkedItems[i]->dataValue("text").toString();
1212 } else {
1213 if (item(modelIndex)) {
1214 qCDebug(DolphinDebug) << i << " " << modelIndex << " " << item(modelIndex)->dataValue("text").toString();
1215 } else {
1216 qCDebug(DolphinDebug) << i << " " << modelIndex << " " << "(not available yet)";
1217 }
1218 ++modelIndex;
1219 }
1220 }
1221
1222 qCDebug(DolphinDebug);
1223 qCDebug(DolphinDebug) << "Bookmarks:";
1224
1225 int bookmarkIndex = 0;
1226 KBookmarkGroup root = m_bookmarkManager->root();
1227 KBookmark bookmark = root.first();
1228 while (!bookmark.isNull()) {
1229 const QString udi = bookmark.metaDataItem("UDI");
1230 const QString text = udi.isEmpty() ? bookmark.text() : udi;
1231 if (bookmark.metaDataItem("IsHidden") == QLatin1String("true")) {
1232 qCDebug(DolphinDebug) << bookmarkIndex << "(Hidden)" << text;
1233 } else {
1234 qCDebug(DolphinDebug) << bookmarkIndex << " " << text;
1235 }
1236
1237 bookmark = root.next(bookmark);
1238 ++bookmarkIndex;
1239 }
1240 }
1241 #endif
1242