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.
21 #include "ui/flipscrollview.h"
24 #include <QMouseEvent>
32 #include <KGlobalSettings>
33 #include <KIconLoader>
34 #include <KColorScheme>
36 #include "ui/itemdelegate.h"
38 using namespace Kickoff
;
40 class FlipScrollView::Private
43 Private(FlipScrollView
*view
)
45 , backArrowHover(false)
46 , flipAnimTimeLine(new QTimeLine())
47 , animLeftToRight(true)
52 delete flipAnimTimeLine
;
55 QModelIndex
currentRoot() const {
56 if (currentRootIndex
.isValid()) {
57 return currentRootIndex
;
59 return q
->rootIndex();
62 QModelIndex
previousRoot() const {
63 if (previousRootIndices
.isEmpty()) {
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);
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>();
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);
102 int previousVerticalOffset() {
103 return previousVerticalOffsets
.isEmpty() ? 0 : previousVerticalOffsets
.top();
105 int treeDepth(const QModelIndex
& headerIndex
) const {
107 QModelIndex index
= headerIndex
;
108 while (index
.isValid()) {
109 index
= index
.parent();
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;
129 void drawHeader(QPainter
*painter
, const QRectF
& rect
,
130 const QModelIndex
& headerIndex
, QStyleOptionViewItem options
) {
131 QFontMetrics
fm(KGlobalSettings::smallestReadableFont());
132 QModelIndex branchIndex
= headerIndex
;
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(' ');
162 textRect
.adjust(0, 0, -textWidth
, 0);
164 textRect
.adjust(textWidth
, 0, 0, 0);
167 painter
->drawText(textRect
, Qt::AlignRight
, previousText
);
173 void drawBackArrow(QPainter
*painter
, QStyle::State state
) {
175 if (state
& QStyle::State_MouseOver
) {
176 painter
->setBrush(q
->palette().highlight());
178 painter
->setBrush(q
->palette().mid());
181 QRect rect
= backArrowRect();
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));
192 if (state
& QStyle::State_Enabled
) {
193 painter
->setPen(Qt::NoPen
);
195 if (state
& QStyle::State_MouseOver
) {
196 painter
->setBrush(q
->palette().highlightedText());
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();
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);
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
;
236 QPersistentModelIndex hoveredIndex
;
237 QPersistentModelIndex watchedIndexForDrag
;
239 QTimeLine
*flipAnimTimeLine
;
240 bool animLeftToRight
;
243 static const int FLIP_ANIM_DURATION
= 200;
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);
262 QPalette
viewPalette(palette());
263 viewPalette
.setColor(QPalette::Window
, palette().color(QPalette::Active
, QPalette::Base
));
264 setPalette(viewPalette
);
265 setAutoFillBackground(true);
267 FlipScrollView::~FlipScrollView()
271 void FlipScrollView::viewRoot()
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());
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()) {
318 QRect itemRect
= visualRect(index
);
319 if (itemRect
.isValid() && hint
== EnsureVisible
) {
320 if (itemRect
.top() < 0) {
321 verticalScrollBar()->setValue(verticalScrollBar()->value() +
323 } else if (itemRect
.bottom() > height()) {
324 verticalScrollBar()->setValue(verticalScrollBar()->value() +
325 (itemRect
.bottom() - height()));
330 bool FlipScrollView::isIndexHidden(const QModelIndex
&) const
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
) {
346 bool parentIsPreviousRoot
= d
->previousRoot().isValid() && index
.parent() == d
->previousRoot();
347 if (parentIsPreviousRoot
&& d
->flipAnimTimeLine
->state() == QTimeLine::NotRunning
) {
351 if (parentIsPreviousRoot
) {
352 topOffset
-= d
->previousVerticalOffset();
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
,
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);
374 itemRect
.translate(-itemRect
.width() * (1 - timeValue
), 0);
377 if (d
->animLeftToRight
) {
378 itemRect
.translate((-timeValue
*itemRect
.width()), 0);
380 itemRect
.translate((timeValue
*itemRect
.width()), 0);
383 return itemRect
.toRect();
386 int FlipScrollView::horizontalOffset() const
391 int FlipScrollView::verticalOffset() const
393 return verticalScrollBar()->value();
396 QRegion
FlipScrollView::visualRegionForSelection(const QItemSelection
& selection
) const
399 foreach(const QModelIndex
& index
, selection
.indexes()) {
400 region
|= visualRect(index
);
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
) {
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());
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());
427 if (d
->currentRoot().isValid()) {
428 index
= d
->currentRoot();
429 d
->setCurrentRoot(d
->currentRoot().parent());
430 setCurrentIndex(index
);
434 if (model()->hasChildren(currentIndex())) {
435 openItem(currentIndex());
436 // return the new current index set by openItem()
437 index
= currentIndex();
445 // clear the hovered index
446 update(d
->hoveredIndex
);
447 d
->hoveredIndex
= index
;
449 //kDebug() << "New index after move" << index.data(Qt::DisplayRole);
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
);
470 d
->setCurrentRoot(index
);
471 setCurrentIndex(model()->index(0, 0, index
));
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()) {
494 d
->setCurrentRoot(d
->currentRoot().parent());
495 setDirtyRegion(rect());
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());
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());
531 if (event
->key() == Qt::Key_Escape
&&
532 d
->currentRoot().isValid()) {
533 moveCursor(MoveLeft
, event
->modifiers());
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
560 if (!event
->rect().intersects(option
.rect
)) {
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
)) {
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());
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));
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
) {
607 painter
.drawPath(tPath
);
608 painter
.resetTransform();
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
);
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);
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);
661 prevHeaderRect
.translate(prevHeaderRect
.width() * timerValue
, 0);
664 if (eventRect
.intersects(prevHeaderRect
) && timerValue
< 1.0) {
665 d
->drawHeader(&painter
, prevHeaderRect
, previousRoot
, viewOptions());
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()) {
680 if (!previousRoot
.isValid()) {
681 opacity
= timerValue
;
682 } else if (!currentRoot
.isValid()) {
683 opacity
= 1 - timerValue
;
687 painter
.setOpacity(opacity
);
688 d
->drawBackArrow(&painter
, state
);
693 void FlipScrollView::startDrag(Qt::DropActions supportedActions
)
695 kDebug() << "Starting UrlItemView drag with actions" << supportedActions
;
697 if (!d
->watchedIndexForDrag
.isValid()) {
701 QDrag
*drag
= new QDrag(this);
702 QMimeData
*mimeData
= model()->mimeData(selectionModel()->selectedIndexes());
704 if (mimeData
->text().isNull()) {
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"