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