Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use columns for file name, directory, and state when files are shown as a list in TreeViews. #664

Merged
merged 12 commits into from
Nov 20, 2023
Merged
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
build
.cache
.DS_Store
.project
.vscode/
Expand All @@ -8,3 +9,5 @@ cmake-build-release/
build
.idea/
.venv
compile_commands.json

2 changes: 2 additions & 0 deletions src/conf/Setting.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ void Setting::initialize(QMap<Id, QString> &keys) {
keys[Id::ShowCommitsId] = "commit/id";
keys[Id::ShowChangedFilesAsList] = "doubletreeview/listview";
keys[Id::ShowChangedFilesInSingleView] = "doubletreeview/single";
keys[Id::ShowChangedFilesMultiColumn] = "doubletreeview/listviewmulticolumn";
keys[Id::HideUntracked] = "untracked.hide";
}

void Prompt::initialize(QMap<Kind, QString> &keys) {
Expand Down
2 changes: 2 additions & 0 deletions src/conf/Setting.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ class Setting : public SettingsTempl<Setting> {
ShowCommitsDate,
ShowCommitsId,
ShowChangedFilesAsList,
ShowChangedFilesMultiColumn, // For the list only
ShowChangedFilesInSingleView,
HideUntracked,
Language,
};
Q_ENUM(Id)
Expand Down
69 changes: 63 additions & 6 deletions src/ui/DiffTreeModel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
// Author: Jason Haslam
//

#include <array>
#include "DiffTreeModel.h"
#include "conf/Settings.h"
#include "git/Blob.h"
Expand All @@ -16,18 +17,37 @@
#include "git/Patch.h"
#include <QStringBuilder>
#include <QUrl>
#include <qobjectdefs.h>

namespace {

const QString kLinkFmt = "<a href='%1'>%2</a>";

const std::array<QString, 3> kModelHeaders = {QObject::tr("File Name"),
QObject::tr("Relative Path"),
QObject::tr("State")};

bool asList() {
return Settings::instance()
->value(Setting::Id::ShowChangedFilesAsList, false)
.toBool();
}

} // namespace

DiffTreeModel::DiffTreeModel(const git::Repository &repo, QObject *parent)
: QAbstractItemModel(parent), mRepo(repo) {}
: QAbstractItemModel(parent), mRepo(repo) {
setMultiColumn(mMultiColumn);
}

DiffTreeModel::~DiffTreeModel() { delete mRoot; }

void DiffTreeModel::setMultiColumn(bool multi) {
beginResetModel();
mMultiColumn = multi;
endResetModel(); // Notify view about the change
}

void DiffTreeModel::createDiffTree() {

for (int patchNum = 0; patchNum < mDiff.count(); ++patchNum) {
Expand Down Expand Up @@ -91,7 +111,22 @@ int DiffTreeModel::rowCount(const QModelIndex &parent) const {
return mDiff ? node(parent)->children().size() : 0;
}

int DiffTreeModel::columnCount(const QModelIndex &parent) const { return 1; }
QVariant DiffTreeModel::headerData(int section, Qt::Orientation orientation,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this. :-)

int role) const {
if (section > 2 || section < 0) {
assert(false);
return QVariant();
}
if (orientation == Qt::Orientation::Vertical)
return QVariant();
if (role != Qt::DisplayRole)
return QVariant();
return kModelHeaders.at(section);
}

int DiffTreeModel::columnCount(const QModelIndex &parent) const {
return !asList() || !mMultiColumn ? 1 : kModelHeaders.size();
}

bool DiffTreeModel::hasChildren(const QModelIndex &parent) const {
return mRoot && node(parent)->hasChildren();
Expand Down Expand Up @@ -134,7 +169,7 @@ void DiffTreeModel::modelIndices(const QModelIndex &parent,
}

for (int i = 0; i < n->children().length(); i++) {
auto child = createIndex(i, 0, n->children()[i]);
auto child = createIndex(i, parent.column(), n->children()[i]);
if (recursive)
modelIndices(child, list);
else if (!node(child)->hasChildren())
Expand Down Expand Up @@ -195,9 +230,15 @@ QVariant DiffTreeModel::data(const QModelIndex &index, int role) const {
return QVariant();

Node *node = this->node(index);

// Skip intermediate path elements for trees showing file lists only.
if (node->hasChildren() && asList())
return QVariant();

switch (role) {
case Qt::DisplayRole:
return node->name();
case Qt::DisplayRole: {
return getDisplayRole(index);
}

// case Qt::DecorationRole: {
// QFileInfo info(node->path());
Expand All @@ -212,7 +253,7 @@ QVariant DiffTreeModel::data(const QModelIndex &index, int role) const {
return node->path();

case Qt::CheckStateRole: {
if (!mDiff.isValid() || !mDiff.isStatusDiff())
if (!mDiff.isValid() || !mDiff.isStatusDiff() || index.column() > 0)
return QVariant();

git::Index index = mDiff.index();
Expand Down Expand Up @@ -380,6 +421,22 @@ Node *DiffTreeModel::node(const QModelIndex &index) const {
return index.isValid() ? static_cast<Node *>(index.internalPointer()) : mRoot;
}

QVariant DiffTreeModel::getDisplayRole(const QModelIndex &index) const {
Node *node = this->node(index);
if (asList() && mMultiColumn) {
QFileInfo fileInfo(node->path(true));
switch (index.column()) {
case 0:
return fileInfo.fileName();
case 1:
return fileInfo.path();
default:
return ""; // State
}
}
return node->name();
}

//#############################################################################
//###### DiffTreeModel::Node ##############################################
//#############################################################################
Expand Down
6 changes: 6 additions & 0 deletions src/ui/DiffTreeModel.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include "git/Tree.h"
#include "git/Repository.h"
#include <QAbstractItemModel>
#include <QAbstractListModel>
#include <QFileIconProvider>
#include "git/Index.h"

Expand Down Expand Up @@ -98,7 +99,10 @@ class DiffTreeModel : public QAbstractItemModel {

void setDiff(const git::Diff &diff = git::Diff());
void refresh(const QStringList &paths);
void setMultiColumn(bool);

QVariant headerData(int section, Qt::Orientation orientation,
int role = Qt::DisplayRole) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
bool hasChildren(const QModelIndex &parent = QModelIndex()) const override;
Expand Down Expand Up @@ -157,6 +161,7 @@ class DiffTreeModel : public QAbstractItemModel {

private:
Node *node(const QModelIndex &index) const;
QVariant getDisplayRole(const QModelIndex &index) const;

QFileIconProvider mIconProvider;

Expand All @@ -165,6 +170,7 @@ class DiffTreeModel : public QAbstractItemModel {
git::Repository mRepo;

bool mListView = false;
bool mMultiColumn{true};
};

#endif /* DIFFTREEMODEL */
99 changes: 47 additions & 52 deletions src/ui/DoubleTreeWidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
#include "StatePushButton.h"
#include "TreeProxy.h"
#include "TreeView.h"
#include "ViewDelegate.h"
#include "Debug.h"
#include "conf/Settings.h"
#include "DiffView/DiffView.h"
Expand Down Expand Up @@ -68,6 +67,21 @@ class SegmentedButton : public QWidget {

} // namespace

QAction *DoubleTreeWidget::setupAppearanceAction(const char *name,
Setting::Id id,
bool defaultValue) {
QAction *action = new QAction(tr(name));
action->setCheckable(true);
action->setChecked(Settings::instance()->value(id, defaultValue).toBool());
connect(action, &QAction::triggered, this, [this, id](bool checked) {
Settings::instance()->setValue(id, checked);
mSelectedFile.filename =
""; // When switching view, it is not possible to restore
RepoView::parentView(this)->refresh();
});
return action;
}

DoubleTreeWidget::DoubleTreeWidget(const git::Repository &repo, QWidget *parent)
: ContentWidget(parent) {
// first column
Expand All @@ -82,39 +96,21 @@ DoubleTreeWidget::DoubleTreeWidget(const git::Repository &repo, QWidget *parent)
ContextMenuButton *contextButton = new ContextMenuButton(this);
QMenu *contextMenu = new QMenu(this);
contextButton->setMenu(contextMenu);
QAction *singleTree = new QAction(tr("Single Tree View"));
singleTree->setCheckable(true);
singleTree->setChecked(
Settings::instance()
->value(Setting::Id::ShowChangedFilesInSingleView, false)
.toBool());
connect(singleTree, &QAction::triggered, this, [this](bool checked) {
Settings::instance()->setValue(Setting::Id::ShowChangedFilesInSingleView,
checked);
RepoView::parentView(this)->refresh();
});
QAction *listView = new QAction(tr("List View"));
listView->setCheckable(true);
listView->setChecked(Settings::instance()
->value(Setting::Id::ShowChangedFilesAsList, false)
.toBool());
connect(listView, &QAction::triggered, this, [this](bool checked) {
Settings::instance()->setValue(Setting::Id::ShowChangedFilesAsList,
checked);
RepoView::parentView(this)->refresh();
});
QAction *hideUntrackedFiles = new QAction(tr("Hide Untracked Files"));
hideUntrackedFiles->setCheckable(true);
hideUntrackedFiles->setChecked(
RepoView::parentView(parent)->repo().appConfig().value<bool>(
"untracked.hide", false));
connect(hideUntrackedFiles, &QAction::triggered, this, [this](bool checked) {
RepoView::parentView(this)->repo().appConfig().setValue("untracked.hide",
checked);
RepoView::parentView(this)->refresh();
});

QAction *singleTree = setupAppearanceAction(
"Single View", Setting::Id::ShowChangedFilesInSingleView);
QAction *listView =
setupAppearanceAction("List View", Setting::Id::ShowChangedFilesAsList);
QAction *multiColumn = setupAppearanceAction(
"Multi Column", Setting::Id::ShowChangedFilesMultiColumn, true);
RepoView::parentView(this)->refresh(); // apply read settings

QAction *hideUntrackedFiles = setupAppearanceAction(
"Hide Untracked Files", Setting::Id::HideUntracked, false);

contextMenu->addAction(singleTree);
contextMenu->addAction(listView);
contextMenu->addAction(multiColumn);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this just an option when the list view is active?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this is true, not sure how to handle it. Shall we mark it as this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can think of 2 possibilities to resolve this:

  1. When turning multi-column mode on, turn on the list view too (it may already be on of course). Also, when turning list view off, turn multi-column off as well (it may already be off of course).
  2. Remove multi-column as a context menu option and, instead, add it as an application option under Tools>Options...>Window (or some more appropriate spot).

The first possibility keeps the UI as you have it and just enforces the semantics. The second restores the original context menu options and makes the multi-column setting be a "meta" setting for the list view - i.e. the user has to decide which kind of list view is desired and be willing to use it consistently.

It would be nice to resolve this somehow now but it could be cleaned up with a later submission too. I'm OK with either.

contextMenu->addAction(hideUntrackedFiles);
QHBoxLayout *buttonLayout = new QHBoxLayout();
buttonLayout->addStretch();
Expand Down Expand Up @@ -160,13 +156,8 @@ DoubleTreeWidget::DoubleTreeWidget(const git::Repository &repo, QWidget *parent)
repoView->updateSubmodules(submodules, recursive, init,
force_checkout);
});
TreeProxy *treewrapperStaged = new TreeProxy(true, this);
treewrapperStaged->setSourceModel(mDiffTreeModel);
stagedFiles->setModel(treewrapperStaged);
stagedFiles->setHeaderHidden(true);
ViewDelegate *stagedDelegate = new ViewDelegate();
stagedDelegate->setDrawArrow(false);
stagedFiles->setItemDelegateForColumn(0, stagedDelegate);

stagedFiles->setModel(new TreeProxy(true, mDiffTreeModel, this));

QHBoxLayout *hBoxLayout = new QHBoxLayout();
QLabel *label = new QLabel(kStagedFiles);
Expand All @@ -192,13 +183,7 @@ DoubleTreeWidget::DoubleTreeWidget(const git::Repository &repo, QWidget *parent)
showFileContextMenu(pos, repoView, unstagedFiles, false);
});

TreeProxy *treewrapperUnstaged = new TreeProxy(false, this);
treewrapperUnstaged->setSourceModel(mDiffTreeModel);
unstagedFiles->setModel(treewrapperUnstaged);
unstagedFiles->setHeaderHidden(true);
ViewDelegate *unstagedDelegate = new ViewDelegate();
unstagedDelegate->setDrawArrow(false);
unstagedFiles->setItemDelegateForColumn(0, unstagedDelegate);
unstagedFiles->setModel(new TreeProxy(false, mDiffTreeModel, this));

hBoxLayout = new QHBoxLayout();
mUnstagedCommitedFiles = new QLabel(kUnstagedFiles);
Expand Down Expand Up @@ -417,12 +402,6 @@ void DoubleTreeWidget::setDiff(const git::Diff &diff, const QString &file,
TreeProxy *proxy = static_cast<TreeProxy *>(unstagedFiles->model());
DiffTreeModel *model = static_cast<DiffTreeModel *>(proxy->sourceModel());
model->setDiff(diff);
// do not expand if to many files exist, it takes really long
// So do it only when there are less than 100
if (diff.isValid() && diff.count() < fileCountExpansionThreshold)
unstagedFiles->expandAll();
else
unstagedFiles->collapseAll();

// Single tree & list view.
bool singleTree =
Expand All @@ -432,15 +411,24 @@ void DoubleTreeWidget::setDiff(const git::Diff &diff, const QString &file,
bool listView = Settings::instance()
->value(Setting::Id::ShowChangedFilesAsList, false)
.toBool();
const bool multiColumn =
Settings::instance()
->value(Setting::Id::ShowChangedFilesMultiColumn, true)
.toBool();

// Widget modifications.
model->enableListView(listView);
model->setMultiColumn(multiColumn);
stagedFiles->setRootIsDecorated(!listView);
unstagedFiles->setRootIsDecorated(!listView);
// mUnstagedCommitedFiles->setVisible(!singleTree);
collapseButtonStagedFiles->setVisible(!listView);
collapseButtonUnstagedFiles->setVisible(!listView);

unstagedFiles->updateView(); // Must be before expandAll/collapseAll is done,
// otherwise the collapse counter is wrong
stagedFiles->updateView();

// If statusDiff, there exist no staged/unstaged, but only
// the commited files must be shown
if (!diff.isValid() || diff.isStatusDiff()) {
Expand All @@ -457,6 +445,13 @@ void DoubleTreeWidget::setDiff(const git::Diff &diff, const QString &file,
mStagedWidget->setVisible(false);
}

// do not expand if to many files exist, it takes really long
// So do it only when there are less than 100
if (diff.isValid() && diff.count() < fileCountExpansionThreshold)
unstagedFiles->expandAll();
else
unstagedFiles->collapseAll();

// Clear editor.
mEditor->clear();

Expand Down
3 changes: 3 additions & 0 deletions src/ui/DoubleTreeWidget.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include "DetailView.h"
#include "git/Index.h"
#include <QModelIndexList>
#include "conf/Settings.h"

class QTreeView;
class TreeView;
Expand Down Expand Up @@ -67,6 +68,8 @@ private slots:
void loadEditorContent(const QModelIndexList &indexes);
void toggleCollapseStagedFiles();
void toggleCollapseUnstagedFiles();
QAction *setupAppearanceAction(const char *name, Setting::Id id,
bool defaultValue = false);

DiffTreeModel *mDiffTreeModel{nullptr};
TreeView *stagedFiles{nullptr};
Expand Down
6 changes: 4 additions & 2 deletions src/ui/TreeProxy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ const QString kLinkFmt = "<a href='%1'>%2</a>";

} // namespace

TreeProxy::TreeProxy(bool staged, QObject *parent)
: QSortFilterProxyModel(parent), mStaged(staged) {}
TreeProxy::TreeProxy(bool staged, QAbstractItemModel *model, QObject *parent)
: mStaged(staged), QSortFilterProxyModel(parent) {
setSourceModel(model);
}

TreeProxy::~TreeProxy() {}

Expand Down
Loading
Loading