]> cloud.milkyroute.net Git - dolphin.git/blob - src/kitemviews/private/kitemlistheaderwidget.cpp
Replace qAsConst with std::as_const
[dolphin.git] / src / kitemviews / private / kitemlistheaderwidget.cpp
1 /*
2 * SPDX-FileCopyrightText: 2011 Peter Penz <peter.penz19@gmail.com>
3 *
4 * SPDX-License-Identifier: GPL-2.0-or-later
5 */
6
7 #include "kitemlistheaderwidget.h"
8 #include "kitemviews/kitemmodelbase.h"
9
10 #include <QApplication>
11 #include <QGraphicsSceneHoverEvent>
12 #include <QPainter>
13 #include <QStyleOptionHeader>
14
15 KItemListHeaderWidget::KItemListHeaderWidget(QGraphicsWidget *parent)
16 : QGraphicsWidget(parent)
17 , m_automaticColumnResizing(true)
18 , m_model(nullptr)
19 , m_offset(0)
20 , m_sidePadding(0)
21 , m_columns()
22 , m_columnWidths()
23 , m_preferredColumnWidths()
24 , m_hoveredIndex(-1)
25 , m_pressedRoleIndex(-1)
26 , m_roleOperation(NoRoleOperation)
27 , m_pressedMousePos()
28 , m_movingRole()
29 {
30 m_movingRole.x = 0;
31 m_movingRole.xDec = 0;
32 m_movingRole.index = -1;
33
34 setAcceptHoverEvents(true);
35 // TODO update when font changes at runtime
36 setFont(QApplication::font("QHeaderView"));
37 }
38
39 KItemListHeaderWidget::~KItemListHeaderWidget()
40 {
41 }
42
43 void KItemListHeaderWidget::setModel(KItemModelBase *model)
44 {
45 if (m_model == model) {
46 return;
47 }
48
49 if (m_model) {
50 disconnect(m_model, &KItemModelBase::sortRoleChanged, this, &KItemListHeaderWidget::slotSortRoleChanged);
51 disconnect(m_model, &KItemModelBase::sortOrderChanged, this, &KItemListHeaderWidget::slotSortOrderChanged);
52 }
53
54 m_model = model;
55
56 if (m_model) {
57 connect(m_model, &KItemModelBase::sortRoleChanged, this, &KItemListHeaderWidget::slotSortRoleChanged);
58 connect(m_model, &KItemModelBase::sortOrderChanged, this, &KItemListHeaderWidget::slotSortOrderChanged);
59 }
60 }
61
62 KItemModelBase *KItemListHeaderWidget::model() const
63 {
64 return m_model;
65 }
66
67 void KItemListHeaderWidget::setAutomaticColumnResizing(bool automatic)
68 {
69 m_automaticColumnResizing = automatic;
70 }
71
72 bool KItemListHeaderWidget::automaticColumnResizing() const
73 {
74 return m_automaticColumnResizing;
75 }
76
77 void KItemListHeaderWidget::setColumns(const QList<QByteArray> &roles)
78 {
79 for (const QByteArray &role : roles) {
80 if (!m_columnWidths.contains(role)) {
81 m_preferredColumnWidths.remove(role);
82 }
83 }
84
85 m_columns = roles;
86 update();
87 }
88
89 QList<QByteArray> KItemListHeaderWidget::columns() const
90 {
91 return m_columns;
92 }
93
94 void KItemListHeaderWidget::setColumnWidth(const QByteArray &role, qreal width)
95 {
96 const qreal minWidth = minimumColumnWidth();
97 if (width < minWidth) {
98 width = minWidth;
99 }
100
101 if (m_columnWidths.value(role) != width) {
102 m_columnWidths.insert(role, width);
103 update();
104 }
105 }
106
107 qreal KItemListHeaderWidget::columnWidth(const QByteArray &role) const
108 {
109 return m_columnWidths.value(role);
110 }
111
112 void KItemListHeaderWidget::setPreferredColumnWidth(const QByteArray &role, qreal width)
113 {
114 m_preferredColumnWidths.insert(role, width);
115 }
116
117 qreal KItemListHeaderWidget::preferredColumnWidth(const QByteArray &role) const
118 {
119 return m_preferredColumnWidths.value(role);
120 }
121
122 void KItemListHeaderWidget::setOffset(qreal offset)
123 {
124 if (m_offset != offset) {
125 m_offset = offset;
126 update();
127 }
128 }
129
130 qreal KItemListHeaderWidget::offset() const
131 {
132 return m_offset;
133 }
134
135 void KItemListHeaderWidget::setSidePadding(qreal width)
136 {
137 if (m_sidePadding != width) {
138 m_sidePadding = width;
139 sidePaddingChanged(width);
140 update();
141 }
142 }
143
144 qreal KItemListHeaderWidget::sidePadding() const
145 {
146 return m_sidePadding;
147 }
148
149 qreal KItemListHeaderWidget::minimumColumnWidth() const
150 {
151 QFontMetricsF fontMetrics(font());
152 return fontMetrics.height() * 4;
153 }
154
155 void KItemListHeaderWidget::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
156 {
157 Q_UNUSED(option)
158 Q_UNUSED(widget)
159
160 if (!m_model) {
161 return;
162 }
163
164 // Draw roles
165 painter->setFont(font());
166 painter->setPen(palette().text().color());
167
168 qreal x = -m_offset + m_sidePadding;
169 int orderIndex = 0;
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);
174 x += roleWidth;
175 ++orderIndex;
176 }
177
178 if (!m_movingRole.pixmap.isNull()) {
179 Q_ASSERT(m_roleOperation == MoveRoleOperation);
180 painter->drawPixmap(m_movingRole.x, 0, m_movingRole.pixmap);
181 }
182 }
183
184 void KItemListHeaderWidget::mousePressEvent(QGraphicsSceneMouseEvent *event)
185 {
186 if (event->button() & Qt::LeftButton) {
187 m_pressedMousePos = event->pos();
188 if (isAbovePaddingGrip(m_pressedMousePos, PaddingGrip::Leading)) {
189 m_roleOperation = ResizePaddingColumnOperation;
190 } else {
191 updatePressedRoleIndex(event->pos());
192 m_roleOperation = isAboveRoleGrip(m_pressedMousePos, m_pressedRoleIndex) ? ResizeRoleOperation : NoRoleOperation;
193 }
194 event->accept();
195 } else {
196 event->ignore();
197 }
198 }
199
200 void KItemListHeaderWidget::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
201 {
202 QGraphicsWidget::mouseReleaseEvent(event);
203
204 if (m_pressedRoleIndex == -1) {
205 return;
206 }
207
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);
219 } else {
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);
226
227 if (resetSortOrder) {
228 m_model->setSortOrder(Qt::AscendingOrder);
229 Q_EMIT sortOrderChanged(Qt::AscendingOrder, Qt::DescendingOrder);
230 }
231 }
232 break;
233 }
234
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);
239 break;
240 }
241
242 case MoveRoleOperation:
243 m_movingRole.pixmap = QPixmap();
244 m_movingRole.x = 0;
245 m_movingRole.xDec = 0;
246 m_movingRole.index = -1;
247 break;
248
249 default:
250 break;
251 }
252
253 m_pressedRoleIndex = -1;
254 m_roleOperation = NoRoleOperation;
255 update();
256
257 QApplication::restoreOverrideCursor();
258 }
259
260 void KItemListHeaderWidget::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
261 {
262 QGraphicsWidget::mouseMoveEvent(event);
263
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));
277 } else {
278 m_movingRole.pixmap = createRolePixmap(roleIndex);
279
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);
284 }
285
286 m_movingRole.xDec = event->pos().x() - roleX;
287 m_movingRole.x = roleX;
288 update();
289 }
290 }
291 break;
292
293 case ResizeRoleOperation: {
294 const QByteArray pressedRole = m_columns[m_pressedRoleIndex];
295
296 qreal previousWidth = m_columnWidths.value(pressedRole);
297 qreal currentWidth = previousWidth;
298 currentWidth += event->pos().x() - event->lastPos().x();
299 currentWidth = qMax(minimumColumnWidth(), currentWidth);
300
301 m_columnWidths.insert(pressedRole, currentWidth);
302 update();
303
304 Q_EMIT columnWidthChanged(pressedRole, currentWidth, previousWidth);
305 break;
306 }
307
308 case ResizePaddingColumnOperation: {
309 qreal currentWidth = m_sidePadding;
310 currentWidth += event->pos().x() - event->lastPos().x();
311 currentWidth = qMax(0.0, currentWidth);
312
313 m_sidePadding = currentWidth;
314
315 update();
316
317 Q_EMIT sidePaddingChanged(currentWidth);
318
319 break;
320 }
321
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;
328 update();
329
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);
336
337 m_movingRole.xDec = event->pos().x() - roleXPosition(role);
338 }
339 }
340 break;
341 }
342
343 default:
344 break;
345 }
346 }
347
348 void KItemListHeaderWidget::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
349 {
350 QGraphicsItem::mouseDoubleClickEvent(event);
351
352 const int roleIndex = roleIndexAt(event->pos());
353 if (roleIndex >= 0 && isAboveRoleGrip(event->pos(), roleIndex)) {
354 const QByteArray role = m_columns.at(roleIndex);
355
356 qreal previousWidth = columnWidth(role);
357 setColumnWidth(role, preferredColumnWidth(role));
358 qreal currentWidth = columnWidth(role);
359
360 Q_EMIT columnWidthChanged(role, currentWidth, previousWidth);
361 Q_EMIT columnWidthChangeFinished(role, currentWidth);
362 }
363 }
364
365 void KItemListHeaderWidget::hoverEnterEvent(QGraphicsSceneHoverEvent *event)
366 {
367 QGraphicsWidget::hoverEnterEvent(event);
368 updateHoveredIndex(event->pos());
369 }
370
371 void KItemListHeaderWidget::hoverLeaveEvent(QGraphicsSceneHoverEvent *event)
372 {
373 QGraphicsWidget::hoverLeaveEvent(event);
374 if (m_hoveredIndex != -1) {
375 Q_EMIT columnUnHovered(m_hoveredIndex);
376 m_hoveredIndex = -1;
377 update();
378 }
379 }
380
381 void KItemListHeaderWidget::hoverMoveEvent(QGraphicsSceneHoverEvent *event)
382 {
383 QGraphicsWidget::hoverMoveEvent(event);
384
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);
390 } else {
391 unsetCursor();
392 }
393 }
394
395 void KItemListHeaderWidget::slotSortRoleChanged(const QByteArray &current, const QByteArray &previous)
396 {
397 Q_UNUSED(current)
398 Q_UNUSED(previous)
399 update();
400 }
401
402 void KItemListHeaderWidget::slotSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous)
403 {
404 Q_UNUSED(current)
405 Q_UNUSED(previous)
406 update();
407 }
408
409 void KItemListHeaderWidget::paintRole(QPainter *painter, const QByteArray &role, const QRectF &rect, int orderIndex, QWidget *widget) const
410 {
411 const auto direction = widget ? widget->layoutDirection() : qApp->layoutDirection();
412
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;
418
419 option.section = orderIndex;
420 option.state = QStyle::State_None | QStyle::State_Raised | QStyle::State_Horizontal;
421 if (isEnabled()) {
422 option.state |= QStyle::State_Enabled;
423 }
424 if (window() && window()->isActiveWindow()) {
425 option.state |= QStyle::State_Active;
426 }
427 if (m_hoveredIndex == orderIndex) {
428 option.state |= QStyle::State_MouseOver;
429 }
430 if (m_pressedRoleIndex == orderIndex) {
431 option.state |= QStyle::State_Sunken;
432 }
433 if (m_model->sortRole() == role) {
434 option.sortIndicator = (m_model->sortOrder() == Qt::AscendingOrder) ? QStyleOptionHeader::SortDown : QStyleOptionHeader::SortUp;
435 }
436 option.rect = rect.toRect();
437 option.orientation = Qt::Horizontal;
438 option.selectedPosition = QStyleOptionHeader::NotAdjacent;
439 option.text = m_model->roleDescription(role);
440
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);
451 };
452
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);
462 } else {
463 option.position = QStyleOptionHeader::Beginning;
464 }
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);
470 } else {
471 option.position = QStyleOptionHeader::End;
472 }
473 } else {
474 option.position = QStyleOptionHeader::Middle;
475 }
476
477 style()->drawControl(QStyle::CE_Header, &option, painter, widget);
478 }
479
480 void KItemListHeaderWidget::updatePressedRoleIndex(const QPointF &pos)
481 {
482 const int pressedIndex = roleIndexAt(pos);
483 if (m_pressedRoleIndex != pressedIndex) {
484 m_pressedRoleIndex = pressedIndex;
485 update();
486 }
487 }
488
489 void KItemListHeaderWidget::updateHoveredIndex(const QPointF &pos)
490 {
491 const int hoverIndex = roleIndexAt(pos);
492
493 if (m_hoveredIndex != hoverIndex) {
494 if (m_hoveredIndex != -1) {
495 Q_EMIT columnUnHovered(m_hoveredIndex);
496 }
497 m_hoveredIndex = hoverIndex;
498 if (m_hoveredIndex != -1) {
499 Q_EMIT columnHovered(m_hoveredIndex);
500 }
501 update();
502 }
503 }
504
505 int KItemListHeaderWidget::roleIndexAt(const QPointF &pos) const
506 {
507 int index = -1;
508
509 qreal x = -m_offset + m_sidePadding;
510 for (const QByteArray &role : std::as_const(m_columns)) {
511 ++index;
512 x += m_columnWidths.value(role);
513 if (pos.x() <= x) {
514 break;
515 }
516 }
517
518 return index;
519 }
520
521 bool KItemListHeaderWidget::isAboveRoleGrip(const QPointF &pos, int roleIndex) const
522 {
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);
527 }
528
529 const int grip = style()->pixelMetric(QStyle::PM_HeaderGripMargin);
530 return pos.x() >= (x - grip) && pos.x() <= x;
531 }
532
533 bool KItemListHeaderWidget::isAbovePaddingGrip(const QPointF &pos, PaddingGrip paddingGrip) const
534 {
535 const qreal lx = -m_offset + m_sidePadding;
536 const int grip = style()->pixelMetric(QStyle::PM_HeaderGripMargin);
537
538 switch (paddingGrip) {
539 case Leading:
540 return pos.x() >= (lx - grip) && pos.x() <= lx;
541 case Trailing: {
542 qreal rx = lx;
543 for (const QByteArray &role : std::as_const(m_columns)) {
544 rx += m_columnWidths.value(role);
545 }
546 return pos.x() >= (rx - grip) && pos.x() <= rx;
547 }
548 default:
549 return false;
550 }
551 }
552
553 QPixmap KItemListHeaderWidget::createRolePixmap(int roleIndex) const
554 {
555 const QByteArray role = m_columns[roleIndex];
556 const qreal roleWidth = m_columnWidths.value(role);
557 const QRect rect(0, 0, roleWidth, size().height());
558
559 QImage image(rect.size(), QImage::Format_ARGB32_Premultiplied);
560
561 QPainter painter(&image);
562 paintRole(&painter, role, rect, roleIndex);
563
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);
569
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));
573
574 return QPixmap::fromImage(image);
575 }
576
577 int KItemListHeaderWidget::targetOfMovingRole() const
578 {
579 const int movingWidth = m_movingRole.pixmap.width();
580 const int movingLeft = m_movingRole.x;
581 const int movingRight = movingLeft + movingWidth - 1;
582
583 int targetIndex = 0;
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;
589
590 const bool isInTarget = (targetWidth >= movingWidth && movingLeft >= targetLeft && movingRight <= targetRight)
591 || (targetWidth < movingWidth && movingLeft <= targetLeft && movingRight >= targetRight);
592
593 if (isInTarget) {
594 return targetIndex;
595 }
596
597 targetLeft += targetWidth;
598 ++targetIndex;
599 }
600
601 return m_movingRole.index;
602 }
603
604 qreal KItemListHeaderWidget::roleXPosition(const QByteArray &role) const
605 {
606 qreal x = -m_offset + m_sidePadding;
607 for (const QByteArray &visibleRole : std::as_const(m_columns)) {
608 if (visibleRole == role) {
609 return x;
610 }
611
612 x += m_columnWidths.value(visibleRole);
613 }
614
615 return -1;
616 }
617
618 #include "moc_kitemlistheaderwidget.cpp"