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),
23 m_preferredColumnWidths(),
24 m_hoveredRoleIndex(-1),
25 m_pressedRoleIndex(-1),
26 m_roleOperation(NoRoleOperation
),
31 m_movingRole
.xDec
= 0;
32 m_movingRole
.index
= -1;
34 setAcceptHoverEvents(true);
37 KItemListHeaderWidget::~KItemListHeaderWidget()
41 void KItemListHeaderWidget::setModel(KItemModelBase
* model
)
43 if (m_model
== model
) {
48 disconnect(m_model
, &KItemModelBase::sortRoleChanged
,
49 this, &KItemListHeaderWidget::slotSortRoleChanged
);
50 disconnect(m_model
, &KItemModelBase::sortOrderChanged
,
51 this, &KItemListHeaderWidget::slotSortOrderChanged
);
57 connect(m_model
, &KItemModelBase::sortRoleChanged
,
58 this, &KItemListHeaderWidget::slotSortRoleChanged
);
59 connect(m_model
, &KItemModelBase::sortOrderChanged
,
60 this, &KItemListHeaderWidget::slotSortOrderChanged
);
64 KItemModelBase
* KItemListHeaderWidget::model() const
69 void KItemListHeaderWidget::setAutomaticColumnResizing(bool automatic
)
71 m_automaticColumnResizing
= automatic
;
74 bool KItemListHeaderWidget::automaticColumnResizing() const
76 return m_automaticColumnResizing
;
79 void KItemListHeaderWidget::setColumns(const QList
<QByteArray
>& roles
)
81 for (const QByteArray
& role
: roles
) {
82 if (!m_columnWidths
.contains(role
)) {
83 m_preferredColumnWidths
.remove(role
);
91 QList
<QByteArray
> KItemListHeaderWidget::columns() const
96 void KItemListHeaderWidget::setColumnWidth(const QByteArray
& role
, qreal width
)
98 const qreal minWidth
= minimumColumnWidth();
99 if (width
< minWidth
) {
103 if (m_columnWidths
.value(role
) != width
) {
104 m_columnWidths
.insert(role
, width
);
109 qreal
KItemListHeaderWidget::columnWidth(const QByteArray
& role
) const
111 return m_columnWidths
.value(role
);
114 void KItemListHeaderWidget::setPreferredColumnWidth(const QByteArray
& role
, qreal width
)
116 m_preferredColumnWidths
.insert(role
, width
);
119 qreal
KItemListHeaderWidget::preferredColumnWidth(const QByteArray
& role
) const
121 return m_preferredColumnWidths
.value(role
);
124 void KItemListHeaderWidget::setOffset(qreal offset
)
126 if (m_offset
!= offset
) {
132 qreal
KItemListHeaderWidget::offset() const
137 qreal
KItemListHeaderWidget::minimumColumnWidth() const
139 QFontMetricsF
fontMetrics(font());
140 return fontMetrics
.height() * 4;
143 void KItemListHeaderWidget::paint(QPainter
* painter
, const QStyleOptionGraphicsItem
* option
, QWidget
* widget
)
153 painter
->setFont(font());
154 painter
->setPen(palette().text().color());
158 for (const QByteArray
& role
: qAsConst(m_columns
)) {
159 const qreal roleWidth
= m_columnWidths
.value(role
);
160 const QRectF
rect(x
, 0, roleWidth
, size().height());
161 paintRole(painter
, role
, rect
, orderIndex
, widget
);
166 if (!m_movingRole
.pixmap
.isNull()) {
167 Q_ASSERT(m_roleOperation
== MoveRoleOperation
);
168 painter
->drawPixmap(m_movingRole
.x
, 0, m_movingRole
.pixmap
);
172 void KItemListHeaderWidget::mousePressEvent(QGraphicsSceneMouseEvent
* event
)
174 if (event
->button() & Qt::LeftButton
) {
175 updatePressedRoleIndex(event
->pos());
176 m_pressedMousePos
= event
->pos();
177 m_roleOperation
= isAboveRoleGrip(m_pressedMousePos
, m_pressedRoleIndex
) ?
178 ResizeRoleOperation
: NoRoleOperation
;
185 void KItemListHeaderWidget::mouseReleaseEvent(QGraphicsSceneMouseEvent
* event
)
187 QGraphicsWidget::mouseReleaseEvent(event
);
189 if (m_pressedRoleIndex
== -1) {
193 switch (m_roleOperation
) {
194 case NoRoleOperation
: {
195 // Only a click has been done and no moving or resizing has been started
196 const QByteArray sortRole
= m_model
->sortRole();
197 const int sortRoleIndex
= m_columns
.indexOf(sortRole
);
198 if (m_pressedRoleIndex
== sortRoleIndex
) {
199 // Toggle the sort order
200 const Qt::SortOrder previous
= m_model
->sortOrder();
201 const Qt::SortOrder current
= (m_model
->sortOrder() == Qt::AscendingOrder
) ?
202 Qt::DescendingOrder
: Qt::AscendingOrder
;
203 m_model
->setSortOrder(current
);
204 Q_EMIT
sortOrderChanged(current
, previous
);
206 // Change the sort role and reset to the ascending order
207 const QByteArray previous
= m_model
->sortRole();
208 const QByteArray current
= m_columns
[m_pressedRoleIndex
];
209 const bool resetSortOrder
= m_model
->sortOrder() == Qt::DescendingOrder
;
210 m_model
->setSortRole(current
, !resetSortOrder
);
211 Q_EMIT
sortRoleChanged(current
, previous
);
213 if (resetSortOrder
) {
214 m_model
->setSortOrder(Qt::AscendingOrder
);
215 Q_EMIT
sortOrderChanged(Qt::AscendingOrder
, Qt::DescendingOrder
);
221 case ResizeRoleOperation
: {
222 const QByteArray pressedRole
= m_columns
[m_pressedRoleIndex
];
223 const qreal currentWidth
= m_columnWidths
.value(pressedRole
);
224 Q_EMIT
columnWidthChangeFinished(pressedRole
, currentWidth
);
228 case MoveRoleOperation
:
229 m_movingRole
.pixmap
= QPixmap();
231 m_movingRole
.xDec
= 0;
232 m_movingRole
.index
= -1;
239 m_pressedRoleIndex
= -1;
240 m_roleOperation
= NoRoleOperation
;
243 QApplication::restoreOverrideCursor();
246 void KItemListHeaderWidget::mouseMoveEvent(QGraphicsSceneMouseEvent
* event
)
248 QGraphicsWidget::mouseMoveEvent(event
);
250 switch (m_roleOperation
) {
251 case NoRoleOperation
:
252 if ((event
->pos() - m_pressedMousePos
).manhattanLength() >= QApplication::startDragDistance()) {
253 // A role gets dragged by the user. Create a pixmap of the role that will get
254 // synchronized on each further mouse-move-event with the mouse-position.
255 m_roleOperation
= MoveRoleOperation
;
256 const int roleIndex
= roleIndexAt(m_pressedMousePos
);
257 m_movingRole
.index
= roleIndex
;
258 if (roleIndex
== 0) {
259 // TODO: It should be configurable whether moving the first role is allowed.
260 // In the context of Dolphin this is not required, however this should be
261 // changed if KItemViews are used in a more generic way.
262 QApplication::setOverrideCursor(QCursor(Qt::ForbiddenCursor
));
264 m_movingRole
.pixmap
= createRolePixmap(roleIndex
);
266 qreal roleX
= -m_offset
;
267 for (int i
= 0; i
< roleIndex
; ++i
) {
268 const QByteArray role
= m_columns
[i
];
269 roleX
+= m_columnWidths
.value(role
);
272 m_movingRole
.xDec
= event
->pos().x() - roleX
;
273 m_movingRole
.x
= roleX
;
279 case ResizeRoleOperation
: {
280 const QByteArray pressedRole
= m_columns
[m_pressedRoleIndex
];
282 qreal previousWidth
= m_columnWidths
.value(pressedRole
);
283 qreal currentWidth
= previousWidth
;
284 currentWidth
+= event
->pos().x() - event
->lastPos().x();
285 currentWidth
= qMax(minimumColumnWidth(), currentWidth
);
287 m_columnWidths
.insert(pressedRole
, currentWidth
);
290 Q_EMIT
columnWidthChanged(pressedRole
, currentWidth
, previousWidth
);
294 case MoveRoleOperation
: {
295 // TODO: It should be configurable whether moving the first role is allowed.
296 // In the context of Dolphin this is not required, however this should be
297 // changed if KItemViews are used in a more generic way.
298 if (m_movingRole
.index
> 0) {
299 m_movingRole
.x
= event
->pos().x() - m_movingRole
.xDec
;
302 const int targetIndex
= targetOfMovingRole();
303 if (targetIndex
> 0 && targetIndex
!= m_movingRole
.index
) {
304 const QByteArray role
= m_columns
[m_movingRole
.index
];
305 const int previousIndex
= m_movingRole
.index
;
306 m_movingRole
.index
= targetIndex
;
307 Q_EMIT
columnMoved(role
, targetIndex
, previousIndex
);
309 m_movingRole
.xDec
= event
->pos().x() - roleXPosition(role
);
320 void KItemListHeaderWidget::mouseDoubleClickEvent(QGraphicsSceneMouseEvent
* event
)
322 QGraphicsItem::mouseDoubleClickEvent(event
);
324 const int roleIndex
= roleIndexAt(event
->pos());
325 if (roleIndex
>= 0 && isAboveRoleGrip(event
->pos(), roleIndex
)) {
326 const QByteArray role
= m_columns
.at(roleIndex
);
328 qreal previousWidth
= columnWidth(role
);
329 setColumnWidth(role
, preferredColumnWidth(role
));
330 qreal currentWidth
= columnWidth(role
);
332 Q_EMIT
columnWidthChanged(role
, currentWidth
, previousWidth
);
333 Q_EMIT
columnWidthChangeFinished(role
, currentWidth
);
337 void KItemListHeaderWidget::hoverEnterEvent(QGraphicsSceneHoverEvent
* event
)
339 QGraphicsWidget::hoverEnterEvent(event
);
340 updateHoveredRoleIndex(event
->pos());
343 void KItemListHeaderWidget::hoverLeaveEvent(QGraphicsSceneHoverEvent
* event
)
345 QGraphicsWidget::hoverLeaveEvent(event
);
346 if (m_hoveredRoleIndex
!= -1) {
347 m_hoveredRoleIndex
= -1;
352 void KItemListHeaderWidget::hoverMoveEvent(QGraphicsSceneHoverEvent
* event
)
354 QGraphicsWidget::hoverMoveEvent(event
);
356 const QPointF
& pos
= event
->pos();
357 updateHoveredRoleIndex(pos
);
358 if (m_hoveredRoleIndex
>= 0 && isAboveRoleGrip(pos
, m_hoveredRoleIndex
)) {
359 setCursor(Qt::SplitHCursor
);
365 void KItemListHeaderWidget::slotSortRoleChanged(const QByteArray
& current
, const QByteArray
& previous
)
372 void KItemListHeaderWidget::slotSortOrderChanged(Qt::SortOrder current
, Qt::SortOrder previous
)
379 void KItemListHeaderWidget::paintRole(QPainter
* painter
,
380 const QByteArray
& role
,
383 QWidget
* widget
) const
385 // The following code is based on the code from QHeaderView::paintSection().
386 // SPDX-FileCopyrightText: 2011 Nokia Corporation and/or its subsidiary(-ies).
387 QStyleOptionHeader option
;
388 option
.section
= orderIndex
;
389 option
.state
= QStyle::State_None
| QStyle::State_Raised
| QStyle::State_Horizontal
;
391 option
.state
|= QStyle::State_Enabled
;
393 if (window() && window()->isActiveWindow()) {
394 option
.state
|= QStyle::State_Active
;
396 if (m_hoveredRoleIndex
== orderIndex
) {
397 option
.state
|= QStyle::State_MouseOver
;
399 if (m_pressedRoleIndex
== orderIndex
) {
400 option
.state
|= QStyle::State_Sunken
;
402 if (m_model
->sortRole() == role
) {
403 option
.sortIndicator
= (m_model
->sortOrder() == Qt::AscendingOrder
) ?
404 QStyleOptionHeader::SortDown
: QStyleOptionHeader::SortUp
;
406 option
.rect
= rect
.toRect();
408 bool paintBackgroundForEmptyArea
= false;
410 if (m_columns
.count() == 1) {
411 option
.position
= QStyleOptionHeader::OnlyOneSection
;
412 } else if (orderIndex
== 0) {
413 option
.position
= QStyleOptionHeader::Beginning
;
414 } else if (orderIndex
== m_columns
.count() - 1) {
415 // We are just painting the header for the last column. Check if there
416 // is some empty space to the right which needs to be filled.
417 if (rect
.right() < size().width()) {
418 option
.position
= QStyleOptionHeader::Middle
;
419 paintBackgroundForEmptyArea
= true;
421 option
.position
= QStyleOptionHeader::End
;
424 option
.position
= QStyleOptionHeader::Middle
;
427 option
.orientation
= Qt::Horizontal
;
428 option
.selectedPosition
= QStyleOptionHeader::NotAdjacent
;
429 option
.text
= m_model
->roleDescription(role
);
431 style()->drawControl(QStyle::CE_Header
, &option
, painter
, widget
);
433 if (paintBackgroundForEmptyArea
) {
434 option
.state
= QStyle::State_None
| QStyle::State_Raised
| QStyle::State_Horizontal
;
435 option
.section
= m_columns
.count();
436 option
.sortIndicator
= QStyleOptionHeader::None
;
438 qreal backgroundRectX
= rect
.x() + rect
.width();
439 QRectF
backgroundRect(backgroundRectX
, 0.0, size().width() - backgroundRectX
, rect
.height());
440 option
.rect
= backgroundRect
.toRect();
441 option
.position
= QStyleOptionHeader::End
;
442 option
.text
= QString();
444 style()->drawControl(QStyle::CE_Header
, &option
, painter
, widget
);
448 void KItemListHeaderWidget::updatePressedRoleIndex(const QPointF
& pos
)
450 const int pressedIndex
= roleIndexAt(pos
);
451 if (m_pressedRoleIndex
!= pressedIndex
) {
452 m_pressedRoleIndex
= pressedIndex
;
457 void KItemListHeaderWidget::updateHoveredRoleIndex(const QPointF
& pos
)
459 const int hoverIndex
= roleIndexAt(pos
);
460 if (m_hoveredRoleIndex
!= hoverIndex
) {
461 m_hoveredRoleIndex
= hoverIndex
;
466 int KItemListHeaderWidget::roleIndexAt(const QPointF
& pos
) const
471 for (const QByteArray
& role
: qAsConst(m_columns
)) {
473 x
+= m_columnWidths
.value(role
);
482 bool KItemListHeaderWidget::isAboveRoleGrip(const QPointF
& pos
, int roleIndex
) const
485 for (int i
= 0; i
<= roleIndex
; ++i
) {
486 const QByteArray role
= m_columns
[i
];
487 x
+= m_columnWidths
.value(role
);
490 const int grip
= style()->pixelMetric(QStyle::PM_HeaderGripMargin
);
491 return pos
.x() >= (x
- grip
) && pos
.x() <= x
;
494 QPixmap
KItemListHeaderWidget::createRolePixmap(int roleIndex
) const
496 const QByteArray role
= m_columns
[roleIndex
];
497 const qreal roleWidth
= m_columnWidths
.value(role
);
498 const QRect
rect(0, 0, roleWidth
, size().height());
500 QImage
image(rect
.size(), QImage::Format_ARGB32_Premultiplied
);
502 QPainter
painter(&image
);
503 paintRole(&painter
, role
, rect
, roleIndex
);
505 // Apply a highlighting-color
506 const QPalette::ColorGroup group
= isActiveWindow() ? QPalette::Active
: QPalette::Inactive
;
507 QColor highlightColor
= palette().color(group
, QPalette::Highlight
);
508 highlightColor
.setAlpha(64);
509 painter
.fillRect(rect
, highlightColor
);
511 // Make the image transparent
512 painter
.setCompositionMode(QPainter::CompositionMode_DestinationIn
);
513 painter
.fillRect(0, 0, image
.width(), image
.height(), QColor(0, 0, 0, 192));
515 return QPixmap::fromImage(image
);
518 int KItemListHeaderWidget::targetOfMovingRole() const
520 const int movingWidth
= m_movingRole
.pixmap
.width();
521 const int movingLeft
= m_movingRole
.x
;
522 const int movingRight
= movingLeft
+ movingWidth
- 1;
525 qreal targetLeft
= -m_offset
;
526 while (targetIndex
< m_columns
.count()) {
527 const QByteArray role
= m_columns
[targetIndex
];
528 const qreal targetWidth
= m_columnWidths
.value(role
);
529 const qreal targetRight
= targetLeft
+ targetWidth
- 1;
531 const bool isInTarget
= (targetWidth
>= movingWidth
&&
532 movingLeft
>= targetLeft
&&
533 movingRight
<= targetRight
) ||
534 (targetWidth
< movingWidth
&&
535 movingLeft
<= targetLeft
&&
536 movingRight
>= targetRight
);
542 targetLeft
+= targetWidth
;
546 return m_movingRole
.index
;
549 qreal
KItemListHeaderWidget::roleXPosition(const QByteArray
& role
) const
552 for (const QByteArray
& visibleRole
: qAsConst(m_columns
)) {
553 if (visibleRole
== role
) {
557 x
+= m_columnWidths
.value(visibleRole
);