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