ProjectExplorer: Sanitize the "Add run config" UI

This functionality was implemented via a pop-up menu, which was close to
unusable if more than a few candidates existed (for example: Qt Creator
with autotests enabled).
We now use a proper dialog. The list of candidates is sortable, can be
filtered and includes information about which project file the target
executable comes from.

Fixes: QTCREATORBUG-19955
Change-Id: Ife087ad69a7e43e280d13c528d21f94a1ae48d4d
Reviewed-by: hjk <hjk@qt.io>
This commit is contained in:
Christian Kandeler
2019-02-19 12:56:28 +01:00
parent ea9dad4719
commit 557cab9164
8 changed files with 270 additions and 28 deletions

View File

@@ -0,0 +1,192 @@
/****************************************************************************
**
** Copyright (C) 2019 The Qt Company Ltd.
** Contact: https://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 https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
****************************************************************************/
#include "addrunconfigdialog.h"
#include "project.h"
#include "target.h"
#include <utils/itemviews.h>
#include <utils/qtcassert.h>
#include <utils/treemodel.h>
#include <QDialogButtonBox>
#include <QHeaderView>
#include <QHBoxLayout>
#include <QItemSelectionModel>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QRegExp>
#include <QSortFilterProxyModel>
#include <QVBoxLayout>
using namespace Utils;
namespace ProjectExplorer {
namespace Internal {
const Qt::ItemDataRole IsCustomRole = Qt::UserRole;
class CandidateTreeItem : public TreeItem
{
Q_DECLARE_TR_FUNCTIONS(ProjectExplorer::Internal::AddRunConfigDialog)
public:
CandidateTreeItem(const RunConfigurationCreationInfo &rci, const FileName &projectRoot)
: m_creationInfo(rci), m_projectRoot(projectRoot)
{ }
RunConfigurationCreationInfo creationInfo() const { return m_creationInfo; }
private:
QVariant data(int column, int role) const override
{
QTC_ASSERT(column < 2, return QVariant());
if (role == IsCustomRole)
return m_creationInfo.projectFilePath.isEmpty();
if (column == 0 && role == Qt::DisplayRole)
return m_creationInfo.displayName;
if (column == 1 && role == Qt::DisplayRole) {
FileName displayPath = m_creationInfo.projectFilePath.relativeChildPath(m_projectRoot);
if (displayPath.isEmpty()) {
displayPath = m_creationInfo.projectFilePath;
QTC_CHECK(displayPath.isEmpty());
}
return displayPath.isEmpty() ? tr("[none]") : displayPath.toUserOutput();
}
return QVariant();
}
const RunConfigurationCreationInfo m_creationInfo;
const FileName m_projectRoot;
};
class CandidatesModel : public TreeModel<TreeItem, CandidateTreeItem>
{
Q_DECLARE_TR_FUNCTIONS(ProjectExplorer::Internal::AddRunConfigDialog)
public:
CandidatesModel(Target *target, QObject *parent) : TreeModel(parent)
{
setHeader({tr("Name"), tr("Source")});
for (const RunConfigurationCreationInfo &rci
: RunConfigurationFactory::creatorsForTarget(target)) {
rootItem()->appendChild(new CandidateTreeItem(rci,
target->project()->projectDirectory()));
}
}
};
class ProxyModel : public QSortFilterProxyModel
{
public:
ProxyModel(QObject *parent) : QSortFilterProxyModel(parent) { }
private:
bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override
{
if (source_left.column() == 0) {
// Let's put the fallback candidates last.
const bool leftIsCustom = sourceModel()->data(source_left, IsCustomRole).toBool();
const bool rightIsCustom = sourceModel()->data(source_right, IsCustomRole).toBool();
if (leftIsCustom != rightIsCustom)
return rightIsCustom;
}
return QSortFilterProxyModel::lessThan(source_left, source_right);
}
};
class CandidatesTreeView : public TreeView
{
public:
CandidatesTreeView(QWidget *parent) : TreeView(parent)
{
setUniformRowHeights(true);
}
private:
QSize sizeHint() const override
{
const int width = columnWidth(0) + columnWidth(1);
const int height = qMin(model()->rowCount() + 10, 10) * rowHeight(model()->index(0, 0))
+ header()->sizeHint().height();
return {width, height};
}
};
AddRunConfigDialog::AddRunConfigDialog(Target *target, QWidget *parent)
: QDialog(parent), m_view(new CandidatesTreeView(this))
{
setWindowTitle(tr("Create Run Configuration"));
const auto model = new CandidatesModel(target, this);
const auto proxyModel = new ProxyModel(this);
proxyModel->setSourceModel(model);
const auto filterEdit = new QLineEdit(this);
m_view->setSelectionMode(TreeView::SingleSelection);
m_view->setSelectionBehavior(TreeView::SelectRows);
m_view->setSortingEnabled(true);
m_view->setModel(proxyModel);
m_view->resizeColumnToContents(0);
m_view->resizeColumnToContents(1);
m_view->sortByColumn(0, Qt::AscendingOrder);
const auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Create"));
connect(filterEdit, &QLineEdit::textChanged, this, [proxyModel](const QString &text) {
proxyModel->setFilterRegExp(QRegExp(text, Qt::CaseInsensitive));
});
connect(m_view, &TreeView::doubleClicked, this, [this] { accept(); });
const auto updateOkButton = [buttonBox, this] {
buttonBox->button(QDialogButtonBox::Ok)
->setEnabled(m_view->selectionModel()->hasSelection());
};
connect(m_view->selectionModel(), &QItemSelectionModel::selectionChanged, this, updateOkButton);
updateOkButton();
connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
const auto layout = new QVBoxLayout(this);
const auto filterLayout = new QHBoxLayout;
filterLayout->addWidget(new QLabel(tr("Filter candidates by name:"), this));
filterLayout->addWidget(filterEdit);
layout->addLayout(filterLayout);
layout->addWidget(m_view);
layout->addWidget(buttonBox);
}
void AddRunConfigDialog::accept()
{
const QModelIndexList selected = m_view->selectionModel()->selectedRows();
QTC_ASSERT(selected.count() == 1, return);
const auto * const proxyModel = static_cast<ProxyModel *>(m_view->model());
const auto * const model = static_cast<CandidatesModel *>(proxyModel->sourceModel());
const TreeItem * const item = model->itemForIndex(proxyModel->mapToSource(selected.first()));
QTC_ASSERT(item, return);
m_creationInfo = static_cast<const CandidateTreeItem *>(item)->creationInfo();
QTC_ASSERT(m_creationInfo.id.isValid(), return);
QDialog::accept();
}
} // namespace Internal
} // namespace ProjectExplorer

View File

@@ -0,0 +1,55 @@
/****************************************************************************
**
** Copyright (C) 2019 The Qt Company Ltd.
** Contact: https://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 https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
****************************************************************************/
#pragma once
#include "runconfiguration.h"
#include <QDialog>
namespace Utils { class TreeView; }
namespace ProjectExplorer {
class Target;
namespace Internal {
class AddRunConfigDialog : public QDialog
{
Q_OBJECT
public:
AddRunConfigDialog(Target *target, QWidget *parent);
RunConfigurationCreationInfo creationInfo() const { return m_creationInfo; }
private:
void accept() override;
Utils::TreeView * const m_view;
RunConfigurationCreationInfo m_creationInfo;
};
} // namespace Internal
} // namespace ProjectExplorer

View File

@@ -10,6 +10,7 @@ include(../../shared/clang/clang_defines.pri)
HEADERS += projectexplorer.h \ HEADERS += projectexplorer.h \
abi.h \ abi.h \
abiwidget.h \ abiwidget.h \
addrunconfigdialog.h \
ansifilterparser.h \ ansifilterparser.h \
buildinfo.h \ buildinfo.h \
clangparser.h \ clangparser.h \
@@ -162,6 +163,7 @@ HEADERS += projectexplorer.h \
SOURCES += projectexplorer.cpp \ SOURCES += projectexplorer.cpp \
abi.cpp \ abi.cpp \
abiwidget.cpp \ abiwidget.cpp \
addrunconfigdialog.cpp \
ansifilterparser.cpp \ ansifilterparser.cpp \
buildinfo.cpp \ buildinfo.cpp \
clangparser.cpp \ clangparser.cpp \

View File

@@ -24,6 +24,7 @@ Project {
"abi.cpp", "abi.h", "abi.cpp", "abi.h",
"abiwidget.cpp", "abiwidget.h", "abiwidget.cpp", "abiwidget.h",
"abstractprocessstep.cpp", "abstractprocessstep.h", "abstractprocessstep.cpp", "abstractprocessstep.h",
"addrunconfigdialog.cpp", "addrunconfigdialog.h",
"allprojectsfilter.cpp", "allprojectsfilter.h", "allprojectsfilter.cpp", "allprojectsfilter.h",
"allprojectsfind.cpp", "allprojectsfind.h", "allprojectsfind.cpp", "allprojectsfind.h",
"ansifilterparser.cpp", "ansifilterparser.h", "ansifilterparser.cpp", "ansifilterparser.h",

View File

@@ -466,6 +466,7 @@ RunConfigurationFactory::availableCreators(Target *parent) const
rci.factory = this; rci.factory = this;
rci.id = m_runConfigBaseId; rci.id = m_runConfigBaseId;
rci.buildKey = ti.buildKey; rci.buildKey = ti.buildKey;
rci.projectFilePath = ti.projectFilePath;
rci.displayName = displayName; rci.displayName = displayName;
rci.displayNameUniquifier = ti.displayNameUniquifier; rci.displayNameUniquifier = ti.displayNameUniquifier;
rci.creationMode = ti.isQtcRunnable || !hasAnyQtcRunnable rci.creationMode = ti.isQtcRunnable || !hasAnyQtcRunnable

View File

@@ -235,6 +235,7 @@ public:
QString buildKey; QString buildKey;
QString displayName; QString displayName;
QString displayNameUniquifier; QString displayNameUniquifier;
Utils::FileName projectFilePath;
CreationMode creationMode = AlwaysCreate; CreationMode creationMode = AlwaysCreate;
bool useTerminal = false; bool useTerminal = false;
}; };

View File

@@ -25,6 +25,7 @@
#include "runsettingspropertiespage.h" #include "runsettingspropertiespage.h"
#include "addrunconfigdialog.h"
#include "buildstepspage.h" #include "buildstepspage.h"
#include "deployconfiguration.h" #include "deployconfiguration.h"
#include "runconfiguration.h" #include "runconfiguration.h"
@@ -92,7 +93,7 @@ RunSettingsWidget::RunSettingsWidget(Target *target) :
m_runConfigurationCombo->setSizeAdjustPolicy(QComboBox::AdjustToContents); m_runConfigurationCombo->setSizeAdjustPolicy(QComboBox::AdjustToContents);
m_runConfigurationCombo->setMinimumContentsLength(15); m_runConfigurationCombo->setMinimumContentsLength(15);
m_addRunToolButton = new QPushButton(tr("Add"), this); m_addRunToolButton = new QPushButton(tr("Add..."), this);
m_removeRunToolButton = new QPushButton(tr("Remove"), this); m_removeRunToolButton = new QPushButton(tr("Remove"), this);
m_renameRunButton = new QPushButton(tr("Rename..."), this); m_renameRunButton = new QPushButton(tr("Rename..."), this);
m_cloneRunButton = new QPushButton(tr("Clone..."), this); m_cloneRunButton = new QPushButton(tr("Clone..."), this);
@@ -187,8 +188,6 @@ RunSettingsWidget::RunSettingsWidget(Target *target) :
m_runLayout->addLayout(disabledHBox); m_runLayout->addLayout(disabledHBox);
m_addRunMenu = new QMenu(m_addRunToolButton);
m_addRunToolButton->setMenu(m_addRunMenu);
RunConfiguration *rc = m_target->activeRunConfiguration(); RunConfiguration *rc = m_target->activeRunConfiguration();
m_runConfigurationCombo->setModel(m_runConfigurationsModel); m_runConfigurationCombo->setModel(m_runConfigurationsModel);
m_runConfigurationCombo->setCurrentIndex( m_runConfigurationCombo->setCurrentIndex(
@@ -200,8 +199,8 @@ RunSettingsWidget::RunSettingsWidget(Target *target) :
setConfigurationWidget(rc); setConfigurationWidget(rc);
connect(m_addRunMenu, &QMenu::aboutToShow, connect(m_addRunToolButton, &QAbstractButton::clicked,
this, &RunSettingsWidget::aboutToShowAddMenu); this, &RunSettingsWidget::showAddRunConfigDialog);
connect(m_runConfigurationCombo, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), connect(m_runConfigurationCombo, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
this, &RunSettingsWidget::currentRunConfigurationChanged); this, &RunSettingsWidget::currentRunConfigurationChanged);
connect(m_removeRunToolButton, &QAbstractButton::clicked, connect(m_removeRunToolButton, &QAbstractButton::clicked,
@@ -225,28 +224,20 @@ RunSettingsWidget::RunSettingsWidget(Target *target) :
this, &RunSettingsWidget::activeRunConfigurationChanged); this, &RunSettingsWidget::activeRunConfigurationChanged);
} }
void RunSettingsWidget::aboutToShowAddMenu() void RunSettingsWidget::showAddRunConfigDialog()
{ {
m_addRunMenu->clear(); AddRunConfigDialog dlg(m_target, this);
QList<QAction *> menuActions; if (dlg.exec() != QDialog::Accepted)
for (const RunConfigurationCreationInfo &item : return;
RunConfigurationFactory::creatorsForTarget(m_target)) { RunConfigurationCreationInfo rci = dlg.creationInfo();
auto action = new QAction(item.displayName, m_addRunMenu); QTC_ASSERT(rci.id.isValid(), return);
connect(action, &QAction::triggered, [item, this] { RunConfiguration *newRC = rci.create(m_target);
RunConfiguration *newRC = item.create(m_target);
if (!newRC) if (!newRC)
return; return;
QTC_CHECK(newRC->id() == item.id); QTC_CHECK(newRC->id() == rci.id);
m_target->addRunConfiguration(newRC); m_target->addRunConfiguration(newRC);
m_target->setActiveRunConfiguration(newRC); m_target->setActiveRunConfiguration(newRC);
m_removeRunToolButton->setEnabled(m_target->runConfigurations().size() > 1); m_removeRunToolButton->setEnabled(m_target->runConfigurations().size() > 1);
});
menuActions.append(action);
}
Utils::sort(menuActions, &QAction::text);
foreach (QAction *action, menuActions)
m_addRunMenu->addAction(action);
} }
void RunSettingsWidget::cloneRunConfiguration() void RunSettingsWidget::cloneRunConfiguration()

View File

@@ -58,7 +58,7 @@ public:
private: private:
void currentRunConfigurationChanged(int index); void currentRunConfigurationChanged(int index);
void aboutToShowAddMenu(); void showAddRunConfigDialog();
void cloneRunConfiguration(); void cloneRunConfiguration();
void removeRunConfiguration(); void removeRunConfiguration();
void activeRunConfigurationChanged(); void activeRunConfigurationChanged();
@@ -91,7 +91,6 @@ private:
NamedWidget *m_deployConfigurationWidget = nullptr; NamedWidget *m_deployConfigurationWidget = nullptr;
QVBoxLayout *m_deployLayout = nullptr; QVBoxLayout *m_deployLayout = nullptr;
BuildStepListWidget *m_deploySteps = nullptr; BuildStepListWidget *m_deploySteps = nullptr;
QMenu *m_addRunMenu;
QMenu *m_addDeployMenu; QMenu *m_addDeployMenu;
bool m_ignoreChange = false; bool m_ignoreChange = false;
using RunConfigItem = QPair<QWidget *, QLabel *>; using RunConfigItem = QPair<QWidget *, QLabel *>;