1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 1997 Torben Weis <weis@stud.uni-frankfurt.de>
4 SPDX-FileCopyrightText: 1999 Dirk Mueller <mueller@kde.org>
5 Portions SPDX-FileCopyrightText: 1999 Preston Brown <pbrown@kde.org>
6 SPDX-FileCopyrightText: 2007 Pino Toscano <pino@kde.org>
7
8 SPDX-License-Identifier: LGPL-2.0-or-later
9*/
10
11#include "kopenwithdialog.h"
12#include "kio_widgets_debug.h"
13#include "kopenwithdialog_p.h"
14
15#include <QApplication>
16#include <QCheckBox>
17#include <QDialogButtonBox>
18#include <QIcon>
19#include <QKeyEvent>
20#include <QLabel>
21#include <QLayout>
22#include <QList>
23#include <QMimeDatabase>
24#include <QScreen>
25#include <QStandardPaths>
26#include <QStyle>
27#include <QStyleOptionButton>
28#include <QtAlgorithms>
29
30#include <KAuthorized>
31#include <KCollapsibleGroupBox>
32#include <KDesktopFile>
33#include <KHistoryComboBox>
34#include <KIO/CommandLauncherJob>
35#include <KLineEdit>
36#include <KLocalizedString>
37#include <KMessageBox>
38#include <KServiceGroup>
39#include <KSharedConfig>
40#include <KShell>
41#include <KStringHandler>
42#include <QDebug>
43#include <kio/desktopexecparser.h>
44#include <kurlauthorized.h>
45#include <kurlcompletion.h>
46#include <kurlrequester.h>
47#include <openwith.h>
48
49#include <KConfigGroup>
50#include <assert.h>
51#ifndef KIO_ANDROID_STUB
52#include <kbuildsycocaprogressdialog.h>
53#endif
54#include <stdlib.h>
55
56inline void
57writeEntry(KConfigGroup &group, const char *key, const KCompletion::CompletionMode &aValue, KConfigBase::WriteConfigFlags flags = KConfigBase::Normal)
58{
59 group.writeEntry(key, value: int(aValue), pFlags: flags);
60}
61
62namespace KDEPrivate
63{
64class AppNode
65{
66public:
67 AppNode()
68 : isDir(false)
69 , parent(nullptr)
70 , fetched(false)
71 {
72 }
73 ~AppNode()
74 {
75 qDeleteAll(c: children);
76 }
77 AppNode(const AppNode &) = delete;
78 AppNode &operator=(const AppNode &) = delete;
79
80 QString icon;
81 QString text;
82 QString tooltip;
83 QString entryPath;
84 QString exec;
85 bool isDir;
86
87 AppNode *parent;
88 bool fetched;
89
90 QList<AppNode *> children;
91};
92
93static bool AppNodeLessThan(KDEPrivate::AppNode *n1, KDEPrivate::AppNode *n2)
94{
95 if (n1->isDir) {
96 if (n2->isDir) {
97 return n1->text.compare(s: n2->text, cs: Qt::CaseInsensitive) < 0;
98 } else {
99 return true;
100 }
101 } else {
102 if (n2->isDir) {
103 return false;
104 } else {
105 return n1->text.compare(s: n2->text, cs: Qt::CaseInsensitive) < 0;
106 }
107 }
108}
109
110}
111
112class KApplicationModelPrivate
113{
114public:
115 explicit KApplicationModelPrivate(KApplicationModel *qq)
116 : q(qq)
117 , root(new KDEPrivate::AppNode())
118 {
119 }
120 ~KApplicationModelPrivate()
121 {
122 delete root;
123 }
124
125 void fillNode(const QString &entryPath, KDEPrivate::AppNode *node);
126
127 KApplicationModel *const q;
128
129 KDEPrivate::AppNode *root;
130};
131
132void KApplicationModelPrivate::fillNode(const QString &_entryPath, KDEPrivate::AppNode *node)
133{
134 KServiceGroup::Ptr root = KServiceGroup::group(relPath: _entryPath);
135 if (!root || !root->isValid()) {
136 return;
137 }
138
139 const KServiceGroup::List list = root->entries();
140
141 for (const KSycocaEntry::Ptr &p : list) {
142 QString icon;
143 QString text;
144 QString tooltip;
145 QString entryPath;
146 QString exec;
147 bool isDir = false;
148 if (p->isType(t: KST_KService)) {
149 const KService::Ptr service(static_cast<KService *>(p.data()));
150
151 if (service->noDisplay()) {
152 continue;
153 }
154
155 icon = service->icon();
156 text = service->name();
157
158 // no point adding a tooltip that only repeats service->name()
159 const QString generic = service->genericName();
160 tooltip = generic != text ? generic : QString();
161
162 exec = service->exec();
163 entryPath = service->entryPath();
164 } else if (p->isType(t: KST_KServiceGroup)) {
165 const KServiceGroup::Ptr serviceGroup(static_cast<KServiceGroup *>(p.data()));
166
167 if (serviceGroup->noDisplay() || serviceGroup->childCount() == 0) {
168 continue;
169 }
170
171 icon = serviceGroup->icon();
172 text = serviceGroup->caption();
173 entryPath = serviceGroup->entryPath();
174 isDir = true;
175 } else {
176 qCWarning(KIO_WIDGETS) << "KServiceGroup: Unexpected object in list!";
177 continue;
178 }
179
180 KDEPrivate::AppNode *newnode = new KDEPrivate::AppNode();
181 newnode->icon = icon;
182 newnode->text = text;
183 newnode->tooltip = tooltip;
184 newnode->entryPath = entryPath;
185 newnode->exec = exec;
186 newnode->isDir = isDir;
187 newnode->parent = node;
188 node->children.append(t: newnode);
189 }
190 std::stable_sort(first: node->children.begin(), last: node->children.end(), comp: KDEPrivate::AppNodeLessThan);
191}
192
193KApplicationModel::KApplicationModel(QObject *parent)
194 : QAbstractItemModel(parent)
195 , d(new KApplicationModelPrivate(this))
196{
197 d->fillNode(entryPath: QString(), node: d->root);
198 const int nRows = rowCount();
199 for (int i = 0; i < nRows; i++) {
200 fetchAll(parent: index(row: i, column: 0));
201 }
202}
203
204KApplicationModel::~KApplicationModel() = default;
205
206bool KApplicationModel::canFetchMore(const QModelIndex &parent) const
207{
208 if (!parent.isValid()) {
209 return false;
210 }
211
212 KDEPrivate::AppNode *node = static_cast<KDEPrivate::AppNode *>(parent.internalPointer());
213 return node->isDir && !node->fetched;
214}
215
216int KApplicationModel::columnCount(const QModelIndex &parent) const
217{
218 Q_UNUSED(parent)
219 return 1;
220}
221
222QVariant KApplicationModel::data(const QModelIndex &index, int role) const
223{
224 if (!index.isValid()) {
225 return QVariant();
226 }
227
228 KDEPrivate::AppNode *node = static_cast<KDEPrivate::AppNode *>(index.internalPointer());
229
230 switch (role) {
231 case Qt::DisplayRole:
232 return node->text;
233 case Qt::DecorationRole:
234 if (!node->icon.isEmpty()) {
235 return QIcon::fromTheme(name: node->icon);
236 }
237 break;
238 case Qt::ToolTipRole:
239 if (!node->tooltip.isEmpty()) {
240 return node->tooltip;
241 }
242 break;
243 default:;
244 }
245 return QVariant();
246}
247
248void KApplicationModel::fetchMore(const QModelIndex &parent)
249{
250 if (!parent.isValid()) {
251 return;
252 }
253
254 KDEPrivate::AppNode *node = static_cast<KDEPrivate::AppNode *>(parent.internalPointer());
255 if (!node->isDir) {
256 return;
257 }
258
259 Q_EMIT layoutAboutToBeChanged();
260 d->fillNode(entryPath: node->entryPath, node);
261 node->fetched = true;
262 Q_EMIT layoutChanged();
263}
264
265void KApplicationModel::fetchAll(const QModelIndex &parent)
266{
267 if (!parent.isValid() || !canFetchMore(parent)) {
268 return;
269 }
270
271 fetchMore(parent);
272
273 int childCount = rowCount(parent);
274 for (int i = 0; i < childCount; i++) {
275 const QModelIndex &child = index(row: i, column: 0, parent);
276 // Recursively call the function for each child node.
277 fetchAll(parent: child);
278 }
279}
280
281bool KApplicationModel::hasChildren(const QModelIndex &parent) const
282{
283 if (!parent.isValid()) {
284 return true;
285 }
286
287 KDEPrivate::AppNode *node = static_cast<KDEPrivate::AppNode *>(parent.internalPointer());
288 return node->isDir;
289}
290
291QVariant KApplicationModel::headerData(int section, Qt::Orientation orientation, int role) const
292{
293 if (orientation != Qt::Horizontal || section != 0) {
294 return QVariant();
295 }
296
297 switch (role) {
298 case Qt::DisplayRole:
299 return i18n("Known Applications");
300 default:
301 return QVariant();
302 }
303}
304
305QModelIndex KApplicationModel::index(int row, int column, const QModelIndex &parent) const
306{
307 if (row < 0 || column != 0) {
308 return QModelIndex();
309 }
310
311 KDEPrivate::AppNode *node = d->root;
312 if (parent.isValid()) {
313 node = static_cast<KDEPrivate::AppNode *>(parent.internalPointer());
314 }
315
316 if (row >= node->children.count()) {
317 return QModelIndex();
318 } else {
319 return createIndex(arow: row, acolumn: 0, adata: node->children.at(i: row));
320 }
321}
322
323QModelIndex KApplicationModel::parent(const QModelIndex &index) const
324{
325 if (!index.isValid()) {
326 return QModelIndex();
327 }
328
329 KDEPrivate::AppNode *node = static_cast<KDEPrivate::AppNode *>(index.internalPointer());
330 if (node->parent->parent) {
331 int id = node->parent->parent->children.indexOf(t: node->parent);
332
333 if (id >= 0 && id < node->parent->parent->children.count()) {
334 return createIndex(arow: id, acolumn: 0, adata: node->parent);
335 } else {
336 return QModelIndex();
337 }
338 } else {
339 return QModelIndex();
340 }
341}
342
343int KApplicationModel::rowCount(const QModelIndex &parent) const
344{
345 if (!parent.isValid()) {
346 return d->root->children.count();
347 }
348
349 KDEPrivate::AppNode *node = static_cast<KDEPrivate::AppNode *>(parent.internalPointer());
350 return node->children.count();
351}
352
353QString KApplicationModel::entryPathFor(const QModelIndex &index) const
354{
355 if (!index.isValid()) {
356 return QString();
357 }
358
359 KDEPrivate::AppNode *node = static_cast<KDEPrivate::AppNode *>(index.internalPointer());
360 return node->entryPath;
361}
362
363QString KApplicationModel::execFor(const QModelIndex &index) const
364{
365 if (!index.isValid()) {
366 return QString();
367 }
368
369 KDEPrivate::AppNode *node = static_cast<KDEPrivate::AppNode *>(index.internalPointer());
370 return node->exec;
371}
372
373bool KApplicationModel::isDirectory(const QModelIndex &index) const
374{
375 if (!index.isValid()) {
376 return false;
377 }
378
379 KDEPrivate::AppNode *node = static_cast<KDEPrivate::AppNode *>(index.internalPointer());
380 return node->isDir;
381}
382
383QTreeViewProxyFilter::QTreeViewProxyFilter(QObject *parent)
384 : QSortFilterProxyModel(parent)
385{
386}
387
388bool QTreeViewProxyFilter::filterAcceptsRow(int sourceRow, const QModelIndex &parent) const
389{
390 QModelIndex index = sourceModel()->index(row: sourceRow, column: 0, parent);
391
392 if (!index.isValid()) {
393 return false;
394 }
395
396 // Match only on leaf nodes, using plain text, not regex
397 return !sourceModel()->hasChildren(parent: index) //
398 && index.data().toString().contains(s: filterRegularExpression().pattern(), cs: Qt::CaseInsensitive);
399}
400
401class KApplicationViewPrivate
402{
403public:
404 KApplicationViewPrivate()
405 : appModel(nullptr)
406 , m_proxyModel(nullptr)
407 {
408 }
409
410 KApplicationModel *appModel;
411 QSortFilterProxyModel *m_proxyModel;
412};
413
414KApplicationView::KApplicationView(QWidget *parent)
415 : QTreeView(parent)
416 , d(new KApplicationViewPrivate)
417{
418 setHeaderHidden(true);
419}
420
421KApplicationView::~KApplicationView() = default;
422
423void KApplicationView::setModels(KApplicationModel *model, QSortFilterProxyModel *proxyModel)
424{
425 if (d->appModel) {
426 disconnect(sender: selectionModel(), signal: &QItemSelectionModel::selectionChanged, receiver: this, slot: &KApplicationView::slotSelectionChanged);
427 }
428
429 QTreeView::setModel(proxyModel); // Here we set the proxy model
430 d->m_proxyModel = proxyModel; // Also store it in a member property to avoid many casts later
431
432 d->appModel = model;
433 if (d->appModel) {
434 connect(sender: selectionModel(), signal: &QItemSelectionModel::selectionChanged, context: this, slot: &KApplicationView::slotSelectionChanged);
435 }
436}
437
438QSortFilterProxyModel *KApplicationView::proxyModel()
439{
440 return d->m_proxyModel;
441}
442
443bool KApplicationView::isDirSel() const
444{
445 if (d->appModel) {
446 QModelIndex index = selectionModel()->currentIndex();
447 index = d->m_proxyModel->mapToSource(proxyIndex: index);
448 return d->appModel->isDirectory(index);
449 }
450 return false;
451}
452
453void KApplicationView::currentChanged(const QModelIndex &current, const QModelIndex &previous)
454{
455 QTreeView::currentChanged(current, previous);
456
457 if (!d->appModel) {
458 return;
459 }
460
461 QModelIndex sourceCurrent = d->m_proxyModel->mapToSource(proxyIndex: current);
462 if (d->appModel->isDirectory(index: sourceCurrent)) {
463 expand(index: current);
464 } else {
465 const QString exec = d->appModel->execFor(index: sourceCurrent);
466 if (!exec.isEmpty()) {
467 Q_EMIT highlighted(name: d->appModel->entryPathFor(index: sourceCurrent), exec: exec);
468 }
469 }
470}
471
472void KApplicationView::slotSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
473{
474 Q_UNUSED(deselected)
475
476 QItemSelection sourceSelected = d->m_proxyModel->mapSelectionToSource(proxySelection: selected);
477
478 const QModelIndexList indexes = sourceSelected.indexes();
479 if (indexes.count() == 1) {
480 QString exec = d->appModel->execFor(index: indexes.at(i: 0));
481 Q_EMIT this->selected(name: d->appModel->entryPathFor(index: indexes.at(i: 0)), exec: exec);
482 }
483}
484
485/***************************************************************
486 *
487 * KOpenWithDialog
488 *
489 ***************************************************************/
490class KOpenWithDialogPrivate
491{
492public:
493 explicit KOpenWithDialogPrivate(KOpenWithDialog *qq)
494 : q(qq)
495 , saveNewApps(false)
496 {
497 }
498
499 KOpenWithDialog *const q;
500
501 /**
502 * Determine MIME type from URLs
503 */
504 void setMimeTypeFromUrls(const QList<QUrl> &_urls);
505
506 void setMimeType(const QString &mimeType);
507
508 void addToMimeAppsList(const QString &serviceId);
509
510 /**
511 * Creates a dialog that lets the user select an application for opening one or more URLs.
512 *
513 * @param text appears as a label on top of the entry box
514 * @param value is the initial value in the entry box
515 */
516 void init(const QString &text, const QString &value);
517
518 /**
519 * Called by checkAccept() in order to save the history of the combobox
520 */
521 void saveComboboxHistory();
522
523 /**
524 * Process the choices made by the user, and return true if everything is OK.
525 * Called by KOpenWithDialog::accept(), i.e. when clicking on OK or typing Return.
526 */
527 bool checkAccept();
528
529 // slots
530 void slotDbClick();
531 void slotFileSelected();
532 void discoverButtonClicked();
533
534 bool saveNewApps;
535 bool m_terminaldirty;
536 KService::Ptr curService;
537 KApplicationView *view;
538 KUrlRequester *edit;
539 QString m_command;
540 QLabel *label;
541 QString qMimeType;
542 QString qMimeTypeComment;
543 KCollapsibleGroupBox *dialogExtension;
544 QCheckBox *terminal;
545 QCheckBox *remember;
546 QCheckBox *nocloseonexit;
547 KService::Ptr m_pService;
548 QDialogButtonBox *buttonBox;
549};
550
551KOpenWithDialog::KOpenWithDialog(const QList<QUrl> &_urls, QWidget *parent)
552 : QDialog(parent)
553 , d(new KOpenWithDialogPrivate(this))
554{
555 setObjectName(QStringLiteral("openwith"));
556 setModal(true);
557 setWindowTitle(i18n("Open With"));
558
559 QString text;
560 if (_urls.count() == 1) {
561 text = i18n(
562 "<qt>Select the program that should be used to open <b>%1</b>. "
563 "If the program is not listed, enter the name or click "
564 "the browse button.</qt>",
565 _urls.first().fileName().toHtmlEscaped());
566 } else
567 // Should never happen ??
568 {
569 text = i18n("Choose the name of the program with which to open the selected files.");
570 }
571 d->setMimeTypeFromUrls(_urls);
572 d->init(text, value: QString());
573}
574
575KOpenWithDialog::KOpenWithDialog(const QList<QUrl> &_urls, const QString &_text, const QString &_value, QWidget *parent)
576 : KOpenWithDialog(_urls, QString(), _text, _value, parent)
577{
578}
579
580KOpenWithDialog::KOpenWithDialog(const QList<QUrl> &_urls, const QString &mimeType, const QString &_text, const QString &_value, QWidget *parent)
581 : QDialog(parent)
582 , d(new KOpenWithDialogPrivate(this))
583{
584 setObjectName(QStringLiteral("openwith"));
585 setModal(true);
586 QString text = _text;
587 if (text.isEmpty() && !_urls.isEmpty()) {
588 if (_urls.count() == 1) {
589 const QString fileName = KStringHandler::csqueeze(str: _urls.first().fileName());
590 text = i18n("<qt>Select the program you want to use to open the file<br/>%1</qt>", fileName.toHtmlEscaped());
591 } else {
592 text = i18np("<qt>Select the program you want to use to open the file.</qt>",
593 "<qt>Select the program you want to use to open the %1 files.</qt>",
594 _urls.count());
595 }
596 }
597 setWindowTitle(i18n("Choose Application"));
598 if (mimeType.isEmpty()) {
599 d->setMimeTypeFromUrls(_urls);
600 } else {
601 d->setMimeType(mimeType);
602 }
603 d->init(text, value: _value);
604}
605
606KOpenWithDialog::KOpenWithDialog(const QString &mimeType, const QString &value, QWidget *parent)
607 : QDialog(parent)
608 , d(new KOpenWithDialogPrivate(this))
609{
610 setObjectName(QStringLiteral("openwith"));
611 setModal(true);
612 setWindowTitle(i18n("Choose Application for %1", mimeType));
613 QString text = i18n(
614 "<qt>Select the program for the file type: <b>%1</b>. "
615 "If the program is not listed, enter the name or click "
616 "the browse button.</qt>",
617 mimeType);
618 d->setMimeType(mimeType);
619 d->init(text, value);
620}
621
622KOpenWithDialog::KOpenWithDialog(QWidget *parent)
623 : QDialog(parent)
624 , d(new KOpenWithDialogPrivate(this))
625{
626 setObjectName(QStringLiteral("openwith"));
627 setModal(true);
628 setWindowTitle(i18n("Choose Application"));
629 QString text = i18n(
630 "<qt>Select a program. "
631 "If the program is not listed, enter the name or click "
632 "the browse button.</qt>");
633 d->qMimeType.clear();
634 d->init(text, value: QString());
635}
636
637void KOpenWithDialogPrivate::setMimeTypeFromUrls(const QList<QUrl> &_urls)
638{
639 if (_urls.count() == 1) {
640 QMimeDatabase db;
641 QMimeType mime = db.mimeTypeForUrl(url: _urls.first());
642 qMimeType = mime.name();
643 if (mime.isDefault()) {
644 qMimeType.clear();
645 } else {
646 qMimeTypeComment = mime.comment();
647 }
648 } else {
649 qMimeType.clear();
650 }
651}
652
653void KOpenWithDialogPrivate::setMimeType(const QString &mimeType)
654{
655 qMimeType = mimeType;
656 QMimeDatabase db;
657 qMimeTypeComment = db.mimeTypeForName(nameOrAlias: mimeType).comment();
658}
659
660void KOpenWithDialogPrivate::init(const QString &_text, const QString &_value)
661{
662 bool bReadOnly = !KAuthorized::authorize(action: KAuthorized::SHELL_ACCESS);
663 m_terminaldirty = false;
664 view = nullptr;
665 m_pService = nullptr;
666 curService = nullptr;
667
668 QBoxLayout *topLayout = new QVBoxLayout(q);
669 label = new QLabel(_text, q);
670 label->setWordWrap(true);
671 topLayout->addWidget(label);
672
673 if (!bReadOnly) {
674 // init the history combo and insert it into the URL-Requester
675 KHistoryComboBox *combo = new KHistoryComboBox();
676 combo->setToolTip(i18n("Type to filter the applications below, or specify the name of a command.\nPress down arrow to navigate the results."));
677 KLineEdit *lineEdit = new KLineEdit(q);
678 lineEdit->setClearButtonEnabled(true);
679 combo->setLineEdit(lineEdit);
680 combo->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLengthWithIcon);
681 combo->setDuplicatesEnabled(false);
682 KConfigGroup cg(KSharedConfig::openStateConfig(), QStringLiteral("Open-with settings"));
683 int max = cg.readEntry(key: "Maximum history", defaultValue: 15);
684 combo->setMaxCount(max);
685 int mode = cg.readEntry(key: "CompletionMode", defaultValue: int(KCompletion::CompletionNone));
686 combo->setCompletionMode(static_cast<KCompletion::CompletionMode>(mode));
687 const QStringList list = cg.readEntry(key: "History", aDefault: QStringList());
688 combo->setHistoryItems(items: list, setCompletionList: true);
689 edit = new KUrlRequester(combo, q);
690 edit->installEventFilter(filterObj: q);
691 } else {
692 edit = new KUrlRequester(q);
693 edit->lineEdit()->setReadOnly(true);
694 edit->button()->hide();
695 }
696
697 edit->setText(_value);
698 edit->setWhatsThis(
699 i18n("Following the command, you can have several place holders which will be replaced "
700 "with the actual values when the actual program is run:\n"
701 "%f - a single file name\n"
702 "%F - a list of files; use for applications that can open several local files at once\n"
703 "%u - a single URL\n"
704 "%U - a list of URLs\n"
705 "%d - the directory of the file to open\n"
706 "%D - a list of directories\n"
707 "%i - the icon\n"
708 "%m - the mini-icon\n"
709 "%c - the comment"));
710
711 topLayout->addWidget(edit);
712
713 if (edit->comboBox()) {
714 KUrlCompletion *comp = new KUrlCompletion(KUrlCompletion::ExeCompletion);
715 edit->comboBox()->setCompletionObject(completionObject: comp);
716 edit->comboBox()->setAutoDeleteCompletionObject(true);
717 }
718
719 QObject::connect(sender: edit, signal: &KUrlRequester::textChanged, context: q, slot: &KOpenWithDialog::slotTextChanged);
720 QObject::connect(sender: edit, signal: &KUrlRequester::urlSelected, context: q, slot: [this]() {
721 slotFileSelected();
722 });
723
724 view = new KApplicationView(q);
725 QTreeViewProxyFilter *proxyModel = new QTreeViewProxyFilter(view);
726 KApplicationModel *appModel = new KApplicationModel(proxyModel);
727 proxyModel->setSourceModel(appModel);
728 proxyModel->setFilterKeyColumn(0);
729 proxyModel->setRecursiveFilteringEnabled(true);
730 view->setModels(model: appModel, proxyModel);
731 topLayout->addWidget(view);
732 topLayout->setStretchFactor(w: view, stretch: 1);
733
734 QObject::connect(sender: view, signal: &KApplicationView::selected, context: q, slot: &KOpenWithDialog::slotSelected);
735 QObject::connect(sender: view, signal: &KApplicationView::highlighted, context: q, slot: &KOpenWithDialog::slotHighlighted);
736 QObject::connect(sender: view, signal: &KApplicationView::doubleClicked, context: q, slot: [this]() {
737 slotDbClick();
738 });
739
740 if (!qMimeType.isNull()) {
741 if (!qMimeTypeComment.isEmpty()) {
742 remember = new QCheckBox(i18n("&Remember application association for all files of type\n\"%1\" (%2)", qMimeTypeComment, qMimeType));
743 } else {
744 remember = new QCheckBox(i18n("&Remember application association for all files of type\n\"%1\"", qMimeType));
745 }
746
747 topLayout->addWidget(remember);
748 } else {
749 remember = nullptr;
750 }
751
752 // Advanced options
753 dialogExtension = new KCollapsibleGroupBox(q);
754 dialogExtension->setTitle(i18n("Terminal options"));
755
756 QVBoxLayout *dialogExtensionLayout = new QVBoxLayout(dialogExtension);
757 dialogExtensionLayout->setContentsMargins(left: 0, top: 0, right: 0, bottom: 0);
758
759 terminal = new QCheckBox(i18n("Run in &terminal"), q);
760 if (bReadOnly) {
761 terminal->hide();
762 }
763 QObject::connect(sender: terminal, signal: &QAbstractButton::toggled, context: q, slot: &KOpenWithDialog::slotTerminalToggled);
764
765 dialogExtensionLayout->addWidget(terminal);
766
767 QStyleOptionButton checkBoxOption;
768 checkBoxOption.initFrom(w: terminal);
769 int checkBoxIndentation = terminal->style()->pixelMetric(metric: QStyle::PM_IndicatorWidth, option: &checkBoxOption, widget: terminal);
770 checkBoxIndentation += terminal->style()->pixelMetric(metric: QStyle::PM_CheckBoxLabelSpacing, option: &checkBoxOption, widget: terminal);
771
772 QBoxLayout *nocloseonexitLayout = new QHBoxLayout();
773 nocloseonexitLayout->setContentsMargins(left: 0, top: 0, right: 0, bottom: 0);
774 QSpacerItem *spacer = new QSpacerItem(checkBoxIndentation, 0, QSizePolicy::Fixed, QSizePolicy::Minimum);
775 nocloseonexitLayout->addItem(spacer);
776
777 nocloseonexit = new QCheckBox(i18n("&Do not close when command exits"), q);
778 nocloseonexit->setChecked(false);
779 nocloseonexit->setDisabled(true);
780
781 // check to see if we use konsole if not disable the nocloseonexit
782 // because we don't know how to do this on other terminal applications
783 KConfigGroup confGroup(KSharedConfig::openConfig(), QStringLiteral("General"));
784 QString preferredTerminal = confGroup.readPathEntry(key: "TerminalApplication", QStringLiteral("konsole"));
785
786 if (bReadOnly || preferredTerminal != QLatin1String("konsole")) {
787 nocloseonexit->hide();
788 }
789
790 nocloseonexitLayout->addWidget(nocloseonexit);
791 dialogExtensionLayout->addLayout(layout: nocloseonexitLayout);
792
793 topLayout->addWidget(dialogExtension);
794
795 if (!qMimeType.isNull() && KService::serviceByDesktopName(QStringLiteral("org.kde.discover"))) {
796 QPushButton *discoverButton = new QPushButton(QIcon::fromTheme(QStringLiteral("plasmadiscover")), i18n("Get more Apps from Discover"));
797 QObject::connect(sender: discoverButton, signal: &QPushButton::clicked, context: q, slot: [this]() {
798 discoverButtonClicked();
799 });
800 topLayout->addWidget(discoverButton);
801 }
802
803 buttonBox = new QDialogButtonBox(q);
804 buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
805 q->connect(sender: buttonBox, signal: &QDialogButtonBox::accepted, context: q, slot: &QDialog::accept);
806 q->connect(sender: buttonBox, signal: &QDialogButtonBox::rejected, context: q, slot: &QDialog::reject);
807 topLayout->addWidget(buttonBox);
808
809 q->setMinimumSize(q->minimumSizeHint());
810 // edit->setText( _value );
811 // The resize is what caused "can't click on items before clicking on Name header" in previous versions.
812 // Probably due to the resizeEvent handler using width().
813 q->resize(w: q->minimumWidth(), h: 0.6 * q->screen()->availableGeometry().height());
814 edit->setFocus();
815 q->slotTextChanged();
816}
817
818void KOpenWithDialogPrivate::discoverButtonClicked()
819{
820 KIO::CommandLauncherJob *job = new KIO::CommandLauncherJob(QStringLiteral("plasma-discover"), {QStringLiteral("--mime"), qMimeType});
821 job->setDesktopName(QStringLiteral("org.kde.discover"));
822 job->start();
823}
824
825// ----------------------------------------------------------------------
826
827KOpenWithDialog::~KOpenWithDialog()
828{
829 d->edit->removeEventFilter(obj: this);
830};
831
832// ----------------------------------------------------------------------
833
834void KOpenWithDialog::slotSelected(const QString & /*_name*/, const QString &_exec)
835{
836 d->buttonBox->button(which: QDialogButtonBox::Ok)->setEnabled(!_exec.isEmpty());
837}
838
839// ----------------------------------------------------------------------
840
841void KOpenWithDialog::slotHighlighted(const QString &entryPath, const QString &)
842{
843 d->curService = KService::serviceByDesktopPath(path: entryPath);
844 if (d->curService && !d->m_terminaldirty) {
845 // ### indicate that default value was restored
846 d->terminal->setChecked(d->curService->terminal());
847 QString terminalOptions = d->curService->terminalOptions();
848 d->nocloseonexit->setChecked((terminalOptions.contains(s: QLatin1String("--noclose"))));
849 d->m_terminaldirty = false; // slotTerminalToggled changed it
850 }
851}
852
853// ----------------------------------------------------------------------
854
855void KOpenWithDialog::slotTextChanged()
856{
857 // Forget about the service only when the selection is empty
858 // otherwise changing text but hitting the same result clears curService
859 bool selectionEmpty = !d->view->currentIndex().isValid();
860 if (d->curService && selectionEmpty) {
861 d->curService = nullptr;
862 }
863 d->buttonBox->button(which: QDialogButtonBox::Ok)->setEnabled(!d->edit->text().isEmpty() || d->curService);
864
865 // escape() because we want plain text matching; the matching is case-insensitive,
866 // see QTreeViewProxyFilter::filterAcceptsRow()
867 d->view->proxyModel()->setFilterRegularExpression(QRegularExpression::escape(str: d->edit->text()));
868
869 // Expand all the nodes when the search string is 3 characters long
870 // If the search string doesn't match anything there will be no nodes to expand
871 if (d->edit->text().size() > 2) {
872 d->view->expandAll();
873 QAbstractItemModel *model = d->view->model();
874 if (model->rowCount() == 1) { // Automatically select the result (first leaf node) if the
875 // filter has only one match
876 QModelIndex leafNodeIdx = model->index(row: 0, column: 0);
877 while (model->hasChildren(parent: leafNodeIdx)) {
878 leafNodeIdx = model->index(row: 0, column: 0, parent: leafNodeIdx);
879 }
880 d->view->setCurrentIndex(leafNodeIdx);
881 }
882 } else {
883 d->view->collapseAll();
884 d->view->setCurrentIndex(d->view->rootIndex()); // Unset and deselect all the elements
885 d->curService = nullptr;
886 }
887}
888
889// ----------------------------------------------------------------------
890
891void KOpenWithDialog::slotTerminalToggled(bool)
892{
893 // ### indicate that default value was overridden
894 d->m_terminaldirty = true;
895 d->nocloseonexit->setDisabled(!d->terminal->isChecked());
896}
897
898// ----------------------------------------------------------------------
899
900void KOpenWithDialogPrivate::slotDbClick()
901{
902 // check if a directory is selected
903 if (view->isDirSel()) {
904 return;
905 }
906 q->accept();
907}
908
909void KOpenWithDialogPrivate::slotFileSelected()
910{
911 // quote the path to avoid unescaped whitespace, backslashes, etc.
912 edit->setText(KShell::quoteArg(arg: edit->text()));
913}
914
915void KOpenWithDialog::setSaveNewApplications(bool b)
916{
917 d->saveNewApps = b;
918}
919
920bool KOpenWithDialogPrivate::checkAccept()
921{
922 auto result = KIO::OpenWith::accept(service&: curService,
923 typedExec: edit->text(),
924 remember: remember && remember->isChecked(),
925 mimeType: qMimeType,
926 openInTerminal: terminal->isChecked(),
927 lingerTerminal: nocloseonexit->isChecked(),
928 saveNewApps);
929 m_pService = curService;
930
931 if (!result.accept) {
932 KMessageBox::error(parent: q, text: result.error);
933 return false;
934 }
935
936#ifndef KIO_ANDROID_STUB
937 if (result.rebuildSycoca) {
938 KBuildSycocaProgressDialog::rebuildKSycoca(parent: q);
939 }
940#endif
941
942 saveComboboxHistory();
943 return true;
944}
945
946bool KOpenWithDialog::eventFilter(QObject *object, QEvent *event)
947{
948 // Detect DownArrow to navigate the results in the QTreeView
949 if (object == d->edit && event->type() == QEvent::ShortcutOverride) {
950 QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
951 if (keyEvent->key() == Qt::Key_Down) {
952 KHistoryComboBox *combo = static_cast<KHistoryComboBox *>(d->edit->comboBox());
953 // FIXME: Disable arrow down in CompletionPopup and CompletionPopupAuto only when the dropdown list is shown.
954 // When popup completion mode is used the down arrow is used to navigate the dropdown list of results
955 if (combo->completionMode() != KCompletion::CompletionPopup && combo->completionMode() != KCompletion::CompletionPopupAuto) {
956 QModelIndex leafNodeIdx = d->view->model()->index(row: 0, column: 0);
957 // Check if we have at least one result or the focus is passed to the empty QTreeView
958 if (d->view->model()->hasChildren(parent: leafNodeIdx)) {
959 d->view->setFocus(Qt::OtherFocusReason);
960 QApplication::sendEvent(receiver: d->view, event: keyEvent);
961 return true;
962 }
963 }
964 }
965 }
966 return QDialog::eventFilter(object, event);
967}
968
969void KOpenWithDialog::accept()
970{
971 if (d->checkAccept()) {
972 QDialog::accept();
973 }
974}
975
976QString KOpenWithDialog::text() const
977{
978 if (!d->m_command.isEmpty()) {
979 return d->m_command;
980 } else {
981 return d->edit->text();
982 }
983}
984
985void KOpenWithDialog::hideNoCloseOnExit()
986{
987 // uncheck the checkbox because the value could be used when "Run in Terminal" is selected
988 d->nocloseonexit->setChecked(false);
989 d->nocloseonexit->hide();
990
991 d->dialogExtension->setVisible(d->nocloseonexit->isVisible() || d->terminal->isVisible());
992}
993
994void KOpenWithDialog::hideRunInTerminal()
995{
996 d->terminal->hide();
997 hideNoCloseOnExit();
998}
999
1000KService::Ptr KOpenWithDialog::service() const
1001{
1002 return d->m_pService;
1003}
1004
1005void KOpenWithDialogPrivate::saveComboboxHistory()
1006{
1007 KHistoryComboBox *combo = static_cast<KHistoryComboBox *>(edit->comboBox());
1008 if (combo) {
1009 combo->addToHistory(item: edit->text());
1010
1011 KConfigGroup cg(KSharedConfig::openStateConfig(), QStringLiteral("Open-with settings"));
1012 cg.writeEntry(key: "History", value: combo->historyItems());
1013 writeEntry(group&: cg, key: "CompletionMode", aValue: combo->completionMode());
1014 // don't store the completion-list, as it contains all of KUrlCompletion's
1015 // executables
1016 cg.sync();
1017 }
1018}
1019
1020#include "moc_kopenwithdialog.cpp"
1021#include "moc_kopenwithdialog_p.cpp"
1022

source code of kio/src/widgets/kopenwithdialog.cpp