File indexing completed on 2024-05-26 04:32:35
0001 /* 0002 SPDX-FileCopyrightText: 2006 Gábor Lehel <illissius@gmail.com> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 #include "NodeView.h" 0007 #include "NodePropertyAction_p.h" 0008 #include "NodeDelegate.h" 0009 #include "NodeViewVisibilityDelegate.h" 0010 #include "kis_node_model.h" 0011 #include "kis_signals_blocker.h" 0012 0013 0014 #include <kconfig.h> 0015 #include <kconfiggroup.h> 0016 #include <kis_config.h> 0017 #include <kis_icon.h> 0018 #include <ksharedconfig.h> 0019 #include <KisKineticScroller.h> 0020 0021 #include <QtDebug> 0022 #include <QContextMenuEvent> 0023 #include <QHeaderView> 0024 #include <QHelpEvent> 0025 #include <QMenu> 0026 #include <QDrag> 0027 #include <QMouseEvent> 0028 #include <QPersistentModelIndex> 0029 #include <QApplication> 0030 #include <QPainter> 0031 #include <QScrollBar> 0032 #include <QScroller> 0033 0034 #include "kis_node_view_color_scheme.h" 0035 0036 0037 #ifdef HAVE_X11 0038 #define DRAG_WHILE_DRAG_WORKAROUND 0039 #endif 0040 0041 #ifdef DRAG_WHILE_DRAG_WORKAROUND 0042 #define DRAG_WHILE_DRAG_WORKAROUND_START() d->isDragging = true 0043 #define DRAG_WHILE_DRAG_WORKAROUND_STOP() d->isDragging = false 0044 #else 0045 #define DRAG_WHILE_DRAG_WORKAROUND_START() 0046 #define DRAG_WHILE_DRAG_WORKAROUND_STOP() 0047 #endif 0048 0049 0050 class Q_DECL_HIDDEN NodeView::Private 0051 { 0052 public: 0053 Private(NodeView* _q) 0054 : delegate(_q, _q) 0055 #ifdef DRAG_WHILE_DRAG_WORKAROUND 0056 , isDragging(false) 0057 #endif 0058 { 0059 } 0060 NodeDelegate delegate; 0061 QPersistentModelIndex hovered; 0062 QPoint lastPos; 0063 0064 #ifdef DRAG_WHILE_DRAG_WORKAROUND 0065 bool isDragging; 0066 #endif 0067 }; 0068 0069 0070 NodeView::NodeView(QWidget *parent) 0071 : QTreeView(parent) 0072 , m_draggingFlag(false) 0073 , d(new Private(this)) 0074 { 0075 setItemDelegate(&d->delegate); 0076 0077 setMouseTracking(true); 0078 setSelectionBehavior(SelectRows); 0079 setDefaultDropAction(Qt::MoveAction); 0080 setVerticalScrollMode(QAbstractItemView::ScrollPerItem); 0081 setSelectionMode(QAbstractItemView::ExtendedSelection); 0082 setRootIsDecorated(false); 0083 0084 header()->hide(); 0085 setDragEnabled(true); 0086 setDragDropMode(QAbstractItemView::DragDrop); 0087 setAcceptDrops(true); 0088 setDropIndicatorShown(true); 0089 0090 { 0091 QScroller *scroller = KisKineticScroller::createPreconfiguredScroller(this); 0092 if (scroller) { 0093 connect(scroller, SIGNAL(stateChanged(QScroller::State)), 0094 this, SLOT(slotScrollerStateChanged(QScroller::State))); 0095 } 0096 } 0097 } 0098 0099 NodeView::~NodeView() 0100 { 0101 delete d; 0102 } 0103 0104 void NodeView::setModel(QAbstractItemModel *model) 0105 { 0106 QTreeView::setModel(model); 0107 0108 if (!this->model()->inherits("KisNodeModel") && !this->model()->inherits("KisNodeFilterProxyModel")) { 0109 qWarning() << "NodeView may not work with" << model->metaObject()->className(); 0110 } 0111 if (this->model()->columnCount() != 3) { 0112 qWarning() << "NodeView: expected 2 model columns, got " << this->model()->columnCount(); 0113 } 0114 0115 if (header()->sectionPosition(VISIBILITY_COL) != 0 || header()->sectionPosition(SELECTED_COL) != 1) { 0116 header()->moveSection(VISIBILITY_COL, 0); 0117 header()->moveSection(SELECTED_COL, 1); 0118 } 0119 0120 KisConfig cfg(true); 0121 if (!cfg.useLayerSelectionCheckbox()) { 0122 header()->hideSection(SELECTED_COL); 0123 } 0124 0125 // the default may be too large for our visibility icon 0126 header()->setMinimumSectionSize(KisNodeViewColorScheme::instance()->visibilityColumnWidth()); 0127 } 0128 0129 void NodeView::addPropertyActions(QMenu *menu, const QModelIndex &index) 0130 { 0131 KisBaseNode::PropertyList list = index.data(KisNodeModel::PropertiesRole).value<KisBaseNode::PropertyList>(); 0132 for (int i = 0, n = list.count(); i < n; ++i) { 0133 if (list.at(i).isMutable) { 0134 PropertyAction *a = new PropertyAction(i, list.at(i), index, menu); 0135 connect(a, SIGNAL(toggled(bool,QPersistentModelIndex,int)), 0136 this, SLOT(slotActionToggled(bool,QPersistentModelIndex,int))); 0137 menu->addAction(a); 0138 } 0139 } 0140 } 0141 0142 void NodeView::updateNode(const QModelIndex &index) 0143 { 0144 dataChanged(index, index); 0145 } 0146 0147 void NodeView::toggleSolo(const QModelIndex &index) { 0148 d->delegate.toggleSolo(index); 0149 } 0150 0151 QItemSelectionModel::SelectionFlags NodeView::selectionCommand(const QModelIndex &index, 0152 const QEvent *event) const 0153 { 0154 /** 0155 * Qt has a bug: when we Ctrl+click on an item, the item's 0156 * selections gets toggled on mouse *press*, whereas usually it is 0157 * done on mouse *release*. Therefore the user cannot do a 0158 * Ctrl+D&D with the default configuration. This code fixes the 0159 * problem by manually returning QItemSelectionModel::NoUpdate 0160 * flag when the user clicks on an item and returning 0161 * QItemSelectionModel::Toggle on release. 0162 */ 0163 0164 if (event && 0165 (event->type() == QEvent::MouseButtonPress || 0166 event->type() == QEvent::MouseButtonRelease) && 0167 index.isValid()) { 0168 0169 const QMouseEvent *mevent = static_cast<const QMouseEvent*>(event); 0170 0171 if (mevent->button() == Qt::RightButton && 0172 selectionModel()->selectedIndexes().contains(index)) { 0173 0174 // Allow calling context menu for multiple layers 0175 return QItemSelectionModel::NoUpdate; 0176 } 0177 0178 if (event->type() == QEvent::MouseButtonPress && 0179 (mevent->modifiers() & Qt::ControlModifier)) { 0180 0181 return QItemSelectionModel::NoUpdate; 0182 } 0183 0184 if (event->type() == QEvent::MouseButtonRelease && 0185 (mevent->modifiers() & Qt::ControlModifier)) { 0186 0187 // Select the entire row, otherwise we only get updates for DEFAULT_COL and then we have to 0188 // manually sync its state with SELECTED_COL. 0189 return QItemSelectionModel::Toggle | QItemSelectionModel::Rows; 0190 } 0191 } 0192 0193 /** 0194 * Qt 5.6 has a bug: it reads global modifiers, not the ones 0195 * passed from event. So if you paste an item using Ctrl+V it'll 0196 * select multiple layers for you 0197 */ 0198 Qt::KeyboardModifiers globalModifiers = QApplication::keyboardModifiers(); 0199 if (!event && globalModifiers != Qt::NoModifier) { 0200 return QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows; 0201 } 0202 0203 return QAbstractItemView::selectionCommand(index, event); 0204 } 0205 0206 QModelIndex NodeView::indexAt(const QPoint &point) const 0207 { 0208 KisNodeViewColorScheme scm; 0209 0210 QModelIndex index = QTreeView::indexAt(point); 0211 if (!index.isValid()) { 0212 // Middle is a good position for both LTR and RTL layouts 0213 // First reset x, then get the x in the middle 0214 index = QTreeView::indexAt(point - QPoint(point.x(), 0) + QPoint(width() / 2, 0)); 0215 } 0216 0217 return index; 0218 } 0219 0220 bool NodeView::viewportEvent(QEvent *e) 0221 { 0222 if (model()) { 0223 switch(e->type()) { 0224 case QEvent::MouseButtonPress: { 0225 DRAG_WHILE_DRAG_WORKAROUND_STOP(); 0226 0227 const QPoint pos = static_cast<QMouseEvent*>(e)->pos(); 0228 d->lastPos = pos; 0229 0230 if (!indexAt(pos).isValid()) { 0231 return QTreeView::viewportEvent(e); 0232 } 0233 QModelIndex index = model()->buddy(indexAt(pos)); 0234 if (d->delegate.editorEvent(e, model(), optionForIndex(index), index)) { 0235 return true; 0236 } 0237 } break; 0238 case QEvent::Leave: { 0239 QEvent e(QEvent::Leave); 0240 d->delegate.editorEvent(&e, model(), optionForIndex(d->hovered), d->hovered); 0241 d->hovered = QModelIndex(); 0242 } break; 0243 case QEvent::MouseMove: { 0244 #ifdef DRAG_WHILE_DRAG_WORKAROUND 0245 if (d->isDragging) { 0246 return false; 0247 } 0248 #endif 0249 0250 const QPoint pos = static_cast<QMouseEvent*>(e)->pos(); 0251 QModelIndex hovered = indexAt(pos); 0252 if (hovered != d->hovered) { 0253 if (d->hovered.isValid()) { 0254 QEvent e(QEvent::Leave); 0255 d->delegate.editorEvent(&e, model(), optionForIndex(d->hovered), d->hovered); 0256 } 0257 if (hovered.isValid()) { 0258 QEvent e(QEvent::Enter); 0259 d->delegate.editorEvent(&e, model(), optionForIndex(hovered), hovered); 0260 } 0261 d->hovered = hovered; 0262 } 0263 /* This is a workaround for a bug in QTreeView that immediately begins a dragging action 0264 when the mouse lands on the decoration/icon of a different index and moves 1 pixel or more */ 0265 Qt::MouseButtons buttons = static_cast<QMouseEvent*>(e)->buttons(); 0266 if ((Qt::LeftButton | Qt::MiddleButton) & buttons) { 0267 if ((pos - d->lastPos).manhattanLength() > qApp->startDragDistance()) { 0268 return QTreeView::viewportEvent(e); 0269 } 0270 return true; 0271 } 0272 } break; 0273 case QEvent::ToolTip: { 0274 const QPoint pos = static_cast<QHelpEvent*>(e)->pos(); 0275 if (!indexAt(pos).isValid()) { 0276 return QTreeView::viewportEvent(e); 0277 } 0278 QModelIndex index = model()->buddy(indexAt(pos)); 0279 return d->delegate.editorEvent(e, model(), optionForIndex(index), index); 0280 } break; 0281 case QEvent::Resize: { 0282 scheduleDelayedItemsLayout(); 0283 break; 0284 } 0285 default: break; 0286 } 0287 } 0288 return QTreeView::viewportEvent(e); 0289 } 0290 0291 void NodeView::contextMenuEvent(QContextMenuEvent *e) 0292 { 0293 QTreeView::contextMenuEvent(e); 0294 QModelIndex i = indexAt(e->pos()); 0295 if (model()) 0296 i = model()->buddy(i); 0297 showContextMenu(e->globalPos(), i); 0298 } 0299 0300 void NodeView::showContextMenu(const QPoint &globalPos, const QModelIndex &index) 0301 { 0302 emit contextMenuRequested(globalPos, index); 0303 } 0304 0305 void NodeView::currentChanged(const QModelIndex ¤t, const QModelIndex &previous) 0306 { 0307 QTreeView::currentChanged(current, previous); 0308 if (current != previous) { 0309 Q_ASSERT(!current.isValid() || current.model() == model()); 0310 KisSignalsBlocker blocker(this); 0311 model()->setData(current, true, KisNodeModel::ActiveRole); 0312 } 0313 } 0314 0315 void NodeView::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &/*roles*/) 0316 { 0317 QTreeView::dataChanged(topLeft, bottomRight); 0318 0319 for (int x = topLeft.row(); x <= bottomRight.row(); ++x) { 0320 for (int y = topLeft.column(); y <= bottomRight.column(); ++y) { 0321 QModelIndex index = topLeft.sibling(x, y); 0322 if (index.data(KisNodeModel::ActiveRole).toBool()) { 0323 if (currentIndex() != index) { 0324 setCurrentIndex(index); 0325 } 0326 0327 return; 0328 } 0329 } 0330 } 0331 } 0332 0333 void NodeView::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) 0334 { 0335 QTreeView::selectionChanged(selected, deselected); 0336 // XXX: selectedIndexes() does not include hidden (collapsed) items, is this really intended? 0337 emit selectionChanged(selectedIndexes()); 0338 } 0339 0340 void NodeView::slotActionToggled(bool on, const QPersistentModelIndex &index, int num) 0341 { 0342 KisBaseNode::PropertyList list = index.data(KisNodeModel::PropertiesRole).value<KisBaseNode::PropertyList>(); 0343 list[num].state = on; 0344 const_cast<QAbstractItemModel*>(index.model())->setData(index, QVariant::fromValue(list), KisNodeModel::PropertiesRole); 0345 } 0346 0347 QStyleOptionViewItem NodeView::optionForIndex(const QModelIndex &index) const 0348 { 0349 QStyleOptionViewItem option = viewOptions(); 0350 option.rect = visualRect(index); 0351 if (index == currentIndex()) 0352 option.state |= QStyle::State_HasFocus; 0353 return option; 0354 } 0355 0356 void NodeView::startDrag(Qt::DropActions supportedActions) 0357 { 0358 DRAG_WHILE_DRAG_WORKAROUND_START(); 0359 QTreeView::startDrag(supportedActions); 0360 } 0361 0362 QPixmap NodeView::createDragPixmap() const 0363 { 0364 const QModelIndexList selectedIndexes = selectionModel()->selectedIndexes(); 0365 Q_ASSERT(!selectedIndexes.isEmpty()); 0366 0367 const int itemCount = selectedIndexes.count(); 0368 0369 // If more than one item is dragged, align the items inside a 0370 // rectangular grid. The maximum grid size is limited to 4 x 4 items. 0371 int xCount = 2; 0372 int size = 96; 0373 if (itemCount > 9) { 0374 xCount = 4; 0375 size = KisIconUtils::SizeLarge; 0376 } 0377 else if (itemCount > 4) { 0378 xCount = 3; 0379 size = KisIconUtils::SizeHuge; 0380 } 0381 else if (itemCount < xCount) { 0382 xCount = itemCount; 0383 } 0384 0385 int yCount = itemCount / xCount; 0386 if (itemCount % xCount != 0) { 0387 ++yCount; 0388 } 0389 0390 if (yCount > xCount) { 0391 yCount = xCount; 0392 } 0393 0394 // Draw the selected items into the grid cells 0395 QPixmap dragPixmap(xCount * size + xCount - 1, yCount * size + yCount - 1); 0396 dragPixmap.fill(Qt::transparent); 0397 0398 QPainter painter(&dragPixmap); 0399 int x = 0; 0400 int y = 0; 0401 Q_FOREACH (const QModelIndex &selectedIndex, selectedIndexes) { 0402 const QImage img = selectedIndex.data(int(KisNodeModel::BeginThumbnailRole) + size).value<QImage>(); 0403 painter.drawPixmap(x, y, QPixmap::fromImage(img.scaled(QSize(size, size), Qt::KeepAspectRatio, Qt::SmoothTransformation))); 0404 0405 x += size + 1; 0406 if (x >= dragPixmap.width()) { 0407 x = 0; 0408 y += size + 1; 0409 } 0410 if (y >= dragPixmap.height()) { 0411 break; 0412 } 0413 } 0414 0415 return dragPixmap; 0416 } 0417 0418 void NodeView::resizeEvent(QResizeEvent * event) 0419 { 0420 KisNodeViewColorScheme scm; 0421 header()->setStretchLastSection(false); 0422 0423 int otherColumnsWidth = scm.visibilityColumnWidth(); 0424 0425 // if layer box is enabled subtract its width from the "Default col". 0426 if (KisConfig(false).useLayerSelectionCheckbox()) { 0427 otherColumnsWidth += scm.selectedButtonColumnWidth(); 0428 } 0429 header()->resizeSection(DEFAULT_COL, event->size().width() - otherColumnsWidth); 0430 header()->resizeSection(SELECTED_COL, scm.selectedButtonColumnWidth()); 0431 header()->resizeSection(VISIBILITY_COL, scm.visibilityColumnWidth()); 0432 0433 setIndentation(scm.indentation()); 0434 QTreeView::resizeEvent(event); 0435 } 0436 0437 void NodeView::paintEvent(QPaintEvent *event) 0438 { 0439 event->accept(); 0440 QTreeView::paintEvent(event); 0441 } 0442 0443 void NodeView::drawBranches(QPainter *painter, const QRect &rect, 0444 const QModelIndex &index) const 0445 { 0446 QStyleOptionViewItem options = viewOptions(); 0447 options.rect = rect; 0448 // This is not really a job for an item delegate, but the logic was already there 0449 d->delegate.drawBranches(painter, options, index); 0450 } 0451 0452 void NodeView::dropEvent(QDropEvent *ev) 0453 { 0454 QTreeView::dropEvent(ev); 0455 DRAG_WHILE_DRAG_WORKAROUND_STOP(); 0456 } 0457 0458 int NodeView::cursorPageIndex() const 0459 { 0460 QSize size(visualRect(model()->index(0, 0, QModelIndex())).width(), visualRect(model()->index(0, 0, QModelIndex())).height()); 0461 int scrollBarValue = verticalScrollBar()->value(); 0462 0463 QPoint cursorPosition = QWidget::mapFromGlobal(QCursor::pos()); 0464 0465 int numberRow = (cursorPosition.y() + scrollBarValue) / size.height(); 0466 0467 //If cursor is at the half button of the page then the move action is performed after the slide, otherwise it is 0468 //performed before the page 0469 if (abs((cursorPosition.y() + scrollBarValue) - size.height()*numberRow) > (size.height()/2)) { 0470 numberRow++; 0471 } 0472 0473 if (numberRow > model()->rowCount(QModelIndex())) { 0474 numberRow = model()->rowCount(QModelIndex()); 0475 } 0476 0477 return numberRow; 0478 } 0479 0480 void NodeView::dragEnterEvent(QDragEnterEvent *ev) 0481 { 0482 DRAG_WHILE_DRAG_WORKAROUND_START(); 0483 0484 QVariant data = QVariant::fromValue( 0485 static_cast<void*>(const_cast<QMimeData*>(ev->mimeData()))); 0486 model()->setData(QModelIndex(), data, KisNodeModel::DropEnabled); 0487 0488 QTreeView::dragEnterEvent(ev); 0489 } 0490 0491 void NodeView::dragMoveEvent(QDragMoveEvent *ev) 0492 { 0493 DRAG_WHILE_DRAG_WORKAROUND_START(); 0494 QTreeView::dragMoveEvent(ev); 0495 } 0496 0497 void NodeView::dragLeaveEvent(QDragLeaveEvent *e) 0498 { 0499 QTreeView::dragLeaveEvent(e); 0500 DRAG_WHILE_DRAG_WORKAROUND_STOP(); 0501 } 0502 0503 bool NodeView::isDragging() const 0504 { 0505 return m_draggingFlag; 0506 } 0507 0508 void NodeView::setDraggingFlag(bool flag) 0509 { 0510 m_draggingFlag = flag; 0511 } 0512 0513 void NodeView::slotUpdateIcons() 0514 { 0515 d->delegate.slotUpdateIcon(); 0516 } 0517 0518 void NodeView::slotScrollerStateChanged(QScroller::State state){ 0519 KisKineticScroller::updateCursor(this, state); 0520 } 0521 0522 void NodeView::slotConfigurationChanged() 0523 { 0524 setIndentation(KisNodeViewColorScheme::instance()->indentation()); 0525 updateSelectedCheckboxColumn(); 0526 d->delegate.slotConfigChanged(); 0527 } 0528 0529 void NodeView::updateSelectedCheckboxColumn() 0530 { 0531 KisConfig cfg(false); 0532 if (cfg.useLayerSelectionCheckbox() == !header()->isSectionHidden(SELECTED_COL)) { 0533 return; 0534 } 0535 header()->setSectionHidden(SELECTED_COL, !cfg.useLayerSelectionCheckbox()); 0536 // add/subtract width based on SELECTED_COL section's visibility 0537 header()->resizeSection(DEFAULT_COL, 0538 size().width() 0539 + (cfg.useLayerSelectionCheckbox() ? header()->sectionSize(SELECTED_COL) 0540 : -header()->sectionSize(SELECTED_COL))); 0541 }