KIO

kopenwithdialog.cpp
1 /*
2  This file is part of the KDE libraries
3  SPDX-FileCopyrightText: 1997 Torben Weis <[email protected]>
4  SPDX-FileCopyrightText: 1999 Dirk Mueller <[email protected]>
5  Portions SPDX-FileCopyrightText: 1999 Preston Brown <[email protected]>
6  SPDX-FileCopyrightText: 2007 Pino Toscano <[email protected]>
7 
8  SPDX-License-Identifier: LGPL-2.0-or-later
9 */
10 
11 #include "kopenwithdialog.h"
12 #include "kopenwithdialog_p.h"
13 #include "kio_widgets_debug.h"
14 
15 #include <QApplication>
16 #include <QDesktopWidget>
17 #include <QDialogButtonBox>
18 #include <QtAlgorithms>
19 #include <QList>
20 #include <QKeyEvent>
21 #include <QLabel>
22 #include <QLayout>
23 #include <QCheckBox>
24 #include <QStyle>
25 #include <QStyleOptionButton>
26 #include <QStandardPaths>
27 #include <QMimeDatabase>
28 #include <QScreen>
29 
30 #include <kurlauthorized.h>
31 #include <KHistoryComboBox>
32 #include <KDesktopFile>
33 #include <KLineEdit>
34 #include <KSharedConfig>
35 #include <KLocalizedString>
36 #include <KMessageBox>
37 #include <KShell>
38 #include <kio/desktopexecparser.h>
39 #include <KStringHandler>
40 #include <kurlcompletion.h>
41 #include <kurlrequester.h>
42 #include <KServiceGroup>
43 #include <KCollapsibleGroupBox>
44 #include <QDebug>
45 
46 #include <assert.h>
47 #include <stdlib.h>
48 #include <kbuildsycocaprogressdialog.h>
49 #include <KConfigGroup>
50 
51 inline void writeEntry(KConfigGroup &group, const char *key,
52  const KCompletion::CompletionMode &aValue,
54 {
55  group.writeEntry(key, int(aValue), flags);
56 }
57 
58 namespace KDEPrivate
59 {
60 
61 class AppNode
62 {
63 public:
64  AppNode()
65  : isDir(false), parent(nullptr), fetched(false)
66  {
67  }
68  ~AppNode()
69  {
70  qDeleteAll(children);
71  }
72  AppNode(const AppNode &) = delete;
73  AppNode &operator=(const AppNode &) = delete;
74 
75  QString icon;
76  QString text;
77  QString tooltip;
78  QString entryPath;
79  QString exec;
80  bool isDir;
81 
82  AppNode *parent;
83  bool fetched;
84 
85  QList<AppNode *> children;
86 };
87 
88 static bool AppNodeLessThan(KDEPrivate::AppNode *n1, KDEPrivate::AppNode *n2)
89 {
90  if (n1->isDir) {
91  if (n2->isDir) {
92  return n1->text.compare(n2->text, Qt::CaseInsensitive) < 0;
93  } else {
94  return true;
95  }
96  } else {
97  if (n2->isDir) {
98  return false;
99  } else {
100  return n1->text.compare(n2->text, Qt::CaseInsensitive) < 0;
101  }
102  }
103 }
104 
105 }
106 
107 class KApplicationModelPrivate
108 {
109 public:
110  explicit KApplicationModelPrivate(KApplicationModel *qq)
111  : q(qq), root(new KDEPrivate::AppNode())
112  {
113  }
114  ~KApplicationModelPrivate()
115  {
116  delete root;
117  }
118 
119  void fillNode(const QString &entryPath, KDEPrivate::AppNode *node);
120 
121  KApplicationModel * const q;
122 
123  KDEPrivate::AppNode *root;
124 };
125 
126 void KApplicationModelPrivate::fillNode(const QString &_entryPath, KDEPrivate::AppNode *node)
127 {
128  KServiceGroup::Ptr root = KServiceGroup::group(_entryPath);
129  if (!root || !root->isValid()) {
130  return;
131  }
132 
133  const KServiceGroup::List list = root->entries();
134 
135  for (const KSycocaEntry::Ptr &p : list) {
136  QString icon;
137  QString text;
138  QString tooltip;
139  QString entryPath;
140  QString exec;
141  bool isDir = false;
142  if (p->isType(KST_KService)) {
143  const KService::Ptr service(static_cast<KService*>(p.data()));
144 
145  if (service->noDisplay()) {
146  continue;
147  }
148 
149  icon = service->icon();
150  text = service->name();
151 
152  // no point adding a tooltip that only repeats service->name()
153  const QString generic = service->genericName();
154  tooltip = generic != text ? generic : QString();
155 
156  exec = service->exec();
157  entryPath = service->entryPath();
158  } else if (p->isType(KST_KServiceGroup)) {
159  const KServiceGroup::Ptr serviceGroup(static_cast<KServiceGroup*>(p.data()));
160 
161  if (serviceGroup->noDisplay() || serviceGroup->childCount() == 0) {
162  continue;
163  }
164 
165  icon = serviceGroup->icon();
166  text = serviceGroup->caption();
167  entryPath = serviceGroup->entryPath();
168  isDir = true;
169  } else {
170  qCWarning(KIO_WIDGETS) << "KServiceGroup: Unexpected object in list!";
171  continue;
172  }
173 
174  KDEPrivate::AppNode *newnode = new KDEPrivate::AppNode();
175  newnode->icon = icon;
176  newnode->text = text;
177  newnode->tooltip = tooltip;
178  newnode->entryPath = entryPath;
179  newnode->exec = exec;
180  newnode->isDir = isDir;
181  newnode->parent = node;
182  node->children.append(newnode);
183  }
184  std::stable_sort(node->children.begin(), node->children.end(), KDEPrivate::AppNodeLessThan);
185 }
186 
187 KApplicationModel::KApplicationModel(QObject *parent)
188  : QAbstractItemModel(parent), d(new KApplicationModelPrivate(this))
189 {
190  d->fillNode(QString(), d->root);
191  const int nRows = rowCount();
192  for (int i = 0; i < nRows; i++) {
193  fetchAll(index(i, 0));
194  }
195 }
196 
197 KApplicationModel::~KApplicationModel()
198 {
199  delete d;
200 }
201 
202 bool KApplicationModel::canFetchMore(const QModelIndex &parent) const
203 {
204  if (!parent.isValid()) {
205  return false;
206  }
207 
208  KDEPrivate::AppNode *node = static_cast<KDEPrivate::AppNode *>(parent.internalPointer());
209  return node->isDir && !node->fetched;
210 }
211 
212 int KApplicationModel::columnCount(const QModelIndex &parent) const
213 {
214  Q_UNUSED(parent)
215  return 1;
216 }
217 
218 QVariant KApplicationModel::data(const QModelIndex &index, int role) const
219 {
220  if (!index.isValid()) {
221  return QVariant();
222  }
223 
224  KDEPrivate::AppNode *node = static_cast<KDEPrivate::AppNode *>(index.internalPointer());
225 
226  switch (role) {
227  case Qt::DisplayRole:
228  return node->text;
229  case Qt::DecorationRole:
230  if (!node->icon.isEmpty()) {
231  return QIcon::fromTheme(node->icon);
232  }
233  break;
234  case Qt::ToolTipRole:
235  if (!node->tooltip.isEmpty()) {
236  return node->tooltip;
237  }
238  break;
239  default:
240  ;
241  }
242  return QVariant();
243 }
244 
245 void KApplicationModel::fetchMore(const QModelIndex &parent)
246 {
247  if (!parent.isValid()) {
248  return;
249  }
250 
251  KDEPrivate::AppNode *node = static_cast<KDEPrivate::AppNode *>(parent.internalPointer());
252  if (!node->isDir) {
253  return;
254  }
255 
256  emit layoutAboutToBeChanged();
257  d->fillNode(node->entryPath, node);
258  node->fetched = true;
259  emit layoutChanged();
260 }
261 
262 void KApplicationModel::fetchAll(const QModelIndex &parent)
263 {
264  if (!parent.isValid() || !canFetchMore(parent)) {
265  return;
266  }
267 
268  fetchMore(parent);
269 
270  int childCount = rowCount(parent);
271  for (int i = 0; i < childCount; i++) {
272  const QModelIndex &child = index(i, 0, parent);
273  // Recursively call the function for each child node.
274  fetchAll(child);
275  }
276 }
277 
278 bool KApplicationModel::hasChildren(const QModelIndex &parent) const
279 {
280  if (!parent.isValid()) {
281  return true;
282  }
283 
284  KDEPrivate::AppNode *node = static_cast<KDEPrivate::AppNode *>(parent.internalPointer());
285  return node->isDir;
286 }
287 
288 QVariant KApplicationModel::headerData(int section, Qt::Orientation orientation, int role) const
289 {
290  if (orientation != Qt::Horizontal || section != 0) {
291  return QVariant();
292  }
293 
294  switch (role) {
295  case Qt::DisplayRole:
296  return i18n("Known Applications");
297  default:
298  return QVariant();
299  }
300 }
301 
302 QModelIndex KApplicationModel::index(int row, int column, const QModelIndex &parent) const
303 {
304  if (row < 0 || column != 0) {
305  return QModelIndex();
306  }
307 
308  KDEPrivate::AppNode *node = d->root;
309  if (parent.isValid()) {
310  node = static_cast<KDEPrivate::AppNode *>(parent.internalPointer());
311  }
312 
313  if (row >= node->children.count()) {
314  return QModelIndex();
315  } else {
316  return createIndex(row, 0, node->children.at(row));
317  }
318 }
319 
320 QModelIndex KApplicationModel::parent(const QModelIndex &index) const
321 {
322  if (!index.isValid()) {
323  return QModelIndex();
324  }
325 
326  KDEPrivate::AppNode *node = static_cast<KDEPrivate::AppNode *>(index.internalPointer());
327  if (node->parent->parent) {
328  int id = node->parent->parent->children.indexOf(node->parent);
329 
330  if (id >= 0 && id < node->parent->parent->children.count()) {
331  return createIndex(id, 0, node->parent);
332  } else {
333  return QModelIndex();
334  }
335  } else {
336  return QModelIndex();
337  }
338 }
339 
340 int KApplicationModel::rowCount(const QModelIndex &parent) const
341 {
342  if (!parent.isValid()) {
343  return d->root->children.count();
344  }
345 
346  KDEPrivate::AppNode *node = static_cast<KDEPrivate::AppNode *>(parent.internalPointer());
347  return node->children.count();
348 }
349 
350 QString KApplicationModel::entryPathFor(const QModelIndex &index) const
351 {
352  if (!index.isValid()) {
353  return QString();
354  }
355 
356  KDEPrivate::AppNode *node = static_cast<KDEPrivate::AppNode *>(index.internalPointer());
357  return node->entryPath;
358 }
359 
360 QString KApplicationModel::execFor(const QModelIndex &index) const
361 {
362  if (!index.isValid()) {
363  return QString();
364  }
365 
366  KDEPrivate::AppNode *node = static_cast<KDEPrivate::AppNode *>(index.internalPointer());
367  return node->exec;
368 }
369 
370 bool KApplicationModel::isDirectory(const QModelIndex &index) const
371 {
372  if (!index.isValid()) {
373  return false;
374  }
375 
376  KDEPrivate::AppNode *node = static_cast<KDEPrivate::AppNode *>(index.internalPointer());
377  return node->isDir;
378 }
379 
380 
381 QTreeViewProxyFilter::QTreeViewProxyFilter(QObject *parent)
382  : QSortFilterProxyModel(parent)
383 {
384 }
385 
386 bool QTreeViewProxyFilter::filterAcceptsRow(int sourceRow, const QModelIndex &parent) const
387 {
388  QModelIndex index = sourceModel()->index(sourceRow, 0, parent);
389 
390  if (!index.isValid()) {
391  return false;
392  }
393 
394  // Match the regexp only on leaf nodes
395  if (!sourceModel()->hasChildren(index) && index.data().toString().contains(filterRegExp())) {
396  return true;
397  }
398 
399  return false;
400 }
401 
402 class KApplicationViewPrivate
403 {
404 public:
405  KApplicationViewPrivate()
406  : appModel(nullptr),
407  m_proxyModel(nullptr)
408  {
409  }
410 
411  KApplicationModel *appModel;
412  QSortFilterProxyModel *m_proxyModel;
413 };
414 
415 KApplicationView::KApplicationView(QWidget *parent)
416  : QTreeView(parent), d(new KApplicationViewPrivate)
417 {
418  setHeaderHidden(true);
419 }
420 
421 KApplicationView::~KApplicationView()
422 {
423  delete d;
424 }
425 
426 void KApplicationView::setModels(KApplicationModel *model, QSortFilterProxyModel *proxyModel)
427 {
428  if (d->appModel) {
429  disconnect(selectionModel(), &QItemSelectionModel::selectionChanged,
430  this, &KApplicationView::slotSelectionChanged);
431  }
432 
433  QTreeView::setModel(proxyModel); // Here we set the proxy model
434  d->m_proxyModel = proxyModel; // Also store it in a member property to avoid many casts later
435 
436  d->appModel = model;
437  if (d->appModel) {
438  connect(selectionModel(), &QItemSelectionModel::selectionChanged,
439  this, &KApplicationView::slotSelectionChanged);
440  }
441 }
442 
443 QSortFilterProxyModel* KApplicationView::proxyModel()
444 {
445  return d->m_proxyModel;
446 }
447 
448 bool KApplicationView::isDirSel() const
449 {
450  if (d->appModel) {
451  QModelIndex index = selectionModel()->currentIndex();
452  index = d->m_proxyModel->mapToSource(index);
453  return d->appModel->isDirectory(index);
454  }
455  return false;
456 }
457 
458 void KApplicationView::currentChanged(const QModelIndex &current, const QModelIndex &previous)
459 {
460  QTreeView::currentChanged(current, previous);
461 
462  if (d->appModel) {
463  QModelIndex sourceCurrent = d->m_proxyModel->mapToSource(current);
464  if(!d->appModel->isDirectory(sourceCurrent)) {
465  QString exec = d->appModel->execFor(sourceCurrent);
466  if (!exec.isEmpty()) {
467  emit highlighted(d->appModel->entryPathFor(sourceCurrent), exec);
468  }
469  }
470  }
471 }
472 
473 void KApplicationView::slotSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
474 {
475  Q_UNUSED(deselected)
476 
477  QItemSelection sourceSelected = d->m_proxyModel->mapSelectionToSource(selected);
478 
479  const QModelIndexList indexes = sourceSelected.indexes();
480  if (indexes.count() == 1) {
481  QString exec = d->appModel->execFor(indexes.at(0));
482  emit this->selected(d->appModel->entryPathFor(indexes.at(0)), exec);
483  }
484 }
485 
486 /***************************************************************
487  *
488  * KOpenWithDialog
489  *
490  ***************************************************************/
491 class KOpenWithDialogPrivate
492 {
493 public:
494  explicit KOpenWithDialogPrivate(KOpenWithDialog *qq)
495  : q(qq), saveNewApps(false)
496  {
497  }
498 
499  KOpenWithDialog * const q;
500 
504  void setMimeTypeFromUrls(const QList<QUrl> &_urls);
505 
506  void setMimeType(const QString &mimeType);
507 
508  void addToMimeAppsList(const QString &serviceId);
509 
516  void init(const QString &text, const QString &value);
517 
521  void saveComboboxHistory();
522 
527  bool checkAccept();
528 
529  // slots
530  void _k_slotDbClick();
531  void _k_slotFileSelected();
532 
533  bool saveNewApps;
534  bool m_terminaldirty;
535  KService::Ptr curService;
536  KApplicationView *view;
537  KUrlRequester *edit;
538  QString m_command;
539  QLabel *label;
540  QString qMimeType;
541  QString qMimeTypeComment;
542  KCollapsibleGroupBox *dialogExtension;
543  QCheckBox *terminal;
544  QCheckBox *remember;
545  QCheckBox *nocloseonexit;
546  KService::Ptr m_pService;
547  QDialogButtonBox *buttonBox;
548 };
549 
551  : QDialog(parent), d(new KOpenWithDialogPrivate(this))
552 {
553  setObjectName(QStringLiteral("openwith"));
554  setModal(true);
555  setWindowTitle(i18n("Open With"));
556 
557  QString text;
558  if (_urls.count() == 1) {
559  text = i18n("<qt>Select the program that should be used to open <b>%1</b>. "
560  "If the program is not listed, enter the name or click "
561  "the browse button.</qt>", _urls.first().fileName().toHtmlEscaped());
562  } else
563  // Should never happen ??
564  {
565  text = i18n("Choose the name of the program with which to open the selected files.");
566  }
567  d->setMimeTypeFromUrls(_urls);
568  d->init(text, QString());
569 }
570 
572  const QString &_value, QWidget *parent)
573  : KOpenWithDialog(_urls, QString(), _text, _value, parent)
574 {
575 }
576 
578  const QString &_text, const QString &_value,
579  QWidget *parent)
580  : QDialog(parent), d(new KOpenWithDialogPrivate(this))
581 {
582  setObjectName(QStringLiteral("openwith"));
583  setModal(true);
584  QString text = _text;
585  if (text.isEmpty() && !_urls.isEmpty()) {
586  if (_urls.count() == 1) {
587  const QString fileName = KStringHandler::csqueeze(_urls.first().fileName());
588  text = i18n("<qt>Select the program you want to use to open the file<br/>%1</qt>", fileName.toHtmlEscaped());
589  } else {
590  text = i18np("<qt>Select the program you want to use to open the file.</qt>",
591  "<qt>Select the program you want to use to open the %1 files.</qt>", _urls.count());
592  }
593  }
594  setWindowTitle(i18n("Choose Application"));
595  if (mimeType.isEmpty()) {
596  d->setMimeTypeFromUrls(_urls);
597  } else {
598  d->setMimeType(mimeType);
599  }
600  d->init(text, _value);
601 }
602 
603 KOpenWithDialog::KOpenWithDialog(const QString &mimeType, const QString &value,
604  QWidget *parent)
605  : QDialog(parent), d(new KOpenWithDialogPrivate(this))
606 {
607  setObjectName(QStringLiteral("openwith"));
608  setModal(true);
609  setWindowTitle(i18n("Choose Application for %1", mimeType));
610  QString text = i18n("<qt>Select the program for the file type: <b>%1</b>. "
611  "If the program is not listed, enter the name or click "
612  "the browse button.</qt>", mimeType);
613  d->setMimeType(mimeType);
614  d->init(text, value);
615 }
616 
618  : QDialog(parent), d(new KOpenWithDialogPrivate(this))
619 {
620  setObjectName(QStringLiteral("openwith"));
621  setModal(true);
622  setWindowTitle(i18n("Choose Application"));
623  QString text = i18n("<qt>Select a program. "
624  "If the program is not listed, enter the name or click "
625  "the browse button.</qt>");
626  d->qMimeType.clear();
627  d->init(text, QString());
628 }
629 
630 void KOpenWithDialogPrivate::setMimeTypeFromUrls(const QList<QUrl> &_urls)
631 {
632  if (_urls.count() == 1) {
633  QMimeDatabase db;
634  QMimeType mime = db.mimeTypeForUrl(_urls.first());
635  qMimeType = mime.name();
636  if (mime.isDefault()) {
637  qMimeType.clear();
638  } else {
639  qMimeTypeComment = mime.comment();
640  }
641  } else {
642  qMimeType.clear();
643  }
644 }
645 
646 void KOpenWithDialogPrivate::setMimeType(const QString &mimeType)
647 {
648  qMimeType = mimeType;
649  QMimeDatabase db;
650  qMimeTypeComment = db.mimeTypeForName(mimeType).comment();
651 }
652 
653 void KOpenWithDialogPrivate::init(const QString &_text, const QString &_value)
654 {
655  bool bReadOnly = !KAuthorized::authorize(QStringLiteral("shell_access"));
656  m_terminaldirty = false;
657  view = nullptr;
658  m_pService = nullptr;
659  curService = nullptr;
660 
661  QBoxLayout *topLayout = new QVBoxLayout;
662  q->setLayout(topLayout);
663  label = new QLabel(_text, q);
664  label->setWordWrap(true);
665  topLayout->addWidget(label);
666 
667  if (!bReadOnly) {
668  // init the history combo and insert it into the URL-Requester
669  KHistoryComboBox *combo = new KHistoryComboBox();
670  combo->setToolTip(i18n("Type to filter the applications below, or specify the name of a command.\nPress down arrow to navigate the results."));
671  KLineEdit *lineEdit = new KLineEdit(q);
672  lineEdit->setClearButtonEnabled(true);
673  combo->setLineEdit(lineEdit);
675  combo->setDuplicatesEnabled(false);
676  KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("Open-with settings"));
677  int max = cg.readEntry("Maximum history", 15);
678  combo->setMaxCount(max);
679  int mode = cg.readEntry("CompletionMode", int(KCompletion::CompletionNone));
680  combo->setCompletionMode(static_cast<KCompletion::CompletionMode>(mode));
681  const QStringList list = cg.readEntry("History", QStringList());
682  combo->setHistoryItems(list, true);
683  edit = new KUrlRequester(combo, q);
684  edit->installEventFilter(q);
685  } else {
686  edit = new KUrlRequester(q);
687  edit->lineEdit()->setReadOnly(true);
688  edit->button()->hide();
689  }
690 
691  edit->setText(_value);
692  edit->setWhatsThis(i18n(
693  "Following the command, you can have several place holders which will be replaced "
694  "with the actual values when the actual program is run:\n"
695  "%f - a single file name\n"
696  "%F - a list of files; use for applications that can open several local files at once\n"
697  "%u - a single URL\n"
698  "%U - a list of URLs\n"
699  "%d - the directory of the file to open\n"
700  "%D - a list of directories\n"
701  "%i - the icon\n"
702  "%m - the mini-icon\n"
703  "%c - the comment"));
704 
705  topLayout->addWidget(edit);
706 
707  if (edit->comboBox()) {
708  KUrlCompletion *comp = new KUrlCompletion(KUrlCompletion::ExeCompletion);
709  edit->comboBox()->setCompletionObject(comp);
710  edit->comboBox()->setAutoDeleteCompletionObject(true);
711  }
712 
713  QObject::connect(edit, &KUrlRequester::textChanged, q, &KOpenWithDialog::slotTextChanged);
714  QObject::connect(edit, SIGNAL(urlSelected(QUrl)), q, SLOT(_k_slotFileSelected()));
715 
716  view = new KApplicationView(q);
717  QTreeViewProxyFilter *proxyModel = new QTreeViewProxyFilter(view);
718  KApplicationModel *appModel = new KApplicationModel(proxyModel);
719  proxyModel->setSourceModel(appModel);
720  proxyModel->setFilterKeyColumn(0);
721  proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
722  proxyModel->setRecursiveFilteringEnabled(true);
723  view->setModels(appModel, proxyModel);
724  topLayout->addWidget(view);
725  topLayout->setStretchFactor(view, 1);
726 
727  QObject::connect(view, &KApplicationView::selected,
728  q, &KOpenWithDialog::slotSelected);
729  QObject::connect(view, &KApplicationView::highlighted,
730  q, &KOpenWithDialog::slotHighlighted);
731  QObject::connect(view, SIGNAL(doubleClicked(QModelIndex)),
732  q, SLOT(_k_slotDbClick()));
733 
734  if (!qMimeType.isNull()) {
735  remember = new QCheckBox(i18n("&Remember application association for all files of type\n\"%1\" (%2)", qMimeTypeComment, qMimeType));
736  // remember->setChecked(true);
737  topLayout->addWidget(remember);
738  } else {
739  remember = nullptr;
740  }
741 
742  //Advanced options
743  dialogExtension = new KCollapsibleGroupBox(q);
744  dialogExtension->setTitle(i18n("Terminal options"));
745 
746  QVBoxLayout *dialogExtensionLayout = new QVBoxLayout;
747  dialogExtensionLayout->setContentsMargins(0, 0, 0, 0);
748 
749  terminal = new QCheckBox(i18n("Run in &terminal"), q);
750  if (bReadOnly) {
751  terminal->hide();
752  }
753  QObject::connect(terminal, &QAbstractButton::toggled, q, &KOpenWithDialog::slotTerminalToggled);
754 
755  dialogExtensionLayout->addWidget(terminal);
756 
757  QStyleOptionButton checkBoxOption;
758  checkBoxOption.initFrom(terminal);
759  int checkBoxIndentation = terminal->style()->pixelMetric(QStyle::PM_IndicatorWidth, &checkBoxOption, terminal);
760  checkBoxIndentation += terminal->style()->pixelMetric(QStyle::PM_CheckBoxLabelSpacing, &checkBoxOption, terminal);
761 
762  QBoxLayout *nocloseonexitLayout = new QHBoxLayout();
763  nocloseonexitLayout->setContentsMargins(0, 0, 0, 0);
764  QSpacerItem *spacer = new QSpacerItem(checkBoxIndentation, 0, QSizePolicy::Fixed, QSizePolicy::Minimum);
765  nocloseonexitLayout->addItem(spacer);
766 
767  nocloseonexit = new QCheckBox(i18n("&Do not close when command exits"), q);
768  nocloseonexit->setChecked(false);
769  nocloseonexit->setDisabled(true);
770 
771  // check to see if we use konsole if not disable the nocloseonexit
772  // because we don't know how to do this on other terminal applications
773  KConfigGroup confGroup(KSharedConfig::openConfig(), QStringLiteral("General"));
774  QString preferredTerminal = confGroup.readPathEntry("TerminalApplication", QStringLiteral("konsole"));
775 
776  if (bReadOnly || preferredTerminal != QLatin1String("konsole")) {
777  nocloseonexit->hide();
778  }
779 
780  nocloseonexitLayout->addWidget(nocloseonexit);
781  dialogExtensionLayout->addLayout(nocloseonexitLayout);
782 
783  dialogExtension->setLayout(dialogExtensionLayout);
784  topLayout->addWidget(dialogExtension);
785 
786  buttonBox = new QDialogButtonBox(q);
787  buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
788  q->connect(buttonBox, &QDialogButtonBox::accepted, q, &QDialog::accept);
789  q->connect(buttonBox, &QDialogButtonBox::rejected, q, &QDialog::reject);
790  topLayout->addWidget(buttonBox);
791 
792  q->setMinimumSize(q->minimumSizeHint());
793  //edit->setText( _value );
794  // The resize is what caused "can't click on items before clicking on Name header" in previous versions.
795  // Probably due to the resizeEvent handler using width().
796 #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
797  q->resize( q->minimumWidth(), 0.6 * QApplication::screens().at(0)->availableGeometry().height());
798 #else
799  q->resize( q->minimumWidth(), 0.6 * q->screen()->availableGeometry().height());
800 #endif
801  edit->setFocus();
802  q->slotTextChanged();
803 }
804 
805 // ----------------------------------------------------------------------
806 
808 {
809  delete d;
810 }
811 
812 // ----------------------------------------------------------------------
813 
814 void KOpenWithDialog::slotSelected(const QString & /*_name*/, const QString &_exec)
815 {
816  d->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!_exec.isEmpty());
817 }
818 
819 // ----------------------------------------------------------------------
820 
821 void KOpenWithDialog::slotHighlighted(const QString &entryPath, const QString &)
822 {
823  d->curService = KService::serviceByDesktopPath(entryPath);
824  if (d->curService && !d->m_terminaldirty) {
825  // ### indicate that default value was restored
826  d->terminal->setChecked(d->curService->terminal());
827  QString terminalOptions = d->curService->terminalOptions();
828  d->nocloseonexit->setChecked((terminalOptions.contains(QLatin1String("--noclose"))));
829  d->m_terminaldirty = false; // slotTerminalToggled changed it
830  }
831 }
832 
833 // ----------------------------------------------------------------------
834 
835 void KOpenWithDialog::slotTextChanged()
836 {
837  // Forget about the service only when the selection is empty
838  // otherwise changing text but hitting the same result clears curService
839  bool selectionEmpty = !d->view->currentIndex().isValid();
840  if (d->curService && selectionEmpty) {
841  d->curService = nullptr;
842  }
843  d->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!d->edit->text().isEmpty() || d->curService);
844 
845  //Update the filter regexp with the new text in the lineedit
846  d->view->proxyModel()->setFilterFixedString(d->edit->text());
847 
848  //Expand all the nodes when the search string is 3 characters long
849  //If the search string doesn't match anything there will be no nodes to expand
850  if (d->edit->text().size() > 2) {
851  d->view->expandAll();
852  QAbstractItemModel *model = d->view->model();
853  if (model->rowCount() == 1) { // Automatically select the result (first leaf node) if the
854  // filter has only one match
855  QModelIndex leafNodeIdx = model->index(0, 0);
856  while (model->hasChildren(leafNodeIdx)) {
857  leafNodeIdx = model->index(0, 0, leafNodeIdx);
858  }
859  d->view->setCurrentIndex(leafNodeIdx);
860  }
861  } else {
862  d->view->collapseAll();
863  d->view->setCurrentIndex(d->view->rootIndex()); // Unset and deselect all the elements
864  d->curService = nullptr;
865  }
866 }
867 
868 // ----------------------------------------------------------------------
869 
870 void KOpenWithDialog::slotTerminalToggled(bool)
871 {
872  // ### indicate that default value was overridden
873  d->m_terminaldirty = true;
874  d->nocloseonexit->setDisabled(!d->terminal->isChecked());
875 }
876 
877 // ----------------------------------------------------------------------
878 
879 void KOpenWithDialogPrivate::_k_slotDbClick()
880 {
881  // check if a directory is selected
882  if (view->isDirSel()) {
883  return;
884  }
885  q->accept();
886 }
887 
888 void KOpenWithDialogPrivate::_k_slotFileSelected()
889 {
890  // quote the path to avoid unescaped whitespace, backslashes, etc.
891  edit->setText(KShell::quoteArg(edit->text()));
892 }
893 
895 {
896  d->saveNewApps = b;
897 }
898 
899 static QString simplifiedExecLineFromService(const QString &fullExec)
900 {
901  QString exec = fullExec;
902  exec.remove(QStringLiteral("%u"), Qt::CaseInsensitive);
903  exec.remove(QStringLiteral("%f"), Qt::CaseInsensitive);
904  exec.remove(QStringLiteral("-caption %c"));
905  exec.remove(QStringLiteral("-caption \"%c\""));
906  exec.remove(QStringLiteral("%i"));
907  exec.remove(QStringLiteral("%m"));
908  return exec.simplified();
909 }
910 
911 void KOpenWithDialogPrivate::addToMimeAppsList(const QString &serviceId /*menu id or storage id*/)
912 {
913  KSharedConfig::Ptr profile = KSharedConfig::openConfig(QStringLiteral("mimeapps.list"), KConfig::NoGlobals, QStandardPaths::GenericConfigLocation);
914 
915  // Save the default application according to mime-apps-spec 1.0
916  KConfigGroup defaultApp(profile, "Default Applications");
917  defaultApp.writeXdgListEntry(qMimeType, QStringList(serviceId));
918 
919  KConfigGroup addedApps(profile, "Added Associations");
920  QStringList apps = addedApps.readXdgListEntry(qMimeType);
921  apps.removeAll(serviceId);
922  apps.prepend(serviceId); // make it the preferred app
923  addedApps.writeXdgListEntry(qMimeType, apps);
924 
925  profile->sync();
926 
927  // Also make sure the "auto embed" setting for this mimetype is off
928  KSharedConfig::Ptr fileTypesConfig = KSharedConfig::openConfig(QStringLiteral("filetypesrc"), KConfig::NoGlobals);
929  fileTypesConfig->group("EmbedSettings").writeEntry(QStringLiteral("embed-") + qMimeType, false);
930  fileTypesConfig->sync();
931 
932  // qDebug() << "rebuilding ksycoca...";
933 
934  // kbuildsycoca is the one reading mimeapps.list, so we need to run it now
936 
937  // could be nullptr if the user canceled the dialog...
938  m_pService = KService::serviceByStorageId(serviceId);
939 }
940 
941 bool KOpenWithDialogPrivate::checkAccept()
942 {
943  const QString typedExec(edit->text());
944  QString fullExec(typedExec);
945 
946  QString serviceName;
947  QString initialServiceName;
948  QString preferredTerminal;
949  QString configPath;
950  QString serviceExec;
951  m_pService = curService;
952  if (!m_pService) {
953  // No service selected - check the command line
954 
955  // Find out the name of the service from the command line, removing args and paths
956  serviceName = KIO::DesktopExecParser::executableName(typedExec);
957  if (serviceName.isEmpty()) {
958  KMessageBox::error(q, i18n("Could not extract executable name from '%1', please type a valid program name.", serviceName));
959  return false;
960  }
961  initialServiceName = serviceName;
962  // Also remember the executableName with a path, if any, for the
963  // check that the executable exists.
964  // qDebug() << "initialServiceName=" << initialServiceName;
965  int i = 1; // We have app, app-2, app-3... Looks better for the user.
966  bool ok = false;
967  // Check if there's already a service by that name, with the same Exec line
968  do {
969  // qDebug() << "looking for service" << serviceName;
970  KService::Ptr serv = KService::serviceByDesktopName(serviceName);
971  ok = !serv; // ok if no such service yet
972  // also ok if we find the exact same service (well, "kwrite" == "kwrite %U")
973  if (serv && !serv->noDisplay() /* #297720 */) {
974  if (serv->isApplication()) {
975  /*// qDebug() << "typedExec=" << typedExec
976  << "serv->exec=" << serv->exec()
977  << "simplifiedExecLineFromService=" << simplifiedExecLineFromService(fullExec);*/
978  serviceExec = simplifiedExecLineFromService(serv->exec());
979  if (typedExec == serviceExec) {
980  ok = true;
981  m_pService = serv;
982  // qDebug() << "OK, found identical service: " << serv->entryPath();
983  } else {
984  // qDebug() << "Exec line differs, service says:" << serviceExec;
985  configPath = serv->entryPath();
986  serviceExec = serv->exec();
987  }
988  } else {
989  // qDebug() << "Found, but not an application:" << serv->entryPath();
990  }
991  }
992  if (!ok) { // service was found, but it was different -> keep looking
993  ++i;
994  serviceName = initialServiceName + QLatin1Char('-') + QString::number(i);
995  }
996  } while (!ok);
997  }
998  if (m_pService) {
999  // Existing service selected
1000  serviceName = m_pService->name();
1001  initialServiceName = serviceName;
1002  fullExec = m_pService->exec();
1003  } else {
1004  const QString binaryName = KIO::DesktopExecParser::executablePath(typedExec);
1005  // qDebug() << "binaryName=" << binaryName;
1006  // Ensure that the typed binary name actually exists (#81190)
1007  if (QStandardPaths::findExecutable(binaryName).isEmpty()) {
1008  KMessageBox::error(q, i18n("'%1' not found, please type a valid program name.", binaryName));
1009  return false;
1010  }
1011  }
1012 
1013  if (terminal->isChecked()) {
1014  KConfigGroup confGroup(KSharedConfig::openConfig(), QStringLiteral("General"));
1015  preferredTerminal = confGroup.readPathEntry("TerminalApplication", QStringLiteral("konsole"));
1016  m_command = preferredTerminal;
1017  // only add --noclose when we are sure it is konsole we're using
1018  if (preferredTerminal == QLatin1String("konsole") && nocloseonexit->isChecked()) {
1019  m_command += QStringLiteral(" --noclose");
1020  }
1021  m_command += QLatin1String(" -e ") + edit->text();
1022  // qDebug() << "Setting m_command to" << m_command;
1023  }
1024  if (m_pService && terminal->isChecked() != m_pService->terminal()) {
1025  m_pService = nullptr; // It's not exactly this service we're running
1026  }
1027 
1028  const bool bRemember = remember && remember->isChecked();
1029  // qDebug() << "bRemember=" << bRemember << "service found=" << m_pService;
1030  if (m_pService) {
1031  if (bRemember) {
1032  // Associate this app with qMimeType in mimeapps.list
1033  Q_ASSERT(!qMimeType.isEmpty()); // we don't show the remember checkbox otherwise
1034  addToMimeAppsList(m_pService->storageId());
1035  }
1036  } else {
1037  const bool createDesktopFile = bRemember || saveNewApps;
1038  if (!createDesktopFile) {
1039  // Create temp service
1040  if (configPath.isEmpty()) {
1041  m_pService = new KService(initialServiceName, fullExec, QString());
1042  } else {
1043  if (!typedExec.contains(QLatin1String("%u"), Qt::CaseInsensitive) &&
1044  !typedExec.contains(QLatin1String("%f"), Qt::CaseInsensitive)) {
1045  int index = serviceExec.indexOf(QLatin1String("%u"), 0, Qt::CaseInsensitive);
1046  if (index == -1) {
1047  index = serviceExec.indexOf(QLatin1String("%f"), 0, Qt::CaseInsensitive);
1048  }
1049  if (index > -1) {
1050  fullExec += QLatin1Char(' ') + serviceExec.midRef(index, 2);
1051  }
1052  }
1053  // qDebug() << "Creating service with Exec=" << fullExec;
1054  m_pService = new KService(configPath);
1055  m_pService->setExec(fullExec);
1056  }
1057  if (terminal->isChecked()) {
1058  m_pService->setTerminal(true);
1059  // only add --noclose when we are sure it is konsole we're using
1060  if (preferredTerminal == QLatin1String("konsole") && nocloseonexit->isChecked()) {
1061  m_pService->setTerminalOptions(QStringLiteral("--noclose"));
1062  }
1063  }
1064  } else {
1065  // If we got here, we can't seem to find a service for what they wanted. Create one.
1066 
1067  QString menuId;
1068 #ifdef Q_OS_WIN32
1069  // on windows, do not use the complete path, but only the default name.
1070  serviceName = QFileInfo(serviceName).fileName();
1071 #endif
1072  QString newPath = KService::newServicePath(false /* ignored argument */, serviceName, &menuId);
1073  // qDebug() << "Creating new service" << serviceName << "(" << newPath << ")" << "menuId=" << menuId;
1074 
1075  KDesktopFile desktopFile(newPath);
1076  KConfigGroup cg = desktopFile.desktopGroup();
1077  cg.writeEntry("Type", "Application");
1078  cg.writeEntry("Name", initialServiceName);
1079  cg.writeEntry("Exec", fullExec);
1080  cg.writeEntry("NoDisplay", true); // don't make it appear in the K menu
1081  if (terminal->isChecked()) {
1082  cg.writeEntry("Terminal", true);
1083  // only add --noclose when we are sure it is konsole we're using
1084  if (preferredTerminal == QLatin1String("konsole") && nocloseonexit->isChecked()) {
1085  cg.writeEntry("TerminalOptions", "--noclose");
1086  }
1087  }
1088  if (!qMimeType.isEmpty()) {
1089  cg.writeXdgListEntry("MimeType", QStringList() << qMimeType);
1090  }
1091  cg.sync();
1092 
1093  if (!qMimeType.isEmpty()) {
1094  addToMimeAppsList(menuId);
1095  }
1096  m_pService = new KService(newPath);
1097  }
1098  }
1099 
1100  saveComboboxHistory();
1101  return true;
1102 }
1103 
1104 bool KOpenWithDialog::eventFilter(QObject *object, QEvent *event)
1105 {
1106  // Detect DownArrow to navigate the results in the QTreeView
1107  if (object == d->edit && event->type() == QEvent::ShortcutOverride) {
1108  QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
1109  if (keyEvent->key() == Qt::Key_Down) {
1110  KHistoryComboBox *combo = static_cast<KHistoryComboBox *>(d->edit->comboBox());
1111  // FIXME: Disable arrow down in CompletionPopup and CompletionPopupAuto only when the dropdown list is shown.
1112  // When popup completion mode is used the down arrow is used to navigate the dropdown list of results
1114  QModelIndex leafNodeIdx = d->view->model()->index(0, 0);
1115  // Check if we have at least one result or the focus is passed to the empty QTreeView
1116  if (d->view->model()->hasChildren(leafNodeIdx)) {
1117  d->view->setFocus(Qt::OtherFocusReason);
1118  QApplication::sendEvent(d->view, keyEvent);
1119  return true;
1120  }
1121  }
1122  }
1123  }
1124  return QDialog::eventFilter(object, event);
1125 }
1126 
1128 {
1129  if (d->checkAccept()) {
1130  QDialog::accept();
1131  }
1132 }
1133 
1135 {
1136  if (!d->m_command.isEmpty()) {
1137  return d->m_command;
1138  } else {
1139  return d->edit->text();
1140  }
1141 }
1142 
1144 {
1145  // uncheck the checkbox because the value could be used when "Run in Terminal" is selected
1146  d->nocloseonexit->setChecked(false);
1147  d->nocloseonexit->hide();
1148 
1149  d->dialogExtension->setVisible(d->nocloseonexit->isVisible() || d->terminal->isVisible());
1150 }
1151 
1153 {
1154  d->terminal->hide();
1156 }
1157 
1159 {
1160  return d->m_pService;
1161 }
1162 
1163 void KOpenWithDialogPrivate::saveComboboxHistory()
1164 {
1165  KHistoryComboBox *combo = static_cast<KHistoryComboBox *>(edit->comboBox());
1166  if (combo) {
1167  combo->addToHistory(edit->text());
1168 
1169  KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("Open-with settings"));
1170  cg.writeEntry("History", combo->historyItems());
1171  writeEntry(cg, "CompletionMode", combo->completionMode());
1172  // don't store the completion-list, as it contains all of KUrlCompletion's
1173  // executables
1174  cg.sync();
1175  }
1176 }
1177 
1178 #include "moc_kopenwithdialog.cpp"
1179 #include "moc_kopenwithdialog_p.cpp"
void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
QString readPathEntry(const QString &pKey, const QString &aDefault) const
int indexOf(QChar ch, int from, Qt::CaseSensitivity cs) const const
static void rebuildKSycoca(QWidget *parent)
Rebuild KSycoca and show a progress dialog while doing so.
bool sync() override
virtual int rowCount(const QModelIndex &parent) const const =0
ShortcutOverride
QString & append(QChar ch)
QEvent::Type type() const const
void setContentsMargins(int left, int top, int right, int bottom)
virtual void reject()
virtual QModelIndex index(int row, int column, const QModelIndex &parent) const const =0
bool noDisplay() const
void setModal(bool modal)
KOpenWithDialog(const QList< QUrl > &urls, QWidget *parent=nullptr)
Create a dialog that asks for a application to open a given URL(s) with.
void hideRunInTerminal()
Hide the "Run in &terminal" Checkbox.
~KOpenWithDialog()
Destructor.
static Ptr serviceByDesktopName(const QString &_name)
virtual void currentChanged(const QModelIndex &current, const QModelIndex &previous) override
AdjustToMinimumContentsLengthWithIcon
This class is a widget showing a lineedit and a button, which invokes a filedialog.
Definition: kurlrequester.h:49
QString simplified() const const
QString findExecutable(const QString &executableName, const QStringList &paths)
void writeEntry(const QString &key, const QVariant &value, WriteConfigFlags pFlags=Normal)
QString i18np(const char *singular, const char *plural, const TYPE &arg...)
OtherFocusReason
QString & remove(int position, int n)
bool isApplication() const
QString exec() const
void addToHistory(const QString &item)
KCompletion::CompletionMode completionMode() const
void initFrom(const QWidget *widget)
virtual void setLineEdit(QLineEdit *)
bool isValid() const const
void addWidget(QWidget *widget, int stretch, Qt::Alignment alignment)
QString number(int n, int base)
int count(const T &value) const const
QString entryPath() const
QMimeType mimeTypeForUrl(const QUrl &url) const const
QString fileName() const const
QString label(StandardShortcut id)
CaseInsensitive
bool isEmpty() const const
virtual void addItem(QLayoutItem *item) override
void setObjectName(const QString &name)
DisplayRole
bool isEmpty() const const
int removeAll(const T &value)
bool sendEvent(QObject *receiver, QEvent *event)
void writeXdgListEntry(const QString &pKey, const QStringList &value, WriteConfigFlags pFlags=Normal)
void error(QWidget *parent, const QString &text, const QString &caption=QString(), Options options=Notify)
"Open With" dialog box.
void * internalPointer() const const
QStringList readXdgListEntry(const QString &pKey, const QStringList &aDefault=QStringList()) const
void setClearButtonEnabled(bool enable)
KCOREADDONS_EXPORT QString csqueeze(const QString &str, int maxlen=40)
T & first()
void accept() override
Reimplemented from QDialog::accept()
QModelIndex parent() const const
void setDuplicatesEnabled(bool enable)
QMimeType mimeTypeForName(const QString &nameOrAlias) const const
static Ptr serviceByDesktopPath(const QString &_path)
virtual void accept()
int key() const const
KCOREADDONS_EXPORT QString quoteArg(const QString &arg)
QList< QScreen * > screens()
QString toHtmlEscaped() const const
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
QStringRef midRef(int position, int n) const const
static KSharedConfig::Ptr openConfig(const QString &fileName=QString(), OpenFlags mode=FullConfig, QStandardPaths::StandardLocation type=QStandardPaths::GenericConfigLocation)
QCA_EXPORT void init()
static QString newServicePath(bool showInMenu, const QString &suggestedName, QString *menuId=nullptr, const QStringList *reservedMenuIds=nullptr)
virtual void setCompletionMode(KCompletion::CompletionMode mode)
QString text() const
QString i18n(const char *text, const TYPE &arg...)
PM_IndicatorWidth
void setSaveNewApplications(bool b)
Set whether a new .desktop file should be created if the user selects an application for which no cor...
QVariant data(int role) const const
static QString executablePath(const QString &execLine)
Given a full command line (e.g.
virtual bool hasChildren(const QModelIndex &parent) const const
void textChanged(const QString &)
Emitted when the text in the lineedit changes.
void hideNoCloseOnExit()
Hide the "Do not &close when command exits" Checkbox.
virtual void setModel(QAbstractItemModel *model) override
void toggled(bool checked)
void setWindowTitle(const QString &)
void prepend(const T &value)
QIcon fromTheme(const QString &name)
Orientation
void setToolTip(const QString &)
virtual bool eventFilter(QObject *o, QEvent *e) override
void setMaxCount(int max)
static Ptr group(const QString &relPath)
void setSizeAdjustPolicy(QComboBox::SizeAdjustPolicy policy)
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
QString toString() const const
virtual bool event(QEvent *event) override
T readEntry(const QString &key, const T &aDefault) const
QString fileName(QUrl::ComponentFormattingOptions options) const const
void setHistoryItems(const QStringList &items)
Key_Down
bool setStretchFactor(QWidget *widget, int stretch)
This class does completion of URLs including user directories (~user) and environment variables...
KService::Ptr service() const
void addLayout(QLayout *layout, int stretch)
static QString executableName(const QString &execLine)
Given a full command line (e.g.
static Ptr serviceByStorageId(const QString &_storageId)
KIOFILEWIDGETS_EXPORT QStringList list(const QString &fileClass)
Returns a list of directories associated with this file-class.
Definition: krecentdirs.cpp:34
QStringList historyItems() const
This file is part of the KDE documentation.
Documentation copyright © 1996-2020 The KDE developers.
Generated on Mon Nov 30 2020 23:01:45 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.