2 * SPDX-FileCopyrightText: 2011 Peter Penz <peter.penz19@gmail.com>
4 * SPDX-License-Identifier: GPL-2.0-or-later
7 #include "kitemlistheaderwidget.h"
8 #include "kitemviews/kitemmodelbase.h"
10 #include <QApplication>
11 #include <QGraphicsSceneHoverEvent>
13 #include <QStyleOptionHeader>
16 KItemListHeaderWidget::KItemListHeaderWidget(QGraphicsWidget
* parent
) :
17 QGraphicsWidget(parent
),
18 m_automaticColumnResizing(true),
24 m_preferredColumnWidths(),
25 m_hoveredRoleIndex(-1),
26 m_pressedRoleIndex(-1),
27 m_roleOperation(NoRoleOperation
),
32 m_movingRole
.xDec
= 0;
33 m_movingRole
.index
= -1;
35 setAcceptHoverEvents(true);
36 // TODO update when font changes at runtime
37 setFont(QApplication::font("QHeaderView"));
40 KItemListHeaderWidget::~KItemListHeaderWidget()
44 void KItemListHeaderWidget::setModel(KItemModelBase
* model
)
46 if (m_model
== model
) {
51 disconnect(m_model
, &KItemModelBase::sortRoleChanged
,
52 this, &KItemListHeaderWidget::slotSortRoleChanged
);
53 disconnect(m_model
, &KItemModelBase::sortOrderChanged
,
54 this, &KItemListHeaderWidget::slotSortOrderChanged
);
60 connect(m_model
, &KItemModelBase::sortRoleChanged
,
61 this, &KItemListHeaderWidget::slotSortRoleChanged
);
62 connect(m_model
, &KItemModelBase::sortOrderChanged
,
63 this, &KItemListHeaderWidget::slotSortOrderChanged
);
67 KItemModelBase
* KItemListHeaderWidget::model() const
72 void KItemListHeaderWidget::setAutomaticColumnResizing(bool automatic
)
74 m_automaticColumnResizing
= automatic
;
77 bool KItemListHeaderWidget::automaticColumnResizing() const
79 return m_automaticColumnResizing
;
82 void KItemListHeaderWidget::setColumns(const QList
<QByteArray
>& roles
)
84 for (const QByteArray
& role
: roles
) {
85 if (!m_columnWidths
.contains(role
)) {
86 m_preferredColumnWidths
.remove(role
);
94 QList
<QByteArray
> KItemListHeaderWidget::columns() const
99 void KItemListHeaderWidget::setColumnWidth(const QByteArray
& role
, qreal width
)
101 const qreal minWidth
= minimumColumnWidth();
102 if (width
< minWidth
) {
106 if (m_columnWidths
.value(role
) != width
) {
107 m_columnWidths
.insert(role
, width
);
112 qreal
KItemListHeaderWidget::columnWidth(const QByteArray
& role
) const
114 return m_columnWidths
.value(role
);
117 void KItemListHeaderWidget::setPreferredColumnWidth(const QByteArray
& role
, qreal width
)
119 m_preferredColumnWidths
.insert(role
, width
);
122 qreal
KItemListHeaderWidget::preferredColumnWidth(const QByteArray
& role
) const
124 return m_preferredColumnWidths
.value(role
);
127 void KItemListHeaderWidget::setOffset(qreal offset
)
129 if (m_offset
!= offset
) {
135 qreal
KItemListHeaderWidget::offset() const
140 void KItemListHeaderWidget::setSidePadding(qreal width
)
142 if (m_sidePadding
!= width
) {
143 m_sidePadding
= width
;
144 sidePaddingChanged(width
);
149 qreal
KItemListHeaderWidget::sidePadding() const
151 return m_sidePadding
;
154 qreal
KItemListHeaderWidget::minimumColumnWidth() const
156 QFontMetricsF
fontMetrics(font());
157 return fontMetrics
.height() * 4;
160 void KItemListHeaderWidget::paint(QPainter
* painter
, const QStyleOptionGraphicsItem
* option
, QWidget
* widget
)
170 painter
->setFont(font());
171 painter
->setPen(palette().text().color());
173 qreal x
= -m_offset
+ m_sidePadding
;
175 for (const QByteArray
& role
: qAsConst(m_columns
)) {
176 const qreal roleWidth
= m_columnWidths
.value(role
);
177 const QRectF
rect(x
, 0, roleWidth
, size().height());
178 paintRole(painter
, role
, rect
, orderIndex
, widget
);
183 if (!m_movingRole
.pixmap
.isNull()) {
184 Q_ASSERT(m_roleOperation
== MoveRoleOperation
);
185 painter
->drawPixmap(m_movingRole
.x
, 0, m_movingRole
.pixmap
);
189 void KItemListHeaderWidget::mousePressEvent(QGraphicsSceneMouseEvent
* event
)
191 if (event
->button() & Qt::LeftButton
) {
192 m_pressedMousePos
= event
->pos();
193 if (isAbovePaddingGrip(m_pressedMousePos
, PaddingGrip::Leading
)) {
194 m_roleOperation
= ResizePaddingColumnOperation
;
196 updatePressedRoleIndex(event
->pos());
197 m_roleOperation
= isAboveRoleGrip(m_pressedMousePos
, m_pressedRoleIndex
) ?
198 ResizeRoleOperation
: NoRoleOperation
;
206 void KItemListHeaderWidget::mouseReleaseEvent(QGraphicsSceneMouseEvent
* event
)
208 QGraphicsWidget::mouseReleaseEvent(event
);
210 if (m_pressedRoleIndex
== -1) {
214 switch (m_roleOperation
) {
215 case NoRoleOperation
: {
216 // Only a click has been done and no moving or resizing has been started
217 const QByteArray sortRole
= m_model
->sortRole();
218 const int sortRoleIndex
= m_columns
.indexOf(sortRole
);
219 if (m_pressedRoleIndex
== sortRoleIndex
) {
220 // Toggle the sort order
221 const Qt::SortOrder previous
= m_model
->sortOrder();
222 const Qt::SortOrder current
= (m_model
->sortOrder() == Qt::AscendingOrder
) ?
223 Qt::DescendingOrder
: Qt::AscendingOrder
;
224 m_model
->setSortOrder(current
);
225 Q_EMIT
sortOrderChanged(current
, previous
);
227 // Change the sort role and reset to the ascending order
228 const QByteArray previous
= m_model
->sortRole();
229 const QByteArray current
= m_columns
[m_pressedRoleIndex
];
230 const bool resetSortOrder
= m_model
->sortOrder() == Qt::DescendingOrder
;
231 m_model
->setSortRole(current
, !resetSortOrder
);
232 Q_EMIT
sortRoleChanged(current
, previous
);
234 if (resetSortOrder
) {
235 m_model
->setSortOrder(Qt::AscendingOrder
);
236 Q_EMIT
sortOrderChanged(Qt::AscendingOrder
, Qt::DescendingOrder
);
242 case ResizeRoleOperation
: {
243 const QByteArray pressedRole
= m_columns
[m_pressedRoleIndex
];
244 const qreal currentWidth
= m_columnWidths
.value(pressedRole
);
245 Q_EMIT
columnWidthChangeFinished(pressedRole
, currentWidth
);
249 case MoveRoleOperation
:
250 m_movingRole
.pixmap
= QPixmap();
252 m_movingRole
.xDec
= 0;
253 m_movingRole
.index
= -1;
260 m_pressedRoleIndex
= -1;
261 m_roleOperation
= NoRoleOperation
;
264 QApplication::restoreOverrideCursor();
267 void KItemListHeaderWidget::mouseMoveEvent(QGraphicsSceneMouseEvent
* event
)
269 QGraphicsWidget::mouseMoveEvent(event
);
271 switch (m_roleOperation
) {
272 case NoRoleOperation
:
273 if ((event
->pos() - m_pressedMousePos
).manhattanLength() >= QApplication::startDragDistance()) {
274 // A role gets dragged by the user. Create a pixmap of the role that will get
275 // synchronized on each further mouse-move-event with the mouse-position.
276 m_roleOperation
= MoveRoleOperation
;
277 const int roleIndex
= roleIndexAt(m_pressedMousePos
);
278 m_movingRole
.index
= roleIndex
;
279 if (roleIndex
== 0) {
280 // TODO: It should be configurable whether moving the first role is allowed.
281 // In the context of Dolphin this is not required, however this should be
282 // changed if KItemViews are used in a more generic way.
283 QApplication::setOverrideCursor(QCursor(Qt::ForbiddenCursor
));
285 m_movingRole
.pixmap
= createRolePixmap(roleIndex
);
287 qreal roleX
= -m_offset
+ m_sidePadding
;
288 for (int i
= 0; i
< roleIndex
; ++i
) {
289 const QByteArray role
= m_columns
[i
];
290 roleX
+= m_columnWidths
.value(role
);
293 m_movingRole
.xDec
= event
->pos().x() - roleX
;
294 m_movingRole
.x
= roleX
;
300 case ResizeRoleOperation
: {
301 const QByteArray pressedRole
= m_columns
[m_pressedRoleIndex
];
303 qreal previousWidth
= m_columnWidths
.value(pressedRole
);
304 qreal currentWidth
= previousWidth
;
305 currentWidth
+= event
->pos().x() - event
->lastPos().x();
306 currentWidth
= qMax(minimumColumnWidth(), currentWidth
);
308 m_columnWidths
.insert(pressedRole
, currentWidth
);
311 Q_EMIT
columnWidthChanged(pressedRole
, currentWidth
, previousWidth
);
315 case ResizePaddingColumnOperation
: {
316 qreal currentWidth
= m_sidePadding
;
317 currentWidth
+= event
->pos().x() - event
->lastPos().x();
318 currentWidth
= qMax(0.0, currentWidth
);
320 m_sidePadding
= currentWidth
;
324 Q_EMIT
sidePaddingChanged(currentWidth
);
329 case MoveRoleOperation
: {
330 // TODO: It should be configurable whether moving the first role is allowed.
331 // In the context of Dolphin this is not required, however this should be
332 // changed if KItemViews are used in a more generic way.
333 if (m_movingRole
.index
> 0) {
334 m_movingRole
.x
= event
->pos().x() - m_movingRole
.xDec
;
337 const int targetIndex
= targetOfMovingRole();
338 if (targetIndex
> 0 && targetIndex
!= m_movingRole
.index
) {
339 const QByteArray role
= m_columns
[m_movingRole
.index
];
340 const int previousIndex
= m_movingRole
.index
;
341 m_movingRole
.index
= targetIndex
;
342 Q_EMIT
columnMoved(role
, targetIndex
, previousIndex
);
344 m_movingRole
.xDec
= event
->pos().x() - roleXPosition(role
);
355 void KItemListHeaderWidget::mouseDoubleClickEvent(QGraphicsSceneMouseEvent
* event
)
357 QGraphicsItem::mouseDoubleClickEvent(event
);
359 const int roleIndex
= roleIndexAt(event
->pos());
360 if (roleIndex
>= 0 && isAboveRoleGrip(event
->pos(), roleIndex
)) {
361 const QByteArray role
= m_columns
.at(roleIndex
);
363 qreal previousWidth
= columnWidth(role
);
364 setColumnWidth(role
, preferredColumnWidth(role
));
365 qreal currentWidth
= columnWidth(role
);
367 Q_EMIT
columnWidthChanged(role
, currentWidth
, previousWidth
);
368 Q_EMIT
columnWidthChangeFinished(role
, currentWidth
);
372 void KItemListHeaderWidget::hoverEnterEvent(QGraphicsSceneHoverEvent
* event
)
374 QGraphicsWidget::hoverEnterEvent(event
);
375 updateHoveredRoleIndex(event
->pos());
378 void KItemListHeaderWidget::hoverLeaveEvent(QGraphicsSceneHoverEvent
* event
)
380 QGraphicsWidget::hoverLeaveEvent(event
);
381 if (m_hoveredRoleIndex
!= -1) {
382 m_hoveredRoleIndex
= -1;
387 void KItemListHeaderWidget::hoverMoveEvent(QGraphicsSceneHoverEvent
* event
)
389 QGraphicsWidget::hoverMoveEvent(event
);
391 const QPointF
& pos
= event
->pos();
392 updateHoveredRoleIndex(pos
);
393 if ((m_hoveredRoleIndex
>= 0 && isAboveRoleGrip(pos
, m_hoveredRoleIndex
)) ||
394 isAbovePaddingGrip(pos
, PaddingGrip::Leading
) ||
395 isAbovePaddingGrip(pos
, PaddingGrip::Trailing
)) {
396 setCursor(Qt::SplitHCursor
);
402 void KItemListHeaderWidget::slotSortRoleChanged(const QByteArray
& current
, const QByteArray
& previous
)
409 void KItemListHeaderWidget::slotSortOrderChanged(Qt::SortOrder current
, Qt::SortOrder previous
)
416 void KItemListHeaderWidget::paintRole(QPainter
* painter
,
417 const QByteArray
& role
,
420 QWidget
* widget
) const
422 const auto direction
= widget
? widget
->layoutDirection() : qApp
->layoutDirection();
424 // The following code is based on the code from QHeaderView::paintSection().
425 // SPDX-FileCopyrightText: 2011 Nokia Corporation and/or its subsidiary(-ies).
426 QStyleOptionHeader option
;
427 option
.direction
= direction
;
428 option
.textAlignment
=
429 direction
== Qt::LeftToRight
433 option
.section
= orderIndex
;
434 option
.state
= QStyle::State_None
| QStyle::State_Raised
| QStyle::State_Horizontal
;
436 option
.state
|= QStyle::State_Enabled
;
438 if (window() && window()->isActiveWindow()) {
439 option
.state
|= QStyle::State_Active
;
441 if (m_hoveredRoleIndex
== orderIndex
) {
442 option
.state
|= QStyle::State_MouseOver
;
444 if (m_pressedRoleIndex
== orderIndex
) {
445 option
.state
|= QStyle::State_Sunken
;
447 if (m_model
->sortRole() == role
) {
448 option
.sortIndicator
= (m_model
->sortOrder() == Qt::AscendingOrder
) ?
449 QStyleOptionHeader::SortDown
: QStyleOptionHeader::SortUp
;
451 option
.rect
= rect
.toRect();
452 option
.orientation
= Qt::Horizontal
;
453 option
.selectedPosition
= QStyleOptionHeader::NotAdjacent
;
454 option
.text
= m_model
->roleDescription(role
);
456 // First we paint any potential empty (padding) space on left and/or right of this role's column.
457 const auto paintPadding
= [&](int section
, const QRectF
&rect
, const QStyleOptionHeader::SectionPosition
&pos
){
458 QStyleOptionHeader padding
;
459 padding
.state
= QStyle::State_None
| QStyle::State_Raised
| QStyle::State_Horizontal
;
460 padding
.section
= section
;
461 padding
.sortIndicator
= QStyleOptionHeader::None
;
462 padding
.rect
= rect
.toRect();
463 padding
.position
= pos
;
464 padding
.text
= QString();
465 style()->drawControl(QStyle::CE_Header
, &padding
, painter
, widget
);
468 if (m_columns
.count() == 1) {
469 option
.position
= QStyleOptionHeader::Middle
;
470 paintPadding(0, QRectF(0.0, 0.0, rect
.left(), rect
.height()), QStyleOptionHeader::Beginning
);
471 paintPadding(1, QRectF(rect
.left(), 0.0, size().width() - rect
.left(), rect
.height()), QStyleOptionHeader::End
);
472 } else if (orderIndex
== 0) {
473 // Paint the header for the first column; check if there is some empty space to the left which needs to be filled.
474 if (rect
.left() > 0) {
475 option
.position
= QStyleOptionHeader::Middle
;
476 paintPadding(0,QRectF(0.0, 0.0, rect
.left(), rect
.height()), QStyleOptionHeader::Beginning
);
478 option
.position
= QStyleOptionHeader::Beginning
;
480 } else if (orderIndex
== m_columns
.count() - 1) {
481 // Paint the header for the last column; check if there is some empty space to the right which needs to be filled.
482 if (rect
.right() < size().width()) {
483 option
.position
= QStyleOptionHeader::Middle
;
484 paintPadding(m_columns
.count(), QRectF(rect
.left(), 0.0, size().width() - rect
.left(), rect
.height()), QStyleOptionHeader::End
);
486 option
.position
= QStyleOptionHeader::End
;
489 option
.position
= QStyleOptionHeader::Middle
;
492 style()->drawControl(QStyle::CE_Header
, &option
, painter
, widget
);
495 void KItemListHeaderWidget::updatePressedRoleIndex(const QPointF
& pos
)
497 const int pressedIndex
= roleIndexAt(pos
);
498 if (m_pressedRoleIndex
!= pressedIndex
) {
499 m_pressedRoleIndex
= pressedIndex
;
504 void KItemListHeaderWidget::updateHoveredRoleIndex(const QPointF
& pos
)
506 const int hoverIndex
= roleIndexAt(pos
);
507 if (m_hoveredRoleIndex
!= hoverIndex
) {
508 m_hoveredRoleIndex
= hoverIndex
;
513 int KItemListHeaderWidget::roleIndexAt(const QPointF
& pos
) const
517 qreal x
= -m_offset
+ m_sidePadding
;
518 for (const QByteArray
& role
: qAsConst(m_columns
)) {
520 x
+= m_columnWidths
.value(role
);
529 bool KItemListHeaderWidget::isAboveRoleGrip(const QPointF
& pos
, int roleIndex
) const
531 qreal x
= -m_offset
+ m_sidePadding
;
532 for (int i
= 0; i
<= roleIndex
; ++i
) {
533 const QByteArray role
= m_columns
[i
];
534 x
+= m_columnWidths
.value(role
);
537 const int grip
= style()->pixelMetric(QStyle::PM_HeaderGripMargin
);
538 return pos
.x() >= (x
- grip
) && pos
.x() <= x
;
541 bool KItemListHeaderWidget::isAbovePaddingGrip(const QPointF
& pos
, PaddingGrip paddingGrip
) const
543 const qreal lx
= -m_offset
+ m_sidePadding
;
544 const int grip
= style()->pixelMetric(QStyle::PM_HeaderGripMargin
);
546 switch (paddingGrip
) {
548 return pos
.x() >= (lx
- grip
) && pos
.x() <= lx
;
552 for (const QByteArray
& role
: qAsConst(m_columns
)) {
553 rx
+= m_columnWidths
.value(role
);
555 return pos
.x() >= (rx
- grip
) && pos
.x() <= rx
;
562 QPixmap
KItemListHeaderWidget::createRolePixmap(int roleIndex
) const
564 const QByteArray role
= m_columns
[roleIndex
];
565 const qreal roleWidth
= m_columnWidths
.value(role
);
566 const QRect
rect(0, 0, roleWidth
, size().height());
568 QImage
image(rect
.size(), QImage::Format_ARGB32_Premultiplied
);
570 QPainter
painter(&image
);
571 paintRole(&painter
, role
, rect
, roleIndex
);
573 // Apply a highlighting-color
574 const QPalette::ColorGroup group
= isActiveWindow() ? QPalette::Active
: QPalette::Inactive
;
575 QColor highlightColor
= palette().color(group
, QPalette::Highlight
);
576 highlightColor
.setAlpha(64);
577 painter
.fillRect(rect
, highlightColor
);
579 // Make the image transparent
580 painter
.setCompositionMode(QPainter::CompositionMode_DestinationIn
);
581 painter
.fillRect(0, 0, image
.width(), image
.height(), QColor(0, 0, 0, 192));
583 return QPixmap::fromImage(image
);
586 int KItemListHeaderWidget::targetOfMovingRole() const
588 const int movingWidth
= m_movingRole
.pixmap
.width();
589 const int movingLeft
= m_movingRole
.x
;
590 const int movingRight
= movingLeft
+ movingWidth
- 1;
593 qreal targetLeft
= -m_offset
+ m_sidePadding
;
594 while (targetIndex
< m_columns
.count()) {
595 const QByteArray role
= m_columns
[targetIndex
];
596 const qreal targetWidth
= m_columnWidths
.value(role
);
597 const qreal targetRight
= targetLeft
+ targetWidth
- 1;
599 const bool isInTarget
= (targetWidth
>= movingWidth
&&
600 movingLeft
>= targetLeft
&&
601 movingRight
<= targetRight
) ||
602 (targetWidth
< movingWidth
&&
603 movingLeft
<= targetLeft
&&
604 movingRight
>= targetRight
);
610 targetLeft
+= targetWidth
;
614 return m_movingRole
.index
;
617 qreal
KItemListHeaderWidget::roleXPosition(const QByteArray
& role
) const
619 qreal x
= -m_offset
+ m_sidePadding
;
620 for (const QByteArray
& visibleRole
: qAsConst(m_columns
)) {
621 if (visibleRole
== role
) {
625 x
+= m_columnWidths
.value(visibleRole
);