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 &current, 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 }