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