/* Copyright 2007 Robert Knight This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // Own #include "ui/flipscrollview.h" // Qt #include #include #include #include #include #include #include // KDE #include #include #include #include #include "ui/itemdelegate.h" using namespace Kickoff; class FlipScrollView::Private { public: Private(FlipScrollView *view) : q(view) , flipAnimTimeLine(new QTimeLine()) , animLeftToRight(true) , itemHeight(-1) { } ~Private() { delete flipAnimTimeLine; } QModelIndex currentRoot() const { if (currentRootIndex.isValid()) { return currentRootIndex; } else { return q->rootIndex(); } } QModelIndex previousRoot() const { if (previousRootIndices.isEmpty()) { return QModelIndex(); } return previousRootIndices.top(); } void setCurrentRoot(const QModelIndex& index) { if (previousRootIndices.isEmpty() || previousRootIndices.top() != index) { // we're entering into a submenu //kDebug() << "pushing" << currentRootIndex.data(Qt::DisplayRole).value(); animLeftToRight = true; hoveredIndex = QModelIndex(); previousRootIndices.push(currentRootIndex); currentRootIndex = index; previousVerticalOffsets.append(q->verticalOffset()); updateScrollBarRange(); q->verticalScrollBar()->setValue(0); } else { // we're exiting to the parent menu //kDebug() << "popping" << previousRootIndices.top().data(Qt::DisplayRole).value(); animLeftToRight = false; hoveredIndex = currentRootIndex; previousRootIndices.pop(); //if (!previousRootIndices.isEmpty()) { // kDebug() << "now the previos root is" << previousRootIndices.top().data(Qt::DisplayRole).value(); //} currentRootIndex = index; updateScrollBarRange(); q->verticalScrollBar()->setValue(previousVerticalOffsets.pop()); } emit q->currentRootChanged(index); if (q->viewOptions().direction == Qt::RightToLeft) { animLeftToRight = !animLeftToRight; } flipAnimTimeLine->setCurrentTime(0); q->update(); } int previousVerticalOffset() { return previousVerticalOffsets.isEmpty() ? 0 : previousVerticalOffsets.top(); } int treeDepth(const QModelIndex& headerIndex) const { int depth = 0; QModelIndex index = headerIndex; while (index.isValid()) { index = index.parent(); depth++; } return depth; } QPainterPath trianglePath(qreal width = 5, qreal height = 10) { QPainterPath path(QPointF(-width / 2, 0.0)); path.lineTo(width, -height / 2); path.lineTo(width, height / 2); path.lineTo(-width / 2, 0.0); return path; } void updateScrollBarRange() { int childCount = q->model()->rowCount(currentRootIndex); int pageSize = q->height(); int itemH = q->sizeHintForIndex(q->model()->index(0, 0)).height(); q->verticalScrollBar()->setRange(0, (childCount * itemH) - pageSize); q->verticalScrollBar()->setPageStep(pageSize); q->verticalScrollBar()->setSingleStep(itemH); } FlipScrollView * const q; QPersistentModelIndex hoveredIndex; QPersistentModelIndex watchedIndexForDrag; QTimeLine *flipAnimTimeLine; bool animLeftToRight; int itemHeight; static const int FLIP_ANIM_DURATION = 200; private: QPersistentModelIndex currentRootIndex; QStack previousRootIndices; QStack previousVerticalOffsets; }; FlipScrollView::FlipScrollView(QWidget *parent) : QAbstractItemView(parent) , d(new Private(this)) { connect(this, SIGNAL(clicked(QModelIndex)), this, SLOT(openItem(QModelIndex))); connect(d->flipAnimTimeLine, SIGNAL(valueChanged(qreal)), this, SLOT(updateFlipAnimation(qreal))); d->flipAnimTimeLine->setDuration(Private::FLIP_ANIM_DURATION); d->flipAnimTimeLine->setCurrentTime(Private::FLIP_ANIM_DURATION); setIconSize(QSize(KIconLoader::SizeMedium, KIconLoader::SizeMedium)); setMouseTracking(true); setAutoScroll(true); QPalette viewPalette(palette()); viewPalette.setColor(QPalette::Window, palette().color(QPalette::Active, QPalette::Base)); setPalette(viewPalette); setAutoFillBackground(true); } FlipScrollView::~FlipScrollView() { delete d; } void FlipScrollView::setCurrentRoot(const QModelIndex &index) { d->setCurrentRoot(index); } void FlipScrollView::viewRoot() { QModelIndex index; while (d->currentRoot().isValid()) { index = d->currentRoot(); d->setCurrentRoot(d->currentRoot().parent()); setCurrentIndex(index); } update(d->hoveredIndex); d->hoveredIndex = index; } QModelIndex FlipScrollView::indexAt(const QPoint& point) const { const int items = model()->rowCount(d->currentRoot()); const int rowIndex = (point.y() + verticalOffset()) / itemHeight(); if (rowIndex < items) { return model()->index(rowIndex, 0, d->currentRoot()); } else { return QModelIndex(); } } int FlipScrollView::itemHeight() const { //TODO: reset on font change if (d->itemHeight < 1) { QModelIndex index = model()->index(0, 0, d->currentRoot()); d->itemHeight = sizeHintForIndex(index).height(); } return d->itemHeight; } void FlipScrollView::scrollTo(const QModelIndex& index, ScrollHint hint) { if (!index.isValid()) { return; } const QRect itemRect = visualRect(index); if (itemRect.isValid() && hint == EnsureVisible) { if (itemRect.top() < 0) { verticalScrollBar()->setValue(verticalScrollBar()->value() + itemRect.top()); } else if (itemRect.bottom() > height()) { verticalScrollBar()->setValue(verticalScrollBar()->value() + (itemRect.bottom() - height())); } update(itemRect); } } bool FlipScrollView::isIndexHidden(const QModelIndex&) const { return false; } QRect FlipScrollView::visualRect(const QModelIndex& index) const { const int leftOffset = ItemDelegate::ITEM_LEFT_MARGIN; if (index.parent() != d->currentRoot() && index.parent() != d->previousRoot() && index.parent() != (QModelIndex)d->hoveredIndex) { return QRect(); } const bool parentIsPreviousRoot = d->previousRoot().isValid() && index.parent() == d->previousRoot(); if (parentIsPreviousRoot && d->flipAnimTimeLine->state() == QTimeLine::NotRunning) { return QRect(); } const int scrollBarWidth = verticalScrollBar()->isVisible() ? verticalScrollBar()->width() : 0; QRect itemRect(leftOffset, index.row() * itemHeight() - verticalOffset() , width() - leftOffset - scrollBarWidth, itemHeight()); const qreal timeValue = d->flipAnimTimeLine->currentValue(); if (index.parent() == d->currentRoot()) { if (d->animLeftToRight) { itemRect.translate(itemRect.width() * (1 - timeValue), 0); } else { itemRect.translate(-itemRect.width() * (1 - timeValue), 0); } } else { if (d->animLeftToRight) { itemRect.translate((-timeValue*itemRect.width()), 0); } else { itemRect.translate((timeValue*itemRect.width()), 0); } } return itemRect; } int FlipScrollView::horizontalOffset() const { return 0; } int FlipScrollView::verticalOffset() const { return verticalScrollBar()->value(); } QRegion FlipScrollView::visualRegionForSelection(const QItemSelection& selection) const { QRegion region; foreach(const QModelIndex& index , selection.indexes()) { region |= visualRect(index); } return region; } QModelIndex FlipScrollView::moveCursor(CursorAction cursorAction, Qt::KeyboardModifiers) { QModelIndex index = currentIndex(); // kDebug() << "Moving cursor with current index" << index.data(Qt::DisplayRole); switch (cursorAction) { case MoveUp: if (!currentIndex().isValid()) { index = model()->index(model()->rowCount(d->currentRoot()) - 1, 0, d->currentRoot()); } else if (currentIndex().row() > 0) { index = currentIndex().sibling(currentIndex().row() - 1, currentIndex().column()); } break; case MoveDown: if (!currentIndex().isValid()) { index = model()->index(0, 0, d->currentRoot()); } else if (currentIndex().row() < model()->rowCount(currentIndex().parent()) - 1) { index = currentIndex().sibling(currentIndex().row() + 1, currentIndex().column()); } break; case MoveLeft: if (d->currentRoot().isValid()) { index = d->currentRoot(); d->setCurrentRoot(d->currentRoot().parent()); setCurrentIndex(index); } break; case MoveRight: if (model()->hasChildren(currentIndex())) { openItem(currentIndex()); // return the new current index set by openItem() index = currentIndex(); } break; default: // Do nothing break; } // clear the hovered index update(d->hoveredIndex); d->hoveredIndex = index; //kDebug() << "New index after move" << index.data(Qt::DisplayRole); return index; } void FlipScrollView::setSelection(const QRect& rect , QItemSelectionModel::SelectionFlags flags) { QItemSelection selection; selection.select(indexAt(rect.topLeft()), indexAt(rect.bottomRight())); selectionModel()->select(selection, flags); } void FlipScrollView::openItem(const QModelIndex& index) { if (model()->canFetchMore(index)) { model()->fetchMore(index); } const bool hasChildren = model()->hasChildren(index); if (hasChildren) { d->setCurrentRoot(index); setCurrentIndex(model()->index(0, 0, index)); } else { //TODO Emit a signal to open/execute the item } } void FlipScrollView::resizeEvent(QResizeEvent*) { d->updateScrollBarRange(); } void FlipScrollView::mousePressEvent(QMouseEvent *event) { d->watchedIndexForDrag = indexAt(event->pos()); QAbstractItemView::mousePressEvent(event); } void FlipScrollView::mouseReleaseEvent(QMouseEvent *event) { d->watchedIndexForDrag = QModelIndex(); QAbstractItemView::mouseReleaseEvent(event); } void FlipScrollView::mouseMoveEvent(QMouseEvent *event) { const QModelIndex itemUnderMouse = indexAt(event->pos()); if (itemUnderMouse != d->hoveredIndex) { update(itemUnderMouse); update(d->hoveredIndex); d->hoveredIndex = itemUnderMouse; setCurrentIndex(d->hoveredIndex); } QAbstractItemView::mouseMoveEvent(event); } void FlipScrollView::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Enter || event->key() == Qt::Key_Return || event->key() == Qt::Key_Right) { moveCursor(MoveRight, event->modifiers()); event->accept(); return; } if (event->key() == Qt::Key_Escape || event->key() == Qt::Key_Left) { if (d->currentRoot().isValid()) { moveCursor(MoveLeft, event->modifiers()); event->accept(); return; } else { // we are already in the leftmost column. kDebug() << "we are in Left-Most column, processing Key_Left"; event->accept(); emit focusNextViewLeft(); return; } } QAbstractItemView::keyPressEvent(event); } void FlipScrollView::leaveEvent(QEvent *event) { Q_UNUSED(event); d->hoveredIndex = QModelIndex(); setCurrentIndex(QModelIndex()); } void FlipScrollView::paintItems(QPainter &painter, QPaintEvent *event, QModelIndex &root) { const int rows = model()->rowCount(root); //kDebug() << "painting" << rows << "items"; for (int i = 0; i < rows; ++i) { QModelIndex index = model()->index(i, 0, root); QStyleOptionViewItem option = viewOptions(); option.rect = visualRect(index); // only draw items intersecting the region of the widget // being updated if (!event->rect().intersects(option.rect)) { continue; } if (selectionModel()->isSelected(index)) { option.state |= QStyle::State_Selected; } if (index == d->hoveredIndex) { option.state |= QStyle::State_MouseOver; } if (index == currentIndex()) { option.state |= QStyle::State_HasFocus; } itemDelegate(index)->paint(&painter, option, index); if (model()->hasChildren(index)) { painter.save(); painter.setPen(Qt::NoPen); // there is an assumption made here that the delegate will fill the background // with the selected color or some similar color which contrasts well with the // highlighted text color if (option.state & QStyle::State_MouseOver) { painter.setBrush(palette().highlight()); } else { painter.setBrush(palette().text()); } QRect triRect = option.rect; QPainterPath tPath = d->trianglePath(); if (option.direction == Qt::LeftToRight) { triRect.setLeft(triRect.right() - ItemDelegate::ITEM_RIGHT_MARGIN); painter.translate(triRect.center().x() - 6, triRect.y() + (option.rect.height() / 2)); } else { triRect.setRight(triRect.left() + ItemDelegate::ITEM_RIGHT_MARGIN); painter.translate(triRect.center().x() + 6, triRect.y() + (option.rect.height() / 2)); } if (option.direction == Qt::LeftToRight) { painter.rotate(180); } painter.drawPath(tPath); painter.resetTransform(); painter.restore(); } } } void FlipScrollView::paintEvent(QPaintEvent * event) { QPalette viewPalette(palette()); viewPalette.setColor(QPalette::Window, palette().color(QPalette::Active, QPalette::Base)); setPalette(viewPalette); setAutoFillBackground(true); QPainter painter(viewport()); painter.setRenderHint(QPainter::Antialiasing); // draw items QModelIndex currentRoot = d->currentRoot(); QModelIndex previousRoot = d->animLeftToRight ? d->previousRoot() : (QModelIndex)d->hoveredIndex; //kDebug() << "current root is" << currentRoot.data(Qt::DisplayRole).value(); paintItems(painter, event, currentRoot); const qreal timerValue = d->flipAnimTimeLine->currentValue(); if (timerValue < 1.0) { //kDebug() << "previous root is" << previousRoot.data(Qt::DisplayRole).value(); paintItems(painter, event, previousRoot); if (d->flipAnimTimeLine->state() != QTimeLine::Running) { d->flipAnimTimeLine->start(); } } // draw navigation QStyle::State state = 0; if (currentRoot.isValid()) { state |= QStyle::State_Enabled; } if (currentRoot.isValid() || previousRoot.isValid()) { qreal opacity = 1.0; if (!previousRoot.isValid()) { opacity = timerValue; } else if (!currentRoot.isValid()) { opacity = 1 - timerValue; } } } void FlipScrollView::startDrag(Qt::DropActions supportedActions) { kDebug() << "Starting UrlItemView drag with actions" << supportedActions; if (!d->watchedIndexForDrag.isValid()) { return; } QDrag *drag = new QDrag(this); QMimeData *mimeData = model()->mimeData(selectionModel()->selectedIndexes()); if (mimeData->text().isNull()) { return; } drag->setMimeData(mimeData); QModelIndex idx = selectionModel()->selectedIndexes().first(); QIcon icon = idx.data(Qt::DecorationRole).value(); drag->setPixmap(icon.pixmap(IconSize(KIconLoader::Desktop))); drag->exec(); } void FlipScrollView::updateFlipAnimation(qreal) { setDirtyRegion(rect()); } #include "moc_flipscrollview.cpp"