]> cloud.milkyroute.net Git - dolphin.git/blob - src/kitemviews/private/kitemlistheaderwidget.cpp
KItemListHeaderWidget: don't crash if widget == nullptr
[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
16 KItemListHeaderWidget::KItemListHeaderWidget(QGraphicsWidget* parent) :
17 QGraphicsWidget(parent),
18 m_automaticColumnResizing(true),
19 m_model(nullptr),
20 m_offset(0),
21 m_leadingPadding(0),
22 m_columns(),
23 m_columnWidths(),
24 m_preferredColumnWidths(),
25 m_hoveredRoleIndex(-1),
26 m_pressedRoleIndex(-1),
27 m_roleOperation(NoRoleOperation),
28 m_pressedMousePos(),
29 m_movingRole()
30 {
31 m_movingRole.x = 0;
32 m_movingRole.xDec = 0;
33 m_movingRole.index = -1;
34
35 setAcceptHoverEvents(true);
36 }
37
38 KItemListHeaderWidget::~KItemListHeaderWidget()
39 {
40 }
41
42 void KItemListHeaderWidget::setModel(KItemModelBase* model)
43 {
44 if (m_model == model) {
45 return;
46 }
47
48 if (m_model) {
49 disconnect(m_model, &KItemModelBase::sortRoleChanged,
50 this, &KItemListHeaderWidget::slotSortRoleChanged);
51 disconnect(m_model, &KItemModelBase::sortOrderChanged,
52 this, &KItemListHeaderWidget::slotSortOrderChanged);
53 }
54
55 m_model = model;
56
57 if (m_model) {
58 connect(m_model, &KItemModelBase::sortRoleChanged,
59 this, &KItemListHeaderWidget::slotSortRoleChanged);
60 connect(m_model, &KItemModelBase::sortOrderChanged,
61 this, &KItemListHeaderWidget::slotSortOrderChanged);
62 }
63 }
64
65 KItemModelBase* KItemListHeaderWidget::model() const
66 {
67 return m_model;
68 }
69
70 void KItemListHeaderWidget::setAutomaticColumnResizing(bool automatic)
71 {
72 m_automaticColumnResizing = automatic;
73 }
74
75 bool KItemListHeaderWidget::automaticColumnResizing() const
76 {
77 return m_automaticColumnResizing;
78 }
79
80 void KItemListHeaderWidget::setColumns(const QList<QByteArray>& roles)
81 {
82 for (const QByteArray& role : roles) {
83 if (!m_columnWidths.contains(role)) {
84 m_preferredColumnWidths.remove(role);
85 }
86 }
87
88 m_columns = roles;
89 update();
90 }
91
92 QList<QByteArray> KItemListHeaderWidget::columns() const
93 {
94 return m_columns;
95 }
96
97 void KItemListHeaderWidget::setColumnWidth(const QByteArray& role, qreal width)
98 {
99 const qreal minWidth = minimumColumnWidth();
100 if (width < minWidth) {
101 width = minWidth;
102 }
103
104 if (m_columnWidths.value(role) != width) {
105 m_columnWidths.insert(role, width);
106 update();
107 }
108 }
109
110 qreal KItemListHeaderWidget::columnWidth(const QByteArray& role) const
111 {
112 return m_columnWidths.value(role);
113 }
114
115 void KItemListHeaderWidget::setPreferredColumnWidth(const QByteArray& role, qreal width)
116 {
117 m_preferredColumnWidths.insert(role, width);
118 }
119
120 qreal KItemListHeaderWidget::preferredColumnWidth(const QByteArray& role) const
121 {
122 return m_preferredColumnWidths.value(role);
123 }
124
125 void KItemListHeaderWidget::setOffset(qreal offset)
126 {
127 if (m_offset != offset) {
128 m_offset = offset;
129 update();
130 }
131 }
132
133 qreal KItemListHeaderWidget::offset() const
134 {
135 return m_offset;
136 }
137
138 void KItemListHeaderWidget::setLeadingPadding(qreal width)
139 {
140 if (m_leadingPadding != width) {
141 m_leadingPadding = width;
142 leadingPaddingChanged(width);
143 update();
144 }
145 }
146
147 qreal KItemListHeaderWidget::leadingPadding() const
148 {
149 return m_leadingPadding;
150 }
151
152 qreal KItemListHeaderWidget::minimumColumnWidth() const
153 {
154 QFontMetricsF fontMetrics(font());
155 return fontMetrics.height() * 4;
156 }
157
158 void KItemListHeaderWidget::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget)
159 {
160 Q_UNUSED(option)
161 Q_UNUSED(widget)
162
163 if (!m_model) {
164 return;
165 }
166
167 // Draw roles
168 painter->setFont(font());
169 painter->setPen(palette().text().color());
170
171 qreal x = -m_offset + m_leadingPadding;
172 int orderIndex = 0;
173 for (const QByteArray& role : qAsConst(m_columns)) {
174 const qreal roleWidth = m_columnWidths.value(role);
175 const QRectF rect(x, 0, roleWidth, size().height());
176 paintRole(painter, role, rect, orderIndex, widget);
177 x += roleWidth;
178 ++orderIndex;
179 }
180
181 if (!m_movingRole.pixmap.isNull()) {
182 Q_ASSERT(m_roleOperation == MoveRoleOperation);
183 painter->drawPixmap(m_movingRole.x, 0, m_movingRole.pixmap);
184 }
185 }
186
187 void KItemListHeaderWidget::mousePressEvent(QGraphicsSceneMouseEvent* event)
188 {
189 if (event->button() & Qt::LeftButton) {
190 m_pressedMousePos = event->pos();
191 if (isAbovePaddingGrip(m_pressedMousePos, PaddingGrip::Leading)) {
192 m_roleOperation = ResizeLeadingColumnOperation;
193 } else {
194 updatePressedRoleIndex(event->pos());
195 m_roleOperation = isAboveRoleGrip(m_pressedMousePos, m_pressedRoleIndex) ?
196 ResizeRoleOperation : NoRoleOperation;
197 }
198 event->accept();
199 } else {
200 event->ignore();
201 }
202 }
203
204 void KItemListHeaderWidget::mouseReleaseEvent(QGraphicsSceneMouseEvent* event)
205 {
206 QGraphicsWidget::mouseReleaseEvent(event);
207
208 if (m_pressedRoleIndex == -1) {
209 return;
210 }
211
212 switch (m_roleOperation) {
213 case NoRoleOperation: {
214 // Only a click has been done and no moving or resizing has been started
215 const QByteArray sortRole = m_model->sortRole();
216 const int sortRoleIndex = m_columns.indexOf(sortRole);
217 if (m_pressedRoleIndex == sortRoleIndex) {
218 // Toggle the sort order
219 const Qt::SortOrder previous = m_model->sortOrder();
220 const Qt::SortOrder current = (m_model->sortOrder() == Qt::AscendingOrder) ?
221 Qt::DescendingOrder : Qt::AscendingOrder;
222 m_model->setSortOrder(current);
223 Q_EMIT sortOrderChanged(current, previous);
224 } else {
225 // Change the sort role and reset to the ascending order
226 const QByteArray previous = m_model->sortRole();
227 const QByteArray current = m_columns[m_pressedRoleIndex];
228 const bool resetSortOrder = m_model->sortOrder() == Qt::DescendingOrder;
229 m_model->setSortRole(current, !resetSortOrder);
230 Q_EMIT sortRoleChanged(current, previous);
231
232 if (resetSortOrder) {
233 m_model->setSortOrder(Qt::AscendingOrder);
234 Q_EMIT sortOrderChanged(Qt::AscendingOrder, Qt::DescendingOrder);
235 }
236 }
237 break;
238 }
239
240 case ResizeRoleOperation: {
241 const QByteArray pressedRole = m_columns[m_pressedRoleIndex];
242 const qreal currentWidth = m_columnWidths.value(pressedRole);
243 Q_EMIT columnWidthChangeFinished(pressedRole, currentWidth);
244 break;
245 }
246
247 case MoveRoleOperation:
248 m_movingRole.pixmap = QPixmap();
249 m_movingRole.x = 0;
250 m_movingRole.xDec = 0;
251 m_movingRole.index = -1;
252 break;
253
254 default:
255 break;
256 }
257
258 m_pressedRoleIndex = -1;
259 m_roleOperation = NoRoleOperation;
260 update();
261
262 QApplication::restoreOverrideCursor();
263 }
264
265 void KItemListHeaderWidget::mouseMoveEvent(QGraphicsSceneMouseEvent* event)
266 {
267 QGraphicsWidget::mouseMoveEvent(event);
268
269 switch (m_roleOperation) {
270 case NoRoleOperation:
271 if ((event->pos() - m_pressedMousePos).manhattanLength() >= QApplication::startDragDistance()) {
272 // A role gets dragged by the user. Create a pixmap of the role that will get
273 // synchronized on each further mouse-move-event with the mouse-position.
274 m_roleOperation = MoveRoleOperation;
275 const int roleIndex = roleIndexAt(m_pressedMousePos);
276 m_movingRole.index = roleIndex;
277 if (roleIndex == 0) {
278 // TODO: It should be configurable whether moving the first role is allowed.
279 // In the context of Dolphin this is not required, however this should be
280 // changed if KItemViews are used in a more generic way.
281 QApplication::setOverrideCursor(QCursor(Qt::ForbiddenCursor));
282 } else {
283 m_movingRole.pixmap = createRolePixmap(roleIndex);
284
285 qreal roleX = -m_offset + m_leadingPadding;
286 for (int i = 0; i < roleIndex; ++i) {
287 const QByteArray role = m_columns[i];
288 roleX += m_columnWidths.value(role);
289 }
290
291 m_movingRole.xDec = event->pos().x() - roleX;
292 m_movingRole.x = roleX;
293 update();
294 }
295 }
296 break;
297
298 case ResizeRoleOperation: {
299 const QByteArray pressedRole = m_columns[m_pressedRoleIndex];
300
301 qreal previousWidth = m_columnWidths.value(pressedRole);
302 qreal currentWidth = previousWidth;
303 currentWidth += event->pos().x() - event->lastPos().x();
304 currentWidth = qMax(minimumColumnWidth(), currentWidth);
305
306 m_columnWidths.insert(pressedRole, currentWidth);
307 update();
308
309 Q_EMIT columnWidthChanged(pressedRole, currentWidth, previousWidth);
310 break;
311 }
312
313 case ResizeLeadingColumnOperation: {
314 qreal currentWidth = m_leadingPadding;
315 currentWidth += event->pos().x() - event->lastPos().x();
316 currentWidth = qMax(0.0, currentWidth);
317
318 m_leadingPadding = currentWidth;
319
320 update();
321
322 Q_EMIT leadingPaddingChanged(currentWidth);
323
324 break;
325 }
326
327 case MoveRoleOperation: {
328 // TODO: It should be configurable whether moving the first role is allowed.
329 // In the context of Dolphin this is not required, however this should be
330 // changed if KItemViews are used in a more generic way.
331 if (m_movingRole.index > 0) {
332 m_movingRole.x = event->pos().x() - m_movingRole.xDec;
333 update();
334
335 const int targetIndex = targetOfMovingRole();
336 if (targetIndex > 0 && targetIndex != m_movingRole.index) {
337 const QByteArray role = m_columns[m_movingRole.index];
338 const int previousIndex = m_movingRole.index;
339 m_movingRole.index = targetIndex;
340 Q_EMIT columnMoved(role, targetIndex, previousIndex);
341
342 m_movingRole.xDec = event->pos().x() - roleXPosition(role);
343 }
344 }
345 break;
346 }
347
348 default:
349 break;
350 }
351 }
352
353 void KItemListHeaderWidget::mouseDoubleClickEvent(QGraphicsSceneMouseEvent* event)
354 {
355 QGraphicsItem::mouseDoubleClickEvent(event);
356
357 const int roleIndex = roleIndexAt(event->pos());
358 if (roleIndex >= 0 && isAboveRoleGrip(event->pos(), roleIndex)) {
359 const QByteArray role = m_columns.at(roleIndex);
360
361 qreal previousWidth = columnWidth(role);
362 setColumnWidth(role, preferredColumnWidth(role));
363 qreal currentWidth = columnWidth(role);
364
365 Q_EMIT columnWidthChanged(role, currentWidth, previousWidth);
366 Q_EMIT columnWidthChangeFinished(role, currentWidth);
367 }
368 }
369
370 void KItemListHeaderWidget::hoverEnterEvent(QGraphicsSceneHoverEvent* event)
371 {
372 QGraphicsWidget::hoverEnterEvent(event);
373 updateHoveredRoleIndex(event->pos());
374 }
375
376 void KItemListHeaderWidget::hoverLeaveEvent(QGraphicsSceneHoverEvent* event)
377 {
378 QGraphicsWidget::hoverLeaveEvent(event);
379 if (m_hoveredRoleIndex != -1) {
380 m_hoveredRoleIndex = -1;
381 update();
382 }
383 }
384
385 void KItemListHeaderWidget::hoverMoveEvent(QGraphicsSceneHoverEvent* event)
386 {
387 QGraphicsWidget::hoverMoveEvent(event);
388
389 const QPointF& pos = event->pos();
390 updateHoveredRoleIndex(pos);
391 if ((m_hoveredRoleIndex >= 0 && isAboveRoleGrip(pos, m_hoveredRoleIndex)) ||
392 isAbovePaddingGrip(pos, PaddingGrip::Leading) ||
393 isAbovePaddingGrip(pos, PaddingGrip::Trailing)) {
394 setCursor(Qt::SplitHCursor);
395 } else {
396 unsetCursor();
397 }
398 }
399
400 void KItemListHeaderWidget::slotSortRoleChanged(const QByteArray& current, const QByteArray& previous)
401 {
402 Q_UNUSED(current)
403 Q_UNUSED(previous)
404 update();
405 }
406
407 void KItemListHeaderWidget::slotSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous)
408 {
409 Q_UNUSED(current)
410 Q_UNUSED(previous)
411 update();
412 }
413
414 void KItemListHeaderWidget::paintRole(QPainter* painter,
415 const QByteArray& role,
416 const QRectF& rect,
417 int orderIndex,
418 QWidget* widget) const
419 {
420 const auto direction = widget ? widget->layoutDirection() : qApp->layoutDirection();
421
422 // The following code is based on the code from QHeaderView::paintSection().
423 // SPDX-FileCopyrightText: 2011 Nokia Corporation and/or its subsidiary(-ies).
424 QStyleOptionHeader option;
425 option.direction = direction;
426 option.textAlignment =
427 direction == Qt::LeftToRight
428 ? Qt::AlignLeft
429 : Qt::AlignRight;
430
431 option.section = orderIndex;
432 option.state = QStyle::State_None | QStyle::State_Raised | QStyle::State_Horizontal;
433 if (isEnabled()) {
434 option.state |= QStyle::State_Enabled;
435 }
436 if (window() && window()->isActiveWindow()) {
437 option.state |= QStyle::State_Active;
438 }
439 if (m_hoveredRoleIndex == orderIndex) {
440 option.state |= QStyle::State_MouseOver;
441 }
442 if (m_pressedRoleIndex == orderIndex) {
443 option.state |= QStyle::State_Sunken;
444 }
445 if (m_model->sortRole() == role) {
446 option.sortIndicator = (m_model->sortOrder() == Qt::AscendingOrder) ?
447 QStyleOptionHeader::SortDown : QStyleOptionHeader::SortUp;
448 }
449 option.rect = rect.toRect();
450 option.orientation = Qt::Horizontal;
451 option.selectedPosition = QStyleOptionHeader::NotAdjacent;
452 option.text = m_model->roleDescription(role);
453
454 // First we paint any potential empty (padding) space on left and/or right of this role's column.
455 const auto paintPadding = [&](int section, const QRectF &rect, const QStyleOptionHeader::SectionPosition &pos){
456 QStyleOptionHeader padding;
457 padding.state = QStyle::State_None | QStyle::State_Raised | QStyle::State_Horizontal;
458 padding.section = section;
459 padding.sortIndicator = QStyleOptionHeader::None;
460 padding.rect = rect.toRect();
461 padding.position = pos;
462 padding.text = QString();
463 style()->drawControl(QStyle::CE_Header, &padding, painter, widget);
464 };
465
466 if (m_columns.count() == 1) {
467 option.position = QStyleOptionHeader::Middle;
468 paintPadding(0, QRectF(0.0, 0.0, rect.left(), rect.height()), QStyleOptionHeader::Beginning);
469 paintPadding(1, QRectF(rect.left(), 0.0, size().width() - rect.left(), rect.height()), QStyleOptionHeader::End);
470 } else if (orderIndex == 0) {
471 // Paint the header for the first column; check if there is some empty space to the left which needs to be filled.
472 if (rect.left() > 0) {
473 option.position = QStyleOptionHeader::Middle;
474 paintPadding(0,QRectF(0.0, 0.0, rect.left(), rect.height()), QStyleOptionHeader::Beginning);
475 } else {
476 option.position = QStyleOptionHeader::Beginning;
477 }
478 } else if (orderIndex == m_columns.count() - 1) {
479 // Paint the header for the last column; check if there is some empty space to the right which needs to be filled.
480 if (rect.right() < size().width()) {
481 option.position = QStyleOptionHeader::Middle;
482 paintPadding(m_columns.count(), QRectF(rect.left(), 0.0, size().width() - rect.left(), rect.height()), QStyleOptionHeader::End);
483 } else {
484 option.position = QStyleOptionHeader::End;
485 }
486 } else {
487 option.position = QStyleOptionHeader::Middle;
488 }
489
490 style()->drawControl(QStyle::CE_Header, &option, painter, widget);
491 }
492
493 void KItemListHeaderWidget::updatePressedRoleIndex(const QPointF& pos)
494 {
495 const int pressedIndex = roleIndexAt(pos);
496 if (m_pressedRoleIndex != pressedIndex) {
497 m_pressedRoleIndex = pressedIndex;
498 update();
499 }
500 }
501
502 void KItemListHeaderWidget::updateHoveredRoleIndex(const QPointF& pos)
503 {
504 const int hoverIndex = roleIndexAt(pos);
505 if (m_hoveredRoleIndex != hoverIndex) {
506 m_hoveredRoleIndex = hoverIndex;
507 update();
508 }
509 }
510
511 int KItemListHeaderWidget::roleIndexAt(const QPointF& pos) const
512 {
513 int index = -1;
514
515 qreal x = -m_offset + m_leadingPadding;
516 for (const QByteArray& role : qAsConst(m_columns)) {
517 ++index;
518 x += m_columnWidths.value(role);
519 if (pos.x() <= x) {
520 break;
521 }
522 }
523
524 return index;
525 }
526
527 bool KItemListHeaderWidget::isAboveRoleGrip(const QPointF& pos, int roleIndex) const
528 {
529 qreal x = -m_offset + m_leadingPadding;
530 for (int i = 0; i <= roleIndex; ++i) {
531 const QByteArray role = m_columns[i];
532 x += m_columnWidths.value(role);
533 }
534
535 const int grip = style()->pixelMetric(QStyle::PM_HeaderGripMargin);
536 return pos.x() >= (x - grip) && pos.x() <= x;
537 }
538
539 bool KItemListHeaderWidget::isAbovePaddingGrip(const QPointF& pos, PaddingGrip paddingGrip) const
540 {
541 const qreal lx = -m_offset + m_leadingPadding;
542 const int grip = style()->pixelMetric(QStyle::PM_HeaderGripMargin);
543
544 switch (paddingGrip) {
545 case Leading:
546 return pos.x() >= (lx - grip) && pos.x() <= lx;
547 case Trailing:
548 {
549 qreal rx = lx;
550 for (const QByteArray& role : qAsConst(m_columns)) {
551 rx += m_columnWidths.value(role);
552 }
553 return pos.x() >= (rx - grip) && pos.x() <= rx;
554 }
555 default:
556 return false;
557 }
558 }
559
560 QPixmap KItemListHeaderWidget::createRolePixmap(int roleIndex) const
561 {
562 const QByteArray role = m_columns[roleIndex];
563 const qreal roleWidth = m_columnWidths.value(role);
564 const QRect rect(0, 0, roleWidth, size().height());
565
566 QImage image(rect.size(), QImage::Format_ARGB32_Premultiplied);
567
568 QPainter painter(&image);
569 paintRole(&painter, role, rect, roleIndex);
570
571 // Apply a highlighting-color
572 const QPalette::ColorGroup group = isActiveWindow() ? QPalette::Active : QPalette::Inactive;
573 QColor highlightColor = palette().color(group, QPalette::Highlight);
574 highlightColor.setAlpha(64);
575 painter.fillRect(rect, highlightColor);
576
577 // Make the image transparent
578 painter.setCompositionMode(QPainter::CompositionMode_DestinationIn);
579 painter.fillRect(0, 0, image.width(), image.height(), QColor(0, 0, 0, 192));
580
581 return QPixmap::fromImage(image);
582 }
583
584 int KItemListHeaderWidget::targetOfMovingRole() const
585 {
586 const int movingWidth = m_movingRole.pixmap.width();
587 const int movingLeft = m_movingRole.x;
588 const int movingRight = movingLeft + movingWidth - 1;
589
590 int targetIndex = 0;
591 qreal targetLeft = -m_offset + m_leadingPadding;
592 while (targetIndex < m_columns.count()) {
593 const QByteArray role = m_columns[targetIndex];
594 const qreal targetWidth = m_columnWidths.value(role);
595 const qreal targetRight = targetLeft + targetWidth - 1;
596
597 const bool isInTarget = (targetWidth >= movingWidth &&
598 movingLeft >= targetLeft &&
599 movingRight <= targetRight) ||
600 (targetWidth < movingWidth &&
601 movingLeft <= targetLeft &&
602 movingRight >= targetRight);
603
604 if (isInTarget) {
605 return targetIndex;
606 }
607
608 targetLeft += targetWidth;
609 ++targetIndex;
610 }
611
612 return m_movingRole.index;
613 }
614
615 qreal KItemListHeaderWidget::roleXPosition(const QByteArray& role) const
616 {
617 qreal x = -m_offset + m_leadingPadding;
618 for (const QByteArray& visibleRole : qAsConst(m_columns)) {
619 if (visibleRole == role) {
620 return x;
621 }
622
623 x += m_columnWidths.value(visibleRole);
624 }
625
626 return -1;
627 }
628