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

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