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