2 * SPDX-FileCopyrightText: 2011 Peter Penz <peter.penz19@gmail.com>
3 * SPDX-FileCopyrightText: 2022, 2024 Felix Ernst <felixernst@kde.org>
5 * SPDX-License-Identifier: GPL-2.0-or-later
8 #include "kitemlistheaderwidget.h"
9 #include "kitemviews/kitemmodelbase.h"
11 #include <QApplication>
12 #include <QGraphicsSceneHoverEvent>
14 #include <QStyleOptionHeader>
19 * @returns a list which has a reversed order of elements compared to @a list.
21 QList
<QByteArray
> reversed(const QList
<QByteArray
> list
)
23 QList
<QByteArray
> reversedList
;
24 for (auto i
= list
.rbegin(); i
!= list
.rend(); i
++) {
25 reversedList
.emplaceBack(*i
);
31 * @returns the index of the column for the name/text of items. This depends on the layoutDirection() and column count of @a itemListHeaderWidget.
33 int nameColumnIndex(const KItemListHeaderWidget
*itemListHeaderWidget
)
35 if (itemListHeaderWidget
->layoutDirection() == Qt::LeftToRight
) {
38 return itemListHeaderWidget
->columns().count() - 1;
42 KItemListHeaderWidget::KItemListHeaderWidget(QGraphicsWidget
*parent
)
43 : QGraphicsWidget(parent
)
44 , m_automaticColumnResizing(true)
51 , m_preferredColumnWidths()
53 , m_pressedRoleIndex(-1)
58 m_movingRole
.xDec
= 0;
59 m_movingRole
.index
= -1;
61 setAcceptHoverEvents(true);
62 // TODO update when font changes at runtime
63 setFont(QApplication::font("QHeaderView"));
66 KItemListHeaderWidget::~KItemListHeaderWidget()
70 void KItemListHeaderWidget::setModel(KItemModelBase
*model
)
72 if (m_model
== model
) {
77 disconnect(m_model
, &KItemModelBase::sortRoleChanged
, this, &KItemListHeaderWidget::slotSortRoleChanged
);
78 disconnect(m_model
, &KItemModelBase::sortOrderChanged
, this, &KItemListHeaderWidget::slotSortOrderChanged
);
84 connect(m_model
, &KItemModelBase::sortRoleChanged
, this, &KItemListHeaderWidget::slotSortRoleChanged
);
85 connect(m_model
, &KItemModelBase::sortOrderChanged
, this, &KItemListHeaderWidget::slotSortOrderChanged
);
89 KItemModelBase
*KItemListHeaderWidget::model() const
94 void KItemListHeaderWidget::setAutomaticColumnResizing(bool automatic
)
96 m_automaticColumnResizing
= automatic
;
99 bool KItemListHeaderWidget::automaticColumnResizing() const
101 return m_automaticColumnResizing
;
104 void KItemListHeaderWidget::setColumns(const QList
<QByteArray
> &roles
)
106 for (const QByteArray
&role
: roles
) {
107 if (!m_columnWidths
.contains(role
)) {
108 m_preferredColumnWidths
.remove(role
);
112 m_columns
= layoutDirection() == Qt::LeftToRight
? roles
: reversed(roles
);
116 QList
<QByteArray
> KItemListHeaderWidget::columns() const
118 return layoutDirection() == Qt::LeftToRight
? m_columns
: reversed(m_columns
);
121 void KItemListHeaderWidget::setColumnWidth(const QByteArray
&role
, qreal width
)
123 const qreal minWidth
= minimumColumnWidth();
124 if (width
< minWidth
) {
128 if (m_columnWidths
.value(role
) != width
) {
129 m_columnWidths
.insert(role
, width
);
134 qreal
KItemListHeaderWidget::columnWidth(const QByteArray
&role
) const
136 return m_columnWidths
.value(role
);
139 void KItemListHeaderWidget::setPreferredColumnWidth(const QByteArray
&role
, qreal width
)
141 m_preferredColumnWidths
.insert(role
, width
);
144 qreal
KItemListHeaderWidget::preferredColumnWidth(const QByteArray
&role
) const
146 return m_preferredColumnWidths
.value(role
);
149 void KItemListHeaderWidget::setOffset(qreal offset
)
151 if (m_offset
!= offset
) {
157 qreal
KItemListHeaderWidget::offset() const
162 void KItemListHeaderWidget::setSidePadding(qreal leftPaddingWidth
, qreal rightPaddingWidth
)
164 bool changed
= false;
165 if (m_leftPadding
!= leftPaddingWidth
) {
166 m_leftPadding
= leftPaddingWidth
;
170 if (m_rightPadding
!= rightPaddingWidth
) {
171 m_rightPadding
= rightPaddingWidth
;
179 Q_EMIT
sidePaddingChanged(leftPaddingWidth
, rightPaddingWidth
);
183 qreal
KItemListHeaderWidget::leftPadding() const
185 return m_leftPadding
;
188 qreal
KItemListHeaderWidget::rightPadding() const
190 return m_rightPadding
;
193 qreal
KItemListHeaderWidget::minimumColumnWidth() const
195 QFontMetricsF
fontMetrics(font());
196 return fontMetrics
.height() * 4;
199 void KItemListHeaderWidget::paint(QPainter
*painter
, const QStyleOptionGraphicsItem
*option
, QWidget
*widget
)
209 painter
->setFont(font());
210 painter
->setPen(palette().text().color());
212 qreal x
= -m_offset
+ m_leftPadding
+ unusedSpace();
214 for (const QByteArray
&role
: std::as_const(m_columns
)) {
215 const qreal roleWidth
= m_columnWidths
.value(role
);
216 const QRectF
rect(x
, 0, roleWidth
, size().height());
217 paintRole(painter
, role
, rect
, orderIndex
, widget
);
222 if (!m_movingRole
.pixmap
.isNull()) {
223 painter
->drawPixmap(m_movingRole
.x
, 0, m_movingRole
.pixmap
);
227 void KItemListHeaderWidget::mousePressEvent(QGraphicsSceneMouseEvent
*event
)
229 if (event
->button() & Qt::LeftButton
) {
230 m_pressedMousePos
= event
->pos();
231 m_pressedGrip
= isAboveResizeGrip(m_pressedMousePos
);
232 if (!m_pressedGrip
) {
233 updatePressedRoleIndex(event
->pos());
241 void KItemListHeaderWidget::mouseReleaseEvent(QGraphicsSceneMouseEvent
*event
)
243 QGraphicsWidget::mouseReleaseEvent(event
);
246 // Emitting a column width change removes automatic column resizing, so we do not emit if only the padding is being changed.
247 // Eception: In mouseMoveEvent() we also resize the last column if the right padding is at zero but the user still quickly resizes beyond the screen
248 // boarder. Such a resize "of the right padding" is let through when automatic column resizing was disabled by that resize.
249 if (m_pressedGrip
->roleToTheLeft
!= "leftPadding" && (m_pressedGrip
->roleToTheRight
!= "rightPadding" || !m_automaticColumnResizing
)) {
250 const qreal currentWidth
= m_columnWidths
.value(m_pressedGrip
->roleToTheLeft
);
251 Q_EMIT
columnWidthChangeFinished(m_pressedGrip
->roleToTheLeft
, currentWidth
);
253 } else if (m_pressedRoleIndex
!= -1 && m_movingRole
.index
== -1) {
254 // Only a click has been done and no moving or resizing has been started
255 const QByteArray sortRole
= m_model
->sortRole();
256 const int sortRoleIndex
= m_columns
.indexOf(sortRole
);
257 if (m_pressedRoleIndex
== sortRoleIndex
) {
258 // Toggle the sort order
259 const Qt::SortOrder previous
= m_model
->sortOrder();
260 const Qt::SortOrder current
= (m_model
->sortOrder() == Qt::AscendingOrder
) ? Qt::DescendingOrder
: Qt::AscendingOrder
;
261 m_model
->setSortOrder(current
);
262 Q_EMIT
sortOrderChanged(current
, previous
);
264 // Change the sort role and reset to the ascending order
265 const QByteArray previous
= m_model
->sortRole();
266 const QByteArray current
= m_columns
[m_pressedRoleIndex
];
267 const bool resetSortOrder
= m_model
->sortOrder() == Qt::DescendingOrder
;
268 m_model
->setSortRole(current
, !resetSortOrder
);
269 Q_EMIT
sortRoleChanged(current
, previous
);
271 if (resetSortOrder
) {
272 m_model
->setSortOrder(Qt::AscendingOrder
);
273 Q_EMIT
sortOrderChanged(Qt::AscendingOrder
, Qt::DescendingOrder
);
278 m_movingRole
.pixmap
= QPixmap();
280 m_movingRole
.xDec
= 0;
281 m_movingRole
.index
= -1;
283 m_pressedGrip
= std::nullopt
;
284 m_pressedRoleIndex
= -1;
287 QApplication::restoreOverrideCursor();
290 void KItemListHeaderWidget::mouseMoveEvent(QGraphicsSceneMouseEvent
*event
)
292 QGraphicsWidget::mouseMoveEvent(event
);
295 if (m_pressedGrip
->roleToTheLeft
== "leftPadding") {
296 qreal currentWidth
= m_leftPadding
;
297 currentWidth
+= event
->pos().x() - event
->lastPos().x();
298 m_leftPadding
= qMax(0.0, currentWidth
);
301 Q_EMIT
sidePaddingChanged(m_leftPadding
, m_rightPadding
);
305 if (m_pressedGrip
->roleToTheRight
== "rightPadding") {
306 qreal currentWidth
= m_rightPadding
;
307 currentWidth
-= event
->pos().x() - event
->lastPos().x();
308 m_rightPadding
= qMax(0.0, currentWidth
);
311 Q_EMIT
sidePaddingChanged(m_leftPadding
, m_rightPadding
);
312 if (m_rightPadding
> 0.0) {
315 // Continue so resizing of the last column beyond the view width is possible.
316 if (currentWidth
> -10) {
317 return; // Automatic column resizing is valuable, so we don't want to give it up just for a few pixels of extra width for the rightmost column.
319 m_automaticColumnResizing
= false;
322 qreal previousWidth
= m_columnWidths
.value(m_pressedGrip
->roleToTheLeft
);
323 qreal currentWidth
= previousWidth
;
324 currentWidth
+= event
->pos().x() - event
->lastPos().x();
325 currentWidth
= qMax(minimumColumnWidth(), currentWidth
);
327 m_columnWidths
.insert(m_pressedGrip
->roleToTheLeft
, currentWidth
);
330 Q_EMIT
columnWidthChanged(m_pressedGrip
->roleToTheLeft
, currentWidth
, previousWidth
);
334 if (m_movingRole
.index
!= -1) {
335 // TODO: It should be configurable whether moving the first role is allowed.
336 // In the context of Dolphin this is not required, however this should be
337 // changed if KItemViews are used in a more generic way.
338 if (m_movingRole
.index
!= nameColumnIndex(this)) {
339 m_movingRole
.x
= event
->pos().x() - m_movingRole
.xDec
;
342 const int targetIndex
= targetOfMovingRole();
343 if (targetIndex
> 0 && targetIndex
!= m_movingRole
.index
) {
344 const QByteArray role
= m_columns
[m_movingRole
.index
];
345 const int previousIndex
= m_movingRole
.index
;
346 m_movingRole
.index
= targetIndex
;
347 if (layoutDirection() == Qt::LeftToRight
) {
348 Q_EMIT
columnMoved(role
, targetIndex
, previousIndex
);
350 Q_EMIT
columnMoved(role
, m_columns
.count() - 1 - targetIndex
, m_columns
.count() - 1 - previousIndex
);
353 m_movingRole
.xDec
= event
->pos().x() - roleXPosition(role
);
359 if ((event
->pos() - m_pressedMousePos
).manhattanLength() >= QApplication::startDragDistance()) {
360 // A role gets dragged by the user. Create a pixmap of the role that will get
361 // synchronized on each further mouse-move-event with the mouse-position.
362 const int roleIndex
= roleIndexAt(m_pressedMousePos
);
363 m_movingRole
.index
= roleIndex
;
364 if (roleIndex
== nameColumnIndex(this)) {
365 // TODO: It should be configurable whether moving the first role is allowed.
366 // In the context of Dolphin this is not required, however this should be
367 // changed if KItemViews are used in a more generic way.
368 QApplication::setOverrideCursor(QCursor(Qt::ForbiddenCursor
));
372 m_movingRole
.pixmap
= createRolePixmap(roleIndex
);
374 qreal roleX
= -m_offset
+ m_leftPadding
+ unusedSpace();
375 for (int i
= 0; i
< roleIndex
; ++i
) {
376 const QByteArray role
= m_columns
[i
];
377 roleX
+= m_columnWidths
.value(role
);
380 m_movingRole
.xDec
= event
->pos().x() - roleX
;
381 m_movingRole
.x
= roleX
;
386 void KItemListHeaderWidget::mouseDoubleClickEvent(QGraphicsSceneMouseEvent
*event
)
388 QGraphicsItem::mouseDoubleClickEvent(event
);
390 const std::optional
<Grip
> doubleClickedGrip
= isAboveResizeGrip(event
->pos());
391 if (!doubleClickedGrip
|| doubleClickedGrip
->roleToTheLeft
.isEmpty()) {
395 qreal previousWidth
= columnWidth(doubleClickedGrip
->roleToTheLeft
);
396 setColumnWidth(doubleClickedGrip
->roleToTheLeft
, preferredColumnWidth(doubleClickedGrip
->roleToTheLeft
));
397 qreal currentWidth
= columnWidth(doubleClickedGrip
->roleToTheLeft
);
399 Q_EMIT
columnWidthChanged(doubleClickedGrip
->roleToTheLeft
, currentWidth
, previousWidth
);
400 Q_EMIT
columnWidthChangeFinished(doubleClickedGrip
->roleToTheLeft
, currentWidth
);
403 void KItemListHeaderWidget::hoverEnterEvent(QGraphicsSceneHoverEvent
*event
)
405 QGraphicsWidget::hoverEnterEvent(event
);
406 updateHoveredIndex(event
->pos());
409 void KItemListHeaderWidget::hoverLeaveEvent(QGraphicsSceneHoverEvent
*event
)
411 QGraphicsWidget::hoverLeaveEvent(event
);
412 if (m_hoveredIndex
!= -1) {
413 Q_EMIT
columnUnHovered(m_hoveredIndex
);
419 void KItemListHeaderWidget::hoverMoveEvent(QGraphicsSceneHoverEvent
*event
)
421 QGraphicsWidget::hoverMoveEvent(event
);
423 const QPointF
&pos
= event
->pos();
424 updateHoveredIndex(pos
);
425 if (isAboveResizeGrip(pos
)) {
426 setCursor(Qt::SplitHCursor
);
432 void KItemListHeaderWidget::slotSortRoleChanged(const QByteArray
¤t
, const QByteArray
&previous
)
439 void KItemListHeaderWidget::slotSortOrderChanged(Qt::SortOrder current
, Qt::SortOrder previous
)
446 void KItemListHeaderWidget::paintRole(QPainter
*painter
, const QByteArray
&role
, const QRectF
&rect
, int orderIndex
, QWidget
*widget
) const
448 // The following code is based on the code from QHeaderView::paintSection().
449 // SPDX-FileCopyrightText: 2011 Nokia Corporation and/or its subsidiary(-ies).
450 QStyleOptionHeader option
;
451 option
.section
= orderIndex
;
452 option
.state
= QStyle::State_None
| QStyle::State_Raised
| QStyle::State_Horizontal
;
454 option
.state
|= QStyle::State_Enabled
;
456 if (window() && window()->isActiveWindow()) {
457 option
.state
|= QStyle::State_Active
;
459 if (m_hoveredIndex
== orderIndex
) {
460 option
.state
|= QStyle::State_MouseOver
;
462 if (m_pressedRoleIndex
== orderIndex
) {
463 option
.state
|= QStyle::State_Sunken
;
465 if (m_model
->sortRole() == role
) {
466 option
.sortIndicator
= (m_model
->sortOrder() == Qt::AscendingOrder
) ? QStyleOptionHeader::SortDown
: QStyleOptionHeader::SortUp
;
468 option
.rect
= rect
.toRect();
469 option
.orientation
= Qt::Horizontal
;
470 option
.selectedPosition
= QStyleOptionHeader::NotAdjacent
;
471 option
.text
= m_model
->roleDescription(role
);
473 // First we paint any potential empty (padding) space on left and/or right of this role's column.
474 const auto paintPadding
= [&](int section
, const QRectF
&rect
, const QStyleOptionHeader::SectionPosition
&pos
) {
475 QStyleOptionHeader padding
;
476 padding
.state
= QStyle::State_None
| QStyle::State_Raised
| QStyle::State_Horizontal
;
477 padding
.section
= section
;
478 padding
.sortIndicator
= QStyleOptionHeader::None
;
479 padding
.rect
= rect
.toRect();
480 padding
.position
= pos
;
481 padding
.text
= QString();
482 style()->drawControl(QStyle::CE_Header
, &padding
, painter
, widget
);
485 if (m_columns
.count() == 1) {
486 option
.position
= QStyleOptionHeader::Middle
;
487 paintPadding(0, QRectF(0.0, 0.0, rect
.left(), rect
.height()), QStyleOptionHeader::Beginning
);
488 paintPadding(1, QRectF(rect
.right(), 0.0, size().width() - rect
.right(), rect
.height()), QStyleOptionHeader::End
);
489 } else if (orderIndex
== 0) {
490 // Paint the header for the first column; check if there is some empty space to the left which needs to be filled.
491 if (rect
.left() > 0) {
492 option
.position
= QStyleOptionHeader::Middle
;
493 paintPadding(0, QRectF(0.0, 0.0, rect
.left(), rect
.height()), QStyleOptionHeader::Beginning
);
495 option
.position
= QStyleOptionHeader::Beginning
;
497 } else if (orderIndex
== m_columns
.count() - 1) {
498 // Paint the header for the last column; check if there is some empty space to the right which needs to be filled.
499 if (rect
.right() < size().width()) {
500 option
.position
= QStyleOptionHeader::Middle
;
501 paintPadding(m_columns
.count(), QRectF(rect
.right(), 0.0, size().width() - rect
.right(), rect
.height()), QStyleOptionHeader::End
);
503 option
.position
= QStyleOptionHeader::End
;
506 option
.position
= QStyleOptionHeader::Middle
;
509 style()->drawControl(QStyle::CE_Header
, &option
, painter
, widget
);
512 void KItemListHeaderWidget::updatePressedRoleIndex(const QPointF
&pos
)
514 const int pressedIndex
= roleIndexAt(pos
);
515 if (m_pressedRoleIndex
!= pressedIndex
) {
516 m_pressedRoleIndex
= pressedIndex
;
521 void KItemListHeaderWidget::updateHoveredIndex(const QPointF
&pos
)
523 const int hoverIndex
= isAboveResizeGrip(pos
) ? -1 : roleIndexAt(pos
);
525 if (m_hoveredIndex
!= hoverIndex
) {
526 if (m_hoveredIndex
!= -1) {
527 Q_EMIT
columnUnHovered(m_hoveredIndex
);
529 m_hoveredIndex
= hoverIndex
;
530 if (m_hoveredIndex
!= -1) {
531 Q_EMIT
columnHovered(m_hoveredIndex
);
537 int KItemListHeaderWidget::roleIndexAt(const QPointF
&pos
) const
539 qreal x
= -m_offset
+ m_leftPadding
+ unusedSpace();
545 for (const QByteArray
&role
: std::as_const(m_columns
)) {
547 x
+= m_columnWidths
.value(role
);
556 std::optional
<const KItemListHeaderWidget::Grip
> KItemListHeaderWidget::isAboveResizeGrip(const QPointF
&position
) const
558 qreal x
= -m_offset
+ m_leftPadding
+ unusedSpace();
559 const int gripWidthTolerance
= style()->pixelMetric(QStyle::PM_HeaderGripMargin
);
561 if (x
- gripWidthTolerance
< position
.x() && position
.x() < x
+ gripWidthTolerance
) {
562 return std::optional
{Grip
{"leftPadding", m_columns
[0]}};
565 for (int i
= 0; i
< m_columns
.count(); ++i
) {
566 const QByteArray role
= m_columns
[i
];
567 x
+= m_columnWidths
.value(role
);
568 if (x
- gripWidthTolerance
< position
.x() && position
.x() < x
+ gripWidthTolerance
) {
569 if (i
+ 1 < m_columns
.count()) {
570 return std::optional
{Grip
{m_columns
[i
], m_columns
[i
+ 1]}};
572 return std::optional
{Grip
{m_columns
[i
], "rightPadding"}};
578 QPixmap
KItemListHeaderWidget::createRolePixmap(int roleIndex
) const
580 const QByteArray role
= m_columns
[roleIndex
];
581 const qreal roleWidth
= m_columnWidths
.value(role
);
582 const QRect
rect(0, 0, roleWidth
, size().height());
584 QImage
image(rect
.size(), QImage::Format_ARGB32_Premultiplied
);
586 QPainter
painter(&image
);
587 paintRole(&painter
, role
, rect
, roleIndex
);
589 // Apply a highlighting-color
590 const QPalette::ColorGroup group
= isActiveWindow() ? QPalette::Active
: QPalette::Inactive
;
591 QColor highlightColor
= palette().color(group
, QPalette::Highlight
);
592 highlightColor
.setAlpha(64);
593 painter
.fillRect(rect
, highlightColor
);
595 // Make the image transparent
596 painter
.setCompositionMode(QPainter::CompositionMode_DestinationIn
);
597 painter
.fillRect(0, 0, image
.width(), image
.height(), QColor(0, 0, 0, 192));
599 return QPixmap::fromImage(image
);
602 int KItemListHeaderWidget::targetOfMovingRole() const
604 const int movingWidth
= m_movingRole
.pixmap
.width();
605 const int movingLeft
= m_movingRole
.x
;
606 const int movingRight
= movingLeft
+ movingWidth
- 1;
609 qreal targetLeft
= -m_offset
+ m_leftPadding
+ unusedSpace();
610 while (targetIndex
< m_columns
.count()) {
611 const QByteArray role
= m_columns
[targetIndex
];
612 const qreal targetWidth
= m_columnWidths
.value(role
);
613 const qreal targetRight
= targetLeft
+ targetWidth
- 1;
615 const bool isInTarget
= (targetWidth
>= movingWidth
&& movingLeft
>= targetLeft
&& movingRight
<= targetRight
)
616 || (targetWidth
< movingWidth
&& movingLeft
<= targetLeft
&& movingRight
>= targetRight
);
622 targetLeft
+= targetWidth
;
626 return m_movingRole
.index
;
629 qreal
KItemListHeaderWidget::roleXPosition(const QByteArray
&role
) const
631 qreal x
= -m_offset
+ m_leftPadding
+ unusedSpace();
632 for (const QByteArray
&visibleRole
: std::as_const(m_columns
)) {
633 if (visibleRole
== role
) {
637 x
+= m_columnWidths
.value(visibleRole
);
643 qreal
KItemListHeaderWidget::unusedSpace() const
645 if (layoutDirection() == Qt::LeftToRight
) {
648 int unusedSpace
= size().width() - m_leftPadding
- m_rightPadding
;
649 for (int i
= 0; i
< m_columns
.count(); ++i
) {
650 const QByteArray role
= m_columns
[i
];
651 unusedSpace
-= m_columnWidths
.value(role
);
653 return qMax(unusedSpace
, 0);
656 #include "moc_kitemlistheaderwidget.cpp"