File indexing completed on 2024-05-12 05:47:30

0001 /*
0002  * SPDX-FileCopyrightText: 2011 Peter Penz <peter.penz19@gmail.com>
0003  *
0004  * SPDX-License-Identifier: GPL-2.0-or-later
0005  */
0006 
0007 #include "kitemlistheaderwidget.h"
0008 #include "kitemviews/kitemmodelbase.h"
0009 
0010 #include <QApplication>
0011 #include <QGraphicsSceneHoverEvent>
0012 #include <QPainter>
0013 #include <QStyleOptionHeader>
0014 
0015 KItemListHeaderWidget::KItemListHeaderWidget(QGraphicsWidget *parent)
0016     : QGraphicsWidget(parent)
0017     , m_automaticColumnResizing(true)
0018     , m_model(nullptr)
0019     , m_offset(0)
0020     , m_sidePadding(0)
0021     , m_columns()
0022     , m_columnWidths()
0023     , m_preferredColumnWidths()
0024     , m_hoveredIndex(-1)
0025     , m_pressedRoleIndex(-1)
0026     , m_roleOperation(NoRoleOperation)
0027     , m_pressedMousePos()
0028     , m_movingRole()
0029 {
0030     m_movingRole.x = 0;
0031     m_movingRole.xDec = 0;
0032     m_movingRole.index = -1;
0033 
0034     setAcceptHoverEvents(true);
0035     // TODO update when font changes at runtime
0036     setFont(QApplication::font("QHeaderView"));
0037 }
0038 
0039 KItemListHeaderWidget::~KItemListHeaderWidget()
0040 {
0041 }
0042 
0043 void KItemListHeaderWidget::setModel(KItemModelBase *model)
0044 {
0045     if (m_model == model) {
0046         return;
0047     }
0048 
0049     if (m_model) {
0050         disconnect(m_model, &KItemModelBase::sortRoleChanged, this, &KItemListHeaderWidget::slotSortRoleChanged);
0051         disconnect(m_model, &KItemModelBase::sortOrderChanged, this, &KItemListHeaderWidget::slotSortOrderChanged);
0052     }
0053 
0054     m_model = model;
0055 
0056     if (m_model) {
0057         connect(m_model, &KItemModelBase::sortRoleChanged, this, &KItemListHeaderWidget::slotSortRoleChanged);
0058         connect(m_model, &KItemModelBase::sortOrderChanged, this, &KItemListHeaderWidget::slotSortOrderChanged);
0059     }
0060 }
0061 
0062 KItemModelBase *KItemListHeaderWidget::model() const
0063 {
0064     return m_model;
0065 }
0066 
0067 void KItemListHeaderWidget::setAutomaticColumnResizing(bool automatic)
0068 {
0069     m_automaticColumnResizing = automatic;
0070 }
0071 
0072 bool KItemListHeaderWidget::automaticColumnResizing() const
0073 {
0074     return m_automaticColumnResizing;
0075 }
0076 
0077 void KItemListHeaderWidget::setColumns(const QList<QByteArray> &roles)
0078 {
0079     for (const QByteArray &role : roles) {
0080         if (!m_columnWidths.contains(role)) {
0081             m_preferredColumnWidths.remove(role);
0082         }
0083     }
0084 
0085     m_columns = roles;
0086     update();
0087 }
0088 
0089 QList<QByteArray> KItemListHeaderWidget::columns() const
0090 {
0091     return m_columns;
0092 }
0093 
0094 void KItemListHeaderWidget::setColumnWidth(const QByteArray &role, qreal width)
0095 {
0096     const qreal minWidth = minimumColumnWidth();
0097     if (width < minWidth) {
0098         width = minWidth;
0099     }
0100 
0101     if (m_columnWidths.value(role) != width) {
0102         m_columnWidths.insert(role, width);
0103         update();
0104     }
0105 }
0106 
0107 qreal KItemListHeaderWidget::columnWidth(const QByteArray &role) const
0108 {
0109     return m_columnWidths.value(role);
0110 }
0111 
0112 void KItemListHeaderWidget::setPreferredColumnWidth(const QByteArray &role, qreal width)
0113 {
0114     m_preferredColumnWidths.insert(role, width);
0115 }
0116 
0117 qreal KItemListHeaderWidget::preferredColumnWidth(const QByteArray &role) const
0118 {
0119     return m_preferredColumnWidths.value(role);
0120 }
0121 
0122 void KItemListHeaderWidget::setOffset(qreal offset)
0123 {
0124     if (m_offset != offset) {
0125         m_offset = offset;
0126         update();
0127     }
0128 }
0129 
0130 qreal KItemListHeaderWidget::offset() const
0131 {
0132     return m_offset;
0133 }
0134 
0135 void KItemListHeaderWidget::setSidePadding(qreal width)
0136 {
0137     if (m_sidePadding != width) {
0138         m_sidePadding = width;
0139         Q_EMIT sidePaddingChanged(width);
0140         update();
0141     }
0142 }
0143 
0144 qreal KItemListHeaderWidget::sidePadding() const
0145 {
0146     return m_sidePadding;
0147 }
0148 
0149 qreal KItemListHeaderWidget::minimumColumnWidth() const
0150 {
0151     QFontMetricsF fontMetrics(font());
0152     return fontMetrics.height() * 4;
0153 }
0154 
0155 void KItemListHeaderWidget::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
0156 {
0157     Q_UNUSED(option)
0158     Q_UNUSED(widget)
0159 
0160     if (!m_model) {
0161         return;
0162     }
0163 
0164     // Draw roles
0165     painter->setFont(font());
0166     painter->setPen(palette().text().color());
0167 
0168     qreal x = -m_offset + m_sidePadding;
0169     int orderIndex = 0;
0170     for (const QByteArray &role : std::as_const(m_columns)) {
0171         const qreal roleWidth = m_columnWidths.value(role);
0172         const QRectF rect(x, 0, roleWidth, size().height());
0173         paintRole(painter, role, rect, orderIndex, widget);
0174         x += roleWidth;
0175         ++orderIndex;
0176     }
0177 
0178     if (!m_movingRole.pixmap.isNull()) {
0179         Q_ASSERT(m_roleOperation == MoveRoleOperation);
0180         painter->drawPixmap(m_movingRole.x, 0, m_movingRole.pixmap);
0181     }
0182 }
0183 
0184 void KItemListHeaderWidget::mousePressEvent(QGraphicsSceneMouseEvent *event)
0185 {
0186     if (event->button() & Qt::LeftButton) {
0187         m_pressedMousePos = event->pos();
0188         if (isAbovePaddingGrip(m_pressedMousePos, PaddingGrip::Leading)) {
0189             m_roleOperation = ResizePaddingColumnOperation;
0190         } else {
0191             updatePressedRoleIndex(event->pos());
0192             m_roleOperation = isAboveRoleGrip(m_pressedMousePos, m_pressedRoleIndex) ? ResizeRoleOperation : NoRoleOperation;
0193         }
0194         event->accept();
0195     } else {
0196         event->ignore();
0197     }
0198 }
0199 
0200 void KItemListHeaderWidget::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
0201 {
0202     QGraphicsWidget::mouseReleaseEvent(event);
0203 
0204     if (m_pressedRoleIndex == -1) {
0205         return;
0206     }
0207 
0208     switch (m_roleOperation) {
0209     case NoRoleOperation: {
0210         // Only a click has been done and no moving or resizing has been started
0211         const QByteArray sortRole = m_model->sortRole();
0212         const int sortRoleIndex = m_columns.indexOf(sortRole);
0213         if (m_pressedRoleIndex == sortRoleIndex) {
0214             // Toggle the sort order
0215             const Qt::SortOrder previous = m_model->sortOrder();
0216             const Qt::SortOrder current = (m_model->sortOrder() == Qt::AscendingOrder) ? Qt::DescendingOrder : Qt::AscendingOrder;
0217             m_model->setSortOrder(current);
0218             Q_EMIT sortOrderChanged(current, previous);
0219         } else {
0220             // Change the sort role and reset to the ascending order
0221             const QByteArray previous = m_model->sortRole();
0222             const QByteArray current = m_columns[m_pressedRoleIndex];
0223             const bool resetSortOrder = m_model->sortOrder() == Qt::DescendingOrder;
0224             m_model->setSortRole(current, !resetSortOrder);
0225             Q_EMIT sortRoleChanged(current, previous);
0226 
0227             if (resetSortOrder) {
0228                 m_model->setSortOrder(Qt::AscendingOrder);
0229                 Q_EMIT sortOrderChanged(Qt::AscendingOrder, Qt::DescendingOrder);
0230             }
0231         }
0232         break;
0233     }
0234 
0235     case ResizeRoleOperation: {
0236         const QByteArray pressedRole = m_columns[m_pressedRoleIndex];
0237         const qreal currentWidth = m_columnWidths.value(pressedRole);
0238         Q_EMIT columnWidthChangeFinished(pressedRole, currentWidth);
0239         break;
0240     }
0241 
0242     case MoveRoleOperation:
0243         m_movingRole.pixmap = QPixmap();
0244         m_movingRole.x = 0;
0245         m_movingRole.xDec = 0;
0246         m_movingRole.index = -1;
0247         break;
0248 
0249     default:
0250         break;
0251     }
0252 
0253     m_pressedRoleIndex = -1;
0254     m_roleOperation = NoRoleOperation;
0255     update();
0256 
0257     QApplication::restoreOverrideCursor();
0258 }
0259 
0260 void KItemListHeaderWidget::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
0261 {
0262     QGraphicsWidget::mouseMoveEvent(event);
0263 
0264     switch (m_roleOperation) {
0265     case NoRoleOperation:
0266         if ((event->pos() - m_pressedMousePos).manhattanLength() >= QApplication::startDragDistance()) {
0267             // A role gets dragged by the user. Create a pixmap of the role that will get
0268             // synchronized on each further mouse-move-event with the mouse-position.
0269             m_roleOperation = MoveRoleOperation;
0270             const int roleIndex = roleIndexAt(m_pressedMousePos);
0271             m_movingRole.index = roleIndex;
0272             if (roleIndex == 0) {
0273                 // TODO: It should be configurable whether moving the first role is allowed.
0274                 // In the context of Dolphin this is not required, however this should be
0275                 // changed if KItemViews are used in a more generic way.
0276                 QApplication::setOverrideCursor(QCursor(Qt::ForbiddenCursor));
0277             } else {
0278                 m_movingRole.pixmap = createRolePixmap(roleIndex);
0279 
0280                 qreal roleX = -m_offset + m_sidePadding;
0281                 for (int i = 0; i < roleIndex; ++i) {
0282                     const QByteArray role = m_columns[i];
0283                     roleX += m_columnWidths.value(role);
0284                 }
0285 
0286                 m_movingRole.xDec = event->pos().x() - roleX;
0287                 m_movingRole.x = roleX;
0288                 update();
0289             }
0290         }
0291         break;
0292 
0293     case ResizeRoleOperation: {
0294         const QByteArray pressedRole = m_columns[m_pressedRoleIndex];
0295 
0296         qreal previousWidth = m_columnWidths.value(pressedRole);
0297         qreal currentWidth = previousWidth;
0298         currentWidth += event->pos().x() - event->lastPos().x();
0299         currentWidth = qMax(minimumColumnWidth(), currentWidth);
0300 
0301         m_columnWidths.insert(pressedRole, currentWidth);
0302         update();
0303 
0304         Q_EMIT columnWidthChanged(pressedRole, currentWidth, previousWidth);
0305         break;
0306     }
0307 
0308     case ResizePaddingColumnOperation: {
0309         qreal currentWidth = m_sidePadding;
0310         currentWidth += event->pos().x() - event->lastPos().x();
0311         currentWidth = qMax(0.0, currentWidth);
0312 
0313         m_sidePadding = currentWidth;
0314 
0315         update();
0316 
0317         Q_EMIT sidePaddingChanged(currentWidth);
0318 
0319         break;
0320     }
0321 
0322     case MoveRoleOperation: {
0323         // TODO: It should be configurable whether moving the first role is allowed.
0324         // In the context of Dolphin this is not required, however this should be
0325         // changed if KItemViews are used in a more generic way.
0326         if (m_movingRole.index > 0) {
0327             m_movingRole.x = event->pos().x() - m_movingRole.xDec;
0328             update();
0329 
0330             const int targetIndex = targetOfMovingRole();
0331             if (targetIndex > 0 && targetIndex != m_movingRole.index) {
0332                 const QByteArray role = m_columns[m_movingRole.index];
0333                 const int previousIndex = m_movingRole.index;
0334                 m_movingRole.index = targetIndex;
0335                 Q_EMIT columnMoved(role, targetIndex, previousIndex);
0336 
0337                 m_movingRole.xDec = event->pos().x() - roleXPosition(role);
0338             }
0339         }
0340         break;
0341     }
0342 
0343     default:
0344         break;
0345     }
0346 }
0347 
0348 void KItemListHeaderWidget::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
0349 {
0350     QGraphicsItem::mouseDoubleClickEvent(event);
0351 
0352     const int roleIndex = roleIndexAt(event->pos());
0353     if (roleIndex >= 0 && isAboveRoleGrip(event->pos(), roleIndex)) {
0354         const QByteArray role = m_columns.at(roleIndex);
0355 
0356         qreal previousWidth = columnWidth(role);
0357         setColumnWidth(role, preferredColumnWidth(role));
0358         qreal currentWidth = columnWidth(role);
0359 
0360         Q_EMIT columnWidthChanged(role, currentWidth, previousWidth);
0361         Q_EMIT columnWidthChangeFinished(role, currentWidth);
0362     }
0363 }
0364 
0365 void KItemListHeaderWidget::hoverEnterEvent(QGraphicsSceneHoverEvent *event)
0366 {
0367     QGraphicsWidget::hoverEnterEvent(event);
0368     updateHoveredIndex(event->pos());
0369 }
0370 
0371 void KItemListHeaderWidget::hoverLeaveEvent(QGraphicsSceneHoverEvent *event)
0372 {
0373     QGraphicsWidget::hoverLeaveEvent(event);
0374     if (m_hoveredIndex != -1) {
0375         Q_EMIT columnUnHovered(m_hoveredIndex);
0376         m_hoveredIndex = -1;
0377         update();
0378     }
0379 }
0380 
0381 void KItemListHeaderWidget::hoverMoveEvent(QGraphicsSceneHoverEvent *event)
0382 {
0383     QGraphicsWidget::hoverMoveEvent(event);
0384 
0385     const QPointF &pos = event->pos();
0386     updateHoveredIndex(pos);
0387     if ((m_hoveredIndex >= 0 && isAboveRoleGrip(pos, m_hoveredIndex)) || isAbovePaddingGrip(pos, PaddingGrip::Leading)
0388         || isAbovePaddingGrip(pos, PaddingGrip::Trailing)) {
0389         setCursor(Qt::SplitHCursor);
0390     } else {
0391         unsetCursor();
0392     }
0393 }
0394 
0395 void KItemListHeaderWidget::slotSortRoleChanged(const QByteArray &current, const QByteArray &previous)
0396 {
0397     Q_UNUSED(current)
0398     Q_UNUSED(previous)
0399     update();
0400 }
0401 
0402 void KItemListHeaderWidget::slotSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous)
0403 {
0404     Q_UNUSED(current)
0405     Q_UNUSED(previous)
0406     update();
0407 }
0408 
0409 void KItemListHeaderWidget::paintRole(QPainter *painter, const QByteArray &role, const QRectF &rect, int orderIndex, QWidget *widget) const
0410 {
0411     const auto direction = widget ? widget->layoutDirection() : qApp->layoutDirection();
0412 
0413     // The following code is based on the code from QHeaderView::paintSection().
0414     // SPDX-FileCopyrightText: 2011 Nokia Corporation and/or its subsidiary(-ies).
0415     QStyleOptionHeader option;
0416     option.direction = direction;
0417     option.textAlignment = direction == Qt::LeftToRight ? Qt::AlignLeft : Qt::AlignRight;
0418 
0419     option.section = orderIndex;
0420     option.state = QStyle::State_None | QStyle::State_Raised | QStyle::State_Horizontal;
0421     if (isEnabled()) {
0422         option.state |= QStyle::State_Enabled;
0423     }
0424     if (window() && window()->isActiveWindow()) {
0425         option.state |= QStyle::State_Active;
0426     }
0427     if (m_hoveredIndex == orderIndex) {
0428         option.state |= QStyle::State_MouseOver;
0429     }
0430     if (m_pressedRoleIndex == orderIndex) {
0431         option.state |= QStyle::State_Sunken;
0432     }
0433     if (m_model->sortRole() == role) {
0434         option.sortIndicator = (m_model->sortOrder() == Qt::AscendingOrder) ? QStyleOptionHeader::SortDown : QStyleOptionHeader::SortUp;
0435     }
0436     option.rect = rect.toRect();
0437     option.orientation = Qt::Horizontal;
0438     option.selectedPosition = QStyleOptionHeader::NotAdjacent;
0439     option.text = m_model->roleDescription(role);
0440 
0441     // First we paint any potential empty (padding) space on left and/or right of this role's column.
0442     const auto paintPadding = [&](int section, const QRectF &rect, const QStyleOptionHeader::SectionPosition &pos) {
0443         QStyleOptionHeader padding;
0444         padding.state = QStyle::State_None | QStyle::State_Raised | QStyle::State_Horizontal;
0445         padding.section = section;
0446         padding.sortIndicator = QStyleOptionHeader::None;
0447         padding.rect = rect.toRect();
0448         padding.position = pos;
0449         padding.text = QString();
0450         style()->drawControl(QStyle::CE_Header, &padding, painter, widget);
0451     };
0452 
0453     if (m_columns.count() == 1) {
0454         option.position = QStyleOptionHeader::Middle;
0455         paintPadding(0, QRectF(0.0, 0.0, rect.left(), rect.height()), QStyleOptionHeader::Beginning);
0456         paintPadding(1, QRectF(rect.left(), 0.0, size().width() - rect.left(), rect.height()), QStyleOptionHeader::End);
0457     } else if (orderIndex == 0) {
0458         // Paint the header for the first column; check if there is some empty space to the left which needs to be filled.
0459         if (rect.left() > 0) {
0460             option.position = QStyleOptionHeader::Middle;
0461             paintPadding(0, QRectF(0.0, 0.0, rect.left(), rect.height()), QStyleOptionHeader::Beginning);
0462         } else {
0463             option.position = QStyleOptionHeader::Beginning;
0464         }
0465     } else if (orderIndex == m_columns.count() - 1) {
0466         // Paint the header for the last column; check if there is some empty space to the right which needs to be filled.
0467         if (rect.right() < size().width()) {
0468             option.position = QStyleOptionHeader::Middle;
0469             paintPadding(m_columns.count(), QRectF(rect.left(), 0.0, size().width() - rect.left(), rect.height()), QStyleOptionHeader::End);
0470         } else {
0471             option.position = QStyleOptionHeader::End;
0472         }
0473     } else {
0474         option.position = QStyleOptionHeader::Middle;
0475     }
0476 
0477     style()->drawControl(QStyle::CE_Header, &option, painter, widget);
0478 }
0479 
0480 void KItemListHeaderWidget::updatePressedRoleIndex(const QPointF &pos)
0481 {
0482     const int pressedIndex = roleIndexAt(pos);
0483     if (m_pressedRoleIndex != pressedIndex) {
0484         m_pressedRoleIndex = pressedIndex;
0485         update();
0486     }
0487 }
0488 
0489 void KItemListHeaderWidget::updateHoveredIndex(const QPointF &pos)
0490 {
0491     const int hoverIndex = roleIndexAt(pos);
0492 
0493     if (m_hoveredIndex != hoverIndex) {
0494         if (m_hoveredIndex != -1) {
0495             Q_EMIT columnUnHovered(m_hoveredIndex);
0496         }
0497         m_hoveredIndex = hoverIndex;
0498         if (m_hoveredIndex != -1) {
0499             Q_EMIT columnHovered(m_hoveredIndex);
0500         }
0501         update();
0502     }
0503 }
0504 
0505 int KItemListHeaderWidget::roleIndexAt(const QPointF &pos) const
0506 {
0507     int index = -1;
0508 
0509     qreal x = -m_offset + m_sidePadding;
0510     for (const QByteArray &role : std::as_const(m_columns)) {
0511         ++index;
0512         x += m_columnWidths.value(role);
0513         if (pos.x() <= x) {
0514             break;
0515         }
0516     }
0517 
0518     return index;
0519 }
0520 
0521 bool KItemListHeaderWidget::isAboveRoleGrip(const QPointF &pos, int roleIndex) const
0522 {
0523     qreal x = -m_offset + m_sidePadding;
0524     for (int i = 0; i <= roleIndex; ++i) {
0525         const QByteArray role = m_columns[i];
0526         x += m_columnWidths.value(role);
0527     }
0528 
0529     const int grip = style()->pixelMetric(QStyle::PM_HeaderGripMargin);
0530     return pos.x() >= (x - grip) && pos.x() <= x;
0531 }
0532 
0533 bool KItemListHeaderWidget::isAbovePaddingGrip(const QPointF &pos, PaddingGrip paddingGrip) const
0534 {
0535     const qreal lx = -m_offset + m_sidePadding;
0536     const int grip = style()->pixelMetric(QStyle::PM_HeaderGripMargin);
0537 
0538     switch (paddingGrip) {
0539     case Leading:
0540         return pos.x() >= (lx - grip) && pos.x() <= lx;
0541     case Trailing: {
0542         qreal rx = lx;
0543         for (const QByteArray &role : std::as_const(m_columns)) {
0544             rx += m_columnWidths.value(role);
0545         }
0546         return pos.x() >= (rx - grip) && pos.x() <= rx;
0547     }
0548     default:
0549         return false;
0550     }
0551 }
0552 
0553 QPixmap KItemListHeaderWidget::createRolePixmap(int roleIndex) const
0554 {
0555     const QByteArray role = m_columns[roleIndex];
0556     const qreal roleWidth = m_columnWidths.value(role);
0557     const QRect rect(0, 0, roleWidth, size().height());
0558 
0559     QImage image(rect.size(), QImage::Format_ARGB32_Premultiplied);
0560 
0561     QPainter painter(&image);
0562     paintRole(&painter, role, rect, roleIndex);
0563 
0564     // Apply a highlighting-color
0565     const QPalette::ColorGroup group = isActiveWindow() ? QPalette::Active : QPalette::Inactive;
0566     QColor highlightColor = palette().color(group, QPalette::Highlight);
0567     highlightColor.setAlpha(64);
0568     painter.fillRect(rect, highlightColor);
0569 
0570     // Make the image transparent
0571     painter.setCompositionMode(QPainter::CompositionMode_DestinationIn);
0572     painter.fillRect(0, 0, image.width(), image.height(), QColor(0, 0, 0, 192));
0573 
0574     return QPixmap::fromImage(image);
0575 }
0576 
0577 int KItemListHeaderWidget::targetOfMovingRole() const
0578 {
0579     const int movingWidth = m_movingRole.pixmap.width();
0580     const int movingLeft = m_movingRole.x;
0581     const int movingRight = movingLeft + movingWidth - 1;
0582 
0583     int targetIndex = 0;
0584     qreal targetLeft = -m_offset + m_sidePadding;
0585     while (targetIndex < m_columns.count()) {
0586         const QByteArray role = m_columns[targetIndex];
0587         const qreal targetWidth = m_columnWidths.value(role);
0588         const qreal targetRight = targetLeft + targetWidth - 1;
0589 
0590         const bool isInTarget = (targetWidth >= movingWidth && movingLeft >= targetLeft && movingRight <= targetRight)
0591             || (targetWidth < movingWidth && movingLeft <= targetLeft && movingRight >= targetRight);
0592 
0593         if (isInTarget) {
0594             return targetIndex;
0595         }
0596 
0597         targetLeft += targetWidth;
0598         ++targetIndex;
0599     }
0600 
0601     return m_movingRole.index;
0602 }
0603 
0604 qreal KItemListHeaderWidget::roleXPosition(const QByteArray &role) const
0605 {
0606     qreal x = -m_offset + m_sidePadding;
0607     for (const QByteArray &visibleRole : std::as_const(m_columns)) {
0608         if (visibleRole == role) {
0609             return x;
0610         }
0611 
0612         x += m_columnWidths.value(visibleRole);
0613     }
0614 
0615     return -1;
0616 }
0617 
0618 #include "moc_kitemlistheaderwidget.cpp"