File indexing completed on 2024-05-12 05:37:01
0001 /* 0002 SPDX-FileCopyrightText: 2016 Kai Uwe Broulik <kde@privat.broulik.de> 0003 0004 SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0005 */ 0006 0007 #include "iconapplet.h" 0008 0009 #include <QAction> 0010 #include <QApplication> 0011 #include <QDir> 0012 #include <QDropEvent> 0013 #include <QFileInfo> 0014 #include <QIcon> 0015 #include <QJsonArray> 0016 #include <QMenu> 0017 #include <QMimeData> 0018 #include <QMimeDatabase> 0019 #include <QProcess> 0020 0021 #include <KAuthorized> 0022 #include <KDesktopFile> 0023 #include <KFileItemActions> 0024 #include <KFileItemListProperties> 0025 #include <KFileUtils> 0026 #include <KJobWidgets> 0027 #include <KLocalizedString> 0028 #include <KNotificationJobUiDelegate> 0029 #include <KProtocolManager> 0030 #include <KService> 0031 #include <KServiceAction> 0032 0033 #include <KIO/ApplicationLauncherJob> 0034 #include <KIO/DropJob> 0035 #include <KIO/FavIconRequestJob> 0036 #include <KIO/OpenFileManagerWindowJob> 0037 #include <KIO/OpenUrlJob> 0038 #include <KIO/StatJob> 0039 0040 #include <abstracttasksmodel.h> 0041 #include <startuptasksmodel.h> 0042 0043 using namespace Qt::StringLiterals; 0044 0045 IconApplet::IconApplet(QObject *parent, const KPluginMetaData &data, const QVariantList &args) 0046 : Plasma::Applet(parent, data, args) 0047 { 0048 } 0049 0050 IconApplet::~IconApplet() 0051 { 0052 // in a handler connected to IconApplet::appletDeleted m_localPath will be empty?! 0053 if (destroyed()) { 0054 QFile::remove(m_localPath); 0055 } 0056 } 0057 0058 void IconApplet::init() 0059 { 0060 populate(); 0061 } 0062 0063 void IconApplet::configChanged() 0064 { 0065 populate(); 0066 } 0067 0068 void IconApplet::populate() 0069 { 0070 const QString entryName = u"url"_s; 0071 m_url = config().readEntry(entryName, QUrl()); 0072 0073 if (!m_url.isValid()) { 0074 // the old applet that used a QML plugin and stored its url 0075 // in plasmoid.configuration.url had its entries stored in [Configuration][General] 0076 // so we look here as well to provide an upgrade path 0077 m_url = config().group(u"General"_s).readEntry(entryName, QUrl()); 0078 } 0079 0080 // our backing desktop file already exists? just read all the things from it 0081 const QString path = localPath(); 0082 if (QFileInfo::exists(path)) { 0083 populateFromDesktopFile(path); 0084 return; 0085 } 0086 0087 if (!m_url.isValid()) { 0088 // invalid url, use dummy data 0089 populateFromDesktopFile(QString()); 0090 return; 0091 } 0092 0093 const QString plasmaIconsFolderPath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/plasma_icons"); 0094 if (!QDir().mkpath(plasmaIconsFolderPath)) { 0095 setLaunchErrorMessage(i18n("Failed to create icon widgets folder '%1'", plasmaIconsFolderPath)); 0096 return; 0097 } 0098 0099 setBusy(true); // unset in populateFromDesktopFile where we'll end up in if all goes well 0100 0101 auto *statJob = KIO::stat(m_url, KIO::HideProgressInfo); 0102 connect(statJob, &KIO::StatJob::finished, this, [=, this] { 0103 QString desiredDesktopFileName = m_url.fileName(); 0104 0105 // in doubt, just encode the entire URL, e.g. https://www.kde.org/ has no filename 0106 if (desiredDesktopFileName.isEmpty()) { 0107 desiredDesktopFileName = KIO::encodeFileName(m_url.toDisplayString()); 0108 } 0109 0110 // We always want it to be a .desktop file (e.g. also for the "Type=Link" at the end) 0111 if (!desiredDesktopFileName.endsWith(QLatin1String(".desktop"))) { 0112 desiredDesktopFileName.append(QLatin1String(".desktop")); 0113 } 0114 0115 QString backingDesktopFile = plasmaIconsFolderPath + QLatin1Char('/'); 0116 // KFileUtils::suggestName always appends a suffix, i.e. it expects that we already know the file already exists 0117 if (QFileInfo::exists(backingDesktopFile + desiredDesktopFileName)) { 0118 desiredDesktopFileName = KFileUtils::suggestName(QUrl::fromLocalFile(plasmaIconsFolderPath), desiredDesktopFileName); 0119 } 0120 backingDesktopFile.append(desiredDesktopFileName); 0121 0122 QString name; // ends up as "Name" in the .desktop file for "Link" files below 0123 0124 const QUrl url = statJob->mostLocalUrl(); 0125 if (url.isLocalFile()) { 0126 const QString localUrlString = url.toLocalFile(); 0127 0128 // if desktop file just copy it over 0129 if (KDesktopFile::isDesktopFile(localUrlString)) { 0130 // if this restriction is set, KIO won't allow running desktop files from outside 0131 // registered services, applications, and so on, in this case we'll use the original 0132 // .desktop file and lose the ability to customize it 0133 if (!KAuthorized::authorize(KAuthorized::RUN_DESKTOP_FILES)) { 0134 populateFromDesktopFile(localUrlString); 0135 // we don't call setLocalPath here as we don't want to store localPath to be a system-location 0136 // so that the fact that we cannot edit is re-evaluated every time 0137 return; 0138 } 0139 0140 if (!QFile::copy(localUrlString, backingDesktopFile)) { 0141 setLaunchErrorMessage(i18n("Failed to copy icon widget desktop file from '%1' to '%2'", localUrlString, backingDesktopFile)); 0142 setBusy(false); 0143 return; 0144 } 0145 0146 // set executable flag on the desktop file so KIO doesn't complain about executing it 0147 QFile file(backingDesktopFile); 0148 file.setPermissions(file.permissions() | QFile::ExeOwner); 0149 0150 populateFromDesktopFile(backingDesktopFile); 0151 setLocalPath(backingDesktopFile); 0152 0153 return; 0154 } 0155 } 0156 0157 // in all other cases just make it a link 0158 0159 QString iconName; 0160 QString genericName; 0161 0162 if (!statJob->error()) { 0163 KFileItem item(statJob->statResult(), url); 0164 0165 if (name.isEmpty()) { 0166 name = item.text(); 0167 } 0168 0169 if (item.mimetype() != QLatin1String("application/octet-stream")) { 0170 iconName = item.iconName(); 0171 genericName = item.mimeComment(); 0172 } 0173 } 0174 0175 // KFileItem might return "." as text for e.g. root folders 0176 if (name == QLatin1Char('.')) { 0177 name.clear(); 0178 } 0179 0180 if (name.isEmpty()) { 0181 name = url.fileName(); 0182 } 0183 0184 if (name.isEmpty()) { 0185 // TODO would be cool to just show the parent folder name instead of the full path 0186 name = url.path(); 0187 } 0188 0189 // For websites the filename e.g. "index.php" is usually not what you want 0190 // also "/" isn't very descript when it's not our local "root" folder 0191 if (name.isEmpty() || url.scheme().startsWith(QLatin1String("http")) || (!url.isLocalFile() && name == QLatin1String("/"))) { 0192 name = url.host(); 0193 } 0194 0195 if (iconName.isEmpty()) { 0196 // In doubt ask KIO::iconNameForUrl, KFileItem can't cope with http:// URLs for instance 0197 iconName = KIO::iconNameForUrl(url); 0198 } 0199 0200 bool downloadFavIcon = false; 0201 0202 if (url.scheme().startsWith(QLatin1String("http"))) { 0203 const QString favIcon = KIO::favIconForUrl(url); 0204 0205 if (!favIcon.isEmpty()) { 0206 iconName = favIcon; 0207 } else { 0208 downloadFavIcon = true; 0209 } 0210 } 0211 0212 KDesktopFile linkDesktopFile(backingDesktopFile); 0213 auto desktopGroup = linkDesktopFile.desktopGroup(); 0214 0215 desktopGroup.writeEntry(QStringLiteral("Name"), name); 0216 desktopGroup.writeEntry(QStringLiteral("Type"), QStringLiteral("Link")); 0217 desktopGroup.writeEntry(QStringLiteral("URL"), url); 0218 desktopGroup.writeEntry(QStringLiteral("Icon"), iconName); 0219 if (!genericName.isEmpty()) { 0220 desktopGroup.writeEntry(QStringLiteral("GenericName"), genericName); 0221 } 0222 0223 linkDesktopFile.sync(); 0224 0225 populateFromDesktopFile(backingDesktopFile); 0226 setLocalPath(backingDesktopFile); 0227 0228 if (downloadFavIcon) { 0229 KIO::FavIconRequestJob *job = new KIO::FavIconRequestJob(m_url); 0230 connect(job, &KIO::FavIconRequestJob::result, this, [job, backingDesktopFile, this](KJob *) { 0231 if (!job->error()) { 0232 KDesktopFile(backingDesktopFile).desktopGroup().writeEntry(QStringLiteral("Icon"), job->iconFile()); 0233 0234 m_iconName = job->iconFile(); 0235 Q_EMIT iconNameChanged(m_iconName); 0236 } 0237 }); 0238 } 0239 }); 0240 } 0241 0242 void IconApplet::populateFromDesktopFile(const QString &path) 0243 { 0244 // path empty? just set icon to "unknown" and call it a day 0245 if (path.isEmpty()) { 0246 setIconName({}); 0247 return; 0248 } 0249 0250 KDesktopFile desktopFile(path); 0251 0252 const QString &name = desktopFile.readName(); 0253 if (m_name != name) { 0254 m_name = name; 0255 Q_EMIT nameChanged(name); 0256 } 0257 0258 const QString &genericName = desktopFile.readGenericName(); 0259 if (m_genericName != genericName) { 0260 m_genericName = genericName; 0261 Q_EMIT genericNameChanged(genericName); 0262 } 0263 0264 setIconName(desktopFile.readIcon()); 0265 0266 delete m_openContainingFolderAction; 0267 m_openContainingFolderAction = nullptr; 0268 m_openWithActions.clear(); 0269 m_jumpListActions.clear(); 0270 0271 setLocalPath(path); 0272 0273 setBusy(false); 0274 } 0275 0276 QUrl IconApplet::url() const 0277 { 0278 return m_url; 0279 } 0280 0281 void IconApplet::setUrl(const QUrl &url) 0282 { 0283 if (m_url != url) { 0284 m_url = url; 0285 Q_EMIT urlChanged(url); 0286 0287 config().writeEntry(QStringLiteral("url"), url); 0288 0289 populate(); 0290 } 0291 } 0292 0293 void IconApplet::setIconName(const QString &iconName) 0294 { 0295 const QString newIconName = (!iconName.isEmpty() ? iconName : QStringLiteral("unknown")); 0296 if (m_iconName != newIconName) { 0297 m_iconName = newIconName; 0298 Q_EMIT iconNameChanged(newIconName); 0299 } 0300 } 0301 0302 QString IconApplet::name() const 0303 { 0304 return m_name; 0305 } 0306 0307 QString IconApplet::iconName() const 0308 { 0309 return m_iconName; 0310 } 0311 0312 QString IconApplet::genericName() const 0313 { 0314 return m_genericName; 0315 } 0316 0317 bool IconApplet::isValid() const 0318 { 0319 return !m_localPath.isEmpty(); 0320 } 0321 0322 QList<QAction *> IconApplet::extraActions() 0323 { 0324 QList<QAction *> actions = Applet::contextualActions(); 0325 if (m_localPath.isEmpty()) { 0326 return actions; 0327 } 0328 0329 KDesktopFile desktopFile(m_localPath); 0330 0331 if (desktopFile.hasLinkType()) { 0332 const QUrl linkUrl = QUrl(desktopFile.readUrl()); 0333 0334 if (linkUrl.isValid() && !linkUrl.scheme().isEmpty()) { 0335 if (m_openWithActions.isEmpty()) { 0336 if (!m_fileItemActions) { 0337 m_fileItemActions = new KFileItemActions(this); 0338 } 0339 KFileItemListProperties itemProperties(KFileItemList({KFileItem(linkUrl)})); 0340 m_fileItemActions->setItemListProperties(itemProperties); 0341 0342 if (!m_openWithMenu) { 0343 m_openWithMenu.reset(new QMenu()); 0344 } 0345 m_openWithMenu->clear(); 0346 m_fileItemActions->insertOpenWithActionsTo(nullptr, m_openWithMenu.get(), QStringList()); 0347 0348 m_openWithActions = m_openWithMenu->actions(); 0349 } 0350 0351 if (!m_openContainingFolderAction) { 0352 if (KProtocolManager::supportsListing(linkUrl)) { 0353 m_openContainingFolderAction = new QAction(QIcon::fromTheme(QStringLiteral("document-open-folder")), i18n("Open Containing Folder"), this); 0354 connect(m_openContainingFolderAction, &QAction::triggered, this, [linkUrl] { 0355 KIO::highlightInFileManager({linkUrl}); 0356 }); 0357 } 0358 } 0359 0360 actions << m_openWithActions; 0361 } 0362 } else { 0363 if (m_jumpListActions.isEmpty()) { 0364 const KService service(m_localPath); 0365 0366 const auto jumpListActions = service.actions(); 0367 for (const KServiceAction &serviceAction : jumpListActions) { 0368 if (serviceAction.noDisplay()) { 0369 continue; 0370 } 0371 0372 QAction *action = new QAction(QIcon::fromTheme(serviceAction.icon()), serviceAction.text(), this); 0373 if (serviceAction.isSeparator()) { 0374 action->setSeparator(true); 0375 } 0376 0377 connect(action, &QAction::triggered, this, [serviceAction]() { 0378 auto *job = new KIO::ApplicationLauncherJob(serviceAction); 0379 auto *delegate = new KNotificationJobUiDelegate(KJobUiDelegate::AutoErrorHandlingEnabled); 0380 job->setUiDelegate(delegate); 0381 job->start(); 0382 }); 0383 0384 m_jumpListActions << action; 0385 } 0386 } 0387 actions << m_jumpListActions; 0388 } 0389 0390 if (!actions.isEmpty()) { 0391 if (!m_separatorAction) { 0392 m_separatorAction = new QAction(this); 0393 m_separatorAction->setSeparator(true); 0394 } 0395 actions << m_separatorAction; 0396 } 0397 0398 if (m_openContainingFolderAction) { 0399 actions << m_openContainingFolderAction; 0400 } 0401 0402 return actions; 0403 } 0404 0405 void IconApplet::run() 0406 { 0407 if (!m_startupTasksModel) { 0408 m_startupTasksModel = new TaskManager::StartupTasksModel(this); 0409 0410 auto handleRow = [this](bool busy, const QModelIndex &parent, int first, int last) { 0411 Q_UNUSED(parent); 0412 for (int i = first; i <= last; ++i) { 0413 const QModelIndex idx = m_startupTasksModel->index(i, 0); 0414 if (idx.data(TaskManager::AbstractTasksModel::LauncherUrlWithoutIcon).toUrl() == QUrl::fromLocalFile(m_localPath)) { 0415 setBusy(busy); 0416 break; 0417 } 0418 } 0419 }; 0420 0421 using namespace std::placeholders; 0422 connect(m_startupTasksModel, &QAbstractItemModel::rowsInserted, this, std::bind(handleRow, true /*busy*/, _1, _2, _3)); 0423 connect(m_startupTasksModel, &QAbstractItemModel::rowsAboutToBeRemoved, this, std::bind(handleRow, false /*busy*/, _1, _2, _3)); 0424 } 0425 0426 KIO::OpenUrlJob *job = new KIO::OpenUrlJob(QUrl::fromLocalFile(m_localPath)); 0427 job->setRunExecutables(true); // so it can launch apps 0428 job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled)); 0429 job->start(); 0430 } 0431 0432 void IconApplet::processDrop(QObject *dropEvent) 0433 { 0434 Q_ASSERT(dropEvent); 0435 const bool isAcceptable = isAcceptableDrag(dropEvent); 0436 Q_ASSERT(isAcceptable); 0437 0438 const auto &urls = urlsFromDrop(dropEvent); 0439 0440 if (urls.isEmpty()) { 0441 return; 0442 } 0443 0444 const QString &localPath = m_url.toLocalFile(); 0445 0446 if (KDesktopFile::isDesktopFile(localPath)) { 0447 auto service = new KService(localPath); 0448 0449 if (service->isApplication()) { 0450 KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(KService::Ptr(service)); 0451 job->setUrls(urls); 0452 job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled)); 0453 job->start(); 0454 return; 0455 } 0456 } 0457 0458 QMimeDatabase db; 0459 const QMimeType mimeType = db.mimeTypeForUrl(m_url); 0460 0461 if (isExecutable(mimeType)) { // isAcceptableDrag has the KAuthorized check for this 0462 QProcess::startDetached(m_url.toLocalFile(), QUrl::toStringList(urls)); 0463 return; 0464 } 0465 0466 if (mimeType.inherits(QStringLiteral("inode/directory"))) { 0467 QMimeData mimeData; 0468 mimeData.setUrls(urls); 0469 0470 // DeclarativeDropEvent isn't public 0471 QDropEvent de(QPointF(dropEvent->property("x").toInt(), dropEvent->property("y").toInt()), 0472 static_cast<Qt::DropActions>(dropEvent->property("proposedActions").toInt()), 0473 &mimeData, 0474 static_cast<Qt::MouseButtons>(dropEvent->property("buttons").toInt()), 0475 static_cast<Qt::KeyboardModifiers>(dropEvent->property("modifiers").toInt())); 0476 0477 KIO::drop(&de, m_url); 0478 return; 0479 } 0480 } 0481 0482 bool IconApplet::isAcceptableDrag(QObject *dropEvent) 0483 { 0484 Q_ASSERT(dropEvent); 0485 0486 const auto &urls = urlsFromDrop(dropEvent); 0487 0488 if (urls.isEmpty()) { 0489 return false; 0490 } 0491 0492 const QString &localPath = m_url.toLocalFile(); 0493 if (KDesktopFile::isDesktopFile(localPath)) { 0494 return true; 0495 } 0496 0497 QMimeDatabase db; 0498 const QMimeType mimeType = db.mimeTypeForUrl(m_url); 0499 0500 if (KAuthorized::authorize(KAuthorized::SHELL_ACCESS) && isExecutable(mimeType)) { 0501 return true; 0502 } 0503 0504 if (mimeType.inherits(QStringLiteral("inode/directory"))) { 0505 return true; 0506 } 0507 0508 return false; 0509 } 0510 0511 QList<QUrl> IconApplet::urlsFromDrop(QObject *dropEvent) 0512 { 0513 // DeclarativeDropEvent and co aren't public 0514 const QObject *mimeData = qvariant_cast<QObject *>(dropEvent->property("mimeData")); 0515 Q_ASSERT(mimeData); 0516 0517 const QJsonArray &droppedUrls = mimeData->property("urls").toJsonArray(); 0518 0519 QList<QUrl> urls; 0520 urls.reserve(droppedUrls.count()); 0521 for (const QJsonValue &droppedUrl : droppedUrls) { 0522 const QUrl url(droppedUrl.toString()); 0523 if (url.isValid()) { 0524 urls.append(url); 0525 } 0526 } 0527 0528 return urls; 0529 } 0530 0531 bool IconApplet::isExecutable(const QMimeType &mimeType) 0532 { 0533 return (mimeType.inherits(QStringLiteral("application/x-executable")) || mimeType.inherits(QStringLiteral("application/x-shellscript"))); 0534 } 0535 0536 void IconApplet::configure() 0537 { 0538 KPropertiesDialog *dialog = m_configDialog.data(); 0539 0540 if (dialog) { 0541 dialog->show(); 0542 dialog->raise(); 0543 return; 0544 } 0545 0546 dialog = new KPropertiesDialog(QUrl::fromLocalFile(m_localPath)); 0547 m_configDialog = dialog; 0548 0549 connect(dialog, &KPropertiesDialog::applied, this, [this] { 0550 KDesktopFile desktopFile(m_localPath); 0551 if (desktopFile.hasLinkType()) { 0552 const QUrl newUrl(desktopFile.readUrl()); 0553 0554 if (m_url != newUrl) { 0555 // make sure to fully repopulate in case the user changed the Link URL 0556 QFile::remove(m_localPath); 0557 0558 setUrl(newUrl); // calls populate() itself, but only if it changed 0559 return; 0560 } 0561 } 0562 0563 populate(); 0564 }); 0565 0566 dialog->setAttribute(Qt::WA_DeleteOnClose, true); 0567 dialog->setFileNameReadOnly(true); 0568 dialog->setWindowTitle(i18n("Properties for %1", m_name)); 0569 dialog->setWindowIcon(QIcon::fromTheme(QStringLiteral("document-properties"))); 0570 dialog->show(); 0571 } 0572 0573 QString IconApplet::localPath() const 0574 { 0575 return config().readEntry(QStringLiteral("localPath")); 0576 } 0577 0578 void IconApplet::setLocalPath(const QString &localPath) 0579 { 0580 const bool oldValid = isValid(); 0581 m_localPath = localPath; 0582 config().writeEntry(QStringLiteral("localPath"), localPath); 0583 if (oldValid != isValid()) { 0584 Q_EMIT isValidChanged(); 0585 } 0586 } 0587 0588 K_PLUGIN_CLASS(IconApplet) 0589 0590 #include "iconapplet.moc"