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 &current, 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"