Commit 17daca60 authored by Friedemann Kleint's avatar Friedemann Kleint

Fixes: Use a model for the submit file list (to resolve the git diff mess)

parent 349c055d
......@@ -36,6 +36,7 @@
#include <QtCore/QDebug>
#include <QtCore/QPointer>
#include <QtCore/QTimer>
#include <QtGui/QPushButton>
......@@ -70,6 +71,42 @@ void QActionPushButton::actionChanged()
setEnabled(a->isEnabled());
}
// Helpers to retrieve model data
static inline bool listModelChecked(const QAbstractItemModel *model, int row, int column = 0)
{
const QModelIndex checkableIndex = model->index(row, column, QModelIndex());
return model->data(checkableIndex, Qt::CheckStateRole).toInt() == Qt::Checked;
}
static inline QString listModelText(const QAbstractItemModel *model, int row, int column)
{
const QModelIndex index = model->index(row, column, QModelIndex());
return model->data(index, Qt::DisplayRole).toString();
}
// Find a check item in a model
static bool listModelContainsCheckedItem(const QAbstractItemModel *model)
{
const int count = model->rowCount();
for (int i = 0; i < count; i++)
if (listModelChecked(model, i, 0))
return true;
return false;
}
// Convenience to extract a list of selected indexes
QList<int> selectedRows(const QAbstractItemView *view)
{
const QModelIndexList indexList = view->selectionModel()->selectedRows(0);
if (indexList.empty())
return QList<int>();
QList<int> rc;
const QModelIndexList::const_iterator cend = indexList.constEnd();
for (QModelIndexList::const_iterator it = indexList.constBegin(); it != cend; ++it)
rc.push_back(it->row());
return rc;
}
// ----------- SubmitEditorWidgetPrivate
struct SubmitEditorWidgetPrivate
{
......@@ -78,11 +115,15 @@ struct SubmitEditorWidgetPrivate
Ui::SubmitEditorWidget m_ui;
bool m_filesSelected;
bool m_filesChecked;
int m_fileNameColumn;
int m_activatedRow;
};
SubmitEditorWidgetPrivate::SubmitEditorWidgetPrivate() :
m_filesSelected(false),
m_filesChecked(false)
m_filesChecked(false),
m_fileNameColumn(1),
m_activatedRow(-1)
{
}
......@@ -92,10 +133,10 @@ SubmitEditorWidget::SubmitEditorWidget(QWidget *parent) :
{
m_d->m_ui.setupUi(this);
// File List
m_d->m_ui.fileList->setSelectionMode(QAbstractItemView::ExtendedSelection);
connect(m_d->m_ui.fileList, SIGNAL(itemActivated(QListWidgetItem*)), this, SLOT(triggerDiffSelected()));
connect(m_d->m_ui.fileList, SIGNAL(itemChanged(QListWidgetItem*)), this, SLOT(fileItemChanged(QListWidgetItem*)));
connect(m_d->m_ui.fileList, SIGNAL(itemSelectionChanged()), this, SLOT(fileSelectionChanged()));
m_d->m_ui.fileView->setSelectionMode(QAbstractItemView::ExtendedSelection);
m_d->m_ui.fileView->setRootIsDecorated(false);
connect(m_d->m_ui.fileView, SIGNAL(doubleClicked(QModelIndex)),
this, SLOT(diffActivated(QModelIndex)));
// Text
m_d->m_ui.description->setFont(QFont(QLatin1String("Courier")));
......@@ -124,8 +165,12 @@ void SubmitEditorWidget::registerActions(QAction *editorUndoAction, QAction *ed
}
if (submitAction) {
if (debug)
qDebug() << submitAction << m_d->m_ui.fileList->count() << "items" << m_d->m_filesChecked;
if (debug) {
int count = 0;
if (const QAbstractItemModel *model = m_d->m_ui.fileView->model())
count = model->rowCount();
qDebug() << submitAction << count << "items" << m_d->m_filesChecked;
}
submitAction->setEnabled(m_d->m_filesChecked);
connect(this, SIGNAL(fileCheckStateChanged(bool)), submitAction, SLOT(setEnabled(bool)));
m_d->m_ui.buttonLayout->addWidget(new QActionPushButton(submitAction));
......@@ -161,7 +206,6 @@ void SubmitEditorWidget::unregisterActions(QAction *editorUndoAction, QAction *
}
}
QString SubmitEditorWidget::trimmedDescriptionText() const
{
// Make sure we have one terminating NL
......@@ -180,91 +224,70 @@ void SubmitEditorWidget::setDescriptionText(const QString &text)
m_d->m_ui.description->setPlainText(text);
}
QStringList SubmitEditorWidget::fileList() const
int SubmitEditorWidget::fileNameColumn() const
{
QStringList rc;
const int count = m_d->m_ui.fileList->count();
for (int i = 0; i < count; i++)
rc.push_back(m_d->m_ui.fileList->item(i)->text());
return rc;
return m_d->m_fileNameColumn;
}
void SubmitEditorWidget::addFilesUnblocked(const QStringList &list, bool checked, bool userCheckable)
void SubmitEditorWidget::setFileNameColumn(int c)
{
if (debug)
qDebug() << Q_FUNC_INFO << list << checked << userCheckable;
foreach (const QString &f, list) {
QListWidgetItem *item = new QListWidgetItem(f);
item->setCheckState(checked ? Qt::Checked : Qt::Unchecked);
if (!userCheckable)
item->setFlags(item->flags() & ~Qt::ItemIsUserCheckable);
m_d->m_ui.fileList->addItem(item);
}
m_d->m_fileNameColumn = c;
}
void SubmitEditorWidget::addFiles(const QStringList &list, bool checked, bool userCheckable)
void SubmitEditorWidget::setFileModel(QAbstractItemModel *model)
{
if (list.empty())
return;
const bool blocked = m_d->m_ui.fileList->blockSignals(true);
addFilesUnblocked(list, checked, userCheckable);
m_d->m_ui.fileList->blockSignals(blocked);
// Did we gain any checked files..update action accordingly
if (!m_d->m_filesChecked && checked) {
m_d->m_filesChecked = true;
emit fileCheckStateChanged(m_d->m_filesChecked);
}
}
m_d->m_ui.fileView->clearSelection(); // trigger the change signals
void SubmitEditorWidget::setFileList(const QStringList &list)
{
// Trigger enabling of menu action
m_d->m_ui.fileList->clearSelection();
m_d->m_ui.fileView->setModel(model);
const bool blocked = m_d->m_ui.fileList->blockSignals(true);
m_d->m_ui.fileList->clear();
if (!list.empty()) {
addFilesUnblocked(list, true, true);
// Checked files added?
if (!m_d->m_filesChecked) {
m_d->m_filesChecked = true;
emit fileCheckStateChanged(m_d->m_filesChecked);
}
if (model->rowCount()) {
const int columnCount = model->columnCount();
for (int c = 0; c < columnCount; c++)
m_d->m_ui.fileView->resizeColumnToContents(c);
}
m_d->m_ui.fileList->blockSignals(blocked);
connect(model, SIGNAL(dataChanged(QModelIndex,QModelIndex)),
this, SLOT(updateSubmitAction()));
connect(model, SIGNAL(modelReset()),
this, SLOT(updateSubmitAction()));
connect(model, SIGNAL(rowsInserted(QModelIndex,int,int)),
this, SLOT(updateSubmitAction()));
connect(model, SIGNAL(rowsRemoved(QModelIndex,int,int)),
this, SLOT(updateSubmitAction()));
connect(m_d->m_ui.fileView->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)),
this, SLOT(updateDiffAction()));
updateActions();
}
static bool containsCheckState(const QListWidget *lw, Qt::CheckState cs)
QAbstractItemModel *SubmitEditorWidget::fileModel() const
{
const int count = lw->count();
for (int i = 0; i < count; i++)
if (lw->item(i)->checkState() == cs)
return true;
return false;
return m_d->m_ui.fileView->model();
}
QStringList SubmitEditorWidget::selectedFiles() const
{
const QList<int> selection = selectedRows(m_d->m_ui.fileView);
if (selection.empty())
return QStringList();
QStringList rc;
const int count = m_d->m_ui.fileList->count();
for (int i = 0; i < count; i++) {
const QListWidgetItem *item = m_d->m_ui.fileList->item(i);
if (item->isSelected())
rc.push_back(item->text());
}
const QAbstractItemModel *model = m_d->m_ui.fileView->model();
const int count = selection.size();
for (int i = 0; i < count; i++)
rc.push_back(listModelText(model, selection.at(i), fileNameColumn()));
return rc;
}
QStringList SubmitEditorWidget::checkedFiles() const
{
QStringList rc;
const int count = m_d->m_ui.fileList->count();
for (int i = 0; i < count; i++) {
const QListWidgetItem *item = m_d->m_ui.fileList->item(i);
if (item->checkState() == Qt::Checked)
rc.push_back(item->text());
}
const QAbstractItemModel *model = m_d->m_ui.fileView->model();
if (!model)
return rc;
const int count = model->rowCount();
for (int i = 0; i < count; i++)
if (listModelChecked(model, i, 0))
rc.push_back(listModelText(model, i, fileNameColumn()));
return rc;
}
......@@ -280,44 +303,61 @@ void SubmitEditorWidget::triggerDiffSelected()
emit diffSelected(sel);
}
void SubmitEditorWidget::fileItemChanged(QListWidgetItem *item)
{
const Qt::CheckState st = item->checkState();
if (debug)
qDebug() << Q_FUNC_INFO << st << item->text() << m_d->m_filesChecked;
// Enable the actions according to check state
switch (st) {
case Qt::Unchecked: // Item was unchecked: Any checked items left?
if (m_d->m_filesChecked && !containsCheckState(m_d->m_ui.fileList, Qt::Checked)) {
m_d->m_filesChecked = false;
emit fileCheckStateChanged(m_d->m_filesChecked);
}
break;
case Qt::Checked:
// Item was Checked. First one?
if (!m_d->m_filesChecked) {
m_d->m_filesChecked = true;
emit fileCheckStateChanged(m_d->m_filesChecked);
}
break;
case Qt::PartiallyChecked: // Errm?
break;
void SubmitEditorWidget::diffActivatedDelayed()
{
const QStringList files = QStringList(listModelText(m_d->m_ui.fileView->model(), m_d->m_activatedRow, fileNameColumn()));
emit diffSelected(files);
}
void SubmitEditorWidget::diffActivated(const QModelIndex &index)
{
// We need to delay the signal, otherwise, the diff editor will not
// be in the foreground.
m_d->m_activatedRow = index.row();
QTimer::singleShot(0, this, SLOT(diffActivatedDelayed()));
}
void SubmitEditorWidget::updateActions()
{
updateSubmitAction();
updateDiffAction();
}
// Enable submit depending on having checked files
void SubmitEditorWidget::updateSubmitAction()
{
const bool newFilesCheckedState = hasCheckedFiles();
if (m_d->m_filesChecked != newFilesCheckedState) {
m_d->m_filesChecked = newFilesCheckedState;
emit fileCheckStateChanged(m_d->m_filesChecked);
}
}
void SubmitEditorWidget::fileSelectionChanged()
// Enable diff depending on selected files
void SubmitEditorWidget::updateDiffAction()
{
const bool newFilesSelected = !m_d->m_ui.fileList->selectedItems().empty();
if (debug)
qDebug() << Q_FUNC_INFO << newFilesSelected;
if (m_d->m_filesSelected != newFilesSelected) {
m_d->m_filesSelected = newFilesSelected;
const bool filesSelected = hasSelection();
if (m_d->m_filesSelected != filesSelected) {
m_d->m_filesSelected = filesSelected;
emit fileSelectionChanged(m_d->m_filesSelected);
if (debug)
qDebug() << Q_FUNC_INFO << m_d->m_filesSelected;
}
}
bool SubmitEditorWidget::hasSelection() const
{
// Not present until model is set
if (const QItemSelectionModel *sm = m_d->m_ui.fileView->selectionModel())
return sm->hasSelection();
return false;
}
bool SubmitEditorWidget::hasCheckedFiles() const
{
if (const QAbstractItemModel *model = m_d->m_ui.fileView->model())
return listModelContainsCheckedItem(model);
return false;
}
void SubmitEditorWidget::changeEvent(QEvent *e)
{
switch (e->type()) {
......
......@@ -42,6 +42,8 @@ QT_BEGIN_NAMESPACE
class QPlainTextEdit;
class QListWidgetItem;
class QAction;
class QAbstractItemModel;
class QModelIndex;
QT_END_NAMESPACE
namespace Core {
......@@ -51,8 +53,9 @@ struct SubmitEditorWidgetPrivate;
/* The submit editor presents the commit message in a text editor and an
* checkable list of modified files in a list window. The user can delete
* files from the list by pressing unchecking them or diff the selection
* by doubleclicking.
* files from the list by unchecking them or diff the selection
* by doubleclicking. A list model which contains the file in a column
* specified by fileNameColumn should be set using setFileModel().
*
* Additionally, standard creator actions can be registered:
* Undo/redo will be set up to work with the description editor.
......@@ -71,7 +74,7 @@ class QWORKBENCH_UTILS_EXPORT SubmitEditorWidget : public QWidget
Q_OBJECT
Q_DISABLE_COPY(SubmitEditorWidget)
Q_PROPERTY(QString descriptionText READ descriptionText WRITE setDescriptionText DESIGNABLE true)
Q_PROPERTY(QStringList fileList READ fileList WRITE setFileList DESIGNABLE true)
Q_PROPERTY(int fileNameColumn READ fileNameColumn WRITE setFileNameColumn DESIGNABLE false)
public:
explicit SubmitEditorWidget(QWidget *parent = 0);
virtual ~SubmitEditorWidget();
......@@ -86,10 +89,11 @@ public:
// Should be used to normalize newlines.
QString trimmedDescriptionText() const;
// The raw file list
QStringList fileList() const;
void addFiles(const QStringList&, bool checked = true, bool userCheckable = true);
void setFileList(const QStringList&);
int fileNameColumn() const;
void setFileNameColumn(int c);
void setFileModel(QAbstractItemModel *model);
QAbstractItemModel *fileModel() const;
// Files to be included in submit
QStringList checkedFiles() const;
......@@ -110,11 +114,16 @@ protected:
private slots:
void triggerDiffSelected();
void fileItemChanged(QListWidgetItem *);
void fileSelectionChanged();
void diffActivated(const QModelIndex &index);
void diffActivatedDelayed();
void fileDataChanged (const QModelIndex &topLeft, const QModelIndex &bottomRight);
void updateActions();
void updateSubmitAction();
void updateDiffAction();
private:
void addFilesUnblocked(const QStringList &list, bool checked, bool userCheckable);
bool hasSelection() const;
bool hasCheckedFiles() const;
SubmitEditorWidgetPrivate *m_d;
};
......
......@@ -39,14 +39,7 @@
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QListWidget" name="fileList">
<property name="font">
<font/>
</property>
<property name="textElideMode">
<enum>Qt::ElideNone</enum>
</property>
</widget>
<widget class="QTreeView" name="fileView"/>
</item>
</layout>
</widget>
......
......@@ -32,8 +32,12 @@
***************************************************************************/
#include "commitdata.h"
#include <utils/qtcassert.h>
#include <QtCore/QDebug>
#include <QtCore/QRegExp>
const char *const kBranchIndicatorC = "# On branch";
namespace Git {
namespace Internal {
......@@ -85,6 +89,130 @@ void CommitData::clear()
untrackedFiles.clear();
}
// Split a state/file spec from git status output
// '#<tab>modified:<blanks>git .pro'
// into state and file ('modified', 'git .pro').
CommitData::StateFilePair splitStateFileSpecification(const QString &line)
{
QPair<QString, QString> rc;
const int statePos = 2;
const int colonIndex = line.indexOf(QLatin1Char(':'), statePos);
if (colonIndex == -1)
return rc;
rc.first = line.mid(statePos, colonIndex - statePos);
int filePos = colonIndex + 1;
const QChar blank = QLatin1Char(' ');
while (line.at(filePos) == blank)
filePos++;
if (filePos < line.size())
rc.second = line.mid(filePos, line.size() - filePos);
return rc;
}
// Convenience to add a state/file spec to a list
static inline bool addStateFileSpecification(const QString &line, QList<CommitData::StateFilePair> *list)
{
const CommitData::StateFilePair sf = splitStateFileSpecification(line);
if (sf.first.isEmpty() || sf.second.isEmpty())
return false;
list->push_back(sf);
return true;
}
/* Parse a git status file list:
* \code
# Changes to be committed:
#<tab>modified:<blanks>git.pro
# Changed but not updated:
#<tab>modified:<blanks>git.pro
# Untracked files:
#<tab>git.pro
\endcode
*/
bool CommitData::parseFilesFromStatus(const QString &output)
{
enum State { None, CommitFiles, NotUpdatedFiles, UntrackedFiles };
const QStringList lines = output.split(QLatin1Char('\n'));
const QString branchIndicator = QLatin1String(kBranchIndicatorC);
const QString commitIndicator = QLatin1String("# Changes to be committed:");
const QString notUpdatedIndicator = QLatin1String("# Changed but not updated:");
const QString untrackedIndicator = QLatin1String("# Untracked files:");
State s = None;
// Match added/changed-not-updated files: "#<tab>modified: foo.cpp"
QRegExp filesPattern(QLatin1String("#\\t[^:]+:\\s+.+"));
QTC_ASSERT(filesPattern.isValid(), return false);
const QStringList::const_iterator cend = lines.constEnd();
for (QStringList::const_iterator it = lines.constBegin(); it != cend; ++it) {
const QString line = *it;
if (line.startsWith(branchIndicator)) {
panelInfo.branch = line.mid(branchIndicator.size() + 1);
} else {
if (line.startsWith(commitIndicator)) {
s = CommitFiles;
} else {
if (line.startsWith(notUpdatedIndicator)) {
s = NotUpdatedFiles;
} else {
if (line.startsWith(untrackedIndicator)) {
// Now match untracked: "#<tab>foo.cpp"
s = UntrackedFiles;
filesPattern = QRegExp(QLatin1String("#\\t.+"));
QTC_ASSERT(filesPattern.isValid(), return false);
} else {
if (filesPattern.exactMatch(line)) {
switch (s) {
case CommitFiles:
addStateFileSpecification(line, &stagedFiles);
break;
case NotUpdatedFiles:
addStateFileSpecification(line, &unstagedFiles);
break;
case UntrackedFiles:
untrackedFiles.push_back(line.mid(2).trimmed());
break;
case None:
break;
}
}
}
}
}
}
}
return !stagedFiles.empty() || !unstagedFiles.empty() || !untrackedFiles.empty();
}
// Convert a spec pair list to a list of file names, optionally
// filter for a state
static QStringList specToFileNames(const QList<CommitData::StateFilePair> &files,
const QString &stateFilter)
{
typedef QList<CommitData::StateFilePair>::const_iterator ConstIterator;
if (files.empty())
return QStringList();
const bool emptyFilter = stateFilter.isEmpty();
QStringList rc;
const ConstIterator cend = files.constEnd();
for (ConstIterator it = files.constBegin(); it != cend; ++it)
if (emptyFilter || stateFilter == it->first)
rc.push_back(it->second);
return rc;
}
QStringList CommitData::stagedFileNames(const QString &stateFilter) const
{
return specToFileNames(stagedFiles, stateFilter);
}
QStringList CommitData::unstagedFileNames(const QString &stateFilter) const
{
return specToFileNames(unstagedFiles, stateFilter);
}
QDebug operator<<(QDebug d, const CommitData &data)
{
d << data.panelInfo << data.panelData;
......
......@@ -35,6 +35,7 @@
#define COMMITDATA_H
#include <QtCore/QStringList>
#include <QtCore/QPair>
QT_BEGIN_NAMESPACE
class QDebug;
......@@ -68,11 +69,24 @@ QDebug operator<<(QDebug d, const GitSubmitEditorPanelData &);
struct CommitData
{
// A pair of state string/file name ('modified', 'file.cpp').
typedef QPair<QString, QString> StateFilePair;
void clear();
// Parse the files and the branch of panelInfo
// from a git status output
bool parseFilesFromStatus(const QString &output);
// Convenience to retrieve the file names from
// the specification list. Optionally filter for a certain state
QStringList stagedFileNames(const QString &stateFilter = QString()) const;
QStringList unstagedFileNames(const QString &stateFilter = QString()) const;
GitSubmitEditorPanelInfo panelInfo;
GitSubmitEditorPanelData panelData;
QStringList stagedFiles;
QStringList unstagedFiles;
QList<StateFilePair> stagedFiles;
QList<StateFilePair> unstagedFiles;
QStringList untrackedFiles;
};
......
......@@ -622,73 +622,6 @@ GitClient::StatusResult GitClient::gitStatus(const QString &workingDirectory,
return StatusChanged;
}
/* Parse a git status file list:
* \code
# Changes to be committed:
#<tab>modified:<blanks>git.pro