diff --git a/qrenderdoc/Widgets/Extended/RDHeaderView.cpp b/qrenderdoc/Widgets/Extended/RDHeaderView.cpp index 8656285a3..df2e7f016 100644 --- a/qrenderdoc/Widgets/Extended/RDHeaderView.cpp +++ b/qrenderdoc/Widgets/Extended/RDHeaderView.cpp @@ -28,6 +28,20 @@ #include #include +///////////////////////////////////////////////////////////////////////////////// +// +// 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()) + sz = sz.expandedTo(v.value()); + } + + int oldSize = m_sections[col].size; + + m_sections[col].size = sz.width(); + + emit sectionResized(col, oldSize, sz.width()); + } +} + +void RDHeaderView::resizeSections(const QList &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::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()) + 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 ¤t, 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); + } + } +} diff --git a/qrenderdoc/Widgets/Extended/RDHeaderView.h b/qrenderdoc/Widgets/Extended/RDHeaderView.h index 9f5a095d3..dc490edca 100644 --- a/qrenderdoc/Widgets/Extended/RDHeaderView.h +++ b/qrenderdoc/Widgets/Extended/RDHeaderView.h @@ -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 &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 ¤t, const QModelIndex &old) override; + + enum ResizeType + { + NoResize, + LeftResize, + RightResize + }; + + QPair checkResizing(QMouseEvent *event); + QPair 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 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()); +} \ No newline at end of file diff --git a/qrenderdoc/Widgets/Extended/RDTableView.cpp b/qrenderdoc/Widgets/Extended/RDTableView.cpp index 368db9319..9aef7564a 100644 --- a/qrenderdoc/Widgets/Extended/RDTableView.cpp +++ b/qrenderdoc/Widgets/Extended/RDTableView.cpp @@ -23,8 +23,397 @@ ******************************************************************************/ #include "RDTableView.h" +#include +#include #include +#include +#include +#include +#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 &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(); + 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(); } diff --git a/qrenderdoc/Widgets/Extended/RDTableView.h b/qrenderdoc/Widgets/Extended/RDTableView.h index 9720c734d..c3faaec79 100644 --- a/qrenderdoc/Widgets/Extended/RDTableView.h +++ b/qrenderdoc/Widgets/Extended/RDTableView.h @@ -25,6 +25,7 @@ #pragma once #include +#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 &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; }; diff --git a/qrenderdoc/Windows/BufferViewer.cpp b/qrenderdoc/Windows/BufferViewer.cpp index 1dcdf7587..085c73007 100644 --- a/qrenderdoc/Windows/BufferViewer.cpp +++ b/qrenderdoc/Windows/BufferViewer.cpp @@ -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 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()