Commit 27cb595b authored by Friedemann Kleint's avatar Friedemann Kleint

Fixes: Add common configuration to the VCSA base plugin; submit message check...

Fixes: Add common configuration to the VCSA base plugin; submit message check script and user name configuration.                                           Details: Extend submit editor widget by configureable fields. Use them in the VCS base submit editor to specify users, provide completion and selection dialog for them.
parent 40eab898
......@@ -33,8 +33,15 @@
#include <QtCore/QDebug>
#include <QtCore/QPointer>
#include <QtCore/QTimer>
#include <QtCore/QSignalMapper>
#include <QtGui/QPushButton>
#include <QtGui/QMenu>
#include <QtGui/QLineEdit>
#include <QtGui/QFormLayout>
#include <QtGui/QHBoxLayout>
#include <QtGui/QToolButton>
#include <QtGui/QSpacerItem>
enum { debug = 0 };
......@@ -104,8 +111,12 @@ QList<int> selectedRows(const QAbstractItemView *view)
}
// ----------- SubmitEditorWidgetPrivate
struct SubmitEditorWidgetPrivate
{
// A pair of position/action to extend context menus
typedef QPair<int, QPointer<QAction> > AdditionalContextMenuAction;
SubmitEditorWidgetPrivate();
Ui::SubmitEditorWidget m_ui;
......@@ -113,13 +124,22 @@ struct SubmitEditorWidgetPrivate
bool m_filesChecked;
int m_fileNameColumn;
int m_activatedRow;
QList<AdditionalContextMenuAction> descriptionEditContextMenuActions;
QFormLayout *m_fieldLayout;
// Field entries (label, line edits)
typedef QPair<QString, QLineEdit*> FieldEntry;
QList<FieldEntry> m_fieldEntries;
QSignalMapper *m_fieldSignalMapper;
};
SubmitEditorWidgetPrivate::SubmitEditorWidgetPrivate() :
m_filesSelected(false),
m_filesChecked(false),
m_fileNameColumn(1),
m_activatedRow(-1)
m_activatedRow(-1),
m_fieldLayout(0),
m_fieldSignalMapper(0)
{
}
......@@ -128,6 +148,10 @@ SubmitEditorWidget::SubmitEditorWidget(QWidget *parent) :
m_d(new SubmitEditorWidgetPrivate)
{
m_d->m_ui.setupUi(this);
m_d->m_ui.description->setContextMenuPolicy(Qt::CustomContextMenu);
connect(m_d->m_ui.description, SIGNAL(customContextMenuRequested(QPoint)),
this, SLOT(editorCustomContextMenuRequested(QPoint)));
// File List
m_d->m_ui.fileView->setSelectionMode(QAbstractItemView::ExtendedSelection);
m_d->m_ui.fileView->setRootIsDecorated(false);
......@@ -212,7 +236,18 @@ QString SubmitEditorWidget::trimmedDescriptionText() const
QString SubmitEditorWidget::descriptionText() const
{
return m_d->m_ui.description->toPlainText();
QString rc = m_d->m_ui.description->toPlainText();
// append field entries
foreach(const SubmitEditorWidgetPrivate::FieldEntry &fe, m_d->m_fieldEntries) {
const QString fieldText = fe.second->text().trimmed();
if (!fieldText.isEmpty()) {
rc += fe.first;
rc += QLatin1Char(' ');
rc += fieldText;
rc += QLatin1Char('\n');
}
}
return rc;
}
void SubmitEditorWidget::setDescriptionText(const QString &text)
......@@ -381,6 +416,74 @@ void SubmitEditorWidget::insertTopWidget(QWidget *w)
m_d->m_ui.vboxLayout->insertWidget(0, w);
}
void SubmitEditorWidget::addDescriptionEditContextMenuAction(QAction *a)
{
m_d->descriptionEditContextMenuActions.push_back(SubmitEditorWidgetPrivate::AdditionalContextMenuAction(-1, a));
}
void SubmitEditorWidget::insertDescriptionEditContextMenuAction(int pos, QAction *a)
{
m_d->descriptionEditContextMenuActions.push_back(SubmitEditorWidgetPrivate::AdditionalContextMenuAction(pos, a));
}
void SubmitEditorWidget::editorCustomContextMenuRequested(const QPoint &pos)
{
QMenu *menu = m_d->m_ui.description->createStandardContextMenu();
// Extend
foreach (const SubmitEditorWidgetPrivate::AdditionalContextMenuAction &a, m_d->descriptionEditContextMenuActions) {
if (a.second) {
if (a.first >= 0) {
menu->insertAction(menu->actions().at(a.first), a.second);
} else {
menu->addAction(a.second);
}
}
}
menu->exec(m_d->m_ui.description->mapToGlobal(pos));
delete menu;
}
QLineEdit *SubmitEditorWidget::addField(const QString &label, bool hasDialogButton)
{
// Insert the form layout below the editor
if (!m_d->m_fieldLayout) {
QHBoxLayout *outerLayout = new QHBoxLayout;
m_d->m_fieldLayout = new QFormLayout;
outerLayout->addLayout(m_d->m_fieldLayout);
outerLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Ignored));
QBoxLayout *descrLayout = qobject_cast<QBoxLayout*>(m_d->m_ui.descriptionBox->layout());
Q_ASSERT(descrLayout);
descrLayout->addLayout(outerLayout);
}
if (hasDialogButton && !m_d->m_fieldSignalMapper) {
m_d->m_fieldSignalMapper = new QSignalMapper;
connect(m_d->m_fieldSignalMapper, SIGNAL(mapped(int)), this, SIGNAL(fieldDialogRequested(int)));
}
// Add a field row consisting of label and line edit
QLineEdit *lineEdit = new QLineEdit;
QHBoxLayout *fieldLayout = new QHBoxLayout;
fieldLayout->addWidget(lineEdit);
if (hasDialogButton) {
QToolButton *dialogButton = new QToolButton;
dialogButton->setText(tr("..."));
connect(dialogButton, SIGNAL(clicked()), m_d->m_fieldSignalMapper, SLOT(map()));
m_d->m_fieldSignalMapper->setMapping(dialogButton, m_d->m_fieldEntries.size());
fieldLayout->addWidget(dialogButton);
}
QToolButton *clearButton = new QToolButton;
clearButton->setText(tr("Clear"));
connect(clearButton, SIGNAL(clicked()), lineEdit, SLOT(clear()));
fieldLayout->addWidget(clearButton);
m_d->m_fieldLayout->addRow(label, fieldLayout);
m_d->m_fieldEntries.push_back(SubmitEditorWidgetPrivate::FieldEntry(label, lineEdit));
return lineEdit;
}
QLineEdit *SubmitEditorWidget::fieldLineEdit(int i) const
{
return m_d->m_fieldEntries.at(i).second;
}
} // namespace Utils
} // namespace Core
......
......@@ -41,6 +41,7 @@ class QListWidgetItem;
class QAction;
class QAbstractItemModel;
class QModelIndex;
class QLineEdit;
QT_END_NAMESPACE
namespace Core {
......@@ -104,10 +105,20 @@ public:
QPlainTextEdit *descriptionEdit() const;
void addDescriptionEditContextMenuAction(QAction *a);
void insertDescriptionEditContextMenuAction(int pos, QAction *a);
// Fields are additional fields consisting of a Label and a Line Edit.
// A field dialog is wired to a button labeled "..." that pops up a chooser
// resulting in text being set
QLineEdit *addField(const QString &label, bool hasDialogButton);
QLineEdit *fieldLineEdit(int i) const;
signals:
void diffSelected(const QStringList &);
void fileSelectionChanged(bool someFileSelected);
void fileCheckStateChanged(bool someFileChecked);
void fieldDialogRequested(int);
protected:
virtual void changeEvent(QEvent *e);
......@@ -120,6 +131,7 @@ private slots:
void updateActions();
void updateSubmitAction();
void updateDiffAction();
void editorCustomContextMenuRequested(const QPoint &);
private:
bool hasSelection() const;
......
......@@ -673,12 +673,14 @@ bool GitPlugin::editorAboutToClose(Core::IEditor *iEditor)
if (editorFile.absoluteFilePath() != changeFile.absoluteFilePath())
return true;
// Prompt user.
const QMessageBox::StandardButton answer = QMessageBox::question(m_core->mainWindow(), tr("Closing git editor"), tr("Do you want to commit the change?"),
QMessageBox::Yes|QMessageBox::No|QMessageBox::Cancel, QMessageBox::Yes);
const VCSBase::VCSBaseSubmitEditor::PromptSubmitResult answer =
editor->promptSubmit(tr("Closing git editor"),
tr("Do you want to commit the change?"),
tr("The commit message check failed. Do you want to commit the change?"));
switch (answer) {
case QMessageBox::Cancel:
case VCSBase::VCSBaseSubmitEditor::SubmitCanceled:
return false; // Keep editing and change file
case QMessageBox::No:
case VCSBase::VCSBaseSubmitEditor::SubmitDiscarded:
cleanChangeTmpFile();
return true; // Cancel all
default:
......
......@@ -961,21 +961,24 @@ bool PerforcePlugin::editorAboutToClose(Core::IEditor *editor)
Core::IFile *fileIFace = editor->file();
if (!fileIFace)
return true;
const PerforceSubmitEditor *perforceEditor = qobject_cast<PerforceSubmitEditor *>(editor);
if (!perforceEditor)
return true;
QFileInfo editorFile(fileIFace->fileName());
QFileInfo changeFile(m_changeTmpFile->fileName());
if (editorFile.absoluteFilePath() == changeFile.absoluteFilePath()) {
const QMessageBox::StandardButton answer =
QMessageBox::question(core->mainWindow(),
tr("Closing p4 Editor"),
tr("Do you want to submit this change list?"),
QMessageBox::Yes|QMessageBox::No|QMessageBox::Cancel, QMessageBox::Yes);
if (answer == QMessageBox::Cancel)
if (editorFile.absoluteFilePath() == changeFile.absoluteFilePath()) {
const VCSBase::VCSBaseSubmitEditor::PromptSubmitResult answer =
perforceEditor->promptSubmit(tr("Closing p4 Editor"),
tr("Do you want to submit this change list?"),
tr("The commit message check failed. Do you want to submit this change list"));
if (answer == VCSBase::VCSBaseSubmitEditor::SubmitCanceled)
return false;
core->fileManager()->blockFileChange(fileIFace);
fileIFace->save();
core->fileManager()->unblockFileChange(fileIFace);
if (answer == QMessageBox::Yes) {
if (answer == VCSBase::VCSBaseSubmitEditor::SubmitConfirmed) {
QByteArray change = m_changeTmpFile->readAll();
m_changeTmpFile->close();
if (!checkP4Command()) {
......
......@@ -464,14 +464,14 @@ bool SubversionPlugin::editorAboutToClose(Core::IEditor *iEditor)
return true; // Oops?!
// Prompt user.
const QMessageBox::StandardButton answer = QMessageBox::question(
Core::ICore::instance()->mainWindow(), tr("Closing Subversion Editor"),
tr("Do you want to commit the change?"),
QMessageBox::Yes|QMessageBox::No|QMessageBox::Cancel, QMessageBox::Yes);
const VCSBase::VCSBaseSubmitEditor::PromptSubmitResult answer =
editor->promptSubmit(tr("Closing Subversion Editor"),
tr("Do you want to commit the change?"),
tr("The commit message check failed. Do you want to commit the change?"));
switch (answer) {
case QMessageBox::Cancel:
case VCSBase::VCSBaseSubmitEditor::SubmitCanceled:
return false; // Keep editing and change file
case QMessageBox::No:
case VCSBase::VCSBaseSubmitEditor::SubmitDiscarded:
cleanChangeTmpFile();
return true; // Cancel all
default:
......
/**************************************************************************
**
** This file is part of Qt Creator
**
** Copyright (c) 2009 Nokia Corporation and/or its subsidiary(-ies).
**
** Contact: Qt Software Information (qt-info@nokia.com)
**
** Commercial Usage
**
** Licensees holding valid Qt Commercial licenses may use this file in
** accordance with the Qt Commercial License Agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and Nokia.
**
** 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 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 2.1 requirements
** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** If you are unsure which license is appropriate for your use, please
** contact the sales department at qt-sales@nokia.com.
**
**************************************************************************/
#include "nicknamedialog.h"
#include "ui_nicknamedialog.h"
#include <QtCore/QDebug>
#include <QtCore/QFile>
#include <QtGui/QPushButton>
#include <QtGui/QStandardItemModel>
#include <QtGui/QSortFilterProxyModel>
namespace VCSBase {
namespace Internal {
struct NickEntry {
void clear();
bool parse(const QString &);
QString nickName() const;
QString name;
QString email;
QString aliasName;
QString aliasEmail;
};
void NickEntry::clear()
{
name.clear();
email.clear();
aliasName.clear();
aliasEmail.clear();
}
// Parse "Hans Mustermann <HM@acme.de> [Alias [<alias@acme.de>]]"
bool NickEntry::parse(const QString &l)
{
clear();
const QChar lessThan = QLatin1Char('<');
const QChar greaterThan = QLatin1Char('>');
// Get first name/mail pair
int mailPos = l.indexOf(lessThan);
if (mailPos == -1)
return false;
name = l.mid(0, mailPos).trimmed();
mailPos++;
const int mailEndPos = l.indexOf(greaterThan, mailPos);
if (mailEndPos == -1)
return false;
email = l.mid(mailPos, mailEndPos - mailPos);
// get optional 2nd name/mail pair
const int aliasNameStart = mailEndPos + 1;
if (aliasNameStart >= l.size())
return true;
int aliasMailPos = l.indexOf(lessThan, aliasNameStart);
if (aliasMailPos == -1) {
aliasName =l.mid(aliasNameStart, l.size() - aliasNameStart).trimmed();
return true;
}
aliasName = l.mid(aliasNameStart, aliasMailPos - aliasNameStart).trimmed();
aliasMailPos++;
const int aliasMailEndPos = l.indexOf(greaterThan, aliasMailPos);
if (aliasMailEndPos == -1)
return true;
aliasEmail = l.mid(aliasMailPos, aliasMailEndPos - aliasMailPos);
return true;
}
// Format "Hans Mustermann <HM@acme.de>"
static inline QString formatNick(const QString &name, const QString &email)
{
QString rc = name;
if (!email.isEmpty()) {
rc += QLatin1String(" <");
rc += email;
rc += QLatin1Char('>');
}
return rc;
}
QString NickEntry::nickName() const
{
return aliasName.isEmpty() ? formatNick(name, email) : formatNick(aliasName, aliasEmail);
}
// Sort by name
bool operator<(const NickEntry &n1, const NickEntry &n2)
{
return n1.name < n2.name;
}
QDebug operator<<(QDebug d, const NickEntry &e)
{
d.nospace() << "Name='" << e.name << "' Mail='" << e.email
<< " Alias='" << e.aliasName << " AliasEmail='" << e.aliasEmail << "'\n";
return d;
}
// Globally cached list
static QList<NickEntry> &nickList()
{
static QList<NickEntry> rc;
return rc;
}
// Create a model populated with the names
static QStandardItemModel *createModel(QObject *parent)
{
QStandardItemModel *rc = new QStandardItemModel(parent);
QStringList headers;
headers << NickNameDialog::tr("Name")
<< NickNameDialog::tr("E-mail")
<< NickNameDialog::tr("Alias")
<< NickNameDialog::tr("Alias e-mail");
rc->setHorizontalHeaderLabels(headers);
foreach(const NickEntry &ne, nickList()) {
QList<QStandardItem *> row;
row.push_back(new QStandardItem(ne.name));
row.push_back(new QStandardItem(ne.email));
row.push_back(new QStandardItem(ne.aliasName));
row.push_back(new QStandardItem(ne.aliasEmail));
rc->appendRow(row);
}
return rc;
}
NickNameDialog::NickNameDialog(QWidget *parent) :
QDialog(parent),
m_ui(new Ui::NickNameDialog),
m_filterModel(new QSortFilterProxyModel(this))
{
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
m_ui->setupUi(this);
okButton()->setEnabled(false);
// Populate model and grow tree to accommodate it
m_filterModel->setSourceModel(createModel(this));
m_filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
m_ui->filterTreeView->setModel(m_filterModel);
const int columnCount = m_filterModel->columnCount();
int treeWidth = 0;
for (int c = 0; c < columnCount; c++) {
m_ui->filterTreeView->resizeColumnToContents(c);
treeWidth += m_ui->filterTreeView->columnWidth(c);
}
m_ui->filterTreeView->setMinimumWidth(treeWidth + 20);
connect(m_ui->filterTreeView, SIGNAL(doubleClicked(QModelIndex)), this,
SLOT(slotDoubleClicked(QModelIndex)));
connect(m_ui->filterTreeView->selectionModel(), SIGNAL(currentRowChanged(QModelIndex,QModelIndex)),
this, SLOT(slotCurrentItemChanged(QModelIndex)));
connect(m_ui->filterLineEdit, SIGNAL(textChanged(QString)),
m_filterModel, SLOT(setFilterFixedString(QString)));
}
NickNameDialog::~NickNameDialog()
{
delete m_ui;
}
QPushButton *NickNameDialog::okButton() const
{
return m_ui->buttonBox->button(QDialogButtonBox::Ok);
}
void NickNameDialog::slotCurrentItemChanged(const QModelIndex &index)
{
okButton()->setEnabled(index.isValid());
}
void NickNameDialog::slotDoubleClicked(const QModelIndex &)
{
if (okButton()->isEnabled())
okButton()->animateClick();
}
QString NickNameDialog::nickName() const
{
const QModelIndex index = m_ui->filterTreeView->selectionModel()->currentIndex();
if (index.isValid()) {
const QModelIndex sourceIndex = m_filterModel->mapToSource(index);
return nickList().at(sourceIndex.row()).nickName();
}
return QString();
}
void NickNameDialog::clearNickNames()
{
nickList().clear();
}
bool NickNameDialog::readNickNamesFromMailCapFile(const QString &fileName, QString *errorMessage)
{
QFile file(fileName);
if (!file.open(QIODevice::ReadOnly|QIODevice::Text)) {
*errorMessage = tr("Cannot open '%1': %2").arg(fileName, file.errorString());
return false;
}
// Split into lines and read
QList<NickEntry> &nl = nickList();
nl.clear();
NickEntry entry;
const QStringList lines = QString::fromUtf8(file.readAll()).trimmed().split(QLatin1Char('\n'));
const int count = lines.size();
for (int i = 0; i < count; i++) {
if (entry.parse(lines.at(i))) {
nl.push_back(entry);
} else {
qWarning("%s: Invalid mail cap entry at line %d: '%s'\n", qPrintable(fileName), i + 1, qPrintable(lines.at(i)));
}
}
qStableSort(nl);
return true;
}
QStringList NickNameDialog::nickNameList()
{
QStringList rc;
foreach(const NickEntry &ne, nickList())
rc.push_back(ne.nickName());
return rc;
}
}
}
/**************************************************************************
**
** This file is part of Qt Creator
**
** Copyright (c) 2009 Nokia Corporation and/or its subsidiary(-ies).
**
** Contact: Qt Software Information (qt-info@nokia.com)
**
** Commercial Usage
**
** Licensees holding valid Qt Commercial licenses may use this file in
** accordance with the Qt Commercial License Agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and Nokia.
**
** 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 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 2.1 requirements
** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** If you are unsure which license is appropriate for your use, please
** contact the sales department at qt-sales@nokia.com.
**
**************************************************************************/
#ifndef NICKNAMEDIALOG_H
#define NICKNAMEDIALOG_H
#include <QtGui/QDialog>
QT_BEGIN_NAMESPACE
namespace Ui {
class NickNameDialog;
}
class QSortFilterProxyModel;
class QModelIndex;
class QPushButton;
QT_END_NAMESPACE
namespace VCSBase {
namespace Internal {
/* Nick name dialog: Manages a list of users read from an extended
* mail cap file, consisting of 4 columns:
* "Name Mail [AliasName [AliasMail]]".
* The names can be used for insertion into "RevBy:" fields; aliases will
* be preferred. */
class NickNameDialog : public QDialog {
Q_OBJECT
public:
explicit NickNameDialog(QWidget *parent = 0);
virtual ~NickNameDialog();
QString nickName() const;
// Fill/clear the global nick name cache
static bool readNickNamesFromMailCapFile(const QString &file, QString *errorMessage);
static void clearNickNames();
// Return a list for a completer on the field line edits
static QStringList nickNameList();
private slots:
void slotCurrentItemChanged(const QModelIndex &);
void slotDoubleClicked(const QModelIndex &);
private:
QPushButton *okButton() const;
Ui::NickNameDialog *m_ui;
QSortFilterProxyModel *m_filterModel;
};
} // namespace Internal
} // namespace VCSBase
#endif // NICKNAMEDIALOG_H
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>NickNameDialog</class>
<widget class="QDialog" name="NickNameDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>618</width>
<height>414</height>
</rect>
</property>
<property name="windowTitle">
<string>Nick Names</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">