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