1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25
26 #include "environmentwidget.h"
27
28 #include <coreplugin/fileutils.h>
29 #include <coreplugin/find/itemviewfind.h>
30
31 #include <utils/algorithm.h>
32 #include <utils/detailswidget.h>
33 #include <utils/environment.h>
34 #include <utils/environmentdialog.h>
35 #include <utils/environmentmodel.h>
36 #include <utils/headerviewstretcher.h>
37 #include <utils/hostosinfo.h>
38 #include <utils/itemviews.h>
39 #include <utils/namevaluevalidator.h>
40 #include <utils/qtcassert.h>
41 #include <utils/stringutils.h>
42 #include <utils/tooltip/tooltip.h>
43
44 #include <QDialogButtonBox>
45 #include <QDir>
46 #include <QFileDialog>
47 #include <QFileInfo>
48 #include <QHBoxLayout>
49 #include <QKeyEvent>
50 #include <QLineEdit>
51 #include <QPushButton>
52 #include <QString>
53 #include <QStyledItemDelegate>
54 #include <QTreeView>
55 #include <QTreeWidget>
56 #include <QTreeWidgetItem>
57 #include <QVBoxLayout>
58
59 namespace ProjectExplorer {
60
61 class PathTreeWidget : public QTreeWidget
62 {
63 public:
sizeHint() const64 QSize sizeHint() const override
65 {
66 return QSize(800, 600);
67 }
68 };
69
70 class PathListDialog : public QDialog
71 {
72 Q_DECLARE_TR_FUNCTIONS(EnvironmentWidget)
73 public:
PathListDialog(const QString & varName,const QString & paths,QWidget * parent)74 PathListDialog(const QString &varName, const QString &paths, QWidget *parent) : QDialog(parent)
75 {
76 const auto mainLayout = new QVBoxLayout(this);
77 const auto viewLayout = new QHBoxLayout;
78 const auto buttonsLayout = new QVBoxLayout;
79 const auto addButton = new QPushButton(tr("Add..."));
80 const auto removeButton = new QPushButton(tr("Remove"));
81 const auto editButton = new QPushButton(tr("Edit..."));
82 buttonsLayout->addWidget(addButton);
83 buttonsLayout->addWidget(removeButton);
84 buttonsLayout->addWidget(editButton);
85 buttonsLayout->addStretch(1);
86 const auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok
87 | QDialogButtonBox::Cancel);
88 viewLayout->addWidget(&m_view);
89 viewLayout->addLayout(buttonsLayout);
90 mainLayout->addLayout(viewLayout);
91 mainLayout->addWidget(buttonBox);
92
93 m_view.setHeaderLabel(varName);
94 m_view.setDragDropMode(QAbstractItemView::InternalMove);
95 const QStringList pathList = paths.split(Utils::HostOsInfo::pathListSeparator(),
96 Qt::SkipEmptyParts);
97 for (const QString &path : pathList)
98 addPath(path);
99
100 connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
101 connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
102 connect(addButton, &QPushButton::clicked, this, [this] {
103 const QString dir = QDir::toNativeSeparators(
104 QFileDialog::getExistingDirectory(this, tr("Choose Directory")));
105 if (!dir.isEmpty())
106 addPath(dir);
107 });
108 connect(removeButton, &QPushButton::clicked, this, [this] {
109 const QList<QTreeWidgetItem *> selected = m_view.selectedItems();
110 QTC_ASSERT(selected.count() == 1, return);
111 delete selected.first();
112 });
113 connect(editButton, &QPushButton::clicked, this, [this] {
114 const QList<QTreeWidgetItem *> selected = m_view.selectedItems();
115 QTC_ASSERT(selected.count() == 1, return);
116 m_view.editItem(selected.first(), 0);
117 });
118 const auto updateButtonStates = [this, removeButton, editButton] {
119 const bool hasSelection = !m_view.selectedItems().isEmpty();
120 removeButton->setEnabled(hasSelection);
121 editButton->setEnabled(hasSelection);
122 };
123 connect(m_view.selectionModel(), &QItemSelectionModel::selectionChanged,
124 this, updateButtonStates);
125 updateButtonStates();
126 }
127
paths() const128 QString paths() const
129 {
130 QStringList pathList;
131 for (int i = 0; i < m_view.topLevelItemCount(); ++i)
132 pathList << m_view.topLevelItem(i)->text(0);
133 return pathList.join(Utils::HostOsInfo::pathListSeparator());
134 }
135
136 private:
addPath(const QString & path)137 void addPath(const QString &path)
138 {
139 const auto item = new QTreeWidgetItem(&m_view, {path});
140 item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable
141 | Qt::ItemIsDragEnabled);
142 }
143
144 PathTreeWidget m_view;
145 };
146
147 class EnvironmentDelegate : public QStyledItemDelegate
148 {
149 public:
EnvironmentDelegate(Utils::EnvironmentModel * model,QTreeView * view)150 EnvironmentDelegate(Utils::EnvironmentModel *model,
151 QTreeView *view)
152 : QStyledItemDelegate(view), m_model(model), m_view(view)
153 {}
154
createEditor(QWidget * parent,const QStyleOptionViewItem & option,const QModelIndex & index) const155 QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override
156 {
157 QWidget *w = QStyledItemDelegate::createEditor(parent, option, index);
158 if (index.column() != 0)
159 return w;
160
161 if (auto edit = qobject_cast<QLineEdit *>(w))
162 edit->setValidator(new Utils::NameValueValidator(
163 edit, m_model, m_view, index, EnvironmentWidget::tr("Variable already exists.")));
164 return w;
165 }
166 private:
167 Utils::EnvironmentModel *m_model;
168 QTreeView *m_view;
169 };
170
171
172 ////
173 // EnvironmentWidget::EnvironmentWidget
174 ////
175
176 class EnvironmentWidgetPrivate
177 {
178 public:
179 Utils::EnvironmentModel *m_model;
180 EnvironmentWidget::Type m_type = EnvironmentWidget::TypeLocal;
181 QString m_baseEnvironmentText;
182 EnvironmentWidget::OpenTerminalFunc m_openTerminalFunc;
183 Utils::DetailsWidget *m_detailsContainer;
184 QTreeView *m_environmentView;
185 QPushButton *m_editButton;
186 QPushButton *m_addButton;
187 QPushButton *m_resetButton;
188 QPushButton *m_unsetButton;
189 QPushButton *m_toggleButton;
190 QPushButton *m_batchEditButton;
191 QPushButton *m_appendPathButton = nullptr;
192 QPushButton *m_prependPathButton = nullptr;
193 QPushButton *m_terminalButton;
194 };
195
EnvironmentWidget(QWidget * parent,Type type,QWidget * additionalDetailsWidget)196 EnvironmentWidget::EnvironmentWidget(QWidget *parent, Type type, QWidget *additionalDetailsWidget)
197 : QWidget(parent), d(std::make_unique<EnvironmentWidgetPrivate>())
198 {
199 d->m_model = new Utils::EnvironmentModel();
200 d->m_type = type;
201 connect(d->m_model, &Utils::EnvironmentModel::userChangesChanged,
202 this, &EnvironmentWidget::userChangesChanged);
203 connect(d->m_model, &QAbstractItemModel::modelReset,
204 this, &EnvironmentWidget::invalidateCurrentIndex);
205
206 connect(d->m_model, &Utils::EnvironmentModel::focusIndex,
207 this, &EnvironmentWidget::focusIndex);
208
209 auto vbox = new QVBoxLayout(this);
210 vbox->setContentsMargins(0, 0, 0, 0);
211
212 d->m_detailsContainer = new Utils::DetailsWidget(this);
213
214 auto details = new QWidget(d->m_detailsContainer);
215 d->m_detailsContainer->setWidget(details);
216 details->setVisible(false);
217
218 auto vbox2 = new QVBoxLayout(details);
219 vbox2->setContentsMargins(0, 0, 0, 0);
220
221 if (additionalDetailsWidget)
222 vbox2->addWidget(additionalDetailsWidget);
223
224 auto horizontalLayout = new QHBoxLayout();
225 horizontalLayout->setContentsMargins(0, 0, 0, 0);
226 auto tree = new Utils::TreeView(this);
227 connect(tree, &QAbstractItemView::activated,
228 tree, [tree](const QModelIndex &idx) { tree->edit(idx); });
229 d->m_environmentView = tree;
230 d->m_environmentView->setModel(d->m_model);
231 d->m_environmentView->setItemDelegate(new EnvironmentDelegate(d->m_model, d->m_environmentView));
232 d->m_environmentView->setMinimumHeight(400);
233 d->m_environmentView->setRootIsDecorated(false);
234 d->m_environmentView->setUniformRowHeights(true);
235 new Utils::HeaderViewStretcher(d->m_environmentView->header(), 1);
236 d->m_environmentView->setSelectionMode(QAbstractItemView::SingleSelection);
237 d->m_environmentView->setSelectionBehavior(QAbstractItemView::SelectItems);
238 d->m_environmentView->setFrameShape(QFrame::NoFrame);
239 QFrame *findWrapper = Core::ItemViewFind::createSearchableWrapper(d->m_environmentView, Core::ItemViewFind::LightColored);
240 findWrapper->setFrameStyle(QFrame::StyledPanel);
241 horizontalLayout->addWidget(findWrapper);
242
243 auto buttonLayout = new QVBoxLayout();
244
245 d->m_editButton = new QPushButton(this);
246 d->m_editButton->setText(tr("Ed&it"));
247 buttonLayout->addWidget(d->m_editButton);
248
249 d->m_addButton = new QPushButton(this);
250 d->m_addButton->setText(tr("&Add"));
251 buttonLayout->addWidget(d->m_addButton);
252
253 d->m_resetButton = new QPushButton(this);
254 d->m_resetButton->setEnabled(false);
255 d->m_resetButton->setText(tr("&Reset"));
256 buttonLayout->addWidget(d->m_resetButton);
257
258 d->m_unsetButton = new QPushButton(this);
259 d->m_unsetButton->setEnabled(false);
260 d->m_unsetButton->setText(tr("&Unset"));
261 buttonLayout->addWidget(d->m_unsetButton);
262
263 d->m_toggleButton = new QPushButton(tr("Disable"), this);
264 buttonLayout->addWidget(d->m_toggleButton);
265 connect(d->m_toggleButton, &QPushButton::clicked, this, [this] {
266 d->m_model->toggleVariable(d->m_environmentView->currentIndex());
267 updateButtons();
268 });
269
270 if (type == TypeLocal) {
271 d->m_appendPathButton = new QPushButton(this);
272 d->m_appendPathButton->setEnabled(false);
273 d->m_appendPathButton->setText(tr("Append Path..."));
274 buttonLayout->addWidget(d->m_appendPathButton);
275 d->m_prependPathButton = new QPushButton(this);
276 d->m_prependPathButton->setEnabled(false);
277 d->m_prependPathButton->setText(tr("Prepend Path..."));
278 buttonLayout->addWidget(d->m_prependPathButton);
279 connect(d->m_appendPathButton, &QAbstractButton::clicked,
280 this, &EnvironmentWidget::appendPathButtonClicked);
281 connect(d->m_prependPathButton, &QAbstractButton::clicked,
282 this, &EnvironmentWidget::prependPathButtonClicked);
283 }
284
285 d->m_batchEditButton = new QPushButton(this);
286 d->m_batchEditButton->setText(tr("&Batch Edit..."));
287 buttonLayout->addWidget(d->m_batchEditButton);
288
289 d->m_terminalButton = new QPushButton(this);
290 d->m_terminalButton->setText(tr("Open &Terminal"));
291 d->m_terminalButton->setToolTip(tr("Open a terminal with this environment set up."));
292 d->m_terminalButton->setEnabled(type == TypeLocal);
293 buttonLayout->addWidget(d->m_terminalButton);
294 buttonLayout->addStretch();
295
296 horizontalLayout->addLayout(buttonLayout);
297 vbox2->addLayout(horizontalLayout);
298
299 vbox->addWidget(d->m_detailsContainer);
300
301 connect(d->m_model, &QAbstractItemModel::dataChanged,
302 this, &EnvironmentWidget::updateButtons);
303
304 connect(d->m_editButton, &QAbstractButton::clicked,
305 this, &EnvironmentWidget::editEnvironmentButtonClicked);
306 connect(d->m_addButton, &QAbstractButton::clicked,
307 this, &EnvironmentWidget::addEnvironmentButtonClicked);
308 connect(d->m_resetButton, &QAbstractButton::clicked,
309 this, &EnvironmentWidget::removeEnvironmentButtonClicked);
310 connect(d->m_unsetButton, &QAbstractButton::clicked,
311 this, &EnvironmentWidget::unsetEnvironmentButtonClicked);
312 connect(d->m_batchEditButton, &QAbstractButton::clicked,
313 this, &EnvironmentWidget::batchEditEnvironmentButtonClicked);
314 connect(d->m_environmentView->selectionModel(), &QItemSelectionModel::currentChanged,
315 this, &EnvironmentWidget::environmentCurrentIndexChanged);
316 connect(d->m_terminalButton, &QAbstractButton::clicked,
317 this, [this] {
318 Utils::Environment env = d->m_model->baseEnvironment();
319 env.modify(d->m_model->userChanges());
320 if (d->m_openTerminalFunc)
321 d->m_openTerminalFunc(env);
322 else
323 Core::FileUtils::openTerminal(QDir::currentPath(), env);
324 });
325 connect(d->m_detailsContainer, &Utils::DetailsWidget::linkActivated,
326 this, &EnvironmentWidget::linkActivated);
327
328 connect(d->m_model, &Utils::EnvironmentModel::userChangesChanged,
329 this, &EnvironmentWidget::updateSummaryText);
330 }
331
~EnvironmentWidget()332 EnvironmentWidget::~EnvironmentWidget()
333 {
334 delete d->m_model;
335 d->m_model = nullptr;
336 }
337
focusIndex(const QModelIndex & index)338 void EnvironmentWidget::focusIndex(const QModelIndex &index)
339 {
340 d->m_environmentView->setCurrentIndex(index);
341 d->m_environmentView->setFocus();
342 // When the current item changes as a result of the call above,
343 // QAbstractItemView::currentChanged() is called. That calls scrollTo(current),
344 // using the default EnsureVisible scroll hint, whereas we want PositionAtTop,
345 // because it ensures that the user doesn't have to scroll down when they've
346 // added a new environment variable and want to edit its value; they'll be able
347 // to see its value as they're typing it.
348 // This only helps to a certain degree - variables whose names start with letters
349 // later in the alphabet cause them fall within the "end" of the view's range,
350 // making it impossible to position them at the top of the view.
351 d->m_environmentView->scrollTo(index, QAbstractItemView::PositionAtTop);
352 }
353
setBaseEnvironment(const Utils::Environment & env)354 void EnvironmentWidget::setBaseEnvironment(const Utils::Environment &env)
355 {
356 d->m_model->setBaseEnvironment(env);
357 }
358
setBaseEnvironmentText(const QString & text)359 void EnvironmentWidget::setBaseEnvironmentText(const QString &text)
360 {
361 d->m_baseEnvironmentText = text;
362 updateSummaryText();
363 }
364
userChanges() const365 Utils::EnvironmentItems EnvironmentWidget::userChanges() const
366 {
367 return d->m_model->userChanges();
368 }
369
setUserChanges(const Utils::EnvironmentItems & list)370 void EnvironmentWidget::setUserChanges(const Utils::EnvironmentItems &list)
371 {
372 d->m_model->setUserChanges(list);
373 updateSummaryText();
374 }
375
setOpenTerminalFunc(const EnvironmentWidget::OpenTerminalFunc & func)376 void EnvironmentWidget::setOpenTerminalFunc(const EnvironmentWidget::OpenTerminalFunc &func)
377 {
378 d->m_openTerminalFunc = func;
379 d->m_terminalButton->setVisible(bool(func));
380 }
381
expand()382 void EnvironmentWidget::expand()
383 {
384 d->m_detailsContainer->setState(Utils::DetailsWidget::Expanded);
385 }
386
updateSummaryText()387 void EnvironmentWidget::updateSummaryText()
388 {
389 Utils::EnvironmentItems list = d->m_model->userChanges();
390 Utils::EnvironmentItem::sort(&list);
391
392 QString text;
393 foreach (const Utils::EnvironmentItem &item, list) {
394 if (item.name != Utils::EnvironmentModel::tr("<VARIABLE>")) {
395 if (!d->m_baseEnvironmentText.isEmpty() || !text.isEmpty())
396 text.append(QLatin1String("<br>"));
397 switch (item.operation) {
398 case Utils::EnvironmentItem::Unset:
399 text.append(tr("Unset <a href=\"%1\"><b>%1</b></a>").arg(item.name.toHtmlEscaped()));
400 break;
401 case Utils::EnvironmentItem::SetEnabled:
402 text.append(tr("Set <a href=\"%1\"><b>%1</b></a> to <b>%2</b>").arg(item.name.toHtmlEscaped(), item.value.toHtmlEscaped()));
403 break;
404 case Utils::EnvironmentItem::Append:
405 text.append(tr("Append <b>%2</b> to <a href=\"%1\"><b>%1</b></a>").arg(item.name.toHtmlEscaped(), item.value.toHtmlEscaped()));
406 break;
407 case Utils::EnvironmentItem::Prepend:
408 text.append(tr("Prepend <b>%2</b> to <a href=\"%1\"><b>%1</b></a>").arg(item.name.toHtmlEscaped(), item.value.toHtmlEscaped()));
409 break;
410 case Utils::EnvironmentItem::SetDisabled:
411 text.append(tr("Set <a href=\"%1\"><b>%1</b></a> to <b>%2</b> [disabled]").arg(item.name.toHtmlEscaped(), item.value.toHtmlEscaped()));
412 break;
413 }
414 }
415 }
416
417 if (text.isEmpty()) {
418 //: %1 is "System Environment" or some such.
419 if (!d->m_baseEnvironmentText.isEmpty())
420 text.prepend(tr("Use <b>%1</b>").arg(d->m_baseEnvironmentText));
421 else
422 text.prepend(tr("<b>No environment changes</b>"));
423 } else {
424 //: Yup, word puzzle. The Set/Unset phrases above are appended to this.
425 //: %1 is "System Environment" or some such.
426 if (!d->m_baseEnvironmentText.isEmpty())
427 text.prepend(tr("Use <b>%1</b> and").arg(d->m_baseEnvironmentText));
428 }
429
430 d->m_detailsContainer->setSummaryText(text);
431 }
432
linkActivated(const QString & link)433 void EnvironmentWidget::linkActivated(const QString &link)
434 {
435 d->m_detailsContainer->setState(Utils::DetailsWidget::Expanded);
436 QModelIndex idx = d->m_model->variableToIndex(link);
437 focusIndex(idx);
438 }
439
updateButtons()440 void EnvironmentWidget::updateButtons()
441 {
442 environmentCurrentIndexChanged(d->m_environmentView->currentIndex());
443 }
444
editEnvironmentButtonClicked()445 void EnvironmentWidget::editEnvironmentButtonClicked()
446 {
447 const QModelIndex current = d->m_environmentView->currentIndex();
448 if (current.column() == 1
449 && d->m_type == TypeLocal
450 && d->m_model->currentEntryIsPathList(current)) {
451 PathListDialog dlg(d->m_model->indexToVariable(current),
452 d->m_model->data(current).toString(), this);
453 if (dlg.exec() == QDialog::Accepted)
454 d->m_model->setData(current, dlg.paths());
455 } else {
456 d->m_environmentView->edit(current);
457 }
458 }
459
addEnvironmentButtonClicked()460 void EnvironmentWidget::addEnvironmentButtonClicked()
461 {
462 QModelIndex index = d->m_model->addVariable();
463 d->m_environmentView->setCurrentIndex(index);
464 d->m_environmentView->edit(index);
465 }
466
removeEnvironmentButtonClicked()467 void EnvironmentWidget::removeEnvironmentButtonClicked()
468 {
469 const QString &name = d->m_model->indexToVariable(d->m_environmentView->currentIndex());
470 d->m_model->resetVariable(name);
471 }
472
473 // unset in Merged Environment Mode means, unset if it comes from the base environment
474 // or remove when it is just a change we added
unsetEnvironmentButtonClicked()475 void EnvironmentWidget::unsetEnvironmentButtonClicked()
476 {
477 const QString &name = d->m_model->indexToVariable(d->m_environmentView->currentIndex());
478 if (!d->m_model->canReset(name))
479 d->m_model->resetVariable(name);
480 else
481 d->m_model->unsetVariable(name);
482 }
483
amendPathList(Utils::NameValueItem::Operation op)484 void EnvironmentWidget::amendPathList(Utils::NameValueItem::Operation op)
485 {
486 const QString varName = d->m_model->indexToVariable(d->m_environmentView->currentIndex());
487 const QString dir = QDir::toNativeSeparators(
488 QFileDialog::getExistingDirectory(this, tr("Choose Directory")));
489 if (dir.isEmpty())
490 return;
491 Utils::NameValueItems changes = d->m_model->userChanges();
492 changes.append({varName, dir, op});
493 d->m_model->setUserChanges(changes);
494 }
495
appendPathButtonClicked()496 void EnvironmentWidget::appendPathButtonClicked()
497 {
498 amendPathList(Utils::NameValueItem::Append);
499 }
500
prependPathButtonClicked()501 void EnvironmentWidget::prependPathButtonClicked()
502 {
503 amendPathList(Utils::NameValueItem::Prepend);
504 }
505
batchEditEnvironmentButtonClicked()506 void EnvironmentWidget::batchEditEnvironmentButtonClicked()
507 {
508 const Utils::EnvironmentItems changes = d->m_model->userChanges();
509
510 const auto newChanges = Utils::EnvironmentDialog::getEnvironmentItems(this, changes);
511
512 if (newChanges)
513 d->m_model->setUserChanges(*newChanges);
514 }
515
environmentCurrentIndexChanged(const QModelIndex & current)516 void EnvironmentWidget::environmentCurrentIndexChanged(const QModelIndex ¤t)
517 {
518 if (current.isValid()) {
519 d->m_editButton->setEnabled(true);
520 const QString &name = d->m_model->indexToVariable(current);
521 bool modified = d->m_model->canReset(name) && d->m_model->changes(name);
522 bool unset = d->m_model->isUnset(name);
523 d->m_resetButton->setEnabled(modified || unset);
524 d->m_unsetButton->setEnabled(!unset);
525 d->m_toggleButton->setEnabled(!unset);
526 d->m_toggleButton->setText(d->m_model->isEnabled(name) ? tr("Disable") : tr("Enable"));
527 } else {
528 d->m_editButton->setEnabled(false);
529 d->m_resetButton->setEnabled(false);
530 d->m_unsetButton->setEnabled(false);
531 d->m_toggleButton->setEnabled(false);
532 d->m_toggleButton->setText(tr("Disable"));
533 }
534 if (d->m_appendPathButton) {
535 const bool isPathList = d->m_model->currentEntryIsPathList(current);
536 d->m_appendPathButton->setEnabled(isPathList);
537 d->m_prependPathButton->setEnabled(isPathList);
538 }
539 }
540
invalidateCurrentIndex()541 void EnvironmentWidget::invalidateCurrentIndex()
542 {
543 environmentCurrentIndexChanged(QModelIndex());
544 }
545
546 } // namespace ProjectExplorer
547