Add custom painting and section handling to RDTableView & RDHeaderView

* This is used primarily for the buffer/mesh viewer to be able to pin
  the index/element column to the left side, group columns together with
  a noticeable separator, and other minor tweaks.
* Unfortunately due to tight private coupling and lack of virtual
  functions in the QTableView and QHeaderView, a few unrelated functions
  have to be re-implemented to point to our own header.
This commit is contained in:
baldurk
2017-06-23 21:38:51 +01:00
parent 5c342332f9
commit c211df25be
5 changed files with 1164 additions and 19 deletions
+619 -1
View File
@@ -28,6 +28,20 @@
#include <QPainter>
#include <QPixmap>
/////////////////////////////////////////////////////////////////////////////////
//
// this file contains a few hardcoded assumptions for my use case, especially
// with the 'custom sizing' mode that allows merging sections and pinning sections
// and so on.
//
// * No handling for moving/rearranging/hiding sections with the custom sizing
// mode. Just needs more careful handling and distinguishing between logical
// and visual indices.
// * Probably a few places vertical orientation isn't handled right, but that
// shouldn't be too bad.
//
/////////////////////////////////////////////////////////////////////////////////
RDHeaderView::RDHeaderView(Qt::Orientation orient, QWidget *parent) : QHeaderView(orient, parent)
{
m_sectionPreview = new QLabel(this);
@@ -37,12 +51,285 @@ RDHeaderView::~RDHeaderView()
{
}
QSize RDHeaderView::sizeHint() const
{
if(!m_customSizing)
return QHeaderView::sizeHint();
return m_sizeHint;
}
void RDHeaderView::setModel(QAbstractItemModel *model)
{
QAbstractItemModel *m = this->model();
if(m)
{
QObject::disconnect(m, &QAbstractItemModel::headerDataChanged, this,
&RDHeaderView::headerDataChanged);
QObject::disconnect(m, &QAbstractItemModel::columnsInserted, this,
&RDHeaderView::columnsInserted);
}
QHeaderView::setModel(model);
QObject::connect(model, &QAbstractItemModel::headerDataChanged, this,
&RDHeaderView::headerDataChanged);
QObject::connect(model, &QAbstractItemModel::columnsInserted, this, &RDHeaderView::columnsInserted);
}
void RDHeaderView::reset()
{
if(m_customSizing)
cacheSections();
}
void RDHeaderView::cacheSections()
{
if(m_suppressSectionCache)
return;
QAbstractItemModel *m = this->model();
int oldCount = m_sections.count();
m_sections.resize(m->columnCount());
// give new sections a default minimum size
for(int col = oldCount; col < m_sections.count(); col++)
m_sections[col].size = 10;
for(int col = 0; col < m_sections.count(); col++)
{
if(m_columnGroupRole > 0)
{
QVariant v = m->data(m->index(0, col), m_columnGroupRole);
if(v.isValid())
m_sections[col].group = v.toInt();
if(col > 0)
{
m_sections[col - 1].groupGap =
(m_sections[col].group != m_sections[col - 1].group) && m_sections[col].group >= 0;
}
}
else
{
m_sections[col].group = col;
m_sections[col].groupGap = true;
}
}
int accum = 0;
for(int col = 0; col < m_sections.count(); col++)
{
if(col == m_pinnedColumns)
m_pinnedWidth = accum;
m_sections[col].offset = accum;
accum += m_sections[col].size;
if(hasGroupGap(col))
accum += groupGapSize();
}
if(m_pinnedColumns >= m_sections.count())
m_pinnedWidth = m_pinnedColumns;
QStyleOptionHeader opt;
initStyleOption(&opt);
QFont f = font();
f.setBold(true);
opt.section = 0;
opt.fontMetrics = QFontMetrics(f);
opt.text = m->headerData(0, orientation(), Qt::DisplayRole).toString();
m_sizeHint = style()->sizeFromContents(QStyle::CT_HeaderSection, &opt, QSize(), this);
m_sizeHint.setWidth(accum);
viewport()->update(viewport()->rect());
}
int RDHeaderView::sectionSize(int logicalIndex) const
{
if(m_customSizing)
{
if(logicalIndex < 0 || logicalIndex >= m_sections.count())
return 0;
return m_sections[logicalIndex].size;
}
return QHeaderView::sectionSize(logicalIndex);
}
int RDHeaderView::sectionViewportPosition(int logicalIndex) const
{
if(m_customSizing)
{
if(logicalIndex < 0 || logicalIndex >= m_sections.count())
return -1;
int offs = m_sections[logicalIndex].offset;
if(logicalIndex >= m_pinnedColumns)
offs -= offset();
return offs;
}
return QHeaderView::sectionViewportPosition(logicalIndex);
}
int RDHeaderView::visualIndexAt(int position) const
{
if(m_customSizing)
{
if(m_sections.isEmpty())
return -1;
if(position >= m_pinnedWidth)
position += offset();
SectionData search;
search.offset = position;
auto it = std::lower_bound(
m_sections.begin(), m_sections.end(), search,
[this](const SectionData &a, const SectionData &b) { return a.offset <= b.offset; });
if(it != m_sections.begin())
--it;
if(it->offset <= position &&
position < (it->offset + it->size + (it->groupGap ? groupGapSize() : 0)))
return (it - m_sections.begin());
return -1;
}
return QHeaderView::visualIndexAt(position);
}
int RDHeaderView::logicalIndexAt(int position) const
{
return visualIndexAt(position);
}
int RDHeaderView::count() const
{
if(m_customSizing)
return m_sections.count();
return QHeaderView::count();
}
void RDHeaderView::resizeSection(int logicalIndex, int size)
{
if(!m_customSizing)
return QHeaderView::resizeSection(logicalIndex, size);
if(logicalIndex >= 0 && logicalIndex < m_sections.count())
{
int oldSize = m_sections[logicalIndex].size;
m_sections[logicalIndex].size = size;
emit sectionResized(logicalIndex, oldSize, size);
}
cacheSections();
}
void RDHeaderView::resizeSections(QHeaderView::ResizeMode mode)
{
if(!m_customSizing)
return resizeSections(mode);
if(mode != ResizeToContents)
return;
QAbstractItemModel *m = this->model();
int rowCount = m->rowCount();
for(int col = 0; col < m_sections.count(); col++)
{
QSize sz;
for(int row = 0; row < rowCount; row++)
{
QVariant v = m->data(m->index(row, col), Qt::SizeHintRole);
if(v.isValid() && v.canConvert<QSize>())
sz = sz.expandedTo(v.value<QSize>());
}
int oldSize = m_sections[col].size;
m_sections[col].size = sz.width();
emit sectionResized(col, oldSize, sz.width());
}
}
void RDHeaderView::resizeSections(const QList<int> &sizes)
{
if(!m_customSizing)
{
for(int i = 0; i < qMin(sizes.count(), QHeaderView::count()); i++)
{
QHeaderView::resizeSection(i, sizes[i]);
}
}
for(int i = 0; i < qMin(sizes.count(), m_sections.count()); i++)
{
int oldSize = m_sections[i].size;
m_sections[i].size = sizes[i];
emit sectionResized(i, oldSize, sizes[i]);
}
cacheSections();
}
bool RDHeaderView::hasGroupGap(int columnIndex) const
{
if(columnIndex >= 0 && columnIndex < m_sections.count())
return m_sections[columnIndex].groupGap;
return false;
}
bool RDHeaderView::hasGroupTitle(int columnIndex) const
{
if(columnIndex == m_sections.count() - 1)
return true;
if(columnIndex >= 0 && columnIndex < m_sections.count())
return m_sections[columnIndex].groupGap || m_sections[columnIndex].group < 0;
return false;
}
void RDHeaderView::headerDataChanged(Qt::Orientation orientation, int logicalFirst, int logicalLast)
{
if(m_customSizing)
cacheSections();
}
void RDHeaderView::columnsInserted(const QModelIndex &parent, int first, int last)
{
if(m_customSizing)
cacheSections();
}
void RDHeaderView::mousePressEvent(QMouseEvent *event)
{
int mousePos = event->x();
int idx = logicalIndexAt(mousePos);
if(idx >= 0 && event->buttons() == Qt::LeftButton)
if(sectionsMovable() && idx >= 0 && event->buttons() == Qt::LeftButton)
{
int secSize = sectionSize(idx);
int secPos = sectionViewportPosition(idx);
@@ -75,6 +362,14 @@ void RDHeaderView::mousePressEvent(QMouseEvent *event)
}
}
if(m_customSizing)
{
m_resizeState = checkResizing(event);
m_cursorPos = QCursor::pos().x();
return QAbstractItemView::mousePressEvent(event);
}
QHeaderView::mousePressEvent(event);
}
@@ -86,9 +381,140 @@ void RDHeaderView::mouseMoveEvent(QMouseEvent *event)
return;
}
if(m_customSizing)
{
if(m_resizeState.first == NoResize || m_resizeState.second < 0 ||
m_resizeState.second >= m_sections.count())
{
auto res = checkResizing(event);
bool hasCursor = testAttribute(Qt::WA_SetCursor);
if(res.first != NoResize)
{
if(!hasCursor)
setCursor(Qt::SplitHCursor);
}
else if(hasCursor)
{
unsetCursor();
}
}
else
{
int curX = QCursor::pos().x();
int delta = curX - m_cursorPos;
int idx = m_resizeState.second;
if(m_resizeState.first == LeftResize && idx > 0)
idx--;
// batch the cache update
m_suppressSectionCache = true;
int firstCol = idx;
int lastCol = idx;
// idx is the last in a group, so search backwards to see if there are neighbour sections we
// should share the resize with
while(firstCol > 0 && m_sections[firstCol - 1].group == m_sections[lastCol].group)
firstCol--;
// how much space could we lose on the columns, in total
int freeSpace = 0;
for(int col = firstCol; col <= lastCol; col++)
freeSpace += m_sections[col].size - minimumSectionSize();
int numCols = lastCol - firstCol + 1;
// spread the delta amonst the colummns
int perSectionDelta = delta / numCols;
// call resizeSection to emit the sectionResized signal but we set m_suppressSectionCache so
// we won't cache sections.
for(int col = firstCol; col <= lastCol; col++)
resizeSection(col, qMax(minimumSectionSize(), m_sections[col].size + perSectionDelta));
// if there was an uneven spread, a few pixels will remain
int remainder = delta - perSectionDelta * numCols;
// loop around for the remainder pixels, assigning them one by one to the smallest/largest
// column.
// this is inefficient but remainder is very small - at most 3.
int step = remainder < 0 ? -1 : 1;
for(int i = 0; i < qAbs(remainder); i++)
{
int chosenCol = firstCol;
for(int col = firstCol; col <= lastCol; col++)
{
if(step > 0 && m_sections[col].size < m_sections[chosenCol].size)
chosenCol = col;
else if(step < 0 && m_sections[col].size > m_sections[chosenCol].size)
chosenCol = col;
}
resizeSection(chosenCol, qMax(minimumSectionSize(), m_sections[chosenCol].size + step));
}
// only updating the cursor when the section is moving means that it becomes 'sticky'. If we
// try to size down below the minimum size and keep going then it doesn't start resizing up
// until it passes the divider again.
int appliedDelta = delta;
// if we were resizing down, at best we removed the remaining free space
if(delta < 0)
appliedDelta = qMax(delta, -freeSpace);
m_cursorPos += appliedDelta;
m_suppressSectionCache = false;
cacheSections();
}
return QAbstractItemView::mouseMoveEvent(event);
}
QHeaderView::mouseMoveEvent(event);
}
QPair<RDHeaderView::ResizeType, int> RDHeaderView::checkResizing(QMouseEvent *event)
{
int mousePos = event->x();
int idx = logicalIndexAt(mousePos);
bool hasCursor = testAttribute(Qt::WA_SetCursor);
bool cursorSet = false;
bool leftResize = idx > 0 && (m_sections[idx - 1].group != m_sections[idx].group);
bool rightResize = idx >= 0 && hasGroupTitle(idx);
if(leftResize || rightResize)
{
int secSize = sectionSize(idx);
int secPos = sectionViewportPosition(idx);
int handleWidth = style()->pixelMetric(QStyle::PM_HeaderGripMargin, 0, this);
int gapWidth = 0;
if(hasGroupGap(idx))
gapWidth = groupGapSize();
if(leftResize && secPos >= 0 && secSize > 0 && mousePos < secPos + handleWidth)
{
return qMakePair(LeftResize, idx);
}
if(rightResize && secPos >= 0 && secSize > 0 &&
mousePos > secPos + secSize - handleWidth - gapWidth)
{
return qMakePair(RightResize, idx);
}
}
return qMakePair(NoResize, -1);
}
void RDHeaderView::mouseReleaseEvent(QMouseEvent *event)
{
if(m_movingSection >= 0)
@@ -132,5 +558,197 @@ void RDHeaderView::mouseReleaseEvent(QMouseEvent *event)
m_movingSection = -1;
if(m_customSizing)
{
m_resizeState = qMakePair(NoResize, -1);
return QAbstractItemView::mouseReleaseEvent(event);
}
QHeaderView::mouseReleaseEvent(event);
}
void RDHeaderView::paintEvent(QPaintEvent *e)
{
if(!m_customSizing)
return QHeaderView::paintEvent(e);
if(count() == 0)
return;
QPainter painter(viewport());
int start = qMax(visualIndexAt(e->rect().left()), 0);
int end = visualIndexAt(e->rect().right());
if(end == -1)
end = count() - 1;
// make sure we always paint the whole header for any merged headers
while(start > 0 && !hasGroupTitle(start - 1))
start--;
while(end < m_sections.count() && !hasGroupTitle(end))
end++;
QRect accumRect;
for(int i = start; i <= end; ++i)
{
int pos = sectionViewportPosition(i);
int size = sectionSize(i);
if(!hasGroupGap(i) && pos < 0)
{
size += pos;
pos = 0;
}
// either set or accumulate this section's rect
if(accumRect.isEmpty())
accumRect.setRect(pos, 0, size, viewport()->height());
else
accumRect.setWidth(accumRect.width() + size);
if(hasGroupTitle(i))
{
painter.save();
accumRect.setWidth(accumRect.width() - 1);
if(accumRect.left() < m_pinnedWidth && i >= m_pinnedColumns)
accumRect.setLeft(m_pinnedWidth);
paintSection(&painter, accumRect, i);
painter.restore();
// if we have more sections to go, reset so we can accumulate the next group
if(i < end)
accumRect = QRect();
}
}
// clear the remainder of the header if there's a gap
if(accumRect.right() < e->rect().right())
{
QStyleOption opt;
opt.init(this);
opt.state |= QStyle::State_Horizontal;
opt.rect =
QRect(accumRect.right() + 1, 0, e->rect().right() - accumRect.right(), viewport()->height());
style()->drawControl(QStyle::CE_HeaderEmptyArea, &opt, &painter, this);
}
}
void RDHeaderView::paintSection(QPainter *painter, const QRect &rect, int section) const
{
if(!m_customSizing)
return QHeaderView::paintSection(painter, rect, section);
if(!rect.isValid())
return;
QStyleOptionHeader opt;
initStyleOption(&opt);
QAbstractItemModel *m = this->model();
if(hasFocus())
opt.state |= (QStyle::State_Active | QStyle::State_HasFocus);
else
opt.state &= ~(QStyle::State_Active | QStyle::State_HasFocus);
QVariant textAlignment = m->headerData(section, orientation(), Qt::TextAlignmentRole);
opt.rect = rect;
opt.section = section;
opt.textAlignment = Qt::AlignLeft | Qt::AlignVCenter;
opt.iconAlignment = Qt::AlignVCenter;
QVariant variant;
if(m_columnGroupRole)
{
variant = m->headerData(section, orientation(), m_columnGroupRole);
if(variant.isValid() && variant.canConvert<QString>())
opt.text = variant.toString();
}
if(opt.text.isEmpty())
opt.text = m->headerData(section, orientation(), Qt::DisplayRole).toString();
int margin = 2 * style()->pixelMetric(QStyle::PM_HeaderMargin, 0, this);
if(textElideMode() != Qt::ElideNone)
opt.text = opt.fontMetrics.elidedText(opt.text, textElideMode(), rect.width() - margin);
if(section == 0 && section == m_sections.count() - 1)
opt.position = QStyleOptionHeader::OnlyOneSection;
else if(section == 0)
opt.position = QStyleOptionHeader::Beginning;
else if(section == m_sections.count() - 1)
opt.position = QStyleOptionHeader::End;
else
opt.position = QStyleOptionHeader::Middle;
opt.orientation = orientation();
bool prevSel = section > 0 && selectionModel()->isColumnSelected(section - 1, QModelIndex());
bool nextSel = section + 1 < m_sections.count() &&
selectionModel()->isColumnSelected(section + 1, QModelIndex());
if(prevSel && nextSel)
opt.selectedPosition = QStyleOptionHeader::NextAndPreviousAreSelected;
else if(prevSel)
opt.selectedPosition = QStyleOptionHeader::PreviousIsSelected;
else if(nextSel)
opt.selectedPosition = QStyleOptionHeader::NextIsSelected;
else
opt.selectedPosition = QStyleOptionHeader::NotAdjacent;
style()->drawControl(QStyle::CE_Header, &opt, painter, this);
}
void RDHeaderView::currentChanged(const QModelIndex &current, const QModelIndex &old)
{
if(!m_customSizing)
return QHeaderView::currentChanged(current, old);
// not optimal at all
if(current != old)
{
QRect r = viewport()->rect();
if(old.isValid())
{
QRect rect = r;
if(orientation() == Qt::Horizontal)
{
rect.setLeft(sectionViewportPosition(old.column()));
rect.setWidth(sectionSize(old.column()));
}
else
{
rect.setTop(sectionViewportPosition(old.column()));
rect.setHeight(sectionSize(old.column()));
}
viewport()->update(rect);
}
if(current.isValid())
{
QRect rect = r;
if(orientation() == Qt::Horizontal)
{
rect.setLeft(sectionViewportPosition(current.column()));
rect.setWidth(sectionSize(current.column()));
}
else
{
rect.setTop(sectionViewportPosition(current.column()));
rect.setHeight(sectionSize(current.column()));
}
viewport()->update(rect);
}
}
}
@@ -36,12 +36,88 @@ public:
explicit RDHeaderView(Qt::Orientation orient, QWidget *parent = 0);
~RDHeaderView();
int groupGapSize() const { return 6; }
QSize sizeHint() const override;
void setModel(QAbstractItemModel *model) override;
void reset() override;
// these aren't virtual so we can't override them properly, but it's convenient for internal use
// and any external calls that go to this type directly to use the correct version
int sectionSize(int logicalIndex) const;
int sectionViewportPosition(int logicalIndex) const;
int visualIndexAt(int position) const;
int logicalIndexAt(int position) const;
int count() const;
void resizeSection(int logicalIndex, int size);
void resizeSections(const QList<int> &sizes);
void resizeSections(QHeaderView::ResizeMode mode);
inline int logicalIndexAt(int x, int y) const;
inline int logicalIndexAt(const QPoint &pos) const;
bool hasGroupGap(int columnIndex) const;
bool hasGroupTitle(int columnIndex) const;
void setColumnGroupRole(int role) { m_columnGroupRole = role; }
int columnGroupRole() const { return m_columnGroupRole; }
void setPinnedColumns(int numColumns) { m_pinnedColumns = numColumns; }
int pinnedColumns() const { return m_pinnedColumns; }
void setCustomSizing(bool sizing) { m_customSizing = sizing; }
int pinnedWidth() { return m_pinnedWidth; }
public slots:
void headerDataChanged(Qt::Orientation orientation, int logicalFirst, int logicalLast);
void columnsInserted(const QModelIndex &parent, int first, int last);
protected:
void mousePressEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
void paintEvent(QPaintEvent *e) override;
void paintSection(QPainter *painter, const QRect &rect, int section) const override;
void currentChanged(const QModelIndex &current, const QModelIndex &old) override;
enum ResizeType
{
NoResize,
LeftResize,
RightResize
};
QPair<ResizeType, int> checkResizing(QMouseEvent *event);
QPair<ResizeType, int> m_resizeState;
int m_cursorPos;
void cacheSections();
struct SectionData
{
int offset = 0;
int size = 0;
int group = 0;
bool groupGap = false;
};
QSize m_sizeHint;
QVector<SectionData> m_sections;
int m_pinnedWidth = 0;
bool m_suppressSectionCache = false;
bool m_customSizing = false;
int m_columnGroupRole = 0;
int m_pinnedColumns = 0;
int m_movingSection = -1;
QLabel *m_sectionPreview;
int m_sectionPreviewOffset = 0;
};
inline int RDHeaderView::logicalIndexAt(int ax, int ay) const
{
return orientation() == Qt::Horizontal ? logicalIndexAt(ax) : logicalIndexAt(ay);
}
inline int RDHeaderView::logicalIndexAt(const QPoint &apos) const
{
return logicalIndexAt(apos.x(), apos.y());
}
+389
View File
@@ -23,8 +23,397 @@
******************************************************************************/
#include "RDTableView.h"
#include <QAbstractButton>
#include <QHeaderView>
#include <QMouseEvent>
#include <QPainter>
#include <QPen>
#include <QScrollBar>
#include "RDHeaderView.h"
RDTableView::RDTableView(QWidget *parent) : QTableView(parent)
{
m_horizontalHeader = new RDHeaderView(Qt::Horizontal, this);
m_horizontalHeader->setCustomSizing(true);
setHorizontalHeader(m_horizontalHeader);
QObject::connect(m_horizontalHeader, &QHeaderView::sectionResized,
[this](int, int, int) { viewport()->update(); });
}
int RDTableView::columnViewportPosition(int column) const
{
return horizontalHeader()->sectionViewportPosition(column);
}
int RDTableView::columnAt(int x) const
{
return horizontalHeader()->visualIndexAt(x);
}
int RDTableView::columnWidth(int column) const
{
return horizontalHeader()->sectionSize(column);
}
void RDTableView::setColumnWidth(int column, int width)
{
horizontalHeader()->resizeSection(column, width);
updateGeometries();
}
void RDTableView::setColumnWidths(const QList<int> &widths)
{
horizontalHeader()->resizeSections(widths);
updateGeometries();
}
void RDTableView::resizeColumnsToContents()
{
horizontalHeader()->resizeSections(QHeaderView::ResizeToContents);
updateGeometries();
}
QRect RDTableView::visualRect(const QModelIndex &index) const
{
if(!index.isValid())
return QRect();
const int row = index.row();
const int col = index.column();
const int gridWidth = showGrid() ? 1 : 0;
return QRect(columnViewportPosition(col), rowViewportPosition(row), columnWidth(col) - gridWidth,
rowHeight(row) - gridWidth);
}
QRegion RDTableView::visualRegionForSelection(const QItemSelection &selection) const
{
QRegion selectionRegion;
const QRect viewRect = viewport()->rect();
QAbstractItemModel *m = model();
for(const QItemSelectionRange &selRange : selection)
{
for(int row = selRange.top(); row <= selRange.bottom(); row++)
{
for(int col = selRange.left(); col <= selRange.right(); col++)
{
const QRect &rangeRect = visualRect(m->index(row, col));
if(viewRect.intersects(rangeRect))
selectionRegion += rangeRect;
}
}
}
return selectionRegion;
}
QModelIndex RDTableView::indexAt(const QPoint &p) const
{
int row = rowAt(p.y());
int col = columnAt(p.x());
if(row < 0 || col < 0)
return QModelIndex();
return model()->index(row, col);
}
void RDTableView::setColumnGroupRole(int role)
{
m_columnGroupRole = role;
m_horizontalHeader->setColumnGroupRole(role);
}
void RDTableView::setPinnedColumns(int numColumns)
{
m_pinnedColumns = numColumns;
m_horizontalHeader->setPinnedColumns(numColumns);
}
void RDTableView::paintEvent(QPaintEvent *e)
{
const int gridWidth = showGrid() ? 1 : 0;
QStyleOptionViewItem opt = viewOptions();
QPainter painter(viewport());
if(model()->rowCount() == 0 || model()->columnCount() == 0)
return;
int firstRow = qMax(verticalHeader()->visualIndexAt(0), 0);
int lastRow = verticalHeader()->visualIndexAt(viewport()->height());
if(lastRow < 0)
lastRow = verticalHeader()->count() - 1;
lastRow = qMin(lastRow, verticalHeader()->count() - 1);
int firstCol = qMax(horizontalHeader()->visualIndexAt(horizontalHeader()->pinnedWidth() + 1), 0);
int lastCol = horizontalHeader()->visualIndexAt(viewport()->width());
if(lastCol < 0)
lastCol = horizontalHeader()->count() - 1;
lastCol = qMin(lastCol, horizontalHeader()->count() - 1);
firstCol = qMax(m_pinnedColumns, firstCol);
for(int row = firstRow; row <= lastRow; row++)
{
for(int col = firstCol; col <= lastCol; col++)
{
const QModelIndex index = model()->index(row, col);
if(index.isValid())
paintCell(&painter, index, opt);
}
for(int col = 0; col < m_pinnedColumns; col++)
{
const QModelIndex index = model()->index(row, col);
if(index.isValid())
paintCell(&painter, index, opt);
}
}
if(gridWidth)
{
QPen prevPen = painter.pen();
QBrush prevBrush = painter.brush();
QColor gridCol(QRgb(style()->styleHint(QStyle::SH_Table_GridLineColor, &opt, this)));
painter.setPen(QPen(gridCol, 0, gridStyle()));
painter.setBrush(QBrush(gridCol));
// draw bottom line of each row
for(int row = firstRow; row <= lastRow; row++)
{
int y = rowViewportPosition(row) + rowHeight(row) - gridWidth;
painter.drawLine(viewport()->rect().left(), y, viewport()->rect().right(), y);
}
int gapSize = m_horizontalHeader->groupGapSize();
// draw lines for each column, and group gaps
for(int col = firstCol; col <= lastCol; col++)
{
int x = columnViewportPosition(col) + columnWidth(col) - gridWidth;
if(m_horizontalHeader->hasGroupGap(col))
painter.drawRect(x, viewport()->rect().top(), gapSize, viewport()->rect().height());
else
painter.drawLine(x, viewport()->rect().top(), x, viewport()->rect().bottom());
}
for(int col = 0; col < m_pinnedColumns; col++)
{
int x = columnViewportPosition(col) + columnWidth(col) - gridWidth;
if(m_horizontalHeader->hasGroupGap(col))
painter.drawRect(x, viewport()->rect().top(), gapSize, viewport()->rect().height());
else
painter.drawLine(x, viewport()->rect().top(), x, viewport()->rect().bottom());
}
painter.setPen(prevPen);
painter.setBrush(prevBrush);
}
}
void RDTableView::paintCell(QPainter *painter, const QModelIndex &index,
const QStyleOptionViewItem &opt)
{
QStyleOptionViewItem cellopt = opt;
cellopt.rect = QRect(columnViewportPosition(index.column()), rowViewportPosition(index.row()),
columnWidth(index.column()), rowHeight(index.row()));
// erase the rect here since we need to draw over any overlapping non-pinned cells and
// there's no way to just clip the above painting :(
if(index.column() < m_pinnedColumns)
painter->eraseRect(cellopt.rect);
if(selectionModel() && selectionModel()->isSelected(index))
cellopt.state |= QStyle::State_Selected;
// draw the background, then the cell
style()->drawPrimitive(QStyle::PE_PanelItemViewRow, &cellopt, painter, this);
itemDelegate(index)->paint(painter, cellopt, index);
}
void RDTableView::scrollTo(const QModelIndex &index, ScrollHint hint)
{
if(!index.isValid())
return;
QRect cellRect = QRect(columnViewportPosition(index.column()), rowViewportPosition(index.row()),
columnWidth(index.column()), rowHeight(index.row()));
QRect dataRect = viewport()->rect();
dataRect.setLeft(horizontalHeader()->pinnedWidth());
// if it's already visible then just bail, common case
if(dataRect.contains(cellRect) && hint == QAbstractItemView::EnsureVisible)
return;
// assume per-item vertical scrolling and per-pixel horizontal scrolling
// for any hint except position at center, we just ensure it's visible horizontally
if(hint != QAbstractItemView::PositionAtCenter)
{
// scroll into view from the left
if(dataRect.left() > cellRect.left())
{
horizontalScrollBar()->setValue(horizontalScrollBar()->value() -
(dataRect.left() - cellRect.left()));
}
// scroll into view from the right
if(dataRect.right() < cellRect.right())
{
horizontalScrollBar()->setValue(horizontalScrollBar()->value() +
(cellRect.right() - dataRect.right()));
}
}
else
{
// center it horizontally from the left
QPoint dataCenter = dataRect.center();
QPoint cellCenter = cellRect.center();
if(dataCenter.x() > cellCenter.x())
{
horizontalScrollBar()->setValue(horizontalScrollBar()->value() -
(dataCenter.x() - cellCenter.x()));
}
// center it horizontally from the right
if(dataCenter.x() < cellCenter.x())
{
horizontalScrollBar()->setValue(horizontalScrollBar()->value() +
(cellCenter.x() - dataCenter.x()));
}
}
// collapse EnsureVisible to either PositionAtTop or PositionAtBottom depending on which side it's
// on, or just return if we only had to make it visible horizontally
if(hint == QAbstractItemView::EnsureVisible)
{
if(dataRect.bottom() < cellRect.bottom())
hint = QAbstractItemView::PositionAtBottom;
else if(dataRect.top() > cellRect.top())
hint = QAbstractItemView::PositionAtTop;
else
return;
}
int firstRow = qMax(verticalHeader()->visualIndexAt(0), 0);
int lastRow = verticalHeader()->visualIndexAt(viewport()->height());
if(lastRow == -1)
lastRow = verticalHeader()->count();
int visibleRows = lastRow - firstRow + 1;
// a partially displayed row doesn't count
if(verticalHeader()->sectionViewportPosition(lastRow) + verticalHeader()->sectionSize(lastRow) >
viewport()->height())
visibleRows--;
if(hint == QAbstractItemView::PositionAtTop)
{
verticalScrollBar()->setValue(index.row());
}
else if(hint == QAbstractItemView::PositionAtBottom)
{
verticalScrollBar()->setValue(index.row() - visibleRows + 1);
}
else if(hint == QAbstractItemView::PositionAtCenter)
{
verticalScrollBar()->setValue(index.row() - (visibleRows + 1) / 2);
}
update(index);
}
void RDTableView::updateGeometries()
{
static bool recurse = false;
if(recurse)
return;
recurse = true;
QAbstractButton *cornerButton = findChild<QAbstractButton *>();
cornerButton->setVisible(false);
QRect geom = viewport()->geometry();
// assume no vertical header
int horizHeight =
qBound(horizontalHeader()->minimumHeight(), horizontalHeader()->sizeHint().height(),
horizontalHeader()->maximumHeight());
setViewportMargins(0, horizHeight, 0, 0);
horizontalHeader()->setGeometry(geom.left(), geom.top() - horizHeight, geom.width(), horizHeight);
// even though it's not visible we need to set the geometry right so that it looks up rows by
// position properly.
verticalHeader()->setGeometry(0, horizHeight, 0, geom.height());
// if the headers are hidden nothing else will update their geometries and some things like
// scrolling etc depend on it being up to date, so hackily call the protected slot. Yuk!
if(verticalHeader()->isHidden())
QMetaObject::invokeMethod(verticalHeader(), "updateGeometries");
if(horizontalHeader()->isHidden())
QMetaObject::invokeMethod(horizontalHeader(), "updateGeometries");
// assume per-item vertical scrolling and per-pixel horizontal scrolling
// vertical scroll bar
{
int firstRow = qMax(verticalHeader()->visualIndexAt(0), 0);
int lastRow = verticalHeader()->visualIndexAt(viewport()->height());
bool last = false;
if(lastRow == -1)
{
last = true;
lastRow = verticalHeader()->count();
}
int visibleRows = lastRow - firstRow + 1;
// a partially displayed row doesn't count
if(verticalHeader()->sectionViewportPosition(lastRow) + verticalHeader()->sectionSize(lastRow) >
viewport()->height())
visibleRows--;
verticalScrollBar()->setRange(0, verticalHeader()->count() - visibleRows);
verticalScrollBar()->setSingleStep(1);
verticalScrollBar()->setPageStep(visibleRows);
if(visibleRows >= verticalHeader()->count())
verticalHeader()->setOffset(0);
else if(last)
verticalHeader()->setOffsetToLastSection();
}
// horizontal scroll bar
{
int totalWidth = horizontalHeader()->sizeHint().width();
horizontalScrollBar()->setPageStep(viewport()->width() - horizontalHeader()->pinnedWidth());
horizontalScrollBar()->setRange(0, totalWidth - viewport()->width());
horizontalScrollBar()->setSingleStep(qMax(totalWidth / (horizontalHeader()->count() + 1), 2));
}
recurse = false;
QAbstractItemView::updateGeometries();
}
void RDTableView::scrollContentsBy(int dx, int dy)
{
QTableView::scrollContentsBy(dx, dy);
viewport()->update();
}
+34
View File
@@ -25,6 +25,7 @@
#pragma once
#include <QTableView>
#include "RDHeaderView.h"
class RDTableView : public QTableView
{
@@ -32,5 +33,38 @@ class RDTableView : public QTableView
public:
explicit RDTableView(QWidget *parent = 0);
// these aren't virtual so we can't override them properly, but it's convenient for internal use
// and any external calls that go to this type directly to use the correct version
RDHeaderView *horizontalHeader() const { return m_horizontalHeader; }
int columnViewportPosition(int column) const;
int columnAt(int x) const;
int columnWidth(int column) const;
void setColumnWidth(int column, int width);
void setColumnWidths(const QList<int> &widths);
void resizeColumnsToContents();
// these ones we CAN override, so even though the implementation is identical to QTableView we
// reimplement so it can pick up the above functions
QRect visualRect(const QModelIndex &index) const override;
QRegion visualRegionForSelection(const QItemSelection &selection) const override;
QModelIndex indexAt(const QPoint &p) const override;
void scrollTo(const QModelIndex &index, ScrollHint hint = QAbstractItemView::EnsureVisible) override;
void setColumnGroupRole(int role);
int columnGroupRole() const { return m_columnGroupRole; }
QStyleOptionViewItem viewOptions() const override { return QTableView::viewOptions(); }
void setPinnedColumns(int numColumns);
int pinnedColumns() const { return m_pinnedColumns; }
protected:
void paintEvent(QPaintEvent *e) override;
void updateGeometries() override;
void scrollContentsBy(int dx, int dy) override;
void paintCell(QPainter *painter, const QModelIndex &index, const QStyleOptionViewItem &opt);
private:
int m_pinnedColumns = 0;
int m_columnGroupRole = 0;
RDHeaderView *m_horizontalHeader;
};
+46 -18
View File
@@ -357,6 +357,8 @@ uint32_t CalcIndex(BufferData *data, uint32_t vertID, int32_t baseVertex)
return idx;
}
static int columnGroupRole = Qt::UserRole + 10000;
class BufferItemModel : public QAbstractItemModel
{
public:
@@ -396,26 +398,29 @@ public:
QVariant headerData(int section, Qt::Orientation orientation, int role) const override
{
if(section < m_ColumnCount && orientation == Qt::Horizontal && role == Qt::DisplayRole)
if(section < m_ColumnCount && orientation == Qt::Horizontal)
{
if(section == 0)
if(role == Qt::DisplayRole || role == columnGroupRole)
{
return meshView ? lit("VTX") : lit("Element");
}
else if(section == 1 && meshView)
{
return lit("IDX");
}
else
{
const FormatElement &el = elementForColumn(section);
if(section == 0)
{
return meshView ? lit("VTX") : lit("Element");
}
else if(section == 1 && meshView)
{
return lit("IDX");
}
else
{
const FormatElement &el = elementForColumn(section);
if(el.format.compCount == 1)
return el.name;
if(el.format.compCount == 1 || role == columnGroupRole)
return el.name;
QChar comps[] = {QLatin1Char('x'), QLatin1Char('y'), QLatin1Char('z'), QLatin1Char('w')};
QChar comps[] = {QLatin1Char('x'), QLatin1Char('y'), QLatin1Char('z'), QLatin1Char('w')};
return QFormatStr("%1.%2").arg(el.name).arg(comps[componentForIndex(section)]);
return QFormatStr("%1.%2").arg(el.name).arg(comps[componentForIndex(section)]);
}
}
}
@@ -448,6 +453,14 @@ public:
uint32_t row = index.row();
int col = index.column();
if(role == columnGroupRole)
{
if(col < reservedColumnCount())
return -1 - col;
else
return columnLookup[col - reservedColumnCount()];
}
if((role == Qt::BackgroundRole || role == Qt::ForegroundRole) && col >= reservedColumnCount())
{
if(meshView)
@@ -1088,6 +1101,9 @@ void BufferViewer::SetupRawView()
ui->dockarea->addToolWindow(ui->vsinData, ToolWindowManager::EmptySpace);
ui->dockarea->setToolWindowProperties(ui->vsinData, ToolWindowManager::HideCloseButton);
ui->vsinData->setPinnedColumns(1);
ui->vsinData->setColumnGroupRole(columnGroupRole);
ui->formatSpecifier->setWindowTitle(tr("Buffer Format"));
ui->dockarea->addToolWindow(ui->formatSpecifier, ToolWindowManager::AreaReference(
ToolWindowManager::BottomOf,
@@ -1191,6 +1207,14 @@ void BufferViewer::SetupMeshView()
ui->vsoutData->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu);
ui->gsoutData->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu);
ui->vsinData->setPinnedColumns(2);
ui->vsoutData->setPinnedColumns(2);
ui->gsoutData->setPinnedColumns(2);
ui->vsinData->setColumnGroupRole(columnGroupRole);
ui->vsoutData->setColumnGroupRole(columnGroupRole);
ui->gsoutData->setColumnGroupRole(columnGroupRole);
QObject::connect(ui->vsinData->horizontalHeader(), &QHeaderView::customContextMenuRequested,
[this](const QPoint &pos) { meshHeaderMenu(MeshDataStage::VSIn, pos); });
QObject::connect(ui->vsoutData->horizontalHeader(), &QHeaderView::customContextMenuRequested,
@@ -2307,17 +2331,21 @@ void BufferViewer::ApplyRowAndColumnDims(int numColumns, RDTableView *view)
{
int start = 0;
QList<int> widths;
// vertex/element
view->setColumnWidth(start++, m_IdxColWidth);
widths << m_IdxColWidth;
// mesh view only - index
if(m_MeshView)
view->setColumnWidth(start++, m_IdxColWidth);
widths << m_IdxColWidth;
for(int i = start; i < numColumns; i++)
view->setColumnWidth(i, m_DataColWidth);
widths << m_DataColWidth;
view->verticalHeader()->setDefaultSectionSize(m_DataRowHeight);
view->setColumnWidths(widths);
}
void BufferViewer::UpdateMeshConfig()