Files
renderdoc/qrenderdoc/Widgets/Extended/RDTreeWidget.cpp
T

837 lines
21 KiB
C++

/******************************************************************************
* The MIT License (MIT)
*
* Copyright (c) 2016-2017 Baldur Karlsson
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
******************************************************************************/
#include "RDTreeWidget.h"
#include <QApplication>
#include <QClipboard>
#include <QColor>
#include <QDebug>
#include <QMouseEvent>
#include <QPainter>
#include <QPen>
#include <QStack>
#include <QToolTip>
#include "Code/Interface/QRDInterface.h"
class RDTreeWidgetModel : public QAbstractItemModel
{
public:
RDTreeWidgetModel(RDTreeWidget *parent) : QAbstractItemModel(parent), widget(parent) {}
QModelIndex indexForItem(RDTreeWidgetItem *item, int column) const
{
if(item->m_parent == NULL)
return QModelIndex();
int row = item->m_parent->indexOfChild(item);
return createIndex(row, column, item);
}
RDTreeWidgetItem *itemForIndex(QModelIndex idx) const
{
if(!idx.isValid())
return widget->m_root;
return (RDTreeWidgetItem *)idx.internalPointer();
}
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override
{
if(row < 0 || column < 0 || row >= rowCount(parent) || column >= columnCount(parent))
return QModelIndex();
RDTreeWidgetItem *par = itemForIndex(parent);
if(par == NULL)
par = widget->m_root;
return createIndex(row, column, par->m_children[row]);
}
void beginAddChild(RDTreeWidgetItem *item)
{
QModelIndex index = indexForItem(item, 0);
beginInsertRows(index, item->childCount(), item->childCount());
}
void endAddChild(RDTreeWidgetItem *item) { endInsertRows(); }
void beginRemoveChildren(RDTreeWidgetItem *parent, int first, int last)
{
beginRemoveRows(indexForItem(parent, 0), first, last);
}
void endRemoveChildren() { endRemoveRows(); }
void itemChanged(RDTreeWidgetItem *item, const QVector<int> &roles)
{
QModelIndex topLeft = indexForItem(item, 0);
QModelIndex bottomRight = indexForItem(item, columnCount() - 1);
emit dataChanged(topLeft, bottomRight, roles);
}
void refresh()
{
emit beginResetModel();
emit endResetModel();
}
void headerRefresh() { emit headerDataChanged(Qt::Horizontal, 0, columnCount() - 1); }
void itemsChanged(RDTreeWidgetItem *p, QPair<int, int> minRowColumn, QPair<int, int> maxRowColumn,
const QVector<int> &roles)
{
QModelIndex topLeft = createIndex(minRowColumn.first, minRowColumn.second, p);
QModelIndex bottomRight = createIndex(maxRowColumn.first, maxRowColumn.second, p);
emit dataChanged(topLeft, bottomRight, roles);
}
QModelIndex parent(const QModelIndex &index) const override
{
if(index.internalPointer() == NULL)
return QModelIndex();
RDTreeWidgetItem *item = itemForIndex(index);
if(item)
return indexForItem(item->m_parent, 0);
return QModelIndex();
}
int rowCount(const QModelIndex &parent = QModelIndex()) const override
{
if(!parent.isValid())
return widget->m_root->childCount();
RDTreeWidgetItem *parentItem = itemForIndex(parent);
if(parentItem)
return parentItem->childCount();
return 0;
}
int columnCount(const QModelIndex &parent = QModelIndex()) const override
{
return widget->m_headers.count();
}
bool hasChildren(const QModelIndex &parent) const override
{
if(!parent.isValid())
return widget->m_root->childCount() > 0;
RDTreeWidgetItem *parentItem = itemForIndex(parent);
if(parentItem)
return parentItem->childCount() > 0;
return false;
}
Qt::ItemFlags flags(const QModelIndex &index) const override
{
if(!index.isValid())
return 0;
return QAbstractItemModel::flags(index);
}
QVariant headerData(int section, Qt::Orientation orientation, int role) const override
{
if(orientation == Qt::Horizontal && role == Qt::DisplayRole && section < widget->m_headers.count())
return widget->m_headers[section];
return QVariant();
}
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override
{
RDTreeWidgetItem *item = itemForIndex(index);
// invisible root element has no data
if(!item->m_parent)
return QVariant();
if(role == Qt::DisplayRole)
{
if(index.column() < item->m_text.count())
return item->m_text[index.column()];
}
else if(role == Qt::DecorationRole)
{
if(widget->m_hoverColumn == index.column())
{
if(widget->m_currentHoverItem == item)
return widget->m_activeHoverIcon;
else
return widget->m_normalHoverIcon;
}
if(index.column() < item->m_icons.count())
return item->m_icons[index.column()];
}
else if(role == Qt::BackgroundRole)
{
// item's background color takes priority
if(item->m_back != QBrush())
return item->m_back;
// otherwise if we're hover-highlighting, use the highlight color at 20% opacity
if(widget->m_currentHoverItem == item)
{
QColor col = widget->palette().color(QPalette::Highlight);
col.setAlphaF(0.2f);
return QBrush(col);
}
// otherwise, no special background
return QVariant();
}
else if(role == Qt::ForegroundRole)
{
// same priority order as background role above
if(item->m_fore != QBrush())
return item->m_fore;
return QVariant();
}
else if(role == Qt::ToolTipRole && !widget->m_instantTooltips)
{
return item->m_tooltip;
}
else if(role == Qt::FontRole)
{
QFont font; // TODO should this come from some default?
font.setItalic(item->m_italic);
font.setBold(item->m_bold);
return font;
}
else if(role < 64 && item->m_customData & 1ULL << role)
{
return item->data(index.column(), role);
}
return QVariant();
}
private:
RDTreeWidget *widget;
};
RDTreeWidgetItem::RDTreeWidgetItem(const QVariantList &values)
{
m_text.reserve(values.size());
for(const QVariant &v : values)
m_text.push_back(v);
m_icons.resize(m_text.size());
}
RDTreeWidgetItem::RDTreeWidgetItem(const std::initializer_list<QVariant> &values)
{
m_text = values;
m_icons.resize(m_text.size());
}
RDTreeWidgetItem::~RDTreeWidgetItem()
{
if(m_parent)
m_parent->removeChild(this);
clear();
delete m_data;
}
QVariant RDTreeWidgetItem::data(int column, int role) const
{
if(column >= m_data->count())
return QVariant();
const QVector<RoleData> &dataVec = (*m_data)[column];
for(const RoleData &d : dataVec)
{
if(d.role == role)
return d.data;
}
return QVariant();
}
void RDTreeWidgetItem::setData(int column, int role, const QVariant &value)
{
// we lazy allocate
if(!m_data)
{
m_data = new QVector<QVector<RoleData>>;
m_data->resize(qMax(m_text.count(), column + 1));
}
if(role < Qt::UserRole)
{
Q_ASSERT(role < 64);
m_customData |= 1ULL << role;
}
// data is allowed to resize above the column count in the widget
if(m_data->count() <= column)
m_data->resize(column + 1);
QVector<RoleData> &dataVec = (*m_data)[column];
for(RoleData &d : dataVec)
{
if(d.role == role)
{
bool different = (d.data != value);
d.data = value;
if(different && role < Qt::UserRole)
m_widget->m_model->itemChanged(this, {role});
return;
}
}
dataVec.push_back(RoleData(role, value));
if(role < Qt::UserRole)
m_widget->m_model->itemChanged(this, {role});
}
void RDTreeWidgetItem::addChild(RDTreeWidgetItem *item)
{
int colCount = item->m_text.count();
// remove it from any previous parent
if(item->m_parent)
item->m_parent->removeChild(this);
// set up its new parent to us
item->m_parent = this;
// set the widget in case this changed
item->setWidget(m_widget);
// resize per-column vectors to column count
item->m_text.resize(colCount);
item->m_icons.resize(colCount);
// data can resize up, but we don't resize it down.
if(item->m_data)
item->m_data->resize(qMax(item->m_data->count(), colCount));
if(m_widget)
m_widget->m_model->beginAddChild(this);
// add to our list of children
m_children.push_back(item);
if(m_widget)
m_widget->m_model->endAddChild(this);
}
void RDTreeWidgetItem::setWidget(RDTreeWidget *widget)
{
if(widget == m_widget)
return;
// if the widget is different, we need to recurse to children
m_widget = widget;
for(RDTreeWidgetItem *item : m_children)
item->setWidget(widget);
}
void RDTreeWidgetItem::dataChanged(int role)
{
if(m_widget)
m_widget->itemDataChanged(this, role);
}
RDTreeWidgetItem *RDTreeWidgetItem::takeChild(int index)
{
if(m_widget)
m_widget->m_model->beginRemoveChildren(this, index, index);
m_children[index]->m_parent = NULL;
RDTreeWidgetItem *ret = m_children.takeAt(index);
if(m_widget)
m_widget->m_model->endRemoveChildren();
return ret;
}
void RDTreeWidgetItem::removeChild(RDTreeWidgetItem *child)
{
if(m_widget)
{
int row = m_children.indexOf(child);
m_widget->m_model->beginRemoveChildren(this, row, row);
}
m_children.removeOne(child);
if(m_widget)
m_widget->m_model->endRemoveChildren();
}
void RDTreeWidgetItem::clear()
{
if(!childCount())
return;
if(m_widget)
m_widget->m_model->beginRemoveChildren(this, 0, childCount() - 1);
while(childCount() > 0)
{
RDTreeWidgetItem *child = takeChild(0);
child->clear();
delete child;
}
if(m_widget)
m_widget->m_model->endRemoveChildren();
}
RDTreeWidget::RDTreeWidget(QWidget *parent) : RDTreeView(parent)
{
setMouseTracking(true);
m_root = new RDTreeWidgetItem;
m_root->m_widget = this;
m_model = new RDTreeWidgetModel(this);
RDTreeView::setModel(m_model);
QObject::connect(this, &RDTreeWidget::activated, [this](const QModelIndex &idx) {
emit itemActivated(m_model->itemForIndex(idx), idx.column());
});
QObject::connect(this, &RDTreeWidget::clicked, [this](const QModelIndex &idx) {
emit itemClicked(m_model->itemForIndex(idx), idx.column());
});
QObject::connect(this, &RDTreeWidget::doubleClicked, [this](const QModelIndex &idx) {
emit itemDoubleClicked(m_model->itemForIndex(idx), idx.column());
});
}
RDTreeWidget::~RDTreeWidget()
{
delete m_root;
delete m_model;
}
void RDTreeWidget::beginUpdate()
{
m_queueUpdates = true;
m_queuedItem = NULL;
m_lowestIndex = m_highestIndex = qMakePair<int, int>(-1, -1);
m_queuedRoles = 0;
}
void RDTreeWidget::endUpdate()
{
m_queueUpdates = false;
if(m_queuedRoles)
{
// if we updated multiple different trees we can't issue a single dataChanged for everything
// under a parent. Refresh the whole model.
if(m_queuedItem == NULL)
{
m_model->refresh();
}
else
{
QVector<int> roles;
for(int r = 0; r < 64; r++)
{
if(m_queuedRoles & (1ULL << r))
roles.push_back(r);
}
m_model->itemsChanged(m_queuedItem, m_lowestIndex, m_highestIndex, roles);
}
}
}
void RDTreeWidget::setColumns(const QStringList &columns)
{
m_headers = columns;
m_model->refresh();
}
void RDTreeWidget::setHeaderText(int column, const QString &text)
{
m_headers[column] = text;
m_model->headerRefresh();
}
RDTreeWidgetItem *RDTreeWidget::selectedItem() const
{
QModelIndexList sel = selectionModel()->selectedIndexes();
if(sel.isEmpty())
return NULL;
return m_model->itemForIndex(sel[0]);
}
RDTreeWidgetItem *RDTreeWidget::currentItem() const
{
return m_model->itemForIndex(currentIndex());
}
void RDTreeWidget::setSelectedItem(RDTreeWidgetItem *node)
{
selectionModel()->select(m_model->indexForItem(node, 0),
QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
}
void RDTreeWidget::setCurrentItem(RDTreeWidgetItem *node)
{
setCurrentIndex(m_model->indexForItem(node, 0));
}
RDTreeWidgetItem *RDTreeWidget::itemAt(const QPoint &p) const
{
return m_model->itemForIndex(indexAt(p));
}
void RDTreeWidget::expandItem(RDTreeWidgetItem *item)
{
expand(m_model->indexForItem(item, 0));
}
void RDTreeWidget::expandAllItems(RDTreeWidgetItem *item)
{
expandItem(item);
for(int c = 0; c < item->childCount(); c++)
{
RDTreeWidgetItem *child = item->child(c);
expandAllItems(child);
}
}
void RDTreeWidget::collapseItem(RDTreeWidgetItem *item)
{
collapse(m_model->indexForItem(item, 0));
}
void RDTreeWidget::collapseAllItems(RDTreeWidgetItem *item)
{
collapseItem(item);
for(int c = 0; c < item->childCount(); c++)
{
RDTreeWidgetItem *child = item->child(c);
collapseAllItems(child);
}
}
void RDTreeWidget::scrollToItem(RDTreeWidgetItem *node)
{
scrollTo(m_model->indexForItem(node, 0));
}
void RDTreeWidget::clear()
{
m_root->clear();
}
void RDTreeWidget::mouseMoveEvent(QMouseEvent *e)
{
QModelIndex idx = indexAt(e->pos());
RDTreeWidgetItem *item = m_model->itemForIndex(idx);
if(idx.column() == m_hoverColumn && m_hoverHandCursor)
setCursor(QCursor(Qt::PointingHandCursor));
else
unsetCursor();
if(m_currentHoverItem == item || m_hoverColumn < 0)
return;
RDTreeWidgetItem *old = m_currentHoverItem;
m_currentHoverItem = item;
// it's only two items, don't try and make a range but just change them both
QVector<int> roles = {Qt::DecorationRole, Qt::BackgroundRole, Qt::ForegroundRole};
if(old)
m_model->itemChanged(old, roles);
m_model->itemChanged(item, roles);
if(m_instantTooltips)
{
QToolTip::hideText();
if(m_currentHoverItem != NULL && !m_currentHoverItem->m_tooltip.isEmpty())
{
// the documentation says:
//
// "If text is empty the tool tip is hidden. If the text is the same as the currently shown
// tooltip, the tip will not move. You can force moving by first hiding the tip with an empty
// text, and then showing the new tip at the new position."
//
// However the actual implementation has some kind of 'fading' check, so if you hide then
// immediately show, it will try to reuse the tooltip and end up not moving it at all if the
// text hasn't changed.
QToolTip::showText(QCursor::pos(), lit(" "), this);
QToolTip::showText(QCursor::pos(), m_currentHoverItem->m_tooltip, this);
}
}
emit mouseMove(e);
RDTreeView::mouseMoveEvent(e);
}
void RDTreeWidget::mouseReleaseEvent(QMouseEvent *e)
{
QModelIndex idx = indexAt(e->pos());
if(idx.isValid() && idx.column() == m_hoverColumn && m_activateOnClick)
{
emit itemActivated(itemAt(e->pos()), idx.column());
}
RDTreeView::mouseReleaseEvent(e);
}
void RDTreeWidget::leaveEvent(QEvent *e)
{
unsetCursor();
if(m_currentHoverItem)
{
RDTreeWidgetItem *item = m_currentHoverItem;
if(!item->m_tooltip.isEmpty() && m_instantTooltips)
QToolTip::hideText();
m_currentHoverItem = NULL;
m_model->itemChanged(item, {Qt::DecorationRole, Qt::BackgroundRole, Qt::ForegroundRole});
}
emit leave(e);
RDTreeView::leaveEvent(e);
}
void RDTreeWidget::focusOutEvent(QFocusEvent *event)
{
if(m_clearSelectionOnFocusLoss)
clearSelection();
RDTreeView::focusOutEvent(event);
}
void RDTreeWidget::keyPressEvent(QKeyEvent *e)
{
if(!m_customCopyPaste && e->matches(QKeySequence::Copy))
{
QModelIndexList sel = selectionModel()->selectedRows();
int stackWidths[16];
int *heapWidths = NULL;
int colCount = m_model->columnCount();
if(colCount >= 16)
heapWidths = new int[colCount];
int *widths = heapWidths ? heapWidths : stackWidths;
for(int i = 0; i < colCount; i++)
widths[i] = 0;
// align the copied data so that each column is the same width
for(QModelIndex idx : sel)
{
RDTreeWidgetItem *item = m_model->itemForIndex(idx);
for(int i = 0; i < qMin(colCount, item->m_text.count()); i++)
widths[i] = qMax(widths[i], item->m_text[i].toString().count());
}
// only align up to 50 characters so one really long item doesn't mess up the whole thing
for(int i = 0; i < colCount; i++)
widths[i] = qMin(50, widths[i]);
QString clipData;
for(QModelIndex idx : sel)
{
RDTreeWidgetItem *item = m_model->itemForIndex(idx);
for(int i = 0; i < qMin(colCount, item->m_text.count()); i++)
{
QString format = i == 0 ? QFormatStr("%1") : QFormatStr(" %1");
clipData += format.arg(item->m_text[i].toString(), -widths[i]);
}
clipData += lit("\n");
}
QClipboard *clipboard = QApplication::clipboard();
clipboard->setText(clipData.trimmed());
delete[] heapWidths;
}
else
{
RDTreeView::keyPressEvent(e);
}
emit(keyPress(e));
}
void RDTreeWidget::drawBranches(QPainter *painter, const QRect &rect, const QModelIndex &index) const
{
// we do our own custom branch rendering to ensure the backgrounds for the +/- markers are filled
// (as otherwise they don't show up well over selection or background fills) as well as to draw
// any vertical branch colors.
painter->save();
// start at the left-most side of the rect
QRect branchRect(rect.left(), rect.top(), indentation(), rect.height());
RDTreeWidgetItem *item = m_model->itemForIndex(index);
// first draw the coloured lines - we're only interested in parents for this, so push all the
// parents onto a stack
QStack<RDTreeWidgetItem *> parents;
RDTreeWidgetItem *parent = item->parent();
while(parent && parent != m_root)
{
parents.push(parent);
parent = parent->parent();
}
// fill in the background behind the lines for the whole row, since by default it doesn't show up
// behind the tree lines.
QRect allLinesRect(rect.left(), rect.top(), (parents.count() + 1) * indentation(), rect.height());
if(!selectionModel()->isSelected(index) && item->m_back != QBrush())
{
painter->fillRect(allLinesRect, item->m_back);
}
// we now iterate from the top-most parent down, moving in from the left
QPen oldPen = painter->pen();
while(!parents.isEmpty())
{
parent = parents.pop();
if(parent->m_treeCol.isValid())
{
// draw a centred pen vertically down the middle of branchRect
painter->setPen(QPen(QBrush(parent->m_treeCol), parent->m_treeColWidth));
QPointF topCentre = QRectF(branchRect).center();
QPointF bottomCentre = topCentre;
topCentre.setY(branchRect.top());
bottomCentre.setY(branchRect.bottom());
painter->drawLine(topCentre, bottomCentre);
}
branchRect.moveLeft(branchRect.left() + indentation());
}
painter->setPen(oldPen);
// branchRect is now over the box/lines for the current item.
// draw a rect of QPalette Base color behind the branch indicator if we have children, since by
// default there might not be one and the indicator won't always show up well over the background
// color
/*
if(item->childCount() > 0)
{
// TODO find a portable rect
const int radius = 9 / 2;
QPoint topleft = branchRect.center();
topleft.setX(topleft.x() - radius + 1);
topleft.setY(topleft.y() - radius + 1);
painter->fillRect(QRect(topleft, QSize(radius * 2, radius * 2)),
palette().brush(QPalette::Base));
}
*/
painter->restore();
RDTreeView::drawBranches(painter, rect, index);
}
void RDTreeWidget::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
{
emit itemSelectionChanged();
RDTreeView::selectionChanged(selected, deselected);
}
void RDTreeWidget::currentChanged(const QModelIndex &current, const QModelIndex &previous)
{
emit currentItemChanged(m_model->itemForIndex(current), m_model->itemForIndex(previous));
RDTreeView::currentChanged(current, previous);
}
void RDTreeWidget::itemDataChanged(RDTreeWidgetItem *item, int role)
{
if(m_queueUpdates)
{
m_queuedRoles |= (1ULL << role);
// for now we only support updating the whole row, with all columns, even if only one column
// changed.
int row = item->m_parent->indexOfChild(item);
// no queued updates yet, set up this one
if(m_lowestIndex.first == -1)
{
m_queuedItem = item;
m_lowestIndex = qMakePair<int, int>(row, 0);
m_highestIndex = qMakePair<int, int>(m_lowestIndex.first, m_headers.count() - 1);
}
else
{
// there's already an update. Check if we can expand it
if(m_queuedItem == item)
{
m_lowestIndex.first = qMin(m_lowestIndex.first, row);
m_highestIndex.first = qMax(m_highestIndex.first, row);
}
else
{
// can't batch updates across multiple parents, so we just fallback to full model refresh
m_queuedItem = NULL;
}
}
}
else
{
m_model->itemChanged(item, {role});
}
}