Files
qt-creator/src/libs/utils/pathchooser.cpp
Eike Ziller 13a947d119 Debugger: Fix attaching to running debug server
For example when developing on Windows, cross compiling to Linux, the
result is not executable from the Windows perspective.
Reverts f2cfd3c01a which changed the code
to use ExistingCommand to get the automatic expansion of app bundles on
OS X, and do automatic expansion of app bundles also when using path
chooser of type File. Choosing an app bundle in a path chooser of type
File would previously lead to an invalid entry in the path chooser
anyhow, because the app bundle itself is not a file.

Change-Id: Ie710c47918d2b8735009e290301ed2683e354f2c
Task-number: QTCREATORBUG-14412
Reviewed-by: Orgad Shaneh <orgads@gmail.com>
Reviewed-by: hjk <hjk@theqtcompany.com>
Reviewed-by: Eike Ziller <eike.ziller@theqtcompany.com>
2015-05-07 12:34:31 +00:00

700 lines
21 KiB
C++

/****************************************************************************
**
** Copyright (C) 2015 The Qt Company Ltd.
** Contact: http://www.qt.io/licensing
**
** This file is part of Qt Creator.
**
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms and
** conditions see http://www.qt.io/terms-conditions. For further information
** use the contact form at http://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 2.1 or version 3 as published by the Free
** Software Foundation and appearing in the file LICENSE.LGPLv21 and
** LICENSE.LGPLv3 included in the packaging of this file. Please review the
** following information to ensure the GNU Lesser General Public License
** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** In addition, as a special exception, The Qt Company gives you certain additional
** rights. These rights are described in The Qt Company LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
****************************************************************************/
#include "pathchooser.h"
#include "fancylineedit.h"
#include "environment.h"
#include "qtcassert.h"
#include "synchronousprocess.h"
#include "hostosinfo.h"
#include <QDebug>
#include <QFileDialog>
#include <QHBoxLayout>
#include <QPushButton>
#include <QStandardPaths>
/*!
\class Utils::PathChooser
\brief The PathChooser class is a control that lets the user choose a path,
consisting of a QLineEdit and
a "Browse" button.
This class has some validation logic for embedding into QWizardPage.
*/
static QString appBundleExpandedPath(const QString &path)
{
if (Utils::HostOsInfo::hostOs() == Utils::OsTypeMac && path.endsWith(QLatin1String(".app"))) {
// possibly expand to Foo.app/Contents/MacOS/Foo
QFileInfo info(path);
if (info.isDir()) {
QString exePath = path + QLatin1String("/Contents/MacOS/") + info.completeBaseName();
if (QFileInfo(exePath).exists())
return exePath;
}
}
return path;
}
namespace Utils {
// ------------------ PathValidatingLineEdit
class PathValidatingLineEdit : public FancyLineEdit
{
public:
explicit PathValidatingLineEdit(PathChooser *chooser, QWidget *parent = 0);
protected:
virtual bool validate(const QString &value, QString *errorMessage) const;
private:
PathChooser *m_chooser;
};
PathValidatingLineEdit::PathValidatingLineEdit(PathChooser *chooser, QWidget *parent) :
FancyLineEdit(parent),
m_chooser(chooser)
{
QTC_ASSERT(chooser, return);
}
bool PathValidatingLineEdit::validate(const QString &value, QString *errorMessage) const
{
return m_chooser->validatePath(value, errorMessage);
}
// ------------------ BinaryVersionToolTipEventFilter
// Event filter to be installed on a lineedit used for entering
// executables, taking the arguments to print the version ('--version').
// On a tooltip event, the version is obtained by running the binary and
// setting its stdout as tooltip.
class BinaryVersionToolTipEventFilter : public QObject
{
public:
explicit BinaryVersionToolTipEventFilter(QLineEdit *le);
virtual bool eventFilter(QObject *, QEvent *);
QStringList arguments() const { return m_arguments; }
void setArguments(const QStringList &arguments) { m_arguments = arguments; }
static QString toolVersion(const QString &binary, const QStringList &arguments);
private:
// Extension point for concatenating existing tooltips.
virtual QString defaultToolTip() const { return QString(); }
QStringList m_arguments;
};
BinaryVersionToolTipEventFilter::BinaryVersionToolTipEventFilter(QLineEdit *le) :
QObject(le)
{
le->installEventFilter(this);
}
bool BinaryVersionToolTipEventFilter::eventFilter(QObject *o, QEvent *e)
{
if (e->type() != QEvent::ToolTip)
return false;
QLineEdit *le = qobject_cast<QLineEdit *>(o);
QTC_ASSERT(le, return false);
const QString binary = le->text();
if (!binary.isEmpty()) {
const QString version = BinaryVersionToolTipEventFilter::toolVersion(QDir::cleanPath(binary), m_arguments);
if (!version.isEmpty()) {
// Concatenate tooltips.
QString tooltip = QLatin1String("<html><head/><body>");
const QString defaultValue = defaultToolTip();
if (!defaultValue.isEmpty()) {
tooltip += QLatin1String("<p>");
tooltip += defaultValue;
tooltip += QLatin1String("</p>");
}
tooltip += QLatin1String("<pre>");
tooltip += version;
tooltip += QLatin1String("</pre><body></html>");
le->setToolTip(tooltip);
}
}
return false;
}
QString BinaryVersionToolTipEventFilter::toolVersion(const QString &binary, const QStringList &arguments)
{
if (binary.isEmpty())
return QString();
QProcess proc;
proc.start(binary, arguments);
if (!proc.waitForStarted())
return QString();
if (!proc.waitForFinished()) {
SynchronousProcess::stopProcess(proc);
return QString();
}
return QString::fromLocal8Bit(QByteArray(proc.readAllStandardOutput()
+ proc.readAllStandardError()));
}
// Extends BinaryVersionToolTipEventFilter to prepend the existing pathchooser
// tooltip to display the full path.
class PathChooserBinaryVersionToolTipEventFilter : public BinaryVersionToolTipEventFilter
{
public:
explicit PathChooserBinaryVersionToolTipEventFilter(PathChooser *pe) :
BinaryVersionToolTipEventFilter(pe->lineEdit()), m_pathChooser(pe) {}
private:
virtual QString defaultToolTip() const
{ return m_pathChooser->errorMessage(); }
const PathChooser *m_pathChooser;
};
// ------------------ PathChooserPrivate
class PathChooserPrivate
{
public:
PathChooserPrivate(PathChooser *chooser);
QString expandedPath(const QString &path) const;
QHBoxLayout *m_hLayout;
PathValidatingLineEdit *m_lineEdit;
PathChooser::Kind m_acceptingKind;
QString m_dialogTitleOverride;
QString m_dialogFilter;
QString m_initialBrowsePathOverride;
QString m_baseDirectory;
Environment m_environment;
BinaryVersionToolTipEventFilter *m_binaryVersionToolTipEventFilter;
QList<QAbstractButton *> m_buttons;
};
PathChooserPrivate::PathChooserPrivate(PathChooser *chooser) :
m_hLayout(new QHBoxLayout),
m_lineEdit(new PathValidatingLineEdit(chooser)),
m_acceptingKind(PathChooser::ExistingDirectory),
m_binaryVersionToolTipEventFilter(0)
{
}
QString PathChooserPrivate::expandedPath(const QString &input) const
{
if (input.isEmpty())
return input;
const QString path = QDir::cleanPath(m_environment.expandVariables(input));
if (path.isEmpty())
return path;
switch (m_acceptingKind) {
case PathChooser::Command:
case PathChooser::ExistingCommand: {
const FileName expanded = m_environment.searchInPath(path, QStringList(m_baseDirectory));
return expanded.isEmpty() ? path : expanded.toString();
}
case PathChooser::Any:
break;
case PathChooser::Directory:
case PathChooser::ExistingDirectory:
case PathChooser::File:
case PathChooser::SaveFile:
if (!m_baseDirectory.isEmpty() && QFileInfo(path).isRelative())
return QFileInfo(m_baseDirectory + QLatin1Char('/') + path).absoluteFilePath();
break;
}
return path;
}
PathChooser::PathChooser(QWidget *parent) :
QWidget(parent),
d(new PathChooserPrivate(this))
{
d->m_hLayout->setContentsMargins(0, 0, 0, 0);
connect(d->m_lineEdit, &FancyLineEdit::validReturnPressed, this, &PathChooser::returnPressed);
connect(d->m_lineEdit, &QLineEdit::textChanged, this, &PathChooser::changed);
connect(d->m_lineEdit, &FancyLineEdit::validChanged, this, &PathChooser::validChanged);
connect(d->m_lineEdit, &QLineEdit::editingFinished, this, &PathChooser::editingFinished);
connect(d->m_lineEdit, &QLineEdit::textChanged, this, &PathChooser::slotTextChanged);
d->m_lineEdit->setMinimumWidth(120);
d->m_hLayout->addWidget(d->m_lineEdit);
d->m_hLayout->setSizeConstraint(QLayout::SetMinimumSize);
addButton(browseButtonLabel(), this, SLOT(slotBrowse()));
setLayout(d->m_hLayout);
setFocusProxy(d->m_lineEdit);
setFocusPolicy(d->m_lineEdit->focusPolicy());
setEnvironment(Environment::systemEnvironment());
}
PathChooser::~PathChooser()
{
delete d;
}
void PathChooser::addButton(const QString &text, QObject *receiver, const char *slotFunc)
{
insertButton(d->m_buttons.count(), text, receiver, slotFunc);
}
void PathChooser::insertButton(int index, const QString &text, QObject *receiver, const char *slotFunc)
{
QPushButton *button = new QPushButton;
button->setText(text);
connect(button, SIGNAL(clicked()), receiver, slotFunc);
d->m_hLayout->insertWidget(index + 1/*line edit*/, button);
d->m_buttons.insert(index, button);
}
QString PathChooser::browseButtonLabel()
{
return HostOsInfo::isMacHost() ? tr("Choose...") : tr("Browse...");
}
QAbstractButton *PathChooser::buttonAtIndex(int index) const
{
return d->m_buttons.at(index);
}
QString PathChooser::baseDirectory() const
{
return d->m_baseDirectory;
}
void PathChooser::setBaseDirectory(const QString &directory)
{
if (d->m_baseDirectory == directory)
return;
d->m_baseDirectory = directory;
triggerChanged();
}
FileName PathChooser::baseFileName() const
{
return FileName::fromString(d->m_baseDirectory);
}
void PathChooser::setBaseFileName(const FileName &base)
{
d->m_baseDirectory = base.toString();
triggerChanged();
}
void PathChooser::setEnvironment(const Environment &env)
{
QString oldExpand = path();
d->m_environment = env;
if (path() != oldExpand) {
triggerChanged();
emit changed(rawPath());
}
}
QString PathChooser::path() const
{
return d->expandedPath(QDir::fromNativeSeparators(d->m_lineEdit->text()));
}
QString PathChooser::rawPath() const
{
return QDir::fromNativeSeparators(d->m_lineEdit->text());
}
FileName PathChooser::fileName() const
{
return FileName::fromString(path());
}
void PathChooser::setPath(const QString &path)
{
d->m_lineEdit->setText(QDir::toNativeSeparators(path));
}
void PathChooser::setFileName(const FileName &fn)
{
d->m_lineEdit->setText(fn.toUserOutput());
}
bool PathChooser::isReadOnly() const
{
return d->m_lineEdit->isReadOnly();
}
void PathChooser::setReadOnly(bool b)
{
d->m_lineEdit->setReadOnly(b);
foreach (QAbstractButton *button, d->m_buttons)
button->setEnabled(!b);
}
void PathChooser::slotBrowse()
{
emit beforeBrowsing();
QString predefined = path();
QFileInfo fi(predefined);
if (!predefined.isEmpty() && !fi.isDir()) {
predefined = fi.path();
fi.setFile(predefined);
}
if ((predefined.isEmpty() || !fi.isDir())
&& !d->m_initialBrowsePathOverride.isNull()) {
predefined = d->m_initialBrowsePathOverride;
fi.setFile(predefined);
if (!fi.isDir()) {
predefined.clear();
fi.setFile(QString());
}
}
// Prompt for a file/dir
QString newPath;
switch (d->m_acceptingKind) {
case PathChooser::Directory:
case PathChooser::ExistingDirectory:
newPath = QFileDialog::getExistingDirectory(this,
makeDialogTitle(tr("Choose Directory")), predefined);
break;
case PathChooser::ExistingCommand:
case PathChooser::Command:
newPath = QFileDialog::getOpenFileName(this,
makeDialogTitle(tr("Choose Executable")), predefined,
d->m_dialogFilter);
newPath = appBundleExpandedPath(newPath);
break;
case PathChooser::File: // fall through
newPath = QFileDialog::getOpenFileName(this,
makeDialogTitle(tr("Choose File")), predefined,
d->m_dialogFilter);
newPath = appBundleExpandedPath(newPath);
break;
case PathChooser::SaveFile:
newPath = QFileDialog::getSaveFileName(this,
makeDialogTitle(tr("Choose File")), predefined,
d->m_dialogFilter);
break;
case PathChooser::Any: {
QFileDialog dialog(this);
dialog.setFileMode(QFileDialog::AnyFile);
dialog.setWindowTitle(makeDialogTitle(tr("Choose File")));
if (fi.exists())
dialog.setDirectory(fi.absolutePath());
// FIXME: fix QFileDialog so that it filters properly: lib*.a
dialog.setNameFilter(d->m_dialogFilter);
if (dialog.exec() == QDialog::Accepted) {
// probably loop here until the *.framework dir match
QStringList paths = dialog.selectedFiles();
if (!paths.isEmpty())
newPath = paths.at(0);
}
break;
}
default:
break;
}
// Delete trailing slashes unless it is "/"|"\\", only
if (!newPath.isEmpty()) {
newPath = QDir::toNativeSeparators(newPath);
if (newPath.size() > 1 && newPath.endsWith(QDir::separator()))
newPath.truncate(newPath.size() - 1);
setPath(newPath);
}
emit browsingFinished();
triggerChanged();
}
void PathChooser::slotTextChanged()
{
emit pathChanged(path());
}
bool PathChooser::isValid() const
{
return d->m_lineEdit->isValid();
}
QString PathChooser::errorMessage() const
{
return d->m_lineEdit->errorMessage();
}
void PathChooser::triggerChanged()
{
d->m_lineEdit->triggerChanged();
}
bool PathChooser::validatePath(const QString &path, QString *errorMessage)
{
QString expandedPath = d->expandedPath(path);
if (path.isEmpty()) {
if (errorMessage)
*errorMessage = tr("The path must not be empty.");
return false;
}
if (expandedPath.isEmpty()) {
if (errorMessage)
*errorMessage = tr("The path \"%1\" expanded to an empty string.").arg(QDir::toNativeSeparators(path));
return false;
}
const QFileInfo fi(expandedPath);
// Check if existing
switch (d->m_acceptingKind) {
case PathChooser::ExistingDirectory: // fall through
if (!fi.exists()) {
if (errorMessage)
*errorMessage = tr("The path \"%1\" does not exist.").arg(QDir::toNativeSeparators(expandedPath));
return false;
}
if (!fi.isDir()) {
if (errorMessage)
*errorMessage = tr("The path \"%1\" is not a directory.").arg(QDir::toNativeSeparators(expandedPath));
return false;
}
break;
case PathChooser::File: // fall through
if (!fi.exists()) {
if (errorMessage)
*errorMessage = tr("The path \"%1\" does not exist.").arg(QDir::toNativeSeparators(expandedPath));
return false;
}
break;
case PathChooser::SaveFile:
if (!fi.absoluteDir().exists()) {
if (errorMessage)
*errorMessage = tr("The directory \"%1\" does not exist.").arg(QDir::toNativeSeparators(fi.absolutePath()));
return false;
}
break;
case PathChooser::ExistingCommand:
if (!fi.exists()) {
if (errorMessage)
*errorMessage = tr("The path \"%1\" does not exist.").arg(QDir::toNativeSeparators(expandedPath));
return false;
}
if (!fi.isExecutable()) {
if (errorMessage)
*errorMessage = tr("Cannot execute \"%1\".").arg(QDir::toNativeSeparators(expandedPath));
return false;
}
break;
case PathChooser::Directory:
if (fi.exists() && !fi.isDir()) {
if (errorMessage)
*errorMessage = tr("The path \"%1\" is not a directory.").arg(QDir::toNativeSeparators(expandedPath));
return false;
}
break;
case PathChooser::Command: // fall through
if (fi.exists() && !fi.isExecutable()) {
if (errorMessage)
*errorMessage = tr("Cannot execute \"%1\".").arg(QDir::toNativeSeparators(expandedPath));
return false;
}
break;
default:
;
}
// Check expected kind
switch (d->m_acceptingKind) {
case PathChooser::ExistingDirectory:
if (!fi.isDir()) {
if (errorMessage)
*errorMessage = tr("The path <b>%1</b> is not a directory.").arg(QDir::toNativeSeparators(expandedPath));
return false;
}
break;
case PathChooser::File:
if (!fi.isFile()) {
if (errorMessage)
*errorMessage = tr("The path <b>%1</b> is not a file.").arg(QDir::toNativeSeparators(expandedPath));
return false;
}
break;
case PathChooser::SaveFile:
if (fi.exists() && fi.isDir()) {
if (errorMessage)
*errorMessage = tr("The path <b>%1</b> is not a file.").arg(QDir::toNativeSeparators(fi.absolutePath()));
return false;
}
break;
case PathChooser::ExistingCommand:
if (!fi.isFile() || !fi.isExecutable()) {
if (errorMessage)
*errorMessage = tr("The path <b>%1</b> is not an executable file.").arg(QDir::toNativeSeparators(expandedPath));
return false;
}
case PathChooser::Command:
break;
case PathChooser::Any:
break;
default:
;
}
if (errorMessage)
*errorMessage = tr("Full path: <b>%1</b>").arg(QDir::toNativeSeparators(expandedPath));
return true;
}
QString PathChooser::label()
{
return tr("Path:");
}
QString PathChooser::homePath()
{
// Return 'users/<name>/Documents' on Windows, since Windows explorer
// does not let people actually display the contents of their home
// directory. Alternatively, create a QtCreator-specific directory?
if (HostOsInfo::isWindowsHost())
return QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
return QDir::homePath();
}
void PathChooser::setExpectedKind(Kind expected)
{
if (d->m_acceptingKind == expected)
return;
d->m_acceptingKind = expected;
d->m_lineEdit->triggerChanged();
}
PathChooser::Kind PathChooser::expectedKind() const
{
return d->m_acceptingKind;
}
void PathChooser::setPromptDialogTitle(const QString &title)
{
d->m_dialogTitleOverride = title;
}
QString PathChooser::promptDialogTitle() const
{
return d->m_dialogTitleOverride;
}
void PathChooser::setPromptDialogFilter(const QString &filter)
{
d->m_dialogFilter = filter;
}
QString PathChooser::promptDialogFilter() const
{
return d->m_dialogFilter;
}
void PathChooser::setInitialBrowsePathBackup(const QString &path)
{
d->m_initialBrowsePathOverride = path;
}
QString PathChooser::makeDialogTitle(const QString &title)
{
if (d->m_dialogTitleOverride.isNull())
return title;
else
return d->m_dialogTitleOverride;
}
FancyLineEdit *PathChooser::lineEdit() const
{
// HACK: Make it work with HistoryCompleter.
if (d->m_lineEdit->objectName().isEmpty())
d->m_lineEdit->setObjectName(objectName() + QLatin1String("LineEdit"));
return d->m_lineEdit;
}
QString PathChooser::toolVersion(const QString &binary, const QStringList &arguments)
{
return BinaryVersionToolTipEventFilter::toolVersion(binary, arguments);
}
void PathChooser::installLineEditVersionToolTip(QLineEdit *le, const QStringList &arguments)
{
BinaryVersionToolTipEventFilter *ef = new BinaryVersionToolTipEventFilter(le);
ef->setArguments(arguments);
}
void PathChooser::setHistoryCompleter(const QString &historyKey)
{
d->m_lineEdit->setHistoryCompleter(historyKey);
}
QStringList PathChooser::commandVersionArguments() const
{
return d->m_binaryVersionToolTipEventFilter ?
d->m_binaryVersionToolTipEventFilter->arguments() :
QStringList();
}
void PathChooser::setCommandVersionArguments(const QStringList &arguments)
{
if (arguments.isEmpty()) {
if (d->m_binaryVersionToolTipEventFilter) {
delete d->m_binaryVersionToolTipEventFilter;
d->m_binaryVersionToolTipEventFilter = 0;
}
} else {
if (!d->m_binaryVersionToolTipEventFilter)
d->m_binaryVersionToolTipEventFilter = new PathChooserBinaryVersionToolTipEventFilter(this);
d->m_binaryVersionToolTipEventFilter->setArguments(arguments);
}
}
} // namespace Utils