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>
15 KItemListHeaderWidget::KItemListHeaderWidget(QGraphicsWidget
*parent
)
16 : QGraphicsWidget(parent
)
17 , m_automaticColumnResizing(true)
23 , m_preferredColumnWidths()
25 , m_pressedRoleIndex(-1)
26 , m_roleOperation(NoRoleOperation
)
31 m_movingRole
.xDec
= 0;
32 m_movingRole
.index
= -1;
34 setAcceptHoverEvents(true);
35 // TODO update when font changes at runtime
36 setFont(QApplication::font("QHeaderView"));
39 KItemListHeaderWidget::~KItemListHeaderWidget()
43 void KItemListHeaderWidget::setModel(KItemModelBase
*model
)
45 if (m_model
== model
) {
50 disconnect(m_model
, &KItemModelBase::sortRoleChanged
, this, &KItemListHeaderWidget::slotSortRoleChanged
);
51 disconnect(m_model
, &KItemModelBase::sortOrderChanged
, this, &KItemListHeaderWidget::slotSortOrderChanged
);
57 connect(m_model
, &KItemModelBase::sortRoleChanged
, this, &KItemListHeaderWidget::slotSortRoleChanged
);
58 connect(m_model
, &KItemModelBase::sortOrderChanged
, this, &KItemListHeaderWidget::slotSortOrderChanged
);
62 KItemModelBase
*KItemListHeaderWidget::model() const
67 void KItemListHeaderWidget::setAutomaticColumnResizing(bool automatic
)
69 m_automaticColumnResizing
= automatic
;
72 bool KItemListHeaderWidget::automaticColumnResizing() const
74 return m_automaticColumnResizing
;
77 void KItemListHeaderWidget::setColumns(const QList
<QByteArray
> &roles
)
79 for (const QByteArray
&role
: roles
) {
80 if (!m_columnWidths
.contains(role
)) {
81 m_preferredColumnWidths
.remove(role
);
89 QList
<QByteArray
> KItemListHeaderWidget::columns() const
94 void KItemListHeaderWidget::setColumnWidth(const QByteArray
&role
, qreal width
)
96 const qreal minWidth
= minimumColumnWidth();
97 if (width
< minWidth
) {
101 if (m_columnWidths
.value(role
) != width
) {
102 m_columnWidths
.insert(role
, width
);
107 qreal
KItemListHeaderWidget::columnWidth(const QByteArray
&role
) const
109 return m_columnWidths
.value(role
);
112 void KItemListHeaderWidget::setPreferredColumnWidth(const QByteArray
&role
, qreal width
)
114 m_preferredColumnWidths
.insert(role
, width
);
117 qreal
KItemListHeaderWidget::preferredColumnWidth(const QByteArray
&role
) const
119 return m_preferredColumnWidths
.value(role
);
122 void KItemListHeaderWidget::setOffset(qreal offset
)
124 if (m_offset
!= offset
) {
130 qreal
KItemListHeaderWidget::offset() const
135 void KItemListHeaderWidget::setSidePadding(qreal width
)
137 if (m_sidePadding
!= width
) {
138 m_sidePadding
= width
;
139 sidePaddingChanged(width
);
144 qreal
KItemListHeaderWidget::sidePadding() const
146 return m_sidePadding
;
149 qreal
KItemListHeaderWidget::minimumColumnWidth() const
151 QFontMetricsF
fontMetrics(font());
152 return fontMetrics
.height() * 4;
155 void KItemListHeaderWidget::paint(QPainter
*painter
, const QStyleOptionGraphicsItem
*option
, QWidget
*widget
)
165 painter
->setFont(font());
166 painter
->setPen(palette().text().color());
168 qreal x
= -m_offset
+ m_sidePadding
;
170 for (const QByteArray
&role
: std::as_const(m_columns
)) {
171 const qreal roleWidth
= m_columnWidths
.value(role
);
172 const QRectF
rect(x
, 0, roleWidth
, size().height());
173 paintRole(painter
, role
, rect
, orderIndex
, widget
);
178 if (!m_movingRole
.pixmap
.isNull()) {
179 Q_ASSERT(m_roleOperation
== MoveRoleOperation
);
180 painter
->drawPixmap(m_movingRole
.x
, 0, m_movingRole
.pixmap
);
184 void KItemListHeaderWidget::mousePressEvent(QGraphicsSceneMouseEvent
*event
)
186 if (event
->button() & Qt::LeftButton
) {
187 m_pressedMousePos
= event
->pos();
188 if (isAbovePaddingGrip(m_pressedMousePos
, PaddingGrip::Leading
)) {
189 m_roleOperation
= ResizePaddingColumnOperation
;
191 updatePressedRoleIndex(event
->pos());
192 m_roleOperation
= isAboveRoleGrip(m_pressedMousePos
, m_pressedRoleIndex
) ? ResizeRoleOperation
: NoRoleOperation
;
200 void KItemListHeaderWidget::mouseReleaseEvent(QGraphicsSceneMouseEvent
*event
)
202 QGraphicsWidget::mouseReleaseEvent(event
);
204 if (m_pressedRoleIndex
== -1) {
208 switch (m_roleOperation
) {
209 case NoRoleOperation
: {
210 // Only a click has been done and no moving or resizing has been started
211 const QByteArray sortRole
= m_model
->sortRole();
212 const int sortRoleIndex
= m_columns
.indexOf(sortRole
);
213 if (m_pressedRoleIndex
== sortRoleIndex
) {
214 // Toggle the sort order
215 const Qt::SortOrder previous
= m_model
->sortOrder();
216 const Qt::SortOrder current
= (m_model
->sortOrder() == Qt::AscendingOrder
) ? Qt::DescendingOrder
: Qt::AscendingOrder
;
217 m_model
->setSortOrder(current
);
218 Q_EMIT
sortOrderChanged(current
, previous
);
220 // Change the sort role and reset to the ascending order
221 const QByteArray previous
= m_model
->sortRole();
222 const QByteArray current
= m_columns
[m_pressedRoleIndex
];
223 const bool resetSortOrder
= m_model
->sortOrder() == Qt::DescendingOrder
;
224 m_model
->setSortRole(current
, !resetSortOrder
);
225 Q_EMIT
sortRoleChanged(current
, previous
);
227 if (resetSortOrder
) {
228 m_model
->setSortOrder(Qt::AscendingOrder
);
229 Q_EMIT
sortOrderChanged(Qt::AscendingOrder
, Qt::DescendingOrder
);
235 case ResizeRoleOperation
: {
236 const QByteArray pressedRole
= m_columns
[m_pressedRoleIndex
];
237 const qreal currentWidth
= m_columnWidths
.value(pressedRole
);
238 Q_EMIT
columnWidthChangeFinished(pressedRole
, currentWidth
);
242 case MoveRoleOperation
:
243 m_movingRole
.pixmap
= QPixmap();
245 m_movingRole
.xDec
= 0;
246 m_movingRole
.index
= -1;
253 m_pressedRoleIndex
= -1;
254 m_roleOperation
= NoRoleOperation
;
257 QApplication::restoreOverrideCursor();
260 void KItemListHeaderWidget::mouseMoveEvent(QGraphicsSceneMouseEvent
*event
)
262 QGraphicsWidget::mouseMoveEvent(event
);
264 switch (m_roleOperation
) {
265 case NoRoleOperation
:
266 if ((event
->pos() - m_pressedMousePos
).manhattanLength() >= QApplication::startDragDistance()) {
267 // A role gets dragged by the user. Create a pixmap of the role that will get
268 // synchronized on each further mouse-move-event with the mouse-position.
269 m_roleOperation
= MoveRoleOperation
;
270 const int roleIndex
= roleIndexAt(m_pressedMousePos
);
271 m_movingRole
.index
= roleIndex
;
272 if (roleIndex
== 0) {
273 // TODO: It should be configurable whether moving the first role is allowed.
274 // In the context of Dolphin this is not required, however this should be
275 // changed if KItemViews are used in a more generic way.
276 QApplication::setOverrideCursor(QCursor(Qt::ForbiddenCursor
));
278 m_movingRole
.pixmap
= createRolePixmap(roleIndex
);
280 qreal roleX
= -m_offset
+ m_sidePadding
;
281 for (int i
= 0; i
< roleIndex
; ++i
) {
282 const QByteArray role
= m_columns
[i
];
283 roleX
+= m_columnWidths
.value(role
);
286 m_movingRole
.xDec
= event
->pos().x() - roleX
;
287 m_movingRole
.x
= roleX
;
293 case ResizeRoleOperation
: {
294 const QByteArray pressedRole
= m_columns
[m_pressedRoleIndex
];
296 qreal previousWidth
= m_columnWidths
.value(pressedRole
);
297 qreal currentWidth
= previousWidth
;
298 currentWidth
+= event
->pos().x() - event
->lastPos().x();
299 currentWidth
= qMax(minimumColumnWidth(), currentWidth
);
301 m_columnWidths
.insert(pressedRole
, currentWidth
);
304 Q_EMIT
columnWidthChanged(pressedRole
, currentWidth
, previousWidth
);
308 case ResizePaddingColumnOperation
: {
309 qreal currentWidth
= m_sidePadding
;
310 currentWidth
+= event
->pos().x() - event
->lastPos().x();
311 currentWidth
= qMax(0.0, currentWidth
);
313 m_sidePadding
= currentWidth
;
317 Q_EMIT
sidePaddingChanged(currentWidth
);
322 case MoveRoleOperation
: {
323 // TODO: It should be configurable whether moving the first role is allowed.
324 // In the context of Dolphin this is not required, however this should be
325 // changed if KItemViews are used in a more generic way.
326 if (m_movingRole
.index
> 0) {
327 m_movingRole
.x
= event
->pos().x() - m_movingRole
.xDec
;
330 const int targetIndex
= targetOfMovingRole();
331 if (targetIndex
> 0 && targetIndex
!= m_movingRole
.index
) {
332 const QByteArray role
= m_columns
[m_movingRole
.index
];
333 const int previousIndex
= m_movingRole
.index
;
334 m_movingRole
.index
= targetIndex
;
335 Q_EMIT
columnMoved(role
, targetIndex
, previousIndex
);
337 m_movingRole
.xDec
= event
->pos().x() - roleXPosition(role
);
348 void KItemListHeaderWidget::mouseDoubleClickEvent(QGraphicsSceneMouseEvent
*event
)
350 QGraphicsItem::mouseDoubleClickEvent(event
);
352 const int roleIndex
= roleIndexAt(event
->pos());
353 if (roleIndex
>= 0 && isAboveRoleGrip(event
->pos(), roleIndex
)) {
354 const QByteArray role
= m_columns
.at(roleIndex
);
356 qreal previousWidth
= columnWidth(role
);
357 setColumnWidth(role
, preferredColumnWidth(role
));
358 qreal currentWidth
= columnWidth(role
);
360 Q_EMIT
columnWidthChanged(role
, currentWidth
, previousWidth
);
361 Q_EMIT
columnWidthChangeFinished(role
, currentWidth
);
365 void KItemListHeaderWidget::hoverEnterEvent(QGraphicsSceneHoverEvent
*event
)
367 QGraphicsWidget::hoverEnterEvent(event
);
368 updateHoveredIndex(event
->pos());
371 void KItemListHeaderWidget::hoverLeaveEvent(QGraphicsSceneHoverEvent
*event
)
373 QGraphicsWidget::hoverLeaveEvent(event
);
374 if (m_hoveredIndex
!= -1) {
375 Q_EMIT
columnUnHovered(m_hoveredIndex
);
381 void KItemListHeaderWidget::hoverMoveEvent(QGraphicsSceneHoverEvent
*event
)
383 QGraphicsWidget::hoverMoveEvent(event
);
385 const QPointF
&pos
= event
->pos();
386 updateHoveredIndex(pos
);
387 if ((m_hoveredIndex
>= 0 && isAboveRoleGrip(pos
, m_hoveredIndex
)) || isAbovePaddingGrip(pos
, PaddingGrip::Leading
)
388 || isAbovePaddingGrip(pos
, PaddingGrip::Trailing
)) {
389 setCursor(Qt::SplitHCursor
);
395 void KItemListHeaderWidget::slotSortRoleChanged(const QByteArray
¤t
, const QByteArray
&previous
)
402 void KItemListHeaderWidget::slotSortOrderChanged(Qt::SortOrder current
, Qt::SortOrder previous
)
409 void KItemListHeaderWidget::paintRole(QPainter
*painter
, const QByteArray
&role
, const QRectF
&rect
, int orderIndex
, QWidget
*widget
) const
411 const auto direction
= widget
? widget
->layoutDirection() : qApp
->layoutDirection();
413 // The following code is based on the code from QHeaderView::paintSection().
414 // SPDX-FileCopyrightText: 2011 Nokia Corporation and/or its subsidiary(-ies).
415 QStyleOptionHeader option
;
416 option
.direction
= direction
;
417 option
.textAlignment
= direction
== Qt::LeftToRight
? Qt::AlignLeft
: Qt::AlignRight
;
419 option
.section
= orderIndex
;
420 option
.state
= QStyle::State_None
| QStyle::State_Raised
| QStyle::State_Horizontal
;
422 option
.state
|= QStyle::State_Enabled
;
424 if (window() && window()->isActiveWindow()) {
425 option
.state
|= QStyle::State_Active
;
427 if (m_hoveredIndex
== orderIndex
) {
428 option
.state
|= QStyle::State_MouseOver
;
430 if (m_pressedRoleIndex
== orderIndex
) {
431 option
.state
|= QStyle::State_Sunken
;
433 if (m_model
->sortRole() == role
) {
434 option
.sortIndicator
= (m_model
->sortOrder() == Qt::AscendingOrder
) ? QStyleOptionHeader::SortDown
: QStyleOptionHeader::SortUp
;
436 option
.rect
= rect
.toRect();
437 option
.orientation
= Qt::Horizontal
;
438 option
.selectedPosition
= QStyleOptionHeader::NotAdjacent
;
439 option
.text
= m_model
->roleDescription(role
);
441 // First we paint any potential empty (padding) space on left and/or right of this role's column.
442 const auto paintPadding
= [&](int section
, const QRectF
&rect
, const QStyleOptionHeader::SectionPosition
&pos
) {
443 QStyleOptionHeader padding
;
444 padding
.state
= QStyle::State_None
| QStyle::State_Raised
| QStyle::State_Horizontal
;
445 padding
.section
= section
;
446 padding
.sortIndicator
= QStyleOptionHeader::None
;
447 padding
.rect
= rect
.toRect();
448 padding
.position
= pos
;
449 padding
.text
= QString();
450 style()->drawControl(QStyle::CE_Header
, &padding
, painter
, widget
);
453 if (m_columns
.count() == 1) {
454 option
.position
= QStyleOptionHeader::Middle
;
455 paintPadding(0, QRectF(0.0, 0.0, rect
.left(), rect
.height()), QStyleOptionHeader::Beginning
);
456 paintPadding(1, QRectF(rect
.left(), 0.0, size().width() - rect
.left(), rect
.height()), QStyleOptionHeader::End
);
457 } else if (orderIndex
== 0) {
458 // Paint the header for the first column; check if there is some empty space to the left which needs to be filled.
459 if (rect
.left() > 0) {
460 option
.position
= QStyleOptionHeader::Middle
;
461 paintPadding(0, QRectF(0.0, 0.0, rect
.left(), rect
.height()), QStyleOptionHeader::Beginning
);
463 option
.position
= QStyleOptionHeader::Beginning
;
465 } else if (orderIndex
== m_columns
.count() - 1) {
466 // Paint the header for the last column; check if there is some empty space to the right which needs to be filled.
467 if (rect
.right() < size().width()) {
468 option
.position
= QStyleOptionHeader::Middle
;
469 paintPadding(m_columns
.count(), QRectF(rect
.left(), 0.0, size().width() - rect
.left(), rect
.height()), QStyleOptionHeader::End
);
471 option
.position
= QStyleOptionHeader::End
;
474 option
.position
= QStyleOptionHeader::Middle
;
477 style()->drawControl(QStyle::CE_Header
, &option
, painter
, widget
);
480 void KItemListHeaderWidget::updatePressedRoleIndex(const QPointF
&pos
)
482 const int pressedIndex
= roleIndexAt(pos
);
483 if (m_pressedRoleIndex
!= pressedIndex
) {
484 m_pressedRoleIndex
= pressedIndex
;
489 void KItemListHeaderWidget::updateHoveredIndex(const QPointF
&pos
)
491 const int hoverIndex
= roleIndexAt(pos
);
493 if (m_hoveredIndex
!= hoverIndex
) {
494 if (m_hoveredIndex
!= -1) {
495 Q_EMIT
columnUnHovered(m_hoveredIndex
);
497 m_hoveredIndex
= hoverIndex
;
498 if (m_hoveredIndex
!= -1) {
499 Q_EMIT
columnHovered(m_hoveredIndex
);
505 int KItemListHeaderWidget::roleIndexAt(const QPointF
&pos
) const
509 qreal x
= -m_offset
+ m_sidePadding
;
510 for (const QByteArray
&role
: std::as_const(m_columns
)) {
512 x
+= m_columnWidths
.value(role
);
521 bool KItemListHeaderWidget::isAboveRoleGrip(const QPointF
&pos
, int roleIndex
) const
523 qreal x
= -m_offset
+ m_sidePadding
;
524 for (int i
= 0; i
<= roleIndex
; ++i
) {
525 const QByteArray role
= m_columns
[i
];
526 x
+= m_columnWidths
.value(role
);
529 const int grip
= style()->pixelMetric(QStyle::PM_HeaderGripMargin
);
530 return pos
.x() >= (x
- grip
) && pos
.x() <= x
;
533 bool KItemListHeaderWidget::isAbovePaddingGrip(const QPointF
&pos
, PaddingGrip paddingGrip
) const
535 const qreal lx
= -m_offset
+ m_sidePadding
;
536 const int grip
= style()->pixelMetric(QStyle::PM_HeaderGripMargin
);
538 switch (paddingGrip
) {
540 return pos
.x() >= (lx
- grip
) && pos
.x() <= lx
;
543 for (const QByteArray
&role
: std::as_const(m_columns
)) {
544 rx
+= m_columnWidths
.value(role
);
546 return pos
.x() >= (rx
- grip
) && pos
.x() <= rx
;
553 QPixmap
KItemListHeaderWidget::createRolePixmap(int roleIndex
) const
555 const QByteArray role
= m_columns
[roleIndex
];
556 const qreal roleWidth
= m_columnWidths
.value(role
);
557 const QRect
rect(0, 0, roleWidth
, size().height());
559 QImage
image(rect
.size(), QImage::Format_ARGB32_Premultiplied
);
561 QPainter
painter(&image
);
562 paintRole(&painter
, role
, rect
, roleIndex
);
564 // Apply a highlighting-color
565 const QPalette::ColorGroup group
= isActiveWindow() ? QPalette::Active
: QPalette::Inactive
;
566 QColor highlightColor
= palette().color(group
, QPalette::Highlight
);
567 highlightColor
.setAlpha(64);
568 painter
.fillRect(rect
, highlightColor
);
570 // Make the image transparent
571 painter
.setCompositionMode(QPainter::CompositionMode_DestinationIn
);
572 painter
.fillRect(0, 0, image
.width(), image
.height(), QColor(0, 0, 0, 192));
574 return QPixmap::fromImage(image
);
577 int KItemListHeaderWidget::targetOfMovingRole() const
579 const int movingWidth
= m_movingRole
.pixmap
.width();
580 const int movingLeft
= m_movingRole
.x
;
581 const int movingRight
= movingLeft
+ movingWidth
- 1;
584 qreal targetLeft
= -m_offset
+ m_sidePadding
;
585 while (targetIndex
< m_columns
.count()) {
586 const QByteArray role
= m_columns
[targetIndex
];
587 const qreal targetWidth
= m_columnWidths
.value(role
);
588 const qreal targetRight
= targetLeft
+ targetWidth
- 1;
590 const bool isInTarget
= (targetWidth
>= movingWidth
&& movingLeft
>= targetLeft
&& movingRight
<= targetRight
)
591 || (targetWidth
< movingWidth
&& movingLeft
<= targetLeft
&& movingRight
>= targetRight
);
597 targetLeft
+= targetWidth
;
601 return m_movingRole
.index
;
604 qreal
KItemListHeaderWidget::roleXPosition(const QByteArray
&role
) const
606 qreal x
= -m_offset
+ m_sidePadding
;
607 for (const QByteArray
&visibleRole
: std::as_const(m_columns
)) {
608 if (visibleRole
== role
) {
612 x
+= m_columnWidths
.value(visibleRole
);
618 #include "moc_kitemlistheaderwidget.cpp"