Use columns for file name, directory, and state when files are shown as a list in TreeViews. Resolves Dense layout issue #547

This commit is contained in:
devnull 2023-08-31 16:30:36 -04:00 committed by Martin Marmsoler
parent e7fd8da4b3
commit 1d6d53ba00
12 changed files with 148 additions and 66 deletions

3
.gitignore vendored
View File

@ -1,4 +1,5 @@
build
.cache
.DS_Store
.project
.vscode/
@ -8,3 +9,5 @@ cmake-build-release/
build
.idea/
.venv
compile_commands.json

View File

@ -7,6 +7,7 @@
// Author: Jason Haslam
//
#include <array>
#include "DiffTreeModel.h"
#include "conf/Settings.h"
#include "git/Blob.h"
@ -16,15 +17,30 @@
#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) {}
: QStandardItemModel(0, kModelHeaders.size(), parent), mRepo(repo) {
for (int i = 0; i < kModelHeaders.size(); ++i) {
setHeaderData(i, Qt::Horizontal, kModelHeaders[i]);
}
}
DiffTreeModel::~DiffTreeModel() { delete mRoot; }
@ -91,7 +107,9 @@ int DiffTreeModel::rowCount(const QModelIndex &parent) const {
return mDiff ? node(parent)->children().size() : 0;
}
int DiffTreeModel::columnCount(const QModelIndex &parent) const { return 1; }
int DiffTreeModel::columnCount(const QModelIndex &parent) const {
return asList() ? QStandardItemModel::columnCount(parent) : 1;
}
bool DiffTreeModel::hasChildren(const QModelIndex &parent) const {
return mRoot && node(parent)->hasChildren();
@ -134,7 +152,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())
@ -195,9 +213,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());
@ -212,7 +236,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();
@ -380,6 +404,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()) {
QFileInfo fileInfo(node->path(true));
switch (index.column()) {
case 0:
return fileInfo.fileName();
case 1:
return fileInfo.path();
default:
return "";
}
}
return node->name();
}
//#############################################################################
//###### DiffTreeModel::Node ##############################################
//#############################################################################

View File

@ -14,7 +14,8 @@
#include "git/Index.h"
#include "git/Tree.h"
#include "git/Repository.h"
#include <QAbstractItemModel>
#include <QStandardItemModel>
#include <QAbstractListModel>
#include <QFileIconProvider>
#include "git/Index.h"
@ -80,7 +81,7 @@ private:
* This Treemodel is similar to the normal tree model, but handles only the
* files in the diff it self and not the complete tree
*/
class DiffTreeModel : public QAbstractItemModel {
class DiffTreeModel : public QStandardItemModel {
Q_OBJECT
public:
@ -157,6 +158,7 @@ private:
private:
Node *node(const QModelIndex &index) const;
QVariant getDisplayRole(const QModelIndex &index) const;
QFileIconProvider mIconProvider;

View File

@ -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"
@ -98,6 +97,7 @@ DoubleTreeWidget::DoubleTreeWidget(const git::Repository &repo, QWidget *parent)
listView->setChecked(Settings::instance()
->value(Setting::Id::ShowChangedFilesAsList, false)
.toBool());
RepoView::parentView(this)->refresh();
connect(listView, &QAction::triggered, this, [this](bool checked) {
Settings::instance()->setValue(Setting::Id::ShowChangedFilesAsList,
checked);
@ -160,13 +160,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);
@ -192,13 +187,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);

View File

@ -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() {}

View File

@ -16,13 +16,14 @@
#include <QSortFilterProxyModel>
#include <QFileIconProvider>
class QAbstractItemModel;
class TreeModel;
class TreeProxy : public QSortFilterProxyModel {
Q_OBJECT
public:
TreeProxy(bool staged, QObject *parent = nullptr);
TreeProxy(bool staged, QAbstractItemModel *model, QObject *parent);
virtual ~TreeProxy();
bool setData(const QModelIndex &index, const QVariant &value,
int role = Qt::EditRole, bool ignoreIndexChanges = false);
@ -30,6 +31,10 @@ public:
void enableFilter(bool enable) { mFilter = enable; }
int columnCount(const QModelIndex &parent = QModelIndex()) const override {
return sourceModel()->columnCount();
}
private:
using QSortFilterProxyModel::setData;
bool filterAcceptsRow(int source_row,

View File

@ -24,6 +24,9 @@
#include "RepoView.h"
#include <QMessageBox>
#include <QPushButton>
#include "conf/Settings.h"
#include <QAbstractItemModel>
#include <memory>
#ifdef Q_OS_WIN
#define ICON_SIZE 48
@ -41,8 +44,29 @@ const QString kLabelFmt = "<p style='color: gray; font-weight: bold'>%1</p>";
} // namespace
TreeView::TreeView(QWidget *parent, const QString &name)
: QTreeView(parent), mSharedDelegate(new ViewDelegate(this)), mName(name) {
: QTreeView(parent), mDelegateCol(0),
mFileListDelegatePtr(std::make_unique<ViewDelegate>(this, true)),
mFileTreeDelegatePtr(std::make_unique<ViewDelegate>(this)), mName(name) {
setObjectName(name);
connect(RepoView::parentView(this)->repo().notifier(),
&git::RepositoryNotifier::referenceUpdated, this,
&TreeView::updateView);
}
void TreeView::updateView() {
QAbstractItemModel *itemModel = model();
if (!itemModel)
return;
// Remove any previous delegate on the current column, get the new current
// column, and set the delegate on that.
setItemDelegateForColumn(mDelegateCol, nullptr);
mDelegateCol = itemModel->columnCount() - 1;
setItemDelegateForColumn(mDelegateCol, mDelegateCol
? mFileListDelegatePtr.get()
: mFileTreeDelegatePtr.get());
setHeaderHidden(mDelegateCol ? false : true);
}
void TreeView::setModel(QAbstractItemModel *model) {

View File

@ -11,6 +11,8 @@
#define TREEVIEW_H
#include <QTreeView>
#include <memory>
#include "ViewDelegate.h"
class QItemDelegate;
class DiffTreeModel;
@ -96,9 +98,12 @@ private:
bool suppressDeselectionHandling{false};
int mCollapseCount; // Counts the number of collapsed folders.
bool mSupressItemExpandStateChanged{false};
void updateView();
QItemDelegate *mSharedDelegate;
QString mName;
std::unique_ptr<ViewDelegate> mFileListDelegatePtr;
std::unique_ptr<ViewDelegate> mFileTreeDelegatePtr;
int mDelegateCol = 0;
};
#endif // TREEVIEW_H

View File

@ -10,37 +10,6 @@ void ViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
QStyleOptionViewItem opt = option;
drawBackground(painter, opt, index);
// Draw >.
if (mDrawArrow && index.model()->hasChildren(index)) {
painter->save();
painter->setRenderHint(QPainter::Antialiasing, true);
QColor color = opt.palette.color(QPalette::Active, QPalette::BrightText);
if (opt.state & QStyle::State_Selected)
color =
!opt.showDecorationSelected
? opt.palette.color(QPalette::Active, QPalette::WindowText)
: opt.palette.color(QPalette::Active, QPalette::HighlightedText);
painter->setPen(color);
painter->setBrush(color);
int x = opt.rect.x() + opt.rect.width() - 3;
int y = opt.rect.y() + (opt.rect.height() / 2);
QPainterPath path;
path.moveTo(x, y);
path.lineTo(x - 5, y - 3);
path.lineTo(x - 5, y + 3);
path.closeSubpath();
painter->drawPath(path);
painter->restore();
// Adjust rect to exclude the arrow.
opt.rect.adjust(0, 0, -11, 0);
}
// Draw badges.
QString status = index.data(TreeModel::StatusRole).toString();
if (!status.isEmpty()) {
@ -49,19 +18,30 @@ void ViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
int width = size.width();
int height = size.height();
auto startIter = status.cbegin(), endIter = status.cend();
int leftAdjust = 0, rightAdjust = -3, leftWidth = 0, rightWidth = -width;
if (mMultiColumn) {
leftAdjust = 3;
rightAdjust = 0;
leftWidth = width;
rightWidth = 0;
std::reverse(status.begin(), status.end());
}
// Add extra space.
opt.rect.adjust(0, 0, -3, 0);
opt.rect.adjust(leftAdjust, 0, rightAdjust, 0);
for (int i = 0; i < status.count(); ++i) {
int x = opt.rect.x() + opt.rect.width();
int y = opt.rect.y() + (opt.rect.height() / 2);
QRect rect(x - width, y - (height / 2), width, height);
QRect rect(mMultiColumn ? opt.rect.x() : x - width, y - (height / 2),
width, height);
Badge::paint(painter,
{Badge::Label(Badge::Label::Type::Status, status.at(i))},
rect, &opt);
// Adjust rect.
opt.rect.adjust(0, 0, -width - 3, 0);
opt.rect.adjust(leftWidth + leftAdjust, 0, rightWidth + rightAdjust, 0);
}
}

View File

@ -9,9 +9,8 @@
*/
class ViewDelegate : public QItemDelegate {
public:
ViewDelegate(QObject *parent = nullptr) : QItemDelegate(parent) {}
void setDrawArrow(bool enable) { mDrawArrow = enable; }
ViewDelegate(QObject *parent, bool multiColumn = false)
: QItemDelegate(parent), mMultiColumn(multiColumn) {}
void paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const override;
@ -20,7 +19,7 @@ public:
const QModelIndex &index) const override;
private:
bool mDrawArrow = true;
bool mMultiColumn = false;
};
#endif // VIEWDELEGATE_H

View File

@ -3,7 +3,9 @@
#include "ui/MainWindow.h"
#include "ui/DoubleTreeWidget.h"
#include "ui/TreeView.h"
#include "ui/TreeProxy.h"
#include "ui/FileContextMenu.h"
#include "conf/Settings.h"
#include <QTextEdit>
@ -22,6 +24,18 @@ using namespace QTest;
\
RepoView *repoView = window.currentView();
static void disableListView(TreeView &treeView, RepoView &repoView) {
auto treeProxy = dynamic_cast<TreeProxy *>(treeView.model());
QVERIFY(treeProxy);
auto diffTreeModel = dynamic_cast<DiffTreeModel *>(treeProxy->sourceModel());
QVERIFY(diffTreeModel);
diffTreeModel->enableListView(false);
Settings::instance()->setValue(Setting::Id::ShowChangedFilesAsList, false);
repoView.refresh();
}
class TestTreeView : public QObject {
Q_OBJECT
@ -46,6 +60,7 @@ void TestTreeView::restoreStagedFileAfterCommit() {
{
auto unstagedTree = doubleTree->findChild<TreeView *>("Unstaged");
QVERIFY(unstagedTree);
disableListView(*unstagedTree, *view);
QAbstractItemModel *unstagedModel = unstagedTree->model();
// Wait for refresh
auto timeout = Timeout(10000, "Repository didn't refresh in time");

View File

@ -13,6 +13,8 @@
#include "ui/MainWindow.h"
#include "ui/RepoView.h"
#include "ui/TreeView.h"
#include "ui/TreeProxy.h"
#include "conf/Settings.h"
#include <QFile>
#include <QTextEdit>
#include <QTextStream>
@ -20,6 +22,18 @@
using namespace Test;
using namespace QTest;
static void disableListView(TreeView &treeView, RepoView &repoView) {
auto treeProxy = dynamic_cast<TreeProxy *>(treeView.model());
QVERIFY(treeProxy);
auto diffTreeModel = dynamic_cast<DiffTreeModel *>(treeProxy->sourceModel());
QVERIFY(diffTreeModel);
diffTreeModel->enableListView(false);
Settings::instance()->setValue(Setting::Id::ShowChangedFilesAsList, false);
repoView.refresh();
}
class TestIndex : public QObject {
Q_OBJECT
@ -56,6 +70,8 @@ void TestIndex::stageAddition() {
auto unstagedFiles = doubleTree->findChild<TreeView *>("Unstaged");
QVERIFY(unstagedFiles);
disableListView(*unstagedFiles, *view);
auto stagedFiles = doubleTree->findChild<TreeView *>("Staged");
QVERIFY(stagedFiles);
@ -151,6 +167,8 @@ void TestIndex::stageDirectory() {
auto unstagedFiles = doubleTree->findChild<TreeView *>("Unstaged");
QVERIFY(unstagedFiles);
disableListView(*unstagedFiles, *view);
auto stagedFiles = doubleTree->findChild<TreeView *>("Staged");
QVERIFY(stagedFiles);