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