/****************************************************************************** * 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 #include #include #include #include #include #include #include #include #include #include "Code/Interface/QRDInterface.h" #include "Code/QRDUtils.h" #include "Code/Resources.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 &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 minRowColumn, QPair maxRowColumn, const QVector &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) | Qt::ItemIsUserCheckable; } 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 || index.column() >= item->m_text.count()) return QVariant(); if(role == Qt::DisplayRole) { return item->m_text[index.column()]; } else if(role == Qt::DecorationRole) { if(widget->m_hoverColumn == index.column()) { if(itemForIndex(widget->m_currentHoverIndex) == item) return widget->m_activeHoverIcon; else return widget->m_normalHoverIcon; } return item->m_icons[index.column()]; } else if(role == Qt::BackgroundRole) { // item's background color takes priority but only if not selected if(item->m_back != QBrush() && !widget->selectionModel()->isSelected(index)) return item->m_back; // otherwise if we're hover-highlighting, use the highlight color at 20% opacity if(itemForIndex(widget->m_currentHoverIndex) == item && widget->m_hoverColumn >= 0) { 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 == Qt::TextAlignmentRole) { if(index.column() < widget->m_alignments.count()) { Qt::Alignment align = widget->m_alignments[index.column()]; if(align != 0) return QVariant(align); } } else if(role < 64 && item->m_customData & 1ULL << role) { return item->data(index.column(), role); } else if(role >= Qt::UserRole) { return item->data(index.column(), role); } return QVariant(); } bool setData(const QModelIndex &index, const QVariant &value, int role) override { RDTreeWidgetItem *item = itemForIndex(index); // invisible root element has no data if(!item->m_parent) return false; bool ret = false; if(role == Qt::DisplayRole) { if(index.column() < item->m_text.count()) { item->m_text[index.column()] = value; ret = true; } } else if(role == Qt::DecorationRole) { if(index.column() < item->m_icons.count()) { item->m_icons[index.column()] = value.value(); ret = true; } } else if(role == Qt::BackgroundRole) { item->m_back = value.value(); ret = true; } else if(role == Qt::ForegroundRole) { item->m_fore = value.value(); ret = true; } else if(role == Qt::ToolTipRole && !widget->m_instantTooltips) { item->m_tooltip = value.toString(); ret = true; } else if(role == Qt::FontRole) { ret = false; } else { item->setData(index.column(), role, value); ret = true; } if(ret) widget->itemDataChanged(item, index.column(), role); return ret; } 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()); for(int i = 0; i < m_text.count(); i++) RichResourceTextInitialise(m_text[i]); } RDTreeWidgetItem::RDTreeWidgetItem(const std::initializer_list &values) { m_text = values; m_icons.resize(m_text.size()); for(int i = 0; i < m_text.count(); i++) RichResourceTextInitialise(m_text[i]); } void RDTreeWidgetItem::checkForResourceId(int col) { RichResourceTextInitialise(m_text[col]); } RDTreeWidgetItem::~RDTreeWidgetItem() { if(m_parent) m_parent->removeChild(this); clear(); delete m_data; } QVariant RDTreeWidgetItem::data(int column, int role) const { if(m_data == NULL || column >= m_data->count()) return QVariant(); const QVector &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>; 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 &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(m_widget && role < Qt::UserRole) m_widget->m_model->itemChanged(this, {role}); } void RDTreeWidgetItem::addChild(RDTreeWidgetItem *item) { int colCount = item->m_text.count(); if(m_widget && colCount < m_widget->m_headers.count()) qCritical() << "Item added with insufficient column data"; // 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->beginAddChild(this); // add to our list of children m_children.push_back(item); if(m_widget) m_widget->endAddChild(this); } void RDTreeWidgetItem::setWidget(RDTreeWidget *widget) { if(widget == m_widget) return; QWidget *parent = widget; while(parent) parent = parent->parentWidget(); // 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 column, int role) { if(m_widget) m_widget->itemDataChanged(this, column, role); } RDTreeWidgetItem *RDTreeWidgetItem::takeChild(int index) { if(m_widget && !m_widget->m_clearing) 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_clearing) 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_clearing) 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_clearing) m_widget->m_model->endRemoveChildren(); } RDTreeWidgetItemIterator::RDTreeWidgetItemIterator(RDTreeWidget *widget) { if(widget->topLevelItemCount() == 0) m_Current = NULL; else m_Current = widget->topLevelItem(0); } RDTreeWidgetItemIterator &RDTreeWidgetItemIterator::operator++() { // depth first if(m_Current->childCount() > 0) { m_Current = m_Current->child(0); return *this; } // otherwise check if we have siblings, recursively up RDTreeWidgetItem *parent = m_Current->parent(); RDTreeWidgetItem *child = m_Current; while(parent) { // if there's a sibling at this level, move to it int idx = parent->indexOfChild(child); if(idx + 1 < parent->childCount()) { m_Current = parent->child(idx + 1); return *this; } // if there are no more siblings at this level, move up child = parent; parent = parent->parent(); // if we just exhausted siblings at the top-level, parent will now be NULL, so we abort. } // no more siblings, stop. m_Current = NULL; return *this; } RDTreeWidgetDelegate::RDTreeWidgetDelegate(RDTreeWidget *parent) : m_widget(parent), ForwardingDelegate(parent) { } RDTreeWidgetDelegate::~RDTreeWidgetDelegate() { } void RDTreeWidgetDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { RDTreeWidgetModel *model = m_widget->m_model; if(index.isValid() && model == index.model()) { RDTreeWidgetItem *item = model->itemForIndex(index); if(index.column() < item->m_text.count()) { QVariant v = item->m_text[index.column()]; if(RichResourceTextCheck(v)) { // draw the item without text, so we get the proper background/selection/etc. // we'd like to be able to use ForwardingDelegate::paint here, but either it calls to // QStyledItemDelegate which will re-fetch the text (bleh), or it calls to the manual // delegate which could do anything. So for this case we just use the style and skip the // delegate and hope it works out. QStyleOptionViewItem opt = option; QStyledItemDelegate::initStyleOption(&opt, index); opt.text.clear(); m_widget->style()->drawControl(QStyle::CE_ItemViewItem, &opt, painter, m_widget); painter->save(); RichResourceTextPaint(m_widget, painter, option.rect, option.font, option.palette, option.state & QStyle::State_MouseOver, m_widget->viewport()->mapFromGlobal(QCursor::pos()), v); painter->restore(); return; } } } return ForwardingDelegate::paint(painter, option, index); } QSize RDTreeWidgetDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { RDTreeWidgetModel *model = m_widget->m_model; if(index.isValid() && model == index.model()) { RDTreeWidgetItem *item = model->itemForIndex(index); if(index.column() < item->m_text.count()) { QVariant v = item->m_text[index.column()]; if(RichResourceTextCheck(v)) return QSize(RichResourceTextWidthHint(m_widget, v), option.fontMetrics.height()); } } return ForwardingDelegate::sizeHint(option, index); } bool RDTreeWidgetDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) { RDTreeWidgetModel *rdmodel = m_widget->m_model; if(event->type() == QEvent::MouseButtonRelease && index.isValid() && rdmodel == model) { RDTreeWidgetItem *item = rdmodel->itemForIndex(index); if(index.column() < item->m_text.count()) { QVariant v = item->m_text[index.column()]; if(RichResourceTextCheck(v)) { // ignore the return value, we always consume clicks on this cell RichResourceTextMouseEvent(m_widget, v, option.rect, (QMouseEvent *)event); return true; } } } return ForwardingDelegate::editorEvent(event, model, option, index); } bool RDTreeWidgetDelegate::linkHover(QMouseEvent *e, const QModelIndex &index) { if(index.isValid()) { RDTreeWidgetItem *item = m_widget->m_model->itemForIndex(index); if(index.column() < item->m_text.count()) { QVariant v = item->m_text[index.column()]; if(RichResourceTextCheck(v)) return RichResourceTextMouseEvent(m_widget, v, m_widget->visualRect(index), e); } } return false; } RDTreeWidget::RDTreeWidget(QWidget *parent) : RDTreeView(parent) { // we'll call this ourselves in drawBranches() RDTreeView::enableBranchRectFill(false); m_delegate = new RDTreeWidgetDelegate(this); RDTreeView::setItemDelegate(m_delegate); header()->setSectionsMovable(false); 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(-1, -1); m_queuedChildren = false; m_queuedRoles = 0; setUpdatesEnabled(false); } void RDTreeWidget::endUpdate() { m_queueUpdates = false; if(m_queuedRoles || m_queuedChildren) { // 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 roles; for(int r = 0; r < 64; r++) { if(m_queuedRoles & (1ULL << r)) roles.push_back(r); } if(m_queuedChildren) { m_model->beginAddChild(m_queuedItem); m_model->endAddChild(m_queuedItem); } if(!roles.isEmpty()) m_model->itemsChanged(m_queuedItem, m_lowestIndex, m_highestIndex, roles); } } setUpdatesEnabled(true); } void RDTreeWidget::setColumnAlignment(int column, Qt::Alignment align) { if(m_alignments.count() <= column) m_alignments.resize(column + 1); m_alignments[column] = align; } void RDTreeWidget::setItemDelegate(QAbstractItemDelegate *delegate) { m_userDelegate = delegate; m_delegate->setForwardDelegate(m_userDelegate); } QAbstractItemDelegate *RDTreeWidget::itemDelegate() const { return m_userDelegate; } 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_clearing = true; m_root->clear(); m_clearing = false; m_currentHoverIndex = QModelIndex(); m_model->refresh(); } void RDTreeWidget::mouseMoveEvent(QMouseEvent *e) { RDTreeWidgetItem *oldHover = m_model->itemForIndex(m_currentHoverIndex); RDTreeView::mouseMoveEvent(e); RDTreeWidgetItem *newHover = m_model->itemForIndex(m_currentHoverIndex); if(m_currentHoverIndex.column() == m_hoverColumn && m_hoverHandCursor) { setCursor(QCursor(Qt::PointingHandCursor)); } else if(m_delegate->linkHover(e, m_currentHoverIndex)) { m_model->itemChanged(m_model->itemForIndex(m_currentHoverIndex), {Qt::DecorationRole}); setCursor(QCursor(Qt::PointingHandCursor)); } else { unsetCursor(); } if(oldHover == newHover) return; // it's only two items, don't try and make a range but just change them both QVector roles = {Qt::DecorationRole, Qt::BackgroundRole, Qt::ForegroundRole}; if(oldHover) m_model->itemChanged(oldHover, roles); m_model->itemChanged(newHover, roles); if(m_instantTooltips) { QToolTip::hideText(); if(newHover && !newHover->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(), newHover->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_currentHoverIndex.isValid()) { RDTreeWidgetItem *item = m_model->itemForIndex(m_currentHoverIndex); if(!item->m_tooltip.isEmpty() && m_instantTooltips) QToolTip::hideText(); 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++) { QString text = item->m_text[i].toString(); widths[i] = qMax(widths[i], text.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"); QString text = item->m_text[i].toString(); clipData += format.arg(text, -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. // 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 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()); // calling this manually here means it won't be called later in RDTreeView::drawBranches, and // allows us to overwrite it with our background filling if we want to. RDTreeWidget::fillBranchesRect(painter, rect, index); if(!selectionModel()->isSelected(index) && item->m_back != QBrush()) { painter->fillRect(allLinesRect, item->m_back); } RDTreeView::drawBranches(painter, rect, index); // we now iterate from the top-most parent down, moving in from the left // we draw this after calling into drawBranches() so we paint on top of the built-in lines 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)); QPoint topCentre = QRect(branchRect).center(); QPoint bottomCentre = topCentre; topCentre.setY(branchRect.top()); bottomCentre.setY(branchRect.bottom()); painter->drawLine(topCentre, bottomCentre); } branchRect.moveLeft(branchRect.left() + indentation()); } painter->setPen(oldPen); } void RDTreeWidget::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) { emit itemSelectionChanged(); RDTreeView::selectionChanged(selected, deselected); } void RDTreeWidget::currentChanged(const QModelIndex ¤t, const QModelIndex &previous) { emit currentItemChanged(m_model->itemForIndex(current), m_model->itemForIndex(previous)); RDTreeView::currentChanged(current, previous); } void RDTreeWidget::itemDataChanged(RDTreeWidgetItem *item, int column, 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(row, 0); m_highestIndex = qMakePair(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}); } emit itemChanged(item, column); } void RDTreeWidget::beginAddChild(RDTreeWidgetItem *item) { if(m_queueUpdates) { m_queuedChildren = true; if(m_lowestIndex.first == -1) { m_queuedItem = item; // make an update of row 0. This will be a bit pessimistic if there are later data changes // in a later row, but we're generally only changing data *or* adding children, not both, // and // in either case this is primarily about batching updates not providing a minimal update // set m_lowestIndex = qMakePair(0, 0); m_highestIndex = qMakePair(0, m_headers.count() - 1); } else { if(m_queuedItem == item) { // there's already an update. don't need to expand it, the m_queuedChildren is enough } else { // can't batch updates across multiple parents, so we just fallback to full model refresh m_queuedItem = NULL; } } } else { m_model->beginAddChild(item); } } void RDTreeWidget::endAddChild(RDTreeWidgetItem *item) { // work is all done in beginAddChild if(!m_queueUpdates) m_model->endAddChild(item); }