/* This file is part of the KDE project SPDX-FileCopyrightText: 2007 Kevin Ottens SPDX-License-Identifier: LGPL-2.0-only */ #include "kfileplacesitem_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static bool isTrash(const KBookmark &bk) { return bk.url().toString() == QLatin1String("trash:/"); } KFilePlacesItem::KFilePlacesItem(KBookmarkManager *manager, const QString &address, const QString &udi, KFilePlacesModel *parent) : QObject(static_cast(parent)) , m_manager(manager) , m_folderIsEmpty(true) , m_isCdrom(false) , m_isAccessible(false) , m_isTeardownAllowed(false) , m_isTeardownOverlayRecommended(false) , m_isTeardownInProgress(false) , m_isSetupInProgress(false) , m_isEjectInProgress(false) , m_isReadOnly(false) { updateDeviceInfo(udi); setBookmark(m_manager->findByAddress(address)); if (udi.isEmpty() && m_bookmark.metaDataItem(QStringLiteral("ID")).isEmpty()) { m_bookmark.setMetaDataItem(QStringLiteral("ID"), generateNewId()); } else if (udi.isEmpty()) { if (isTrash(m_bookmark)) { KConfig cfg(QStringLiteral("trashrc"), KConfig::SimpleConfig); const KConfigGroup group = cfg.group(QStringLiteral("Status")); m_folderIsEmpty = group.readEntry("Empty", true); } } // Hide SSHFS network device mounted by kdeconnect, since we already have the kdeconnect:// place. if (isDevice() && m_access && device().vendor() == QLatin1String("fuse.sshfs")) { const QString storageFilePath = m_access->filePath(); // Not using findByPath() as it resolves symlinks, potentially blocking, // but here we know we query for an existing actual mount point. const auto mountPoints = KMountPoint::currentMountPoints(); auto it = std::find_if(mountPoints.cbegin(), mountPoints.cend(), [&storageFilePath](const KMountPoint::Ptr &mountPoint) { return mountPoint->mountPoint() == storageFilePath; }); if (it != mountPoints.cend()) { if ((*it)->mountedFrom().startsWith(QLatin1String("kdeconnect@"))) { // Hide only if the user never set the "Hide" checkbox on the device. if (m_bookmark.metaDataItem(QStringLiteral("IsHidden")).isEmpty()) { setHidden(true); } } } } } KFilePlacesItem::~KFilePlacesItem() { } QString KFilePlacesItem::id() const { if (isDevice()) { return bookmark().metaDataItem(QStringLiteral("UDI")); } else { return bookmark().metaDataItem(QStringLiteral("ID")); } } bool KFilePlacesItem::hasSupportedScheme(const QStringList &schemes) const { if (schemes.isEmpty()) { return true; } // StorageAccess is always local, doesn't need to be accessible to know this if (m_access && schemes.contains(QLatin1String("file"))) { return true; } if (m_networkShare && schemes.contains(m_networkShare->url().scheme())) { return true; } if (m_player) { const QStringList protocols = m_player->supportedProtocols(); for (const QString &protocol : protocols) { if (schemes.contains(protocol)) { return true; } } } return false; } bool KFilePlacesItem::isDevice() const { return !bookmark().metaDataItem(QStringLiteral("UDI")).isEmpty(); } KFilePlacesModel::DeviceAccessibility KFilePlacesItem::deviceAccessibility() const { if (m_isTeardownInProgress || m_isEjectInProgress) { return KFilePlacesModel::TeardownInProgress; } else if (m_isSetupInProgress) { return KFilePlacesModel::SetupInProgress; } else if (m_isAccessible) { return KFilePlacesModel::Accessible; } else { return KFilePlacesModel::SetupNeeded; } } bool KFilePlacesItem::isTeardownAllowed() const { return m_isTeardownAllowed; } bool KFilePlacesItem::isTeardownOverlayRecommended() const { return m_isTeardownOverlayRecommended; } bool KFilePlacesItem::isEjectAllowed() const { return m_isCdrom; } KBookmark KFilePlacesItem::bookmark() const { return m_bookmark; } void KFilePlacesItem::setBookmark(const KBookmark &bookmark) { m_bookmark = bookmark; if (m_device.isValid()) { m_bookmark.setMetaDataItem(QStringLiteral("UDI"), m_device.udi()); if (m_volume && !m_volume->uuid().isEmpty()) { m_bookmark.setMetaDataItem(QStringLiteral("uuid"), m_volume->uuid()); } } if (bookmark.metaDataItem(QStringLiteral("isSystemItem")) == QLatin1String("true")) { // This context must stay as it is - the translated system bookmark names // are created with 'KFile System Bookmarks' as their context, so this // ensures the right string is picked from the catalog. // (coles, 13th May 2009) m_text = i18nc("KFile System Bookmarks", bookmark.text().toUtf8().data()); } else { m_text = bookmark.text(); } if (!isDevice()) { const QString protocol = bookmark.url().scheme(); if (protocol == QLatin1String("timeline") || protocol == QLatin1String("recentlyused")) { m_groupType = KFilePlacesModel::RecentlySavedType; } else if (protocol.contains(QLatin1String("search"))) { m_groupType = KFilePlacesModel::SearchForType; } else if (protocol == QLatin1String("bluetooth") || protocol == QLatin1String("obexftp") || protocol == QLatin1String("kdeconnect")) { m_groupType = KFilePlacesModel::DevicesType; } else if (protocol == QLatin1String("tags")) { m_groupType = KFilePlacesModel::TagsType; } else if (protocol == QLatin1String("remote") || KProtocolInfo::protocolClass(protocol) != QLatin1String(":local")) { m_groupType = KFilePlacesModel::RemoteType; } else { m_groupType = KFilePlacesModel::PlacesType; } } else { if (m_drive && (m_drive->isHotpluggable() || m_drive->isRemovable())) { m_groupType = KFilePlacesModel::RemovableDevicesType; } else if (m_networkShare) { m_groupType = KFilePlacesModel::RemoteType; } else { m_groupType = KFilePlacesModel::DevicesType; } } switch (m_groupType) { case KFilePlacesModel::PlacesType: m_groupName = i18nc("@item", "Places"); break; case KFilePlacesModel::RemoteType: m_groupName = i18nc("@item", "Remote"); break; case KFilePlacesModel::RecentlySavedType: m_groupName = i18nc("@item The place group section name for recent dynamic lists", "Recent"); break; case KFilePlacesModel::SearchForType: m_groupName = i18nc("@item", "Search For"); break; case KFilePlacesModel::DevicesType: m_groupName = i18nc("@item", "Devices"); break; case KFilePlacesModel::RemovableDevicesType: m_groupName = i18nc("@item", "Removable Devices"); break; case KFilePlacesModel::TagsType: m_groupName = i18nc("@item", "Tags"); break; case KFilePlacesModel::UnknownType: Q_UNREACHABLE(); break; } } Solid::Device KFilePlacesItem::device() const { return m_device; } QVariant KFilePlacesItem::data(int role) const { if (role == KFilePlacesModel::GroupRole) { return QVariant(m_groupName); } else if (role != KFilePlacesModel::HiddenRole && role != Qt::BackgroundRole && isDevice()) { return deviceData(role); } else { return bookmarkData(role); } } KFilePlacesModel::GroupType KFilePlacesItem::groupType() const { return m_groupType; } bool KFilePlacesItem::isHidden() const { return m_bookmark.metaDataItem(QStringLiteral("IsHidden")) == QLatin1String("true"); } void KFilePlacesItem::setHidden(bool hide) { if (m_bookmark.isNull() || isHidden() == hide) { return; } m_bookmark.setMetaDataItem(QStringLiteral("IsHidden"), hide ? QStringLiteral("true") : QStringLiteral("false")); } QVariant KFilePlacesItem::bookmarkData(int role) const { KBookmark b = bookmark(); if (b.isNull()) { return QVariant(); } switch (role) { case Qt::DisplayRole: return m_text; case Qt::DecorationRole: return QIcon::fromTheme(iconNameForBookmark(b)); case Qt::ToolTipRole: { const KFilePlacesModel::GroupType type = groupType(); // Don't display technical gibberish in the URL, particularly search. if (type != KFilePlacesModel::RecentlySavedType && type != KFilePlacesModel::SearchForType && type != KFilePlacesModel::TagsType) { return b.url().toDisplayString(QUrl::PreferLocalFile); } return QString(); } case Qt::BackgroundRole: if (isHidden()) { return QColor(Qt::lightGray); } else { return QVariant(); } case KFilePlacesModel::UrlRole: return b.url(); case KFilePlacesModel::SetupNeededRole: return false; case KFilePlacesModel::HiddenRole: return isHidden(); case KFilePlacesModel::IconNameRole: return iconNameForBookmark(b); default: return QVariant(); } } QVariant KFilePlacesItem::deviceData(int role) const { Solid::Device d = device(); if (d.isValid()) { switch (role) { case Qt::DisplayRole: if (m_deviceDisplayName.isEmpty()) { m_deviceDisplayName = d.displayName(); } return m_deviceDisplayName; case Qt::DecorationRole: // qDebug() << "adding emblems" << m_emblems << "to device icon" << m_deviceIconName; return KIconUtils::addOverlays(m_deviceIconName, m_emblems); case Qt::ToolTipRole: { if (m_access && m_isAccessible) { // For loop devices, show backing file path rather than /dev/loop123. QString mountedFrom = m_backingFile; if (mountedFrom.isEmpty() && m_block) { mountedFrom = m_block->device(); } if (!mountedFrom.isEmpty()) { return i18nc("@info:tooltip path (mounted from)", "%1 (from %2)", m_access->filePath(), mountedFrom); } } else if (!m_backingFile.isEmpty()) { return m_backingFile; } else if (m_block) { return m_block->device(); } return QString(); } case KFilePlacesModel::UrlRole: if (m_access) { const QString path = m_access->filePath(); return path.isEmpty() ? QUrl() : QUrl::fromLocalFile(path); } else if (m_disc && (m_disc->availableContent() & Solid::OpticalDisc::Audio) != 0) { Solid::Block *block = d.as(); if (block) { QString device = block->device(); return QUrl(QStringLiteral("audiocd:/?device=%1").arg(device)); } // We failed to get the block device. Assume audiocd:/ can // figure it out, but cannot handle multiple disc drives. // See https://bugs.kde.org/show_bug.cgi?id=314544#c40 return QUrl(QStringLiteral("audiocd:/")); } else if (m_player) { const QStringList protocols = m_player->supportedProtocols(); if (!protocols.isEmpty()) { const QString protocol = protocols.first(); if (protocol == QLatin1String("mtp")) { return QUrl(QStringLiteral("%1:udi=%2").arg(protocol, d.udi())); } else { QUrl url; url.setScheme(protocol); url.setHost(d.udi().section(QLatin1Char('/'), -1)); url.setPath(QStringLiteral("/")); return url; } } return QVariant(); } else { return QVariant(); } case KFilePlacesModel::SetupNeededRole: if (m_access) { return !m_isAccessible; } else { return QVariant(); } case KFilePlacesModel::TeardownAllowedRole: if (m_access) { return m_isTeardownAllowed; } else { return QVariant(); } case KFilePlacesModel::EjectAllowedRole: return m_isAccessible && m_isCdrom; case KFilePlacesModel::TeardownOverlayRecommendedRole: return m_isTeardownOverlayRecommended; case KFilePlacesModel::DeviceAccessibilityRole: return deviceAccessibility(); case KFilePlacesModel::FixedDeviceRole: { if (m_drive != nullptr) { return !m_drive->isHotpluggable() && !m_drive->isRemovable(); } return true; } case KFilePlacesModel::CapacityBarRecommendedRole: return m_isAccessible && !m_isCdrom && !m_networkShare && !m_isReadOnly; case KFilePlacesModel::IconNameRole: return m_deviceIconName; default: return QVariant(); } } else { return QVariant(); } } KBookmark KFilePlacesItem::createBookmark(KBookmarkManager *manager, const QString &label, const QUrl &url, const QString &iconName, KFilePlacesItem *after) { KBookmarkGroup root = manager->root(); if (root.isNull()) { return KBookmark(); } QString empty_icon = iconName; if (url.toString() == QLatin1String("trash:/")) { if (empty_icon.endsWith(QLatin1String("-full"))) { empty_icon.chop(5); } else if (empty_icon.isEmpty()) { empty_icon = QStringLiteral("user-trash"); } } KBookmark bookmark = root.addBookmark(label, url, empty_icon); bookmark.setMetaDataItem(QStringLiteral("ID"), generateNewId()); if (after) { root.moveBookmark(bookmark, after->bookmark()); } return bookmark; } KBookmark KFilePlacesItem::createSystemBookmark(KBookmarkManager *manager, const char *untranslatedLabel, const QUrl &url, const QString &iconName, const KBookmark &after) { KBookmark bookmark = createBookmark(manager, QString::fromUtf8(untranslatedLabel), url, iconName); if (!bookmark.isNull()) { bookmark.setMetaDataItem(QStringLiteral("isSystemItem"), QStringLiteral("true")); } if (!after.isNull()) { manager->root().moveBookmark(bookmark, after); } return bookmark; } KBookmark KFilePlacesItem::createDeviceBookmark(KBookmarkManager *manager, const Solid::Device &device) { KBookmarkGroup root = manager->root(); if (root.isNull()) { return KBookmark(); } KBookmark bookmark = root.createNewSeparator(); bookmark.setMetaDataItem(QStringLiteral("UDI"), device.udi()); bookmark.setMetaDataItem(QStringLiteral("isSystemItem"), QStringLiteral("true")); const auto storage = device.as(); if (storage) { bookmark.setMetaDataItem(QStringLiteral("uuid"), storage->uuid()); } return bookmark; } KBookmark KFilePlacesItem::createTagBookmark(KBookmarkManager *manager, const QString &tag) { // TODO: Currently KFilePlacesItem::setBookmark() only decides by the "isSystemItem" property // if the label text should be looked up for translation. So there is a small risk that // labelTexts which match existing untranslated system labels accidentally get translated. KBookmark bookmark = createBookmark(manager, tag, QUrl(QLatin1String("tags:/") + tag), QStringLiteral("tag")); if (!bookmark.isNull()) { bookmark.setMetaDataItem(QStringLiteral("tag"), tag); bookmark.setMetaDataItem(QStringLiteral("isSystemItem"), QStringLiteral("true")); } return bookmark; } QString KFilePlacesItem::generateNewId() { static int count = 0; // return QString::number(count++); return QString::number(QDateTime::currentSecsSinceEpoch()) + QLatin1Char('/') + QString::number(count++); // return QString::number(QDateTime::currentSecsSinceEpoch()) // + '/' + QString::number(qrand()); } bool KFilePlacesItem::updateDeviceInfo(const QString &udi) { if (m_device.udi() == udi) { return false; } if (m_access) { m_access->disconnect(this); } if (m_opticalDrive) { m_opticalDrive->disconnect(this); } m_device = Solid::Device(udi); if (m_device.isValid()) { m_access = m_device.as(); m_volume = m_device.as(); m_block = m_device.as(); m_disc = m_device.as(); m_player = m_device.as(); m_networkShare = m_device.as(); m_deviceIconName = m_device.icon(); m_emblems = m_device.emblems(); if (auto *genericIface = m_device.as()) { m_backingFile = genericIface->property(QStringLiteral("BackingFile")).toString(); } m_drive = nullptr; m_opticalDrive = nullptr; Solid::Device parentDevice = m_device; while (parentDevice.isValid() && !m_drive) { m_drive = parentDevice.as(); m_opticalDrive = parentDevice.as(); parentDevice = parentDevice.parent(); } if (m_access) { connect(m_access.data(), &Solid::StorageAccess::setupRequested, this, [this] { m_isSetupInProgress = true; Q_EMIT itemChanged(id(), {KFilePlacesModel::DeviceAccessibilityRole}); }); connect(m_access.data(), &Solid::StorageAccess::setupDone, this, [this] { m_isSetupInProgress = false; Q_EMIT itemChanged(id(), {KFilePlacesModel::DeviceAccessibilityRole}); }); connect(m_access.data(), &Solid::StorageAccess::teardownRequested, this, [this] { m_isTeardownInProgress = true; Q_EMIT itemChanged(id(), {KFilePlacesModel::DeviceAccessibilityRole}); }); connect(m_access.data(), &Solid::StorageAccess::teardownDone, this, [this] { m_isTeardownInProgress = false; Q_EMIT itemChanged(id(), {KFilePlacesModel::DeviceAccessibilityRole}); }); connect(m_access.data(), &Solid::StorageAccess::accessibilityChanged, this, &KFilePlacesItem::onAccessibilityChanged); onAccessibilityChanged(m_access->isAccessible()); } if (m_opticalDrive) { connect(m_opticalDrive.data(), &Solid::OpticalDrive::ejectRequested, this, [this] { m_isEjectInProgress = true; Q_EMIT itemChanged(id(), {KFilePlacesModel::DeviceAccessibilityRole}); }); connect(m_opticalDrive.data(), &Solid::OpticalDrive::ejectDone, this, [this] { m_isEjectInProgress = false; Q_EMIT itemChanged(id(), {KFilePlacesModel::DeviceAccessibilityRole}); }); } } else { m_access = nullptr; m_volume = nullptr; m_disc = nullptr; m_player = nullptr; m_drive = nullptr; m_opticalDrive = nullptr; m_networkShare = nullptr; m_deviceIconName.clear(); m_emblems.clear(); } return true; } void KFilePlacesItem::onAccessibilityChanged(bool isAccessible) { m_isAccessible = isAccessible; m_isCdrom = m_device.is() || m_opticalDrive || (m_volume && m_volume->fsType() == QLatin1String("iso9660")); m_emblems = m_device.emblems(); if (auto generic = m_device.as()) { // TODO add Solid API for this. m_isReadOnly = generic->property(QStringLiteral("ReadOnly")).toBool(); } m_isTeardownAllowed = isAccessible; if (m_isTeardownAllowed) { if (m_access->filePath() == QDir::rootPath()) { m_isTeardownAllowed = false; } else { const auto homeDevice = Solid::Device::storageAccessFromPath(QDir::homePath()); const auto *homeAccess = homeDevice.as(); if (homeAccess && m_access->filePath() == homeAccess->filePath()) { m_isTeardownAllowed = false; } } } m_isTeardownOverlayRecommended = m_isTeardownAllowed && !m_networkShare; if (m_isTeardownOverlayRecommended) { if (m_drive && !m_drive->isHotpluggable() && !m_drive->isRemovable()) { m_isTeardownOverlayRecommended = false; } } Q_EMIT itemChanged(id()); } QString KFilePlacesItem::iconNameForBookmark(const KBookmark &bookmark) const { if (!m_folderIsEmpty && isTrash(bookmark)) { return bookmark.icon() + QLatin1String("-full"); } else { return bookmark.icon(); } } #include "moc_kfileplacesitem_p.cpp"