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