From 1d6d53ba009e374f361d017a0238a583d49831bf Mon Sep 17 00:00:00 2001 From: devnull Date: Thu, 31 Aug 2023 16:30:36 -0400 Subject: [PATCH] Use columns for file name, directory, and state when files are shown as a list in TreeViews. Resolves Dense layout issue #547 --- .gitignore | 3 +++ src/ui/DiffTreeModel.cpp | 52 ++++++++++++++++++++++++++++++++----- src/ui/DiffTreeModel.h | 6 +++-- src/ui/DoubleTreeWidget.cpp | 19 +++----------- src/ui/TreeProxy.cpp | 6 +++-- src/ui/TreeProxy.h | 7 ++++- src/ui/TreeView.cpp | 26 ++++++++++++++++++- src/ui/TreeView.h | 7 ++++- src/ui/ViewDelegate.cpp | 48 ++++++++++------------------------ src/ui/ViewDelegate.h | 7 +++-- test/TreeView.cpp | 15 +++++++++++ test/index.cpp | 18 +++++++++++++ 12 files changed, 148 insertions(+), 66 deletions(-) diff --git a/.gitignore b/.gitignore index 6786e6c1..ca6f9e25 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ build +.cache .DS_Store .project .vscode/ @@ -8,3 +9,5 @@ cmake-build-release/ build .idea/ .venv +compile_commands.json + diff --git a/src/ui/DiffTreeModel.cpp b/src/ui/DiffTreeModel.cpp index a722705c..6745d18d 100644 --- a/src/ui/DiffTreeModel.cpp +++ b/src/ui/DiffTreeModel.cpp @@ -7,6 +7,7 @@ // Author: Jason Haslam // +#include #include "DiffTreeModel.h" #include "conf/Settings.h" #include "git/Blob.h" @@ -16,15 +17,30 @@ #include "git/Patch.h" #include #include +#include namespace { const QString kLinkFmt = "%2"; +const std::array 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(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 ############################################## //############################################################################# diff --git a/src/ui/DiffTreeModel.h b/src/ui/DiffTreeModel.h index 9b1d2dba..0dc424fc 100644 --- a/src/ui/DiffTreeModel.h +++ b/src/ui/DiffTreeModel.h @@ -14,7 +14,8 @@ #include "git/Index.h" #include "git/Tree.h" #include "git/Repository.h" -#include +#include +#include #include #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; diff --git a/src/ui/DoubleTreeWidget.cpp b/src/ui/DoubleTreeWidget.cpp index c5974670..45a005f8 100644 --- a/src/ui/DoubleTreeWidget.cpp +++ b/src/ui/DoubleTreeWidget.cpp @@ -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); diff --git a/src/ui/TreeProxy.cpp b/src/ui/TreeProxy.cpp index f956c3a6..2b447f85 100644 --- a/src/ui/TreeProxy.cpp +++ b/src/ui/TreeProxy.cpp @@ -23,8 +23,10 @@ const QString kLinkFmt = "%2"; } // 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() {} diff --git a/src/ui/TreeProxy.h b/src/ui/TreeProxy.h index 2e8d75ae..93ab7a4f 100644 --- a/src/ui/TreeProxy.h +++ b/src/ui/TreeProxy.h @@ -16,13 +16,14 @@ #include #include +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, diff --git a/src/ui/TreeView.cpp b/src/ui/TreeView.cpp index a7895dd1..75c720b4 100644 --- a/src/ui/TreeView.cpp +++ b/src/ui/TreeView.cpp @@ -24,6 +24,9 @@ #include "RepoView.h" #include #include +#include "conf/Settings.h" +#include +#include #ifdef Q_OS_WIN #define ICON_SIZE 48 @@ -41,8 +44,29 @@ const QString kLabelFmt = "

%1

"; } // namespace TreeView::TreeView(QWidget *parent, const QString &name) - : QTreeView(parent), mSharedDelegate(new ViewDelegate(this)), mName(name) { + : QTreeView(parent), mDelegateCol(0), + mFileListDelegatePtr(std::make_unique(this, true)), + mFileTreeDelegatePtr(std::make_unique(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) { diff --git a/src/ui/TreeView.h b/src/ui/TreeView.h index 138fd34e..87cc642f 100644 --- a/src/ui/TreeView.h +++ b/src/ui/TreeView.h @@ -11,6 +11,8 @@ #define TREEVIEW_H #include +#include +#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 mFileListDelegatePtr; + std::unique_ptr mFileTreeDelegatePtr; + int mDelegateCol = 0; }; #endif // TREEVIEW_H diff --git a/src/ui/ViewDelegate.cpp b/src/ui/ViewDelegate.cpp index 194a915a..8e69662e 100644 --- a/src/ui/ViewDelegate.cpp +++ b/src/ui/ViewDelegate.cpp @@ -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); } } diff --git a/src/ui/ViewDelegate.h b/src/ui/ViewDelegate.h index 03f168a0..fc81c09a 100644 --- a/src/ui/ViewDelegate.h +++ b/src/ui/ViewDelegate.h @@ -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 diff --git a/test/TreeView.cpp b/test/TreeView.cpp index 246e8efb..d8576722 100644 --- a/test/TreeView.cpp +++ b/test/TreeView.cpp @@ -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 @@ -22,6 +24,18 @@ using namespace QTest; \ RepoView *repoView = window.currentView(); +static void disableListView(TreeView &treeView, RepoView &repoView) { + auto treeProxy = dynamic_cast(treeView.model()); + QVERIFY(treeProxy); + + auto diffTreeModel = dynamic_cast(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("Unstaged"); QVERIFY(unstagedTree); + disableListView(*unstagedTree, *view); QAbstractItemModel *unstagedModel = unstagedTree->model(); // Wait for refresh auto timeout = Timeout(10000, "Repository didn't refresh in time"); diff --git a/test/index.cpp b/test/index.cpp index c6592afe..f3fdc3c8 100644 --- a/test/index.cpp +++ b/test/index.cpp @@ -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 #include #include @@ -20,6 +22,18 @@ using namespace Test; using namespace QTest; +static void disableListView(TreeView &treeView, RepoView &repoView) { + auto treeProxy = dynamic_cast(treeView.model()); + QVERIFY(treeProxy); + + auto diffTreeModel = dynamic_cast(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("Unstaged"); QVERIFY(unstagedFiles); + disableListView(*unstagedFiles, *view); + auto stagedFiles = doubleTree->findChild("Staged"); QVERIFY(stagedFiles); @@ -151,6 +167,8 @@ void TestIndex::stageDirectory() { auto unstagedFiles = doubleTree->findChild("Unstaged"); QVERIFY(unstagedFiles); + disableListView(*unstagedFiles, *view); + auto stagedFiles = doubleTree->findChild("Staged"); QVERIFY(stagedFiles);