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 | |
56 | inline void |
57 | writeEntry(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 | |
62 | namespace KDEPrivate |
63 | { |
64 | class AppNode |
65 | { |
66 | public: |
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 | |
93 | static 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 | |
112 | class KApplicationModelPrivate |
113 | { |
114 | public: |
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 | |
132 | void 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 | |
193 | KApplicationModel::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 | |
204 | KApplicationModel::~KApplicationModel() = default; |
205 | |
206 | bool 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 | |
216 | int KApplicationModel::columnCount(const QModelIndex &parent) const |
217 | { |
218 | Q_UNUSED(parent) |
219 | return 1; |
220 | } |
221 | |
222 | QVariant 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 | |
248 | void 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 | |
265 | void 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 | |
281 | bool 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 | |
291 | QVariant KApplicationModel::(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 | |
305 | QModelIndex 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 | |
323 | QModelIndex 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 | |
343 | int 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 | |
353 | QString 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 | |
363 | QString 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 | |
373 | bool 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 | |
383 | QTreeViewProxyFilter::QTreeViewProxyFilter(QObject *parent) |
384 | : QSortFilterProxyModel(parent) |
385 | { |
386 | } |
387 | |
388 | bool 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 | |
401 | class KApplicationViewPrivate |
402 | { |
403 | public: |
404 | KApplicationViewPrivate() |
405 | : appModel(nullptr) |
406 | , m_proxyModel(nullptr) |
407 | { |
408 | } |
409 | |
410 | KApplicationModel *appModel; |
411 | QSortFilterProxyModel *m_proxyModel; |
412 | }; |
413 | |
414 | KApplicationView::KApplicationView(QWidget *parent) |
415 | : QTreeView(parent) |
416 | , d(new KApplicationViewPrivate) |
417 | { |
418 | setHeaderHidden(true); |
419 | } |
420 | |
421 | KApplicationView::~KApplicationView() = default; |
422 | |
423 | void 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 | |
438 | QSortFilterProxyModel *KApplicationView::proxyModel() |
439 | { |
440 | return d->m_proxyModel; |
441 | } |
442 | |
443 | bool 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 | |
453 | void KApplicationView::currentChanged(const QModelIndex ¤t, 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 | |
472 | void 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 | ***************************************************************/ |
490 | class KOpenWithDialogPrivate |
491 | { |
492 | public: |
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 ; |
543 | KCollapsibleGroupBox *dialogExtension; |
544 | QCheckBox *terminal; |
545 | QCheckBox *remember; |
546 | QCheckBox *nocloseonexit; |
547 | KService::Ptr m_pService; |
548 | QDialogButtonBox *buttonBox; |
549 | }; |
550 | |
551 | KOpenWithDialog::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 | |
575 | KOpenWithDialog::KOpenWithDialog(const QList<QUrl> &_urls, const QString &_text, const QString &_value, QWidget *parent) |
576 | : KOpenWithDialog(_urls, QString(), _text, _value, parent) |
577 | { |
578 | } |
579 | |
580 | KOpenWithDialog::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 | |
606 | KOpenWithDialog::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 | |
622 | KOpenWithDialog::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 | |
637 | void 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 | |
653 | void KOpenWithDialogPrivate::setMimeType(const QString &mimeType) |
654 | { |
655 | qMimeType = mimeType; |
656 | QMimeDatabase db; |
657 | qMimeTypeComment = db.mimeTypeForName(nameOrAlias: mimeType).comment(); |
658 | } |
659 | |
660 | void 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 | |
818 | void 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 | |
827 | KOpenWithDialog::~KOpenWithDialog() |
828 | { |
829 | d->edit->removeEventFilter(obj: this); |
830 | }; |
831 | |
832 | // ---------------------------------------------------------------------- |
833 | |
834 | void KOpenWithDialog::slotSelected(const QString & /*_name*/, const QString &_exec) |
835 | { |
836 | d->buttonBox->button(which: QDialogButtonBox::Ok)->setEnabled(!_exec.isEmpty()); |
837 | } |
838 | |
839 | // ---------------------------------------------------------------------- |
840 | |
841 | void 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 | |
855 | void 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 | |
891 | void 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 | |
900 | void KOpenWithDialogPrivate::slotDbClick() |
901 | { |
902 | // check if a directory is selected |
903 | if (view->isDirSel()) { |
904 | return; |
905 | } |
906 | q->accept(); |
907 | } |
908 | |
909 | void KOpenWithDialogPrivate::slotFileSelected() |
910 | { |
911 | // quote the path to avoid unescaped whitespace, backslashes, etc. |
912 | edit->setText(KShell::quoteArg(arg: edit->text())); |
913 | } |
914 | |
915 | void KOpenWithDialog::setSaveNewApplications(bool b) |
916 | { |
917 | d->saveNewApps = b; |
918 | } |
919 | |
920 | bool 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 | |
946 | bool 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 | |
969 | void KOpenWithDialog::accept() |
970 | { |
971 | if (d->checkAccept()) { |
972 | QDialog::accept(); |
973 | } |
974 | } |
975 | |
976 | QString 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 | |
985 | void 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 | |
994 | void KOpenWithDialog::hideRunInTerminal() |
995 | { |
996 | d->terminal->hide(); |
997 | hideNoCloseOnExit(); |
998 | } |
999 | |
1000 | KService::Ptr KOpenWithDialog::service() const |
1001 | { |
1002 | return d->m_pService; |
1003 | } |
1004 | |
1005 | void 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 | |