File indexing completed on 2024-12-01 03:41:16
0001 /* 0002 This file is part of the KDE libraries 0003 SPDX-FileCopyrightText: 1997 Torben Weis <weis@stud.uni-frankfurt.de> 0004 SPDX-FileCopyrightText: 1999 Dirk Mueller <mueller@kde.org> 0005 Portions SPDX-FileCopyrightText: 1999 Preston Brown <pbrown@kde.org> 0006 SPDX-FileCopyrightText: 2007 Pino Toscano <pino@kde.org> 0007 0008 SPDX-License-Identifier: LGPL-2.0-or-later 0009 */ 0010 0011 #include "kopenwithdialog.h" 0012 #include "kio_widgets_debug.h" 0013 #include "kopenwithdialog_p.h" 0014 0015 #include <QApplication> 0016 #include <QCheckBox> 0017 #include <QDialogButtonBox> 0018 #include <QIcon> 0019 #include <QKeyEvent> 0020 #include <QLabel> 0021 #include <QLayout> 0022 #include <QList> 0023 #include <QMimeDatabase> 0024 #include <QScreen> 0025 #include <QStandardPaths> 0026 #include <QStyle> 0027 #include <QStyleOptionButton> 0028 #include <QtAlgorithms> 0029 0030 #include <KAuthorized> 0031 #include <KCollapsibleGroupBox> 0032 #include <KDesktopFile> 0033 #include <KHistoryComboBox> 0034 #include <KIO/CommandLauncherJob> 0035 #include <KLineEdit> 0036 #include <KLocalizedString> 0037 #include <KMessageBox> 0038 #include <KServiceGroup> 0039 #include <KSharedConfig> 0040 #include <KShell> 0041 #include <KStringHandler> 0042 #include <QDebug> 0043 #include <kio/desktopexecparser.h> 0044 #include <kurlauthorized.h> 0045 #include <kurlcompletion.h> 0046 #include <kurlrequester.h> 0047 0048 #include <KConfigGroup> 0049 #include <assert.h> 0050 #ifndef KIO_ANDROID_STUB 0051 #include <kbuildsycocaprogressdialog.h> 0052 #endif 0053 #include <stdlib.h> 0054 0055 inline void 0056 writeEntry(KConfigGroup &group, const char *key, const KCompletion::CompletionMode &aValue, KConfigBase::WriteConfigFlags flags = KConfigBase::Normal) 0057 { 0058 group.writeEntry(key, int(aValue), flags); 0059 } 0060 0061 namespace KDEPrivate 0062 { 0063 class AppNode 0064 { 0065 public: 0066 AppNode() 0067 : isDir(false) 0068 , parent(nullptr) 0069 , fetched(false) 0070 { 0071 } 0072 ~AppNode() 0073 { 0074 qDeleteAll(children); 0075 } 0076 AppNode(const AppNode &) = delete; 0077 AppNode &operator=(const AppNode &) = delete; 0078 0079 QString icon; 0080 QString text; 0081 QString tooltip; 0082 QString entryPath; 0083 QString exec; 0084 bool isDir; 0085 0086 AppNode *parent; 0087 bool fetched; 0088 0089 QList<AppNode *> children; 0090 }; 0091 0092 static bool AppNodeLessThan(KDEPrivate::AppNode *n1, KDEPrivate::AppNode *n2) 0093 { 0094 if (n1->isDir) { 0095 if (n2->isDir) { 0096 return n1->text.compare(n2->text, Qt::CaseInsensitive) < 0; 0097 } else { 0098 return true; 0099 } 0100 } else { 0101 if (n2->isDir) { 0102 return false; 0103 } else { 0104 return n1->text.compare(n2->text, Qt::CaseInsensitive) < 0; 0105 } 0106 } 0107 } 0108 0109 } 0110 0111 class KApplicationModelPrivate 0112 { 0113 public: 0114 explicit KApplicationModelPrivate(KApplicationModel *qq) 0115 : q(qq) 0116 , root(new KDEPrivate::AppNode()) 0117 { 0118 } 0119 ~KApplicationModelPrivate() 0120 { 0121 delete root; 0122 } 0123 0124 void fillNode(const QString &entryPath, KDEPrivate::AppNode *node); 0125 0126 KApplicationModel *const q; 0127 0128 KDEPrivate::AppNode *root; 0129 }; 0130 0131 void KApplicationModelPrivate::fillNode(const QString &_entryPath, KDEPrivate::AppNode *node) 0132 { 0133 KServiceGroup::Ptr root = KServiceGroup::group(_entryPath); 0134 if (!root || !root->isValid()) { 0135 return; 0136 } 0137 0138 const KServiceGroup::List list = root->entries(); 0139 0140 for (const KSycocaEntry::Ptr &p : list) { 0141 QString icon; 0142 QString text; 0143 QString tooltip; 0144 QString entryPath; 0145 QString exec; 0146 bool isDir = false; 0147 if (p->isType(KST_KService)) { 0148 const KService::Ptr service(static_cast<KService *>(p.data())); 0149 0150 if (service->noDisplay()) { 0151 continue; 0152 } 0153 0154 icon = service->icon(); 0155 text = service->name(); 0156 0157 // no point adding a tooltip that only repeats service->name() 0158 const QString generic = service->genericName(); 0159 tooltip = generic != text ? generic : QString(); 0160 0161 exec = service->exec(); 0162 entryPath = service->entryPath(); 0163 } else if (p->isType(KST_KServiceGroup)) { 0164 const KServiceGroup::Ptr serviceGroup(static_cast<KServiceGroup *>(p.data())); 0165 0166 if (serviceGroup->noDisplay() || serviceGroup->childCount() == 0) { 0167 continue; 0168 } 0169 0170 icon = serviceGroup->icon(); 0171 text = serviceGroup->caption(); 0172 entryPath = serviceGroup->entryPath(); 0173 isDir = true; 0174 } else { 0175 qCWarning(KIO_WIDGETS) << "KServiceGroup: Unexpected object in list!"; 0176 continue; 0177 } 0178 0179 KDEPrivate::AppNode *newnode = new KDEPrivate::AppNode(); 0180 newnode->icon = icon; 0181 newnode->text = text; 0182 newnode->tooltip = tooltip; 0183 newnode->entryPath = entryPath; 0184 newnode->exec = exec; 0185 newnode->isDir = isDir; 0186 newnode->parent = node; 0187 node->children.append(newnode); 0188 } 0189 std::stable_sort(node->children.begin(), node->children.end(), KDEPrivate::AppNodeLessThan); 0190 } 0191 0192 KApplicationModel::KApplicationModel(QObject *parent) 0193 : QAbstractItemModel(parent) 0194 , d(new KApplicationModelPrivate(this)) 0195 { 0196 d->fillNode(QString(), d->root); 0197 const int nRows = rowCount(); 0198 for (int i = 0; i < nRows; i++) { 0199 fetchAll(index(i, 0)); 0200 } 0201 } 0202 0203 KApplicationModel::~KApplicationModel() = default; 0204 0205 bool KApplicationModel::canFetchMore(const QModelIndex &parent) const 0206 { 0207 if (!parent.isValid()) { 0208 return false; 0209 } 0210 0211 KDEPrivate::AppNode *node = static_cast<KDEPrivate::AppNode *>(parent.internalPointer()); 0212 return node->isDir && !node->fetched; 0213 } 0214 0215 int KApplicationModel::columnCount(const QModelIndex &parent) const 0216 { 0217 Q_UNUSED(parent) 0218 return 1; 0219 } 0220 0221 QVariant KApplicationModel::data(const QModelIndex &index, int role) const 0222 { 0223 if (!index.isValid()) { 0224 return QVariant(); 0225 } 0226 0227 KDEPrivate::AppNode *node = static_cast<KDEPrivate::AppNode *>(index.internalPointer()); 0228 0229 switch (role) { 0230 case Qt::DisplayRole: 0231 return node->text; 0232 case Qt::DecorationRole: 0233 if (!node->icon.isEmpty()) { 0234 return QIcon::fromTheme(node->icon); 0235 } 0236 break; 0237 case Qt::ToolTipRole: 0238 if (!node->tooltip.isEmpty()) { 0239 return node->tooltip; 0240 } 0241 break; 0242 default:; 0243 } 0244 return QVariant(); 0245 } 0246 0247 void KApplicationModel::fetchMore(const QModelIndex &parent) 0248 { 0249 if (!parent.isValid()) { 0250 return; 0251 } 0252 0253 KDEPrivate::AppNode *node = static_cast<KDEPrivate::AppNode *>(parent.internalPointer()); 0254 if (!node->isDir) { 0255 return; 0256 } 0257 0258 Q_EMIT layoutAboutToBeChanged(); 0259 d->fillNode(node->entryPath, node); 0260 node->fetched = true; 0261 Q_EMIT layoutChanged(); 0262 } 0263 0264 void KApplicationModel::fetchAll(const QModelIndex &parent) 0265 { 0266 if (!parent.isValid() || !canFetchMore(parent)) { 0267 return; 0268 } 0269 0270 fetchMore(parent); 0271 0272 int childCount = rowCount(parent); 0273 for (int i = 0; i < childCount; i++) { 0274 const QModelIndex &child = index(i, 0, parent); 0275 // Recursively call the function for each child node. 0276 fetchAll(child); 0277 } 0278 } 0279 0280 bool KApplicationModel::hasChildren(const QModelIndex &parent) const 0281 { 0282 if (!parent.isValid()) { 0283 return true; 0284 } 0285 0286 KDEPrivate::AppNode *node = static_cast<KDEPrivate::AppNode *>(parent.internalPointer()); 0287 return node->isDir; 0288 } 0289 0290 QVariant KApplicationModel::headerData(int section, Qt::Orientation orientation, int role) const 0291 { 0292 if (orientation != Qt::Horizontal || section != 0) { 0293 return QVariant(); 0294 } 0295 0296 switch (role) { 0297 case Qt::DisplayRole: 0298 return i18n("Known Applications"); 0299 default: 0300 return QVariant(); 0301 } 0302 } 0303 0304 QModelIndex KApplicationModel::index(int row, int column, const QModelIndex &parent) const 0305 { 0306 if (row < 0 || column != 0) { 0307 return QModelIndex(); 0308 } 0309 0310 KDEPrivate::AppNode *node = d->root; 0311 if (parent.isValid()) { 0312 node = static_cast<KDEPrivate::AppNode *>(parent.internalPointer()); 0313 } 0314 0315 if (row >= node->children.count()) { 0316 return QModelIndex(); 0317 } else { 0318 return createIndex(row, 0, node->children.at(row)); 0319 } 0320 } 0321 0322 QModelIndex KApplicationModel::parent(const QModelIndex &index) const 0323 { 0324 if (!index.isValid()) { 0325 return QModelIndex(); 0326 } 0327 0328 KDEPrivate::AppNode *node = static_cast<KDEPrivate::AppNode *>(index.internalPointer()); 0329 if (node->parent->parent) { 0330 int id = node->parent->parent->children.indexOf(node->parent); 0331 0332 if (id >= 0 && id < node->parent->parent->children.count()) { 0333 return createIndex(id, 0, node->parent); 0334 } else { 0335 return QModelIndex(); 0336 } 0337 } else { 0338 return QModelIndex(); 0339 } 0340 } 0341 0342 int KApplicationModel::rowCount(const QModelIndex &parent) const 0343 { 0344 if (!parent.isValid()) { 0345 return d->root->children.count(); 0346 } 0347 0348 KDEPrivate::AppNode *node = static_cast<KDEPrivate::AppNode *>(parent.internalPointer()); 0349 return node->children.count(); 0350 } 0351 0352 QString KApplicationModel::entryPathFor(const QModelIndex &index) const 0353 { 0354 if (!index.isValid()) { 0355 return QString(); 0356 } 0357 0358 KDEPrivate::AppNode *node = static_cast<KDEPrivate::AppNode *>(index.internalPointer()); 0359 return node->entryPath; 0360 } 0361 0362 QString KApplicationModel::execFor(const QModelIndex &index) const 0363 { 0364 if (!index.isValid()) { 0365 return QString(); 0366 } 0367 0368 KDEPrivate::AppNode *node = static_cast<KDEPrivate::AppNode *>(index.internalPointer()); 0369 return node->exec; 0370 } 0371 0372 bool KApplicationModel::isDirectory(const QModelIndex &index) const 0373 { 0374 if (!index.isValid()) { 0375 return false; 0376 } 0377 0378 KDEPrivate::AppNode *node = static_cast<KDEPrivate::AppNode *>(index.internalPointer()); 0379 return node->isDir; 0380 } 0381 0382 QTreeViewProxyFilter::QTreeViewProxyFilter(QObject *parent) 0383 : QSortFilterProxyModel(parent) 0384 { 0385 } 0386 0387 bool QTreeViewProxyFilter::filterAcceptsRow(int sourceRow, const QModelIndex &parent) const 0388 { 0389 QModelIndex index = sourceModel()->index(sourceRow, 0, parent); 0390 0391 if (!index.isValid()) { 0392 return false; 0393 } 0394 0395 // Match only on leaf nodes, using plain text, not regex 0396 return !sourceModel()->hasChildren(index) // 0397 && index.data().toString().contains(filterRegularExpression().pattern(), Qt::CaseInsensitive); 0398 } 0399 0400 class KApplicationViewPrivate 0401 { 0402 public: 0403 KApplicationViewPrivate() 0404 : appModel(nullptr) 0405 , m_proxyModel(nullptr) 0406 { 0407 } 0408 0409 KApplicationModel *appModel; 0410 QSortFilterProxyModel *m_proxyModel; 0411 }; 0412 0413 KApplicationView::KApplicationView(QWidget *parent) 0414 : QTreeView(parent) 0415 , d(new KApplicationViewPrivate) 0416 { 0417 setHeaderHidden(true); 0418 } 0419 0420 KApplicationView::~KApplicationView() = default; 0421 0422 void KApplicationView::setModels(KApplicationModel *model, QSortFilterProxyModel *proxyModel) 0423 { 0424 if (d->appModel) { 0425 disconnect(selectionModel(), &QItemSelectionModel::selectionChanged, this, &KApplicationView::slotSelectionChanged); 0426 } 0427 0428 QTreeView::setModel(proxyModel); // Here we set the proxy model 0429 d->m_proxyModel = proxyModel; // Also store it in a member property to avoid many casts later 0430 0431 d->appModel = model; 0432 if (d->appModel) { 0433 connect(selectionModel(), &QItemSelectionModel::selectionChanged, this, &KApplicationView::slotSelectionChanged); 0434 } 0435 } 0436 0437 QSortFilterProxyModel *KApplicationView::proxyModel() 0438 { 0439 return d->m_proxyModel; 0440 } 0441 0442 bool KApplicationView::isDirSel() const 0443 { 0444 if (d->appModel) { 0445 QModelIndex index = selectionModel()->currentIndex(); 0446 index = d->m_proxyModel->mapToSource(index); 0447 return d->appModel->isDirectory(index); 0448 } 0449 return false; 0450 } 0451 0452 void KApplicationView::currentChanged(const QModelIndex ¤t, const QModelIndex &previous) 0453 { 0454 QTreeView::currentChanged(current, previous); 0455 0456 if (!d->appModel) { 0457 return; 0458 } 0459 0460 QModelIndex sourceCurrent = d->m_proxyModel->mapToSource(current); 0461 if (d->appModel->isDirectory(sourceCurrent)) { 0462 expand(current); 0463 } else { 0464 const QString exec = d->appModel->execFor(sourceCurrent); 0465 if (!exec.isEmpty()) { 0466 Q_EMIT highlighted(d->appModel->entryPathFor(sourceCurrent), exec); 0467 } 0468 } 0469 } 0470 0471 void KApplicationView::slotSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected) 0472 { 0473 Q_UNUSED(deselected) 0474 0475 QItemSelection sourceSelected = d->m_proxyModel->mapSelectionToSource(selected); 0476 0477 const QModelIndexList indexes = sourceSelected.indexes(); 0478 if (indexes.count() == 1) { 0479 QString exec = d->appModel->execFor(indexes.at(0)); 0480 Q_EMIT this->selected(d->appModel->entryPathFor(indexes.at(0)), exec); 0481 } 0482 } 0483 0484 /*************************************************************** 0485 * 0486 * KOpenWithDialog 0487 * 0488 ***************************************************************/ 0489 class KOpenWithDialogPrivate 0490 { 0491 public: 0492 explicit KOpenWithDialogPrivate(KOpenWithDialog *qq) 0493 : q(qq) 0494 , saveNewApps(false) 0495 { 0496 } 0497 0498 KOpenWithDialog *const q; 0499 0500 /** 0501 * Determine MIME type from URLs 0502 */ 0503 void setMimeTypeFromUrls(const QList<QUrl> &_urls); 0504 0505 void setMimeType(const QString &mimeType); 0506 0507 void addToMimeAppsList(const QString &serviceId); 0508 0509 /** 0510 * Creates a dialog that lets the user select an application for opening one or more URLs. 0511 * 0512 * @param text appears as a label on top of the entry box 0513 * @param value is the initial value in the entry box 0514 */ 0515 void init(const QString &text, const QString &value); 0516 0517 /** 0518 * Called by checkAccept() in order to save the history of the combobox 0519 */ 0520 void saveComboboxHistory(); 0521 0522 /** 0523 * Process the choices made by the user, and return true if everything is OK. 0524 * Called by KOpenWithDialog::accept(), i.e. when clicking on OK or typing Return. 0525 */ 0526 bool checkAccept(); 0527 0528 // slots 0529 void slotDbClick(); 0530 void slotFileSelected(); 0531 void discoverButtonClicked(); 0532 0533 bool saveNewApps; 0534 bool m_terminaldirty; 0535 KService::Ptr curService; 0536 KApplicationView *view; 0537 KUrlRequester *edit; 0538 QString m_command; 0539 QLabel *label; 0540 QString qMimeType; 0541 QString qMimeTypeComment; 0542 KCollapsibleGroupBox *dialogExtension; 0543 QCheckBox *terminal; 0544 QCheckBox *remember; 0545 QCheckBox *nocloseonexit; 0546 KService::Ptr m_pService; 0547 QDialogButtonBox *buttonBox; 0548 }; 0549 0550 KOpenWithDialog::KOpenWithDialog(const QList<QUrl> &_urls, QWidget *parent) 0551 : QDialog(parent) 0552 , d(new KOpenWithDialogPrivate(this)) 0553 { 0554 setObjectName(QStringLiteral("openwith")); 0555 setModal(true); 0556 setWindowTitle(i18n("Open With")); 0557 0558 QString text; 0559 if (_urls.count() == 1) { 0560 text = i18n( 0561 "<qt>Select the program that should be used to open <b>%1</b>. " 0562 "If the program is not listed, enter the name or click " 0563 "the browse button.</qt>", 0564 _urls.first().fileName().toHtmlEscaped()); 0565 } else 0566 // Should never happen ?? 0567 { 0568 text = i18n("Choose the name of the program with which to open the selected files."); 0569 } 0570 d->setMimeTypeFromUrls(_urls); 0571 d->init(text, QString()); 0572 } 0573 0574 KOpenWithDialog::KOpenWithDialog(const QList<QUrl> &_urls, const QString &_text, const QString &_value, QWidget *parent) 0575 : KOpenWithDialog(_urls, QString(), _text, _value, parent) 0576 { 0577 } 0578 0579 KOpenWithDialog::KOpenWithDialog(const QList<QUrl> &_urls, const QString &mimeType, const QString &_text, const QString &_value, QWidget *parent) 0580 : QDialog(parent) 0581 , d(new KOpenWithDialogPrivate(this)) 0582 { 0583 setObjectName(QStringLiteral("openwith")); 0584 setModal(true); 0585 QString text = _text; 0586 if (text.isEmpty() && !_urls.isEmpty()) { 0587 if (_urls.count() == 1) { 0588 const QString fileName = KStringHandler::csqueeze(_urls.first().fileName()); 0589 text = i18n("<qt>Select the program you want to use to open the file<br/>%1</qt>", fileName.toHtmlEscaped()); 0590 } else { 0591 text = i18np("<qt>Select the program you want to use to open the file.</qt>", 0592 "<qt>Select the program you want to use to open the %1 files.</qt>", 0593 _urls.count()); 0594 } 0595 } 0596 setWindowTitle(i18n("Choose Application")); 0597 if (mimeType.isEmpty()) { 0598 d->setMimeTypeFromUrls(_urls); 0599 } else { 0600 d->setMimeType(mimeType); 0601 } 0602 d->init(text, _value); 0603 } 0604 0605 KOpenWithDialog::KOpenWithDialog(const QString &mimeType, const QString &value, QWidget *parent) 0606 : QDialog(parent) 0607 , d(new KOpenWithDialogPrivate(this)) 0608 { 0609 setObjectName(QStringLiteral("openwith")); 0610 setModal(true); 0611 setWindowTitle(i18n("Choose Application for %1", mimeType)); 0612 QString text = i18n( 0613 "<qt>Select the program for the file type: <b>%1</b>. " 0614 "If the program is not listed, enter the name or click " 0615 "the browse button.</qt>", 0616 mimeType); 0617 d->setMimeType(mimeType); 0618 d->init(text, value); 0619 } 0620 0621 KOpenWithDialog::KOpenWithDialog(QWidget *parent) 0622 : QDialog(parent) 0623 , d(new KOpenWithDialogPrivate(this)) 0624 { 0625 setObjectName(QStringLiteral("openwith")); 0626 setModal(true); 0627 setWindowTitle(i18n("Choose Application")); 0628 QString text = i18n( 0629 "<qt>Select a program. " 0630 "If the program is not listed, enter the name or click " 0631 "the browse button.</qt>"); 0632 d->qMimeType.clear(); 0633 d->init(text, QString()); 0634 } 0635 0636 void KOpenWithDialogPrivate::setMimeTypeFromUrls(const QList<QUrl> &_urls) 0637 { 0638 if (_urls.count() == 1) { 0639 QMimeDatabase db; 0640 QMimeType mime = db.mimeTypeForUrl(_urls.first()); 0641 qMimeType = mime.name(); 0642 if (mime.isDefault()) { 0643 qMimeType.clear(); 0644 } else { 0645 qMimeTypeComment = mime.comment(); 0646 } 0647 } else { 0648 qMimeType.clear(); 0649 } 0650 } 0651 0652 void KOpenWithDialogPrivate::setMimeType(const QString &mimeType) 0653 { 0654 qMimeType = mimeType; 0655 QMimeDatabase db; 0656 qMimeTypeComment = db.mimeTypeForName(mimeType).comment(); 0657 } 0658 0659 void KOpenWithDialogPrivate::init(const QString &_text, const QString &_value) 0660 { 0661 bool bReadOnly = !KAuthorized::authorize(KAuthorized::SHELL_ACCESS); 0662 m_terminaldirty = false; 0663 view = nullptr; 0664 m_pService = nullptr; 0665 curService = nullptr; 0666 0667 QBoxLayout *topLayout = new QVBoxLayout(q); 0668 label = new QLabel(_text, q); 0669 label->setWordWrap(true); 0670 topLayout->addWidget(label); 0671 0672 if (!bReadOnly) { 0673 // init the history combo and insert it into the URL-Requester 0674 KHistoryComboBox *combo = new KHistoryComboBox(); 0675 combo->setToolTip(i18n("Type to filter the applications below, or specify the name of a command.\nPress down arrow to navigate the results.")); 0676 KLineEdit *lineEdit = new KLineEdit(q); 0677 lineEdit->setClearButtonEnabled(true); 0678 combo->setLineEdit(lineEdit); 0679 combo->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLengthWithIcon); 0680 combo->setDuplicatesEnabled(false); 0681 KConfigGroup cg(KSharedConfig::openStateConfig(), QStringLiteral("Open-with settings")); 0682 int max = cg.readEntry("Maximum history", 15); 0683 combo->setMaxCount(max); 0684 int mode = cg.readEntry("CompletionMode", int(KCompletion::CompletionNone)); 0685 combo->setCompletionMode(static_cast<KCompletion::CompletionMode>(mode)); 0686 const QStringList list = cg.readEntry("History", QStringList()); 0687 combo->setHistoryItems(list, true); 0688 edit = new KUrlRequester(combo, q); 0689 edit->installEventFilter(q); 0690 } else { 0691 edit = new KUrlRequester(q); 0692 edit->lineEdit()->setReadOnly(true); 0693 edit->button()->hide(); 0694 } 0695 0696 edit->setText(_value); 0697 edit->setWhatsThis( 0698 i18n("Following the command, you can have several place holders which will be replaced " 0699 "with the actual values when the actual program is run:\n" 0700 "%f - a single file name\n" 0701 "%F - a list of files; use for applications that can open several local files at once\n" 0702 "%u - a single URL\n" 0703 "%U - a list of URLs\n" 0704 "%d - the directory of the file to open\n" 0705 "%D - a list of directories\n" 0706 "%i - the icon\n" 0707 "%m - the mini-icon\n" 0708 "%c - the comment")); 0709 0710 topLayout->addWidget(edit); 0711 0712 if (edit->comboBox()) { 0713 KUrlCompletion *comp = new KUrlCompletion(KUrlCompletion::ExeCompletion); 0714 edit->comboBox()->setCompletionObject(comp); 0715 edit->comboBox()->setAutoDeleteCompletionObject(true); 0716 } 0717 0718 QObject::connect(edit, &KUrlRequester::textChanged, q, &KOpenWithDialog::slotTextChanged); 0719 QObject::connect(edit, &KUrlRequester::urlSelected, q, [this]() { 0720 slotFileSelected(); 0721 }); 0722 0723 view = new KApplicationView(q); 0724 QTreeViewProxyFilter *proxyModel = new QTreeViewProxyFilter(view); 0725 KApplicationModel *appModel = new KApplicationModel(proxyModel); 0726 proxyModel->setSourceModel(appModel); 0727 proxyModel->setFilterKeyColumn(0); 0728 proxyModel->setRecursiveFilteringEnabled(true); 0729 view->setModels(appModel, proxyModel); 0730 topLayout->addWidget(view); 0731 topLayout->setStretchFactor(view, 1); 0732 0733 QObject::connect(view, &KApplicationView::selected, q, &KOpenWithDialog::slotSelected); 0734 QObject::connect(view, &KApplicationView::highlighted, q, &KOpenWithDialog::slotHighlighted); 0735 QObject::connect(view, &KApplicationView::doubleClicked, q, [this]() { 0736 slotDbClick(); 0737 }); 0738 0739 if (!qMimeType.isNull()) { 0740 if (!qMimeTypeComment.isEmpty()) { 0741 remember = new QCheckBox(i18n("&Remember application association for all files of type\n\"%1\" (%2)", qMimeTypeComment, qMimeType)); 0742 } else { 0743 remember = new QCheckBox(i18n("&Remember application association for all files of type\n\"%1\"", qMimeType)); 0744 } 0745 0746 topLayout->addWidget(remember); 0747 } else { 0748 remember = nullptr; 0749 } 0750 0751 // Advanced options 0752 dialogExtension = new KCollapsibleGroupBox(q); 0753 dialogExtension->setTitle(i18n("Terminal options")); 0754 0755 QVBoxLayout *dialogExtensionLayout = new QVBoxLayout(dialogExtension); 0756 dialogExtensionLayout->setContentsMargins(0, 0, 0, 0); 0757 0758 terminal = new QCheckBox(i18n("Run in &terminal"), q); 0759 if (bReadOnly) { 0760 terminal->hide(); 0761 } 0762 QObject::connect(terminal, &QAbstractButton::toggled, q, &KOpenWithDialog::slotTerminalToggled); 0763 0764 dialogExtensionLayout->addWidget(terminal); 0765 0766 QStyleOptionButton checkBoxOption; 0767 checkBoxOption.initFrom(terminal); 0768 int checkBoxIndentation = terminal->style()->pixelMetric(QStyle::PM_IndicatorWidth, &checkBoxOption, terminal); 0769 checkBoxIndentation += terminal->style()->pixelMetric(QStyle::PM_CheckBoxLabelSpacing, &checkBoxOption, terminal); 0770 0771 QBoxLayout *nocloseonexitLayout = new QHBoxLayout(); 0772 nocloseonexitLayout->setContentsMargins(0, 0, 0, 0); 0773 QSpacerItem *spacer = new QSpacerItem(checkBoxIndentation, 0, QSizePolicy::Fixed, QSizePolicy::Minimum); 0774 nocloseonexitLayout->addItem(spacer); 0775 0776 nocloseonexit = new QCheckBox(i18n("&Do not close when command exits"), q); 0777 nocloseonexit->setChecked(false); 0778 nocloseonexit->setDisabled(true); 0779 0780 // check to see if we use konsole if not disable the nocloseonexit 0781 // because we don't know how to do this on other terminal applications 0782 KConfigGroup confGroup(KSharedConfig::openConfig(), QStringLiteral("General")); 0783 QString preferredTerminal = confGroup.readPathEntry("TerminalApplication", QStringLiteral("konsole")); 0784 0785 if (bReadOnly || preferredTerminal != QLatin1String("konsole")) { 0786 nocloseonexit->hide(); 0787 } 0788 0789 nocloseonexitLayout->addWidget(nocloseonexit); 0790 dialogExtensionLayout->addLayout(nocloseonexitLayout); 0791 0792 topLayout->addWidget(dialogExtension); 0793 0794 if (!qMimeType.isNull() && KService::serviceByDesktopName(QStringLiteral("org.kde.discover"))) { 0795 QPushButton *discoverButton = new QPushButton(QIcon::fromTheme(QStringLiteral("plasmadiscover")), i18n("Get more Apps from Discover")); 0796 QObject::connect(discoverButton, &QPushButton::clicked, q, [this]() { 0797 discoverButtonClicked(); 0798 }); 0799 topLayout->addWidget(discoverButton); 0800 } 0801 0802 buttonBox = new QDialogButtonBox(q); 0803 buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); 0804 q->connect(buttonBox, &QDialogButtonBox::accepted, q, &QDialog::accept); 0805 q->connect(buttonBox, &QDialogButtonBox::rejected, q, &QDialog::reject); 0806 topLayout->addWidget(buttonBox); 0807 0808 q->setMinimumSize(q->minimumSizeHint()); 0809 // edit->setText( _value ); 0810 // The resize is what caused "can't click on items before clicking on Name header" in previous versions. 0811 // Probably due to the resizeEvent handler using width(). 0812 q->resize(q->minimumWidth(), 0.6 * q->screen()->availableGeometry().height()); 0813 edit->setFocus(); 0814 q->slotTextChanged(); 0815 } 0816 0817 void KOpenWithDialogPrivate::discoverButtonClicked() 0818 { 0819 KIO::CommandLauncherJob *job = new KIO::CommandLauncherJob(QStringLiteral("plasma-discover"), {QStringLiteral("--mime"), qMimeType}); 0820 job->setDesktopName(QStringLiteral("org.kde.discover")); 0821 job->start(); 0822 } 0823 0824 // ---------------------------------------------------------------------- 0825 0826 KOpenWithDialog::~KOpenWithDialog() 0827 { 0828 d->edit->removeEventFilter(this); 0829 }; 0830 0831 // ---------------------------------------------------------------------- 0832 0833 void KOpenWithDialog::slotSelected(const QString & /*_name*/, const QString &_exec) 0834 { 0835 d->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!_exec.isEmpty()); 0836 } 0837 0838 // ---------------------------------------------------------------------- 0839 0840 void KOpenWithDialog::slotHighlighted(const QString &entryPath, const QString &) 0841 { 0842 d->curService = KService::serviceByDesktopPath(entryPath); 0843 if (d->curService && !d->m_terminaldirty) { 0844 // ### indicate that default value was restored 0845 d->terminal->setChecked(d->curService->terminal()); 0846 QString terminalOptions = d->curService->terminalOptions(); 0847 d->nocloseonexit->setChecked((terminalOptions.contains(QLatin1String("--noclose")))); 0848 d->m_terminaldirty = false; // slotTerminalToggled changed it 0849 } 0850 } 0851 0852 // ---------------------------------------------------------------------- 0853 0854 void KOpenWithDialog::slotTextChanged() 0855 { 0856 // Forget about the service only when the selection is empty 0857 // otherwise changing text but hitting the same result clears curService 0858 bool selectionEmpty = !d->view->currentIndex().isValid(); 0859 if (d->curService && selectionEmpty) { 0860 d->curService = nullptr; 0861 } 0862 d->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!d->edit->text().isEmpty() || d->curService); 0863 0864 // escape() because we want plain text matching; the matching is case-insensitive, 0865 // see QTreeViewProxyFilter::filterAcceptsRow() 0866 d->view->proxyModel()->setFilterRegularExpression(QRegularExpression::escape(d->edit->text())); 0867 0868 // Expand all the nodes when the search string is 3 characters long 0869 // If the search string doesn't match anything there will be no nodes to expand 0870 if (d->edit->text().size() > 2) { 0871 d->view->expandAll(); 0872 QAbstractItemModel *model = d->view->model(); 0873 if (model->rowCount() == 1) { // Automatically select the result (first leaf node) if the 0874 // filter has only one match 0875 QModelIndex leafNodeIdx = model->index(0, 0); 0876 while (model->hasChildren(leafNodeIdx)) { 0877 leafNodeIdx = model->index(0, 0, leafNodeIdx); 0878 } 0879 d->view->setCurrentIndex(leafNodeIdx); 0880 } 0881 } else { 0882 d->view->collapseAll(); 0883 d->view->setCurrentIndex(d->view->rootIndex()); // Unset and deselect all the elements 0884 d->curService = nullptr; 0885 } 0886 } 0887 0888 // ---------------------------------------------------------------------- 0889 0890 void KOpenWithDialog::slotTerminalToggled(bool) 0891 { 0892 // ### indicate that default value was overridden 0893 d->m_terminaldirty = true; 0894 d->nocloseonexit->setDisabled(!d->terminal->isChecked()); 0895 } 0896 0897 // ---------------------------------------------------------------------- 0898 0899 void KOpenWithDialogPrivate::slotDbClick() 0900 { 0901 // check if a directory is selected 0902 if (view->isDirSel()) { 0903 return; 0904 } 0905 q->accept(); 0906 } 0907 0908 void KOpenWithDialogPrivate::slotFileSelected() 0909 { 0910 // quote the path to avoid unescaped whitespace, backslashes, etc. 0911 edit->setText(KShell::quoteArg(edit->text())); 0912 } 0913 0914 void KOpenWithDialog::setSaveNewApplications(bool b) 0915 { 0916 d->saveNewApps = b; 0917 } 0918 0919 static QString simplifiedExecLineFromService(const QString &fullExec) 0920 { 0921 QString exec = fullExec; 0922 exec.remove(QLatin1String("%u"), Qt::CaseInsensitive); 0923 exec.remove(QLatin1String("%f"), Qt::CaseInsensitive); 0924 exec.remove(QLatin1String("-caption %c")); 0925 exec.remove(QLatin1String("-caption \"%c\"")); 0926 exec.remove(QLatin1String("%i")); 0927 exec.remove(QLatin1String("%m")); 0928 return exec.simplified(); 0929 } 0930 0931 void KOpenWithDialogPrivate::addToMimeAppsList(const QString &serviceId /*menu id or storage id*/) 0932 { 0933 KSharedConfig::Ptr profile = KSharedConfig::openConfig(QStringLiteral("mimeapps.list"), KConfig::NoGlobals, QStandardPaths::GenericConfigLocation); 0934 0935 // Save the default application according to mime-apps-spec 1.0 0936 KConfigGroup defaultApp(profile, QStringLiteral("Default Applications")); 0937 defaultApp.writeXdgListEntry(qMimeType, QStringList(serviceId)); 0938 0939 KConfigGroup addedApps(profile, QStringLiteral("Added Associations")); 0940 QStringList apps = addedApps.readXdgListEntry(qMimeType); 0941 apps.removeAll(serviceId); 0942 apps.prepend(serviceId); // make it the preferred app 0943 addedApps.writeXdgListEntry(qMimeType, apps); 0944 0945 profile->sync(); 0946 0947 // Also make sure the "auto embed" setting for this MIME type is off 0948 KSharedConfig::Ptr fileTypesConfig = KSharedConfig::openConfig(QStringLiteral("filetypesrc"), KConfig::NoGlobals); 0949 fileTypesConfig->group(QStringLiteral("EmbedSettings")).writeEntry(QStringLiteral("embed-") + qMimeType, false); 0950 fileTypesConfig->sync(); 0951 0952 // qDebug() << "rebuilding ksycoca..."; 0953 0954 // kbuildsycoca is the one reading mimeapps.list, so we need to run it now 0955 #ifndef KIO_ANDROID_STUB 0956 KBuildSycocaProgressDialog::rebuildKSycoca(q); 0957 #endif 0958 0959 // could be nullptr if the user canceled the dialog... 0960 m_pService = KService::serviceByStorageId(serviceId); 0961 } 0962 0963 bool KOpenWithDialogPrivate::checkAccept() 0964 { 0965 const QString typedExec(edit->text()); 0966 QString fullExec(typedExec); 0967 0968 QString serviceName; 0969 QString initialServiceName; 0970 QString preferredTerminal; 0971 QString configPath; 0972 QString serviceExec; 0973 m_pService = curService; 0974 if (!m_pService) { 0975 // No service selected - check the command line 0976 0977 // Find out the name of the service from the command line, removing args and paths 0978 serviceName = KIO::DesktopExecParser::executableName(typedExec); 0979 if (serviceName.isEmpty()) { 0980 KMessageBox::error(q, i18n("Could not extract executable name from '%1', please type a valid program name.", serviceName)); 0981 return false; 0982 } 0983 initialServiceName = serviceName; 0984 // Also remember the executableName with a path, if any, for the 0985 // check that the executable exists. 0986 // qDebug() << "initialServiceName=" << initialServiceName; 0987 int i = 1; // We have app, app-2, app-3... Looks better for the user. 0988 bool ok = false; 0989 // Check if there's already a service by that name, with the same Exec line 0990 do { 0991 // qDebug() << "looking for service" << serviceName; 0992 KService::Ptr serv = KService::serviceByDesktopName(serviceName); 0993 ok = !serv; // ok if no such service yet 0994 // also ok if we find the exact same service (well, "kwrite" == "kwrite %U") 0995 if (serv && !serv->noDisplay() /* #297720 */) { 0996 if (serv->isApplication()) { 0997 /*// qDebug() << "typedExec=" << typedExec 0998 << "serv->exec=" << serv->exec() 0999 << "simplifiedExecLineFromService=" << simplifiedExecLineFromService(fullExec);*/ 1000 serviceExec = simplifiedExecLineFromService(serv->exec()); 1001 if (typedExec == serviceExec) { 1002 ok = true; 1003 m_pService = serv; 1004 // qDebug() << "OK, found identical service: " << serv->entryPath(); 1005 } else { 1006 // qDebug() << "Exec line differs, service says:" << serviceExec; 1007 configPath = serv->entryPath(); 1008 serviceExec = serv->exec(); 1009 } 1010 } else { 1011 // qDebug() << "Found, but not an application:" << serv->entryPath(); 1012 } 1013 } 1014 if (!ok) { // service was found, but it was different -> keep looking 1015 ++i; 1016 serviceName = initialServiceName + QLatin1Char('-') + QString::number(i); 1017 } 1018 } while (!ok); 1019 } 1020 if (m_pService) { 1021 // Existing service selected 1022 serviceName = m_pService->name(); 1023 initialServiceName = serviceName; 1024 fullExec = m_pService->exec(); 1025 } else { 1026 const QString binaryName = KIO::DesktopExecParser::executablePath(typedExec); 1027 // qDebug() << "binaryName=" << binaryName; 1028 // Ensure that the typed binary name actually exists (#81190) 1029 if (QStandardPaths::findExecutable(binaryName).isEmpty()) { 1030 // QStandardPaths::findExecutable does not find non-executable files. 1031 // Give a better error message for the case of a existing but non-executable file. 1032 // https://bugs.kde.org/show_bug.cgi?id=437880 1033 const QString msg = QFileInfo::exists(binaryName) 1034 ? xi18nc("@info", "<filename>%1</filename> does not appear to be an executable program.", binaryName) 1035 : xi18nc("@info", "<filename>%1</filename> was not found; please enter a valid path to an executable program.", binaryName); 1036 1037 KMessageBox::error(q, msg); 1038 return false; 1039 } 1040 } 1041 1042 if (terminal->isChecked()) { 1043 KConfigGroup confGroup(KSharedConfig::openConfig(), QStringLiteral("General")); 1044 preferredTerminal = confGroup.readPathEntry("TerminalApplication", QStringLiteral("konsole")); 1045 m_command = preferredTerminal; 1046 // only add --noclose when we are sure it is konsole we're using 1047 if (preferredTerminal == QLatin1String("konsole") && nocloseonexit->isChecked()) { 1048 m_command += QStringLiteral(" --noclose"); 1049 } 1050 m_command += QLatin1String(" -e ") + edit->text(); 1051 // qDebug() << "Setting m_command to" << m_command; 1052 } 1053 if (m_pService && terminal->isChecked() != m_pService->terminal()) { 1054 m_pService = nullptr; // It's not exactly this service we're running 1055 } 1056 1057 const bool bRemember = remember && remember->isChecked(); 1058 // qDebug() << "bRemember=" << bRemember << "service found=" << m_pService; 1059 if (m_pService) { 1060 if (bRemember) { 1061 // Associate this app with qMimeType in mimeapps.list 1062 Q_ASSERT(!qMimeType.isEmpty()); // we don't show the remember checkbox otherwise 1063 addToMimeAppsList(m_pService->storageId()); 1064 } 1065 } else { 1066 const bool createDesktopFile = bRemember || saveNewApps; 1067 if (!createDesktopFile) { 1068 // Create temp service 1069 if (configPath.isEmpty()) { 1070 m_pService = new KService(initialServiceName, fullExec, QString()); 1071 } else { 1072 if (!typedExec.contains(QLatin1String("%u"), Qt::CaseInsensitive) && !typedExec.contains(QLatin1String("%f"), Qt::CaseInsensitive)) { 1073 int index = serviceExec.indexOf(QLatin1String("%u"), 0, Qt::CaseInsensitive); 1074 if (index == -1) { 1075 index = serviceExec.indexOf(QLatin1String("%f"), 0, Qt::CaseInsensitive); 1076 } 1077 if (index > -1) { 1078 fullExec += QLatin1Char(' ') + QStringView(serviceExec).mid(index, 2); 1079 } 1080 } 1081 // qDebug() << "Creating service with Exec=" << fullExec; 1082 m_pService = new KService(configPath); 1083 m_pService->setExec(fullExec); 1084 } 1085 if (terminal->isChecked()) { 1086 m_pService->setTerminal(true); 1087 // only add --noclose when we are sure it is konsole we're using 1088 if (preferredTerminal == QLatin1String("konsole") && nocloseonexit->isChecked()) { 1089 m_pService->setTerminalOptions(QStringLiteral("--noclose")); 1090 } 1091 } 1092 } else { 1093 // If we got here, we can't seem to find a service for what they wanted. Create one. 1094 1095 QString menuId; 1096 #ifdef Q_OS_WIN32 1097 // on windows, do not use the complete path, but only the default name. 1098 serviceName = QFileInfo(serviceName).fileName(); 1099 #endif 1100 QString newPath = KService::newServicePath(false /* ignored argument */, serviceName, &menuId); 1101 // qDebug() << "Creating new service" << serviceName << "(" << newPath << ")" << "menuId=" << menuId; 1102 1103 KDesktopFile desktopFile(newPath); 1104 KConfigGroup cg = desktopFile.desktopGroup(); 1105 cg.writeEntry("Type", "Application"); 1106 1107 // For the user visible name, use the executable name with any 1108 // arguments appended, but with desktop-file specific expansion 1109 // arguments removed. This is done to more clearly communicate the 1110 // actual command used to the user and makes it easier to 1111 // distinguish things like "qdbus". 1112 QString name = KIO::DesktopExecParser::executableName(fullExec); 1113 auto view = QStringView{fullExec}.trimmed(); 1114 int index = view.indexOf(QLatin1Char(' ')); 1115 if (index > 0) { 1116 name.append(view.mid(index)); 1117 } 1118 cg.writeEntry("Name", simplifiedExecLineFromService(name)); 1119 1120 // if we select a binary for a scheme handler, then it's safe to assume it can handle URLs 1121 if (qMimeType.startsWith(QLatin1String("x-scheme-handler/"))) { 1122 if (!typedExec.contains(QLatin1String("%u"), Qt::CaseInsensitive) && !typedExec.contains(QLatin1String("%f"), Qt::CaseInsensitive)) { 1123 fullExec += QStringLiteral(" %u"); 1124 } 1125 } 1126 1127 cg.writeEntry("Exec", fullExec); 1128 cg.writeEntry("NoDisplay", true); // don't make it appear in the K menu 1129 if (terminal->isChecked()) { 1130 cg.writeEntry("Terminal", true); 1131 // only add --noclose when we are sure it is konsole we're using 1132 if (preferredTerminal == QLatin1String("konsole") && nocloseonexit->isChecked()) { 1133 cg.writeEntry("TerminalOptions", "--noclose"); 1134 } 1135 } 1136 if (!qMimeType.isEmpty()) { 1137 cg.writeXdgListEntry("MimeType", QStringList() << qMimeType); 1138 } 1139 cg.sync(); 1140 1141 if (!qMimeType.isEmpty()) { 1142 addToMimeAppsList(menuId); 1143 } 1144 m_pService = new KService(newPath); 1145 } 1146 } 1147 1148 saveComboboxHistory(); 1149 return true; 1150 } 1151 1152 bool KOpenWithDialog::eventFilter(QObject *object, QEvent *event) 1153 { 1154 // Detect DownArrow to navigate the results in the QTreeView 1155 if (object == d->edit && event->type() == QEvent::ShortcutOverride) { 1156 QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event); 1157 if (keyEvent->key() == Qt::Key_Down) { 1158 KHistoryComboBox *combo = static_cast<KHistoryComboBox *>(d->edit->comboBox()); 1159 // FIXME: Disable arrow down in CompletionPopup and CompletionPopupAuto only when the dropdown list is shown. 1160 // When popup completion mode is used the down arrow is used to navigate the dropdown list of results 1161 if (combo->completionMode() != KCompletion::CompletionPopup && combo->completionMode() != KCompletion::CompletionPopupAuto) { 1162 QModelIndex leafNodeIdx = d->view->model()->index(0, 0); 1163 // Check if we have at least one result or the focus is passed to the empty QTreeView 1164 if (d->view->model()->hasChildren(leafNodeIdx)) { 1165 d->view->setFocus(Qt::OtherFocusReason); 1166 QApplication::sendEvent(d->view, keyEvent); 1167 return true; 1168 } 1169 } 1170 } 1171 } 1172 return QDialog::eventFilter(object, event); 1173 } 1174 1175 void KOpenWithDialog::accept() 1176 { 1177 if (d->checkAccept()) { 1178 QDialog::accept(); 1179 } 1180 } 1181 1182 QString KOpenWithDialog::text() const 1183 { 1184 if (!d->m_command.isEmpty()) { 1185 return d->m_command; 1186 } else { 1187 return d->edit->text(); 1188 } 1189 } 1190 1191 void KOpenWithDialog::hideNoCloseOnExit() 1192 { 1193 // uncheck the checkbox because the value could be used when "Run in Terminal" is selected 1194 d->nocloseonexit->setChecked(false); 1195 d->nocloseonexit->hide(); 1196 1197 d->dialogExtension->setVisible(d->nocloseonexit->isVisible() || d->terminal->isVisible()); 1198 } 1199 1200 void KOpenWithDialog::hideRunInTerminal() 1201 { 1202 d->terminal->hide(); 1203 hideNoCloseOnExit(); 1204 } 1205 1206 KService::Ptr KOpenWithDialog::service() const 1207 { 1208 return d->m_pService; 1209 } 1210 1211 void KOpenWithDialogPrivate::saveComboboxHistory() 1212 { 1213 KHistoryComboBox *combo = static_cast<KHistoryComboBox *>(edit->comboBox()); 1214 if (combo) { 1215 combo->addToHistory(edit->text()); 1216 1217 KConfigGroup cg(KSharedConfig::openStateConfig(), QStringLiteral("Open-with settings")); 1218 cg.writeEntry("History", combo->historyItems()); 1219 writeEntry(cg, "CompletionMode", combo->completionMode()); 1220 // don't store the completion-list, as it contains all of KUrlCompletion's 1221 // executables 1222 cg.sync(); 1223 } 1224 } 1225 1226 #include "moc_kopenwithdialog.cpp" 1227 #include "moc_kopenwithdialog_p.cpp"