add more spacing
[personal-kdebase.git] / workspace / plasma / applets / kickoff / ui / flipscrollview.cpp
blob17a254fd8de583a7b8c4ff3fc298b6ca57b774f2
1 /*
2 Copyright 2007 Robert Knight <robertknight@gmail.com>
4 This library is free software; you can redistribute it and/or
5 modify it under the terms of the GNU Library General Public
6 License as published by the Free Software Foundation; either
7 version 2 of the License, or (at your option) any later version.
9 This library 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 GNU
12 Library General Public License for more details.
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to
16 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17 Boston, MA 02110-1301, USA.
20 // Own
21 #include "ui/flipscrollview.h"
23 // Qt
24 #include <QMouseEvent>
25 #include <QPainter>
26 #include <QScrollBar>
27 #include <QStack>
28 #include <QTimeLine>
30 // KDE
31 #include <KDebug>
32 #include <KGlobalSettings>
33 #include <KIconLoader>
34 #include <KColorScheme>
36 #include "ui/itemdelegate.h"
38 using namespace Kickoff;
40 class FlipScrollView::Private
42 public:
43 Private(FlipScrollView *view)
44 : q(view)
45 , backArrowHover(false)
46 , flipAnimTimeLine(new QTimeLine())
47 , animLeftToRight(true)
48 , itemHeight(-1) {
51 ~Private() {
52 delete flipAnimTimeLine;
55 QModelIndex currentRoot() const {
56 if (currentRootIndex.isValid()) {
57 return currentRootIndex;
58 } else {
59 return q->rootIndex();
62 QModelIndex previousRoot() const {
63 if (previousRootIndices.isEmpty()) {
64 return QModelIndex();
66 return previousRootIndices.top();
69 void setCurrentRoot(const QModelIndex& index) {
70 if (previousRootIndices.isEmpty() || previousRootIndices.top() != index) {
71 // we're entering into a submenu
72 //kDebug() << "pushing" << currentRootIndex.data(Qt::DisplayRole).value<QString>();
73 animLeftToRight = true;
74 hoveredIndex = QModelIndex();
75 previousRootIndices.push(currentRootIndex);
76 currentRootIndex = index;
77 previousVerticalOffsets.append(q->verticalOffset());
78 updateScrollBarRange();
79 q->verticalScrollBar()->setValue(0);
80 } else {
81 // we're exiting to the parent menu
82 //kDebug() << "popping" << previousRootIndices.top().data(Qt::DisplayRole).value<QString>();
83 animLeftToRight = false;
84 hoveredIndex = currentRootIndex;
85 previousRootIndices.pop();
86 //if (!previousRootIndices.isEmpty()) {
87 // kDebug() << "now the previos root is" << previousRootIndices.top().data(Qt::DisplayRole).value<QString>();
88 //}
89 currentRootIndex = index;
90 updateScrollBarRange();
91 q->verticalScrollBar()->setValue(previousVerticalOffsets.pop());
94 if (q->viewOptions().direction == Qt::RightToLeft) {
95 animLeftToRight = !animLeftToRight;
98 flipAnimTimeLine->setCurrentTime(0);
99 q->update();
102 int previousVerticalOffset() {
103 return previousVerticalOffsets.isEmpty() ? 0 : previousVerticalOffsets.top();
105 int treeDepth(const QModelIndex& headerIndex) const {
106 int depth = 0;
107 QModelIndex index = headerIndex;
108 while (index.isValid()) {
109 index = index.parent();
110 depth++;
112 return depth;
115 QRect headerRect(const QModelIndex& headerIndex = QModelIndex()) const {
116 Q_UNUSED(headerIndex)
117 QFontMetrics fm(KGlobalSettings::smallestReadableFont());
118 const int top = ItemDelegate::TOP_OFFSET - q->verticalScrollBar()->value();
119 int minHeight = ItemDelegate::FIRST_HEADER_HEIGHT;
121 QRect rect(backArrowRect().right() + ItemDelegate::BACK_ARROW_SPACING, top,
122 q->width() - backArrowRect().width() - ItemDelegate::BACK_ARROW_SPACING + 1,
123 qMax(fm.height(), minHeight) + 4 + ItemDelegate::HEADER_BOTTOM_MARGIN);
125 //kDebug() << "flip header is" << rect;
126 return rect;
129 void drawHeader(QPainter *painter, const QRectF& rect,
130 const QModelIndex& headerIndex, QStyleOptionViewItem options) {
131 QFontMetrics fm(KGlobalSettings::smallestReadableFont());
132 QModelIndex branchIndex = headerIndex;
134 painter->save();
135 painter->setFont(KGlobalSettings::smallestReadableFont());
136 painter->setPen(QPen(q->palette().text(), 0));
138 QString currentText = i18n("All Applications");
139 QString previousText;
140 bool ltr = options.direction == Qt::LeftToRight;
141 QString sep = ltr ? " > " : " < "; //TODO: this is very lame; use a graphical arrow instead
143 if (branchIndex.isValid()) {
144 currentText = branchIndex.data(Qt::DisplayRole).value<QString>();
145 branchIndex = branchIndex.parent();
147 while (branchIndex.isValid()) {
148 previousText.prepend(branchIndex.data(Qt::DisplayRole).value<QString>() + sep);
149 branchIndex = branchIndex.parent();
153 const qreal rightMargin = q->style()->pixelMetric(QStyle::PM_ScrollBarExtent) + 6;
154 const qreal top = rect.bottom() - fm.height() - 1 - ItemDelegate::HEADER_BOTTOM_MARGIN;
155 QRectF textRect(rect.left(), top, rect.width() - rightMargin, fm.height());
156 painter->setPen(QPen(KColorScheme(QPalette::Active).foreground(KColorScheme::InactiveText), 1));
157 painter->drawText(textRect, Qt::AlignRight | Qt::AlignVCenter, currentText);
159 if (!previousText.isEmpty()) {
160 int textWidth = fm.width(currentText) + fm.width(' ');
161 if (ltr) {
162 textRect.adjust(0, 0, -textWidth, 0);
163 } else {
164 textRect.adjust(textWidth, 0, 0, 0);
167 painter->drawText(textRect, Qt::AlignRight, previousText);
170 painter->restore();
173 void drawBackArrow(QPainter *painter, QStyle::State state) {
174 painter->save();
175 if (state & QStyle::State_MouseOver) {
176 painter->setBrush(q->palette().highlight());
177 } else {
178 painter->setBrush(q->palette().mid());
181 QRect rect = backArrowRect();
183 // background
184 painter->setPen(Qt::NoPen);
185 painter->drawRect(rect);
187 painter->setPen(QPen(q->palette().dark(), 0));
188 painter->drawLine(backArrowRect().topRight() + QPointF(0.5, 0),
189 backArrowRect().bottomRight() + QPointF(0.5, 0));
191 // centre triangle
192 if (state & QStyle::State_Enabled) {
193 painter->setPen(Qt::NoPen);
195 if (state & QStyle::State_MouseOver) {
196 painter->setBrush(q->palette().highlightedText());
197 } else {
198 painter->setBrush(q->palette().dark());
200 painter->translate(rect.center());
201 if (painter->layoutDirection() == Qt::RightToLeft) {
202 painter->rotate(180);
204 painter->drawPath(trianglePath());
205 painter->resetTransform();
207 painter->restore();
210 QPainterPath trianglePath(qreal width = 5, qreal height = 10) {
211 QPainterPath path(QPointF(-width / 2, 0.0));
212 path.lineTo(width, -height / 2);
213 path.lineTo(width, height / 2);
214 path.lineTo(-width / 2, 0.0);
216 return path;
219 QRect backArrowRect() const {
220 return QRect(0, 0, ItemDelegate::BACK_ARROW_WIDTH, q->height());
223 void updateScrollBarRange() {
224 int childCount = q->model()->rowCount(currentRootIndex);
225 int pageSize = q->height();
226 int headerHeight = headerRect(currentRoot()).height();
227 int itemH = q->sizeHintForIndex(q->model()->index(0, 0)).height();
228 q->verticalScrollBar()->setRange(0, (childCount * itemH) +
229 headerHeight - pageSize);
230 q->verticalScrollBar()->setPageStep(pageSize);
231 q->verticalScrollBar()->setSingleStep(itemH);
234 FlipScrollView * const q;
235 bool backArrowHover;
236 QPersistentModelIndex hoveredIndex;
237 QPersistentModelIndex watchedIndexForDrag;
239 QTimeLine *flipAnimTimeLine;
240 bool animLeftToRight;
242 int itemHeight;
243 static const int FLIP_ANIM_DURATION = 200;
245 private:
246 QPersistentModelIndex currentRootIndex;
247 QStack<QPersistentModelIndex> previousRootIndices;
248 QStack<int> previousVerticalOffsets;
251 FlipScrollView::FlipScrollView(QWidget *parent)
252 : QAbstractItemView(parent)
253 , d(new Private(this))
255 connect(this, SIGNAL(clicked(QModelIndex)), this, SLOT(openItem(QModelIndex)));
256 connect(d->flipAnimTimeLine, SIGNAL(valueChanged(qreal)), this, SLOT(updateFlipAnimation(qreal)));
257 d->flipAnimTimeLine->setDuration(Private::FLIP_ANIM_DURATION);
258 d->flipAnimTimeLine->setCurrentTime(Private::FLIP_ANIM_DURATION);
259 setIconSize(QSize(KIconLoader::SizeMedium, KIconLoader::SizeMedium));
260 setMouseTracking(true);
261 setAutoScroll(true);
262 QPalette viewPalette(palette());
263 viewPalette.setColor(QPalette::Window, palette().color(QPalette::Active, QPalette::Base));
264 setPalette(viewPalette);
265 setAutoFillBackground(true);
267 FlipScrollView::~FlipScrollView()
269 delete d;
271 void FlipScrollView::viewRoot()
273 QModelIndex index;
274 while (d->currentRoot().isValid()) {
275 index = d->currentRoot();
276 d->setCurrentRoot(d->currentRoot().parent());
277 setCurrentIndex(index);
279 update(d->hoveredIndex);
280 d->hoveredIndex = index;
283 QModelIndex FlipScrollView::indexAt(const QPoint& point) const
285 int topOffset = d->headerRect(d->currentRoot()).height() - verticalOffset();
286 int items = model()->rowCount(d->currentRoot());
288 int rowIndex = (point.y() - topOffset) / itemHeight();
290 QRect itemRect = rect();
291 itemRect.setTop(itemRect.top() + topOffset);
292 itemRect.setLeft(d->backArrowRect().right() + ItemDelegate::BACK_ARROW_SPACING);
294 if (rowIndex < items && itemRect.contains(point)) {
295 return model()->index(rowIndex, 0, d->currentRoot());
296 } else {
297 return QModelIndex();
301 int FlipScrollView::itemHeight() const
303 //TODO: reset on font change
304 if (d->itemHeight < 1) {
305 QModelIndex index = model()->index(0, 0, d->currentRoot());
306 d->itemHeight = sizeHintForIndex(index).height();
309 return d->itemHeight;
312 void FlipScrollView::scrollTo(const QModelIndex& index , ScrollHint hint)
314 if (!index.isValid()) {
315 return;
318 QRect itemRect = visualRect(index);
319 if (itemRect.isValid() && hint == EnsureVisible) {
320 if (itemRect.top() < 0) {
321 verticalScrollBar()->setValue(verticalScrollBar()->value() +
322 itemRect.top());
323 } else if (itemRect.bottom() > height()) {
324 verticalScrollBar()->setValue(verticalScrollBar()->value() +
325 (itemRect.bottom() - height()));
330 bool FlipScrollView::isIndexHidden(const QModelIndex&) const
332 return false;
335 QRect FlipScrollView::visualRect(const QModelIndex& index) const
337 int topOffset = d->headerRect(index.parent()).height();
338 int leftOffset = d->backArrowRect().width() + ItemDelegate::BACK_ARROW_SPACING;
340 if (index.parent() != d->currentRoot() &&
341 index.parent() != d->previousRoot() &&
342 index.parent() != (QModelIndex)d->hoveredIndex) {
343 return QRect();
346 bool parentIsPreviousRoot = d->previousRoot().isValid() && index.parent() == d->previousRoot();
347 if (parentIsPreviousRoot && d->flipAnimTimeLine->state() == QTimeLine::NotRunning) {
348 return QRect();
351 if (parentIsPreviousRoot) {
352 topOffset -= d->previousVerticalOffset();
353 } else {
354 topOffset -= verticalOffset();
358 int scrollBarWidth = verticalScrollBar()->isVisible() ? verticalScrollBar()->width() : 0;
359 int height = sizeHintForIndex(index).height();
360 QRect itemRect(leftOffset, topOffset + index.row() * height,
361 width() - leftOffset - scrollBarWidth, height);
363 int scrollBarWidth = verticalScrollBar()->isVisible() ?
364 verticalScrollBar()->width() : 0;
365 QRectF itemRect(leftOffset, topOffset + index.row() * itemHeight(),
366 width() - leftOffset - scrollBarWidth - ItemDelegate::BACK_ARROW_SPACING,
367 itemHeight());
369 const qreal timeValue = d->flipAnimTimeLine->currentValue();
370 if (index.parent() == d->currentRoot()) {
371 if (d->animLeftToRight) {
372 itemRect.translate(itemRect.width() * (1 - timeValue), 0);
373 } else {
374 itemRect.translate(-itemRect.width() * (1 - timeValue), 0);
376 } else {
377 if (d->animLeftToRight) {
378 itemRect.translate((-timeValue*itemRect.width()), 0);
379 } else {
380 itemRect.translate((timeValue*itemRect.width()), 0);
383 return itemRect.toRect();
386 int FlipScrollView::horizontalOffset() const
388 return 0;
391 int FlipScrollView::verticalOffset() const
393 return verticalScrollBar()->value();
396 QRegion FlipScrollView::visualRegionForSelection(const QItemSelection& selection) const
398 QRegion region;
399 foreach(const QModelIndex& index , selection.indexes()) {
400 region |= visualRect(index);
402 return region;
404 QModelIndex FlipScrollView::moveCursor(CursorAction cursorAction, Qt::KeyboardModifiers)
406 QModelIndex index = currentIndex();
407 // kDebug() << "Moving cursor with current index" << index.data(Qt::DisplayRole);
408 switch (cursorAction) {
409 case MoveUp:
410 if (!currentIndex().isValid()) {
411 index = model()->index(model()->rowCount(d->currentRoot()) - 1, 0, d->currentRoot());
412 } else if (currentIndex().row() > 0) {
413 index = currentIndex().sibling(currentIndex().row() - 1,
414 currentIndex().column());
416 break;
417 case MoveDown:
418 if (!currentIndex().isValid()) {
419 index = model()->index(0, 0, d->currentRoot());
420 } else if (currentIndex().row() <
421 model()->rowCount(currentIndex().parent()) - 1) {
422 index = currentIndex().sibling(currentIndex().row() + 1,
423 currentIndex().column());
425 break;
426 case MoveLeft:
427 if (d->currentRoot().isValid()) {
428 index = d->currentRoot();
429 d->setCurrentRoot(d->currentRoot().parent());
430 setCurrentIndex(index);
432 break;
433 case MoveRight:
434 if (model()->hasChildren(currentIndex())) {
435 openItem(currentIndex());
436 // return the new current index set by openItem()
437 index = currentIndex();
439 break;
440 default:
441 // Do nothing
442 break;
445 // clear the hovered index
446 update(d->hoveredIndex);
447 d->hoveredIndex = index;
449 //kDebug() << "New index after move" << index.data(Qt::DisplayRole);
451 return index;
454 void FlipScrollView::setSelection(const QRect& rect , QItemSelectionModel::SelectionFlags flags)
456 QItemSelection selection;
457 selection.select(indexAt(rect.topLeft()), indexAt(rect.bottomRight()));
458 selectionModel()->select(selection, flags);
461 void FlipScrollView::openItem(const QModelIndex& index)
463 if (model()->canFetchMore(index)) {
464 model()->fetchMore(index);
467 bool hasChildren = model()->hasChildren(index);
469 if (hasChildren) {
470 d->setCurrentRoot(index);
471 setCurrentIndex(model()->index(0, 0, index));
472 } else {
473 //TODO Emit a signal to open/execute the item
477 void FlipScrollView::resizeEvent(QResizeEvent*)
479 d->updateScrollBarRange();
482 void FlipScrollView::mousePressEvent(QMouseEvent *event)
484 d->watchedIndexForDrag = indexAt(event->pos());
485 QAbstractItemView::mousePressEvent(event);
488 void FlipScrollView::mouseReleaseEvent(QMouseEvent *event)
490 d->watchedIndexForDrag = QModelIndex();
492 if (d->backArrowRect().contains(event->pos()) && d->currentRoot().isValid()) {
493 // go up one level
494 d->setCurrentRoot(d->currentRoot().parent());
495 setDirtyRegion(rect());
496 } else {
497 QAbstractItemView::mouseReleaseEvent(event);
501 void FlipScrollView::mouseMoveEvent(QMouseEvent *event)
503 bool mouseOverBackArrow = d->backArrowRect().contains(event->pos());
505 if (mouseOverBackArrow != d->backArrowHover) {
506 d->backArrowHover = mouseOverBackArrow;
507 setDirtyRegion(d->backArrowRect());
508 } else {
509 const QModelIndex itemUnderMouse = indexAt(event->pos());
510 if (itemUnderMouse != d->hoveredIndex) {
511 update(itemUnderMouse);
512 update(d->hoveredIndex);
514 d->hoveredIndex = itemUnderMouse;
515 setCurrentIndex(d->hoveredIndex);
518 QAbstractItemView::mouseMoveEvent(event);
522 void FlipScrollView::keyPressEvent(QKeyEvent *event)
524 if (event->key() == Qt::Key_Enter ||
525 event->key() == Qt::Key_Return) {
526 moveCursor(MoveRight, event->modifiers());
527 event->accept();
528 return;
531 if (event->key() == Qt::Key_Escape &&
532 d->currentRoot().isValid()) {
533 moveCursor(MoveLeft, event->modifiers());
534 event->accept();
535 return;
538 QAbstractItemView::keyPressEvent(event);
541 void FlipScrollView::leaveEvent(QEvent *event)
543 d->hoveredIndex = QModelIndex();
544 setCurrentIndex(QModelIndex());
547 void FlipScrollView::paintItems(QPainter &painter, QPaintEvent *event, QModelIndex &root)
549 const int rows = model()->rowCount(root);
550 //kDebug() << "painting" << rows << "items";
552 for (int i = 0; i < rows; ++i) {
553 QModelIndex index = model()->index(i, 0, root);
555 QStyleOptionViewItem option = viewOptions();
556 option.rect = visualRect(index);
558 // only draw items intersecting the region of the widget
559 // being updated
560 if (!event->rect().intersects(option.rect)) {
561 continue;
564 if (selectionModel()->isSelected(index)) {
565 option.state |= QStyle::State_Selected;
568 if (index == d->hoveredIndex) {
569 option.state |= QStyle::State_MouseOver;
572 if (index == currentIndex()) {
573 option.state |= QStyle::State_HasFocus;
576 itemDelegate(index)->paint(&painter, option, index);
578 if (model()->hasChildren(index)) {
579 painter.save();
580 painter.setPen(Qt::NoPen);
581 // there is an assumption made here that the delegate will fill the background
582 // with the selected color or some similar color which contrasts well with the
583 // highlighted text color
584 if (option.state & QStyle::State_MouseOver) {
585 painter.setBrush(palette().highlight());
586 } else {
587 painter.setBrush(palette().text());
590 QRect triRect = option.rect;
591 QPainterPath tPath = d->trianglePath();
592 if (option.direction == Qt::LeftToRight) {
593 triRect.setLeft(triRect.right() - ItemDelegate::ITEM_RIGHT_MARGIN);
594 painter.translate(triRect.center().x() - 6, triRect.y() + (option.rect.height() / 2));
596 } else {
597 triRect.setRight(triRect.left() + ItemDelegate::ITEM_RIGHT_MARGIN);
598 painter.translate(triRect.center().x() + 6, triRect.y() + (option.rect.height() / 2));
603 if (option.direction == Qt::LeftToRight) {
604 painter.rotate(180);
607 painter.drawPath(tPath);
608 painter.resetTransform();
609 painter.restore();
614 void FlipScrollView::paintEvent(QPaintEvent * event)
616 QPalette viewPalette(palette());
617 viewPalette.setColor(QPalette::Window, palette().color(QPalette::Active, QPalette::Base));
618 setPalette(viewPalette);
619 setAutoFillBackground(true);
621 QPainter painter(viewport());
622 painter.setRenderHint(QPainter::Antialiasing);
624 // draw items
625 QModelIndex currentRoot = d->currentRoot();
626 QModelIndex previousRoot = d->animLeftToRight ? d->previousRoot() : (QModelIndex)d->hoveredIndex;
627 //kDebug() << "current root is" << currentRoot.data(Qt::DisplayRole).value<QString>();
629 paintItems(painter, event, currentRoot);
631 const qreal timerValue = d->flipAnimTimeLine->currentValue();
633 if (timerValue < 1.0) {
634 //kDebug() << "previous root is" << previousRoot.data(Qt::DisplayRole).value<QString>();
635 paintItems(painter, event, previousRoot);
637 if (d->flipAnimTimeLine->state() != QTimeLine::Running) {
638 d->flipAnimTimeLine->start();
642 QRectF eventRect = event->rect();
644 // draw header for current view
645 QRectF headerRect = d->headerRect(currentRoot);
646 if (d->animLeftToRight) {
647 headerRect.translate(headerRect.width() * (1 - timerValue), 0);
648 } else {
649 headerRect.translate(-headerRect.width() * (1 - timerValue), 0);
652 if (eventRect.intersects(headerRect)) {
653 d->drawHeader(&painter, headerRect, currentRoot, viewOptions());
656 // draw header for previous view
657 QRectF prevHeaderRect = d->headerRect(previousRoot);
658 if (d->animLeftToRight) {
659 prevHeaderRect.translate(-prevHeaderRect.width() * timerValue, 0);
660 } else {
661 prevHeaderRect.translate(prevHeaderRect.width() * timerValue, 0);
664 if (eventRect.intersects(prevHeaderRect) && timerValue < 1.0) {
665 d->drawHeader(&painter, prevHeaderRect, previousRoot, viewOptions());
668 // draw navigation
669 QStyle::State state = 0;
670 if (currentRoot.isValid()) {
671 state |= QStyle::State_Enabled;
674 if (d->backArrowHover) {
675 state |= QStyle::State_MouseOver;
678 if (currentRoot.isValid() || previousRoot.isValid()) {
679 qreal opacity = 1.0;
680 if (!previousRoot.isValid()) {
681 opacity = timerValue;
682 } else if (!currentRoot.isValid()) {
683 opacity = 1 - timerValue;
686 painter.save();
687 painter.setOpacity(opacity);
688 d->drawBackArrow(&painter, state);
689 painter.restore();
693 void FlipScrollView::startDrag(Qt::DropActions supportedActions)
695 kDebug() << "Starting UrlItemView drag with actions" << supportedActions;
697 if (!d->watchedIndexForDrag.isValid()) {
698 return;
701 QDrag *drag = new QDrag(this);
702 QMimeData *mimeData = model()->mimeData(selectionModel()->selectedIndexes());
704 if (mimeData->text().isNull()) {
705 return;
708 drag->setMimeData(mimeData);
710 QModelIndex idx = selectionModel()->selectedIndexes().first();
711 QIcon icon = idx.data(Qt::DecorationRole).value<QIcon>();
712 drag->setPixmap(icon.pixmap(IconSize(KIconLoader::Desktop)));
714 Qt::DropAction dropAction = drag->exec();
715 QAbstractItemView::startDrag(supportedActions);
718 void FlipScrollView::updateFlipAnimation(qreal)
720 setDirtyRegion(rect());
723 #include "flipscrollview.moc"