]> cloud.milkyroute.net Git - dolphin.git/blob - src/kitemviews/private/kitemlistheaderwidget.cpp
9f4f868d0dd6333350734a5a3a98c9e5266e2799
[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_sidePadding(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 // TODO update when font changes at runtime
37 setFont(QApplication::font("QHeaderView"));
38 }
39
40 KItemListHeaderWidget::~KItemListHeaderWidget()
41 {
42 }
43
44 void KItemListHeaderWidget::setModel(KItemModelBase* model)
45 {
46 if (m_model == model) {
47 return;
48 }
49
50 if (m_model) {
51 disconnect(m_model, &KItemModelBase::sortRoleChanged,
52 this, &KItemListHeaderWidget::slotSortRoleChanged);
53 disconnect(m_model, &KItemModelBase::sortOrderChanged,
54 this, &KItemListHeaderWidget::slotSortOrderChanged);
55 }
56
57 m_model = model;
58
59 if (m_model) {
60 connect(m_model, &KItemModelBase::sortRoleChanged,
61 this, &KItemListHeaderWidget::slotSortRoleChanged);
62 connect(m_model, &KItemModelBase::sortOrderChanged,
63 this, &KItemListHeaderWidget::slotSortOrderChanged);
64 }
65 }
66
67 KItemModelBase* KItemListHeaderWidget::model() const
68 {
69 return m_model;
70 }
71
72 void KItemListHeaderWidget::setAutomaticColumnResizing(bool automatic)
73 {
74 m_automaticColumnResizing = automatic;
75 }
76
77 bool KItemListHeaderWidget::automaticColumnResizing() const
78 {
79 return m_automaticColumnResizing;
80 }
81
82 void KItemListHeaderWidget::setColumns(const QList<QByteArray>& roles)
83 {
84 for (const QByteArray& role : roles) {
85 if (!m_columnWidths.contains(role)) {
86 m_preferredColumnWidths.remove(role);
87 }
88 }
89
90 m_columns = roles;
91 update();
92 }
93
94 QList<QByteArray> KItemListHeaderWidget::columns() const
95 {
96 return m_columns;
97 }
98
99 void KItemListHeaderWidget::setColumnWidth(const QByteArray& role, qreal width)
100 {
101 const qreal minWidth = minimumColumnWidth();
102 if (width < minWidth) {
103 width = minWidth;
104 }
105
106 if (m_columnWidths.value(role) != width) {
107 m_columnWidths.insert(role, width);
108 update();
109 }
110 }
111
112 qreal KItemListHeaderWidget::columnWidth(const QByteArray& role) const
113 {
114 return m_columnWidths.value(role);
115 }
116
117 void KItemListHeaderWidget::setPreferredColumnWidth(const QByteArray& role, qreal width)
118 {
119 m_preferredColumnWidths.insert(role, width);
120 }
121
122 qreal KItemListHeaderWidget::preferredColumnWidth(const QByteArray& role) const
123 {
124 return m_preferredColumnWidths.value(role);
125 }
126
127 void KItemListHeaderWidget::setOffset(qreal offset)
128 {
129 if (m_offset != offset) {
130 m_offset = offset;
131 update();
132 }
133 }
134
135 qreal KItemListHeaderWidget::offset() const
136 {
137 return m_offset;
138 }
139
140 void KItemListHeaderWidget::setSidePadding(qreal width)
141 {
142 if (m_sidePadding != width) {
143 m_sidePadding = width;
144 sidePaddingChanged(width);
145 update();
146 }
147 }
148
149 qreal KItemListHeaderWidget::sidePadding() const
150 {
151 return m_sidePadding;
152 }
153
154 qreal KItemListHeaderWidget::minimumColumnWidth() const
155 {
156 QFontMetricsF fontMetrics(font());
157 return fontMetrics.height() * 4;
158 }
159
160 void KItemListHeaderWidget::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget)
161 {
162 Q_UNUSED(option)
163 Q_UNUSED(widget)
164
165 if (!m_model) {
166 return;
167 }
168
169 // Draw roles
170 painter->setFont(font());
171 painter->setPen(palette().text().color());
172
173 qreal x = -m_offset + m_sidePadding;
174 int orderIndex = 0;
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);
179 x += roleWidth;
180 ++orderIndex;
181 }
182
183 if (!m_movingRole.pixmap.isNull()) {
184 Q_ASSERT(m_roleOperation == MoveRoleOperation);
185 painter->drawPixmap(m_movingRole.x, 0, m_movingRole.pixmap);
186 }
187 }
188
189 void KItemListHeaderWidget::mousePressEvent(QGraphicsSceneMouseEvent* event)
190 {
191 if (event->button() & Qt::LeftButton) {
192 m_pressedMousePos = event->pos();
193 if (isAbovePaddingGrip(m_pressedMousePos, PaddingGrip::Leading)) {
194 m_roleOperation = ResizePaddingColumnOperation;
195 } else {
196 updatePressedRoleIndex(event->pos());
197 m_roleOperation = isAboveRoleGrip(m_pressedMousePos, m_pressedRoleIndex) ?
198 ResizeRoleOperation : NoRoleOperation;
199 }
200 event->accept();
201 } else {
202 event->ignore();
203 }
204 }
205
206 void KItemListHeaderWidget::mouseReleaseEvent(QGraphicsSceneMouseEvent* event)
207 {
208 QGraphicsWidget::mouseReleaseEvent(event);
209
210 if (m_pressedRoleIndex == -1) {
211 return;
212 }
213
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);
226 } else {
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);
233
234 if (resetSortOrder) {
235 m_model->setSortOrder(Qt::AscendingOrder);
236 Q_EMIT sortOrderChanged(Qt::AscendingOrder, Qt::DescendingOrder);
237 }
238 }
239 break;
240 }
241
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);
246 break;
247 }
248
249 case MoveRoleOperation:
250 m_movingRole.pixmap = QPixmap();
251 m_movingRole.x = 0;
252 m_movingRole.xDec = 0;
253 m_movingRole.index = -1;
254 break;
255
256 default:
257 break;
258 }
259
260 m_pressedRoleIndex = -1;
261 m_roleOperation = NoRoleOperation;
262 update();
263
264 QApplication::restoreOverrideCursor();
265 }
266
267 void KItemListHeaderWidget::mouseMoveEvent(QGraphicsSceneMouseEvent* event)
268 {
269 QGraphicsWidget::mouseMoveEvent(event);
270
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));
284 } else {
285 m_movingRole.pixmap = createRolePixmap(roleIndex);
286
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);
291 }
292
293 m_movingRole.xDec = event->pos().x() - roleX;
294 m_movingRole.x = roleX;
295 update();
296 }
297 }
298 break;
299
300 case ResizeRoleOperation: {
301 const QByteArray pressedRole = m_columns[m_pressedRoleIndex];
302
303 qreal previousWidth = m_columnWidths.value(pressedRole);
304 qreal currentWidth = previousWidth;
305 currentWidth += event->pos().x() - event->lastPos().x();
306 currentWidth = qMax(minimumColumnWidth(), currentWidth);
307
308 m_columnWidths.insert(pressedRole, currentWidth);
309 update();
310
311 Q_EMIT columnWidthChanged(pressedRole, currentWidth, previousWidth);
312 break;
313 }
314
315 case ResizePaddingColumnOperation: {
316 qreal currentWidth = m_sidePadding;
317 currentWidth += event->pos().x() - event->lastPos().x();
318 currentWidth = qMax(0.0, currentWidth);
319
320 m_sidePadding = currentWidth;
321
322 update();
323
324 Q_EMIT sidePaddingChanged(currentWidth);
325
326 break;
327 }
328
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;
335 update();
336
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);
343
344 m_movingRole.xDec = event->pos().x() - roleXPosition(role);
345 }
346 }
347 break;
348 }
349
350 default:
351 break;
352 }
353 }
354
355 void KItemListHeaderWidget::mouseDoubleClickEvent(QGraphicsSceneMouseEvent* event)
356 {
357 QGraphicsItem::mouseDoubleClickEvent(event);
358
359 const int roleIndex = roleIndexAt(event->pos());
360 if (roleIndex >= 0 && isAboveRoleGrip(event->pos(), roleIndex)) {
361 const QByteArray role = m_columns.at(roleIndex);
362
363 qreal previousWidth = columnWidth(role);
364 setColumnWidth(role, preferredColumnWidth(role));
365 qreal currentWidth = columnWidth(role);
366
367 Q_EMIT columnWidthChanged(role, currentWidth, previousWidth);
368 Q_EMIT columnWidthChangeFinished(role, currentWidth);
369 }
370 }
371
372 void KItemListHeaderWidget::hoverEnterEvent(QGraphicsSceneHoverEvent* event)
373 {
374 QGraphicsWidget::hoverEnterEvent(event);
375 updateHoveredRoleIndex(event->pos());
376 }
377
378 void KItemListHeaderWidget::hoverLeaveEvent(QGraphicsSceneHoverEvent* event)
379 {
380 QGraphicsWidget::hoverLeaveEvent(event);
381 if (m_hoveredRoleIndex != -1) {
382 m_hoveredRoleIndex = -1;
383 update();
384 }
385 }
386
387 void KItemListHeaderWidget::hoverMoveEvent(QGraphicsSceneHoverEvent* event)
388 {
389 QGraphicsWidget::hoverMoveEvent(event);
390
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);
397 } else {
398 unsetCursor();
399 }
400 }
401
402 void KItemListHeaderWidget::slotSortRoleChanged(const QByteArray& current, const QByteArray& previous)
403 {
404 Q_UNUSED(current)
405 Q_UNUSED(previous)
406 update();
407 }
408
409 void KItemListHeaderWidget::slotSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous)
410 {
411 Q_UNUSED(current)
412 Q_UNUSED(previous)
413 update();
414 }
415
416 void KItemListHeaderWidget::paintRole(QPainter* painter,
417 const QByteArray& role,
418 const QRectF& rect,
419 int orderIndex,
420 QWidget* widget) const
421 {
422 const auto direction = widget ? widget->layoutDirection() : qApp->layoutDirection();
423
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
430 ? Qt::AlignLeft
431 : Qt::AlignRight;
432
433 option.section = orderIndex;
434 option.state = QStyle::State_None | QStyle::State_Raised | QStyle::State_Horizontal;
435 if (isEnabled()) {
436 option.state |= QStyle::State_Enabled;
437 }
438 if (window() && window()->isActiveWindow()) {
439 option.state |= QStyle::State_Active;
440 }
441 if (m_hoveredRoleIndex == orderIndex) {
442 option.state |= QStyle::State_MouseOver;
443 }
444 if (m_pressedRoleIndex == orderIndex) {
445 option.state |= QStyle::State_Sunken;
446 }
447 if (m_model->sortRole() == role) {
448 option.sortIndicator = (m_model->sortOrder() == Qt::AscendingOrder) ?
449 QStyleOptionHeader::SortDown : QStyleOptionHeader::SortUp;
450 }
451 option.rect = rect.toRect();
452 option.orientation = Qt::Horizontal;
453 option.selectedPosition = QStyleOptionHeader::NotAdjacent;
454 option.text = m_model->roleDescription(role);
455
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);
466 };
467
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);
477 } else {
478 option.position = QStyleOptionHeader::Beginning;
479 }
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);
485 } else {
486 option.position = QStyleOptionHeader::End;
487 }
488 } else {
489 option.position = QStyleOptionHeader::Middle;
490 }
491
492 style()->drawControl(QStyle::CE_Header, &option, painter, widget);
493 }
494
495 void KItemListHeaderWidget::updatePressedRoleIndex(const QPointF& pos)
496 {
497 const int pressedIndex = roleIndexAt(pos);
498 if (m_pressedRoleIndex != pressedIndex) {
499 m_pressedRoleIndex = pressedIndex;
500 update();
501 }
502 }
503
504 void KItemListHeaderWidget::updateHoveredRoleIndex(const QPointF& pos)
505 {
506 const int hoverIndex = roleIndexAt(pos);
507 if (m_hoveredRoleIndex != hoverIndex) {
508 m_hoveredRoleIndex = hoverIndex;
509 update();
510 }
511 }
512
513 int KItemListHeaderWidget::roleIndexAt(const QPointF& pos) const
514 {
515 int index = -1;
516
517 qreal x = -m_offset + m_sidePadding;
518 for (const QByteArray& role : qAsConst(m_columns)) {
519 ++index;
520 x += m_columnWidths.value(role);
521 if (pos.x() <= x) {
522 break;
523 }
524 }
525
526 return index;
527 }
528
529 bool KItemListHeaderWidget::isAboveRoleGrip(const QPointF& pos, int roleIndex) const
530 {
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);
535 }
536
537 const int grip = style()->pixelMetric(QStyle::PM_HeaderGripMargin);
538 return pos.x() >= (x - grip) && pos.x() <= x;
539 }
540
541 bool KItemListHeaderWidget::isAbovePaddingGrip(const QPointF& pos, PaddingGrip paddingGrip) const
542 {
543 const qreal lx = -m_offset + m_sidePadding;
544 const int grip = style()->pixelMetric(QStyle::PM_HeaderGripMargin);
545
546 switch (paddingGrip) {
547 case Leading:
548 return pos.x() >= (lx - grip) && pos.x() <= lx;
549 case Trailing:
550 {
551 qreal rx = lx;
552 for (const QByteArray& role : qAsConst(m_columns)) {
553 rx += m_columnWidths.value(role);
554 }
555 return pos.x() >= (rx - grip) && pos.x() <= rx;
556 }
557 default:
558 return false;
559 }
560 }
561
562 QPixmap KItemListHeaderWidget::createRolePixmap(int roleIndex) const
563 {
564 const QByteArray role = m_columns[roleIndex];
565 const qreal roleWidth = m_columnWidths.value(role);
566 const QRect rect(0, 0, roleWidth, size().height());
567
568 QImage image(rect.size(), QImage::Format_ARGB32_Premultiplied);
569
570 QPainter painter(&image);
571 paintRole(&painter, role, rect, roleIndex);
572
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);
578
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));
582
583 return QPixmap::fromImage(image);
584 }
585
586 int KItemListHeaderWidget::targetOfMovingRole() const
587 {
588 const int movingWidth = m_movingRole.pixmap.width();
589 const int movingLeft = m_movingRole.x;
590 const int movingRight = movingLeft + movingWidth - 1;
591
592 int targetIndex = 0;
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;
598
599 const bool isInTarget = (targetWidth >= movingWidth &&
600 movingLeft >= targetLeft &&
601 movingRight <= targetRight) ||
602 (targetWidth < movingWidth &&
603 movingLeft <= targetLeft &&
604 movingRight >= targetRight);
605
606 if (isInTarget) {
607 return targetIndex;
608 }
609
610 targetLeft += targetWidth;
611 ++targetIndex;
612 }
613
614 return m_movingRole.index;
615 }
616
617 qreal KItemListHeaderWidget::roleXPosition(const QByteArray& role) const
618 {
619 qreal x = -m_offset + m_sidePadding;
620 for (const QByteArray& visibleRole : qAsConst(m_columns)) {
621 if (visibleRole == role) {
622 return x;
623 }
624
625 x += m_columnWidths.value(visibleRole);
626 }
627
628 return -1;
629 }
630