diff options
Diffstat (limited to 'src/libs/utils/environmentmodel.cpp')
-rw-r--r-- | src/libs/utils/environmentmodel.cpp | 446 |
1 files changed, 442 insertions, 4 deletions
diff --git a/src/libs/utils/environmentmodel.cpp b/src/libs/utils/environmentmodel.cpp index 1332959784..6ac9b35640 100644 --- a/src/libs/utils/environmentmodel.cpp +++ b/src/libs/utils/environmentmodel.cpp @@ -1,20 +1,458 @@ -// Copyright (C) 2016 The Qt Company Ltd. +// Copyright (C) 2019 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "environmentmodel.h" +#include "algorithm.h" #include "environment.h" +#include "hostosinfo.h" +#include "namevaluedictionary.h" +#include "namevalueitem.h" +#include "qtcassert.h" +#include "utilstr.h" + +#include <QFileInfo> +#include <QFont> +#include <QGuiApplication> +#include <QPalette> +#include <QString> namespace Utils { -Environment EnvironmentModel::baseEnvironment() const +namespace Internal { + +class EnvironmentModelPrivate +{ +public: + void updateResultNameValueDictionary() + { + m_resultNameValueDictionary = m_baseNameValueDictionary; + m_resultNameValueDictionary.modify(m_items); + // Add removed variables again and mark them as "<UNSET>" so + // that the user can actually see those removals: + for (const EnvironmentItem &item : std::as_const(m_items)) { + if (item.operation == EnvironmentItem::Unset) + m_resultNameValueDictionary.set(item.name, Tr::tr("<UNSET>")); + } + } + + int findInChanges(const QString &name) const + { + for (int i = 0; i < m_items.size(); ++i) + if (m_items.at(i).name.compare(name, + m_baseNameValueDictionary.nameCaseSensitivity()) == 0) { + return i; + } + return -1; + } + + int findInResultInsertPosition(const QString &name) const + { + NameValueDictionary::const_iterator it; + int i = 0; + for (it = m_resultNameValueDictionary.constBegin(); + it != m_resultNameValueDictionary.constEnd(); + ++it, ++i) + if (it.key() > DictKey(name, m_resultNameValueDictionary.nameCaseSensitivity())) + return i; + return m_resultNameValueDictionary.size(); + } + + int findInResult(const QString &name) const + { + NameValueDictionary::const_iterator it; + int i = 0; + for (it = m_resultNameValueDictionary.constBegin(); + it != m_resultNameValueDictionary.constEnd(); + ++it, ++i) + if (m_resultNameValueDictionary.key(it) + .compare(name, m_resultNameValueDictionary.nameCaseSensitivity()) == 0) { + return i; + } + return -1; + } + + NameValueDictionary m_baseNameValueDictionary; + NameValueDictionary m_resultNameValueDictionary; + EnvironmentItems m_items; +}; + +} // namespace Internal + +EnvironmentModel::EnvironmentModel(QObject *parent) + : QAbstractTableModel(parent) + , d(std::make_unique<Internal::EnvironmentModelPrivate>()) +{} + +EnvironmentModel::~EnvironmentModel() = default; + +QString EnvironmentModel::indexToVariable(const QModelIndex &index) const { - return Environment(baseNameValueDictionary()); + const auto it = std::next(d->m_resultNameValueDictionary.constBegin(), index.row()); + return d->m_resultNameValueDictionary.key(it); } void EnvironmentModel::setBaseEnvironment(const Environment &env) { - setBaseNameValueDictionary(env.toDictionary()); + const NameValueDictionary dictionary = env.toDictionary(); + if (d->m_baseNameValueDictionary == dictionary) + return; + beginResetModel(); + d->m_baseNameValueDictionary = dictionary; + d->updateResultNameValueDictionary(); + endResetModel(); +} + +int EnvironmentModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + + return d->m_resultNameValueDictionary.size(); +} +int EnvironmentModel::columnCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + + return 2; +} + +bool EnvironmentModel::changes(const QString &name) const +{ + return d->findInChanges(name) >= 0; +} + +Environment EnvironmentModel::baseEnvironment() const +{ + return Environment(d->m_baseNameValueDictionary); +} + +QVariant EnvironmentModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + const auto resultIterator = std::next(d->m_resultNameValueDictionary.constBegin(), index.row()); + + switch (role) { + case Qt::DisplayRole: + case Qt::EditRole: + case Qt::ToolTipRole: + if (index.column() == 0) + return d->m_resultNameValueDictionary.key(resultIterator); + if (index.column() == 1) { + // Do not return "<UNSET>" when editing a previously unset variable: + if (role == Qt::EditRole) { + int pos = d->findInChanges(indexToVariable(index)); + if (pos != -1 && d->m_items.at(pos).operation == EnvironmentItem::Unset) + return QString(); + } + QString value = d->m_resultNameValueDictionary.value(resultIterator); + if (role == Qt::ToolTipRole && value.length() > 80) { + if (currentEntryIsPathList(index)) { + // For path lists, display one entry per line without separator + const QChar sep = Utils::HostOsInfo::pathListSeparator(); + value = value.replace(sep, '\n'); + } else { + // Use html to enable text wrapping + value = value.toHtmlEscaped(); + value.prepend(QLatin1String("<html><body>")); + value.append(QLatin1String("</body></html>")); + } + } + return value; + } + break; + case Qt::FontRole: { + QFont f; + f.setStrikeOut(!d->m_resultNameValueDictionary.isEnabled(resultIterator)); + return f; + } + case Qt::ForegroundRole: { + const QPalette p = QGuiApplication::palette(); + return p.color(changes(d->m_resultNameValueDictionary.key(resultIterator)) + ? QPalette::Link : QPalette::Text); + } + } + return QVariant(); +} + +Qt::ItemFlags EnvironmentModel::flags(const QModelIndex &index) const +{ + Q_UNUSED(index) + return Qt::ItemIsSelectable | Qt::ItemIsEnabled; +} + +QVariant EnvironmentModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Vertical || role != Qt::DisplayRole) + return QVariant(); + return section == 0 ? Tr::tr("Variable") : Tr::tr("Value"); +} + +/// ***************** +/// Utility functions +/// ***************** +QModelIndex EnvironmentModel::variableToIndex(const QString &name) const +{ + int row = d->findInResult(name); + if (row == -1) + return QModelIndex(); + return index(row, 0); +} + +bool EnvironmentModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!index.isValid() || role != Qt::EditRole) + return false; + + // ignore changes to already set values: + if (data(index, role) == value) + return true; + + const QString oldName = data(this->index(index.row(), 0, QModelIndex())).toString(); + const QString oldValue = data(this->index(index.row(), 1, QModelIndex()), Qt::EditRole).toString(); + int changesPos = d->findInChanges(oldName); + + if (index.column() == 0) { + //fail if a variable with the same name already exists + const QString &newName = HostOsInfo::isWindowsHost() ? value.toString().toUpper() + : value.toString(); + if (newName.isEmpty() || newName.contains('=')) + return false; + // Does the new name exist already? + if (d->m_resultNameValueDictionary.hasKey(newName) || newName.isEmpty()) + return false; + + EnvironmentItem newVariable(newName, oldValue); + + if (changesPos != -1) + resetVariable(oldName); // restore the original base variable again + + QModelIndex newIndex = addVariable(newVariable); // add the new variable + emit focusIndex(newIndex.sibling(newIndex.row(), 1)); // hint to focus on the value + return true; + } else if (index.column() == 1) { + // We are changing an existing value: + const QString stringValue = value.toString(); + if (changesPos != -1) { + const auto oldIt = d->m_baseNameValueDictionary.constFind(oldName); + const auto newIt = d->m_resultNameValueDictionary.constFind(oldName); + // We have already changed this value + if (oldIt != d->m_baseNameValueDictionary.constEnd() + && stringValue == d->m_baseNameValueDictionary.value(oldIt) + && d->m_baseNameValueDictionary.isEnabled(oldIt) + == d->m_resultNameValueDictionary.isEnabled(newIt)) { + // ... and now went back to the base value + d->m_items.removeAt(changesPos); + } else { + // ... and changed it again + d->m_items[changesPos].value = stringValue; + if (d->m_items[changesPos].operation == EnvironmentItem::Unset) + d->m_items[changesPos].operation = EnvironmentItem::SetEnabled; + } + } else { + // Add a new change item: + d->m_items.append(EnvironmentItem(oldName, stringValue)); + } + d->updateResultNameValueDictionary(); + emit dataChanged(index, index); + emit userChangesChanged(); + return true; + } + return false; +} + +QModelIndex EnvironmentModel::addVariable() +{ + return addVariable(EnvironmentItem("NEWVAR", "VALUE")); +} + +QModelIndex EnvironmentModel::addVariable(const EnvironmentItem &item) +{ + // Return existing index if the name is already in the result set: + int pos = d->findInResult(item.name); + if (pos >= 0 && pos < d->m_resultNameValueDictionary.size()) + return index(pos, 0, QModelIndex()); + + int insertPos = d->findInResultInsertPosition(item.name); + int changePos = d->findInChanges(item.name); + if (d->m_baseNameValueDictionary.hasKey(item.name)) { + // We previously unset this! + Q_ASSERT(changePos >= 0); + // Do not insert a line here as we listed the variable as <UNSET> before! + Q_ASSERT(d->m_items.at(changePos).name == item.name); + Q_ASSERT(d->m_items.at(changePos).operation == EnvironmentItem::Unset); + Q_ASSERT(d->m_items.at(changePos).value.isEmpty()); + d->m_items[changePos] = item; + emit dataChanged(index(insertPos, 0, QModelIndex()), index(insertPos, 1, QModelIndex())); + } else { + // We add something that is not in the base dictionary + // Insert a new line! + beginInsertRows(QModelIndex(), insertPos, insertPos); + Q_ASSERT(changePos < 0); + d->m_items.append(item); + d->updateResultNameValueDictionary(); + endInsertRows(); + } + emit userChangesChanged(); + return index(insertPos, 0, QModelIndex()); +} + +void EnvironmentModel::resetVariable(const QString &name) +{ + int rowInChanges = d->findInChanges(name); + if (rowInChanges < 0) + return; + + int rowInResult = d->findInResult(name); + if (rowInResult < 0) + return; + + if (d->m_baseNameValueDictionary.hasKey(name)) { + d->m_items.removeAt(rowInChanges); + d->updateResultNameValueDictionary(); + emit dataChanged(index(rowInResult, 0, QModelIndex()), index(rowInResult, 1, QModelIndex())); + emit userChangesChanged(); + } else { + // Remove the line completely! + beginRemoveRows(QModelIndex(), rowInResult, rowInResult); + d->m_items.removeAt(rowInChanges); + d->updateResultNameValueDictionary(); + endRemoveRows(); + emit userChangesChanged(); + } +} + +void EnvironmentModel::unsetVariable(const QString &name) +{ + // This does not change the number of rows as we will display a <UNSET> + // in place of the original variable! + int row = d->findInResult(name); + if (row < 0) + return; + + // look in d->m_items for the variable + int pos = d->findInChanges(name); + if (pos != -1) { + d->m_items[pos].operation = EnvironmentItem::Unset; + d->m_items[pos].value.clear(); + d->updateResultNameValueDictionary(); + emit dataChanged(index(row, 0, QModelIndex()), index(row, 1, QModelIndex())); + emit userChangesChanged(); + return; + } + d->m_items.append(EnvironmentItem(name, QString(), EnvironmentItem::Unset)); + d->updateResultNameValueDictionary(); + emit dataChanged(index(row, 0, QModelIndex()), index(row, 1, QModelIndex())); + emit userChangesChanged(); +} + +void EnvironmentModel::toggleVariable(const QModelIndex &idx) +{ + const QString name = indexToVariable(idx); + const auto newIt = d->m_resultNameValueDictionary.constFind(name); + QTC_ASSERT(newIt != d->m_resultNameValueDictionary.constEnd(), return); + const auto op = d->m_resultNameValueDictionary.isEnabled(newIt) + ? EnvironmentItem::SetDisabled : EnvironmentItem::SetEnabled; + const int changesPos = d->findInChanges(name); + if (changesPos != -1) { + const auto oldIt = d->m_baseNameValueDictionary.constFind(name); + if (oldIt == d->m_baseNameValueDictionary.constEnd() + || oldIt.value().first != newIt.value().first) { + d->m_items[changesPos].operation = op; + } else { + d->m_items.removeAt(changesPos); + } + } else { + d->m_items.append({name, d->m_resultNameValueDictionary.value(newIt), op}); + } + d->updateResultNameValueDictionary(); + emit dataChanged(index(idx.row(), 0), index(idx.row(), 1)); + emit userChangesChanged(); +} + +bool EnvironmentModel::isUnset(const QString &name) +{ + const int pos = d->findInChanges(name); + return pos == -1 ? false : d->m_items.at(pos).operation == EnvironmentItem::Unset; +} + +bool EnvironmentModel::isEnabled(const QString &name) const +{ + return d->m_resultNameValueDictionary.isEnabled(d->m_resultNameValueDictionary.constFind(name)); +} + +bool EnvironmentModel::canReset(const QString &name) +{ + return d->m_baseNameValueDictionary.hasKey(name); +} + +EnvironmentItems EnvironmentModel::userChanges() const +{ + return d->m_items; +} + +void EnvironmentModel::setUserChanges(const EnvironmentItems &items) +{ + EnvironmentItems filtered = Utils::filtered(items, [](const EnvironmentItem &i) { + return i.name != "export " && !i.name.contains('='); + }); + // We assume nobody is reordering the items here. + if (filtered == d->m_items) + return; + beginResetModel(); + d->m_items = filtered; + for (EnvironmentItem &item : d->m_items) { + QString &name = item.name; + name = name.trimmed(); + if (name.startsWith("export ")) + name = name.mid(7).trimmed(); + if (d->m_baseNameValueDictionary.osType() == OsTypeWindows) { + // NameValueDictionary variable names are case-insensitive under windows, but we still + // want to preserve the case of pre-existing variables. + auto it = d->m_baseNameValueDictionary.constFind(name); + if (it != d->m_baseNameValueDictionary.constEnd()) + name = d->m_baseNameValueDictionary.key(it); + } + } + + d->updateResultNameValueDictionary(); + endResetModel(); + emit userChangesChanged(); +} + +bool EnvironmentModel::currentEntryIsPathList(const QModelIndex ¤t) const +{ + if (!current.isValid()) + return false; + + // Look at the name first and check it against some well-known path variables. Extend as needed. + const QString varName = indexToVariable(current); + if (varName.compare("PATH", Utils::HostOsInfo::fileNameCaseSensitivity()) == 0) + return true; + if (Utils::HostOsInfo::isMacHost() && varName == "DYLD_LIBRARY_PATH") + return true; + if (Utils::HostOsInfo::isAnyUnixHost() && varName == "LD_LIBRARY_PATH") + return true; + if (varName == "PKG_CONFIG_DIR") + return true; + if (Utils::HostOsInfo::isWindowsHost() + && QStringList{"INCLUDE", "LIB", "LIBPATH"}.contains(varName)) { + return true; + } + + // Now check the value: If it's a list of strings separated by the platform's path separator + // and at least one of the strings is an existing directory, then that's enough proof for us. + QModelIndex valueIndex = current; + if (valueIndex.column() == 0) + valueIndex = valueIndex.siblingAtColumn(1); + const QStringList entries = data(valueIndex).toString() + .split(Utils::HostOsInfo::pathListSeparator(), Qt::SkipEmptyParts); + if (entries.length() < 2) + return false; + return Utils::anyOf(entries, [](const QString &d) { return QFileInfo(d).isDir(); }); } } // namespace Utils |