Prefix duplicate names in Open Documents to make them unique

If a document has a file name associated with it then the prefix is
composed from path components (subdirectories), starting from the one
where the file is located and going up the parents until the resulting
name becomes unique among other open documents.

If a document doesn't have an associated file name, then a sequential
number (starting from 1) is appended to the display name of the
document.

This feature is useful when working with big projects that have lots
of idendical file names across different subdirectories (e.g.
Makefile.in, main.cpp, etc.) that need to be edited at the same
time. It allows to easily recognize such a file when switching
between documents in the editor, w/o the need to place the
mouse pointer over the name entry to get its full path.

Started-by: Dmitriy Kuminov <coding@dmik.org>
Task-number: QTCREATORBUG-10185
Change-Id: I633ea6d9b9b4fce8b67335dbcce1bda29254efde
Reviewed-by: Eike Ziller <eike.ziller@theqtcompany.com>
This commit is contained in:
Orgad Shaneh
2015-02-25 14:36:03 +02:00
committed by Orgad Shaneh
parent 991cf84991
commit aee35662a2
20 changed files with 191 additions and 42 deletions

View File

@@ -601,7 +601,7 @@ void BazaarPlugin::showCommitWidget(const QList<VcsBaseClient::StatusItem> &stat
const QString msg = tr("Commit changes for \"%1\"."). const QString msg = tr("Commit changes for \"%1\".").
arg(QDir::toNativeSeparators(m_submitRepository)); arg(QDir::toNativeSeparators(m_submitRepository));
commitEditor->document()->setDisplayName(msg); commitEditor->document()->setPreferredDisplayName(msg);
const BranchInfo branch = m_client->synchronousBranchQuery(m_submitRepository); const BranchInfo branch = m_client->synchronousBranchQuery(m_submitRepository);
commitEditor->setFields(m_submitRepository, branch, commitEditor->setFields(m_submitRepository, branch,

View File

@@ -42,7 +42,7 @@ CommitEditor::CommitEditor(const VcsBase::VcsBaseSubmitEditorParameters *paramet
: VcsBase::VcsBaseSubmitEditor(parameters, new BazaarCommitWidget), : VcsBase::VcsBaseSubmitEditor(parameters, new BazaarCommitWidget),
m_fileModel(0) m_fileModel(0)
{ {
document()->setDisplayName(tr("Commit Editor")); document()->setPreferredDisplayName(tr("Commit Editor"));
} }
BazaarCommitWidget *CommitEditor::commitWidget() BazaarCommitWidget *CommitEditor::commitWidget()

View File

@@ -41,7 +41,7 @@ using namespace ClearCase::Internal;
ClearCaseSubmitEditor::ClearCaseSubmitEditor(const VcsBase::VcsBaseSubmitEditorParameters *parameters) : ClearCaseSubmitEditor::ClearCaseSubmitEditor(const VcsBase::VcsBaseSubmitEditorParameters *parameters) :
VcsBase::VcsBaseSubmitEditor(parameters, new ClearCaseSubmitEditorWidget) VcsBase::VcsBaseSubmitEditor(parameters, new ClearCaseSubmitEditorWidget)
{ {
document()->setDisplayName(tr("ClearCase Check In")); document()->setPreferredDisplayName(tr("ClearCase Check In"));
} }
ClearCaseSubmitEditorWidget *ClearCaseSubmitEditor::submitEditorWidget() ClearCaseSubmitEditorWidget *ClearCaseSubmitEditor::submitEditorWidget()

View File

@@ -40,6 +40,7 @@
#include <QDir> #include <QDir>
#include <QIcon> #include <QIcon>
#include <QMimeData> #include <QMimeData>
#include <QSet>
#include <QUrl> #include <QUrl>
namespace Core { namespace Core {
@@ -69,11 +70,40 @@ public:
int indexOfFilePath(const Utils::FileName &filePath) const; int indexOfFilePath(const Utils::FileName &filePath) const;
int indexOfDocument(IDocument *document) const; int indexOfDocument(IDocument *document) const;
bool disambiguateDisplayNames(DocumentModel::Entry *entry);
private slots: private slots:
friend class DocumentModel; friend class DocumentModel;
void itemChanged(); void itemChanged();
private: private:
class DynamicEntry
{
public:
DocumentModel::Entry *entry;
int pathComponents;
DynamicEntry(DocumentModel::Entry *e) :
entry(e),
pathComponents(0)
{
}
DocumentModel::Entry *operator->() const { return entry; }
void disambiguate()
{
entry->document->setUniqueDisplayName(entry->fileName().fileName(++pathComponents));
}
void setNumberedName(int number)
{
entry->document->setUniqueDisplayName(QStringLiteral("%1 (%2)")
.arg(entry->document->displayName())
.arg(number));
}
};
const QIcon m_lockedIcon; const QIcon m_lockedIcon;
const QIcon m_unlockedIcon; const QIcon m_unlockedIcon;
@@ -139,6 +169,11 @@ QString DocumentModel::Entry::displayName() const
return document ? document->displayName() : m_displayName; return document ? document->displayName() : m_displayName;
} }
QString DocumentModel::Entry::plainDisplayName() const
{
return document ? document->plainDisplayName() : m_displayName;
}
Id DocumentModel::Entry::id() const Id DocumentModel::Entry::id() const
{ {
return document ? document->id() : m_id; return document ? document->id() : m_id;
@@ -199,28 +234,37 @@ void DocumentModelPrivate::addEntry(DocumentModel::Entry *entry)
// replace a non-loaded entry (aka 'restored') if possible // replace a non-loaded entry (aka 'restored') if possible
int previousIndex = indexOfFilePath(fileName); int previousIndex = indexOfFilePath(fileName);
if (previousIndex >= 0) { if (previousIndex >= 0) {
if (entry->document && m_entries.at(previousIndex)->document == 0) {
DocumentModel::Entry *previousEntry = m_entries.at(previousIndex); DocumentModel::Entry *previousEntry = m_entries.at(previousIndex);
m_entries[previousIndex] = entry; const bool replace = entry->document && !previousEntry->document;
if (replace) {
delete previousEntry; delete previousEntry;
m_entries[previousIndex] = entry;
if (!fixedPath.isEmpty()) if (!fixedPath.isEmpty())
m_entryByFixedPath[fixedPath] = entry; m_entryByFixedPath[fixedPath] = entry;
connect(entry->document, SIGNAL(changed()), this, SLOT(itemChanged()));
} else { } else {
delete entry; delete entry;
entry = previousEntry;
} }
previousEntry = 0;
disambiguateDisplayNames(entry);
if (replace)
connect(entry->document, SIGNAL(changed()), this, SLOT(itemChanged()));
return; return;
} }
int index; int index;
QString displayName = entry->displayName(); const QString displayName = entry->plainDisplayName();
for (index = 0; index < m_entries.count(); ++index) { for (index = 0; index < m_entries.count(); ++index) {
if (displayName.localeAwareCompare(m_entries.at(index)->displayName()) < 0) int cmp = displayName.localeAwareCompare(m_entries.at(index)->plainDisplayName());
if (cmp < 0)
break;
if (cmp == 0 && fileName < d->m_entries.at(index)->fileName())
break; break;
} }
int row = index + 1/*<no document>*/; int row = index + 1/*<no document>*/;
beginInsertRows(QModelIndex(), row, row); beginInsertRows(QModelIndex(), row, row);
m_entries.insert(index, entry); m_entries.insert(index, entry);
disambiguateDisplayNames(entry);
if (!fixedPath.isEmpty()) if (!fixedPath.isEmpty())
m_entryByFixedPath[fixedPath] = entry; m_entryByFixedPath[fixedPath] = entry;
if (entry->document) if (entry->document)
@@ -228,6 +272,71 @@ void DocumentModelPrivate::addEntry(DocumentModel::Entry *entry)
endInsertRows(); endInsertRows();
} }
bool DocumentModelPrivate::disambiguateDisplayNames(DocumentModel::Entry *entry)
{
const QString displayName = entry->plainDisplayName();
int minIdx = -1, maxIdx = -1;
QList<DynamicEntry> dups;
for (int i = 0, total = m_entries.count(); i < total; ++i) {
DocumentModel::Entry *e = m_entries.at(i);
if (!e->document)
continue;
if (e == entry || e->plainDisplayName() == displayName) {
e->document->setUniqueDisplayName(QString());
dups += DynamicEntry(e);
maxIdx = i;
if (minIdx < 0)
minIdx = i;
}
}
const int dupsCount = dups.count();
if (dupsCount == 0)
return false;
if (dupsCount > 1) {
int serial = 0;
int count = 0;
// increase uniqueness unless no dups are left
forever {
bool seenDups = false;
for (int i = 0; i < dupsCount - 1; ++i) {
DynamicEntry &e = dups[i];
const Utils::FileName myFileName = e->document->filePath();
if (e->document->isTemporary() || myFileName.isEmpty() || count > 10) {
// path-less entry, append number
e.setNumberedName(++serial);
continue;
}
for (int j = i + 1; j < dupsCount; ++j) {
DynamicEntry &e2 = dups[j];
if (e->displayName() == e2->displayName()) {
const Utils::FileName otherFileName = e2->document->filePath();
if (otherFileName.isEmpty())
continue;
seenDups = true;
e2.disambiguate();
if (j > maxIdx)
maxIdx = j;
}
}
if (seenDups) {
e.disambiguate();
++count;
break;
}
}
if (!seenDups)
break;
}
}
emit dataChanged(index(minIdx + 1, 0), index(maxIdx + 1, 0));
return true;
}
int DocumentModelPrivate::indexOfFilePath(const Utils::FileName &filePath) const int DocumentModelPrivate::indexOfFilePath(const Utils::FileName &filePath) const
{ {
if (filePath.isEmpty()) if (filePath.isEmpty())
@@ -285,6 +394,7 @@ void DocumentModelPrivate::removeDocument(int idx)
} }
if (IDocument *document = entry->document) if (IDocument *document = entry->document)
disconnect(document, SIGNAL(changed()), this, SLOT(itemChanged())); disconnect(document, SIGNAL(changed()), this, SLOT(itemChanged()));
disambiguateDisplayNames(entry);
delete entry; delete entry;
} }
@@ -298,6 +408,14 @@ void DocumentModel::removeAllRestoredEntries()
d->endRemoveRows(); d->endRemoveRows();
} }
} }
QSet<QString> displayNames;
foreach (DocumentModel::Entry *entry, d->m_entries) {
const QString displayName = entry->plainDisplayName();
if (displayNames.contains(displayName))
continue;
displayNames.insert(displayName);
d->disambiguateDisplayNames(entry);
}
} }
QList<IEditor *> DocumentModel::editorsForDocument(IDocument *document) QList<IEditor *> DocumentModel::editorsForDocument(IDocument *document)
@@ -406,10 +524,12 @@ QVariant DocumentModelPrivate::data(const QModelIndex &index, int role) const
} }
const DocumentModel::Entry *e = m_entries.at(entryIndex); const DocumentModel::Entry *e = m_entries.at(entryIndex);
switch (role) { switch (role) {
case Qt::DisplayRole: case Qt::DisplayRole: {
return (e->document && e->document->isModified()) QString name = e->displayName();
? e->displayName() + QLatin1Char('*') if (e->document && e->document->isModified())
: e->displayName(); name += QLatin1Char('*');
return name;
}
case Qt::DecorationRole: case Qt::DecorationRole:
{ {
bool showLock = false; bool showLock = false;
@@ -420,9 +540,7 @@ QVariant DocumentModelPrivate::data(const QModelIndex &index, int role) const
return showLock ? m_lockedIcon : QIcon(); return showLock ? m_lockedIcon : QIcon();
} }
case Qt::ToolTipRole: case Qt::ToolTipRole:
return e->fileName().isEmpty() return e->fileName().isEmpty() ? e->displayName() : e->fileName().toUserOutput();
? e->displayName()
: e->fileName().toUserOutput();
default: default:
return QVariant(); return QVariant();
} }
@@ -484,9 +602,11 @@ void DocumentModelPrivate::itemChanged()
} }
if (!found && !fixedPath.isEmpty()) if (!found && !fixedPath.isEmpty())
m_entryByFixedPath[fixedPath] = entry; m_entryByFixedPath[fixedPath] = entry;
if (!disambiguateDisplayNames(d->m_entries.at(idx))) {
QModelIndex mindex = index(idx + 1/*<no document>*/, 0); QModelIndex mindex = index(idx + 1/*<no document>*/, 0);
emit dataChanged(mindex, mindex); emit dataChanged(mindex, mindex);
} }
}
QList<DocumentModel::Entry *> DocumentModel::entries() QList<DocumentModel::Entry *> DocumentModel::entries()
{ {

View File

@@ -61,6 +61,8 @@ public:
IDocument *document; IDocument *document;
Utils::FileName fileName() const; Utils::FileName fileName() const;
QString displayName() const; QString displayName() const;
QString plainDisplayName() const;
QString uniqueDisplayName() const;
Id id() const; Id id() const;
Utils::FileName m_fileName; Utils::FileName m_fileName;
QString m_displayName; QString m_displayName;

View File

@@ -2386,7 +2386,7 @@ IEditor *EditorManager::openEditorWithContents(Id editorId,
} }
if (!title.isEmpty()) if (!title.isEmpty())
edt->document()->setDisplayName(title); edt->document()->setPreferredDisplayName(title);
EditorManagerPrivate::addEditor(edt); EditorManagerPrivate::addEditor(edt);

View File

@@ -225,7 +225,8 @@ void OpenEditorsWindow::addHistoryItems(const QList<EditLocation> &history, Edit
if (hi.document.isNull() || documentsDone.contains(hi.document)) if (hi.document.isNull() || documentsDone.contains(hi.document))
continue; continue;
documentsDone.insert(hi.document.data()); documentsDone.insert(hi.document.data());
QString title = hi.document->displayName(); DocumentModel::Entry *entry = DocumentModel::entryForDocument(hi.document);
QString title = entry ? entry->displayName() : hi.document->displayName();
QTC_ASSERT(!title.isEmpty(), continue); QTC_ASSERT(!title.isEmpty(), continue);
QTreeWidgetItem *item = new QTreeWidgetItem(); QTreeWidgetItem *item = new QTreeWidgetItem();
if (hi.document->isModified()) if (hi.document->isModified())

View File

@@ -83,7 +83,8 @@ public:
QString mimeType; QString mimeType;
Utils::FileName filePath; Utils::FileName filePath;
QString displayName; QString preferredDisplayName;
QString uniqueDisplayName;
QString autoSaveName; QString autoSaveName;
InfoBar *infoBar; InfoBar *infoBar;
Id id; Id id;
@@ -258,13 +259,16 @@ void IDocument::setFilePath(const Utils::FileName &filePath)
/*! /*!
Returns the string to display for this document, e.g. in the open document combo box Returns the string to display for this document, e.g. in the open document combo box
and pane. and pane.
The returned string has the following priority:
* Unique display name set by the document model
* Preferred display name set by the owner
* Base name of the document's file name
\sa setDisplayName() \sa setDisplayName()
*/ */
QString IDocument::displayName() const QString IDocument::displayName() const
{ {
if (!d->displayName.isEmpty()) return d->uniqueDisplayName.isEmpty() ? plainDisplayName() : d->uniqueDisplayName;
return d->displayName;
return d->filePath.fileName();
} }
/*! /*!
@@ -274,12 +278,30 @@ QString IDocument::displayName() const
\sa displayName() \sa displayName()
\sa filePath() \sa filePath()
*/ */
void IDocument::setDisplayName(const QString &name) void IDocument::setPreferredDisplayName(const QString &name)
{ {
if (name == d->displayName) if (name == d->preferredDisplayName)
return; return;
d->displayName = name; d->preferredDisplayName = name;
emit changed(); emit changed();
} }
/*!
\internal
Returns displayName without disambiguation.
*/
QString IDocument::plainDisplayName() const
{
return d->preferredDisplayName.isEmpty() ? d->filePath.fileName() : d->preferredDisplayName;
}
/*!
\internal
Sets unique display name for the document. Used by the document model.
*/
void IDocument::setUniqueDisplayName(const QString &name)
{
d->uniqueDisplayName = name;
}
} // namespace Core } // namespace Core

View File

@@ -92,7 +92,9 @@ public:
Utils::FileName filePath() const; Utils::FileName filePath() const;
virtual void setFilePath(const Utils::FileName &filePath); virtual void setFilePath(const Utils::FileName &filePath);
QString displayName() const; QString displayName() const;
void setDisplayName(const QString &name); void setPreferredDisplayName(const QString &name);
QString plainDisplayName() const;
void setUniqueDisplayName(const QString &name);
virtual bool isFileReadOnly() const; virtual bool isFileReadOnly() const;
bool isTemporary() const; bool isTemporary() const;

View File

@@ -50,10 +50,12 @@ OpenDocumentsFilter::OpenDocumentsFilter()
setPriority(High); setPriority(High);
setIncludedByDefault(true); setIncludedByDefault(true);
connect(EditorManager::instance(), SIGNAL(editorOpened(Core::IEditor*)), connect(DocumentModel::model(), &QAbstractItemModel::dataChanged,
this, SLOT(refreshInternally())); this, &OpenDocumentsFilter::refreshInternally);
connect(EditorManager::instance(), SIGNAL(editorsClosed(QList<Core::IEditor*>)), connect(DocumentModel::model(), &QAbstractItemModel::rowsInserted,
this, SLOT(refreshInternally())); this, &OpenDocumentsFilter::refreshInternally);
connect(DocumentModel::model(), &QAbstractItemModel::rowsRemoved,
this, &OpenDocumentsFilter::refreshInternally);
} }
QList<LocatorFilterEntry> OpenDocumentsFilter::matchesFor(QFutureInterface<LocatorFilterEntry> &future, const QString &entry_) QList<LocatorFilterEntry> OpenDocumentsFilter::matchesFor(QFutureInterface<LocatorFilterEntry> &future, const QString &entry_)

View File

@@ -394,7 +394,7 @@ void CodepasterPlugin::finishFetch(const QString &titleDescription,
// Open editor with title. // Open editor with title.
IEditor *editor = EditorManager::openEditor(fileName); IEditor *editor = EditorManager::openEditor(fileName);
QTC_ASSERT(editor, return); QTC_ASSERT(editor, return);
editor->document()->setDisplayName(titleDescription); editor->document()->setPreferredDisplayName(titleDescription);
} }
CodepasterPlugin *CodepasterPlugin::instance() CodepasterPlugin *CodepasterPlugin::instance()

View File

@@ -307,7 +307,7 @@ void DisassemblerAgent::setContentsToDocument(const DisassemblerLines &contents)
d->document->setPlainText(contents.toString()); d->document->setPlainText(contents.toString());
d->document->setDisplayName(_("Disassembler (%1)") d->document->setPreferredDisplayName(_("Disassembler (%1)")
.arg(d->location.functionName())); .arg(d->location.functionName()));
updateBreakpointMarkers(); updateBreakpointMarkers();

View File

@@ -88,7 +88,7 @@ bool DiffEditorDocument::save(QString *errorString, const QString &fileName, boo
const QFileInfo fi(fileName); const QFileInfo fi(fileName);
setTemporary(false); setTemporary(false);
setFilePath(Utils::FileName::fromString(fi.absoluteFilePath())); setFilePath(Utils::FileName::fromString(fi.absoluteFilePath()));
setDisplayName(QString()); setPreferredDisplayName(QString());
return true; return true;
} }

View File

@@ -98,7 +98,7 @@ Core::IDocument *DiffEditorManager::findOrCreate(const QString &vcsId, const QSt
document = qobject_cast<Internal::DiffEditorDocument *>(diffEditor->document()); document = qobject_cast<Internal::DiffEditorDocument *>(diffEditor->document());
QTC_ASSERT(diffEditor, return 0); QTC_ASSERT(diffEditor, return 0);
document->setDisplayName(displayName); document->setPreferredDisplayName(displayName);
m_instance->m_idToDocument.insert(vcsId, document); m_instance->m_idToDocument.insert(vcsId, document);

View File

@@ -1011,7 +1011,7 @@ IEditor *GitPlugin::openSubmitEditor(const QString &fileName, const CommitData &
title = tr("Git Commit"); title = tr("Git Commit");
} }
IDocument *document = submitEditor->document(); IDocument *document = submitEditor->document();
document->setDisplayName(title); document->setPreferredDisplayName(title);
VcsBasePlugin::setSource(document, m_submitRepository); VcsBasePlugin::setSource(document, m_submitRepository);
connect(submitEditor, SIGNAL(diff(QStringList,QStringList)), this, SLOT(submitEditorDiff(QStringList,QStringList))); connect(submitEditor, SIGNAL(diff(QStringList,QStringList)), this, SLOT(submitEditorDiff(QStringList,QStringList)));
connect(submitEditor, SIGNAL(merge(QStringList)), this, SLOT(submitEditorMerge(QStringList))); connect(submitEditor, SIGNAL(merge(QStringList)), this, SLOT(submitEditorMerge(QStringList)));

View File

@@ -45,7 +45,7 @@ CommitEditor::CommitEditor(const VcsBaseSubmitEditorParameters *parameters)
: VcsBaseSubmitEditor(parameters, new MercurialCommitWidget), : VcsBaseSubmitEditor(parameters, new MercurialCommitWidget),
fileModel(0) fileModel(0)
{ {
document()->setDisplayName(tr("Commit Editor")); document()->setPreferredDisplayName(tr("Commit Editor"));
} }
MercurialCommitWidget *CommitEditor::commitWidget() MercurialCommitWidget *CommitEditor::commitWidget()

View File

@@ -581,7 +581,7 @@ void MercurialPlugin::showCommitWidget(const QList<VcsBaseClient::StatusItem> &s
const QString msg = tr("Commit changes for \"%1\"."). const QString msg = tr("Commit changes for \"%1\".").
arg(QDir::toNativeSeparators(m_submitRepository)); arg(QDir::toNativeSeparators(m_submitRepository));
commitEditor->document()->setDisplayName(msg); commitEditor->document()->setPreferredDisplayName(msg);
QString branch = versionControl()->vcsTopic(m_submitRepository); QString branch = versionControl()->vcsTopic(m_submitRepository);
commitEditor->setFields(m_submitRepository, branch, commitEditor->setFields(m_submitRepository, branch,

View File

@@ -48,7 +48,7 @@ PerforceSubmitEditor::PerforceSubmitEditor(const VcsBase::VcsBaseSubmitEditorPar
VcsBaseSubmitEditor(parameters, new PerforceSubmitEditorWidget), VcsBaseSubmitEditor(parameters, new PerforceSubmitEditorWidget),
m_fileModel(new VcsBase::SubmitFileModel(this)) m_fileModel(new VcsBase::SubmitFileModel(this))
{ {
document()->setDisplayName(tr("Perforce Submit")); document()->setPreferredDisplayName(tr("Perforce Submit"));
setFileModel(m_fileModel); setFileModel(m_fileModel);
} }

View File

@@ -39,7 +39,7 @@ using namespace Subversion::Internal;
SubversionSubmitEditor::SubversionSubmitEditor(const VcsBase::VcsBaseSubmitEditorParameters *parameters) : SubversionSubmitEditor::SubversionSubmitEditor(const VcsBase::VcsBaseSubmitEditorParameters *parameters) :
VcsBase::VcsBaseSubmitEditor(parameters, new VcsBase::SubmitEditorWidget) VcsBase::VcsBaseSubmitEditor(parameters, new VcsBase::SubmitEditorWidget)
{ {
document()->setDisplayName(tr("Subversion Submit")); document()->setPreferredDisplayName(tr("Subversion Submit"));
setDescriptionMandatory(false); setDescriptionMandatory(false);
} }

View File

@@ -185,7 +185,7 @@ VcsBaseSubmitEditor::VcsBaseSubmitEditor(const VcsBaseSubmitEditorParameters *pa
d(new VcsBaseSubmitEditorPrivate(parameters, editorWidget, this)) d(new VcsBaseSubmitEditorPrivate(parameters, editorWidget, this))
{ {
setWidget(d->m_widget); setWidget(d->m_widget);
document()->setDisplayName(QCoreApplication::translate("VCS", d->m_parameters->displayName)); document()->setPreferredDisplayName(QCoreApplication::translate("VCS", d->m_parameters->displayName));
// Message font according to settings // Message font according to settings
CompletingTextEdit *descriptionEdit = editorWidget->descriptionEdit(); CompletingTextEdit *descriptionEdit = editorWidget->descriptionEdit();