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