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 ¤t, 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"