/* * Copyright © 2008, 2009 Fredrik Höglund * * The smooth scrolling code is based on the code in KHTMLView, * Copyright © 2006-2008 Germain Garand * Copyright © 2008 Allan Sandfeld Jensen * * 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. */ #include "abstractitemview.h" #include "proxymodel.h" #include "style.h" #include #include #include #include #include #include #include static const int sSmoothScrollTime = 140; static const int sSmoothScrollTick = 14; AbstractItemView::AbstractItemView(QGraphicsWidget *parent) : QGraphicsWidget(parent), m_itemFrame(0), m_delegate(0), m_lastScrollValue(0), m_viewScrolled(false), m_dx(0), m_ddx(0), m_dddx(0), m_rdx(0), m_dy(0), m_ddy(0), m_dddy(0), m_rdy(0), m_smoothScrolling(false), m_autoScrollSpeed(0), m_drawShadows(true) { m_scrollBar = new Plasma::ScrollBar(this); connect(m_scrollBar, SIGNAL(valueChanged(int)), SLOT(scrollBarValueChanged(int))); connect(m_scrollBar->nativeWidget(), SIGNAL(actionTriggered(int)), SLOT(scrollBarActionTriggered(int))); connect(m_scrollBar->nativeWidget(), SIGNAL(sliderReleased()), SLOT(scrollBarSliderReleased())); m_itemFrame = new Plasma::FrameSvg(this); m_itemFrame->setImagePath("widgets/viewitem"); m_itemFrame->setCacheAllRenderedFrames(true); m_itemFrame->setElementPrefix("normal"); connect(m_itemFrame, SIGNAL(repaintNeeded()), this, SLOT(svgChanged())); // This is a dummy widget that's never shown - it's just passed to // KFileItemDelegate in the style options, so it will use the widget's // style to draw the view item backgrounds. m_styleWidget = new QWidget; m_style = new FolderViewStyle; m_styleWidget->setStyle(m_style); const int size = style()->pixelMetric(QStyle::PM_LargeIconSize); m_iconSize = QSize(size, size); } AbstractItemView::~AbstractItemView() { delete m_styleWidget; delete m_style; } void AbstractItemView::setModel(QAbstractItemModel *model) { m_model = static_cast(model); m_dirModel = static_cast(m_model->sourceModel()); connect(m_model, SIGNAL(rowsInserted(QModelIndex,int,int)), SLOT(rowsInserted(QModelIndex,int,int))); connect(m_model, SIGNAL(rowsRemoved(QModelIndex,int,int)), SLOT(rowsRemoved(QModelIndex,int,int))); connect(m_model, SIGNAL(modelReset()), SLOT(modelReset())); connect(m_model, SIGNAL(layoutChanged()), SLOT(layoutChanged())); connect(m_model, SIGNAL(dataChanged(QModelIndex,QModelIndex)), SLOT(dataChanged(QModelIndex,QModelIndex))); } QAbstractItemModel *AbstractItemView::model() const { return m_model; } void AbstractItemView::setSelectionModel(QItemSelectionModel *model) { m_selectionModel = model; } QItemSelectionModel *AbstractItemView::selectionModel() const { return m_selectionModel; } void AbstractItemView::setItemDelegate(KFileItemDelegate *delegate) { m_delegate = static_cast(delegate); connect(m_delegate, SIGNAL(closeEditor(QWidget*,QAbstractItemDelegate::EndEditHint)), SLOT(closeEditor(QWidget*,QAbstractItemDelegate::EndEditHint))); connect(m_delegate, SIGNAL(commitData(QWidget*)), SLOT(commitData(QWidget*))); } KFileItemDelegate *AbstractItemView::itemDelegate() const { return m_delegate; } void AbstractItemView::setIconSize(const QSize &iconSize) { m_iconSize = iconSize; } QSize AbstractItemView::iconSize() const { return m_iconSize; } void AbstractItemView::setDrawShadows(bool on) { if (m_drawShadows != on) { m_drawShadows = on; markAreaDirty(visibleArea()); update(); } } bool AbstractItemView::drawShadows() const { return m_drawShadows; } QScrollBar *AbstractItemView::verticalScrollBar() const { return m_scrollBar->nativeWidget(); } QRect AbstractItemView::visibleArea() const { return mapToViewport(contentsRect()).toAlignedRect(); } // Marks the given rect in viewport coordinates, as dirty and schedules a repaint. void AbstractItemView::markAreaDirty(const QRect &rect) { if (!rect.isEmpty() && rect.intersects(visibleArea())) { m_dirtyRegion += rect; update(mapFromViewport(rect)); } } // This function scrolls the contents of the backbuffer the distance the scrollbar // has moved since the last time this function was called. QRect AbstractItemView::scrollBackBuffer() { const int value = m_scrollBar->value(); const int delta = m_lastScrollValue - value; m_lastScrollValue = value; if (qAbs(delta) >= m_pixmap.height()) { return visibleArea(); } QRect dirty = m_pixmap.rect(); return mapToViewport(dirty.translated(contentsRect().topLeft().toPoint())).toAlignedRect(); } void AbstractItemView::prepareBackBuffer() { const QRect cr = contentsRect().toRect(); // Make sure the backbuffer pixmap has the same size as the content rect if (m_pixmap.size() != cr.size()) { QPixmap pixmap(cr.size()); pixmap.fill(Qt::transparent); if (!m_pixmap.isNull()) { // Static content optimization { QPainter p(&pixmap); p.setCompositionMode(QPainter::CompositionMode_Source); p.drawPixmap(0, 0, m_pixmap); } QRegion region(pixmap.rect()); region -= m_pixmap.rect(); region.translate(0, m_scrollBar->value()); m_dirtyRegion |= region; } else { m_dirtyRegion = QRegion(visibleArea()); } m_pixmap = pixmap; } if (m_viewScrolled) { m_dirtyRegion += scrollBackBuffer(); m_viewScrolled = false; } } // This function draws the backbuffer pixmap on the widget, and fades out the top // and bottom if as needed. void AbstractItemView::syncBackBuffer(QPainter *painter, const QRect &clipRect) { const QRect cr = contentsRect().toRect(); const int fadeHeight = 16; const QRect topFadeRect(cr.x(), cr.y(), cr.width(), fadeHeight); const QRect bottomFadeRect(cr.bottomLeft() - QPoint(0, fadeHeight), QSize(cr.width(), fadeHeight)); int scrollValue = m_scrollBar->value(); int maxValue = m_scrollBar->maximum(); // Draw the backbuffer on the widget // ================================= if ((scrollValue > 0 && topFadeRect.intersects(clipRect)) || (scrollValue < maxValue && bottomFadeRect.intersects(clipRect))) { QPixmap pixmap = m_pixmap; QPainter p(&pixmap); p.setCompositionMode(QPainter::CompositionMode_DestinationIn); // Fade out the top section of the pixmap if the scrollbar slider isn't at the top if (scrollValue > 0 && topFadeRect.intersects(clipRect)) { if (m_topFadeTile.isNull()) { m_topFadeTile = QPixmap(256, fadeHeight); m_topFadeTile.fill(Qt::transparent); QLinearGradient g(0, 0, 0, fadeHeight); g.setColorAt(0, Qt::transparent); g.setColorAt(1, Qt::black); QPainter p(&m_topFadeTile); p.setCompositionMode(QPainter::CompositionMode_Source); p.fillRect(0, 0, 256, fadeHeight, g); p.end(); } p.drawTiledPixmap(0, 0, m_pixmap.width(), fadeHeight, m_topFadeTile); } // Fade out the bottom part of the pixmap if the scrollbar slider isn't at the bottom if (scrollValue < maxValue && bottomFadeRect.intersects(clipRect)) { if (m_topFadeTile.isNull()) { m_bottomFadeTile = QPixmap(256, fadeHeight); m_bottomFadeTile.fill(Qt::transparent); QLinearGradient g(0, 0, 0, fadeHeight); g.setColorAt(0, Qt::black); g.setColorAt(1, Qt::transparent); QPainter p(&m_bottomFadeTile); p.setCompositionMode(QPainter::CompositionMode_Source); p.fillRect(0, 0, 256, fadeHeight, g); p.end(); } p.drawTiledPixmap(0, m_pixmap.height() - fadeHeight, m_pixmap.width(), fadeHeight, m_bottomFadeTile); } p.end(); painter->drawPixmap(cr.topLeft(), pixmap); } else { painter->drawPixmap(cr.topLeft(), m_pixmap); } } QSize AbstractItemView::doTextLayout(QTextLayout &layout, const QSize &constraints, Qt::Alignment alignment, QTextOption::WrapMode wrapMode) const { QTextOption to; to.setAlignment(alignment); to.setTextDirection(layoutDirection()); to.setWrapMode(wrapMode); layout.setTextOption(to); QFontMetricsF fm = QFontMetricsF(layout.font()); QTextLine line; qreal leading = fm.leading(); qreal widthUsed = 0; qreal height = 0; layout.beginLayout(); while ((line = layout.createLine()).isValid()) { // Make the last line that will fit infinitely long. // drawTextLayout() will handle this by fading the line out // if it won't fit inside the constraints. if (height + 2 * fm.lineSpacing() > constraints.height()) { line.setLineWidth(INT_MAX); if (line.naturalTextWidth() < constraints.width()) { line.setLineWidth(constraints.width()); widthUsed = qMax(widthUsed, line.naturalTextWidth()); } else { widthUsed = constraints.width(); } } else { line.setLineWidth(constraints.width()); widthUsed = qMax(widthUsed, line.naturalTextWidth()); } line.setPosition(QPointF(0, height)); height += line.height() + leading; } layout.endLayout(); return QSize(widthUsed, height); } void AbstractItemView::drawTextLayout(QPainter *painter, const QTextLayout &layout, const QRect &rect) const { // Create the alpha gradient for the fade out effect QLinearGradient alphaGradient(0, 0, 1, 0); alphaGradient.setCoordinateMode(QGradient::ObjectBoundingMode); if (layout.textOption().textDirection() == Qt::LeftToRight) { alphaGradient.setColorAt(0, QColor(0, 0, 0, 255)); alphaGradient.setColorAt(1, QColor(0, 0, 0, 0)); } else { alphaGradient.setColorAt(0, QColor(0, 0, 0, 0)); alphaGradient.setColorAt(1, QColor(0, 0, 0, 255)); } QFontMetrics fm(layout.font()); QList fadeRects; QList haloRects; int fadeWidth = 30; // Compute halo and fade rects for (int i = 0; i < layout.lineCount(); i++) { const QTextLine line = layout.lineAt(i); const QRectF lr = line.naturalTextRect(); // Add a fade out rect to the list if the line is too long if (lr.width() > rect.width()) { int x = rect.width() - fadeWidth; int y = lr.y(); QRect r = QStyle::visualRect(layout.textOption().textDirection(), QRect(QPoint(), rect.size()), QRect(x, y, fadeWidth, int(lr.height()))); fadeRects.append(r); } haloRects.append((lr & rect.translated(-rect.topLeft())).toAlignedRect()); } // Create a pixmap for the text QPixmap pixmap(rect.size()); pixmap.fill(Qt::transparent); QPainter p(&pixmap); p.setPen(painter->pen()); int y = 0; if (layout.textOption().alignment() & Qt::AlignVCenter) { const int h = fm.height() * layout.lineCount(); if (h < pixmap.height()) { y = (pixmap.height() - h) / 2; } } p.translate(0, y); // Draw each line in the layout for (int i = 0; i < layout.lineCount(); i++) { const QTextLine line = layout.lineAt(i); const QRectF tr = line.naturalTextRect(); if (tr.width() > rect.width()) { if (layoutDirection() == Qt::LeftToRight) { line.draw(&p, QPointF(-tr.x(), 0)); } else { line.draw(&p, QPointF(rect.width() - tr.right(), 0)); } } else { line.draw(&p, QPointF()); } } // Reduce the alpha in each fade out rect using the alpha gradient if (!fadeRects.isEmpty()) { p.setCompositionMode(QPainter::CompositionMode_DestinationIn); foreach (const QRect &rect, fadeRects) { p.fillRect(rect, alphaGradient); } } p.end(); if (drawShadows()) { const QColor color = painter->pen().color(); if (qGray(color.rgb()) < 192) { // Draw halos painter->translate(0, y); foreach (const QRect &haloRect, haloRects) { Plasma::PaintUtils::drawHalo(painter, haloRect.translated(rect.topLeft())); } painter->translate(0, -y); } else { // Draw shadow QImage shadow = pixmap.toImage(); Plasma::PaintUtils::shadowBlur(shadow, 2, Qt::black); // Make the shadow twice as dark quint32 * const pixels = reinterpret_cast(shadow.bits()); for (int i = 0; i < shadow.width() * shadow.height(); i++) { pixels[i] = qMin(255, qAlpha(pixels[i]) * 2) << 24; } painter->drawImage(rect.topLeft() + QPoint(1, 1), shadow); } } // Draw the text pixmap painter->drawPixmap(rect.topLeft(), pixmap); } void AbstractItemView::rowsInserted(const QModelIndex &parent, int first, int last) { Q_UNUSED(parent) Q_UNUSED(first) Q_UNUSED(last) } void AbstractItemView::rowsRemoved(const QModelIndex &parent, int first, int last) { Q_UNUSED(parent) Q_UNUSED(first) Q_UNUSED(last) } void AbstractItemView::modelReset() { } void AbstractItemView::layoutChanged() { } void AbstractItemView::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) { Q_UNUSED(topLeft) Q_UNUSED(bottomRight) } void AbstractItemView::commitData(QWidget *editor) { Q_UNUSED(editor) } void AbstractItemView::closeEditor(QWidget *editor, QAbstractItemDelegate::EndEditHint hint) { Q_UNUSED(editor) Q_UNUSED(hint) } void AbstractItemView::svgChanged() { } void AbstractItemView::scrollTo(const QModelIndex &index, QAbstractItemView::ScrollHint hint) { Q_UNUSED(hint) const QRectF r = mapFromViewport(visualRect(index)); if (r.top() < 0) { smoothScroll(0, r.top()); } else if (r.bottom() > geometry().height()) { smoothScroll(0, r.bottom() - geometry().height()); } } void AbstractItemView::autoScroll(ScrollDirection direction, int pixelsPerSecond) { m_scrollDirection = direction; m_autoScrollSetSpeed = direction == ScrollUp ? -pixelsPerSecond : pixelsPerSecond; if (!m_autoScrollTimer.isActive()) { m_autoScrollSpeed = 1; m_autoScrollTime.restart(); m_autoScrollTimer.start(1000 / 30, this); } } void AbstractItemView::stopAutoScrolling() { m_autoScrollSetSpeed = 0; m_autoScrollTimer.stop(); } void AbstractItemView::scrollBarValueChanged(int value) { Q_UNUSED(value) m_viewScrolled = true; update(); } void AbstractItemView::scrollBarActionTriggered(int action) { switch (action) { case QAbstractSlider::SliderSingleStepAdd: case QAbstractSlider::SliderSingleStepSub: case QAbstractSlider::SliderPageStepAdd: case QAbstractSlider::SliderPageStepSub: stopAutoScrolling(); smoothScroll(0, m_scrollBar->nativeWidget()->sliderPosition() - m_scrollBar->value()); break; case QAbstractSlider::SliderToMinimum: case QAbstractSlider::SliderToMaximum: stopAutoScrolling(); // Use a delayed call since the value won't propagate until after this function returns QMetaObject::invokeMethod(this, "finishedScrolling", Qt::QueuedConnection); break; } } void AbstractItemView::scrollBarSliderReleased() { finishedScrolling(); } void AbstractItemView::finishedScrolling() { } void AbstractItemView::timerEvent(QTimerEvent *event) { if (event->timerId() == m_smoothScrollTimer.timerId()) { scrollTick(); } else if (event->timerId() == m_autoScrollTimer.timerId()) { int step = qRound(m_autoScrollTime.elapsed() * (m_autoScrollSpeed / 1000.)); m_autoScrollTime.restart(); if (m_scrollDirection == ScrollUp && m_scrollBar->value() > m_scrollBar->minimum()) { m_scrollBar->setValue(qMax(m_scrollBar->minimum(), m_scrollBar->value() + step)); } else if (m_scrollDirection == ScrollDown && m_scrollBar->value() < m_scrollBar->maximum()) { m_scrollBar->setValue(qMin(m_scrollBar->maximum(), m_scrollBar->value() + step)); } else { m_autoScrollSetSpeed = 0; m_autoScrollSpeed = 0; } if (m_autoScrollSetSpeed > m_autoScrollSpeed) { int delta; if (m_autoScrollSpeed >= 0) { delta = qBound(2, m_autoScrollSpeed * 2, 30); } else { delta = qBound(2, qAbs(m_autoScrollSpeed) / 2, 30); } m_autoScrollSpeed = qMin(m_autoScrollSpeed + delta, m_autoScrollSetSpeed); } else if (m_autoScrollSetSpeed < m_autoScrollSpeed) { int delta; if (m_autoScrollSpeed >= 0) { delta = qBound(2, m_autoScrollSpeed / 2, 30); } else { delta = qBound(2, qAbs(m_autoScrollSpeed * 2), 30); } m_autoScrollSpeed = qMax(m_autoScrollSetSpeed, m_autoScrollSpeed - delta); } if (m_autoScrollSpeed == 0 && m_autoScrollSetSpeed == 0) { m_autoScrollTimer.stop(); } } } void AbstractItemView::startScrolling() { m_smoothScrolling = true; m_smoothScrollTimer.start(sSmoothScrollTick, this); } void AbstractItemView::stopScrolling() { m_smoothScrollTimer.stop(); m_dx = m_dy = 0; m_ddx = m_ddy = 0; m_rdx = m_rdy = 0; m_dddx = m_dddy = 0; m_smoothScrolling = false; finishedScrolling(); } void AbstractItemView::smoothScroll(int dx, int dy) { // full scroll is remaining scroll plus new scroll m_dx = m_dx + dx; m_dy = m_dy + dy; if (m_dx == 0 && m_dy == 0) return; int steps = sSmoothScrollTime/sSmoothScrollTick; // average step size (stored in 1/16 px/step) m_ddx = (m_dx*16)/(steps+1); m_ddy = (m_dy*16)/(steps+1); if (qAbs(m_ddx) < 64 && qAbs(m_ddy) < 64) { // Don't move slower than average 4px/step in minimum one direction if (m_ddx > 0) m_ddx = qMax(m_ddx, 64); if (m_ddy > 0) m_ddy = qMax(m_ddy, 64); if (m_ddx < 0) m_ddx = qMin(m_ddx, -64); if (m_ddy < 0) m_ddy = qMin(m_ddy, -64); // This means fewer than normal steps steps = qMax(m_ddx ? (m_dx*16)/m_ddx : 0, m_ddy ? (m_dy*16)/m_ddy : 0); if (steps < 1) steps = 1; m_ddx = (m_dx*16)/(steps+1); m_ddy = (m_dy*16)/(steps+1); } // step size starts at double average speed and ends at 0 m_ddx *= 2; m_ddy *= 2; // deacceleration speed m_dddx = (m_ddx+1)/steps; m_dddy = (m_ddy+1)/steps; if (!m_smoothScrolling) { startScrolling(); scrollTick(); } m_smoothScrollStopwatch.start(); } void AbstractItemView::scrollTick() { if (m_dx == 0 && m_dy == 0) { stopScrolling(); return; } // step size + remaining partial step int tddx = m_ddx + m_rdx; int tddy = m_ddy + m_rdy; // don't go under 1px/step if (tddx > 0 && tddx < 16) tddx = 16; if (tddy > 0 && tddy < 16) tddy = 16; if (tddx < 0 && tddx > -16) tddx = -16; if (tddy < 0 && tddy > -16) tddy = -16; // full pixel steps to scroll in this step int ddx = tddx / 16; int ddy = tddy / 16; // remaining partial step (this is especially needed for 1.x sized steps) m_rdx = tddx % 16; m_rdy = tddy % 16; // limit step to requested scrolling distance if (qAbs(ddx) > qAbs(m_dx)) ddx = m_dx; if (qAbs(ddy) > qAbs(m_dy)) ddy = m_dy; // Don't stop if deaccelerated too fast if (!ddx) ddx = m_dx; if (!ddy) ddy = m_dy; // update remaining scroll m_dx -= ddx; m_dy -= ddy; m_scrollBar->setValue(m_scrollBar->value() + ddy); // only consider decelerating if we aren't too far behind schedule if (m_smoothScrollStopwatch.elapsed() < 2 * sSmoothScrollTick) { // update scrolling speed int dddx = m_dddx; int dddy = m_dddy; // don't change direction if (abs(dddx) > abs(m_ddx)) dddx = m_ddx; if (abs(dddy) > abs(m_ddy)) dddy = m_ddy; m_ddx -= dddx; m_ddy -= dddy; } m_smoothScrollStopwatch.start(); } #include "moc_abstractitemview.cpp"