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