]> cloud.milkyroute.net Git - dolphin.git/blob - src/kitemviews/private/kitemlistheaderwidget.cpp
Full row highlight implementation
[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 // The following code is based on the code from QHeaderView::paintSection().
421 // SPDX-FileCopyrightText: 2011 Nokia Corporation and/or its subsidiary(-ies).
422 QStyleOptionHeader option;
423 option.section = orderIndex;
424 option.state = QStyle::State_None | QStyle::State_Raised | QStyle::State_Horizontal;
425 if (isEnabled()) {
426 option.state |= QStyle::State_Enabled;
427 }
428 if (window() && window()->isActiveWindow()) {
429 option.state |= QStyle::State_Active;
430 }
431 if (m_hoveredRoleIndex == orderIndex) {
432 option.state |= QStyle::State_MouseOver;
433 }
434 if (m_pressedRoleIndex == orderIndex) {
435 option.state |= QStyle::State_Sunken;
436 }
437 if (m_model->sortRole() == role) {
438 option.sortIndicator = (m_model->sortOrder() == Qt::AscendingOrder) ?
439 QStyleOptionHeader::SortDown : QStyleOptionHeader::SortUp;
440 }
441 option.rect = rect.toRect();
442 option.orientation = Qt::Horizontal;
443 option.selectedPosition = QStyleOptionHeader::NotAdjacent;
444 option.text = m_model->roleDescription(role);
445
446 // First we paint any potential empty (padding) space on left and/or right of this role's column.
447 const auto paintPadding = [&](int section, const QRectF &rect, const QStyleOptionHeader::SectionPosition &pos){
448 QStyleOptionHeader padding;
449 padding.state = QStyle::State_None | QStyle::State_Raised | QStyle::State_Horizontal;
450 padding.section = section;
451 padding.sortIndicator = QStyleOptionHeader::None;
452 padding.rect = rect.toRect();
453 padding.position = pos;
454 padding.text = QString();
455 style()->drawControl(QStyle::CE_Header, &padding, painter, widget);
456 };
457
458 if (m_columns.count() == 1) {
459 option.position = QStyleOptionHeader::Middle;
460 paintPadding(0, QRectF(0.0, 0.0, rect.left(), rect.height()), QStyleOptionHeader::Beginning);
461 paintPadding(1, QRectF(rect.left(), 0.0, size().width() - rect.left(), rect.height()), QStyleOptionHeader::End);
462 } else if (orderIndex == 0) {
463 // Paint the header for the first column; check if there is some empty space to the left which needs to be filled.
464 if (rect.left() > 0) {
465 option.position = QStyleOptionHeader::Middle;
466 paintPadding(0,QRectF(0.0, 0.0, rect.left(), rect.height()), QStyleOptionHeader::Beginning);
467 } else {
468 option.position = QStyleOptionHeader::Beginning;
469 }
470 } else if (orderIndex == m_columns.count() - 1) {
471 // Paint the header for the last column; check if there is some empty space to the right which needs to be filled.
472 if (rect.right() < size().width()) {
473 option.position = QStyleOptionHeader::Middle;
474 paintPadding(m_columns.count(), QRectF(rect.left(), 0.0, size().width() - rect.left(), rect.height()), QStyleOptionHeader::End);
475 } else {
476 option.position = QStyleOptionHeader::End;
477 }
478 } else {
479 option.position = QStyleOptionHeader::Middle;
480 }
481
482 style()->drawControl(QStyle::CE_Header, &option, painter, widget);
483 }
484
485 void KItemListHeaderWidget::updatePressedRoleIndex(const QPointF& pos)
486 {
487 const int pressedIndex = roleIndexAt(pos);
488 if (m_pressedRoleIndex != pressedIndex) {
489 m_pressedRoleIndex = pressedIndex;
490 update();
491 }
492 }
493
494 void KItemListHeaderWidget::updateHoveredRoleIndex(const QPointF& pos)
495 {
496 const int hoverIndex = roleIndexAt(pos);
497 if (m_hoveredRoleIndex != hoverIndex) {
498 m_hoveredRoleIndex = hoverIndex;
499 update();
500 }
501 }
502
503 int KItemListHeaderWidget::roleIndexAt(const QPointF& pos) const
504 {
505 int index = -1;
506
507 qreal x = -m_offset + m_leadingPadding;
508 for (const QByteArray& role : qAsConst(m_columns)) {
509 ++index;
510 x += m_columnWidths.value(role);
511 if (pos.x() <= x) {
512 break;
513 }
514 }
515
516 return index;
517 }
518
519 bool KItemListHeaderWidget::isAboveRoleGrip(const QPointF& pos, int roleIndex) const
520 {
521 qreal x = -m_offset + m_leadingPadding;
522 for (int i = 0; i <= roleIndex; ++i) {
523 const QByteArray role = m_columns[i];
524 x += m_columnWidths.value(role);
525 }
526
527 const int grip = style()->pixelMetric(QStyle::PM_HeaderGripMargin);
528 return pos.x() >= (x - grip) && pos.x() <= x;
529 }
530
531 bool KItemListHeaderWidget::isAbovePaddingGrip(const QPointF& pos, PaddingGrip paddingGrip) const
532 {
533 const qreal lx = -m_offset + m_leadingPadding;
534 const int grip = style()->pixelMetric(QStyle::PM_HeaderGripMargin);
535
536 switch (paddingGrip) {
537 case Leading:
538 return pos.x() >= (lx - grip) && pos.x() <= lx;
539 case Trailing:
540 {
541 qreal rx = lx;
542 for (const QByteArray& role : qAsConst(m_columns)) {
543 rx += m_columnWidths.value(role);
544 }
545 return pos.x() >= (rx - grip) && pos.x() <= rx;
546 }
547 default:
548 return false;
549 }
550 }
551
552 QPixmap KItemListHeaderWidget::createRolePixmap(int roleIndex) const
553 {
554 const QByteArray role = m_columns[roleIndex];
555 const qreal roleWidth = m_columnWidths.value(role);
556 const QRect rect(0, 0, roleWidth, size().height());
557
558 QImage image(rect.size(), QImage::Format_ARGB32_Premultiplied);
559
560 QPainter painter(&image);
561 paintRole(&painter, role, rect, roleIndex);
562
563 // Apply a highlighting-color
564 const QPalette::ColorGroup group = isActiveWindow() ? QPalette::Active : QPalette::Inactive;
565 QColor highlightColor = palette().color(group, QPalette::Highlight);
566 highlightColor.setAlpha(64);
567 painter.fillRect(rect, highlightColor);
568
569 // Make the image transparent
570 painter.setCompositionMode(QPainter::CompositionMode_DestinationIn);
571 painter.fillRect(0, 0, image.width(), image.height(), QColor(0, 0, 0, 192));
572
573 return QPixmap::fromImage(image);
574 }
575
576 int KItemListHeaderWidget::targetOfMovingRole() const
577 {
578 const int movingWidth = m_movingRole.pixmap.width();
579 const int movingLeft = m_movingRole.x;
580 const int movingRight = movingLeft + movingWidth - 1;
581
582 int targetIndex = 0;
583 qreal targetLeft = -m_offset + m_leadingPadding;
584 while (targetIndex < m_columns.count()) {
585 const QByteArray role = m_columns[targetIndex];
586 const qreal targetWidth = m_columnWidths.value(role);
587 const qreal targetRight = targetLeft + targetWidth - 1;
588
589 const bool isInTarget = (targetWidth >= movingWidth &&
590 movingLeft >= targetLeft &&
591 movingRight <= targetRight) ||
592 (targetWidth < movingWidth &&
593 movingLeft <= targetLeft &&
594 movingRight >= targetRight);
595
596 if (isInTarget) {
597 return targetIndex;
598 }
599
600 targetLeft += targetWidth;
601 ++targetIndex;
602 }
603
604 return m_movingRole.index;
605 }
606
607 qreal KItemListHeaderWidget::roleXPosition(const QByteArray& role) const
608 {
609 qreal x = -m_offset + m_leadingPadding;
610 for (const QByteArray& visibleRole : qAsConst(m_columns)) {
611 if (visibleRole == role) {
612 return x;
613 }
614
615 x += m_columnWidths.value(visibleRole);
616 }
617
618 return -1;
619 }
620