1/*
2 This file is part of the KDE project
3 SPDX-FileCopyrightText: 2007 Kevin Ottens <ervin@kde.org>
4
5 SPDX-License-Identifier: LGPL-2.0-only
6*/
7
8#include "kfileplacesitem_p.h"
9
10#include <QDateTime>
11#include <QDir>
12#include <QIcon>
13
14#include <KBookmarkManager>
15#include <KConfig>
16#include <KConfigGroup>
17#include <KIconUtils>
18#include <KLocalizedString>
19#include <KMountPoint>
20#include <kprotocolinfo.h>
21#include <solid/block.h>
22#include <solid/genericinterface.h>
23#include <solid/networkshare.h>
24#include <solid/opticaldisc.h>
25#include <solid/opticaldrive.h>
26#include <solid/portablemediaplayer.h>
27#include <solid/storageaccess.h>
28#include <solid/storagedrive.h>
29#include <solid/storagevolume.h>
30
31static bool isTrash(const KBookmark &bk)
32{
33 return bk.url().toString() == QLatin1String("trash:/");
34}
35
36KFilePlacesItem::KFilePlacesItem(KBookmarkManager *manager, const QString &address, const QString &udi, KFilePlacesModel *parent)
37 : QObject(static_cast<QObject *>(parent))
38 , m_manager(manager)
39 , m_folderIsEmpty(true)
40 , m_isCdrom(false)
41 , m_isAccessible(false)
42 , m_isTeardownAllowed(false)
43 , m_isTeardownOverlayRecommended(false)
44 , m_isTeardownInProgress(false)
45 , m_isSetupInProgress(false)
46 , m_isEjectInProgress(false)
47 , m_isReadOnly(false)
48{
49 updateDeviceInfo(udi);
50
51 setBookmark(m_manager->findByAddress(address));
52
53 if (udi.isEmpty() && m_bookmark.metaDataItem(QStringLiteral("ID")).isEmpty()) {
54 m_bookmark.setMetaDataItem(QStringLiteral("ID"), value: generateNewId());
55 } else if (udi.isEmpty()) {
56 if (isTrash(bk: m_bookmark)) {
57 KConfig cfg(QStringLiteral("trashrc"), KConfig::SimpleConfig);
58 const KConfigGroup group = cfg.group(QStringLiteral("Status"));
59 m_folderIsEmpty = group.readEntry(key: "Empty", defaultValue: true);
60 }
61 }
62
63 // Hide SSHFS network device mounted by kdeconnect, since we already have the kdeconnect:// place.
64 if (isDevice() && !isHidden() && m_access && device().vendor() == QLatin1String("fuse.sshfs")) {
65 const QString storageFilePath = m_access->filePath();
66 // kdeconnect mounts into runtime dir or temp, anyone else cannot be kdeconnect.
67 QString runtimePath = QStandardPaths::writableLocation(type: QStandardPaths::RuntimeLocation);
68 if (runtimePath.isEmpty()) {
69 runtimePath = QStandardPaths::writableLocation(type: QStandardPaths::TempLocation);
70 }
71 if (!runtimePath.isEmpty()) {
72 runtimePath.append(c: QLatin1Char('/'));
73 }
74
75 if (runtimePath.isEmpty() || storageFilePath.startsWith(s: runtimePath)) {
76 // Not using findByPath() as it resolves symlinks, potentially blocking,
77 // but here we know we query for an existing actual mount point.
78 const auto mountPoints = KMountPoint::currentMountPoints();
79 auto it = std::find_if(first: mountPoints.cbegin(), last: mountPoints.cend(), pred: [&storageFilePath](const KMountPoint::Ptr &mountPoint) {
80 return mountPoint->mountPoint() == storageFilePath;
81 });
82 if (it != mountPoints.cend()) {
83 if ((*it)->mountedFrom().startsWith(s: QLatin1String("kdeconnect@"))) {
84 // Hide only if the user never set the "Hide" checkbox on the device.
85 if (m_bookmark.metaDataItem(QStringLiteral("IsHidden")).isEmpty()) {
86 setHidden(true);
87 }
88 }
89 }
90 }
91 }
92}
93
94KFilePlacesItem::~KFilePlacesItem()
95{
96}
97
98QString KFilePlacesItem::id() const
99{
100 if (isDevice()) {
101 return bookmark().metaDataItem(QStringLiteral("UDI"));
102 } else {
103 return bookmark().metaDataItem(QStringLiteral("ID"));
104 }
105}
106
107bool KFilePlacesItem::hasSupportedScheme(const QStringList &schemes) const
108{
109 if (schemes.isEmpty()) {
110 return true;
111 }
112
113 // StorageAccess is always local, doesn't need to be accessible to know this
114 if (m_access && schemes.contains(str: QLatin1String("file"))) {
115 return true;
116 }
117
118 if (m_networkShare && schemes.contains(str: m_networkShare->url().scheme())) {
119 return true;
120 }
121
122 if (m_player) {
123 const QStringList protocols = m_player->supportedProtocols();
124 for (const QString &protocol : protocols) {
125 if (schemes.contains(str: protocol)) {
126 return true;
127 }
128 }
129 }
130
131 return false;
132}
133
134bool KFilePlacesItem::isDevice() const
135{
136 return !bookmark().metaDataItem(QStringLiteral("UDI")).isEmpty();
137}
138
139KFilePlacesModel::DeviceAccessibility KFilePlacesItem::deviceAccessibility() const
140{
141 if (m_isTeardownInProgress || m_isEjectInProgress) {
142 return KFilePlacesModel::TeardownInProgress;
143 } else if (m_isSetupInProgress) {
144 return KFilePlacesModel::SetupInProgress;
145 } else if (m_isAccessible) {
146 return KFilePlacesModel::Accessible;
147 } else {
148 return KFilePlacesModel::SetupNeeded;
149 }
150}
151
152bool KFilePlacesItem::isTeardownAllowed() const
153{
154 return m_isTeardownAllowed;
155}
156
157bool KFilePlacesItem::isTeardownOverlayRecommended() const
158{
159 return m_isTeardownOverlayRecommended;
160}
161
162bool KFilePlacesItem::isEjectAllowed() const
163{
164 return m_isCdrom;
165}
166
167KBookmark KFilePlacesItem::bookmark() const
168{
169 return m_bookmark;
170}
171
172void KFilePlacesItem::setBookmark(const KBookmark &bookmark)
173{
174 m_bookmark = bookmark;
175
176 if (m_device.isValid()) {
177 m_bookmark.setMetaDataItem(QStringLiteral("UDI"), value: m_device.udi());
178 if (m_volume && !m_volume->uuid().isEmpty()) {
179 m_bookmark.setMetaDataItem(QStringLiteral("uuid"), value: m_volume->uuid());
180 }
181 }
182
183 if (bookmark.metaDataItem(QStringLiteral("isSystemItem")) == QLatin1String("true")) {
184 // This context must stay as it is - the translated system bookmark names
185 // are created with 'KFile System Bookmarks' as their context, so this
186 // ensures the right string is picked from the catalog.
187 // (coles, 13th May 2009)
188
189 m_text = i18nc("KFile System Bookmarks", bookmark.text().toUtf8().data());
190 } else {
191 m_text = bookmark.text();
192 }
193
194 if (!isDevice()) {
195 const QString protocol = bookmark.url().scheme();
196 if (protocol == QLatin1String("timeline") || protocol == QLatin1String("recentlyused")) {
197 m_groupType = KFilePlacesModel::RecentlySavedType;
198 } else if (protocol.contains(s: QLatin1String("search"))) {
199 m_groupType = KFilePlacesModel::SearchForType;
200 } else if (protocol == QLatin1String("bluetooth") || protocol == QLatin1String("obexftp") || protocol == QLatin1String("kdeconnect")) {
201 m_groupType = KFilePlacesModel::DevicesType;
202 } else if (protocol == QLatin1String("tags")) {
203 m_groupType = KFilePlacesModel::TagsType;
204 } else if (protocol == QLatin1String("remote") || KProtocolInfo::protocolClass(protocol) != QLatin1String(":local")) {
205 m_groupType = KFilePlacesModel::RemoteType;
206 } else {
207 m_groupType = KFilePlacesModel::PlacesType;
208 }
209 } else {
210 if (m_drive && m_drive->isRemovable()) {
211 m_groupType = KFilePlacesModel::RemovableDevicesType;
212 } else if (m_networkShare) {
213 m_groupType = KFilePlacesModel::RemoteType;
214 } else {
215 m_groupType = KFilePlacesModel::DevicesType;
216 }
217 }
218
219 switch (m_groupType) {
220 case KFilePlacesModel::PlacesType:
221 m_groupName = i18nc("@item", "Places");
222 break;
223 case KFilePlacesModel::RemoteType:
224 m_groupName = i18nc("@item", "Remote");
225 break;
226 case KFilePlacesModel::RecentlySavedType:
227 m_groupName = i18nc("@item The place group section name for recent dynamic lists", "Recent");
228 break;
229 case KFilePlacesModel::SearchForType:
230 m_groupName = i18nc("@item", "Search For");
231 break;
232 case KFilePlacesModel::DevicesType:
233 m_groupName = i18nc("@item", "Devices");
234 break;
235 case KFilePlacesModel::RemovableDevicesType:
236 m_groupName = i18nc("@item", "Removable Devices");
237 break;
238 case KFilePlacesModel::TagsType:
239 m_groupName = i18nc("@item", "Tags");
240 break;
241 case KFilePlacesModel::UnknownType:
242 Q_UNREACHABLE();
243 break;
244 }
245}
246
247Solid::Device KFilePlacesItem::device() const
248{
249 return m_device;
250}
251
252QVariant KFilePlacesItem::data(int role) const
253{
254 if (role == KFilePlacesModel::GroupRole) {
255 return QVariant(m_groupName);
256 } else if (role != KFilePlacesModel::HiddenRole && role != Qt::BackgroundRole && isDevice()) {
257 return deviceData(role);
258 } else {
259 return bookmarkData(role);
260 }
261}
262
263KFilePlacesModel::GroupType KFilePlacesItem::groupType() const
264{
265 return m_groupType;
266}
267
268bool KFilePlacesItem::isHidden() const
269{
270 return m_bookmark.metaDataItem(QStringLiteral("IsHidden")) == QLatin1String("true");
271}
272
273void KFilePlacesItem::setHidden(bool hide)
274{
275 if (m_bookmark.isNull() || isHidden() == hide) {
276 return;
277 }
278 m_bookmark.setMetaDataItem(QStringLiteral("IsHidden"), value: hide ? QStringLiteral("true") : QStringLiteral("false"));
279}
280
281QVariant KFilePlacesItem::bookmarkData(int role) const
282{
283 KBookmark b = bookmark();
284
285 if (b.isNull()) {
286 return QVariant();
287 }
288
289 switch (role) {
290 case Qt::DisplayRole:
291 return m_text;
292 case Qt::DecorationRole:
293 return QIcon::fromTheme(name: iconNameForBookmark(bookmark: b));
294 case Qt::ToolTipRole: {
295 const KFilePlacesModel::GroupType type = groupType();
296 // Don't display technical gibberish in the URL, particularly search.
297 if (type != KFilePlacesModel::RecentlySavedType && type != KFilePlacesModel::SearchForType && type != KFilePlacesModel::TagsType) {
298 return b.url().toDisplayString(options: QUrl::PreferLocalFile);
299 }
300 return QString();
301 }
302 case Qt::BackgroundRole:
303 if (isHidden()) {
304 return QColor(Qt::lightGray);
305 } else {
306 return QVariant();
307 }
308 case KFilePlacesModel::UrlRole:
309 return b.url();
310 case KFilePlacesModel::SetupNeededRole:
311 return false;
312 case KFilePlacesModel::HiddenRole:
313 return isHidden();
314 case KFilePlacesModel::IconNameRole:
315 return iconNameForBookmark(bookmark: b);
316 default:
317 return QVariant();
318 }
319}
320
321QVariant KFilePlacesItem::deviceData(int role) const
322{
323 Solid::Device d = device();
324
325 if (d.isValid()) {
326 switch (role) {
327 case Qt::DisplayRole:
328 if (m_deviceDisplayName.isEmpty()) {
329 m_deviceDisplayName = d.displayName();
330 }
331 return m_deviceDisplayName;
332 case Qt::DecorationRole:
333 // qDebug() << "adding emblems" << m_emblems << "to device icon" << m_deviceIconName;
334 return KIconUtils::addOverlays(iconName: m_deviceIconName, overlays: m_emblems);
335 case Qt::ToolTipRole: {
336 if (m_access && m_isAccessible) {
337 // For loop devices, show backing file path rather than /dev/loop123.
338 QString mountedFrom = m_backingFile;
339 if (mountedFrom.isEmpty() && m_block) {
340 mountedFrom = m_block->device();
341 }
342
343 if (!mountedFrom.isEmpty()) {
344 return i18nc("@info:tooltip path (mounted from)", "%1 (from %2)", m_access->filePath(), mountedFrom);
345 }
346 } else if (!m_backingFile.isEmpty()) {
347 return m_backingFile;
348 } else if (m_block) {
349 return m_block->device();
350 }
351 return QString();
352 }
353 case KFilePlacesModel::UrlRole:
354 if (m_access) {
355 const QString path = m_access->filePath();
356 return path.isEmpty() ? QUrl() : QUrl::fromLocalFile(localfile: path);
357 } else if (m_disc && (m_disc->availableContent() & Solid::OpticalDisc::Audio) != 0) {
358 Solid::Block *block = d.as<Solid::Block>();
359 if (block) {
360 QString device = block->device();
361 return QUrl(QStringLiteral("audiocd:/?device=%1").arg(a: device));
362 }
363 // We failed to get the block device. Assume audiocd:/ can
364 // figure it out, but cannot handle multiple disc drives.
365 // See https://bugs.kde.org/show_bug.cgi?id=314544#c40
366 return QUrl(QStringLiteral("audiocd:/"));
367 } else if (m_player) {
368 const QStringList protocols = m_player->supportedProtocols();
369 if (!protocols.isEmpty()) {
370 const QString protocol = protocols.first();
371 if (protocol == QLatin1String("mtp")) {
372 return QUrl(QStringLiteral("%1:udi=%2").arg(args: protocol, args: d.udi()));
373 } else {
374 QUrl url;
375 url.setScheme(protocol);
376 url.setHost(host: d.udi().section(asep: QLatin1Char('/'), astart: -1));
377 url.setPath(QStringLiteral("/"));
378 return url;
379 }
380 }
381 return QVariant();
382 } else {
383 return QVariant();
384 }
385 case KFilePlacesModel::SetupNeededRole:
386 if (m_access) {
387 return !m_isAccessible;
388 } else {
389 return QVariant();
390 }
391
392 case KFilePlacesModel::TeardownAllowedRole:
393 if (m_access) {
394 return m_isTeardownAllowed;
395 } else {
396 return QVariant();
397 }
398
399 case KFilePlacesModel::EjectAllowedRole:
400 return m_isAccessible && m_isCdrom;
401
402 case KFilePlacesModel::TeardownOverlayRecommendedRole:
403 return m_isTeardownOverlayRecommended;
404
405 case KFilePlacesModel::DeviceAccessibilityRole:
406 return deviceAccessibility();
407
408 case KFilePlacesModel::FixedDeviceRole: {
409 if (m_drive != nullptr) {
410 return !m_drive->isRemovable();
411 }
412 return true;
413 }
414
415 case KFilePlacesModel::CapacityBarRecommendedRole:
416 return m_isAccessible && !m_isCdrom && !m_networkShare && !m_isReadOnly;
417
418 case KFilePlacesModel::IconNameRole:
419 return m_deviceIconName;
420
421 default:
422 return QVariant();
423 }
424 } else {
425 return QVariant();
426 }
427}
428
429KBookmark KFilePlacesItem::createBookmark(KBookmarkManager *manager, const QString &label, const QUrl &url, const QString &iconName, KFilePlacesItem *after)
430{
431 KBookmarkGroup root = manager->root();
432 if (root.isNull()) {
433 return KBookmark();
434 }
435 QString empty_icon = iconName;
436 if (url.toString() == QLatin1String("trash:/")) {
437 if (empty_icon.endsWith(s: QLatin1String("-full"))) {
438 empty_icon.chop(n: 5);
439 } else if (empty_icon.isEmpty()) {
440 empty_icon = QStringLiteral("user-trash");
441 }
442 }
443 KBookmark bookmark = root.addBookmark(text: label, url, icon: empty_icon);
444 bookmark.setMetaDataItem(QStringLiteral("ID"), value: generateNewId());
445
446 if (after) {
447 root.moveBookmark(bookmark, after: after->bookmark());
448 }
449
450 return bookmark;
451}
452
453KBookmark KFilePlacesItem::createSystemBookmark(KBookmarkManager *manager,
454 const char *untranslatedLabel,
455 const QUrl &url,
456 const QString &iconName,
457 const KBookmark &after)
458{
459 KBookmark bookmark = createBookmark(manager, label: QString::fromUtf8(utf8: untranslatedLabel), url, iconName);
460 if (!bookmark.isNull()) {
461 bookmark.setMetaDataItem(QStringLiteral("isSystemItem"), QStringLiteral("true"));
462 }
463 if (!after.isNull()) {
464 manager->root().moveBookmark(bookmark, after);
465 }
466 return bookmark;
467}
468
469KBookmark KFilePlacesItem::createDeviceBookmark(KBookmarkManager *manager, const Solid::Device &device)
470{
471 KBookmarkGroup root = manager->root();
472 if (root.isNull()) {
473 return KBookmark();
474 }
475 KBookmark bookmark = root.createNewSeparator();
476 bookmark.setMetaDataItem(QStringLiteral("UDI"), value: device.udi());
477 bookmark.setMetaDataItem(QStringLiteral("isSystemItem"), QStringLiteral("true"));
478
479 const auto storage = device.as<Solid::StorageVolume>();
480 if (storage) {
481 bookmark.setMetaDataItem(QStringLiteral("uuid"), value: storage->uuid());
482 }
483 return bookmark;
484}
485
486KBookmark KFilePlacesItem::createTagBookmark(KBookmarkManager *manager, const QString &tag)
487{
488 // TODO: Currently KFilePlacesItem::setBookmark() only decides by the "isSystemItem" property
489 // if the label text should be looked up for translation. So there is a small risk that
490 // labelTexts which match existing untranslated system labels accidentally get translated.
491 KBookmark bookmark = createBookmark(manager, label: tag, url: QUrl(QLatin1String("tags:/") + tag), QStringLiteral("tag"));
492 if (!bookmark.isNull()) {
493 bookmark.setMetaDataItem(QStringLiteral("tag"), value: tag);
494 bookmark.setMetaDataItem(QStringLiteral("isSystemItem"), QStringLiteral("true"));
495 }
496
497 return bookmark;
498}
499
500QString KFilePlacesItem::generateNewId()
501{
502 static int count = 0;
503
504 // return QString::number(count++);
505
506 return QString::number(QDateTime::currentSecsSinceEpoch()) + QLatin1Char('/') + QString::number(count++);
507
508 // return QString::number(QDateTime::currentSecsSinceEpoch())
509 // + '/' + QString::number(qrand());
510}
511
512bool KFilePlacesItem::updateDeviceInfo(const QString &udi)
513{
514 if (m_device.udi() == udi) {
515 return false;
516 }
517
518 if (m_access) {
519 m_access->disconnect(receiver: this);
520 }
521
522 if (m_opticalDrive) {
523 m_opticalDrive->disconnect(receiver: this);
524 }
525
526 m_device = Solid::Device(udi);
527 if (m_device.isValid()) {
528 m_access = m_device.as<Solid::StorageAccess>();
529 m_volume = m_device.as<Solid::StorageVolume>();
530 m_block = m_device.as<Solid::Block>();
531 m_disc = m_device.as<Solid::OpticalDisc>();
532 m_player = m_device.as<Solid::PortableMediaPlayer>();
533 m_networkShare = m_device.as<Solid::NetworkShare>();
534 m_deviceIconName = m_device.icon();
535 m_emblems = m_device.emblems();
536
537 if (auto *genericIface = m_device.as<Solid::GenericInterface>()) {
538 m_backingFile = genericIface->property(QStringLiteral("BackingFile")).toString();
539 }
540
541 m_drive = nullptr;
542 m_opticalDrive = nullptr;
543
544 Solid::Device parentDevice = m_device;
545 while (parentDevice.isValid() && !m_drive) {
546 m_drive = parentDevice.as<Solid::StorageDrive>();
547 m_opticalDrive = parentDevice.as<Solid::OpticalDrive>();
548 parentDevice = parentDevice.parent();
549 }
550
551 if (m_access) {
552 connect(sender: m_access.data(), signal: &Solid::StorageAccess::setupRequested, context: this, slot: [this] {
553 m_isSetupInProgress = true;
554 Q_EMIT itemChanged(id: id(), roles: {KFilePlacesModel::DeviceAccessibilityRole});
555 });
556 connect(sender: m_access.data(), signal: &Solid::StorageAccess::setupDone, context: this, slot: [this] {
557 m_isSetupInProgress = false;
558 Q_EMIT itemChanged(id: id(), roles: {KFilePlacesModel::DeviceAccessibilityRole});
559 });
560
561 connect(sender: m_access.data(), signal: &Solid::StorageAccess::teardownRequested, context: this, slot: [this] {
562 m_isTeardownInProgress = true;
563 Q_EMIT itemChanged(id: id(), roles: {KFilePlacesModel::DeviceAccessibilityRole});
564 });
565 connect(sender: m_access.data(), signal: &Solid::StorageAccess::teardownDone, context: this, slot: [this] {
566 m_isTeardownInProgress = false;
567 Q_EMIT itemChanged(id: id(), roles: {KFilePlacesModel::DeviceAccessibilityRole});
568 });
569
570 connect(sender: m_access.data(), signal: &Solid::StorageAccess::accessibilityChanged, context: this, slot: &KFilePlacesItem::onAccessibilityChanged);
571 onAccessibilityChanged(m_access->isAccessible());
572 }
573
574 if (m_opticalDrive) {
575 connect(sender: m_opticalDrive.data(), signal: &Solid::OpticalDrive::ejectRequested, context: this, slot: [this] {
576 m_isEjectInProgress = true;
577 Q_EMIT itemChanged(id: id(), roles: {KFilePlacesModel::DeviceAccessibilityRole});
578 });
579 connect(sender: m_opticalDrive.data(), signal: &Solid::OpticalDrive::ejectDone, context: this, slot: [this] {
580 m_isEjectInProgress = false;
581 Q_EMIT itemChanged(id: id(), roles: {KFilePlacesModel::DeviceAccessibilityRole});
582 });
583 }
584 } else {
585 m_access = nullptr;
586 m_volume = nullptr;
587 m_disc = nullptr;
588 m_player = nullptr;
589 m_drive = nullptr;
590 m_opticalDrive = nullptr;
591 m_networkShare = nullptr;
592 m_deviceIconName.clear();
593 m_emblems.clear();
594 }
595
596 return true;
597}
598
599void KFilePlacesItem::onAccessibilityChanged(bool isAccessible)
600{
601 m_isAccessible = isAccessible;
602 m_isCdrom = m_device.is<Solid::OpticalDrive>() || m_opticalDrive || (m_volume && m_volume->fsType() == QLatin1String("iso9660"));
603 m_emblems = m_device.emblems();
604
605 if (auto generic = m_device.as<Solid::GenericInterface>()) {
606 // TODO add Solid API for this.
607 m_isReadOnly = generic->property(QStringLiteral("ReadOnly")).toBool();
608 }
609
610 m_isTeardownAllowed = isAccessible;
611 if (m_isTeardownAllowed) {
612 if (m_access->filePath() == QDir::rootPath()) {
613 m_isTeardownAllowed = false;
614 } else {
615 const auto homeDevice = Solid::Device::storageAccessFromPath(path: QDir::homePath());
616 const auto *homeAccess = homeDevice.as<Solid::StorageAccess>();
617 if (homeAccess && m_access->filePath() == homeAccess->filePath()) {
618 m_isTeardownAllowed = false;
619 }
620 }
621 }
622
623 m_isTeardownOverlayRecommended = m_isTeardownAllowed && !m_networkShare;
624 if (m_isTeardownOverlayRecommended) {
625 if (m_drive && !m_drive->isRemovable()) {
626 m_isTeardownOverlayRecommended = false;
627 }
628 }
629
630 Q_EMIT itemChanged(id: id());
631}
632
633QString KFilePlacesItem::iconNameForBookmark(const KBookmark &bookmark) const
634{
635 if (!m_folderIsEmpty && isTrash(bk: bookmark)) {
636 return bookmark.icon() + QLatin1String("-full");
637 } else {
638 return bookmark.icon();
639 }
640}
641
642#include "moc_kfileplacesitem_p.cpp"
643

source code of kio/src/filewidgets/kfileplacesitem.cpp