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 build
.cache
.DS_Store .DS_Store
.project .project
.vscode/ .vscode/
@ -8,3 +9,5 @@ cmake-build-release/
build build
.idea/ .idea/
.venv .venv
compile_commands.json

View File

@ -7,6 +7,7 @@
// Author: Jason Haslam // Author: Jason Haslam
// //
#include <array>
#include "DiffTreeModel.h" #include "DiffTreeModel.h"
#include "conf/Settings.h" #include "conf/Settings.h"
#include "git/Blob.h" #include "git/Blob.h"
@ -16,15 +17,30 @@
#include "git/Patch.h" #include "git/Patch.h"
#include <QStringBuilder> #include <QStringBuilder>
#include <QUrl> #include <QUrl>
#include <qobjectdefs.h>
namespace { namespace {
const QString kLinkFmt = "<a href='%1'>%2</a>"; 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 } // namespace
DiffTreeModel::DiffTreeModel(const git::Repository &repo, QObject *parent) 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; } DiffTreeModel::~DiffTreeModel() { delete mRoot; }
@ -91,7 +107,9 @@ int DiffTreeModel::rowCount(const QModelIndex &parent) const {
return mDiff ? node(parent)->children().size() : 0; 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 { bool DiffTreeModel::hasChildren(const QModelIndex &parent) const {
return mRoot && node(parent)->hasChildren(); return mRoot && node(parent)->hasChildren();
@ -134,7 +152,7 @@ void DiffTreeModel::modelIndices(const QModelIndex &parent,
} }
for (int i = 0; i < n->children().length(); i++) { 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) if (recursive)
modelIndices(child, list); modelIndices(child, list);
else if (!node(child)->hasChildren()) else if (!node(child)->hasChildren())
@ -195,9 +213,15 @@ QVariant DiffTreeModel::data(const QModelIndex &index, int role) const {
return QVariant(); return QVariant();
Node *node = this->node(index); Node *node = this->node(index);
// Skip intermediate path elements for trees showing file lists only.
if (node->hasChildren() && asList())
return QVariant();
switch (role) { switch (role) {
case Qt::DisplayRole: case Qt::DisplayRole: {
return node->name(); return getDisplayRole(index);
}
// case Qt::DecorationRole: { // case Qt::DecorationRole: {
// QFileInfo info(node->path()); // QFileInfo info(node->path());
@ -212,7 +236,7 @@ QVariant DiffTreeModel::data(const QModelIndex &index, int role) const {
return node->path(); return node->path();
case Qt::CheckStateRole: { case Qt::CheckStateRole: {
if (!mDiff.isValid() || !mDiff.isStatusDiff()) if (!mDiff.isValid() || !mDiff.isStatusDiff() || index.column() > 0)
return QVariant(); return QVariant();
git::Index index = mDiff.index(); 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; 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 ############################################## //###### DiffTreeModel::Node ##############################################
//############################################################################# //#############################################################################

View File

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

View File

@ -15,7 +15,6 @@
#include "StatePushButton.h" #include "StatePushButton.h"
#include "TreeProxy.h" #include "TreeProxy.h"
#include "TreeView.h" #include "TreeView.h"
#include "ViewDelegate.h"
#include "Debug.h" #include "Debug.h"
#include "conf/Settings.h" #include "conf/Settings.h"
#include "DiffView/DiffView.h" #include "DiffView/DiffView.h"
@ -98,6 +97,7 @@ DoubleTreeWidget::DoubleTreeWidget(const git::Repository &repo, QWidget *parent)
listView->setChecked(Settings::instance() listView->setChecked(Settings::instance()
->value(Setting::Id::ShowChangedFilesAsList, false) ->value(Setting::Id::ShowChangedFilesAsList, false)
.toBool()); .toBool());
RepoView::parentView(this)->refresh();
connect(listView, &QAction::triggered, this, [this](bool checked) { connect(listView, &QAction::triggered, this, [this](bool checked) {
Settings::instance()->setValue(Setting::Id::ShowChangedFilesAsList, Settings::instance()->setValue(Setting::Id::ShowChangedFilesAsList,
checked); checked);
@ -160,13 +160,8 @@ DoubleTreeWidget::DoubleTreeWidget(const git::Repository &repo, QWidget *parent)
repoView->updateSubmodules(submodules, recursive, init, repoView->updateSubmodules(submodules, recursive, init,
force_checkout); force_checkout);
}); });
TreeProxy *treewrapperStaged = new TreeProxy(true, this);
treewrapperStaged->setSourceModel(mDiffTreeModel); stagedFiles->setModel(new TreeProxy(true, mDiffTreeModel, this));
stagedFiles->setModel(treewrapperStaged);
stagedFiles->setHeaderHidden(true);
ViewDelegate *stagedDelegate = new ViewDelegate();
stagedDelegate->setDrawArrow(false);
stagedFiles->setItemDelegateForColumn(0, stagedDelegate);
QHBoxLayout *hBoxLayout = new QHBoxLayout(); QHBoxLayout *hBoxLayout = new QHBoxLayout();
QLabel *label = new QLabel(kStagedFiles); QLabel *label = new QLabel(kStagedFiles);
@ -192,13 +187,7 @@ DoubleTreeWidget::DoubleTreeWidget(const git::Repository &repo, QWidget *parent)
showFileContextMenu(pos, repoView, unstagedFiles, false); showFileContextMenu(pos, repoView, unstagedFiles, false);
}); });
TreeProxy *treewrapperUnstaged = new TreeProxy(false, this); unstagedFiles->setModel(new TreeProxy(false, mDiffTreeModel, this));
treewrapperUnstaged->setSourceModel(mDiffTreeModel);
unstagedFiles->setModel(treewrapperUnstaged);
unstagedFiles->setHeaderHidden(true);
ViewDelegate *unstagedDelegate = new ViewDelegate();
unstagedDelegate->setDrawArrow(false);
unstagedFiles->setItemDelegateForColumn(0, unstagedDelegate);
hBoxLayout = new QHBoxLayout(); hBoxLayout = new QHBoxLayout();
mUnstagedCommitedFiles = new QLabel(kUnstagedFiles); mUnstagedCommitedFiles = new QLabel(kUnstagedFiles);

View File

@ -23,8 +23,10 @@ const QString kLinkFmt = "<a href='%1'>%2</a>";
} // namespace } // namespace
TreeProxy::TreeProxy(bool staged, QObject *parent) TreeProxy::TreeProxy(bool staged, QAbstractItemModel *model, QObject *parent)
: QSortFilterProxyModel(parent), mStaged(staged) {} : mStaged(staged), QSortFilterProxyModel(parent) {
setSourceModel(model);
}
TreeProxy::~TreeProxy() {} TreeProxy::~TreeProxy() {}

View File

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

View File

@ -24,6 +24,9 @@
#include "RepoView.h" #include "RepoView.h"
#include <QMessageBox> #include <QMessageBox>
#include <QPushButton> #include <QPushButton>
#include "conf/Settings.h"
#include <QAbstractItemModel>
#include <memory>
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
#define ICON_SIZE 48 #define ICON_SIZE 48
@ -41,8 +44,29 @@ const QString kLabelFmt = "<p style='color: gray; font-weight: bold'>%1</p>";
} // namespace } // namespace
TreeView::TreeView(QWidget *parent, const QString &name) 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); 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) { void TreeView::setModel(QAbstractItemModel *model) {

View File

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

View File

@ -10,37 +10,6 @@ void ViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
QStyleOptionViewItem opt = option; QStyleOptionViewItem opt = option;
drawBackground(painter, opt, index); 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. // Draw badges.
QString status = index.data(TreeModel::StatusRole).toString(); QString status = index.data(TreeModel::StatusRole).toString();
if (!status.isEmpty()) { if (!status.isEmpty()) {
@ -49,19 +18,30 @@ void ViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
int width = size.width(); int width = size.width();
int height = size.height(); 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. // 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) { for (int i = 0; i < status.count(); ++i) {
int x = opt.rect.x() + opt.rect.width(); int x = opt.rect.x() + opt.rect.width();
int y = opt.rect.y() + (opt.rect.height() / 2); 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::paint(painter,
{Badge::Label(Badge::Label::Type::Status, status.at(i))}, {Badge::Label(Badge::Label::Type::Status, status.at(i))},
rect, &opt); rect, &opt);
// Adjust rect. // 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 { class ViewDelegate : public QItemDelegate {
public: public:
ViewDelegate(QObject *parent = nullptr) : QItemDelegate(parent) {} ViewDelegate(QObject *parent, bool multiColumn = false)
: QItemDelegate(parent), mMultiColumn(multiColumn) {}
void setDrawArrow(bool enable) { mDrawArrow = enable; }
void paint(QPainter *painter, const QStyleOptionViewItem &option, void paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const override; const QModelIndex &index) const override;
@ -20,7 +19,7 @@ public:
const QModelIndex &index) const override; const QModelIndex &index) const override;
private: private:
bool mDrawArrow = true; bool mMultiColumn = false;
}; };
#endif // VIEWDELEGATE_H #endif // VIEWDELEGATE_H

View File

@ -3,7 +3,9 @@
#include "ui/MainWindow.h" #include "ui/MainWindow.h"
#include "ui/DoubleTreeWidget.h" #include "ui/DoubleTreeWidget.h"
#include "ui/TreeView.h" #include "ui/TreeView.h"
#include "ui/TreeProxy.h"
#include "ui/FileContextMenu.h" #include "ui/FileContextMenu.h"
#include "conf/Settings.h"
#include <QTextEdit> #include <QTextEdit>
@ -22,6 +24,18 @@ using namespace QTest;
\ \
RepoView *repoView = window.currentView(); 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 { class TestTreeView : public QObject {
Q_OBJECT Q_OBJECT
@ -46,6 +60,7 @@ void TestTreeView::restoreStagedFileAfterCommit() {
{ {
auto unstagedTree = doubleTree->findChild<TreeView *>("Unstaged"); auto unstagedTree = doubleTree->findChild<TreeView *>("Unstaged");
QVERIFY(unstagedTree); QVERIFY(unstagedTree);
disableListView(*unstagedTree, *view);
QAbstractItemModel *unstagedModel = unstagedTree->model(); QAbstractItemModel *unstagedModel = unstagedTree->model();
// Wait for refresh // Wait for refresh
auto timeout = Timeout(10000, "Repository didn't refresh in time"); auto timeout = Timeout(10000, "Repository didn't refresh in time");

View File

@ -13,6 +13,8 @@
#include "ui/MainWindow.h" #include "ui/MainWindow.h"
#include "ui/RepoView.h" #include "ui/RepoView.h"
#include "ui/TreeView.h" #include "ui/TreeView.h"
#include "ui/TreeProxy.h"
#include "conf/Settings.h"
#include <QFile> #include <QFile>
#include <QTextEdit> #include <QTextEdit>
#include <QTextStream> #include <QTextStream>
@ -20,6 +22,18 @@
using namespace Test; using namespace Test;
using namespace QTest; 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 { class TestIndex : public QObject {
Q_OBJECT Q_OBJECT
@ -56,6 +70,8 @@ void TestIndex::stageAddition() {
auto unstagedFiles = doubleTree->findChild<TreeView *>("Unstaged"); auto unstagedFiles = doubleTree->findChild<TreeView *>("Unstaged");
QVERIFY(unstagedFiles); QVERIFY(unstagedFiles);
disableListView(*unstagedFiles, *view);
auto stagedFiles = doubleTree->findChild<TreeView *>("Staged"); auto stagedFiles = doubleTree->findChild<TreeView *>("Staged");
QVERIFY(stagedFiles); QVERIFY(stagedFiles);
@ -151,6 +167,8 @@ void TestIndex::stageDirectory() {
auto unstagedFiles = doubleTree->findChild<TreeView *>("Unstaged"); auto unstagedFiles = doubleTree->findChild<TreeView *>("Unstaged");
QVERIFY(unstagedFiles); QVERIFY(unstagedFiles);
disableListView(*unstagedFiles, *view);
auto stagedFiles = doubleTree->findChild<TreeView *>("Staged"); auto stagedFiles = doubleTree->findChild<TreeView *>("Staged");
QVERIFY(stagedFiles); QVERIFY(stagedFiles);