diff options
39 files changed, 7076 insertions, 7 deletions
diff --git a/doc/src/examples/wheel.qdoc b/doc/src/examples/wheel.qdoc new file mode 100644 index 0000000..1ea85fc --- /dev/null +++ b/doc/src/examples/wheel.qdoc @@ -0,0 +1,109 @@ +/**************************************************************************** +** +** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the documentation of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:FDL$ +** Commercial Usage +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in a +** written agreement between you and Nokia. +** +** GNU Free Documentation License +** Alternatively, this file may be used under the terms of the GNU Free +** Documentation License version 1.3 as published by the Free Software +** Foundation and appearing in the file included in the packaging of this +** file. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +/*! + \example scroller/wheel + \title Wheel Scroller Example + + The Wheel Scroller Example shows how to use QScroller, QScrollEvent + and QScrollPrepareEvent to implement smooth scrolling for a + custom Widget. + + \section1 Basics + + The QScroller class is the main part of the smooth scrolling + mechanism in Qt. It keeps track of the current scroll position and + speed and updates the object through events. + QScroller will get touch events via the QFlickGesture. + It will query the target object through a QScrollPrepareEvent for + the scroll area and other information. + QScroller will send QScrollEvents to inform the target object about + the current scroll position. + The target object (usually a QWidget or a QGraphicsObject) will + then need to update it's graphical representation to reflect the + new scroll position. + + \section1 The Wheel Widget class + + To demonstrate how to use the QScroller we implement a QWidget that + looks and works like the wheel of a slot machine. + The wheel can be started via touch events and will continue getting + slower. + Additionally the wheel should appear as if no border exists (which + would seem unnatural) and the scrolling should snap to center one + item. + + In the widget we need to grab the QFlickGesture. The gesture itself + will setAcceptTouchEvents for us, so we don't need to do that here. + + \snippet examples/scroller/wheel/wheelwidget.cpp 0 + + The widget will get gesture events but in addition we also will + get the events from QScroller. + We will need to accept the QScrollPrepareEvent to indicate that + a scrolling should really be started from the given position. + + \snippet examples/scroller/wheel/wheelwidget.cpp 1 + + We should call all three set functions form QScrollPrepareEvent. + + \list + \o \c setViewportSize to indicate our viewport size. Actually the + given code could be improved by giving our size minus the borders. + \o \c setMaxContentPos to indicate the maximum values for the scroll + position. The minimum values are implicitely set to 0. + In our example we give a very high number here and hope that the user + is not patient enough to scroll until the very end. + \o \c setContentPos to indicate the current scroll position. + We give a position in the middle of the huge scroll area. + Actually we give this position every time a new scroll is started so + the user will only reach the end if he continuously scrolls in one + direction which is not very likely. + \endlist + + The handling of the QScrollEvent is a lengthly code not fully shown here. + \snippet examples/scroller/wheel/wheelwidget.cpp 2 + + In principle it does three steps. + \list + \o It calculates and updates the current scroll position as given by + QScroller. + \o It repaints the widget so that the new position is shown. + \o It centers the item as soon as the scrolling stopps. + \endlist + + The following code does the centering. + \snippet examples/scroller/wheel/wheelwidget.cpp 3 + + We check if the scrolling is finished which is indicated in the + QScrollEvent by the \c isLast flag. + We then check if the item is not already centered and if not start a new + scroll by calling QScroller::scrollTo. + + As you can see the QScroller can be used for other things besides simple + scroll areas. +*/ diff --git a/examples/examples.pro b/examples/examples.pro index f233aba..968740d 100644 --- a/examples/examples.pro +++ b/examples/examples.pro @@ -20,6 +20,7 @@ SUBDIRS = \ mainwindows \ painting \ richtext \ + scroller \ sql \ tools \ tutorials \ diff --git a/examples/scroller/graphicsview/graphicsview.pro b/examples/scroller/graphicsview/graphicsview.pro new file mode 100644 index 0000000..dcebe62 --- /dev/null +++ b/examples/scroller/graphicsview/graphicsview.pro @@ -0,0 +1,8 @@ +TEMPLATE = app +SOURCES = main.cpp + +# install +target.path = $$[QT_INSTALL_EXAMPLES]/scroller/graphicsview +sources.files = $$SOURCES $$HEADERS $$RESOURCES $$FORMS graphicsview.pro +sources.path = $$[QT_INSTALL_EXAMPLES]/scroller/graphicsview +INSTALLS += target sources diff --git a/examples/scroller/graphicsview/main.cpp b/examples/scroller/graphicsview/main.cpp new file mode 100644 index 0000000..e28978f --- /dev/null +++ b/examples/scroller/graphicsview/main.cpp @@ -0,0 +1,292 @@ +/**************************************************************************** +** +** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include <QtCore> +#include <QtGui> + +#define NUM_ITEMS 100 +#define NUM_LISTS 10 + +/*! + \class RectObject + Note that it needs to be a QGraphicsObject or else the gestures will not work correctly. +*/ +class RectObject : public QGraphicsObject +{ + Q_OBJECT + +public: + + RectObject(const QString &text, qreal x, qreal y, qreal width, qreal height, QBrush brush, QGraphicsItem *parent = 0) + : QGraphicsObject(parent) + , m_text(text) + , m_rect(x, y, width, height) + , m_pen(brush.color().lighter(), 3.0) + , m_brush(brush) + { + setFlag(QGraphicsItem::ItemClipsToShape, true); + } + + QRectF boundingRect() const + { + // here we only want the size of the children and not the size of the children of the children... + qreal halfpw = m_pen.widthF() / 2; + QRectF rect = m_rect; + if (halfpw > 0.0) + rect.adjust(-halfpw, -halfpw, halfpw, halfpw); + + return rect; + } + + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) + { + Q_UNUSED(option); + Q_UNUSED(widget); + painter->setPen(m_pen); + painter->setBrush(m_brush); + painter->drawRect(m_rect); + + painter->setPen(Qt::black); + QFont f; + f.setPixelSize(m_rect.height()); + painter->setFont(f); + painter->drawText(m_rect, Qt::AlignCenter, m_text); + } + + QString m_text; + QRectF m_rect; + QPen m_pen; + QBrush m_brush; +}; + +class ViewObject : public QGraphicsObject +{ + Q_OBJECT +public: + ViewObject(QGraphicsObject *parent) + : QGraphicsObject(parent) + { } + + QRectF boundingRect() const + { + QRectF rect; + foreach (QGraphicsItem *item, childItems()) + rect |= item->boundingRect().translated(item->pos()); + return rect; + } + + void paint(QPainter*, const QStyleOptionGraphicsItem*, QWidget*) + { } +}; + +class ListObject : public QGraphicsObject +{ + Q_OBJECT + +public: + ListObject(const QSizeF &size, bool useTouch) + { + m_size = size; + setFlag(QGraphicsItem::ItemClipsChildrenToShape, true); + // grab gesture via Touch or Mouse events + QScroller::grabGesture(this, useTouch ? QScroller::TouchGesture : QScroller::LeftMouseButtonGesture); + + // this needs to be QGraphicsOBJECT - otherwise gesture recognition + // will not work for the parent of the viewport (in this case the + // list) + m_viewport = new ViewObject(this); + + } + + QGraphicsObject *viewport() const + { + return m_viewport; + } + + bool event(QEvent *e) + { + switch (e->type()) { +// ![2] + case QEvent::ScrollPrepare: { + QScrollPrepareEvent *se = static_cast<QScrollPrepareEvent *>(e); + se->setViewportSize(m_size); + QRectF br = m_viewport->boundingRect(); + se->setContentPosRange(QRectF(0, 0, + qMax(qreal(0), br.width() - m_size.width()), + qMax(qreal(0), br.height() - m_size.height()))); + se->setContentPos(-m_viewport->pos()); + se->accept(); + return true; + } +// ![1] +// ![2] + case QEvent::Scroll: { + QScrollEvent *se = static_cast<QScrollEvent *>(e); + m_viewport->setPos(-se->contentPos() - se->overshootDistance()); + return true; + } +// ![2] + default: + break; + } + return QGraphicsObject::event(e); + } + + bool sceneEvent(QEvent *e) + { + switch (e->type()) { + case QEvent::TouchBegin: { + // We need to return true for the TouchBegin here in the + // top-most graphics object - otherwise gestures in our parent + // objects will NOT work at all (the accept() flag is already + // set due to our setAcceptTouchEvents(true) call in the c'tor + return true; + + } + case QEvent::GraphicsSceneMousePress: { + // We need to return true for the MousePress here in the + // top-most graphics object - otherwise gestures in our parent + // objects will NOT work at all (the accept() flag is already + // set to true by Qt) + return true; + + } + default: + break; + } + return QGraphicsObject::sceneEvent(e); + } + + QRectF boundingRect() const + { + return QRectF(0, 0, m_size.width() + 3, m_size.height()); + } + + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) + { + Q_UNUSED(option); + Q_UNUSED(widget); + painter->setPen(QPen(QColor(100, 100, 100), 3.0)); + painter->drawRect(QRect(1.5, 1.5, m_size.width() - 3, m_size.height() - 3)); + } + + QSizeF m_size; + ViewObject *m_viewport; +}; + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + MainWindow(bool useTouch) + { + m_scene = new QGraphicsScene(); + + // -- make the main list + ListObject *mainList = new ListObject(QSizeF(780, 400), useTouch); + mainList->setObjectName(QLatin1String("MainList")); + m_scene->addItem(mainList); +// ![3] + for (int i=0; i<NUM_LISTS; i++) { + ListObject *childList = new ListObject(QSizeF(mainList->m_size.width()/3, mainList->m_size.height()), useTouch); + childList->setObjectName(QString("ChildList %1").arg(i)); + fillList(childList); + childList->setParentItem(mainList->viewport()); + childList->setPos(i*mainList->m_size.width()/3, 0); + } + mainList->viewport()->setPos(0, 0); + + + /* + list1->setTransformOriginPoint(200, 200); + list1->setRotation(135); + list1->setPos(20 + 200 * .41, 20 + 200 * .41); + */ +// ![3] + + m_view = new QGraphicsView(m_scene); + setCentralWidget(m_view); + setWindowTitle(tr("Gesture example")); + m_scene->setSceneRect(0, 0, m_view->viewport()->width(), m_view->viewport()->height()); + } + + /** + * Fills the list object \a list with RectObjects. + */ + void fillList(ListObject *list) + { + qreal h = list->m_size.height() / 10; + for (int i=0; i<NUM_ITEMS; i++) { + QColor color = QColor(255*i/NUM_ITEMS, 255*(NUM_ITEMS-i)/NUM_ITEMS, 127*(i%2)+64*(i/2%2)); + QString text = QLatin1String("Item #") + QString::number(i); + QGraphicsItem *rect = new RectObject(text, 0, 0, list->m_size.width() - 6, h - 3, QBrush(color), list->viewport()); + rect->setPos(3, h*i+3); + } + list->viewport()->setPos(0, 0); + } + + +protected: + void resizeEvent(QResizeEvent *e) + { + // resize the scene according to our own size to prevent scrolling + m_scene->setSceneRect(0, 0, m_view->viewport()->width(), m_view->viewport()->height()); + QMainWindow::resizeEvent(e); + } + + QGraphicsScene *m_scene; + QGraphicsView *m_view; +}; + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + bool useTouch = (app.arguments().contains(QLatin1String("--touch"))); + MainWindow mw(useTouch); + mw.show(); +#ifdef Q_WS_MAC + mw.raise(); +#endif + return app.exec(); +} + +#include "main.moc" diff --git a/examples/scroller/plot/main.cpp b/examples/scroller/plot/main.cpp new file mode 100644 index 0000000..a4e2add --- /dev/null +++ b/examples/scroller/plot/main.cpp @@ -0,0 +1,183 @@ + +#include <QApplication> +#include <QListWidget> +#include <QListWidgetItem> +#include <QSplitter> +#include <QStackedWidget> +#include <QSignalMapper> +#include <QMainWindow> +#include <QMenuBar> +#include <QActionGroup> +#include <QWebView> +#include <QTimer> +#include <QScroller> + +#include <QtDebug> + +#include <QGesture> + +#include "settingswidget.h" +#include "plotwidget.h" + + +class MainWindow : public QMainWindow +{ + Q_OBJECT +public: + MainWindow(bool smallscreen, bool touch) + : QMainWindow(), m_touch(touch) + { + m_list = new QListWidget(); + m_list->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); + m_list_scroller = installKineticScroller(m_list); + + for (int i = 0; i < 1000; ++i) + new QListWidgetItem(QString("This is a test text %1 %2").arg(i).arg(QString("--------").left(i % 8)), m_list); + + connect(m_list, SIGNAL(itemActivated(QListWidgetItem*)), this, SLOT(listItemActivated(QListWidgetItem*))); + connect(m_list, SIGNAL(itemClicked(QListWidgetItem*)), this, SLOT(listItemClicked(QListWidgetItem*))); + connect(m_list, SIGNAL(itemPressed(QListWidgetItem*)), this, SLOT(listItemPressed(QListWidgetItem*))); + connect(m_list, SIGNAL(itemSelectionChanged()), this, SLOT(listItemSelectionChanged())); + connect(m_list, SIGNAL(currentItemChanged(QListWidgetItem*,QListWidgetItem*)), this, SLOT(listItemCurrentChanged(QListWidgetItem*))); + + m_web = new QWebView(); + m_web_scroller = installKineticScroller(m_web); + + QTimer::singleShot(1000, this, SLOT(loadUrl())); + + m_settings = new SettingsWidget(smallscreen); + installKineticScroller(m_settings); + m_plot = new PlotWidget(smallscreen); + + QStackedWidget *stack = new QStackedWidget(); + stack->addWidget(m_list); + stack->addWidget(m_web); + + QActionGroup *pages = new QActionGroup(this); + pages->setExclusive(true); + QSignalMapper *mapper = new QSignalMapper(this); + connect(mapper, SIGNAL(mapped(int)), stack, SLOT(setCurrentIndex(int))); + + createAction("List", pages, mapper, 0, true); + createAction("Web", pages, mapper, 1); + + if (smallscreen) { + stack->addWidget(m_settings); + stack->addWidget(m_plot); + + createAction("Settings", pages, mapper, 2); + createAction("Plot", pages, mapper, 3); + + setCentralWidget(stack); + } else { + QSplitter *split = new QSplitter(); + m_settings->setMinimumWidth(m_settings->sizeHint().width()); + split->addWidget(stack); + split->addWidget(m_settings); + split->addWidget(m_plot); + setCentralWidget(split); + } + menuBar()->addMenu(QLatin1String("Pages"))->addActions(pages->actions()); + connect(stack, SIGNAL(currentChanged(int)), this, SLOT(pageChanged(int))); + pageChanged(0); + } + +private slots: + void pageChanged(int page) + { + if (page < 0 || page > 1) + return; + switch (page) { + case 0: + m_settings->setScroller(m_list); + m_plot->setScroller(m_list); + break; + case 1: + m_settings->setScroller(m_web); + m_plot->setScroller(m_web); + break; + default: + break; + } + } + + void loadUrl() + { + m_web->load(QUrl("http://www.google.com")); + } + + void listItemActivated(QListWidgetItem *lwi) { qWarning() << "Item ACTIVATED: " << lwi->text(); } + void listItemClicked(QListWidgetItem *lwi) { qWarning() << "Item CLICKED: " << lwi->text(); } + void listItemPressed(QListWidgetItem *lwi) { qWarning() << "Item PRESSED: " << lwi->text(); } + void listItemCurrentChanged(QListWidgetItem *lwi) { qWarning() << "Item CURRENT: " << (lwi ? lwi->text() : QString("(none)")); } + void listItemSelectionChanged() + { + int n = m_list->selectedItems().count(); + qWarning("Item%s SELECTED: %d", n == 1 ? "" : "s", n); + foreach (QListWidgetItem *lwi, m_list->selectedItems()) + qWarning() << " " << lwi->text(); + } + +private: + QAction *createAction(const char *text, QActionGroup *group, QSignalMapper *mapper, int mapping, bool checked = false) + { + QAction *a = new QAction(QLatin1String(text), group); + a->setCheckable(true); + a->setChecked(checked); +#if defined(Q_WS_MAC) + a->setMenuRole(QAction::NoRole); +#endif + mapper->setMapping(a, mapping); + connect(a, SIGNAL(toggled(bool)), mapper, SLOT(map())); + return a; + } + + QScroller *installKineticScroller(QWidget *w) + { + if (QAbstractScrollArea *area = qobject_cast<QAbstractScrollArea *>(w)) { + QScroller::grabGesture(area->viewport(), m_touch ? QScroller::TouchGesture : QScroller::LeftMouseButtonGesture); + return QScroller::scroller(area->viewport()); + } else if (QWebView *web = qobject_cast<QWebView *>(w)) { + QScroller::grabGesture(web, m_touch ? QScroller::TouchGesture : QScroller::LeftMouseButtonGesture); + } + return QScroller::scroller(w); + } + +private: + QListWidget *m_list; + QWebView *m_web; + QScroller *m_list_scroller, *m_web_scroller; + SettingsWidget *m_settings; + PlotWidget *m_plot; + bool m_touch; +}; + +int main(int argc, char **argv) +{ + QApplication a(argc, argv); + +#if defined(Q_WS_MAEMO_5) || defined(Q_WS_S60) || defined(Q_WS_WINCE) + bool smallscreen = true; +#else + bool smallscreen = false; +#endif + bool touch = false; + + if (a.arguments().contains(QLatin1String("--small"))) + smallscreen = true; + if (a.arguments().contains(QLatin1String("--touch"))) + touch = true; + + MainWindow *mw = new MainWindow(smallscreen, touch); + if (smallscreen) + mw->showMaximized(); + else + mw->show(); +#if defined(Q_WS_MAC) + mw->raise(); +#endif + + return a.exec(); +} + +#include "main.moc" diff --git a/examples/scroller/plot/plot.pro b/examples/scroller/plot/plot.pro new file mode 100644 index 0000000..8c37b04 --- /dev/null +++ b/examples/scroller/plot/plot.pro @@ -0,0 +1,18 @@ +HEADERS = settingswidget.h \ + plotwidget.h +SOURCES = settingswidget.cpp \ + plotwidget.cpp \ + main.cpp + +QT += webkit + +# install +target.path = $$[QT_INSTALL_EXAMPLES]/scroller/plot +sources.files = $$SOURCES $$HEADERS $$RESOURCES $$FORMS plot.pro +sources.path = $$[QT_INSTALL_EXAMPLES]/scroller/plot +INSTALLS += target sources + +symbian { + TARGET.UID3 = 0xA000CF66 + include($$QT_SOURCE_TREE/examples/symbianpkgrules.pri) +} diff --git a/examples/scroller/plot/plotwidget.cpp b/examples/scroller/plot/plotwidget.cpp new file mode 100644 index 0000000..5f0df67 --- /dev/null +++ b/examples/scroller/plot/plotwidget.cpp @@ -0,0 +1,205 @@ +/**************************************************************************** +** +** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include <QPushButton> +#include <QTextStream> +#include <QColor> +#include <QPainter> +#include <QLabel> +#include <QResizeEvent> +#include <QPlastiqueStyle> +#include <QAbstractScrollArea> + +#include "plotwidget.h" +#include "qscroller.h" + +PlotWidget::PlotWidget(bool /*smallscreen*/) + : QWidget(), m_widget(0) +{ + setWindowTitle(QLatin1String("Plot")); + m_clear = new QPushButton(QLatin1String("Clear"), this); +#if defined(Q_WS_MAEMO_5) + m_clear->setStyle(new QPlastiqueStyle()); + m_clear->setFixedHeight(55); +#endif + connect(m_clear, SIGNAL(clicked()), this, SLOT(reset())); + m_legend = new QLabel(this); + QString legend; + QTextStream ts(&legend); + // ok. this wouldn't pass the w3c html verification... + ts << "<table style=\"color:#000;\" border=\"0\">"; + ts << "<tr><td width=\"30\" bgcolor=\"" << QColor(Qt::red).light().name() << "\" /><td>Velocity X</td></tr>"; + ts << "<tr><td width=\"30\" bgcolor=\"" << QColor(Qt::red).dark().name() << "\" /><td>Velocity Y</td></tr>"; + ts << "<tr><td width=\"30\" bgcolor=\"" << QColor(Qt::green).light().name() << "\" /><td>Content Position X</td></tr>"; + ts << "<tr><td width=\"30\" bgcolor=\"" << QColor(Qt::green).dark().name() << "\" /><td>Content Position Y</td></tr>"; + ts << "<tr><td width=\"30\" bgcolor=\"" << QColor(Qt::blue).light().name() << "\" /><td>Overshoot Position X</td></tr>"; + ts << "<tr><td width=\"30\" bgcolor=\"" << QColor(Qt::blue).dark().name() << "\" /><td>Overshoot Position Y</td></tr>"; + ts << "</table>"; + m_legend->setText(legend); +} + +void PlotWidget::setScroller(QWidget *widget) +{ + if (QAbstractScrollArea *area = qobject_cast<QAbstractScrollArea *>(widget)) + widget = area->viewport(); + + if (m_widget) + m_widget->removeEventFilter(this); + m_widget = widget; + reset(); + if (m_widget) + m_widget->installEventFilter(this); +} + +bool PlotWidget::eventFilter(QObject *obj, QEvent *ev) +{ + if (ev->type() == QEvent::Scroll) { + QScrollEvent *se = static_cast<QScrollEvent *>(ev); + QScroller *scroller = QScroller::scroller(m_widget); + + QPointF v = scroller->velocity(); + //v.rx() *= scroller->pixelPerMeter().x(); + //v.ry() *= scroller->pixelPerMeter().y(); + + PlotItem pi = { v, se->contentPos(), se->overshootDistance() }; + addPlotItem(pi); + } + + return QWidget::eventFilter(obj, ev); +} + +static inline void doMaxMin(const QPointF &v, qreal &minmaxv) +{ + minmaxv = qMax(minmaxv, qMax(qAbs(v.x()), qAbs(v.y()))); +} + +void PlotWidget::addPlotItem(const PlotItem &pi) +{ + m_plotitems.append(pi); + minMaxVelocity = minMaxPosition = 0; + + while (m_plotitems.size() > 500) + m_plotitems.removeFirst(); + + foreach (const PlotItem &pi, m_plotitems) { + doMaxMin(pi.velocity, minMaxVelocity); + doMaxMin(pi.contentPosition, minMaxPosition); + doMaxMin(pi.overshootPosition, minMaxPosition); + } + update(); +} + +void PlotWidget::reset() +{ + m_plotitems.clear(); + minMaxVelocity = minMaxPosition = 0; + update(); +} + +void PlotWidget::resizeEvent(QResizeEvent *) +{ + QSize cs = m_clear->sizeHint(); + QSize ls = m_legend->sizeHint(); + m_clear->setGeometry(4, 4, cs.width(), cs.height()); + m_legend->setGeometry(4, height() - ls.height() - 4, ls.width(), ls.height()); +} + +void PlotWidget::paintEvent(QPaintEvent *) +{ +#define SCALE(v, mm) ((qreal(1) - (v / mm)) * qreal(0.5) * height()) + + QColor rvColor = Qt::red; + QColor cpColor = Qt::green; + QColor opColor = Qt::blue; + + + QPainter p(this); + //p.setRenderHints(QPainter::Antialiasing); //too slow for 60fps + p.fillRect(rect(), Qt::white); + + p.setPen(Qt::black); + p.drawLine(0, SCALE(0, 1), width(), SCALE(0, 1)); + + if (m_plotitems.isEmpty()) + return; + + int x = 2; + int offset = m_plotitems.size() - width() / 2; + QList<PlotItem>::const_iterator it = m_plotitems.constBegin(); + if (offset > 0) + it += (offset - 1); + + const PlotItem *last = &(*it++); + + while (it != m_plotitems.constEnd()) { + p.setPen(rvColor.light()); + p.drawLine(qreal(x - 2), SCALE(last->velocity.x(), minMaxVelocity), + qreal(x), SCALE(it->velocity.x(), minMaxVelocity)); + p.setPen(rvColor.dark()); + p.drawLine(qreal(x - 2), SCALE(last->velocity.y(), minMaxVelocity), + qreal(x), SCALE(it->velocity.y(), minMaxVelocity)); + + p.setPen(cpColor.light()); + p.drawLine(qreal(x - 2), SCALE(last->contentPosition.x(), minMaxPosition), + qreal(x), SCALE(it->contentPosition.x(), minMaxPosition)); + p.setPen(cpColor.dark()); + p.drawLine(qreal(x - 2), SCALE(last->contentPosition.y(), minMaxPosition), + qreal(x), SCALE(it->contentPosition.y(), minMaxPosition)); + + p.setPen(opColor.light()); + p.drawLine(qreal(x - 2), SCALE(last->overshootPosition.x(), minMaxPosition), + qreal(x), SCALE(it->overshootPosition.x(), minMaxPosition)); + p.setPen(opColor.dark()); + p.drawLine(qreal(x - 2), SCALE(last->overshootPosition.y(), minMaxPosition), + qreal(x), SCALE(it->overshootPosition.y(), minMaxPosition)); + + last = &(*it++); + x += 2; + } + + QString toptext = QString("%1 [m/s] / %2 [pix]").arg(minMaxVelocity, 0, 'f', 2).arg(minMaxPosition, 0, 'f', 2); + QString bottomtext = QString("-%1 [m/s] / -%2 [pix]").arg(minMaxVelocity, 0, 'f', 2).arg(minMaxPosition, 0, 'f', 2); + + p.setPen(Qt::black); + p.drawText(rect(), Qt::AlignTop | Qt::AlignHCenter, toptext); + p.drawText(rect(), Qt::AlignBottom | Qt::AlignHCenter, bottomtext); +#undef SCALE +} diff --git a/examples/scroller/plot/plotwidget.h b/examples/scroller/plot/plotwidget.h new file mode 100644 index 0000000..3b4d92d --- /dev/null +++ b/examples/scroller/plot/plotwidget.h @@ -0,0 +1,88 @@ +/**************************************************************************** +** +** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef PLOTWIDGET_H +#define PLOTWIDGET_H + +#include <QWidget> +#include <QPointF> + +class QPushButton; +class QLabel; + +class QScroller; + +class PlotWidget : public QWidget +{ + Q_OBJECT + +public: + PlotWidget(bool smallscreen = false); + + void setScroller(QWidget *widget); + +public slots: + void reset(); + +protected: + void resizeEvent(QResizeEvent *); + void paintEvent(QPaintEvent *); + + bool eventFilter(QObject *obj, QEvent *ev); + +private: + + struct PlotItem { + QPointF velocity; + QPointF contentPosition; + QPointF overshootPosition; + }; + + void addPlotItem(const PlotItem &pi); + + QWidget *m_widget; + QList<PlotItem> m_plotitems; + qreal minMaxVelocity, minMaxPosition; + QPushButton *m_clear; + QLabel *m_legend; +}; + +#endif diff --git a/examples/scroller/plot/settingswidget.cpp b/examples/scroller/plot/settingswidget.cpp new file mode 100644 index 0000000..af1e621 --- /dev/null +++ b/examples/scroller/plot/settingswidget.cpp @@ -0,0 +1,690 @@ +/**************************************************************************** +** +** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include <QVariant> +#include <QSlider> +#include <QHBoxLayout> +#include <QLabel> +#include <QPushButton> +#include <QComboBox> +#include <QSpinBox> +#include <QGroupBox> +#include <QToolButton> +#include <QCheckBox> +#include <QScrollBar> +#include <QPainter> +#include <QScrollArea> +#include <QScrollPrepareEvent> +#include <QApplication> +#include <QPlainTextEdit> +#include <QTextBlock> +#include <qnumeric.h> + +#include <QEasingCurve> + +#include <QDebug> + +#include "math.h" + +#include "settingswidget.h" +#include "qscroller.h" +#include "qscrollerproperties.h" + +class SnapOverlay : public QWidget +{ + Q_OBJECT +public: + SnapOverlay(QWidget *w) + : QWidget(w) + { + setAttribute(Qt::WA_TransparentForMouseEvents); + + if (QAbstractScrollArea *area = qobject_cast<QAbstractScrollArea *>(w)) { + connect(area->horizontalScrollBar(), SIGNAL(valueChanged(int)), this, SLOT(update())); + connect(area->verticalScrollBar(), SIGNAL(valueChanged(int)), this, SLOT(update())); + area->viewport()->installEventFilter(this); + } + } + void clear(Qt::Orientation o) + { + m_snap[o].clear(); + update(); + } + + void set(Qt::Orientation o, qreal first, qreal step) + { + m_snap[o] = QList<qreal>() << -Q_INFINITY << first << step; + update(); + } + + void set(Qt::Orientation o, const QList<qreal> &list) + { + m_snap[o] = list; + update(); + } + +protected: + bool eventFilter(QObject *o, QEvent *e) + { + if (QAbstractScrollArea *area = qobject_cast<QAbstractScrollArea *>(parentWidget())) { + if (area->viewport() == o) { + if (e->type() == QEvent::Move || e->type() == QEvent::Resize) { + setGeometry(area->viewport()->rect()); + } + } + } + return false; + } + + void paintEvent(QPaintEvent *e) + { + if (QAbstractScrollArea *area = qobject_cast<QAbstractScrollArea *>(parentWidget())) { + int dx = area->horizontalScrollBar()->value(); + int dy = area->verticalScrollBar()->value(); + + QPainter paint(this); + paint.fillRect(e->rect(), Qt::transparent); + paint.setPen(QPen(Qt::red, 9)); + + if (m_snap[Qt::Horizontal].isEmpty()) { + } else if (m_snap[Qt::Horizontal][0] == -Q_INFINITY) { + int start = int(m_snap[Qt::Horizontal][1]); + int step = int(m_snap[Qt::Horizontal][2]); + if (step > 0) { + for (int i = start; i < area->horizontalScrollBar()->maximum(); i += step) + paint.drawPoint(i - dx, 5); + } + } else { + foreach (qreal r, m_snap[Qt::Horizontal]) + paint.drawPoint(int(r) - dx, 5); + } + paint.setPen(QPen(Qt::green, 9)); + if (m_snap[Qt::Vertical].isEmpty()) { + } else if (m_snap[Qt::Vertical][0] == -Q_INFINITY) { + int start = int(m_snap[Qt::Vertical][1]); + int step = int(m_snap[Qt::Vertical][2]); + if (step > 0) { + for (int i = start; i < area->verticalScrollBar()->maximum(); i += step) + paint.drawPoint(5, i - dy); + } + } else { + foreach (qreal r, m_snap[Qt::Vertical]) + paint.drawPoint(5, int(r) - dy); + } + } + } + +private: + QMap<Qt::Orientation, QList<qreal> > m_snap; +}; + +struct MetricItem +{ + QScrollerProperties::ScrollMetric metric; + const char *name; + int scaling; + const char *unit; + QVariant min, max; + QVariant step; +}; + +class MetricItemUpdater : public QObject +{ + Q_OBJECT +public: + MetricItemUpdater(MetricItem *item) + : m_item(item) + , m_widget(0) + , m_slider(0) + , m_combo(0) + , m_valueLabel(0) + { + m_frameRateType = QVariant::fromValue(QScrollerProperties::Standard).userType(); + m_overshootPolicyType = QVariant::fromValue(QScrollerProperties::OvershootWhenScrollable).userType(); + + if (m_item->min.type() == QVariant::EasingCurve) { + m_combo = new QComboBox(); + m_combo->addItem("OutQuad", QEasingCurve::OutQuad); + m_combo->addItem("OutCubic", QEasingCurve::OutCubic); + m_combo->addItem("OutQuart", QEasingCurve::OutQuart); + m_combo->addItem("OutQuint", QEasingCurve::OutQuint); + m_combo->addItem("OutExpo", QEasingCurve::OutExpo); + m_combo->addItem("OutSine", QEasingCurve::OutSine); + m_combo->addItem("OutCirc", QEasingCurve::OutCirc); + } else if (m_item->min.userType() == m_frameRateType) { + m_combo = new QComboBox(); + m_combo->addItem("Standard", QScrollerProperties::Standard); + m_combo->addItem("60 FPS", QScrollerProperties::Fps60); + m_combo->addItem("30 FPS", QScrollerProperties::Fps30); + m_combo->addItem("20 FPS", QScrollerProperties::Fps20); + } else if (m_item->min.userType() == m_overshootPolicyType) { + m_combo = new QComboBox(); + m_combo->addItem("When Scrollable", QScrollerProperties::OvershootWhenScrollable); + m_combo->addItem("Always On", QScrollerProperties::OvershootAlwaysOn); + m_combo->addItem("Always Off", QScrollerProperties::OvershootAlwaysOff); + } else { + m_slider = new QSlider(Qt::Horizontal); + m_slider->setSingleStep(1); + m_slider->setMinimum(-1); + m_slider->setMaximum(qRound((m_item->max.toReal() - m_item->min.toReal()) / m_item->step.toReal())); + m_slider->setValue(-1); + m_valueLabel = new QLabel(); + } + m_nameLabel = new QLabel(QLatin1String(m_item->name)); + if (m_item->unit && m_item->unit[0]) + m_nameLabel->setText(m_nameLabel->text() + QLatin1String(" [") + QLatin1String(m_item->unit) + QLatin1String("]")); + m_resetButton = new QToolButton(); + m_resetButton->setText(QLatin1String("Reset")); + m_resetButton->setEnabled(false); + + connect(m_resetButton, SIGNAL(clicked()), this, SLOT(reset())); + if (m_slider) { + connect(m_slider, SIGNAL(valueChanged(int)), this, SLOT(controlChanged(int))); + m_slider->setMinimum(0); + } else if (m_combo) { + connect(m_combo, SIGNAL(currentIndexChanged(int)), this, SLOT(controlChanged(int))); + } + } + + void setScroller(QWidget *widget) + { + m_widget = widget; + QScroller *scroller = QScroller::scroller(widget); + QScrollerProperties properties = QScroller::scroller(widget)->scrollerProperties(); + + if (m_slider) + m_slider->setEnabled(scroller); + if (m_combo) + m_combo->setEnabled(scroller); + m_nameLabel->setEnabled(scroller); + if (m_valueLabel) + m_valueLabel->setEnabled(scroller); + m_resetButton->setEnabled(scroller); + + if (!scroller) + return; + + m_default_value = properties.scrollMetric(m_item->metric); + valueChanged(m_default_value); + } + + QWidget *nameLabel() { return m_nameLabel; } + QWidget *valueLabel() { return m_valueLabel; } + QWidget *valueControl() { if (m_combo) return m_combo; else return m_slider; } + QWidget *resetButton() { return m_resetButton; } + +private slots: + void valueChanged(const QVariant &v) + { + m_value = v; + if (m_slider) { + switch (m_item->min.type()) { + case QMetaType::Float: + case QVariant::Double: { + m_slider->setValue(qRound((m_value.toReal() * m_item->scaling - m_item->min.toReal()) / m_item->step.toReal())); + break; + } + case QVariant::Int: { + m_slider->setValue(qRound((m_value.toInt() * m_item->scaling - m_item->min.toInt()) / m_item->step.toInt())); + break; + } + default: break; + } + } else if (m_combo) { + if (m_item->min.type() == QVariant::EasingCurve) { + m_combo->setCurrentIndex(m_combo->findData(v.toEasingCurve().type())); + } else if (m_item->min.userType() == m_overshootPolicyType) { + m_combo->setCurrentIndex(m_combo->findData(v.value<QScrollerProperties::OvershootPolicy>())); + } else if (m_item->min.userType() == m_frameRateType) { + m_combo->setCurrentIndex(m_combo->findData(v.value<QScrollerProperties::FrameRates>())); + } + } + } + + void controlChanged(int value) + { + bool combo = (m_combo && (sender() == m_combo)); + QString text; + + if (m_slider && !combo) { + switch (m_item->min.type()) { + case QMetaType::Float: + case QVariant::Double: { + qreal d = m_item->min.toReal() + qreal(value) * m_item->step.toReal(); + text = QString::number(d); + m_value = d / qreal(m_item->scaling); + break; + } + case QVariant::Int: { + int i = m_item->min.toInt() + qRound(qreal(value) * m_item->step.toReal()); + text = QString::number(i); + m_value = i / m_item->scaling; + break; + } + default: break; + } + } else if (m_combo && combo) { + if (m_item->min.type() == QVariant::EasingCurve) { + m_value = QVariant(QEasingCurve(static_cast<QEasingCurve::Type>(m_combo->itemData(value).toInt()))); + } else if (m_item->min.userType() == m_overshootPolicyType) { + m_value = QVariant::fromValue(static_cast<QScrollerProperties::OvershootPolicy>(m_combo->itemData(value).toInt())); + } else if (m_item->min.userType() == m_frameRateType) { + m_value = QVariant::fromValue(static_cast<QScrollerProperties::FrameRates>(m_combo->itemData(value).toInt())); + } + } + if (m_valueLabel) + m_valueLabel->setText(text); + if (m_widget && QScroller::scroller(m_widget)) { + QScrollerProperties properties = QScroller::scroller(m_widget)->scrollerProperties(); + properties.setScrollMetric(m_item->metric, m_value); + QScroller::scroller(m_widget)->setScrollerProperties(properties); + } + + m_resetButton->setEnabled(m_value != m_default_value); + } + + void reset() + { + QScrollerProperties properties = QScroller::scroller(m_widget)->scrollerProperties(); + properties.setScrollMetric(m_item->metric, m_value); + QScroller::scroller(m_widget)->setScrollerProperties(properties); + valueChanged(m_default_value); + } + +private: + MetricItem *m_item; + int m_frameRateType; + int m_overshootPolicyType; + + QWidget *m_widget; + QSlider *m_slider; + QComboBox *m_combo; + QLabel *m_nameLabel, *m_valueLabel; + QToolButton *m_resetButton; + + QVariant m_value, m_default_value; +}; + +#define METRIC(x) QScrollerProperties::x, #x + +MetricItem items[] = { + { METRIC(MousePressEventDelay), 1000, "ms", qreal(0), qreal(2000), qreal(10) }, + { METRIC(DragStartDistance), 1000, "mm", qreal(1), qreal(20), qreal(0.1) }, + { METRIC(DragVelocitySmoothingFactor), 1, "", qreal(0), qreal(1), qreal(0.01) }, + { METRIC(AxisLockThreshold), 1, "", qreal(0), qreal(1), qreal(0.01) }, + + { METRIC(ScrollingCurve), 1, "", QEasingCurve(), 0, 0 }, + { METRIC(DecelerationFactor), 1, "", qreal(0), qreal(3), qreal(0.01) }, + + { METRIC(MinimumVelocity), 1, "m/s", qreal(0), qreal(7), qreal(0.01) }, + { METRIC(MaximumVelocity), 1, "m/s", qreal(0), qreal(7), qreal(0.01) }, + { METRIC(MaximumClickThroughVelocity), 1, "m/s", qreal(0), qreal(7), qreal(0.01) }, + + { METRIC(AcceleratingFlickMaximumTime), 1000, "ms", qreal(100), qreal(5000), qreal(100) }, + { METRIC(AcceleratingFlickSpeedupFactor), 1, "", qreal(1), qreal(7), qreal(0.1) }, + + { METRIC(SnapPositionRatio), 1, "", qreal(0.1), qreal(0.9), qreal(0.1) }, + { METRIC(SnapTime), 1000, "ms", qreal(0), qreal(2000), qreal(10) }, + + { METRIC(OvershootDragResistanceFactor), 1, "", qreal(0), qreal(1), qreal(0.01) }, + { METRIC(OvershootDragDistanceFactor), 1, "", qreal(0), qreal(1), qreal(0.01) }, + { METRIC(OvershootScrollDistanceFactor), 1, "", qreal(0), qreal(1), qreal(0.01) }, + { METRIC(OvershootScrollTime), 1000, "ms", qreal(0), qreal(2000), qreal(10) }, + + { METRIC(HorizontalOvershootPolicy), 1, "", QVariant::fromValue(QScrollerProperties::OvershootWhenScrollable), 0, 0 }, + { METRIC(VerticalOvershootPolicy), 1, "", QVariant::fromValue(QScrollerProperties::OvershootWhenScrollable), 0, 0 }, + { METRIC(FrameRate), 1, "", QVariant::fromValue(QScrollerProperties::Standard), 0, 0 }, +}; + +#undef METRIC + +void SettingsWidget::addToGrid(QGridLayout *grid, QWidget *label, int widgetCount, ...) +{ + va_list args; + va_start(args, widgetCount); + + int rows = grid->rowCount(); + int cols = grid->columnCount(); + + if (label) { + if (m_smallscreen) + grid->addWidget(label, rows++, 0, 1, qMax(cols, widgetCount)); + else + grid->addWidget(label, rows, 0); + } + for (int i = 0; i < widgetCount; i++) { + if (QWidget *w = va_arg(args, QWidget *)) + grid->addWidget(w, rows, m_smallscreen ? i : i + 1); + } + va_end(args); +} + +SettingsWidget::SettingsWidget(bool smallscreen) + : QScrollArea() + , m_widget(0) + , m_snapoverlay(0) + , m_smallscreen(smallscreen) +{ + setWindowTitle(QLatin1String("Settings")); + QWidget *view = new QWidget(); + QVBoxLayout *layout = new QVBoxLayout(view); + QGroupBox *grp; + QGridLayout *grid; + + // GROUP: SCROLL METRICS + + grp = new QGroupBox(QLatin1String("Scroll Metrics")); + grid = new QGridLayout(); + grid->setVerticalSpacing(m_smallscreen ? 4 : 2); + + for (int i = 0; i < int(sizeof(items) / sizeof(items[0])); i++) { + MetricItemUpdater *u = new MetricItemUpdater(items + i); + u->setParent(this); + addToGrid(grid, u->nameLabel(), 3, u->valueControl(), u->valueLabel(), u->resetButton()); + m_metrics.append(u); + } + grp->setLayout(grid); + layout->addWidget(grp); + + // GROUP: SCROLL TO + + grp = new QGroupBox(QLatin1String("Scroll To")); + grid = new QGridLayout(); + grid->setVerticalSpacing(m_smallscreen ? 4 : 2); + + m_scrollx = new QSpinBox(); + m_scrolly = new QSpinBox(); + m_scrolltime = new QSpinBox(); + m_scrolltime->setRange(0, 10000); + m_scrolltime->setValue(1000); + m_scrolltime->setSuffix(QLatin1String(" ms")); + QPushButton *go = new QPushButton(QLatin1String("Go")); + connect(go, SIGNAL(clicked()), this, SLOT(scrollTo())); + connect(m_scrollx, SIGNAL(editingFinished()), this, SLOT(scrollTo())); + connect(m_scrolly, SIGNAL(editingFinished()), this, SLOT(scrollTo())); + connect(m_scrolltime, SIGNAL(editingFinished()), this, SLOT(scrollTo())); + grid->addWidget(new QLabel(QLatin1String("X:")), 0, 0); + grid->addWidget(m_scrollx, 0, 1); + grid->addWidget(new QLabel(QLatin1String("Y:")), 0, 2); + grid->addWidget(m_scrolly, 0, 3); + int row = smallscreen ? 1 : 0; + int col = smallscreen ? 0 : 4; + grid->addWidget(new QLabel(QLatin1String("in")), row, col++); + grid->addWidget(m_scrolltime, row, col++); + if (smallscreen) { + grid->addWidget(go, row, col + 1); + } else { + grid->addWidget(go, row, col); + grid->setColumnStretch(5, 1); + grid->setColumnStretch(6, 1); + } + grid->setColumnStretch(1, 1); + grid->setColumnStretch(3, 1); + grp->setLayout(grid); + layout->addWidget(grp); + + QLayout *snapbox = new QHBoxLayout(); + + // GROUP: SNAP POINTS X + + grp = new QGroupBox(QLatin1String("Snap Positions X")); + QBoxLayout *vbox = new QVBoxLayout(); + vbox->setSpacing(m_smallscreen ? 4 : 2); + m_snapx = new QComboBox(); + m_snapx->addItem(QLatin1String("No Snapping"), NoSnap); + m_snapx->addItem(QLatin1String("Snap to Interval"), SnapToInterval); + m_snapx->addItem(QLatin1String("Snap to List"), SnapToList); + connect(m_snapx, SIGNAL(currentIndexChanged(int)), this, SLOT(snapModeChanged(int))); + vbox->addWidget(m_snapx); + + m_snapxinterval = new QWidget(); + grid = new QGridLayout(); + grid->setVerticalSpacing(m_smallscreen ? 4 : 2); + m_snapxfirst = new QSpinBox(); + connect(m_snapxfirst, SIGNAL(valueChanged(int)), this, SLOT(snapPositionsChanged())); + grid->addWidget(new QLabel("First:"), 0, 0); + grid->addWidget(m_snapxfirst, 0, 1); + m_snapxstep = new QSpinBox(); + connect(m_snapxstep, SIGNAL(valueChanged(int)), this, SLOT(snapPositionsChanged())); + grid->addWidget(new QLabel("Interval:"), 0, 2); + grid->addWidget(m_snapxstep, 0, 3); + m_snapxinterval->setLayout(grid); + vbox->addWidget(m_snapxinterval); + m_snapxinterval->hide(); + + m_snapxlist = new QPlainTextEdit(); + m_snapxlist->setToolTip(QLatin1String("One snap position per line. Empty lines are ignored.")); + m_snapxlist->installEventFilter(this); + connect(m_snapxlist, SIGNAL(textChanged()), this, SLOT(snapPositionsChanged())); + vbox->addWidget(m_snapxlist); + m_snapxlist->hide(); + + vbox->addStretch(100); + grp->setLayout(vbox); + snapbox->addWidget(grp); + + // GROUP: SNAP POINTS Y + + grp = new QGroupBox(QLatin1String("Snap Positions Y")); + vbox = new QVBoxLayout(); + vbox->setSpacing(m_smallscreen ? 4 : 2); + m_snapy = new QComboBox(); + m_snapy->addItem(QLatin1String("No Snapping"), NoSnap); + m_snapy->addItem(QLatin1String("Snap to Interval"), SnapToInterval); + m_snapy->addItem(QLatin1String("Snap to List"), SnapToList); + connect(m_snapy, SIGNAL(currentIndexChanged(int)), this, SLOT(snapModeChanged(int))); + vbox->addWidget(m_snapy); + + m_snapyinterval = new QWidget(); + grid = new QGridLayout(); + grid->setVerticalSpacing(m_smallscreen ? 4 : 2); + m_snapyfirst = new QSpinBox(); + connect(m_snapyfirst, SIGNAL(valueChanged(int)), this, SLOT(snapPositionsChanged())); + grid->addWidget(new QLabel("First:"), 0, 0); + grid->addWidget(m_snapyfirst, 0, 1); + m_snapystep = new QSpinBox(); + connect(m_snapystep, SIGNAL(valueChanged(int)), this, SLOT(snapPositionsChanged())); + grid->addWidget(new QLabel("Interval:"), 0, 2); + grid->addWidget(m_snapystep, 0, 3); + m_snapyinterval->setLayout(grid); + vbox->addWidget(m_snapyinterval); + m_snapyinterval->hide(); + + m_snapylist = new QPlainTextEdit(); + m_snapylist->setToolTip(QLatin1String("One snap position per line. Empty lines are ignored.")); + m_snapylist->installEventFilter(this); + connect(m_snapylist, SIGNAL(textChanged()), this, SLOT(snapPositionsChanged())); + vbox->addWidget(m_snapylist); + m_snapylist->hide(); + + vbox->addStretch(100); + grp->setLayout(vbox); + snapbox->addWidget(grp); + + layout->addLayout(snapbox); + + layout->addStretch(100); + setWidget(view); + setWidgetResizable(true); +} + +void SettingsWidget::setScroller(QWidget *widget) +{ + delete m_snapoverlay; + if (m_widget) + m_widget->removeEventFilter(this); + QAbstractScrollArea *area = qobject_cast<QAbstractScrollArea *>(widget); + if (area) + widget = area->viewport(); + m_widget = widget; + m_widget->installEventFilter(this); + m_snapoverlay = new SnapOverlay(area); + QScrollerProperties properties = QScroller::scroller(widget)->scrollerProperties(); + + QMutableListIterator<MetricItemUpdater *> it(m_metrics); + while (it.hasNext()) + it.next()->setScroller(widget); + + if (!widget) + return; + + updateScrollRanges(); +} + +bool SettingsWidget::eventFilter(QObject *o, QEvent *e) +{ + if (o == m_widget && e->type() == QEvent::Resize) + updateScrollRanges(); + return false; +} + +void SettingsWidget::updateScrollRanges() +{ + QScrollPrepareEvent spe(QPoint(0, 0)); + QApplication::sendEvent(m_widget, &spe); + + QSizeF vp = spe.viewportSize(); + QRectF maxc = spe.contentPosRange(); + + m_scrollx->setRange(qRound(-vp.width()), qRound(maxc.width() + vp.width())); + m_scrolly->setRange(qRound(-vp.height()), qRound(maxc.height() + vp.height())); + + m_snapxfirst->setRange(maxc.left(), maxc.right()); + m_snapxstep->setRange(0, maxc.width()); + m_snapyfirst->setRange(maxc.top(), maxc.bottom()); + m_snapystep->setRange(0, maxc.height()); +} + +void SettingsWidget::scrollTo() +{ + if (QApplication::activePopupWidget()) + return; + if ((sender() == m_scrollx) && !m_scrollx->hasFocus()) + return; + if ((sender() == m_scrolly) && !m_scrolly->hasFocus()) + return; + if ((sender() == m_scrolltime) && !m_scrolltime->hasFocus()) + return; + + if (QScroller *scroller = QScroller::scroller(m_widget)) + scroller->scrollTo(QPointF(m_scrollx->value(), m_scrolly->value()), m_scrolltime->value()); +} + +void SettingsWidget::snapModeChanged(int mode) +{ + if (sender() == m_snapx) { + m_snapxmode = static_cast<SnapMode>(mode); + m_snapxinterval->setVisible(mode == SnapToInterval); + m_snapxlist->setVisible(mode == SnapToList); + snapPositionsChanged(); + } else if (sender() == m_snapy) { + m_snapymode = static_cast<SnapMode>(mode); + m_snapyinterval->setVisible(mode == SnapToInterval); + m_snapylist->setVisible(mode == SnapToList); + snapPositionsChanged(); + } +} + +void SettingsWidget::snapPositionsChanged() +{ + QScroller *s = QScroller::scroller(m_widget); + if (!s) + return; + + switch (m_snapxmode) { + case NoSnap: + s->setSnapPositionsX(QList<qreal>()); + m_snapoverlay->clear(Qt::Horizontal); + break; + case SnapToInterval: + s->setSnapPositionsX(m_snapxfirst->value(), m_snapxstep->value()); + m_snapoverlay->set(Qt::Horizontal, m_snapxfirst->value(), m_snapxstep->value()); + break; + case SnapToList: + s->setSnapPositionsX(toPositionList(m_snapxlist, m_snapxfirst->minimum(), m_snapxfirst->maximum())); + m_snapoverlay->set(Qt::Horizontal, toPositionList(m_snapxlist, m_snapxfirst->minimum(), m_snapxfirst->maximum())); + break; + } + switch (m_snapymode) { + case NoSnap: + s->setSnapPositionsY(QList<qreal>()); + m_snapoverlay->clear(Qt::Vertical); + break; + case SnapToInterval: + s->setSnapPositionsY(m_snapyfirst->value(), m_snapystep->value()); + m_snapoverlay->set(Qt::Vertical, m_snapyfirst->value(), m_snapystep->value()); + break; + case SnapToList: + s->setSnapPositionsY(toPositionList(m_snapylist, m_snapyfirst->minimum(), m_snapyfirst->maximum())); + m_snapoverlay->set(Qt::Vertical, toPositionList(m_snapylist, m_snapyfirst->minimum(), m_snapyfirst->maximum())); + break; + } +} + +QList<qreal> SettingsWidget::toPositionList(QPlainTextEdit *list, int vmin, int vmax) +{ + QList<qreal> snaps; + QList<QTextEdit::ExtraSelection> extrasel; + QTextEdit::ExtraSelection uline; + uline.format.setUnderlineColor(Qt::red); + uline.format.setUnderlineStyle(QTextCharFormat::WaveUnderline); + int line = 0; + + foreach (const QString &str, list->toPlainText().split(QLatin1Char('\n'))) { + ++line; + if (str.isEmpty()) + continue; + bool ok = false; + double d = str.toDouble(&ok); + if (ok && d >= vmin && d <= vmax) { + snaps << d; + } else { + QTextEdit::ExtraSelection esel = uline; + esel.cursor = QTextCursor(list->document()->findBlockByLineNumber(line - 1)); + esel.cursor.select(QTextCursor::LineUnderCursor); + extrasel << esel; + } + } + list->setExtraSelections(extrasel); + return snaps; +} + +#include "settingswidget.moc" diff --git a/examples/scroller/plot/settingswidget.h b/examples/scroller/plot/settingswidget.h new file mode 100644 index 0000000..2fb268c --- /dev/null +++ b/examples/scroller/plot/settingswidget.h @@ -0,0 +1,107 @@ +/**************************************************************************** +** +** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef SETTINGSWIDGET_H +#define SETTINGSWIDGET_H + +#include <QScrollArea> + +class QScroller; +class QGridLayout; +class QSpinBox; +class QComboBox; +class QCheckBox; +class QPlainTextEdit; + +class MetricItemUpdater; +class SnapOverlay; + +class SettingsWidget : public QScrollArea +{ + Q_OBJECT + +public: + SettingsWidget(bool smallscreen = false); + + void setScroller(QWidget *widget); + +protected: + bool eventFilter(QObject *, QEvent *); + +private slots: + void scrollTo(); + void snapModeChanged(int); + void snapPositionsChanged(); + +private: + enum SnapMode { + NoSnap, + SnapToInterval, + SnapToList + }; + + void addToGrid(QGridLayout *grid, QWidget *label, int widgetCount, ...); + QList<qreal> toPositionList(QPlainTextEdit *list, int vmin, int vmax); + void updateScrollRanges(); + + QWidget *m_widget; + QSpinBox *m_scrollx, *m_scrolly, *m_scrolltime; + QList<MetricItemUpdater *> m_metrics; + + SnapMode m_snapxmode; + QComboBox *m_snapx; + QWidget *m_snapxinterval; + QPlainTextEdit *m_snapxlist; + QSpinBox *m_snapxfirst; + QSpinBox *m_snapxstep; + + SnapMode m_snapymode; + QComboBox *m_snapy; + QWidget *m_snapyinterval; + QPlainTextEdit *m_snapylist; + QSpinBox *m_snapyfirst; + QSpinBox *m_snapystep; + SnapOverlay *m_snapoverlay; + + bool m_smallscreen; +}; + +#endif diff --git a/examples/scroller/scroller.pro b/examples/scroller/scroller.pro new file mode 100644 index 0000000..e830745 --- /dev/null +++ b/examples/scroller/scroller.pro @@ -0,0 +1,11 @@ +TEMPLATE = subdirs +SUBDIRS = graphicsview \ + plot \ + wheel + +# install +sources.files = *.pro +sources.path = $$[QT_INSTALL_EXAMPLES]/scroller +INSTALLS += sources + +symbian: include($$QT_SOURCE_TREE/examples/symbianpkgrules.pri) diff --git a/examples/scroller/wheel/main.cpp b/examples/scroller/wheel/main.cpp new file mode 100644 index 0000000..4264377 --- /dev/null +++ b/examples/scroller/wheel/main.cpp @@ -0,0 +1,119 @@ +/**************************************************************************** +** +** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include <QtGui> +#include <qmath.h> + +#include "wheelwidget.h" + +class MainWindow : public QMainWindow +{ + Q_OBJECT +public: + MainWindow(bool touch) + : QMainWindow() + { + makeSlotMachine(touch); + setCentralWidget(m_slotMachine); + } + + void makeSlotMachine(bool touch) + { + if (QApplication::desktop()->width() > 1000) { + QFont f = font(); + f.setPointSize(f.pointSize() * 2); + setFont(f); + } + + m_slotMachine = new QWidget(this); + QGridLayout *grid = new QGridLayout(m_slotMachine); + grid->setSpacing(20); + + QStringList colors; + colors << "Red" << "Magenta" << "Peach" << "Orange" << "Yellow" << "Citro" << "Green" << "Cyan" << "Blue" << "Violet"; + + m_wheel1 = new StringWheelWidget(touch); + m_wheel1->setItems( colors ); + grid->addWidget( m_wheel1, 0, 0 ); + + m_wheel2 = new StringWheelWidget(touch); + m_wheel2->setItems( colors ); + grid->addWidget( m_wheel2, 0, 1 ); + + m_wheel3 = new StringWheelWidget(touch); + m_wheel3->setItems( colors ); + grid->addWidget( m_wheel3, 0, 2 ); + + QPushButton *shakeButton = new QPushButton(tr("Shake")); + connect(shakeButton, SIGNAL(clicked()), this, SLOT(rotateRandom())); + + grid->addWidget( shakeButton, 1, 0, 1, 3 ); + } + +private slots: + void rotateRandom() + { + m_wheel1->scrollTo(m_wheel1->currentIndex() + (qrand() % 200)); + m_wheel2->scrollTo(m_wheel2->currentIndex() + (qrand() % 200)); + m_wheel3->scrollTo(m_wheel3->currentIndex() + (qrand() % 200)); + } + +private: + QWidget *m_slotMachine; + + StringWheelWidget *m_wheel1; + StringWheelWidget *m_wheel2; + StringWheelWidget *m_wheel3; +}; + +int main(int argc, char **argv) +{ + QApplication a(argc, argv); + + bool touch = a.arguments().contains(QLatin1String("--touch")); + + MainWindow *mw = new MainWindow(touch); + mw->show(); + + return a.exec(); +} + +#include "main.moc" diff --git a/examples/scroller/wheel/wheel.pro b/examples/scroller/wheel/wheel.pro new file mode 100644 index 0000000..1f9b789 --- /dev/null +++ b/examples/scroller/wheel/wheel.pro @@ -0,0 +1,16 @@ +HEADERS = wheelwidget.h +SOURCES = wheelwidget.cpp \ + main.cpp + +QT += webkit + +# install +target.path = $$[QT_INSTALL_EXAMPLES]/scroller/wheel +sources.files = $$SOURCES $$HEADERS $$RESOURCES $$FORMS wheel.pro +sources.path = $$[QT_INSTALL_EXAMPLES]/scroller/wheel +INSTALLS += target sources + +symbian { + TARGET.UID3 = 0xA000CF66 + include($$QT_SOURCE_TREE/examples/symbianpkgrules.pri) +} diff --git a/examples/scroller/wheel/wheelwidget.cpp b/examples/scroller/wheel/wheelwidget.cpp new file mode 100644 index 0000000..64a459b --- /dev/null +++ b/examples/scroller/wheel/wheelwidget.cpp @@ -0,0 +1,276 @@ +/**************************************************************************** +** +** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include <QtGui> + +#include "wheelwidget.h" + +#define WHEEL_SCROLL_OFFSET 50000.0 + +AbstractWheelWidget::AbstractWheelWidget(bool touch, QWidget *parent) + : QWidget(parent) + , m_currentItem(0) + , m_itemOffset(0) +{ +// ![0] + QScroller::grabGesture(this, touch ? QScroller::TouchGesture : QScroller::LeftMouseButtonGesture); +// ![0] +} + +AbstractWheelWidget::~AbstractWheelWidget() +{ } + +int AbstractWheelWidget::currentIndex() const +{ + return m_currentItem; +} + +void AbstractWheelWidget::setCurrentIndex(int index) +{ + if (index >= 0 && index < itemCount()) { + m_currentItem = index; + m_itemOffset = 0; + update(); + } +} + +bool AbstractWheelWidget::event(QEvent *e) +{ + switch (e->type()) { +// ![1] + case QEvent::ScrollPrepare: + { + // We set the snap positions as late as possible so that we are sure + // we get the correct itemHeight + QScroller *scroller = QScroller::scroller(this); + scroller->setSnapPositionsY( WHEEL_SCROLL_OFFSET, itemHeight() ); + + QScrollPrepareEvent *se = static_cast<QScrollPrepareEvent *>(e); + se->setViewportSize(QSizeF(size())); + // we claim a huge scrolling area and a huge content position and + // hope that the user doesn't notice that the scroll area is restricted + se->setContentPosRange(QRectF(0.0, 0.0, 0.0, WHEEL_SCROLL_OFFSET * 2)); + se->setContentPos(QPointF(0.0, WHEEL_SCROLL_OFFSET + m_currentItem * itemHeight() + m_itemOffset)); + se->accept(); + return true; + } +// ![1] +// ![2] + case QEvent::Scroll: + { + QScrollEvent *se = static_cast<QScrollEvent *>(e); + + qreal y = se->contentPos().y(); + int iy = y - WHEEL_SCROLL_OFFSET; + int ih = itemHeight(); + +// ![2] + + // -- calculate the current item position and offset and redraw the widget + int ic = itemCount(); + if (ic>0) { + m_currentItem = iy / ih % ic; + m_itemOffset = iy % ih; + + // take care when scrolling backwards. Modulo returns negative numbers + if (m_itemOffset < 0) { + m_itemOffset += ih; + m_currentItem--; + } + + if (m_currentItem < 0) + m_currentItem += ic; + } + // -- repaint + update(); + + se->accept(); + return true; + } + default: + return QWidget::event(e); + } + return true; +} + +void AbstractWheelWidget::paintEvent(QPaintEvent* event) +{ + Q_UNUSED( event ); + + // -- first calculate size and position. + int w = width(); + int h = height(); + + QPainter painter(this); + QPalette palette = QApplication::palette(); + QPalette::ColorGroup colorGroup = isEnabled() ? QPalette::Active : QPalette::Disabled; + + // linear gradient brush + QLinearGradient grad(0.5, 0, 0.5, 1.0); + grad.setColorAt(0, palette.color(colorGroup, QPalette::ButtonText)); + grad.setColorAt(0.2, palette.color(colorGroup, QPalette::Button)); + grad.setColorAt(0.8, palette.color(colorGroup, QPalette::Button)); + grad.setColorAt(1.0, palette.color(colorGroup, QPalette::ButtonText)); + grad.setCoordinateMode( QGradient::ObjectBoundingMode ); + QBrush gBrush( grad ); + + // paint a border and background + painter.setPen(palette.color(colorGroup, QPalette::ButtonText)); + painter.setBrush(gBrush); + // painter.setBrushOrigin( QPointF( 0.0, 0.0 ) ); + painter.drawRect( 0, 0, w-1, h-1 ); + + // paint inner border + painter.setPen(palette.color(colorGroup, QPalette::Button)); + painter.setBrush(Qt::NoBrush); + painter.drawRect( 1, 1, w-3, h-3 ); + + // paint the items + painter.setClipRect( QRect( 3, 3, w-6, h-6 ) ); + painter.setPen(palette.color(colorGroup, QPalette::ButtonText)); + + int iH = itemHeight(); + int iC = itemCount(); + if (iC > 0) { + + m_itemOffset = m_itemOffset % iH; + + for (int i=-h/2/iH; i<=h/2/iH+1; i++) { + + int itemNum = m_currentItem + i; + while (itemNum < 0) + itemNum += iC; + while (itemNum >= iC) + itemNum -= iC; + + paintItem(&painter, itemNum, QRect(6, h/2 +i*iH - m_itemOffset - iH/2, w-6, iH )); + } + } + + // draw a transparent bar over the center + QColor highlight = palette.color(colorGroup, QPalette::Highlight); + highlight.setAlpha(150); + + QLinearGradient grad2(0.5, 0, 0.5, 1.0); + grad2.setColorAt(0, highlight); + grad2.setColorAt(1.0, highlight.lighter()); + grad2.setCoordinateMode( QGradient::ObjectBoundingMode ); + QBrush gBrush2( grad2 ); + + QLinearGradient grad3(0.5, 0, 0.5, 1.0); + grad3.setColorAt(0, highlight); + grad3.setColorAt(1.0, highlight.darker()); + grad3.setCoordinateMode( QGradient::ObjectBoundingMode ); + QBrush gBrush3( grad3 ); + + painter.fillRect( QRect( 0, h/2 - iH/2, w, iH/2 ), gBrush2 ); + painter.fillRect( QRect( 0, h/2, w, iH/2 ), gBrush3 ); +} + +/*! + Rotates the wheel widget to a given index. + You can also give an index greater than itemCount or less than zero in which + case the wheel widget will scroll in the given direction and end up with + (index % itemCount) +*/ +void AbstractWheelWidget::scrollTo(int index) +{ + QScroller *scroller = QScroller::scroller(this); + + scroller->scrollTo(QPointF(0, WHEEL_SCROLL_OFFSET + index * itemHeight()), 5000); +} + +/*! + \class StringWheelWidget + \brief The StringWheelWidget class is an implementation of the AbstractWheelWidget class that draws QStrings as items. + \sa AbstractWheelWidget +*/ + +StringWheelWidget::StringWheelWidget(bool touch) + : AbstractWheelWidget(touch) +{ } + +QStringList StringWheelWidget::items() const +{ + return m_items; +} + +void StringWheelWidget::setItems( const QStringList &items ) +{ + m_items = items; + if (m_currentItem >= items.count()) + m_currentItem = items.count()-1; + update(); +} + + +QSize StringWheelWidget::sizeHint() const +{ + // determine font size + QFontMetrics fm(font()); + + return QSize( fm.width("m") * 10 + 6, fm.height() * 7 + 6 ); +} + +QSize StringWheelWidget::minimumSizeHint() const +{ + QFontMetrics fm(font()); + + return QSize( fm.width("m") * 5 + 6, fm.height() * 3 + 6 ); +} + +void StringWheelWidget::paintItem(QPainter* painter, int index, const QRect &rect) +{ + painter->drawText(rect, Qt::AlignCenter, m_items.at(index)); +} + +int StringWheelWidget::itemHeight() const +{ + QFontMetrics fm(font()); + return fm.height(); +} + +int StringWheelWidget::itemCount() const +{ + return m_items.count(); +} + + diff --git a/examples/scroller/wheel/wheelwidget.h b/examples/scroller/wheel/wheelwidget.h new file mode 100644 index 0000000..818b6ab --- /dev/null +++ b/examples/scroller/wheel/wheelwidget.h @@ -0,0 +1,102 @@ +/**************************************************************************** +** +** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef WHEELWIDGET_H +#define WHEELWIDGET_H + +#include <QWidget> +#include <QStringList> + +class QPainter; +class QRect; + +class AbstractWheelWidget : public QWidget { + Q_OBJECT + +public: + AbstractWheelWidget(bool touch, QWidget *parent = 0); + virtual ~AbstractWheelWidget(); + + int currentIndex() const; + void setCurrentIndex(int index); + + bool event(QEvent*); + void paintEvent(QPaintEvent *e); + virtual void paintItem(QPainter* painter, int index, const QRect &rect) = 0; + + virtual int itemHeight() const = 0; + virtual int itemCount() const = 0; + +public slots: + void scrollTo(int index); + +signals: + void stopped(int index); + +protected: + int m_currentItem; + int m_itemOffset; // 0-itemHeight() + qreal m_lastY; +}; + + +class StringWheelWidget : public AbstractWheelWidget { + Q_OBJECT + +public: + StringWheelWidget(bool touch); + + QStringList items() const; + void setItems( const QStringList &items ); + + QSize sizeHint() const; + QSize minimumSizeHint() const; + + void paintItem(QPainter* painter, int index, const QRect &rect); + + int itemHeight() const; + int itemCount() const; + +private: + QStringList m_items; +}; + +#endif // WHEELWIDGET_H diff --git a/src/corelib/kernel/qcoreevent.cpp b/src/corelib/kernel/qcoreevent.cpp index d23ea4c..4da5fa9 100644 --- a/src/corelib/kernel/qcoreevent.cpp +++ b/src/corelib/kernel/qcoreevent.cpp @@ -231,6 +231,8 @@ QT_BEGIN_NAMESPACE \value WinIdChange The window system identifer for this native widget has changed \value Gesture A gesture was triggered (QGestureEvent) \value GestureOverride A gesture override was triggered (QGestureEvent) + \value ScrollPrepare The object needs to fill in its geometry information (QScrollPrepareEvent) + \value Scroll The object needs to scroll to the supplied position (QScrollEvent) User events should have values between \c User and \c{MaxUser}: diff --git a/src/corelib/kernel/qcoreevent.h b/src/corelib/kernel/qcoreevent.h index 4c91aaf..08b751c 100644 --- a/src/corelib/kernel/qcoreevent.h +++ b/src/corelib/kernel/qcoreevent.h @@ -288,6 +288,9 @@ public: Gesture = 198, GestureOverride = 202, #endif + ScrollPrepare = 204, + Scroll = 205, + // 512 reserved for Qt Jambi's MetaCall event // 513 reserved for Qt Jambi's DeleteOnMainThread event diff --git a/src/gui/itemviews/qabstractitemview.cpp b/src/gui/itemviews/qabstractitemview.cpp index 8af6013..f2d8303 100644 --- a/src/gui/itemviews/qabstractitemview.cpp +++ b/src/gui/itemviews/qabstractitemview.cpp @@ -62,6 +62,9 @@ #include <qaccessible.h> #endif #include <private/qsoftkeymanager_p.h> +#ifndef QT_NO_GESTURE +# include <qscroller.h> +#endif QT_BEGIN_NAMESPACE @@ -191,6 +194,37 @@ void QAbstractItemViewPrivate::checkMouseMove(const QPersistentModelIndex &index } } +// stores and restores the selection and current item when flicking +void QAbstractItemViewPrivate::_q_scrollerStateChanged() +{ + Q_Q(QAbstractItemView); + + if (QScroller *scroller = QScroller::scroller(viewport)) { + switch (scroller->state()) { + case QScroller::Pressed: + // store the current selection in case we start scrolling + if (q->selectionModel()) { + oldSelection = q->selectionModel()->selection(); + oldCurrent = q->selectionModel()->currentIndex(); + } + break; + + case QScroller::Dragging: + // restore the old selection if we really start scrolling + if (q->selectionModel()) { + q->selectionModel()->select(oldSelection, QItemSelectionModel::ClearAndSelect); + q->selectionModel()->setCurrentIndex(oldCurrent, QItemSelectionModel::NoUpdate); + } + // fall through + + default: + oldSelection = QItemSelection(); + oldCurrent = QModelIndex(); + break; + } + } +} + /*! \class QAbstractItemView @@ -1616,6 +1650,11 @@ bool QAbstractItemView::viewportEvent(QEvent *event) case QEvent::WindowDeactivate: d->viewport->update(); break; + case QEvent::ScrollPrepare: + executeDelayedItemsLayout(); + connect(QScroller::scroller(d->viewport), SIGNAL(stateChanged(QScroller::State)), this, SLOT(_q_scrollerStateChanged()), Qt::UniqueConnection); + break; + default: break; } diff --git a/src/gui/itemviews/qabstractitemview.h b/src/gui/itemviews/qabstractitemview.h index 0f86f62..6f7db09 100644 --- a/src/gui/itemviews/qabstractitemview.h +++ b/src/gui/itemviews/qabstractitemview.h @@ -359,6 +359,7 @@ private: Q_PRIVATE_SLOT(d_func(), void _q_modelDestroyed()) Q_PRIVATE_SLOT(d_func(), void _q_layoutChanged()) Q_PRIVATE_SLOT(d_func(), void _q_headerDataChanged()) + Q_PRIVATE_SLOT(d_func(), void _q_scrollerStateChanged()) friend class QTreeViewPrivate; // needed to compile with MSVC friend class QAccessibleItemRow; diff --git a/src/gui/itemviews/qabstractitemview_p.h b/src/gui/itemviews/qabstractitemview_p.h index 03b413a..be20dce 100644 --- a/src/gui/itemviews/qabstractitemview_p.h +++ b/src/gui/itemviews/qabstractitemview_p.h @@ -114,6 +114,7 @@ public: virtual void _q_modelDestroyed(); virtual void _q_layoutChanged(); void _q_headerDataChanged() { doDelayedItemsLayout(); } + void _q_scrollerStateChanged(); void fetchMore(); @@ -414,6 +415,12 @@ public: QAbstractItemView::ScrollMode verticalScrollMode; QAbstractItemView::ScrollMode horizontalScrollMode; +#ifndef QT_NO_GESTURES + // the selection before the last mouse down. In case we have to restore it for scrolling + QItemSelection oldSelection; + QModelIndex oldCurrent; +#endif + bool currentIndexSet; bool wrapItemText; diff --git a/src/gui/kernel/qevent.cpp b/src/gui/kernel/qevent.cpp index 5633cb8..422aeeb 100644 --- a/src/gui/kernel/qevent.cpp +++ b/src/gui/kernel/qevent.cpp @@ -4568,4 +4568,223 @@ const QGestureEventPrivate *QGestureEvent::d_func() const #endif // QT_NO_GESTURES +/*! + \class QScrollPrepareEvent + \since 4.8 + \ingroup events + + \brief The QScrollPrepareEvent class is send in preparation of a scrolling. + + The scroll prepare event is send before scrolling (usually by QScroller) is started. + The object receiving this event should set viewportSize, maxContentPos and contentPos. + It also should accept this event to indicate that scrolling should be started. + + It is not guaranteed that a QScrollEvent will be send after an acceepted + QScrollPrepareEvent, e.g. in a case where the maximum content position is (0,0). + + \sa QScrollEvent, QScroller +*/ + +/*! + Creates new QScrollPrepareEvent + The \a startPos is the position of a touch or mouse event that started the scrolling. +*/ +QScrollPrepareEvent::QScrollPrepareEvent(const QPointF &startPos) + : QEvent(QEvent::ScrollPrepare) +{ + d = reinterpret_cast<QEventPrivate *>(new QScrollPrepareEventPrivate()); + d_func()->startPos = startPos; +} + +/*! + Destroys QScrollEvent. +*/ +QScrollPrepareEvent::~QScrollPrepareEvent() +{ + delete reinterpret_cast<QScrollPrepareEventPrivate *>(d); +} + +/*! + Returns the position of the touch or mouse event that started the scrolling. +*/ +QPointF QScrollPrepareEvent::startPos() const +{ + return d_func()->startPos; +} + +/*! + Returns size of the area that is to be scrolled as set by setViewportSize + + \sa setViewportSize() +*/ +QSizeF QScrollPrepareEvent::viewportSize() const +{ + return d_func()->viewportSize; +} + +/*! + Returns the range of coordinates for the content as set by setContentPosRange(). +*/ +QRectF QScrollPrepareEvent::contentPosRange() const +{ + return d_func()->contentPosRange; +} + +/*! + Returns the current position of the content as set by setContentPos. +*/ +QPointF QScrollPrepareEvent::contentPos() const +{ + return d_func()->contentPos; +} + + +/*! + Sets the size of the area that is to be scrolled to \a size. + + \sa viewportSize() +*/ +void QScrollPrepareEvent::setViewportSize(const QSizeF &size) +{ + d_func()->viewportSize = size; +} + +/*! + Sets the range of content coordinates to \a rect. + + \sa contentPosRange() +*/ +void QScrollPrepareEvent::setContentPosRange(const QRectF &rect) +{ + d_func()->contentPosRange = rect; +} + +/*! + Sets the current content position to \a pos. + + \sa contentPos() +*/ +void QScrollPrepareEvent::setContentPos(const QPointF &pos) +{ + d_func()->contentPos = pos; +} + + +/*! + \internal +*/ +QScrollPrepareEventPrivate *QScrollPrepareEvent::d_func() +{ + return reinterpret_cast<QScrollPrepareEventPrivate *>(d); +} + +/*! + \internal +*/ +const QScrollPrepareEventPrivate *QScrollPrepareEvent::d_func() const +{ + return reinterpret_cast<const QScrollPrepareEventPrivate *>(d); +} + +/*! + \class QScrollEvent + \since 4.8 + \ingroup events + + \brief The QScrollEvent class is send when scrolling. + + The scroll event is send to indicate that the receiver should be scrolled. + Usually the receiver should be something visual like QWidget or QGraphicsObject. + + Some care should be taken that no conflicting QScrollEvents are sent from two + sources. Using QScroller::scrollTo is save however. + + \sa QScrollPrepareEvent, QScroller +*/ + +/*! + \enum QScrollEvent::ScrollState + + This enum describes the states a scroll event can have. + + \value ScrollStarted Set for the first scroll event of a scroll activity. + + \value ScrollUpdated Set for all but the first and the last scroll event of a scroll activity. + + \value ScrollFinished Set for the last scroll event of a scroll activity. + + \sa QScrollEvent::scrollState() +*/ + +/*! + Creates a new QScrollEvent + \a contentPos is the new content position, \a overshootDistance is the + new overshoot distance while \a scrollState indicates if this scroll + event is the first one, the last one or some event in between. +*/ +QScrollEvent::QScrollEvent(const QPointF &contentPos, const QPointF &overshootDistance, ScrollState scrollState) + : QEvent(QEvent::Scroll) +{ + d = reinterpret_cast<QEventPrivate *>(new QScrollEventPrivate()); + d_func()->contentPos = contentPos; + d_func()->overshoot= overshootDistance; + d_func()->state = scrollState; +} + +/*! + Destroys QScrollEvent. +*/ +QScrollEvent::~QScrollEvent() +{ + delete reinterpret_cast<QScrollEventPrivate *>(d); +} + +/*! + Returns the new scroll position. +*/ +QPointF QScrollEvent::contentPos() const +{ + return d_func()->contentPos; +} + +/*! + Returns the new overshoot distance. + See QScroller for an explanation of the term overshoot. + + \sa QScroller +*/ +QPointF QScrollEvent::overshootDistance() const +{ + return d_func()->overshoot; +} + +/*! + Returns the current scroll state as a combination of ScrollStateFlag values. + ScrollStarted (or ScrollFinished) will be set, if this scroll event is the first (or last) event in a scrolling activity. + Please note that both values can be set at the same time, if the activity consists of a single QScrollEvent. + All other scroll events in between will have their state set to ScrollUpdated. + + A widget could for example revert selections when scrolling is started and stopped. +*/ +QScrollEvent::ScrollState QScrollEvent::scrollState() const +{ + return d_func()->state; +} + +/*! + \internal +*/ +QScrollEventPrivate *QScrollEvent::d_func() +{ + return reinterpret_cast<QScrollEventPrivate *>(d); +} + +/*! + \internal +*/ +const QScrollEventPrivate *QScrollEvent::d_func() const +{ + return reinterpret_cast<const QScrollEventPrivate *>(d); +} + QT_END_NAMESPACE diff --git a/src/gui/kernel/qevent.h b/src/gui/kernel/qevent.h index 9c70c02..6c0076d 100644 --- a/src/gui/kernel/qevent.h +++ b/src/gui/kernel/qevent.h @@ -880,6 +880,52 @@ private: }; #endif // QT_NO_GESTURES +class QScrollPrepareEventPrivate; +class Q_GUI_EXPORT QScrollPrepareEvent : public QEvent +{ +public: + QScrollPrepareEvent(const QPointF &startPos); + ~QScrollPrepareEvent(); + + QPointF startPos() const; + + QSizeF viewportSize() const; + QRectF contentPosRange() const; + QPointF contentPos() const; + + void setViewportSize(const QSizeF &size); + void setContentPosRange(const QRectF &rect); + void setContentPos(const QPointF &pos); + +private: + QScrollPrepareEventPrivate *d_func(); + const QScrollPrepareEventPrivate *d_func() const; +}; + + +class QScrollEventPrivate; +class Q_GUI_EXPORT QScrollEvent : public QEvent +{ +public: + enum ScrollState + { + ScrollStarted, + ScrollUpdated, + ScrollFinished + }; + + QScrollEvent(const QPointF &contentPos, const QPointF &overshoot, ScrollState scrollState); + ~QScrollEvent(); + + QPointF contentPos() const; + QPointF overshootDistance() const; + ScrollState scrollState() const; + +private: + QScrollEventPrivate *d_func(); + const QScrollEventPrivate *d_func() const; +}; + QT_END_NAMESPACE QT_END_HEADER diff --git a/src/gui/kernel/qevent_p.h b/src/gui/kernel/qevent_p.h index e323aa9..02f41e7 100644 --- a/src/gui/kernel/qevent_p.h +++ b/src/gui/kernel/qevent_p.h @@ -178,6 +178,34 @@ public: QUrl url; }; + +class QScrollPrepareEventPrivate +{ +public: + inline QScrollPrepareEventPrivate() + : target(0) + { + } + + QObject* target; + QPointF startPos; + QSizeF viewportSize; + QRectF contentPosRange; + QPointF contentPos; +}; + +class QScrollEventPrivate +{ +public: + inline QScrollEventPrivate() + { + } + + QPointF contentPos; + QPointF overshoot; + QScrollEvent::ScrollState state; +}; + QT_END_NAMESPACE #endif // QEVENT_P_H diff --git a/src/gui/util/qflickgesture.cpp b/src/gui/util/qflickgesture.cpp new file mode 100644 index 0000000..fa04754 --- /dev/null +++ b/src/gui/util/qflickgesture.cpp @@ -0,0 +1,677 @@ +/**************************************************************************** +** +** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qgesture.h" +#include "qcoreapplication.h" +#include "qevent.h" +#include "qwidget.h" +#include "qgraphicsitem.h" +#include "qgraphicsscene.h" +#include "qgraphicssceneevent.h" +#include "qgraphicsview.h" +#include "qscroller.h" +#include "private/qevent_p.h" +#include "private/qflickgesture_p.h" +#include "qdebug.h" + +#ifndef QT_NO_GESTURES + +QT_BEGIN_NAMESPACE + +//#define QFLICKGESTURE_DEBUG + +#ifdef QFLICKGESTURE_DEBUG +# define qFGDebug qDebug +#else +# define qFGDebug while (false) qDebug +#endif + +extern bool qt_sendSpontaneousEvent(QObject *receiver, QEvent *event); + +static QMouseEvent *copyMouseEvent(QEvent *e) +{ + switch (e->type()) { + case QEvent::MouseButtonPress: + case QEvent::MouseButtonRelease: + case QEvent::MouseMove: { + QMouseEvent *me = static_cast<QMouseEvent *>(e); + return new QMouseEvent(me->type(), QPoint(0, 0), me->globalPos(), me->button(), me->buttons(), me->modifiers()); + } + case QEvent::GraphicsSceneMousePress: + case QEvent::GraphicsSceneMouseRelease: + case QEvent::GraphicsSceneMouseMove: { + QGraphicsSceneMouseEvent *me = static_cast<QGraphicsSceneMouseEvent *>(e); +#if 1 + QEvent::Type met = me->type() == QEvent::GraphicsSceneMousePress ? QEvent::MouseButtonPress : + (me->type() == QEvent::GraphicsSceneMouseRelease ? QEvent::MouseButtonRelease : QEvent::MouseMove); + return new QMouseEvent(met, QPoint(0, 0), me->screenPos(), me->button(), me->buttons(), me->modifiers()); +#else + QGraphicsSceneMouseEvent *copy = new QGraphicsSceneMouseEvent(me->type()); + copy->setPos(me->pos()); + copy->setScenePos(me->scenePos()); + copy->setScreenPos(me->screenPos()); + for (int i = 0x1; i <= 0x10; i <<= 1) { + Qt::MouseButton button = Qt::MouseButton(i); + copy->setButtonDownPos(button, me->buttonDownPos(button)); + copy->setButtonDownScenePos(button, me->buttonDownScenePos(button)); + copy->setButtonDownScreenPos(button, me->buttonDownScreenPos(button)); + } + copy->setLastPos(me->lastPos()); + copy->setLastScenePos(me->lastScenePos()); + copy->setLastScreenPos(me->lastScreenPos()); + copy->setButtons(me->buttons()); + copy->setButton(me->button()); + copy->setModifiers(me->modifiers()); + return copy; +#endif + } + default: + return 0; + } +} + +class PressDelayHandler : public QObject +{ +private: + PressDelayHandler(QObject *parent = 0) + : QObject(parent) + , pressDelayTimer(0) + , sendingEvent(false) + , mouseButton(Qt::NoButton) + , mouseTarget(0) + { } + + static PressDelayHandler *inst; + +public: + enum { + UngrabMouseBefore = 1, + RegrabMouseAfterwards = 2 + }; + + static PressDelayHandler *instance() + { + static PressDelayHandler *inst = 0; + if (!inst) + inst = new PressDelayHandler(QCoreApplication::instance()); + return inst; + } + + bool shouldEventBeIgnored(QEvent *) const + { + return sendingEvent; + } + + bool isDelaying() const + { + return !pressDelayEvent.isNull(); + } + + void pressed(QEvent *e, int delay) + { + if (!pressDelayEvent) { + pressDelayEvent.reset(copyMouseEvent(e)); + pressDelayTimer = startTimer(delay); + mouseTarget = QApplication::widgetAt(pressDelayEvent->globalPos()); + mouseButton = pressDelayEvent->button(); + qFGDebug() << "QFG: consuming/delaying mouse press"; + } else { + qFGDebug() << "QFG: NOT consuming/delaying mouse press"; + } + e->setAccepted(true); + } + + bool released(QEvent *e, bool scrollerWasActive, bool scrollerIsActive) + { + // consume this event if the scroller was or is active + bool result = scrollerWasActive || scrollerIsActive; + + // stop the timer + if (pressDelayTimer) { + killTimer(pressDelayTimer); + pressDelayTimer = 0; + } + // we still haven't even sent the press, so do it now + if (pressDelayEvent && mouseTarget && !scrollerIsActive) { + QScopedPointer<QMouseEvent> releaseEvent(copyMouseEvent(e)); + + qFGDebug() << "QFG: re-sending mouse press (due to release) for " << mouseTarget; + sendMouseEvent(pressDelayEvent.data(), UngrabMouseBefore); + + qFGDebug() << "QFG: faking mouse release (due to release) for " << mouseTarget; + sendMouseEvent(releaseEvent.data()); + + result = true; // consume this event + } else if (mouseTarget && scrollerIsActive) { + // we grabbed the mouse expicitly when the scroller became active, so undo that now + sendMouseEvent(0, UngrabMouseBefore); + } + pressDelayEvent.reset(0); + mouseTarget = 0; + return result; + } + + void scrollerWasIntercepted() + { + qFGDebug() << "QFG: deleting delayed mouse press, since scroller was only intercepted"; + if (pressDelayEvent) { + // we still haven't even sent the press, so just throw it away now + if (pressDelayTimer) { + killTimer(pressDelayTimer); + pressDelayTimer = 0; + } + pressDelayEvent.reset(0); + } + mouseTarget = 0; + } + + void scrollerBecameActive() + { + if (pressDelayEvent) { + // we still haven't even sent the press, so just throw it away now + qFGDebug() << "QFG: deleting delayed mouse press, since scroller is active now"; + if (pressDelayTimer) { + killTimer(pressDelayTimer); + pressDelayTimer = 0; + } + pressDelayEvent.reset(0); + mouseTarget = 0; + } else if (mouseTarget) { + // we did send a press, so we need to fake a release now + Qt::MouseButtons mouseButtons = QApplication::mouseButtons(); + + // release all pressed mouse buttons + /*for (int i = 0; i < 32; ++i) { + if (mouseButtons & (1 << i)) { + Qt::MouseButton b = static_cast<Qt::MouseButton>(1 << i); + mouseButtons &= ~b; + QPoint farFarAway(-QWIDGETSIZE_MAX, -QWIDGETSIZE_MAX); + + qFGDebug() << "QFG: sending a fake mouse release at far-far-away to " << mouseTarget; + QMouseEvent re(QEvent::MouseButtonRelease, QPoint(), farFarAway, + b, mouseButtons, QApplication::keyboardModifiers()); + sendMouseEvent(&re); + } + }*/ + + QPoint farFarAway(-QWIDGETSIZE_MAX, -QWIDGETSIZE_MAX); + + qFGDebug() << "QFG: sending a fake mouse release at far-far-away to " << mouseTarget; + QMouseEvent re(QEvent::MouseButtonRelease, QPoint(), farFarAway, + mouseButton, QApplication::mouseButtons() & ~mouseButton, + QApplication::keyboardModifiers()); + sendMouseEvent(&re, RegrabMouseAfterwards); + // don't clear the mouseTarget just yet, since we need to explicitly ungrab the mouse on release! + } + } + +protected: + void timerEvent(QTimerEvent *e) + { + if (e->timerId() == pressDelayTimer) { + if (pressDelayEvent && mouseTarget) { + qFGDebug() << "QFG: timer event: re-sending mouse press to " << mouseTarget; + sendMouseEvent(pressDelayEvent.data(), UngrabMouseBefore); + } + pressDelayEvent.reset(0); + + if (pressDelayTimer) { + killTimer(pressDelayTimer); + pressDelayTimer = 0; + } + } + } + + void sendMouseEvent(QMouseEvent *me, int flags = 0) + { + if (mouseTarget) { + sendingEvent = true; + + QGraphicsItem *grabber = 0; + if (mouseTarget->parentWidget()) { + if (QGraphicsView *gv = qobject_cast<QGraphicsView *>(mouseTarget->parentWidget())) { + if (gv->scene()) + grabber = gv->scene()->mouseGrabberItem(); + } + } + + if (grabber && (flags & UngrabMouseBefore)) { + // GraphicsView Mouse Handling Workaround #1: + // we need to ungrab the mouse before re-sending the press, + // since the scene had already set the mouse grabber to the + // original (and consumed) event's receiver + qFGDebug() << "QFG: ungrabbing" << grabber; + grabber->ungrabMouse(); + } + + if (me) { + QMouseEvent copy(me->type(), mouseTarget->mapFromGlobal(me->globalPos()), me->globalPos(), me->button(), me->buttons(), me->modifiers()); + qt_sendSpontaneousEvent(mouseTarget, ©); + } + + if (grabber && (flags & RegrabMouseAfterwards)) { + // GraphicsView Mouse Handling Workaround #2: + // we need to re-grab the mouse after sending a faked mouse + // release, since we still need the mouse moves for the gesture + // (the scene will clear the item's mouse grabber status on + // release). + qFGDebug() << "QFG: re-grabbing" << grabber; + grabber->grabMouse(); + } + sendingEvent = false; + } + } + + +private: + int pressDelayTimer; + QScopedPointer<QMouseEvent> pressDelayEvent; + bool sendingEvent; + Qt::MouseButton mouseButton; + QPointer<QWidget> mouseTarget; +}; + + +/*! + \internal + \class QFlickGesture + \since 4.8 + \brief The QFlickGesture class describes a flicking gesture made by the user. + \ingroup gestures + The QFlickGesture is more complex than the QPanGesture that uses QScroller and QScrollerProperties + to decide if it is triggered. + This gesture is reacting on touch event as compared to the QMouseFlickGesture. + + \sa {Gestures Programming}, QScroller, QScrollerProperties, QMouseFlickGesture +*/ + +/*! + \internal +*/ +QFlickGesture::QFlickGesture(QObject *receiver, Qt::MouseButton button, QObject *parent) + : QGesture(*new QFlickGesturePrivate, parent) +{ + d_func()->q_ptr = this; + d_func()->receiver = receiver; + d_func()->receiverScroller = (receiver && QScroller::hasScroller(receiver)) ? QScroller::scroller(receiver) : 0; + d_func()->button = button; +} + +QFlickGesture::~QFlickGesture() +{ } + +QFlickGesturePrivate::QFlickGesturePrivate() + : receiverScroller(0), button(Qt::NoButton), macIgnoreWheel(false) +{ } + + +// +// QFlickGestureRecognizer +// + + +QFlickGestureRecognizer::QFlickGestureRecognizer(Qt::MouseButton button) +{ + this->button = button; +} + +/*! \reimp + */ +QGesture *QFlickGestureRecognizer::create(QObject *target) +{ + QGraphicsObject *go = qobject_cast<QGraphicsObject*>(target); + if (go && button == Qt::NoButton) { + go->setAcceptTouchEvents(true); + } + return new QFlickGesture(target, button); +} + +/*! \internal + The recognize function detects a touch event suitable to start the attached QScroller. + The QFlickGesture will be triggered as soon as the scroller is no longer in the state + QScroller::Inactive or QScroller::Pressed. It will be finished or canceled + at the next QEvent::TouchEnd. + Note that the QScroller might continue scrolling (kinetically) at this point. + */ +QGestureRecognizer::Result QFlickGestureRecognizer::recognize(QGesture *state, + QObject *watched, + QEvent *event) +{ + Q_UNUSED(watched); + + static QElapsedTimer monotonicTimer; + if (!monotonicTimer.isValid()) + monotonicTimer.start(); + + QFlickGesture *q = static_cast<QFlickGesture *>(state); + QFlickGesturePrivate *d = q->d_func(); + + QScroller *scroller = d->receiverScroller; + if (!scroller) + return Ignore; // nothing to do without a scroller? + + QWidget *receiverWidget = qobject_cast<QWidget *>(d->receiver); + QGraphicsObject *receiverGraphicsObject = qobject_cast<QGraphicsObject *>(d->receiver); + + // this is only set for events that we inject into the event loop via sendEvent() + if (PressDelayHandler::instance()->shouldEventBeIgnored(event)) { + //qFGDebug() << state << "QFG: ignored event: " << event->type(); + return Ignore; + } + + const QMouseEvent *me = 0; + const QGraphicsSceneMouseEvent *gsme = 0; + const QTouchEvent *te = 0; + QPoint globalPos; + + // qFGDebug() << "FlickGesture "<<state<<"watched:"<<watched<<"receiver"<<d->receiver<<"event"<<event->type()<<"button"<<button; + + switch (event->type()) { + case QEvent::MouseButtonPress: + case QEvent::MouseButtonRelease: + case QEvent::MouseMove: + if (!receiverWidget) + return Ignore; + if (button != Qt::NoButton) { + me = static_cast<const QMouseEvent *>(event); + globalPos = me->globalPos(); + } + break; + case QEvent::GraphicsSceneMousePress: + case QEvent::GraphicsSceneMouseRelease: + case QEvent::GraphicsSceneMouseMove: + if (!receiverGraphicsObject) + return Ignore; + if (button != Qt::NoButton) { + gsme = static_cast<const QGraphicsSceneMouseEvent *>(event); + globalPos = gsme->screenPos(); + } + break; + case QEvent::TouchBegin: + case QEvent::TouchEnd: + case QEvent::TouchUpdate: + if (button == Qt::NoButton) { + te = static_cast<const QTouchEvent *>(event); + if (!te->touchPoints().isEmpty()) + globalPos = te->touchPoints().at(0).screenPos().toPoint(); + } + break; + +#if defined(Q_WS_MAC) + // the only way to distinguish between real mouse wheels and wheel + // events generated by the native 2 finger swipe gesture is to listen + // for these events (according to Apple's Cocoa Event-Handling Guide) + + case QEvent::NativeGesture: { + QNativeGestureEvent *nge = static_cast<QNativeGestureEvent *>(event); + if (nge->gestureType == QNativeGestureEvent::GestureBegin) + d->macIgnoreWheel = true; + else if (nge->gestureType == QNativeGestureEvent::GestureEnd) + d->macIgnoreWheel = false; + break; + } +#endif + + // consume all wheel events if the scroller is active + case QEvent::Wheel: + if (d->macIgnoreWheel || (scroller->state() != QScroller::Inactive)) + return Ignore | ConsumeEventHint; + break; + + // consume all dbl click events if the scroller is active + case QEvent::MouseButtonDblClick: + if (scroller->state() != QScroller::Inactive) + return Ignore | ConsumeEventHint; + break; + + default: + break; + } + + if (!me && !gsme && !te) // Neither mouse nor touch + return Ignore; + + // get the current pointer position in local coordinates. + QPointF point; + QScroller::Input inputType = (QScroller::Input) 0; + + switch (event->type()) { + case QEvent::MouseButtonPress: + if (me && me->button() == button && me->buttons() == button) { + point = me->globalPos(); + inputType = QScroller::InputPress; + } else if (me) { + scroller->stop(); + return CancelGesture; + } + break; + case QEvent::MouseButtonRelease: + if (me && me->button() == button) { + point = me->globalPos(); + inputType = QScroller::InputRelease; + } + break; + case QEvent::MouseMove: +#ifdef Q_OS_SYMBIAN + // Qt on Symbian tracks the button state internally, while Qt on Win/Mac/Unix + // relies on the windowing system to report the current buttons state. + if (me && (me->buttons() == button || !me->buttons())) { +#else + if (me && me->buttons() == button) { +#endif + point = me->globalPos(); + inputType = QScroller::InputMove; + } + break; + + case QEvent::GraphicsSceneMousePress: + if (gsme && gsme->button() == button && gsme->buttons() == button) { + point = gsme->scenePos(); + inputType = QScroller::InputPress; + } else if (gsme) { + scroller->stop(); + return CancelGesture; + } + break; + case QEvent::GraphicsSceneMouseRelease: + if (gsme && gsme->button() == button) { + point = gsme->scenePos(); + inputType = QScroller::InputRelease; + } + break; + case QEvent::GraphicsSceneMouseMove: +#ifdef Q_OS_SYMBIAN + // Qt on Symbian tracks the button state internally, while Qt on Win/Mac/Unix + // relies on the windowing system to report the current buttons state. + if (gsme && (gsme->buttons() == button || !me->buttons())) { +#else + if (gsme && gsme->buttons() == button) { +#endif + point = gsme->scenePos(); + inputType = QScroller::InputMove; + } + break; + + case QEvent::TouchBegin: + inputType = QScroller::InputPress; + // fall through + case QEvent::TouchEnd: + if (!inputType) + inputType = QScroller::InputRelease; + // fallthrough + case QEvent::TouchUpdate: + if (!inputType) + inputType = QScroller::InputMove; + + if (te->deviceType() == QTouchEvent::TouchPad) { + if (te->touchPoints().count() != 2) // 2 fingers on pad + return Ignore; + + point = te->touchPoints().at(0).startScenePos() + + ((te->touchPoints().at(0).scenePos() - te->touchPoints().at(0).startScenePos()) + + (te->touchPoints().at(1).scenePos() - te->touchPoints().at(1).startScenePos())) / 2; + } else { // TouchScreen + if (te->touchPoints().count() != 1) // 1 finger on screen + return Ignore; + + point = te->touchPoints().at(0).scenePos(); + } + break; + + default: + break; + } + + // Check for an active scroller at globalPos + if (inputType == QScroller::InputPress) { + foreach (QScroller *as, QScroller::activeScrollers()) { + if (as != scroller) { + QRegion scrollerRegion; + + if (QWidget *w = qobject_cast<QWidget *>(as->target())) { + scrollerRegion = QRect(w->mapToGlobal(QPoint(0, 0)), w->size()); + } else if (QGraphicsObject *go = qobject_cast<QGraphicsObject *>(as->target())) { + if (go->scene() && !go->scene()->views().isEmpty()) { + foreach (QGraphicsView *gv, go->scene()->views()) + scrollerRegion |= gv->mapFromScene(go->mapToScene(go->boundingRect())) + .translated(gv->mapToGlobal(QPoint(0, 0))); + } + } + // active scrollers always have priority + if (scrollerRegion.contains(globalPos)) + return Ignore; + } + } + } + + bool scrollerWasDragging = (scroller->state() == QScroller::Dragging); + bool scrollerWasScrolling = (scroller->state() == QScroller::Scrolling); + + if (inputType) { + if (QWidget *w = qobject_cast<QWidget *>(d->receiver)) + point = w->mapFromGlobal(point.toPoint()); + else if (QGraphicsObject *go = qobject_cast<QGraphicsObject *>(d->receiver)) + point = go->mapFromScene(point); + + // inform the scroller about the new event + scroller->handleInput(inputType, point, monotonicTimer.elapsed()); + } + + // depending on the scroller state return the gesture state + Result result(0); + bool scrollerIsActive = (scroller->state() == QScroller::Dragging || + scroller->state() == QScroller::Scrolling); + + // Consume all mouse events while dragging or scrolling to avoid nasty + // side effects with Qt's standard widgets. + if ((me || gsme) && scrollerIsActive) + result |= ConsumeEventHint; + + // The only problem with this approach is that we consume the + // MouseRelease when we start the scrolling with a flick gesture, so we + // have to fake a MouseRelease "somewhere" to not mess with the internal + // states of Qt's widgets (a QPushButton would stay in 'pressed' state + // forever, if it doesn't receive a MouseRelease). + if (me || gsme) { + if (!scrollerWasDragging && !scrollerWasScrolling && scrollerIsActive) + PressDelayHandler::instance()->scrollerBecameActive(); + else if (scrollerWasScrolling && (scroller->state() == QScroller::Dragging || scroller->state() == QScroller::Inactive)) + PressDelayHandler::instance()->scrollerWasIntercepted(); + } + + if (!inputType) { + result |= Ignore; + } else { + switch (event->type()) { + case QEvent::MouseButtonPress: + case QEvent::GraphicsSceneMousePress: + if (scroller->state() == QScroller::Pressed) { + int pressDelay = int(1000 * scroller->scrollerProperties().scrollMetric(QScrollerProperties::MousePressEventDelay).toReal()); + if (pressDelay > 0) { + result |= ConsumeEventHint; + + PressDelayHandler::instance()->pressed(event, pressDelay); + event->accept(); + } + } + // fall through + case QEvent::TouchBegin: + q->setHotSpot(globalPos); + result |= scrollerIsActive ? TriggerGesture : MayBeGesture; + break; + + case QEvent::MouseMove: + case QEvent::GraphicsSceneMouseMove: + if (PressDelayHandler::instance()->isDelaying()) + result |= ConsumeEventHint; + // fall through + case QEvent::TouchUpdate: + result |= scrollerIsActive ? TriggerGesture : Ignore; + break; + + case QEvent::GraphicsSceneMouseRelease: + case QEvent::MouseButtonRelease: + if (PressDelayHandler::instance()->released(event, scrollerWasDragging || scrollerWasScrolling, scrollerIsActive)) + result |= ConsumeEventHint; + // fall through + case QEvent::TouchEnd: + result |= scrollerIsActive ? FinishGesture : CancelGesture; + break; + + default: + result |= Ignore; + break; + } + } + return result; +} + + +/*! \reimp + */ +void QFlickGestureRecognizer::reset(QGesture *state) +{ + QGestureRecognizer::reset(state); +} + +QT_END_NAMESPACE + +#endif // QT_NO_GESTURES diff --git a/src/gui/util/qflickgesture_p.h b/src/gui/util/qflickgesture_p.h new file mode 100644 index 0000000..c3c263b --- /dev/null +++ b/src/gui/util/qflickgesture_p.h @@ -0,0 +1,113 @@ +/**************************************************************************** +** +** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QFLICKGESTURE_P_H +#define QFLICKGESTURE_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of other Qt classes. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qevent.h" +#include "qgesturerecognizer.h" +#include "private/qgesture_p.h" +#include "qscroller.h" +#include "qscopedpointer.h" + +#ifndef QT_NO_GESTURES + +QT_BEGIN_NAMESPACE + +class QFlickGesturePrivate; +class QGraphicsItem; + +class Q_GUI_EXPORT QFlickGesture : public QGesture +{ + Q_OBJECT + Q_DECLARE_PRIVATE(QFlickGesture) + +public: + QFlickGesture(QObject *receiver, Qt::MouseButton button, QObject *parent = 0); + ~QFlickGesture(); + + friend class QFlickGestureRecognizer; +}; + +class PressDelayHandler; + +class QFlickGesturePrivate : public QGesturePrivate +{ + Q_DECLARE_PUBLIC(QFlickGesture) +public: + QFlickGesturePrivate(); + + QPointer<QObject> receiver; + QScroller *receiverScroller; + Qt::MouseButton button; // NoButton == Touch + bool macIgnoreWheel; + static PressDelayHandler *pressDelayHandler; +}; + +class QFlickGestureRecognizer : public QGestureRecognizer +{ +public: + QFlickGestureRecognizer(Qt::MouseButton button); + + QGesture *create(QObject *target); + QGestureRecognizer::Result recognize(QGesture *state, QObject *watched, QEvent *event); + void reset(QGesture *state); + +private: + Qt::MouseButton button; // NoButton == Touch +}; + +QT_END_NAMESPACE + +#endif // QT_NO_GESTURES + +#endif // QFLICKGESTURE_P_H diff --git a/src/gui/util/qscroller.cpp b/src/gui/util/qscroller.cpp new file mode 100644 index 0000000..d6b7aaf --- /dev/null +++ b/src/gui/util/qscroller.cpp @@ -0,0 +1,1984 @@ +/**************************************************************************** +** +** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qevent.h" +#include "qwidget.h" +#include "qscroller.h" +#include "private/qflickgesture_p.h" +#include "private/qscroller_p.h" +#include "qscrollerproperties.h" +#include "private/qscrollerproperties_p.h" +#include "qnumeric.h" +#include "math.h" + +#include <QTime> +#include <QElapsedTimer> +#include <QMap> +#include <QApplication> +#include <QAbstractScrollArea> +#include <QGraphicsObject> +#include <QGraphicsScene> +#include <QGraphicsView> +#include <QDesktopWidget> +#include <QtCore/qmath.h> +#include <QtGui/qevent.h> +#include <qnumeric.h> + +#include <QtDebug> + + +QT_BEGIN_NAMESPACE + +bool qt_sendSpontaneousEvent(QObject *receiver, QEvent *event); + +//#define QSCROLLER_DEBUG + +#ifdef QSCROLLER_DEBUG +# define qScrollerDebug qDebug +#else +# define qScrollerDebug while (false) qDebug +#endif + +QDebug &operator<<(QDebug &dbg, const QScrollerPrivate::ScrollSegment &s) +{ + dbg << "\n Time: start:" << s.startTime << " duration:" << s.deltaTime; + dbg << "\n Pos: start:" << s.startPos << " delta:" << s.deltaPos; + dbg << "\n Curve: type:" << s.curve.type() << " max progress:" << s.maxProgress << "\n"; + return dbg; +} + + +// a few helper operators to make the code below a lot more readable: +// otherwise a lot of ifs would have to be multi-line to check both the x +// and y coordinate separately. + +// returns true only if the abs. value of BOTH x and y are <= f +inline bool operator<=(const QPointF &p, qreal f) +{ + return (qAbs(p.x()) <= f) && (qAbs(p.y()) <= f); +} + +// returns true only if the abs. value of BOTH x and y are < f +inline bool operator<(const QPointF &p, qreal f) +{ + return (qAbs(p.x()) < f) && (qAbs(p.y()) < f); +} + +// returns true if the abs. value of EITHER x or y are >= f +inline bool operator>=(const QPointF &p, qreal f) +{ + return (qAbs(p.x()) >= f) || (qAbs(p.y()) >= f); +} + +// returns true if the abs. value of EITHER x or y are > f +inline bool operator>(const QPointF &p, qreal f) +{ + return (qAbs(p.x()) > f) || (qAbs(p.y()) > f); +} + +// returns a new point with both coordinates having the abs. value of the original one +inline QPointF qAbs(const QPointF &p) +{ + return QPointF(qAbs(p.x()), qAbs(p.y())); +} + +// returns a new point with all components of p1 multiplied by the corresponding components of p2 +inline QPointF operator*(const QPointF &p1, const QPointF &p2) +{ + return QPointF(p1.x() * p2.x(), p1.y() * p2.y()); +} + +// returns a new point with all components of p1 divided by the corresponding components of p2 +inline QPointF operator/(const QPointF &p1, const QPointF &p2) +{ + return QPointF(p1.x() / p2.x(), p1.y() / p2.y()); +} + +inline QPointF clampToRect(const QPointF &p, const QRectF &rect) +{ + qreal x = qBound(rect.left(), p.x(), rect.right()); + qreal y = qBound(rect.top(), p.y(), rect.bottom()); + return QPointF(x, y); +} + +// returns -1, 0 or +1 according to r being <0, ==0 or >0 +inline int qSign(qreal r) +{ + return (r < 0) ? -1 : ((r > 0) ? 1 : 0); +} + +// this version is not mathematically exact, but it just works for every +// easing curve type (even custom ones) + +static qreal differentialForProgress(const QEasingCurve &curve, qreal pos) +{ + const qreal dx = 0.01; + qreal left = (pos < qreal(0.5)) ? pos : pos - qreal(dx); + qreal right = (pos >= qreal(0.5)) ? pos : pos + qreal(dx); + qreal d = (curve.valueForProgress(right) - curve.valueForProgress(left)) / qreal(dx); + + //qScrollerDebug() << "differentialForProgress(type: " << curve.type() << ", pos: " << pos << ") = " << d; + + return d; +} + +// this version is not mathematically exact, but it just works for every +// easing curve type (even custom ones) + +static qreal progressForValue(const QEasingCurve &curve, qreal value) +{ + if (curve.type() >= QEasingCurve::InElastic && + curve.type() < QEasingCurve::Custom) { + qWarning("progressForValue(): QEasingCurves of type %d do not have an inverse, since they are not injective.", curve.type()); + return value; + } + if (value < qreal(0) || value > qreal(1)) + return value; + + qreal progress = value, left(0), right(1); + for (int iterations = 6; iterations; --iterations) { + qreal v = curve.valueForProgress(progress); + if (v < value) + left = progress; + else if (v > value) + right = progress; + else + break; + progress = (left + right) / qreal(2); + } + return progress; +} + + +class QScrollTimer : public QAbstractAnimation +{ +public: + QScrollTimer(QScrollerPrivate *_d) + : d(_d), ignoreUpdate(false), skip(0) + { } + + int duration() const + { + return -1; + } + + void start() + { + // QAbstractAnimation::start() will immediately call + // updateCurrentTime(), but our state is not set correctly yet + ignoreUpdate = true; + QAbstractAnimation::start(); + ignoreUpdate = false; + skip = 0; + } + +protected: + void updateCurrentTime(int /*currentTime*/) + { + if (!ignoreUpdate) { + if (++skip >= d->frameRateSkip()) { + skip = 0; + d->timerTick(); + } + } + } + +private: + QScrollerPrivate *d; + bool ignoreUpdate; + int skip; +}; + +/*! + \class QScroller + \brief The QScroller class enables kinetic scrolling for any scrolling widget or graphics item. + \since 4.8 + + With kinetic scrolling, the user can push the widget in a given + direction and it will continue to scroll in this direction until it is + stopped either by the user or by friction. Aspects of inertia, friction + and other physical concepts can be changed in order to fine-tune an + intuitive user experience. + + The QScroller object is the object that stores the current position and + speed of the scrolling and takes care of updates. + QScroller can be triggered by a flick gesture + + \code + QWidget *w = ...; + QScroller::grabGesture(w, QScroller::LeftMouseButtonGesture); + \endcode + + or directly like this: + + \code + QWidget *w = ...; + QScroller *scroller = QScroller::scroller(w); + scroller->scrollTo(QPointF(100, 100)); + \endcode + + The scrolled QObjects will be receive a QScrollPrepareEvent whenever the scroller needs to + update its geometry information and a QScrollEvent whenever the content of the object should + actually be scrolled. + + The scroller uses the global QAbstractAnimation timer to generate its QScrollEvents, but this + can be slowed down with QScrollerProperties::FrameRate on a per-QScroller basis. + + Several examples in the \c scroller examples directory show how QScroller, + QScrollEvent and the scroller gesture can be used. + + Even though this kinetic scroller has a huge number of settings available via + QScrollerProperties, we recommend that you leave them all at their default, platform optimized + values. In case you really want to change them you can experiment with the \c plot example in + the \c scroller examples directory first. + + \sa QScrollEvent, QScrollPrepareEvent, QScrollerProperties +*/ + + +QMap<QObject *, QScroller *> QScrollerPrivate::allScrollers; +QSet<QScroller *> QScrollerPrivate::activeScrollers; + +/*! + Returns \c true if a QScroller object was already created for \a target; \c false otherwise. + + \sa scroller() +*/ +bool QScroller::hasScroller(QObject *target) +{ + return (QScrollerPrivate::allScrollers.value(target)); +} + +/*! + Returns the scroller for the given \a target. + As long as the object exist this function will always return the same QScroller. + If a QScroller does not exist yet for the \a target, it will implicitly be created. + At no point will two QScrollers be active on one object. + + \sa hasScroller(), target() +*/ +QScroller *QScroller::scroller(QObject *target) +{ + if (!target) { + qWarning("QScroller::scroller() was called with a null target."); + return 0; + } + + if (QScrollerPrivate::allScrollers.contains(target)) + return QScrollerPrivate::allScrollers.value(target); + + QScroller *s = new QScroller(target); + QScrollerPrivate::allScrollers.insert(target, s); + return s; +} + +/*! + \overload + This is the const version of scroller(). +*/ +const QScroller *QScroller::scroller(const QObject *target) +{ + return scroller(const_cast<QObject*>(target)); +} + +/*! + Returns an application wide list of currently active, i.e. state() != + QScroller::Inactive, QScroller objects. + This routine is mostly useful when writing your own gesture recognizer. +*/ +QList<QScroller *> QScroller::activeScrollers() +{ + return QScrollerPrivate::activeScrollers.toList(); +} + +/*! + Returns the target object of this scroller. + \sa hasScroller(), scroller() + */ +QObject *QScroller::target() const +{ + Q_D(const QScroller); + return d->target; +} + +/*! + \fn QScroller::scrollerPropertiesChanged(const QScrollerProperties &newProperties); + + QScroller emits this signal whenever its scroller properties have been + changed. \a newProperties are the new scroller properties. + + \sa scrollerProperties +*/ + + +/*! \property QScroller::scrollerProperties + \brief The scroller properties of this scroller. + The properties will be used by the QScroller to determine its scrolling behaviour. +*/ +QScrollerProperties QScroller::scrollerProperties() const +{ + Q_D(const QScroller); + return d->properties; +} + +void QScroller::setScrollerProperties(const QScrollerProperties &sp) +{ + Q_D(QScroller); + if (d->properties != sp) { + d->properties = sp; + emit scrollerPropertiesChanged(sp); + + // we need to force the recalculation here, since the overshootPolicy may have changed and + // exisiting segments may include an overshoot animation. + d->recalcScrollingSegments(true); + } +} + + +/*! + Registers a custom scroll gesture recognizer and grabs it for the \a + target and returns the resulting gesture type. If \a scrollGestureType is + set to TouchGesture the gesture will trigger on touch events - if set to + one of LeftMouseButtonGesture, RightMouseButtonGesture or + MiddleMouseButtonGesture it will trigger on mouse events of the + corresponding button. + + Only one scroll gesture can be active on a single object at the same + time, so if you call this function twice on the same object, it will + ungrab the existing gesture before grabbing the new one. + + Please note: To avoid nasty side-effects, all mouse events will be + consumed while the gesture is triggered. Since the mouse press event is + not consumed, the gesture needs to also send a fake mouse release event + at the global position \c{(INT_MIN, INT_MIN)} to make sure that it + doesn't mess with the internal states of the widget that received the + mouse press in the first place (which could e.g. be a QPushButton + inside a QScrollArea). +*/ +Qt::GestureType QScroller::grabGesture(QObject *target, ScrollerGestureType scrollGestureType) +{ + // ensure that a scroller for target is created + QScroller *s = scroller(target); + if (!s) + return Qt::GestureType(0); + + QScrollerPrivate *sp = s->d_ptr; + if (sp->recognizer) + ungrabGesture(target); // ungrab the old gesture + + Qt::MouseButton button; + switch (scrollGestureType) { + case LeftMouseButtonGesture : button = Qt::LeftButton; break; + case RightMouseButtonGesture : button = Qt::RightButton; break; + case MiddleMouseButtonGesture: button = Qt::MiddleButton; break; + default : + case TouchGesture : button = Qt::NoButton; break; // NoButton == Touch + } + + sp->recognizer = new QFlickGestureRecognizer(button); + sp->recognizerType = QGestureRecognizer::registerRecognizer(sp->recognizer); + + if (target->isWidgetType()) { + QWidget *widget = static_cast<QWidget *>(target); + widget->grabGesture(sp->recognizerType); + if (scrollGestureType == TouchGesture) + widget->setAttribute(Qt::WA_AcceptTouchEvents); + + } else if(QGraphicsObject *go = qobject_cast<QGraphicsObject*>(target)) { + if (scrollGestureType == TouchGesture) + go->setAcceptTouchEvents(true); + go->grabGesture(sp->recognizerType); + } + return sp->recognizerType; +} + +/*! + Returns the gesture type currently grabbed for the \a target or 0 if no + gesture is grabbed. +*/ +Qt::GestureType QScroller::grabbedGesture(QObject *target) +{ + QScroller *s = scroller(target); + if (s && s->d_ptr) + return s->d_ptr->recognizerType; + else + return Qt::GestureType(0); +} + +/*! + Ungrabs the gesture for the \a target. +*/ +void QScroller::ungrabGesture(QObject *target) +{ + QScroller *s = scroller(target); + if (!s) + return; + + QScrollerPrivate *sp = s->d_ptr; + if (!sp->recognizer) + return; // nothing to do + + if (target->isWidgetType()) { + QWidget *widget = static_cast<QWidget *>(target); + widget->ungrabGesture(sp->recognizerType); + + } else if(QGraphicsObject *go = qobject_cast<QGraphicsObject*>(target)) { + go->ungrabGesture(sp->recognizerType); + } + + QGestureRecognizer::unregisterRecognizer(sp->recognizerType); + // do not delete the recognizer. The QGestureManager is doing this. + sp->recognizer = 0; +} + +/*! + \internal +*/ +QScroller::QScroller(QObject *target) + : d_ptr(new QScrollerPrivate(this, target)) +{ + Q_ASSERT(target); // you can't create a scroller without a target in any normal way + Q_D(QScroller); + d->init(); +} + +/*! + \internal +*/ +QScroller::~QScroller() +{ + Q_D(QScroller); + QGestureRecognizer::unregisterRecognizer(d->recognizerType); + // do not delete the recognizer. The QGestureManager is doing this. + d->recognizer = 0; + QScrollerPrivate::allScrollers.remove(d->target); + QScrollerPrivate::activeScrollers.remove(this); + + delete d_ptr; +} + + +/*! + \fn QScroller::stateChanged(QScroller::State newState); + + QScroller emits this signal whenever the state changes. \a newState is the new State. + + \sa state +*/ + +/*! + \property QScroller::state + \brief the state of the scroller + + \sa QScroller::State +*/ +QScroller::State QScroller::state() const +{ + Q_D(const QScroller); + return d->state; +} + +/*! + Stops the scroller and resets the state back to Inactive. +*/ +void QScroller::stop() +{ + Q_D(QScroller); + if (d->state != Inactive) { + QPointF here = clampToRect(d->contentPosition, d->contentPosRange); + qreal snapX = d->nextSnapPos(here.x(), 0, Qt::Horizontal); + qreal snapY = d->nextSnapPos(here.y(), 0, Qt::Vertical); + QPointF snap = here; + if (!qIsNaN(snapX)) + snap.setX(snapX); + if (!qIsNaN(snapY)) + snap.setY(snapY); + d->contentPosition = snap; + d->overshootPosition = QPointF(0, 0); + + d->setState(Inactive); + } +} + +/*! + \brief Returns the pixel per meter metric for the scrolled widget. + + The value is reported for both the x and y axis separately by using a QPointF. + + \note Please note that this value should be physically correct, while the actual DPI settings + that Qt returns for the display may be reported wrongly (on purpose) by the underlying + windowing system (e.g. Mac OS X or Maemo 5). +*/ +QPointF QScroller::pixelPerMeter() const +{ + Q_D(const QScroller); + QPointF ppm = d->pixelPerMeter; + + if (QGraphicsObject *go = qobject_cast<QGraphicsObject *>(d->target)) { + QTransform viewtr; + //TODO: the first view isn't really correct - maybe use an additional field in the prepare event? + if (go->scene() && !go->scene()->views().isEmpty()) + viewtr = go->scene()->views().first()->viewportTransform(); + QTransform tr = go->deviceTransform(viewtr); + if (tr.isScaling()) { + QPointF p0 = tr.map(QPointF(0, 0)); + QPointF px = tr.map(QPointF(1, 0)); + QPointF py = tr.map(QPointF(0, 1)); + ppm.rx() /= QLineF(p0, px).length(); + ppm.ry() /= QLineF(p0, py).length(); + } + } + return ppm; +} + +/*! + \brief Returns the current velocity of the scroller. + + Returns the current scrolling velocity in meter per second when in the state Scrolling. + Returns a null velocity otherwise. + + The velocity is reported for both the x and y axis separately by using a QPointF. + + \sa pixelPerMeter() +*/ +QPointF QScroller::velocity() const +{ + Q_D(const QScroller); + const QScrollerPropertiesPrivate *sp = d->properties.d.data(); + + switch (state()) { + case Dragging: + return d->releaseVelocity; + case Scrolling: { + QPointF vel; + qint64 now = d->monotonicTimer.elapsed(); + + if (!d->xSegments.isEmpty()) { + const QScrollerPrivate::ScrollSegment &s = d->xSegments.head(); + qreal progress = qreal(now - s.startTime) / (qreal(s.deltaTime) / s.maxProgress); + qreal v = qSign(s.deltaPos) * qreal(s.deltaTime) / s.maxProgress / qreal(1000) * sp->decelerationFactor * qreal(0.5) * differentialForProgress(s.curve, progress); + vel.setX(v); + } + + if (!d->ySegments.isEmpty()) { + const QScrollerPrivate::ScrollSegment &s = d->ySegments.head(); + qreal progress = qreal(now - s.startTime) / (qreal(s.deltaTime) / s.maxProgress); + qreal v = qSign(s.deltaPos) * qreal(s.deltaTime) / s.maxProgress / qreal(1000) * sp->decelerationFactor * qreal(0.5) * differentialForProgress(s.curve, progress); + vel.setY(v); + } + //qScrollerDebug() << "Velocity: " << vel; + return vel; + } + default: + return QPointF(0, 0); + } +} + +/*! + \brief Returns the target position for the scroll movement. + + Returns the planned final position for the current scroll movement or the current + position if the scroller is not in the scrolling state. + The result is undefined when the scroller is in the inactive state. + + The target position is in pixel. + + \sa pixelPerMeter(), scrollTo() +*/ +QPointF QScroller::finalPosition() const +{ + Q_D(const QScroller); + return QPointF(d->scrollingSegmentsEndPos(Qt::Horizontal), + d->scrollingSegmentsEndPos(Qt::Vertical)); +} + +/*! + Starts scrolling the widget so that point \a pos is at the top-left position in + the viewport. + + The behaviour when scrolling outside the valid scroll area is undefined. + In this case the scroller might or might not overshoot. + + The scrolling speed will be calculated so that the given position will + be reached after a platform-defined time span (e.g. 1 second for Maemo 5). + + \a pos is given in viewport coordinates. + + \sa ensureVisible() +*/ +void QScroller::scrollTo(const QPointF &pos) +{ + // we could make this adjustable via QScrollerProperties + scrollTo(pos, 300); +} + +/*! \overload + + This version will reach its destination position in \a scrollTime milli seconds. +*/ +void QScroller::scrollTo(const QPointF &pos, int scrollTime) +{ + Q_D(QScroller); + + if (d->state == Pressed || d->state == Dragging ) + return; + + // no need to resend a prepare event if we are already scrolling + if (d->state == Inactive && !d->prepareScrolling(QPointF())) + return; + + QPointF newpos = clampToRect(pos, d->contentPosRange); + qreal snapX = d->nextSnapPos(newpos.x(), 0, Qt::Horizontal); + qreal snapY = d->nextSnapPos(newpos.y(), 0, Qt::Vertical); + if (!qIsNaN(snapX)) + newpos.setX(snapX); + if (!qIsNaN(snapY)) + newpos.setY(snapY); + + qScrollerDebug() << "QScroller::scrollTo(req:" << pos << " [pix] / snap:" << newpos << ", " << scrollTime << " [ms])"; + + if (newpos == d->contentPosition + d->overshootPosition) + return; + + QPointF vel = velocity(); + + if (scrollTime < 0) + scrollTime = 0; + qreal time = qreal(scrollTime) / 1000; + + d->createScrollToSegments(vel.x(), time, newpos.x(), Qt::Horizontal, QScrollerPrivate::ScrollTypeScrollTo); + d->createScrollToSegments(vel.y(), time, newpos.y(), Qt::Vertical, QScrollerPrivate::ScrollTypeScrollTo); + + if (!scrollTime) + d->setContentPositionHelperScrolling(); + d->setState(scrollTime ? Scrolling : Inactive); +} + +/*! + Starts scrolling so that the rectangle \a rect is visible inside the + viewport with additional margins specified in pixels by \a xmargin and \a ymargin around + the rect. + + In cases where it is not possible to fit the rect plus margins inside the viewport the contents + are scrolled so that as much as possible is visible from \a rect. + + The scrolling speed will be calculated so that the given position will + be reached after a platform-defined time span (e.g. 1 second for Maemo 5). + + This function performs the actual scrolling by calling scrollTo(). +*/ +void QScroller::ensureVisible(const QRectF &rect, qreal xmargin, qreal ymargin) +{ + // we could make this adjustable via QScrollerProperties + ensureVisible(rect, xmargin, ymargin, 1000); +} + +/*! \overload + + This version will reach its destination position in \a scrollTime milli seconds. +*/ +void QScroller::ensureVisible(const QRectF &rect, qreal xmargin, qreal ymargin, int scrollTime) +{ + Q_D(QScroller); + + if (d->state == Pressed || d->state == Dragging ) + return; + + if (d->state == Inactive && !d->prepareScrolling(QPointF())) + return; + + // -- calculate the current pos (or the position after the current scroll) + QPointF startPos = d->contentPosition + d->overshootPosition; + startPos = QPointF(d->scrollingSegmentsEndPos(Qt::Horizontal), + d->scrollingSegmentsEndPos(Qt::Vertical)); + + QRectF marginRect(rect.x() - xmargin, rect.y() - ymargin, + rect.width() + 2 * xmargin, rect.height() + 2 * ymargin); + + QSizeF visible = d->viewportSize; + QRectF visibleRect(startPos, visible); + + qScrollerDebug() << "QScroller::ensureVisible(" << rect << " [pix], " << xmargin << " [pix], " << ymargin << " [pix], " << scrollTime << "[ms])"; + qScrollerDebug() << " --> content position:" << d->contentPosition; + + if (visibleRect.contains(marginRect)) + return; + + QPointF newPos = startPos; + + if (visibleRect.width() < rect.width()) { + // at least try to move the rect into view + if (rect.left() > visibleRect.left()) + newPos.setX(rect.left()); + else if (rect.right() < visibleRect.right()) + newPos.setX(rect.right() - visible.width()); + + } else if (visibleRect.width() < marginRect.width()) { + newPos.setX(rect.center().x() - visibleRect.width() / 2); + } else if (marginRect.left() > visibleRect.left()) { + newPos.setX(marginRect.left()); + } else if (marginRect.right() < visibleRect.right()) { + newPos.setX(marginRect.right() - visible.width()); + } + + if (visibleRect.height() < rect.height()) { + // at least try to move the rect into view + if (rect.top() > visibleRect.top()) + newPos.setX(rect.top()); + else if (rect.bottom() < visibleRect.bottom()) + newPos.setX(rect.bottom() - visible.height()); + + } else if (visibleRect.height() < marginRect.height()) { + newPos.setY(rect.center().y() - visibleRect.height() / 2); + } else if (marginRect.top() > visibleRect.top()) { + newPos.setY(marginRect.top()); + } else if (marginRect.bottom() < visibleRect.bottom()) { + newPos.setY(marginRect.bottom() - visible.height()); + } + + // clamp to maximum content position + newPos = clampToRect(newPos, d->contentPosRange); + if (newPos == startPos) + return; + + scrollTo(newPos, scrollTime); +} + +/*! This function resends the QScrollPrepareEvent. + * Calling resendPrepareEvent triggers a QScrollPrepareEvent from the scroller. + * This will allow the receiver to re-set content position and content size while + * scrolling. + * Calling this function while in the Inactive state is useless as the prepare event + * is send again right before scrolling starts. + */ +void QScroller::resendPrepareEvent() +{ + Q_D(QScroller); + d->prepareScrolling(d->pressPosition); +} + +/*! Set the snap positions for the horizontal axis. + * Set the snap positions to a list of \a positions. + * This will overwrite all previously set snap positions and also a previously + * set snapping interval. + * Snapping can be deactivated by setting an empty list of positions. + */ +void QScroller::setSnapPositionsX(const QList<qreal> &positions) +{ + Q_D(QScroller); + d->snapPositionsX = positions; + d->snapIntervalX = 0.0; + + d->recalcScrollingSegments(); +} + +/*! Set the snap positions for the horizontal axis. + * Set the snap positions to regular spaced intervals. + * The first snap position will be at \a first from the beginning of the list. The next at \a first + \a interval and so on. + * This can be used to implement a list header. + * This will overwrite all previously set snap positions and also a previously + * set snapping interval. + * Snapping can be deactivated by setting an interval of 0.0 + */ +void QScroller::setSnapPositionsX(qreal first, qreal interval) +{ + Q_D(QScroller); + d->snapFirstX = first; + d->snapIntervalX = interval; + d->snapPositionsX.clear(); + + d->recalcScrollingSegments(); +} + +/*! Set the snap positions for the vertical axis. + * Set the snap positions to a list of \a positions. + * This will overwrite all previously set snap positions and also a previously + * set snapping interval. + * Snapping can be deactivated by setting an empty list of positions. + */ +void QScroller::setSnapPositionsY(const QList<qreal> &positions) +{ + Q_D(QScroller); + d->snapPositionsY = positions; + d->snapIntervalY = 0.0; + + d->recalcScrollingSegments(); +} + +/*! Set the snap positions for the vertical axis. + * Set the snap positions to regular spaced intervals. + * The first snap position will be at \a first. The next at \a first + \a interval and so on. + * This will overwrite all previously set snap positions and also a previously + * set snapping interval. + * Snapping can be deactivated by setting an interval of 0.0 + */ +void QScroller::setSnapPositionsY(qreal first, qreal interval) +{ + Q_D(QScroller); + d->snapFirstY = first; + d->snapIntervalY = interval; + d->snapPositionsY.clear(); + + d->recalcScrollingSegments(); +} + + + +// -------------- private ------------ + +QScrollerPrivate::QScrollerPrivate(QScroller *q, QObject *_target) + : target(_target) + , recognizer(0) + , recognizerType(Qt::CustomGesture) + , state(QScroller::Inactive) + , firstScroll(true) + , pressTimestamp(0) + , lastTimestamp(0) + , snapFirstX(-1.0) + , snapIntervalX(0.0) + , snapFirstY(-1.0) + , snapIntervalY(0.0) + , scrollTimer(new QScrollTimer(this)) + , q_ptr(q) +{ + connect(target, SIGNAL(destroyed(QObject*)), this, SLOT(targetDestroyed())); +} + +void QScrollerPrivate::init() +{ + setDpiFromWidget(0); + monotonicTimer.start(); +} + +void QScrollerPrivate::sendEvent(QObject *o, QEvent *e) +{ + qt_sendSpontaneousEvent(o, e); +} + +const char *QScrollerPrivate::stateName(QScroller::State state) +{ + switch (state) { + case QScroller::Inactive: return "inactive"; + case QScroller::Pressed: return "pressed"; + case QScroller::Dragging: return "dragging"; + case QScroller::Scrolling: return "scrolling"; + default: return "(invalid)"; + } +} + +const char *QScrollerPrivate::inputName(QScroller::Input input) +{ + switch (input) { + case QScroller::InputPress: return "press"; + case QScroller::InputMove: return "move"; + case QScroller::InputRelease: return "release"; + default: return "(invalid)"; + } +} + +void QScrollerPrivate::targetDestroyed() +{ + scrollTimer->stop(); + delete q_ptr; +} + +void QScrollerPrivate::timerTick() +{ + struct timerevent { + QScroller::State state; + typedef void (QScrollerPrivate::*timerhandler_t)(); + timerhandler_t handler; + }; + + timerevent timerevents[] = { + { QScroller::Dragging, &QScrollerPrivate::timerEventWhileDragging }, + { QScroller::Scrolling, &QScrollerPrivate::timerEventWhileScrolling }, + }; + + for (int i = 0; i < int(sizeof(timerevents) / sizeof(*timerevents)); ++i) { + timerevent *te = timerevents + i; + + if (state == te->state) { + (this->*te->handler)(); + return; + } + } + + scrollTimer->stop(); +} + +/*! + This function is used by gesture recognizers to inform the scroller about a new input event. + The scroller will change its internal state() according to the input event and its attached + scroller properties. Since the scroller doesn't care about the actual kind of input device the + event came from, you need to decompose the event into the \a input type, a \a position and a + milli-second \a timestamp. The \a position needs to be in the target's coordinate system. + The return value is \c true if the event should be consumed by the calling filter or \c false + if the event should be forwarded to the control. + + \note Using grabGesture() should be sufficient for most use cases though. +*/ +bool QScroller::handleInput(Input input, const QPointF &position, qint64 timestamp) +{ + Q_D(QScroller); + + qScrollerDebug() << "QScroller::handleInput(" << input << ", " << d->stateName(d->state) << ", " << position << ", " << timestamp << ")"; + struct statechange { + State state; + Input input; + typedef bool (QScrollerPrivate::*inputhandler_t)(const QPointF &position, qint64 timestamp); + inputhandler_t handler; + }; + + statechange statechanges[] = { + { QScroller::Inactive, InputPress, &QScrollerPrivate::pressWhileInactive }, + { QScroller::Pressed, InputMove, &QScrollerPrivate::moveWhilePressed }, + { QScroller::Pressed, InputRelease, &QScrollerPrivate::releaseWhilePressed }, + { QScroller::Dragging, InputMove, &QScrollerPrivate::moveWhileDragging }, + { QScroller::Dragging, InputRelease, &QScrollerPrivate::releaseWhileDragging }, + { QScroller::Scrolling, InputPress, &QScrollerPrivate::pressWhileScrolling } + }; + + for (int i = 0; i < int(sizeof(statechanges) / sizeof(*statechanges)); ++i) { + statechange *sc = statechanges + i; + + if (d->state == sc->state && input == sc->input) + return (d->*sc->handler)(position - d->overshootPosition, timestamp); + } + return false; +} + +#ifdef Q_WS_MAEMO_5 + +QPointF QScrollerPrivate::realDpi(int screen) +{ + Q_UNUSED(screen); + // The DPI value is hardcoded to 96 on Maemo5: + // https://projects.maemo.org/bugzilla/show_bug.cgi?id=152525 + // This value (260) is only correct for the N900 though, but + // there's no way to get the real DPI at run time. + return QPointF(260, 260); +} + +#elif defined(Q_WS_MAC) + +// implemented in qscroller_mac.mm + +#else + +QPointF QScrollerPrivate::realDpi(int screen) +{ + QWidget *w = QApplication::desktop()->screen(screen); + return QPointF(w->physicalDpiX(), w->physicalDpiY()); +} + +#endif + +/*! \internal + Returns the resolution of the used screen. +*/ +QPointF QScrollerPrivate::dpi() const +{ + return pixelPerMeter / qreal(39.3700787); +} + +/*! \internal + Sets the resolution used for scrolling. + This resolution is only used by the kinetic scroller. If you change this + then the scroller will behave quite different as a lot of the values are + given in physical distances (millimeter). +*/ +void QScrollerPrivate::setDpi(const QPointF &dpi) +{ + pixelPerMeter = dpi * qreal(39.3700787); +} + +/*! \internal + Sets the dpi used for scrolling to the value of the widget. +*/ +void QScrollerPrivate::setDpiFromWidget(QWidget *widget) +{ + QDesktopWidget *dw = QApplication::desktop(); + setDpi(realDpi(widget ? dw->screenNumber(widget) : dw->primaryScreen())); +} + +/*! \internal + Updates the velocity during dragging. + Sets releaseVelocity. +*/ +void QScrollerPrivate::updateVelocity(const QPointF &deltaPixelRaw, qint64 deltaTime) +{ + Q_Q(QScroller); + QPointF ppm = q->pixelPerMeter(); + const QScrollerPropertiesPrivate *sp = properties.d.data(); + QPointF deltaPixel = deltaPixelRaw; + + qScrollerDebug() << "QScroller::updateVelocity(" << deltaPixelRaw << " [delta pix], " << deltaTime << " [delta ms])"; + + // faster than 2.5mm/ms seems bogus (that would be a screen height in ~20 ms) + if (((deltaPixelRaw / qreal(deltaTime)).manhattanLength() / ((ppm.x() + ppm.y()) / 2) * 1000) > qreal(2.5)) + deltaPixel = deltaPixelRaw * qreal(2.5) * ppm / 1000 / (deltaPixelRaw / qreal(deltaTime)).manhattanLength(); + + qreal inversSmoothingFactor = ((qreal(1) - sp->dragVelocitySmoothingFactor) * qreal(deltaTime) / qreal(1000)); + QPointF newv = -deltaPixel / qreal(deltaTime) * qreal(1000) / ppm; + newv = newv * (qreal(1) - inversSmoothingFactor) + releaseVelocity * inversSmoothingFactor; + + if (deltaPixel.x()) + releaseVelocity.setX(qBound(-sp->maximumVelocity, newv.x(), sp->maximumVelocity)); + if (deltaPixel.y()) + releaseVelocity.setY(qBound(-sp->maximumVelocity, newv.y(), sp->maximumVelocity)); + + qScrollerDebug() << " --> new velocity:" << releaseVelocity; +} + +void QScrollerPrivate::pushSegment(ScrollType type, qreal deltaTime, qreal startPos, qreal endPos, QEasingCurve::Type curve, Qt::Orientation orientation, qreal maxProgress) +{ + if (startPos == endPos) + return; + + ScrollSegment s; + if (orientation == Qt::Horizontal && !xSegments.isEmpty()) + s.startTime = xSegments.last().startTime + xSegments.last().deltaTime; + else if (orientation == Qt::Vertical && !ySegments.isEmpty()) + s.startTime = ySegments.last().startTime + ySegments.last().deltaTime; + else + s.startTime = monotonicTimer.elapsed(); + + s.startPos = startPos; + s.deltaPos = endPos - startPos; + s.deltaTime = deltaTime * 1000; + s.maxProgress = maxProgress; + s.curve.setType(curve); + s.type = type; + + if (orientation == Qt::Horizontal) + xSegments.enqueue(s); + else + ySegments.enqueue(s); + + qScrollerDebug() << "+++ Added a new ScrollSegment: " << s; +} + + +/*! \internal + Clears the old segments and recalculates them if the current segments are not longer valid +*/ +void QScrollerPrivate::recalcScrollingSegments(bool forceRecalc) +{ + Q_Q(QScroller); + QPointF ppm = q->pixelPerMeter(); + + releaseVelocity = q->velocity(); + + if (forceRecalc || !scrollingSegmentsValid(Qt::Horizontal)) + createScrollingSegments(releaseVelocity.x(), contentPosition.x() + overshootPosition.x(), ppm.x(), Qt::Horizontal); + + if (forceRecalc || !scrollingSegmentsValid(Qt::Vertical)) + createScrollingSegments(releaseVelocity.y(), contentPosition.y() + overshootPosition.y(), ppm.y(), Qt::Vertical); +} + +/*! \internal + Returns the end position after the current scroll has finished. +*/ +qreal QScrollerPrivate::scrollingSegmentsEndPos(Qt::Orientation orientation) const +{ + const QQueue<ScrollSegment> *segments; + qreal endPos; + + if (orientation == Qt::Horizontal) { + segments = &xSegments; + endPos = contentPosition.x() + overshootPosition.x(); + } else { + segments = &ySegments; + endPos = contentPosition.y() + overshootPosition.y(); + } + + if (!segments->isEmpty()) { + const ScrollSegment &last = segments->last(); + endPos = last.startPos + last.deltaPos; + } + + return endPos; +} + +/*! \internal + Checks if the scroller segment end in a valid position. +*/ +bool QScrollerPrivate::scrollingSegmentsValid(Qt::Orientation orientation) +{ + QQueue<ScrollSegment> *segments; + qreal minPos; + qreal maxPos; + + if (orientation == Qt::Horizontal) { + segments = &xSegments; + minPos = contentPosRange.left(); + maxPos = contentPosRange.right(); + } else { + segments = &ySegments; + minPos = contentPosRange.top(); + maxPos = contentPosRange.bottom(); + } + + if (segments->isEmpty()) + return true; + + const ScrollSegment &last = segments->last(); + qreal endPos = last.startPos + last.deltaPos; + + if (last.type == ScrollTypeScrollTo) + return true; // scrollTo is always valid + + if (last.type == ScrollTypeOvershoot && + endPos != minPos && endPos != maxPos) + return false; + + if (endPos < minPos || endPos > maxPos) + return false; + + if (endPos == minPos || endPos == maxPos) // the begin and the end of the list are always ok + return true; + + qreal nextSnap = nextSnapPos(endPos, 0, orientation); + if (!qIsNaN(nextSnap) && endPos != nextSnap) + return false; + + return true; +} + +/*! \internal + Creates the sections needed to scroll to the specific \a endPos to the segments queue. +*/ +void QScrollerPrivate::createScrollToSegments(qreal v, qreal deltaTime, qreal endPos, Qt::Orientation orientation, ScrollType type) +{ + Q_UNUSED(v); + + if (orientation == Qt::Horizontal) { + xSegments.clear(); + } else { + ySegments.clear(); + } + + qScrollerDebug() << "+++ createScrollToSegments: t:" << deltaTime << "ep:" << endPos << "o:" << int(orientation); + + const QScrollerPropertiesPrivate *sp = properties.d.data(); + + qreal startPos = (orientation == Qt::Horizontal) ? contentPosition.x() + overshootPosition.x() + : contentPosition.y() + overshootPosition.y(); + qreal deltaPos = endPos - startPos; + + pushSegment(type, deltaTime * 0.3, startPos, startPos + deltaPos * 0.5, QEasingCurve::InQuad, orientation); + pushSegment(type, deltaTime * 0.7, startPos + deltaPos * 0.5, endPos, sp->scrollingCurve.type(), orientation); +} + +/*! \internal +*/ +void QScrollerPrivate::createScrollingSegments(qreal v, qreal startPos, qreal ppm, Qt::Orientation orientation) +{ + const QScrollerPropertiesPrivate *sp = properties.d.data(); + + QScrollerProperties::OvershootPolicy policy; + qreal minPos; + qreal maxPos; + qreal viewSize; + + if (orientation == Qt::Horizontal) { + xSegments.clear(); + policy = sp->hOvershootPolicy; + minPos = contentPosRange.left(); + maxPos = contentPosRange.right(); + viewSize = viewportSize.width(); + } else { + ySegments.clear(); + policy = sp->vOvershootPolicy; + minPos = contentPosRange.top(); + maxPos = contentPosRange.bottom(); + viewSize = viewportSize.height(); + } + + bool alwaysOvershoot = (policy == QScrollerProperties::OvershootAlwaysOn); + bool noOvershoot = (policy == QScrollerProperties::OvershootAlwaysOff) || !sp->overshootScrollDistanceFactor; + bool canOvershoot = !noOvershoot && (alwaysOvershoot || maxPos); + + qScrollerDebug() << "+++ createScrollingSegments: s:" << startPos << "maxPos:" << maxPos << "o:" << int(orientation); + + // -- check if we are in overshoot + if (startPos < minPos) { + createScrollToSegments(v, sp->overshootScrollTime * 0.5, minPos, orientation, ScrollTypeOvershoot); + return; + } + + if (startPos > maxPos) { + createScrollToSegments(v, sp->overshootScrollTime * 0.5, maxPos, orientation, ScrollTypeOvershoot); + return; + } + + qScrollerDebug() << "v = " << v << ", decelerationFactor = " << sp->decelerationFactor << ", curveType = " << sp->scrollingCurve.type(); + + // This is only correct for QEasingCurve::OutQuad (linear velocity, + // constant deceleration), but the results look and feel ok for OutExpo + // and OutSine as well + + // v(t) = deltaTime * a * 0.5 * differentialForProgress(t / deltaTime) + // v(0) = vrelease + // v(deltaTime) = 0 + // deltaTime = (2 * vrelease) / (a * differntial(0)) + + // pos(t) = integrate(v(t)dt) + // pos(t) = vrelease * t - 0.5 * a * t * t + // pos(t) = deltaTime * a * 0.5 * progress(t / deltaTime) * deltaTime + // deltaPos = pos(deltaTime) + + qreal deltaTime = (qreal(2) * qAbs(v)) / (sp->decelerationFactor * differentialForProgress(sp->scrollingCurve, 0)); + qreal deltaPos = qSign(v) * deltaTime * deltaTime * qreal(0.5) * sp->decelerationFactor * ppm; + qreal endPos = startPos + deltaPos; + + qScrollerDebug() << " Real Delta:" << deltaPos; + + // -- determine snap points + qreal nextSnap = nextSnapPos(endPos, 0, orientation); + qreal lowerSnapPos = nextSnapPos(startPos, -1, orientation); + qreal higherSnapPos = nextSnapPos(startPos, 1, orientation); + + qScrollerDebug() << " Real Delta:" << lowerSnapPos <<"-"<<nextSnap <<"-"<<higherSnapPos; + + // - check if we can reach another snap point + if (nextSnap > higherSnapPos || qIsNaN(higherSnapPos)) + higherSnapPos = nextSnap; + if (nextSnap < lowerSnapPos || qIsNaN(lowerSnapPos)) + lowerSnapPos = nextSnap; + + if (qAbs(v) < sp->minimumVelocity) { + + qScrollerDebug() << "### below minimum Vel" << orientation; + + // - no snap points or already at one + if (qIsNaN(nextSnap) || nextSnap == startPos) + return; // nothing to do, no scrolling needed. + + // - decide which point to use + + qreal snapDistance = higherSnapPos - lowerSnapPos; + + qreal pressDistance = (orientation == Qt::Horizontal) ? + lastPosition.x() - pressPosition.x() : + lastPosition.y() - pressPosition.y(); + + // if not dragged far enough, pick the next snap point. + if (sp->snapPositionRatio == 0.0 || qAbs(pressDistance / sp->snapPositionRatio) > snapDistance) + endPos = nextSnap; + else if (pressDistance < 0.0) + endPos = lowerSnapPos; + else + endPos = higherSnapPos; + + deltaPos = endPos - startPos; + pushSegment(ScrollTypeFlick, sp->snapTime * 0.3, startPos, startPos + deltaPos * 0.3, QEasingCurve::InQuad, orientation); + pushSegment(ScrollTypeFlick, sp->snapTime * 0.7, startPos + deltaPos * 0.3, endPos, sp->scrollingCurve.type(), orientation); + return; + } + + // - go to the next snappoint if there is one + if (v > 0 && !qIsNaN(higherSnapPos)) { + // change the time in relation to the changed end position + if (endPos - startPos) + deltaTime *= qAbs((higherSnapPos - startPos) / (endPos - startPos)); + if (deltaTime > sp->snapTime) + deltaTime = sp->snapTime; + endPos = higherSnapPos; + + } else if (v < 0 && !qIsNaN(lowerSnapPos)) { + // change the time in relation to the changed end position + if (endPos - startPos) + deltaTime *= qAbs((lowerSnapPos - startPos) / (endPos - startPos)); + if (deltaTime > sp->snapTime) + deltaTime = sp->snapTime; + endPos = lowerSnapPos; + + // -- check if we are overshooting + } else if (endPos < minPos || endPos > maxPos) { + qreal stopPos = endPos < minPos ? minPos : maxPos; + + qScrollerDebug() << "Overshoot: delta:" << (stopPos - startPos); + + qreal maxProgress = progressForValue(sp->scrollingCurve, qAbs((stopPos - startPos) / deltaPos)); + qScrollerDebug() << "Overshoot maxp:" << maxProgress; + + pushSegment(ScrollTypeFlick, deltaTime * maxProgress, startPos, stopPos, sp->scrollingCurve.type(), orientation, maxProgress); + + if (canOvershoot) { + qreal endV = qSign(v) * deltaTime * sp->decelerationFactor * qreal(0.5) * differentialForProgress(sp->scrollingCurve, maxProgress); + qScrollerDebug() << "Overshoot: velocity" << endV; + qScrollerDebug() << "Overshoot: maxVelocity" << sp->maximumVelocity; + qScrollerDebug() << "Overshoot: viewsize" << viewSize; + qScrollerDebug() << "Overshoot: factor" << sp->overshootScrollDistanceFactor; + + qreal oDistance = viewSize * sp->overshootScrollDistanceFactor * endV / sp->maximumVelocity; + qreal oDeltaTime = sp->overshootScrollTime; + + pushSegment(ScrollTypeOvershoot, oDeltaTime * 0.5, stopPos, stopPos + oDistance, sp->scrollingCurve.type(), orientation); + pushSegment(ScrollTypeOvershoot, oDeltaTime * 0.3, stopPos + oDistance, stopPos + oDistance * 0.3, QEasingCurve::InQuad, orientation); + pushSegment(ScrollTypeOvershoot, oDeltaTime * 0.2, stopPos + oDistance * 0.3, stopPos, QEasingCurve::OutQuad, orientation); + } + return; + } + + pushSegment(ScrollTypeFlick, deltaTime, startPos, endPos, sp->scrollingCurve.type(), orientation); +} + + +/*! Prepares scrolling by sending a QScrollPrepareEvent to the receiver widget. + Returns true if the scrolling was accepted and a target was returned. +*/ +bool QScrollerPrivate::prepareScrolling(const QPointF &position) +{ + QScrollPrepareEvent spe(position); + spe.ignore(); + sendEvent(target, &spe); + + qScrollerDebug() << "QScrollPrepareEvent returned from" << target << "with" << spe.isAccepted() << "mcp:" << spe.contentPosRange() << "cp:" << spe.contentPos(); + if (spe.isAccepted()) { + QPointF oldContentPos = contentPosition + overshootPosition; + QPointF contentDelta = spe.contentPos() - oldContentPos; + + viewportSize = spe.viewportSize(); + contentPosRange = spe.contentPosRange(); + if (contentPosRange.width() < 0) + contentPosRange.setWidth(0); + if (contentPosRange.height() < 0) + contentPosRange.setHeight(0); + contentPosition = clampToRect(spe.contentPos(), contentPosRange); + overshootPosition = spe.contentPos() - contentPosition; + + // - check if the content position was moved + if (contentDelta != QPointF(0, 0)) { + // need to correct all segments + for (int i = 0; i < xSegments.count(); i++) + xSegments[i].startPos -= contentDelta.x(); + + for (int i = 0; i < ySegments.count(); i++) + ySegments[i].startPos -= contentDelta.y(); + } + + if (QWidget *w = qobject_cast<QWidget *>(target)) + setDpiFromWidget(w); + if (QGraphicsObject *go = qobject_cast<QGraphicsObject *>(target)) { + //TODO: the first view isn't really correct - maybe use an additional field in the prepare event? + if (go->scene() && !go->scene()->views().isEmpty()) + setDpiFromWidget(go->scene()->views().first()); + } + + if (state == QScroller::Scrolling) { + recalcScrollingSegments(); + } + return true; + } + + return false; +} + +void QScrollerPrivate::handleDrag(const QPointF &position, qint64 timestamp) +{ + const QScrollerPropertiesPrivate *sp = properties.d.data(); + + QPointF deltaPixel = position - lastPosition; + qint64 deltaTime = timestamp - lastTimestamp; + + if (sp->axisLockThreshold) { + int dx = qAbs(deltaPixel.x()); + int dy = qAbs(deltaPixel.y()); + if (dx || dy) { + bool vertical = (dy > dx); + qreal alpha = qreal(vertical ? dx : dy) / qreal(vertical ? dy : dx); + //qScrollerDebug() << "QScroller::handleDrag() -- axis lock:" << alpha << " / " << axisLockThreshold << "- isvertical:" << vertical << "- dx:" << dx << "- dy:" << dy; + if (alpha <= sp->axisLockThreshold) { + if (vertical) + deltaPixel.setX(0); + else + deltaPixel.setY(0); + } + } + } + + // calculate velocity (if the user would release the mouse NOW) + updateVelocity(deltaPixel, deltaTime); + + // restrict velocity, if content is not scrollable + QRectF max = contentPosRange; + bool canScrollX = (max.width() > 0) || (sp->hOvershootPolicy == QScrollerProperties::OvershootAlwaysOn); + bool canScrollY = (max.height() > 0) || (sp->vOvershootPolicy == QScrollerProperties::OvershootAlwaysOn); + + if (!canScrollX) { + deltaPixel.setX(0); + releaseVelocity.setX(0); + } + if (!canScrollY) { + deltaPixel.setY(0); + releaseVelocity.setY(0); + } + +// if (firstDrag) { +// // Do not delay the first drag +// setContentPositionHelper(q->contentPosition() - overshootDistance - deltaPixel); +// dragDistance = QPointF(0, 0); +// } else { + dragDistance += deltaPixel; +// } +//qScrollerDebug() << "######################" << deltaPixel << position.y() << lastPosition.y(); + if (canScrollX) + lastPosition.setX(position.x()); + if (canScrollY) + lastPosition.setY(position.y()); + lastTimestamp = timestamp; +} + +bool QScrollerPrivate::pressWhileInactive(const QPointF &position, qint64 timestamp) +{ + if (prepareScrolling(position)) { + const QScrollerPropertiesPrivate *sp = properties.d.data(); + + if (!contentPosRange.isNull() || + (sp->hOvershootPolicy == QScrollerProperties::OvershootAlwaysOn) || + (sp->vOvershootPolicy == QScrollerProperties::OvershootAlwaysOn)) { + + lastPosition = pressPosition = position; + lastTimestamp = pressTimestamp = timestamp; + setState(QScroller::Pressed); + } + } + return false; +} + +bool QScrollerPrivate::releaseWhilePressed(const QPointF &, qint64) +{ + if (overshootPosition != QPointF(0.0, 0.0)) { + setState(QScroller::Scrolling); + return true; + } else { + setState(QScroller::Inactive); + return false; + } +} + +bool QScrollerPrivate::moveWhilePressed(const QPointF &position, qint64 timestamp) +{ + Q_Q(QScroller); + const QScrollerPropertiesPrivate *sp = properties.d.data(); + QPointF ppm = q->pixelPerMeter(); + + QPointF deltaPixel = position - pressPosition; + + bool moveAborted = false; + bool moveStarted = (((deltaPixel / ppm).manhattanLength()) > sp->dragStartDistance); + + // check the direction of the mouse drag and abort if it's too much in the wrong direction. + if (moveStarted) { + QRectF max = contentPosRange; + bool canScrollX = (max.width() > 0); + bool canScrollY = (max.height() > 0); + + if (sp->hOvershootPolicy == QScrollerProperties::OvershootAlwaysOn) + canScrollX = true; + if (sp->vOvershootPolicy == QScrollerProperties::OvershootAlwaysOn) + canScrollY = true; + + if (qAbs(deltaPixel.x() / ppm.x()) < qAbs(deltaPixel.y() / ppm.y())) { + if (!canScrollY) + moveAborted = true; + } else { + if (!canScrollX) + moveAborted = true; + } + } + + if (moveAborted) { + setState(QScroller::Inactive); + moveStarted = false; + + } else if (moveStarted) { + setState(QScroller::Dragging); + + // subtract the dragStartDistance + deltaPixel = deltaPixel - deltaPixel * (sp->dragStartDistance / deltaPixel.manhattanLength()); + + if (deltaPixel != QPointF(0, 0)) { + // handleDrag updates lastPosition, lastTimestamp and velocity + handleDrag(pressPosition + deltaPixel, timestamp); + } + } + return moveStarted; +} + +bool QScrollerPrivate::moveWhileDragging(const QPointF &position, qint64 timestamp) +{ + // handleDrag updates lastPosition, lastTimestamp and velocity + handleDrag(position, timestamp); + return true; +} + +void QScrollerPrivate::timerEventWhileDragging() +{ + if (dragDistance != QPointF(0, 0)) { + qScrollerDebug() << "QScroller::timerEventWhileDragging() -- dragDistance:" << dragDistance; + + setContentPositionHelperDragging(-dragDistance); + dragDistance = QPointF(0, 0); + } +} + +bool QScrollerPrivate::releaseWhileDragging(const QPointF &position, qint64 timestamp) +{ + Q_Q(QScroller); + const QScrollerPropertiesPrivate *sp = properties.d.data(); + + // check if we moved at all - this can happen if you stop a running + // scroller with a press and release shortly afterwards + QPointF deltaPixel = position - pressPosition; + if (((deltaPixel / q->pixelPerMeter()).manhattanLength()) > sp->dragStartDistance) { + + // handle accelerating flicks + if ((oldVelocity != QPointF(0, 0)) && sp->acceleratingFlickMaximumTime && + ((timestamp - pressTimestamp) < qint64(sp->acceleratingFlickMaximumTime * 1000))) { + + // - determine if the direction was changed + int signX = 0, signY = 0; + if (releaseVelocity.x()) + signX = (releaseVelocity.x() > 0) == (oldVelocity.x() > 0) ? 1 : -1; + if (releaseVelocity.y()) + signY = (releaseVelocity.y() > 0) == (oldVelocity.y() > 0) ? 1 : -1; + + if (signX > 0) + releaseVelocity.setX(qBound(-sp->maximumVelocity, + oldVelocity.x() * sp->acceleratingFlickSpeedupFactor, + sp->maximumVelocity)); + if (signY > 0) + releaseVelocity.setY(qBound(-sp->maximumVelocity, + oldVelocity.y() * sp->acceleratingFlickSpeedupFactor, + sp->maximumVelocity)); + } + } + + QPointF ppm = q->pixelPerMeter(); + createScrollingSegments(releaseVelocity.x(), contentPosition.x() + overshootPosition.x(), ppm.x(), Qt::Horizontal); + createScrollingSegments(releaseVelocity.y(), contentPosition.y() + overshootPosition.y(), ppm.y(), Qt::Vertical); + + qScrollerDebug() << "QScroller::releaseWhileDragging() -- velocity:" << releaseVelocity << "-- minimum velocity:" << sp->minimumVelocity << "overshoot" << overshootPosition; + + if (xSegments.isEmpty() && ySegments.isEmpty()) + setState(QScroller::Inactive); + else + setState(QScroller::Scrolling); + + return true; +} + +void QScrollerPrivate::timerEventWhileScrolling() +{ + qScrollerDebug() << "QScroller::timerEventWhileScrolling()"; + + setContentPositionHelperScrolling(); + if (xSegments.isEmpty() && ySegments.isEmpty()) + setState(QScroller::Inactive); +} + +bool QScrollerPrivate::pressWhileScrolling(const QPointF &position, qint64 timestamp) +{ + Q_Q(QScroller); + + if ((q->velocity() <= properties.d->maximumClickThroughVelocity) && + (overshootPosition == QPointF(0.0, 0.0))) { + setState(QScroller::Inactive); + return false; + } else { + lastPosition = pressPosition = position; + lastTimestamp = pressTimestamp = timestamp; + setState(QScroller::Pressed); + setState(QScroller::Dragging); + return true; + } +} + +/*! \internal + This function handles all state changes of the scroller. +*/ +void QScrollerPrivate::setState(QScroller::State newstate) +{ + Q_Q(QScroller); + bool sendLastScroll = false; + + if (state == newstate) + return; + + qScrollerDebug() << q << "QScroller::setState(" << stateName(newstate) << ")"; + + switch (newstate) { + case QScroller::Inactive: + scrollTimer->stop(); + + // send the last scroll event (but only after the current state change was finished) + if (!firstScroll) + sendLastScroll = true; + + releaseVelocity = QPointF(0, 0); + break; + + case QScroller::Pressed: + scrollTimer->stop(); + + oldVelocity = releaseVelocity; + releaseVelocity = QPointF(0, 0); + break; + + case QScroller::Dragging: + dragDistance = QPointF(0, 0); + if (state == QScroller::Pressed) + scrollTimer->start(); + break; + + case QScroller::Scrolling: + scrollTimer->start(); + break; + } + + qSwap(state, newstate); + + if (sendLastScroll) { + QScrollEvent se(contentPosition, overshootPosition, QScrollEvent::ScrollFinished); + sendEvent(target, &se); + firstScroll = true; + } + if (state == QScroller::Dragging || state == QScroller::Scrolling) + activeScrollers.insert(q); + else + activeScrollers.remove(q); + emit q->stateChanged(state); +} + + +/*! \internal + Helps when setting the content position. + It will try to move the content by the requested delta but stop in case + when we are coming back from an overshoot or a scrollTo. + It will also indicate a new overshooting condition by the overshootX and oversthootY flags. + + In this cases it will reset the velocity variables and other flags. + + Also keeps track of the current over-shooting value in overshootPosition. + + \a deltaPos is the amount of pixels the current content position should be moved +*/ +void QScrollerPrivate::setContentPositionHelperDragging(const QPointF &deltaPos) +{ + Q_Q(QScroller); + QPointF ppm = q->pixelPerMeter(); + const QScrollerPropertiesPrivate *sp = properties.d.data(); + QPointF v = q->velocity(); + + if (sp->overshootDragResistanceFactor) + overshootPosition /= sp->overshootDragResistanceFactor; + + QPointF oldPos = contentPosition + overshootPosition; + QPointF newPos = oldPos + deltaPos; + + qScrollerDebug() << "QScroller::setContentPositionHelperDragging(" << deltaPos << " [pix])"; + qScrollerDebug() << " --> overshoot:" << overshootPosition << "- old pos:" << oldPos << "- new pos:" << newPos; + + QPointF oldClampedPos = clampToRect(oldPos, contentPosRange); + QPointF newClampedPos = clampToRect(newPos, contentPosRange); + + // --- handle overshooting and stop if the coordinate is going back inside the normal area + bool alwaysOvershootX = (sp->hOvershootPolicy == QScrollerProperties::OvershootAlwaysOn); + bool alwaysOvershootY = (sp->vOvershootPolicy == QScrollerProperties::OvershootAlwaysOn); + bool noOvershootX = (sp->hOvershootPolicy == QScrollerProperties::OvershootAlwaysOff) || + ((state == QScroller::Dragging) && !sp->overshootDragResistanceFactor) || + !sp->overshootDragDistanceFactor; + bool noOvershootY = (sp->vOvershootPolicy == QScrollerProperties::OvershootAlwaysOff) || + ((state == QScroller::Dragging) && !sp->overshootDragResistanceFactor) || + !sp->overshootDragDistanceFactor; + bool canOvershootX = !noOvershootX && (alwaysOvershootX || contentPosRange.width()); + bool canOvershootY = !noOvershootY && (alwaysOvershootY || contentPosRange.height()); + + qreal oldOvershootX = (canOvershootX) ? oldPos.x() - oldClampedPos.x() : 0; + qreal oldOvershootY = (canOvershootY) ? oldPos.y() - oldClampedPos.y() : 0; + + qreal newOvershootX = (canOvershootX) ? newPos.x() - newClampedPos.x() : 0; + qreal newOvershootY = (canOvershootY) ? newPos.y() - newClampedPos.y() : 0; + + qreal maxOvershootX = viewportSize.width() * sp->overshootDragDistanceFactor; + qreal maxOvershootY = viewportSize.height() * sp->overshootDragDistanceFactor; + + qScrollerDebug() << " --> noOs:" << noOvershootX << "drf:" << sp->overshootDragResistanceFactor << "mdf:" << sp->overshootScrollDistanceFactor << "ossP:"<<sp->hOvershootPolicy; + qScrollerDebug() << " --> canOS:" << canOvershootX << "newOS:" << newOvershootX << "maxOS:" << maxOvershootX; + + if (sp->overshootDragResistanceFactor) { + oldOvershootX *= sp->overshootDragResistanceFactor; + oldOvershootY *= sp->overshootDragResistanceFactor; + newOvershootX *= sp->overshootDragResistanceFactor; + newOvershootY *= sp->overshootDragResistanceFactor; + } + + // -- stop at the maximum overshoot distance + + newOvershootX = qBound(-maxOvershootX, newOvershootX, maxOvershootX); + newOvershootY = qBound(-maxOvershootY, newOvershootY, maxOvershootY); + + overshootPosition.setX(newOvershootX); + overshootPosition.setY(newOvershootY); + contentPosition = newClampedPos; + + QScrollEvent se(contentPosition, overshootPosition, firstScroll ? QScrollEvent::ScrollStarted : QScrollEvent::ScrollUpdated); + sendEvent(target, &se); + firstScroll = false; + + qScrollerDebug() << " --> new position:" << newClampedPos << "- new overshoot:" << overshootPosition << + "- overshoot x/y?:" << overshootPosition; +} + + +qreal QScrollerPrivate::nextSegmentPosition(QQueue<ScrollSegment> &segments, qint64 now, qreal oldPos) +{ + qreal pos = oldPos; + + // check the X segments for new positions + while (!segments.isEmpty()) { + const ScrollSegment s = segments.head(); + + if ((s.startTime + s.deltaTime) <= now) { + segments.dequeue(); + pos = s.startPos + s.deltaPos; + } else if (s.startTime <= now) { + qreal progress = qreal(now - s.startTime) / (qreal(s.deltaTime) / s.maxProgress); + pos = s.startPos + s.deltaPos * s.curve.valueForProgress(progress) / s.curve.valueForProgress(s.maxProgress); + break; + } else { + break; + } + } + return pos; +} + +void QScrollerPrivate::setContentPositionHelperScrolling() +{ + qint64 now = monotonicTimer.elapsed(); + QPointF newPos = contentPosition + overshootPosition; + + newPos.setX(nextSegmentPosition(xSegments, now, newPos.x())); + newPos.setY(nextSegmentPosition(ySegments, now, newPos.y())); + + // -- set the position and handle overshoot + qScrollerDebug() << "QScroller::setContentPositionHelperScrolling()"; + qScrollerDebug() << " --> overshoot:" << overshootPosition << "- new pos:" << newPos; + + QPointF newClampedPos = clampToRect(newPos, contentPosRange); + + overshootPosition = newPos - newClampedPos; + contentPosition = newClampedPos; + + QScrollEvent se(contentPosition, overshootPosition, firstScroll ? QScrollEvent::ScrollStarted : QScrollEvent::ScrollUpdated); + sendEvent(target, &se); + firstScroll = false; + + qScrollerDebug() << " --> new position:" << newClampedPos << "- new overshoot:" << overshootPosition; +} + +/*! \internal + * Returns the next snap point in direction. + * If \a direction >0 it will return the next snap point that is larger than the current position. + * If \a direction <0 it will return the next snap point that is smaller than the current position. + * If \a direction ==0 it will return the nearest snap point (or the current position if we are already + * on a snap point. + * Returns the nearest snap position or NaN if no such point could be found. + */ +qreal QScrollerPrivate::nextSnapPos(qreal p, int dir, Qt::Orientation orientation) +{ + qreal bestSnapPos = Q_QNAN; + qreal bestSnapPosDist = Q_INFINITY; + + qreal minPos; + qreal maxPos; + + if (orientation == Qt::Horizontal) { + minPos = contentPosRange.left(); + maxPos = contentPosRange.right(); + } else { + minPos = contentPosRange.top(); + maxPos = contentPosRange.bottom(); + } + + if (orientation == Qt::Horizontal) { + // the snap points in the list + foreach (qreal snapPos, snapPositionsX) { + qreal snapPosDist = snapPos - p; + if ((dir > 0 && snapPosDist < 0) || + (dir < 0 && snapPosDist > 0)) + continue; // wrong direction + if (snapPos < minPos || snapPos > maxPos ) + continue; // invalid + + if (qIsNaN(bestSnapPos) || + qAbs(snapPosDist) < bestSnapPosDist ) { + bestSnapPos = snapPos; + bestSnapPosDist = qAbs(snapPosDist); + } + } + + // the snap point interval + if (snapIntervalX > 0.0) { + qreal first = minPos + snapFirstX; + qreal snapPos; + if (dir > 0) + snapPos = qCeil((p - first) / snapIntervalX) * snapIntervalX + first; + else if (dir < 0) + snapPos = qFloor((p - first) / snapIntervalX) * snapIntervalX + first; + else if (p <= first) + snapPos = first; + else + { + qreal last = qFloor((maxPos - first) / snapIntervalX) * snapIntervalX + first; + if (p >= last) + snapPos = last; + else + snapPos = qRound((p - first) / snapIntervalX) * snapIntervalX + first; + } + + if (snapPos >= first && snapPos <= maxPos ) { + qreal snapPosDist = snapPos - p; + + if (qIsNaN(bestSnapPos) || + qAbs(snapPosDist) < bestSnapPosDist ) { + bestSnapPos = snapPos; + bestSnapPosDist = qAbs(snapPosDist); + } + } + } + + } else { // (orientation == Qt::Vertical) + // the snap points in the list + foreach (qreal snapPos, snapPositionsY) { + qreal snapPosDist = snapPos - p; + if ((dir > 0 && snapPosDist < 0) || + (dir < 0 && snapPosDist > 0)) + continue; // wrong direction + if (snapPos < minPos || snapPos > maxPos ) + continue; // invalid + + if (qIsNaN(bestSnapPos) || + qAbs(snapPosDist) < bestSnapPosDist) { + bestSnapPos = snapPos; + bestSnapPosDist = qAbs(snapPosDist); + } + } + + // the snap point interval + if (snapIntervalY > 0.0) { + qreal first = minPos + snapFirstY; + qreal snapPos; + if (dir > 0) + snapPos = qCeil((p - first) / snapIntervalY) * snapIntervalY + first; + else if (dir < 0) + snapPos = qFloor((p - first) / snapIntervalY) * snapIntervalY + first; + else if (p <= first) + snapPos = first; + else + { + qreal last = qFloor((maxPos - first) / snapIntervalY) * snapIntervalY + first; + if (p >= last) + snapPos = last; + else + snapPos = qRound((p - first) / snapIntervalY) * snapIntervalY + first; + } + + if (snapPos >= first && snapPos <= maxPos ) { + qreal snapPosDist = snapPos - p; + + if (qIsNaN(bestSnapPos) || + qAbs(snapPosDist) < bestSnapPosDist) { + bestSnapPos = snapPos; + bestSnapPosDist = qAbs(snapPosDist); + } + } + } + } + + return bestSnapPos; +} + +/*! + \enum QScroller::State + + This enum contains the different QScroller states. + + \value Inactive The scroller is not scrolling and nothing is pressed. + \value Pressed A touch event was received or the mouse button pressed but the scroll area is currently not dragged. + \value Dragging The scroll area is currently following the touch point or mouse. + \value Scrolling The scroll area is moving on it's own. +*/ + +/*! + \enum QScroller::ScrollerGestureType + + This enum contains the different gesture types that are supported by the QScroller gesture recognizer. + + \value TouchGesture The gesture recognizer will only trigger on touch + events. Specifically it will react on single touch points when using a + touch screen and dual touch points when using a touchpad. + \value LeftMouseButtonGesture The gesture recognizer will only trigger on left mouse button events. + \value MiddleMouseButtonGesture The gesture recognizer will only trigger on middle mouse button events. + \value RightMouseButtonGesture The gesture recognizer will only trigger on right mouse button events. +*/ + +/*! + \enum QScroller::Input + + This enum contains an input device agnostic view of input events that are relevant for QScroller. + + \value InputPress The user pressed the input device (e.g. QEvent::MouseButtonPress, + QEvent::GraphicsSceneMousePress, QEvent::TouchBegin) + + \value InputMove The user moved the input device (e.g. QEvent::MouseMove, + QEvent::GraphicsSceneMouseMove, QEvent::TouchUpdate) + + \value InputRelease The user released the input device (e.g. QEvent::MouseButtonRelease, + QEvent::GraphicsSceneMouseRelease, QEvent::TouchEnd) + +*/ + +QT_END_NAMESPACE diff --git a/src/gui/util/qscroller.h b/src/gui/util/qscroller.h new file mode 100644 index 0000000..34e1125 --- /dev/null +++ b/src/gui/util/qscroller.h @@ -0,0 +1,149 @@ +/**************************************************************************** +** +** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QSCROLLER_H +#define QSCROLLER_H + +#include <QtCore/QObject> +#include <QtCore/QPointF> +#include <QtGui/QScrollerProperties> + +QT_BEGIN_HEADER + +QT_BEGIN_NAMESPACE + +QT_MODULE(Gui) + +class QWidget; +class QScrollerPrivate; +class QScrollerProperties; +class QFlickGestureRecognizer; +class QMouseFlickGestureRecognizer; + +class Q_GUI_EXPORT QScroller : public QObject +{ + Q_OBJECT + Q_PROPERTY(State state READ state NOTIFY stateChanged) + Q_PROPERTY(QScrollerProperties scrollerProperties READ scrollerProperties WRITE setScrollerProperties NOTIFY scrollerPropertiesChanged) + Q_ENUMS(State) + +public: + enum State + { + Inactive, + Pressed, + Dragging, + Scrolling, + }; + + enum ScrollerGestureType + { + TouchGesture, + LeftMouseButtonGesture, + RightMouseButtonGesture, + MiddleMouseButtonGesture + }; + + enum Input + { + InputPress = 1, + InputMove, + InputRelease + }; + + static bool hasScroller(QObject *target); + + static QScroller *scroller(QObject *target); + static const QScroller *scroller(const QObject *target); + + static Qt::GestureType grabGesture(QObject *target, ScrollerGestureType gestureType = TouchGesture); + static Qt::GestureType grabbedGesture(QObject *target); + static void ungrabGesture(QObject *target); + + static QList<QScroller *> activeScrollers(); + + QObject *target() const; + + State state() const; + + bool handleInput(Input input, const QPointF &position, qint64 timestamp = 0); + + void stop(); + QPointF velocity() const; + QPointF finalPosition() const; + QPointF pixelPerMeter() const; + + QScrollerProperties scrollerProperties() const; + + void setSnapPositionsX( const QList<qreal> &positions ); + void setSnapPositionsX( qreal first, qreal interval ); + void setSnapPositionsY( const QList<qreal> &positions ); + void setSnapPositionsY( qreal first, qreal interval ); + +public Q_SLOTS: + void setScrollerProperties(const QScrollerProperties &prop); + void scrollTo(const QPointF &pos); + void scrollTo(const QPointF &pos, int scrollTime); + void ensureVisible(const QRectF &rect, qreal xmargin, qreal ymargin); + void ensureVisible(const QRectF &rect, qreal xmargin, qreal ymargin, int scrollTime); + void resendPrepareEvent(); + +Q_SIGNALS: + void stateChanged(QScroller::State newstate); + void scrollerPropertiesChanged(const QScrollerProperties &); + +private: + QScrollerPrivate *d_ptr; + + QScroller(QObject *target); + virtual ~QScroller(); + + Q_DISABLE_COPY(QScroller) + Q_DECLARE_PRIVATE(QScroller) + + friend class QFlickGestureRecognizer; +}; + +QT_END_NAMESPACE + +QT_END_HEADER + +#endif // QSCROLLER_H diff --git a/src/gui/util/qscroller_mac.mm b/src/gui/util/qscroller_mac.mm new file mode 100644 index 0000000..3203036 --- /dev/null +++ b/src/gui/util/qscroller_mac.mm @@ -0,0 +1,69 @@ +/**************************************************************************** +** +** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#import <Cocoa/Cocoa.h> + +#include "qscroller_p.h" + +#ifdef Q_WS_MAC + +QPointF QScrollerPrivate::realDpi(int screen) +{ + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + NSArray *nsscreens = [NSScreen screens]; + + if (screen < 0 || screen >= int([nsscreens count])) + screen = 0; + + NSScreen *nsscreen = [nsscreens objectAtIndex:screen]; + CGDirectDisplayID display = [[[nsscreen deviceDescription] objectForKey:@"NSScreenNumber"] intValue]; + + CGSize mmsize = CGDisplayScreenSize(display); + if (mmsize.width > 0 && mmsize.height > 0) { + return QPointF(CGDisplayPixelsWide(display) / mmsize.width, + CGDisplayPixelsHigh(display) / mmsize.height) * qreal(25.4); + } else { + return QPointF(); + } + [pool release]; +} + +#endif diff --git a/src/gui/util/qscroller_p.h b/src/gui/util/qscroller_p.h new file mode 100644 index 0000000..98f34f7 --- /dev/null +++ b/src/gui/util/qscroller_p.h @@ -0,0 +1,205 @@ +/**************************************************************************** +** +** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QSCROLLER_P_H +#define QSCROLLER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QObject> +#include <QPointer> +#include <QQueue> +#include <QSet> +#include <QEasingCurve> +#include <QElapsedTimer> +#include <QSizeF> +#include <QPointF> +#include <QRectF> +#include <qscroller.h> +#include <qscrollerproperties.h> +#include <private/qscrollerproperties_p.h> +#include <QAbstractAnimation> + +QT_BEGIN_NAMESPACE + +class QFlickGestureRecognizer; + +class QScrollTimer; + +class QScrollerPrivate : public QObject +{ + Q_OBJECT + Q_DECLARE_PUBLIC(QScroller) + +public: + QScrollerPrivate(QScroller *q, QObject *target); + void init(); + + void sendEvent(QObject *o, QEvent *e); + + void setState(QScroller::State s); + + enum ScrollType { + ScrollTypeFlick = 0, + ScrollTypeScrollTo, + ScrollTypeOvershoot + }; + + struct ScrollSegment { + qint64 startTime; + qint64 deltaTime; + qreal startPos; + qreal deltaPos; + QEasingCurve curve; + qreal maxProgress; + ScrollType type; + }; + + bool pressWhileInactive(const QPointF &position, qint64 timestamp); + bool moveWhilePressed(const QPointF &position, qint64 timestamp); + bool releaseWhilePressed(const QPointF &position, qint64 timestamp); + bool moveWhileDragging(const QPointF &position, qint64 timestamp); + bool releaseWhileDragging(const QPointF &position, qint64 timestamp); + bool pressWhileScrolling(const QPointF &position, qint64 timestamp); + + void timerTick(); + void timerEventWhileDragging(); + void timerEventWhileScrolling(); + + bool prepareScrolling(const QPointF &position); + void handleDrag(const QPointF &position, qint64 timestamp); + + QPointF realDpi(int screen); + QPointF dpi() const; + void setDpi(const QPointF &dpi); + void setDpiFromWidget(QWidget *widget); + + void updateVelocity(const QPointF &deltaPixelRaw, qint64 deltaTime); + void pushSegment(ScrollType type, qreal deltaTime, qreal startPos, qreal endPos, QEasingCurve::Type curve, Qt::Orientation orientation, qreal maxProgress = 1.0); + void recalcScrollingSegments(bool forceRecalc = false); + qreal scrollingSegmentsEndPos(Qt::Orientation orientation) const; + bool scrollingSegmentsValid(Qt::Orientation orientation); + void createScrollToSegments(qreal v, qreal deltaTime, qreal endPos, Qt::Orientation orientation, ScrollType type); + void createScrollingSegments(qreal v, qreal startPos, qreal ppm, Qt::Orientation orientation); + + void setContentPositionHelperDragging(const QPointF &deltaPos); + void setContentPositionHelperScrolling(); + + qreal nextSnapPos(qreal p, int dir, Qt::Orientation orientation); + static qreal nextSegmentPosition(QQueue<ScrollSegment> &segments, qint64 now, qreal oldPos); + + inline int frameRateSkip() const { return properties.d.data()->frameRate; } + + static const char *stateName(QScroller::State state); + static const char *inputName(QScroller::Input input); + +public slots: + void targetDestroyed(); + +public: + // static + static QMap<QObject *, QScroller *> allScrollers; + static QSet<QScroller *> activeScrollers; + + // non static + QObject *target; + QScrollerProperties properties; + QFlickGestureRecognizer *recognizer; + Qt::GestureType recognizerType; + + // scroller state: + + // QPointer<QObject> scrollTarget; + QSizeF viewportSize; + QRectF contentPosRange; + QPointF contentPosition; + QPointF overshootPosition; // the number of pixels we are overshooting (before overshootDragResistanceFactor) + + // state + + bool enabled; + QScroller::State state; + bool firstScroll; // true if we haven't already send a scroll event + + QPointF oldVelocity; // the release velocity of the last drag + + QPointF pressPosition; + QPointF lastPosition; + qint64 pressTimestamp; + qint64 lastTimestamp; + + QPointF dragDistance; // the distance we should move during the next drag timer event + + QQueue<ScrollSegment> xSegments; + QQueue<ScrollSegment> ySegments; + + // snap positions + QList<qreal> snapPositionsX; + qreal snapFirstX; + qreal snapIntervalX; + QList<qreal> snapPositionsY; + qreal snapFirstY; + qreal snapIntervalY; + + QPointF pixelPerMeter; + + QElapsedTimer monotonicTimer; + + QPointF releaseVelocity; // the starting velocity of the scrolling state + QScrollTimer *scrollTimer; + + QScroller *q_ptr; +}; + + +QT_END_NAMESPACE + +#endif // QSCROLLER_P_H + diff --git a/src/gui/util/qscrollerproperties.cpp b/src/gui/util/qscrollerproperties.cpp new file mode 100644 index 0000000..4fba489 --- /dev/null +++ b/src/gui/util/qscrollerproperties.cpp @@ -0,0 +1,418 @@ +/**************************************************************************** +** +** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include <QPointer> +#include <QObject> +#include <QtCore/qmath.h> +#ifdef Q_WS_WIN +# include <QLibrary> +#endif + +#include "qscrollerproperties.h" +#include "private/qscrollerproperties_p.h" + +QT_BEGIN_NAMESPACE + +static QScrollerPropertiesPrivate *userDefaults = 0; +static QScrollerPropertiesPrivate *systemDefaults = 0; + +QScrollerPropertiesPrivate *QScrollerPropertiesPrivate::defaults() +{ + if (!systemDefaults) { + QScrollerPropertiesPrivate spp; +#ifdef Q_WS_MAEMO_5 + spp.mousePressEventDelay = qreal(0); + spp.dragStartDistance = qreal(2.5 / 1000); + spp.dragVelocitySmoothingFactor = qreal(0.15); + spp.axisLockThreshold = qreal(0); + spp.scrollingCurve.setType(QEasingCurve::OutQuad); + spp.decelerationFactor = 1.0; + spp.minimumVelocity = qreal(0.0195); + spp.maximumVelocity = qreal(6.84); + spp.maximumClickThroughVelocity = qreal(0.0684); + spp.acceleratingFlickMaximumTime = qreal(0.125); + spp.acceleratingFlickSpeedupFactor = qreal(3.0); + spp.snapPositionRatio = qreal(0.25); + spp.snapTime = qreal(1); + spp.overshootDragResistanceFactor = qreal(1); + spp.overshootDragDistanceFactor = qreal(0.3); + spp.overshootScrollDistanceFactor = qreal(0.3); + spp.overshootScrollTime = qreal(0.5); + spp.hOvershootPolicy = QScrollerProperties::OvershootWhenScrollable; + spp.vOvershootPolicy = QScrollerProperties::OvershootWhenScrollable; + spp.frameRate = QScrollerProperties::Fps30; +#else + spp.mousePressEventDelay = qreal(0.25); + spp.dragStartDistance = qreal(5.0 / 1000); + spp.dragVelocitySmoothingFactor = qreal(0.02); + spp.axisLockThreshold = qreal(0); + spp.scrollingCurve.setType(QEasingCurve::OutQuad); + spp.decelerationFactor = qreal(0.125); + spp.minimumVelocity = qreal(50.0 / 1000); + spp.maximumVelocity = qreal(500.0 / 1000); + spp.maximumClickThroughVelocity = qreal(66.5 / 1000); + spp.acceleratingFlickMaximumTime = qreal(1.25); + spp.acceleratingFlickSpeedupFactor = qreal(3.0); + spp.snapPositionRatio = qreal(0.5); + spp.snapTime = qreal(0.3); + spp.overshootDragResistanceFactor = qreal(0.5); + spp.overshootDragDistanceFactor = qreal(1); + spp.overshootScrollDistanceFactor = qreal(0.5); + spp.overshootScrollTime = qreal(0.7); +# ifdef Q_WS_WIN + if (QLibrary::resolve(QLatin1String("UxTheme"), "BeginPanningFeedback")) + spp.overshootScrollTime = qreal(0.35); +# endif + spp.hOvershootPolicy = QScrollerProperties::OvershootWhenScrollable; + spp.vOvershootPolicy = QScrollerProperties::OvershootWhenScrollable; + spp.frameRate = QScrollerProperties::Standard; +#endif + systemDefaults = new QScrollerPropertiesPrivate(spp); + } + return new QScrollerPropertiesPrivate(userDefaults ? *userDefaults : *systemDefaults); +} + +/*! + \class QScrollerProperties + \brief The QScrollerProperties class stores the settings for a QScroller. + \since 4.8 + + The QScrollerProperties class stores the parameters used by QScroller. + + The default settings are platform dependant and Qt will emulate the + platform behaviour for kinetic scrolling. + + As a convention the QScrollerProperties are in physical units (meter, + seconds) and will be converted by QScroller using the current DPI. + + \sa QScroller +*/ + +/*! + Constructs new scroller properties. +*/ +QScrollerProperties::QScrollerProperties() + : d(QScrollerPropertiesPrivate::defaults()) +{ +} + +/*! + Constructs a copy of \a sp. +*/ +QScrollerProperties::QScrollerProperties(const QScrollerProperties &sp) + : d(new QScrollerPropertiesPrivate(*sp.d)) +{ +} + +/*! + Assigns \a sp to these scroller properties and returns a reference to these scroller properties. +*/ +QScrollerProperties &QScrollerProperties::operator=(const QScrollerProperties &sp) +{ + *d.data() = *sp.d.data(); + return *this; +} + +/*! + Destroys the scroller properties. +*/ +QScrollerProperties::~QScrollerProperties() +{ +} + +/*! + Returns true if these scroller properties are equal to \a sp; otherwise returns false. +*/ +bool QScrollerProperties::operator==(const QScrollerProperties &sp) const +{ + return *d.data() == *sp.d.data(); +} + +/*! + Returns true if these scroller properties are different from \a sp; otherwise returns false. +*/ +bool QScrollerProperties::operator!=(const QScrollerProperties &sp) const +{ + return !(*d.data() == *sp.d.data()); +} + +bool QScrollerPropertiesPrivate::operator==(const QScrollerPropertiesPrivate &p) const +{ + bool same = true; + same &= (mousePressEventDelay == p.mousePressEventDelay); + same &= (dragStartDistance == p.dragStartDistance); + same &= (dragVelocitySmoothingFactor == p.dragVelocitySmoothingFactor); + same &= (axisLockThreshold == p.axisLockThreshold); + same &= (scrollingCurve == p.scrollingCurve); + same &= (decelerationFactor == p.decelerationFactor); + same &= (minimumVelocity == p.minimumVelocity); + same &= (maximumVelocity == p.maximumVelocity); + same &= (maximumClickThroughVelocity == p.maximumClickThroughVelocity); + same &= (acceleratingFlickMaximumTime == p.acceleratingFlickMaximumTime); + same &= (acceleratingFlickSpeedupFactor == p.acceleratingFlickSpeedupFactor); + same &= (snapPositionRatio == p.snapPositionRatio); + same &= (snapTime == p.snapTime); + same &= (overshootDragResistanceFactor == p.overshootDragResistanceFactor); + same &= (overshootDragDistanceFactor == p.overshootDragDistanceFactor); + same &= (overshootScrollDistanceFactor == p.overshootScrollDistanceFactor); + same &= (overshootScrollTime == p.overshootScrollTime); + same &= (hOvershootPolicy == p.hOvershootPolicy); + same &= (vOvershootPolicy == p.vOvershootPolicy); + same &= (frameRate == p.frameRate); + return same; +} + +/*! + Sets the scroller properties returned by the default constructor to \a sp. + + Use this function to override the platform default properties returned by the default + constructor. If you only want to change the scroller properties of a single scroller, then use + QScroller::setScrollerProperties() + + \note Calling this function will not change the content of already existing + QScrollerProperties objects. + + \sa unsetDefaultScrollerProperties() +*/ +void QScrollerProperties::setDefaultScrollerProperties(const QScrollerProperties &sp) +{ + if (!userDefaults) + userDefaults = new QScrollerPropertiesPrivate(*sp.d); + else + *userDefaults = *sp.d; +} + +/*! + Sets the scroller properties returned by the default constructor back to the platform default + properties. + + \sa setDefaultScrollerProperties() +*/ +void QScrollerProperties::unsetDefaultScrollerProperties() +{ + delete userDefaults; + userDefaults = 0; +} + +/*! + Query the \a metric value of the scroller properties. + + \sa setScrollMetric(), ScrollMetric +*/ +QVariant QScrollerProperties::scrollMetric(ScrollMetric metric) const +{ + switch (metric) { + case MousePressEventDelay: return d->mousePressEventDelay; + case DragStartDistance: return d->dragStartDistance; + case DragVelocitySmoothingFactor: return d->dragVelocitySmoothingFactor; + case AxisLockThreshold: return d->axisLockThreshold; + case ScrollingCurve: return d->scrollingCurve; + case DecelerationFactor: return d->decelerationFactor; + case MinimumVelocity: return d->minimumVelocity; + case MaximumVelocity: return d->maximumVelocity; + case MaximumClickThroughVelocity: return d->maximumClickThroughVelocity; + case AcceleratingFlickMaximumTime: return d->acceleratingFlickMaximumTime; + case AcceleratingFlickSpeedupFactor:return d->acceleratingFlickSpeedupFactor; + case SnapPositionRatio: return d->snapPositionRatio; + case SnapTime: return d->snapTime; + case OvershootDragResistanceFactor: return d->overshootDragResistanceFactor; + case OvershootDragDistanceFactor: return d->overshootDragDistanceFactor; + case OvershootScrollDistanceFactor: return d->overshootScrollDistanceFactor; + case OvershootScrollTime: return d->overshootScrollTime; + case HorizontalOvershootPolicy: return QVariant::fromValue(d->hOvershootPolicy); + case VerticalOvershootPolicy: return QVariant::fromValue(d->vOvershootPolicy); + case FrameRate: return QVariant::fromValue(d->frameRate); + case ScrollMetricCount: break; + } + return QVariant(); +} + +/*! + Set a specific value of the \a metric ScrollerMetric to \a value. + + \sa scrollMetric(), ScrollMetric +*/ +void QScrollerProperties::setScrollMetric(ScrollMetric metric, const QVariant &value) +{ + switch (metric) { + case MousePressEventDelay: d->mousePressEventDelay = value.toReal(); break; + case DragStartDistance: d->dragStartDistance = value.toReal(); break; + case DragVelocitySmoothingFactor: d->dragVelocitySmoothingFactor = qBound(qreal(0), value.toReal(), qreal(1)); break; + case AxisLockThreshold: d->axisLockThreshold = qBound(qreal(0), value.toReal(), qreal(1)); break; + case ScrollingCurve: d->scrollingCurve = value.toEasingCurve(); break; + case DecelerationFactor: d->decelerationFactor = value.toReal(); break; + case MinimumVelocity: d->minimumVelocity = value.toReal(); break; + case MaximumVelocity: d->maximumVelocity = value.toReal(); break; + case MaximumClickThroughVelocity: d->maximumClickThroughVelocity = value.toReal(); break; + case AcceleratingFlickMaximumTime: d->acceleratingFlickMaximumTime = value.toReal(); break; + case AcceleratingFlickSpeedupFactor:d->acceleratingFlickSpeedupFactor = value.toReal(); break; + case SnapPositionRatio: d->snapPositionRatio = qBound(qreal(0), value.toReal(), qreal(1)); break; + case SnapTime: d->snapTime = value.toReal(); break; + case OvershootDragResistanceFactor: d->overshootDragResistanceFactor = value.toReal(); break; + case OvershootDragDistanceFactor: d->overshootDragDistanceFactor = qBound(qreal(0), value.toReal(), qreal(1)); break; + case OvershootScrollDistanceFactor: d->overshootScrollDistanceFactor = qBound(qreal(0), value.toReal(), qreal(1)); break; + case OvershootScrollTime: d->overshootScrollTime = value.toReal(); break; + case HorizontalOvershootPolicy: d->hOvershootPolicy = value.value<QScrollerProperties::OvershootPolicy>(); break; + case VerticalOvershootPolicy: d->vOvershootPolicy = value.value<QScrollerProperties::OvershootPolicy>(); break; + case FrameRate: d->frameRate = value.value<QScrollerProperties::FrameRates>(); break; + case ScrollMetricCount: break; + } +} + +/*! + \enum QScrollerProperties::FrameRates + + This enum describes the available frame rates used while dragging or scrolling. + + \value Fps60 60 frames per second + \value Fps30 30 frames per second + \value Fps20 20 frames per second + \value Standard the default value is 60 frames per second (which corresponds to QAbstractAnimation). +*/ + +/*! + \enum QScrollerProperties::OvershootPolicy + + This enum describes the various modes of overshooting. + + \value OvershootWhenScrollable Overshooting is when the content is scrollable. This is the + default. + + \value OvershootAlwaysOff Overshooting is never enabled (even when the content is scrollable). + + \value OvershootAlwaysOn Overshooting is always enabled (even when the content is not + scrollable). +*/ + +/*! + \enum QScrollerProperties::ScrollMetric + + This enum contains the different scroll metric types. When not indicated otherwise the + setScrollMetric function expects a QVariant of a real value. + + See the QScroller documentation for a further explanation of the concepts behind the different + values. + + \value MousePressEventDelay This is the time a mouse press event will be delayed when starting + a flick gesture in \c{[s]}. If the gesture is triggered within that time, no mouse press or + release will be sent to the scrolled object. If it triggers after that delay the (delayed) + mouse press plus a faked release event (at global postion \c{QPoint(-QWIDGETSIZE_MAX, + -QWIDGETSIZE_MAX)} will be sent. If the gesture is canceled, then both the (delayed) mouse + press plus the real release event will be delivered. + + \value DragStartDistance This is the minimum distance the touch or mouse point needs to be + moved before the flick gesture is triggered in \c m. + + \value DragVelocitySmoothingFactor A value that describes how much new drag velocities are + included in the final scrolling velocity. This value should be in the range between \c 0 and \c + 1. Low values meaning that the last dragging velocity is not very important. + + \value AxisLockThreshold If greater than zero a scroll movement will be restricted to one axis + only if the movement is inside an angle about the axis. The threshold must be in the range \c 0 + to \c 1. + + \value ScrollingCurve The QEasingCurve used when decelerating the scrolling velocity after an + user initiated flick. Please note that this is the easing curve for the positions, \bold{not} + the velocity: the default is QEasingCurve::OutQuad, which results is a linear decrease in + velocity (1st derivative) and a constant deceleration (2nd derivative). + + \value DecelerationFactor This factor influences how long it takes the scroller to decelerate + to 0 velocity. The actual value heavily depends on the chosen ScrollingCurve, but for most + types the value should be in the range from \c 0.1 to \c 2.0 + + \value MinimumVelocity The minimum velocity that is needed after ending the touch or releasing + the mouse to start scrolling in \c{m/s}. + + \value MaximumVelocity This is the maximum velocity that can be reached in \c{m/s}. + + \value MaximumClickThroughVelocity This is the maximum allowed scroll speed for a click-through + in \c{m/s}. This means that a click on a currently (slowly) scrolling object will not only stop + the scrolling but the click event will also be delivered to the UI control - this is very + useful when using exponential-type scrolling curves. + + \value AcceleratingFlickMaximumTime This is the maximum time in \c seconds that a flick gesture + can take to be recognized as an accelerating flick. If set to zero no such gesture will be + detected. An "accelerating flick" is a flick gesture executed on an already scrolling object. + In such cases the scrolling speed is multiplied by AcceleratingFlickSpeedupFactor in order to + accelerate it. + + \value AcceleratingFlickSpeedupFactor The current speed will be multiplied by this number if an + accelerating flick is detected. Should be \c{> 1}. + + \value SnapPositionRatio This is the distance that the user must drag the area beween two snap + points in order to snap it to the next position. e.g. \c{0.33} means that the scroll must only + reach one third of the distance between two snap points to snap to the next one. The ratio must + be in the range \c 0 to \c 1. + + \value SnapTime This is the time factor for the scrolling curve. A lower value means that the + scrolling will take longer. The scrolling distance is independet of this value. + + \value OvershootDragResistanceFactor This value is the factor between the mouse dragging and + the actual scroll area movement (during overshoot). The factor must be in the range \c 0 to \c + 1. + + \value OvershootDragDistanceFactor This is the maximum distance for overshoot movements while + dragging. The actual overshoot distance will be calculated by multiplying this value with the + viewport size of the scrolled object. The factor must be in the range \c 0 to \c 1. + + \value OvershootScrollDistanceFactor This is the maximum distance for overshoot movements while + scrolling. The actual overshoot distance will be calculated by multiplying this value with the + viewport size of the scrolled object. The factor must be in the range \c 0 to \c 1. + + \value OvershootScrollTime This is the time in \c seconds that will be used to play the + complete overshoot animation. + + \value HorizontalOvershootPolicy This is the horizontal overshooting policy (see OvershootPolicy). + + \value VerticalOvershootPolicy This is the horizontal overshooting policy (see OvershootPolicy). + + \value FrameRate This is the frame rate which should be used while dragging or scrolling. + QScroller uses a QAbstractAnimation timer internally to sync all scrolling operations to other + animations that might be active at the same time. If the Standard value of 60 frames per + second is too fast for your use case, you can lower the frames per second with this setting + (while still being in-sync with QAbstractAnimation). Please note that only the values of the + FrameRates enum are allowed here. + + \value ScrollMetricCount This is just used when enumerating the metrics. It is always the last + entry. +*/ + +QT_END_NAMESPACE diff --git a/src/gui/util/qscrollerproperties.h b/src/gui/util/qscrollerproperties.h new file mode 100644 index 0000000..5284876 --- /dev/null +++ b/src/gui/util/qscrollerproperties.h @@ -0,0 +1,140 @@ +/**************************************************************************** +** +** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QSCROLLERPROPERTIES_H +#define QSCROLLERPROPERTIES_H + +#include <QtCore/QScopedPointer> +#include <QtCore/QMetaType> +#include <QtCore/QVariant> + +QT_BEGIN_HEADER + +QT_BEGIN_NAMESPACE + +QT_MODULE(Gui) + +class QScroller; +class QScrollerPrivate; +class QScrollerPropertiesPrivate; + +class Q_GUI_EXPORT QScrollerProperties +{ +public: + QScrollerProperties(); + QScrollerProperties(const QScrollerProperties &sp); + QScrollerProperties &operator=(const QScrollerProperties &sp); + virtual ~QScrollerProperties(); + + bool operator==(const QScrollerProperties &sp) const; + bool operator!=(const QScrollerProperties &sp) const; + + static void setDefaultScrollerProperties(const QScrollerProperties &sp); + static void unsetDefaultScrollerProperties(); + + enum OvershootPolicy + { + OvershootWhenScrollable, + OvershootAlwaysOff, + OvershootAlwaysOn + }; + + enum FrameRates { + Standard, + Fps60, + Fps30, + Fps20 + }; + + enum ScrollMetric + { + MousePressEventDelay, // qreal [s] + DragStartDistance, // qreal [m] + DragVelocitySmoothingFactor, // qreal [0..1/s] (complex calculation involving time) v = v_new* DASF + v_old * (1-DASF) + AxisLockThreshold, // qreal [0..1] atan(|min(dx,dy)|/|max(dx,dy)|) + + ScrollingCurve, // QEasingCurve + DecelerationFactor, // slope of the curve + + MinimumVelocity, // qreal [m/s] + MaximumVelocity, // qreal [m/s] + MaximumClickThroughVelocity, // qreal [m/s] + + AcceleratingFlickMaximumTime, // qreal [s] + AcceleratingFlickSpeedupFactor, // qreal [1..] + + SnapPositionRatio, // qreal [0..1] + SnapTime, // qreal [s] + + OvershootDragResistanceFactor, // qreal [0..1] + OvershootDragDistanceFactor, // qreal [0..1] + OvershootScrollDistanceFactor, // qreal [0..1] + OvershootScrollTime, // qreal [s] + + HorizontalOvershootPolicy, // enum OvershootPolicy + VerticalOvershootPolicy, // enum OvershootPolicy + FrameRate, // enum FrameRates + + ScrollMetricCount + }; + + QVariant scrollMetric(ScrollMetric metric) const; + void setScrollMetric(ScrollMetric metric, const QVariant &value); + +protected: + QScopedPointer<QScrollerPropertiesPrivate> d; + +private: + QScrollerProperties(QScrollerPropertiesPrivate &dd); + + friend class QScrollerPropertiesPrivate; + friend class QScroller; + friend class QScrollerPrivate; +}; + +QT_END_NAMESPACE + +Q_DECLARE_METATYPE(QScrollerProperties::OvershootPolicy); +Q_DECLARE_METATYPE(QScrollerProperties::FrameRates); + +QT_END_HEADER + +#endif // QSCROLLERPROPERTIES_H diff --git a/src/gui/util/qscrollerproperties_p.h b/src/gui/util/qscrollerproperties_p.h new file mode 100644 index 0000000..093f615 --- /dev/null +++ b/src/gui/util/qscrollerproperties_p.h @@ -0,0 +1,94 @@ +/**************************************************************************** +** +** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QSCROLLERPROPERTIES_P_H +#define QSCROLLERPROPERTIES_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QPointF> +#include <QEasingCurve> +#include <qscrollerproperties.h> + +QT_BEGIN_NAMESPACE + +class QScrollerPropertiesPrivate +{ +public: + static QScrollerPropertiesPrivate *defaults(); + + bool operator==(const QScrollerPropertiesPrivate &) const; + + qreal mousePressEventDelay; + qreal dragStartDistance; + qreal dragVelocitySmoothingFactor; + qreal axisLockThreshold; + QEasingCurve scrollingCurve; + qreal decelerationFactor; + qreal minimumVelocity; + qreal maximumVelocity; + qreal maximumClickThroughVelocity; + qreal acceleratingFlickMaximumTime; + qreal acceleratingFlickSpeedupFactor; + qreal snapPositionRatio; + qreal snapTime; + qreal overshootDragResistanceFactor; + qreal overshootDragDistanceFactor; + qreal overshootScrollDistanceFactor; + qreal overshootScrollTime; + QScrollerProperties::OvershootPolicy hOvershootPolicy; + QScrollerProperties::OvershootPolicy vOvershootPolicy; + QScrollerProperties::FrameRates frameRate; +}; + +QT_END_NAMESPACE + +#endif // QSCROLLERPROPERTIES_P_H + diff --git a/src/gui/util/util.pri b/src/gui/util/util.pri index f125f82..2814a2d 100644 --- a/src/gui/util/util.pri +++ b/src/gui/util/util.pri @@ -6,6 +6,11 @@ HEADERS += \ util/qcompleter_p.h \ util/qdesktopservices.h \ util/qsystemtrayicon_p.h \ + util/qscroller.h \ + util/qscroller_p.h \ + util/qscrollerproperties.h \ + util/qscrollerproperties_p.h \ + util/qflickgesture_p.h \ util/qundogroup.h \ util/qundostack.h \ util/qundostack_p.h \ @@ -15,6 +20,9 @@ SOURCES += \ util/qsystemtrayicon.cpp \ util/qcompleter.cpp \ util/qdesktopservices.cpp \ + util/qscroller.cpp \ + util/qscrollerproperties.cpp \ + util/qflickgesture.cpp \ util/qundogroup.cpp \ util/qundostack.cpp \ util/qundoview.cpp @@ -57,3 +65,7 @@ symbian { DEFINES += USE_SCHEMEHANDLER } } + +macx { + OBJECTIVE_SOURCES += util/qscroller_mac.mm +} diff --git a/src/gui/widgets/qabstractscrollarea.cpp b/src/gui/widgets/qabstractscrollarea.cpp index 30ce23b..13942ea 100644 --- a/src/gui/widgets/qabstractscrollarea.cpp +++ b/src/gui/widgets/qabstractscrollarea.cpp @@ -53,6 +53,8 @@ #include "qpainter.h" #include "qmargins.h" +#include <QDebug> + #include "qabstractscrollarea_p.h" #include <qwidget.h> @@ -62,6 +64,10 @@ #include <private/qt_mac_p.h> #include <private/qt_cocoa_helpers_mac_p.h> #endif +#ifdef Q_WS_WIN +# include <qlibrary.h> +# include <windows.h> +#endif QT_BEGIN_NAMESPACE @@ -295,9 +301,14 @@ void QAbstractScrollAreaPrivate::init() q->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); layoutChildren(); #ifndef Q_WS_MAC -#ifndef QT_NO_GESTURES +# ifndef QT_NO_GESTURES viewport->grabGesture(Qt::PanGesture); +# endif #endif +#ifdef Q_WS_MAEMO_5 +# ifndef QT_NO_GESTURES + // viewport->grabGesture(Qt::TouchFlickGesture); +# endif #endif } @@ -552,6 +563,11 @@ void QAbstractScrollArea::setViewport(QWidget *widget) d->viewport->grabGesture(Qt::PanGesture); #endif #endif +#ifdef Q_WS_MAEMO_5 +#ifndef QT_NO_GESTURES +// d->viewport->grabGesture(Qt::TouchFlickGesture); +#endif +#endif d->layoutChildren(); if (isVisible()) d->viewport->show(); @@ -986,6 +1002,66 @@ bool QAbstractScrollArea::event(QEvent *e) return false; } #endif // QT_NO_GESTURES + case QEvent::ScrollPrepare: + { + QScrollPrepareEvent *se = static_cast<QScrollPrepareEvent *>(e); + if( d->canStartScrollingAt(se->startPos().toPoint()) ) { + QScrollBar *hBar = horizontalScrollBar(); + QScrollBar *vBar = verticalScrollBar(); + + se->setViewportSize( QSizeF(viewport()->size()) ); + se->setContentPosRange( QRectF(0, 0, hBar->maximum(), vBar->maximum()) ); + se->setContentPos( QPointF(hBar->value(), vBar->value()) ); + se->accept(); + return true; + } + return false; + } + case QEvent::Scroll: + { + QScrollEvent *se = static_cast<QScrollEvent *>(e); + + QScrollBar *hBar = horizontalScrollBar(); + QScrollBar *vBar = verticalScrollBar(); + hBar->setValue(se->contentPos().x()); + vBar->setValue(se->contentPos().y()); + +#ifdef Q_WS_WIN + typedef BOOL (*PtrBeginPanningFeedback)(HWND); + typedef BOOL (*PtrUpdatePanningFeedback)(HWND, LONG, LONG, BOOL); + typedef BOOL (*PtrEndPanningFeedback)(HWND, BOOL); + + static PtrBeginPanningFeedback ptrBeginPanningFeedback = 0; + static PtrUpdatePanningFeedback ptrUpdatePanningFeedback = 0; + static PtrEndPanningFeedback ptrEndPanningFeedback = 0; + + if (!ptrBeginPanningFeedback) + ptrBeginPanningFeedback = (PtrBeginPanningFeedback) QLibrary::resolve(QLatin1String("UxTheme"), "BeginPanningFeedback"); + if (!ptrUpdatePanningFeedback) + ptrUpdatePanningFeedback = (PtrUpdatePanningFeedback) QLibrary::resolve(QLatin1String("UxTheme"), "UpdatePanningFeedback"); + if (!ptrEndPanningFeedback) + ptrEndPanningFeedback = (PtrEndPanningFeedback) QLibrary::resolve(QLatin1String("UxTheme"), "EndPanningFeedback"); + + if (ptrBeginPanningFeedback && ptrUpdatePanningFeedback && ptrEndPanningFeedback) { + WId wid = window()->winId(); + + if (!se->overshootDistance().isNull() && d->overshoot.isNull()) + ptrBeginPanningFeedback(wid); + if (!se->overshootDistance().isNull()) + ptrUpdatePanningFeedback(wid, -se->overshootDistance().x(), -se->overshootDistance().y(), false); + if (se->overshootDistance().isNull() && !d->overshoot.isNull()) + ptrEndPanningFeedback(wid, true); + } else +#endif + { + QPoint delta = d->overshoot - se->overshootDistance().toPoint(); + if (!delta.isNull()) + viewport()->move(viewport()->pos() + delta); + } + d->overshoot = se->overshootDistance().toPoint(); + + return true; + } case QEvent::StyleChange: case QEvent::LayoutDirectionChange: case QEvent::ApplicationLayoutDirectionChange: @@ -1047,6 +1123,9 @@ bool QAbstractScrollArea::viewportEvent(QEvent *e) case QEvent::GestureOverride: return event(e); #endif + case QEvent::ScrollPrepare: + case QEvent::Scroll: + return event(e); default: break; } @@ -1303,6 +1382,30 @@ void QAbstractScrollArea::scrollContentsBy(int, int) viewport()->update(); } +bool QAbstractScrollAreaPrivate::canStartScrollingAt( const QPoint &startPos ) +{ + Q_Q(QAbstractScrollArea); + + // don't start scrolling when a drag mode has been set. + // don't start scrolling on a movable item. + if (QGraphicsView *view = qobject_cast<QGraphicsView *>(q)) { + if (view->dragMode() != QGraphicsView::NoDrag) + return false; + + QGraphicsItem *childItem = view->itemAt(startPos); + + if (childItem && (childItem->flags() & QGraphicsItem::ItemIsMovable)) + return false; + } + + // don't start scrolling on a QAbstractSlider + if (qobject_cast<QAbstractSlider *>(q->viewport()->childAt(startPos))) { + return false; + } + + return true; +} + void QAbstractScrollAreaPrivate::_q_hslide(int x) { Q_Q(QAbstractScrollArea); diff --git a/src/gui/widgets/qabstractscrollarea_p.h b/src/gui/widgets/qabstractscrollarea_p.h index 9a0d66f..8dbb3e4 100644 --- a/src/gui/widgets/qabstractscrollarea_p.h +++ b/src/gui/widgets/qabstractscrollarea_p.h @@ -84,11 +84,13 @@ public: int left, top, right, bottom; // viewport margin int xoffset, yoffset; + QPoint overshoot; void init(); void layoutChildren(); // ### Fix for 4.4, talk to Bjoern E or Girish. virtual void scrollBarPolicyChanged(Qt::Orientation, Qt::ScrollBarPolicy) {} + bool canStartScrollingAt( const QPoint &startPos ); void _q_hslide(int); void _q_vslide(int); diff --git a/tests/auto/gui.pro b/tests/auto/gui.pro index c94272c..48b8cec 100644 --- a/tests/auto/gui.pro +++ b/tests/auto/gui.pro @@ -146,6 +146,7 @@ SUBDIRS=\ qregion \ qscrollarea \ qscrollbar \ + qscroller \ qsharedpointer_and_qwidget \ qshortcut \ qsidebar \ diff --git a/tests/auto/qscroller/qscroller.pro b/tests/auto/qscroller/qscroller.pro new file mode 100644 index 0000000..845dcb9 --- /dev/null +++ b/tests/auto/qscroller/qscroller.pro @@ -0,0 +1,3 @@ +load(qttest_p4) + +SOURCES += tst_qscroller.cpp diff --git a/tests/auto/qscroller/tst_qscroller.cpp b/tests/auto/qscroller/tst_qscroller.cpp new file mode 100644 index 0000000..61d3980 --- /dev/null +++ b/tests/auto/qscroller/tst_qscroller.cpp @@ -0,0 +1,529 @@ +/**************************************************************************** +** +** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the $MODULE$ of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include <QtGui> +#include <QtTest> +// #include <QDebug> + +class tst_QScrollerWidget : public QWidget +{ +public: + tst_QScrollerWidget() + : QWidget() + { + reset(); + } + + void reset() + { + receivedPrepare = false; + receivedScroll = false; + receivedFirst = false; + receivedLast = false; + receivedOvershoot = false; + } + + bool event(QEvent *e) + { + switch (e->type()) { + case QEvent::Gesture: + e->setAccepted(false); // better reject the event or QGestureManager will make trouble + return false; + + case QEvent::ScrollPrepare: + { + receivedPrepare = true; + QScrollPrepareEvent *se = static_cast<QScrollPrepareEvent *>(e); + se->setViewportSize(QSizeF(100,100)); + se->setContentPosRange(scrollArea); + se->setContentPos(scrollPosition); + se->accept(); + return true; + } + + case QEvent::Scroll: + { + receivedScroll = true; + QScrollEvent *se = static_cast<QScrollEvent *>(e); + // qDebug() << "Scroll for"<<this<<"pos"<<se->scrollPos()<<"ov"<<se->overshoot()<<"first"<<se->isFirst()<<"last"<<se->isLast(); + + if (se->scrollState() == QScrollEvent::ScrollStarted) + receivedFirst = true; + if (se->scrollState() == QScrollEvent::ScrollFinished) + receivedLast = true; + + currentPos = se->contentPos(); + overshoot = se->overshootDistance(); + if (!qFuzzyCompare( overshoot.x() + 1.0, 1.0 ) || + !qFuzzyCompare( overshoot.y() + 1.0, 1.0 )) + receivedOvershoot = true; + return true; + } + + default: + return QObject::event(e); + } + } + + + QRectF scrollArea; + QPointF scrollPosition; + + bool receivedPrepare; + bool receivedScroll; + bool receivedFirst; + bool receivedLast; + bool receivedOvershoot; + + QPointF currentPos; + QPointF overshoot; +}; + + +class tst_QScroller : public QObject +{ + Q_OBJECT +public: + tst_QScroller() { } + ~tst_QScroller() { } + +private: + void kineticScroll( tst_QScrollerWidget *sw, QPointF from, QPoint touchStart, QPoint touchUpdate, QPoint touchEnd); + void kineticScrollNoTest( tst_QScrollerWidget *sw, QPointF from, QPoint touchStart, QPoint touchUpdate, QPoint touchEnd); + +private slots: + void staticScrollers(); + void scrollerProperties(); + void scrollTo(); + void scroll(); + void overshoot(); +}; + +/*! \internal + Generates touchBegin, touchUpdate and touchEnd events to trigger scrolling. + Tests some in between states but does not wait until scrolling is finished. +*/ +void tst_QScroller::kineticScroll( tst_QScrollerWidget *sw, QPointF from, QPoint touchStart, QPoint touchUpdate, QPoint touchEnd) +{ + sw->scrollPosition = from; + sw->currentPos= from; + + QScroller *s1 = QScroller::scroller(sw); + QCOMPARE( s1->state(), QScroller::Inactive ); + + QScrollerProperties sp1 = QScroller::scroller(sw)->scrollerProperties(); + int fps = 60; + + QTouchEvent::TouchPoint rawTouchPoint; + rawTouchPoint.setId(0); + + // send the touch begin event + QTouchEvent::TouchPoint touchPoint(0); + touchPoint.setState(Qt::TouchPointPressed); + touchPoint.setPos(touchStart); + touchPoint.setScenePos(touchStart); + touchPoint.setScreenPos(touchStart); + QTouchEvent touchEvent1(QEvent::TouchBegin, + QTouchEvent::TouchScreen, + Qt::NoModifier, + Qt::TouchPointPressed, + (QList<QTouchEvent::TouchPoint>() << touchPoint)); + QApplication::sendEvent(sw, &touchEvent1); + + QCOMPARE( s1->state(), QScroller::Pressed ); + + // send the touch update far enough to trigger a scroll + QTest::qWait(200); // we need to wait a little or else the speed would be infinite. now we have around 500 pixel per second. + touchPoint.setPos(touchUpdate); + touchPoint.setScenePos(touchUpdate); + touchPoint.setScreenPos(touchUpdate); + QTouchEvent touchEvent2(QEvent::TouchUpdate, + QTouchEvent::TouchScreen, + Qt::NoModifier, + Qt::TouchPointMoved, + (QList<QTouchEvent::TouchPoint>() << touchPoint)); + QApplication::sendEvent(sw, &touchEvent2); + + QCOMPARE( s1->state(), QScroller::Dragging ); + QCOMPARE( sw->receivedPrepare, true ); + + + QTest::qWait(1000 / fps * 2); // wait until the first scroll move + QCOMPARE( sw->receivedFirst, true ); + QCOMPARE( sw->receivedScroll, true ); + QCOMPARE( sw->receivedOvershoot, false ); + + // note that the scrolling goes in a different direction than the mouse move + QPoint calculatedPos = from.toPoint() - touchUpdate - touchStart; + QVERIFY(qAbs(sw->currentPos.x() - calculatedPos.x()) < 1.0); + QVERIFY(qAbs(sw->currentPos.y() - calculatedPos.y()) < 1.0); + + // send the touch end + touchPoint.setPos(touchEnd); + touchPoint.setScenePos(touchEnd); + touchPoint.setScreenPos(touchEnd); + QTouchEvent touchEvent5(QEvent::TouchEnd, + QTouchEvent::TouchScreen, + Qt::NoModifier, + Qt::TouchPointReleased, + (QList<QTouchEvent::TouchPoint>() << touchPoint)); + QApplication::sendEvent(sw, &touchEvent5); +} + +/*! \internal + Generates touchBegin, touchUpdate and touchEnd events to trigger scrolling. + This function does not have any in between tests, it does not expect the scroller to actually scroll. +*/ +void tst_QScroller::kineticScrollNoTest( tst_QScrollerWidget *sw, QPointF from, QPoint touchStart, QPoint touchUpdate, QPoint touchEnd) +{ + sw->scrollPosition = from; + sw->currentPos = from; + + QScroller *s1 = QScroller::scroller(sw); + QCOMPARE( s1->state(), QScroller::Inactive ); + + QScrollerProperties sp1 = s1->scrollerProperties(); + int fps = 60; + + QTouchEvent::TouchPoint rawTouchPoint; + rawTouchPoint.setId(0); + + // send the touch begin event + QTouchEvent::TouchPoint touchPoint(0); + touchPoint.setState(Qt::TouchPointPressed); + touchPoint.setPos(touchStart); + touchPoint.setScenePos(touchStart); + touchPoint.setScreenPos(touchStart); + QTouchEvent touchEvent1(QEvent::TouchBegin, + QTouchEvent::TouchScreen, + Qt::NoModifier, + Qt::TouchPointPressed, + (QList<QTouchEvent::TouchPoint>() << touchPoint)); + QApplication::sendEvent(sw, &touchEvent1); + + // send the touch update far enough to trigger a scroll + QTest::qWait(200); // we need to wait a little or else the speed would be infinite. now we have around 500 pixel per second. + touchPoint.setPos(touchUpdate); + touchPoint.setScenePos(touchUpdate); + touchPoint.setScreenPos(touchUpdate); + QTouchEvent touchEvent2(QEvent::TouchUpdate, + QTouchEvent::TouchScreen, + Qt::NoModifier, + Qt::TouchPointMoved, + (QList<QTouchEvent::TouchPoint>() << touchPoint)); + QApplication::sendEvent(sw, &touchEvent2); + + QTest::qWait(1000 / fps * 2); // wait until the first scroll move + + // send the touch end + touchPoint.setPos(touchEnd); + touchPoint.setScenePos(touchEnd); + touchPoint.setScreenPos(touchEnd); + QTouchEvent touchEvent5(QEvent::TouchEnd, + QTouchEvent::TouchScreen, + Qt::NoModifier, + Qt::TouchPointReleased, + (QList<QTouchEvent::TouchPoint>() << touchPoint)); + QApplication::sendEvent(sw, &touchEvent5); +} + + +void tst_QScroller::staticScrollers() +{ + // scrollers + { + QObject *o1 = new QObject(this); + QObject *o2 = new QObject(this); + + // get scroller for object + QScroller *s1 = QScroller::scroller(o1); + QScroller *s2 = QScroller::scroller(o2); + + QVERIFY(s1); + QVERIFY(s2); + QVERIFY(s1 != s2); + + QVERIFY(!QScroller::scroller(static_cast<const QObject*>(0))); + QCOMPARE(QScroller::scroller(o1), s1); + + delete o1; + delete o2; + } + + // the same for properties + { + QObject *o1 = new QObject(this); + QObject *o2 = new QObject(this); + + // get scroller for object + QScrollerProperties sp1 = QScroller::scroller(o1)->scrollerProperties(); + QScrollerProperties sp2 = QScroller::scroller(o2)->scrollerProperties(); + + // default properties should be the same + QVERIFY(sp1 == sp2); + + QCOMPARE(QScroller::scroller(o1)->scrollerProperties(), sp1); + + delete o1; + delete o2; + } +} + +void tst_QScroller::scrollerProperties() +{ + QObject *o1 = new QObject(this); + QScrollerProperties sp1 = QScroller::scroller(o1)->scrollerProperties(); + + QScrollerProperties::ScrollMetric metrics[] = + { + QScrollerProperties::MousePressEventDelay, // qreal [s] + QScrollerProperties::DragStartDistance, // qreal [m] + QScrollerProperties::DragVelocitySmoothingFactor, // qreal [0..1/s] (complex calculation involving time) v = v_new* DASF + v_old * (1-DASF) + QScrollerProperties::AxisLockThreshold, // qreal [0..1] atan(|min(dx,dy)|/|max(dx,dy)|) + + QScrollerProperties::DecelerationFactor, // slope of the curve + + QScrollerProperties::MinimumVelocity, // qreal [m/s] + QScrollerProperties::MaximumVelocity, // qreal [m/s] + QScrollerProperties::MaximumClickThroughVelocity, // qreal [m/s] + + QScrollerProperties::AcceleratingFlickMaximumTime, // qreal [s] + QScrollerProperties::AcceleratingFlickSpeedupFactor, // qreal [1..] + + QScrollerProperties::SnapPositionRatio, // qreal [0..1] + QScrollerProperties::SnapTime, // qreal [s] + + QScrollerProperties::OvershootDragResistanceFactor, // qreal [0..1] + QScrollerProperties::OvershootDragDistanceFactor, // qreal [0..1] + QScrollerProperties::OvershootScrollDistanceFactor, // qreal [0..1] + QScrollerProperties::OvershootScrollTime, // qreal [s] + }; + + for (unsigned int i = 0; i < sizeof(metrics) / sizeof(metrics[0]); i++) { + sp1.setScrollMetric(metrics[i], 0.9); + QCOMPARE(sp1.scrollMetric(metrics[i]).toDouble(), 0.9); + } + sp1.setScrollMetric(QScrollerProperties::ScrollingCurve, QEasingCurve(QEasingCurve::OutQuart)); + QCOMPARE(sp1.scrollMetric(QScrollerProperties::ScrollingCurve).toEasingCurve().type(), QEasingCurve::OutQuart); + + sp1.setScrollMetric(QScrollerProperties::HorizontalOvershootPolicy, QVariant::fromValue(QScrollerProperties::OvershootAlwaysOff)); + QCOMPARE(sp1.scrollMetric(QScrollerProperties::HorizontalOvershootPolicy).value<QScrollerProperties::OvershootPolicy>(), QScrollerProperties::OvershootAlwaysOff); + + sp1.setScrollMetric(QScrollerProperties::VerticalOvershootPolicy, QVariant::fromValue(QScrollerProperties::OvershootAlwaysOn)); + QCOMPARE(sp1.scrollMetric(QScrollerProperties::VerticalOvershootPolicy).value<QScrollerProperties::OvershootPolicy>(), QScrollerProperties::OvershootAlwaysOn); + + sp1.setScrollMetric(QScrollerProperties::FrameRate, QVariant::fromValue(QScrollerProperties::Fps20)); + QCOMPARE(sp1.scrollMetric(QScrollerProperties::FrameRate).value<QScrollerProperties::FrameRates>(), QScrollerProperties::Fps20); +} + +void tst_QScroller::scrollTo() +{ + { + tst_QScrollerWidget *sw = new tst_QScrollerWidget(); + sw->scrollArea = QRectF( 0, 0, 1000, 1000 ); + sw->scrollPosition = QPointF( 500, 500 ); + + QScroller *s1 = QScroller::scroller(sw); + QCOMPARE( s1->state(), QScroller::Inactive ); + + // a normal scroll + s1->scrollTo(QPointF(100,100), 100); + QTest::qWait(200); + + QCOMPARE( sw->receivedPrepare, true ); + QCOMPARE( sw->receivedScroll, true ); + QCOMPARE( sw->receivedFirst, true ); + QCOMPARE( sw->receivedLast, true ); + QCOMPARE( sw->receivedOvershoot, false ); + QVERIFY(qFuzzyCompare( sw->currentPos.x(), 100 )); + QVERIFY(qFuzzyCompare( sw->currentPos.y(), 100 )); + + delete sw; + } +} + +void tst_QScroller::scroll() +{ +#ifndef QT_NO_GESTURES + + // -- good case. normal scroll + + tst_QScrollerWidget *sw = new tst_QScrollerWidget(); + sw->scrollArea = QRectF(0, 0, 1000, 1000); + QScroller::grabGesture(sw, QScroller::TouchGesture); + sw->setGeometry(100, 100, 400, 300); + + QScroller *s1 = QScroller::scroller(sw); + kineticScroll(sw, QPointF(500, 500), QPoint(0, 0), QPoint(100, 100), QPoint(200, 200)); + // now we should be scrolling + QCOMPARE( s1->state(), QScroller::Scrolling ); + + // wait until finished, check that no further first scroll is send + sw->receivedFirst = false; + sw->receivedScroll = false; + while (s1->state() == QScroller::Scrolling) + QTest::qWait(100); + + QCOMPARE( sw->receivedFirst, false ); + QCOMPARE( sw->receivedScroll, true ); + QCOMPARE( sw->receivedLast, true ); + QVERIFY(sw->currentPos.x() < 400); + QVERIFY(sw->currentPos.y() < 400); + + // -- try to scroll when nothing to scroll + + sw->reset(); + sw->scrollArea = QRectF(0, 0, 0, 1000); + kineticScrollNoTest(sw, QPointF(0, 500), QPoint(0, 0), QPoint(100, 0), QPoint(200, 0)); + + while (s1->state() != QScroller::Inactive) + QTest::qWait(20); + + QCOMPARE(sw->currentPos.x(), 0.0); + QCOMPARE(sw->currentPos.y(), 500.0); + + delete sw; +#endif +} + +void tst_QScroller::overshoot() +{ +#ifndef QT_NO_GESTURES + tst_QScrollerWidget *sw = new tst_QScrollerWidget(); + sw->scrollArea = QRectF(0, 0, 1000, 1000); + QScroller::grabGesture(sw, QScroller::TouchGesture); + sw->setGeometry(100, 100, 400, 300); + + QScroller *s1 = QScroller::scroller(sw); + QScrollerProperties sp1 = s1->scrollerProperties(); + + sp1.setScrollMetric(QScrollerProperties::OvershootDragResistanceFactor, 0.5); + sp1.setScrollMetric(QScrollerProperties::OvershootDragDistanceFactor, 0.2); + sp1.setScrollMetric(QScrollerProperties::OvershootScrollDistanceFactor, 0.2); + + // -- try to scroll with overshoot (when scrollable good case) + + sp1.setScrollMetric(QScrollerProperties::HorizontalOvershootPolicy, QVariant::fromValue(QScrollerProperties::OvershootWhenScrollable)); + s1->setScrollerProperties(sp1); + kineticScrollNoTest(sw, QPointF(500, 500), QPoint(0, 0), QPoint(400, 0), QPoint(490, 0)); + + while (s1->state() != QScroller::Inactive) + QTest::qWait(20); + + //qDebug() << "Overshoot fuzzy: "<<sw->currentPos; + QVERIFY(qFuzzyCompare( sw->currentPos.x(), 0 )); + QVERIFY(qFuzzyCompare( sw->currentPos.y(), 500 )); + QCOMPARE( sw->receivedOvershoot, true ); + + // -- try to scroll with overshoot (when scrollable bad case) + sw->reset(); + sw->scrollArea = QRectF(0, 0, 0, 1000); + + sp1.setScrollMetric(QScrollerProperties::HorizontalOvershootPolicy, QVariant::fromValue(QScrollerProperties::OvershootWhenScrollable)); + s1->setScrollerProperties(sp1); + kineticScrollNoTest(sw, QPointF(0, 500), QPoint(0, 0), QPoint(400, 0), QPoint(490, 0)); + + while (s1->state() != QScroller::Inactive) + QTest::qWait(20); + + //qDebug() << "Overshoot fuzzy: "<<sw->currentPos; + QVERIFY(qFuzzyCompare( sw->currentPos.x(), 0 )); + QVERIFY(qFuzzyCompare( sw->currentPos.y(), 500 )); + QCOMPARE( sw->receivedOvershoot, false ); + + // -- try to scroll with overshoot (always on) + sw->reset(); + sw->scrollArea = QRectF(0, 0, 0, 1000); + + sp1.setScrollMetric(QScrollerProperties::HorizontalOvershootPolicy, QVariant::fromValue(QScrollerProperties::OvershootAlwaysOn)); + s1->setScrollerProperties(sp1); + kineticScrollNoTest(sw, QPointF(0, 500), QPoint(0, 0), QPoint(400, 0), QPoint(490, 0)); + + while (s1->state() != QScroller::Inactive) + QTest::qWait(20); + + //qDebug() << "Overshoot fuzzy: "<<sw->currentPos; + + QVERIFY(qFuzzyCompare( sw->currentPos.x(), 0 )); + QVERIFY(qFuzzyCompare( sw->currentPos.y(), 500 )); + QCOMPARE( sw->receivedOvershoot, true ); + + // -- try to scroll with overshoot (always off) + sw->reset(); + sw->scrollArea = QRectF(0, 0, 1000, 1000); + + sp1.setScrollMetric(QScrollerProperties::HorizontalOvershootPolicy, QVariant::fromValue(QScrollerProperties::OvershootAlwaysOff)); + s1->setScrollerProperties(sp1); + kineticScrollNoTest(sw, QPointF(500, 500), QPoint(0, 0), QPoint(400, 0), QPoint(490, 0)); + + while (s1->state() != QScroller::Inactive) + QTest::qWait(20); + + QVERIFY(qFuzzyCompare( sw->currentPos.x(), 0 )); + QVERIFY(qFuzzyCompare( sw->currentPos.y(), 500 )); + QCOMPARE( sw->receivedOvershoot, false ); + + // -- try to scroll with overshoot (always on but max overshoot = 0) + sp1.setScrollMetric(QScrollerProperties::OvershootDragDistanceFactor, 0.0); + sp1.setScrollMetric(QScrollerProperties::OvershootScrollDistanceFactor, 0.0); + sw->reset(); + sw->scrollArea = QRectF(0, 0, 1000, 1000); + + sp1.setScrollMetric(QScrollerProperties::HorizontalOvershootPolicy, QVariant::fromValue(QScrollerProperties::OvershootAlwaysOn)); + s1->setScrollerProperties(sp1); + kineticScrollNoTest(sw, QPointF(500, 500), QPoint(0, 0), QPoint(400, 0), QPoint(490, 0)); + + while (s1->state() != QScroller::Inactive) + QTest::qWait(20); + + QVERIFY(qFuzzyCompare( sw->currentPos.x(), 0 )); + QVERIFY(qFuzzyCompare( sw->currentPos.y(), 500 )); + QCOMPARE( sw->receivedOvershoot, false ); + + + delete sw; +#endif +} + + +QTEST_MAIN(tst_QScroller) + +#include "tst_qscroller.moc" diff --git a/tools/qml/texteditautoresizer_maemo5.h b/tools/qml/texteditautoresizer_maemo5.h index bb5567a..fd35ca5 100644 --- a/tools/qml/texteditautoresizer_maemo5.h +++ b/tools/qml/texteditautoresizer_maemo5.h @@ -41,7 +41,7 @@ #include <QtGui/qplaintextedit.h> #include <QtGui/qtextedit.h> -#include <QtGui/qabstractkineticscroller.h> +#include <QtGui/qscroller.h> #include <QtGui/qscrollarea.h> #include <QtDebug> @@ -102,11 +102,11 @@ void TextEditAutoResizer::textEditChanged() QPoint scrollto = area->widget()->mapFrom(edit, cursor.center()); QPoint margin(10 + cursor.width(), 2 * cursor.height()); - if (QAbstractKineticScroller *scroller = area->property("kineticScroller").value<QAbstractKineticScroller *>()) { - scroller->ensureVisible(scrollto, margin.x(), margin.y()); - } else { - area->ensureVisible(scrollto.x(), scrollto.y(), margin.x(), margin.y()); - } +#ifdef Q_WS_MAEMO_5 + QScroller::scroller(area)->ensureVisible(scrollto, margin.x(), margin.y()); +#else + area->ensureVisible(scrollto.x(), scrollto.y(), margin.x(), margin.y()); +#endif } } |