[Qt][WK2] Fix scrolling in touch mode
[WebKit-https.git] / Source / WebKit2 / UIProcess / qt / QtWebPageEventHandler.cpp
1 /*
2  * Copyright (C) 2010, 2011 Nokia Corporation and/or its subsidiary(-ies)
3  *
4  * This library is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU Library General Public
6  * License as published by the Free Software Foundation; either
7  * version 2 of the License, or (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  * Library General Public License for more details.
13  *
14  * You should have received a copy of the GNU Library General Public License
15  * along with this program; see the file COPYING.LIB.  If not, write to
16  * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17  * Boston, MA 02110-1301, USA.
18  *
19  */
20
21 #include "config.h"
22 #include "QtWebPageEventHandler.h"
23
24 #include "NativeWebKeyboardEvent.h"
25 #include "NativeWebMouseEvent.h"
26 #include "NativeWebWheelEvent.h"
27 #include "QtViewportInteractionEngine.h"
28 #include "WebPageProxy.h"
29 #include "qquickwebpage_p.h"
30 #include "qquickwebview_p.h"
31 #include <QCursor>
32 #include <QDrag>
33 #include <QGuiApplication>
34 #include <QInputPanel>
35 #include <QMimeData>
36 #include <QtQuick/QQuickCanvas>
37 #include <QStyleHints>
38 #include <QTextFormat>
39 #include <QTouchEvent>
40 #include <QTransform>
41 #include <WebCore/DragData.h>
42 #include <WebCore/Editor.h>
43
44 using namespace WebCore;
45
46 namespace WebKit {
47
48 static inline Qt::DropAction dragOperationToDropAction(unsigned dragOperation)
49 {
50     Qt::DropAction result = Qt::IgnoreAction;
51     if (dragOperation & DragOperationCopy)
52         result = Qt::CopyAction;
53     else if (dragOperation & DragOperationMove)
54         result = Qt::MoveAction;
55     else if (dragOperation & DragOperationGeneric)
56         result = Qt::MoveAction;
57     else if (dragOperation & DragOperationLink)
58         result = Qt::LinkAction;
59     return result;
60 }
61
62 static inline Qt::DropActions dragOperationToDropActions(unsigned dragOperations)
63 {
64     Qt::DropActions result = Qt::IgnoreAction;
65     if (dragOperations & DragOperationCopy)
66         result |= Qt::CopyAction;
67     if (dragOperations & DragOperationMove)
68         result |= Qt::MoveAction;
69     if (dragOperations & DragOperationGeneric)
70         result |= Qt::MoveAction;
71     if (dragOperations & DragOperationLink)
72         result |= Qt::LinkAction;
73     return result;
74 }
75
76 static inline WebCore::DragOperation dropActionToDragOperation(Qt::DropActions actions)
77 {
78     unsigned result = 0;
79     if (actions & Qt::CopyAction)
80         result |= DragOperationCopy;
81     if (actions & Qt::MoveAction)
82         result |= (DragOperationMove | DragOperationGeneric);
83     if (actions & Qt::LinkAction)
84         result |= DragOperationLink;
85     if (result == (DragOperationCopy | DragOperationMove | DragOperationGeneric | DragOperationLink))
86         result = DragOperationEvery;
87     return (DragOperation)result;
88 }
89
90 QtWebPageEventHandler::QtWebPageEventHandler(WKPageRef pageRef, QQuickWebPage* qmlWebPage, QQuickWebView* qmlWebView)
91     : m_webPageProxy(toImpl(pageRef))
92     , m_interactionEngine(0)
93     , m_panGestureRecognizer(this)
94     , m_pinchGestureRecognizer(this)
95     , m_tapGestureRecognizer(this)
96     , m_webPage(qmlWebPage)
97     , m_webView(qmlWebView)
98     , m_previousClickButton(Qt::NoButton)
99     , m_clickCount(0)
100     , m_postponeTextInputStateChanged(false)
101 {
102     connect(qApp->inputPanel(), SIGNAL(visibleChanged()), this, SLOT(inputPanelVisibleChanged()));
103 }
104
105 QtWebPageEventHandler::~QtWebPageEventHandler()
106 {
107     disconnect(qApp->inputPanel(), SIGNAL(visibleChanged()), this, SLOT(inputPanelVisibleChanged()));
108 }
109
110 void QtWebPageEventHandler::handleMouseMoveEvent(QMouseEvent* ev)
111 {
112     // For some reason mouse press results in mouse hover (which is
113     // converted to mouse move for WebKit). We ignore these hover
114     // events by comparing lastPos with newPos.
115     // NOTE: lastPos from the event always comes empty, so we work
116     // around that here.
117     static QPointF lastPos = QPointF();
118     QTransform fromItemTransform = m_webPage->transformFromItem();
119     QPointF webPagePoint = fromItemTransform.map(ev->localPos());
120     if (lastPos == webPagePoint)
121         return;
122     lastPos = webPagePoint;
123
124     m_webPageProxy->handleMouseEvent(NativeWebMouseEvent(ev, fromItemTransform, /*eventClickCount*/ 0));
125 }
126
127 void QtWebPageEventHandler::handleMousePressEvent(QMouseEvent* ev)
128 {
129     QTransform fromItemTransform = m_webPage->transformFromItem();
130     QPointF webPagePoint = fromItemTransform.map(ev->localPos());
131
132     if (m_clickTimer.isActive()
133         && m_previousClickButton == ev->button()
134         && (webPagePoint - m_lastClick).manhattanLength() < qApp->styleHints()->startDragDistance()) {
135         m_clickCount++;
136     } else {
137         m_clickCount = 1;
138         m_previousClickButton = ev->button();
139     }
140
141     m_webPageProxy->handleMouseEvent(NativeWebMouseEvent(ev, fromItemTransform, m_clickCount));
142
143     m_lastClick = webPagePoint;
144     m_clickTimer.start(qApp->styleHints()->mouseDoubleClickInterval(), this);
145 }
146
147 void QtWebPageEventHandler::handleMouseReleaseEvent(QMouseEvent* ev)
148 {
149     QTransform fromItemTransform = m_webPage->transformFromItem();
150     m_webPageProxy->handleMouseEvent(NativeWebMouseEvent(ev, fromItemTransform, /*eventClickCount*/ 0));
151 }
152
153 void QtWebPageEventHandler::handleWheelEvent(QWheelEvent* ev)
154 {
155     QTransform fromItemTransform = m_webPage->transformFromItem();
156     m_webPageProxy->handleWheelEvent(NativeWebWheelEvent(ev, fromItemTransform));
157 }
158
159 void QtWebPageEventHandler::handleHoverLeaveEvent(QHoverEvent* ev)
160 {
161     // To get the correct behavior of mouseout, we need to turn the Leave event of our webview into a mouse move
162     // to a very far region.
163     QTransform fromItemTransform = m_webPage->transformFromItem();
164     QHoverEvent fakeEvent(QEvent::HoverMove, QPoint(INT_MIN, INT_MIN), fromItemTransform.map(ev->oldPosF()));
165     fakeEvent.setTimestamp(ev->timestamp());
166     handleHoverMoveEvent(&fakeEvent);
167 }
168
169 void QtWebPageEventHandler::handleHoverMoveEvent(QHoverEvent* ev)
170 {
171     QTransform fromItemTransform = m_webPage->transformFromItem();
172     QMouseEvent me(QEvent::MouseMove, fromItemTransform.map(ev->posF()), Qt::NoButton, Qt::NoButton, Qt::NoModifier);
173     me.setAccepted(ev->isAccepted());
174     me.setTimestamp(ev->timestamp());
175     handleMouseMoveEvent(&me);
176 }
177
178 void QtWebPageEventHandler::handleDragEnterEvent(QDragEnterEvent* ev)
179 {
180     m_webPageProxy->resetDragOperation();
181     QTransform fromItemTransform = m_webPage->transformFromItem();
182     // FIXME: Should not use QCursor::pos()
183     DragData dragData(ev->mimeData(), fromItemTransform.map(ev->pos()), QCursor::pos(), dropActionToDragOperation(ev->possibleActions()));
184     m_webPageProxy->dragEntered(&dragData);
185     ev->acceptProposedAction();
186 }
187
188 void QtWebPageEventHandler::handleDragLeaveEvent(QDragLeaveEvent* ev)
189 {
190     bool accepted = ev->isAccepted();
191
192     // FIXME: Should not use QCursor::pos()
193     DragData dragData(0, IntPoint(), QCursor::pos(), DragOperationNone);
194     m_webPageProxy->dragExited(&dragData);
195     m_webPageProxy->resetDragOperation();
196
197     ev->setAccepted(accepted);
198 }
199
200 void QtWebPageEventHandler::handleDragMoveEvent(QDragMoveEvent* ev)
201 {
202     bool accepted = ev->isAccepted();
203
204     QTransform fromItemTransform = m_webPage->transformFromItem();
205     // FIXME: Should not use QCursor::pos()
206     DragData dragData(ev->mimeData(), fromItemTransform.map(ev->pos()), QCursor::pos(), dropActionToDragOperation(ev->possibleActions()));
207     m_webPageProxy->dragUpdated(&dragData);
208     ev->setDropAction(dragOperationToDropAction(m_webPageProxy->dragSession().operation));
209     if (m_webPageProxy->dragSession().operation != DragOperationNone)
210         ev->accept();
211
212     ev->setAccepted(accepted);
213 }
214
215 void QtWebPageEventHandler::handleDropEvent(QDropEvent* ev)
216 {
217     bool accepted = ev->isAccepted();
218     QTransform fromItemTransform = m_webPage->transformFromItem();
219     // FIXME: Should not use QCursor::pos()
220     DragData dragData(ev->mimeData(), fromItemTransform.map(ev->pos()), QCursor::pos(), dropActionToDragOperation(ev->possibleActions()));
221     SandboxExtension::Handle handle;
222     SandboxExtension::HandleArray sandboxExtensionForUpload;
223     m_webPageProxy->performDrag(&dragData, String(), handle, sandboxExtensionForUpload);
224     ev->setDropAction(dragOperationToDropAction(m_webPageProxy->dragSession().operation));
225     ev->accept();
226
227     ev->setAccepted(accepted);
228 }
229
230 void QtWebPageEventHandler::handlePotentialSingleTapEvent(const QTouchEvent::TouchPoint& point)
231 {
232 #if ENABLE(TOUCH_EVENTS)
233     if (point.pos() == QPointF()) {
234         // An empty point deactivates the highlighting.
235         m_webPageProxy->handlePotentialActivation(IntPoint(), IntSize());
236     } else {
237         QTransform fromItemTransform = m_webPage->transformFromItem();
238         m_webPageProxy->handlePotentialActivation(IntPoint(fromItemTransform.map(point.pos()).toPoint()), IntSize(point.rect().size().toSize()));
239     }
240 #else
241     Q_UNUSED(point);
242 #endif
243 }
244
245 void QtWebPageEventHandler::handleSingleTapEvent(const QTouchEvent::TouchPoint& point)
246 {
247     m_postponeTextInputStateChanged = true;
248
249     QTransform fromItemTransform = m_webPage->transformFromItem();
250     WebGestureEvent gesture(WebEvent::GestureSingleTap, fromItemTransform.map(point.pos()).toPoint(), point.screenPos().toPoint(), WebEvent::Modifiers(0), 0, IntSize(point.rect().size().toSize()), FloatPoint(0, 0));
251     m_webPageProxy->handleGestureEvent(gesture);
252 }
253
254 void QtWebPageEventHandler::handleDoubleTapEvent(const QTouchEvent::TouchPoint& point)
255 {
256     QTransform fromItemTransform = m_webPage->transformFromItem();
257     m_webPageProxy->findZoomableAreaForPoint(fromItemTransform.map(point.pos()).toPoint(), IntSize(point.rect().size().toSize()));
258 }
259
260 void QtWebPageEventHandler::timerEvent(QTimerEvent* ev)
261 {
262     int timerId = ev->timerId();
263     if (timerId == m_clickTimer.timerId())
264         m_clickTimer.stop();
265     else
266         QObject::timerEvent(ev);
267 }
268
269 void QtWebPageEventHandler::handleKeyPressEvent(QKeyEvent* ev)
270 {
271     m_webPageProxy->handleKeyboardEvent(NativeWebKeyboardEvent(ev));
272 }
273
274 void QtWebPageEventHandler::handleKeyReleaseEvent(QKeyEvent* ev)
275 {
276     m_webPageProxy->handleKeyboardEvent(NativeWebKeyboardEvent(ev));
277 }
278
279 void QtWebPageEventHandler::handleFocusInEvent(QFocusEvent*)
280 {
281     m_webPageProxy->viewStateDidChange(WebPageProxy::ViewIsFocused | WebPageProxy::ViewWindowIsActive);
282 }
283
284 void QtWebPageEventHandler::handleFocusOutEvent(QFocusEvent*)
285 {
286     m_webPageProxy->viewStateDidChange(WebPageProxy::ViewIsFocused | WebPageProxy::ViewWindowIsActive);
287 }
288
289 void QtWebPageEventHandler::setViewportInteractionEngine(QtViewportInteractionEngine* engine)
290 {
291     m_interactionEngine = engine;
292 }
293
294 void QtWebPageEventHandler::handleInputMethodEvent(QInputMethodEvent* ev)
295 {
296     QString commit = ev->commitString();
297     QString composition = ev->preeditString();
298
299     int replacementStart = ev->replacementStart();
300     int replacementLength = ev->replacementLength();
301
302     // NOTE: We might want to handle events of one char as special
303     // and resend them as key events to make web site completion work.
304
305     int cursorPositionWithinComposition = 0;
306
307     Vector<CompositionUnderline> underlines;
308
309     for (int i = 0; i < ev->attributes().size(); ++i) {
310         const QInputMethodEvent::Attribute& attr = ev->attributes().at(i);
311         switch (attr.type) {
312         case QInputMethodEvent::TextFormat: {
313             if (composition.isEmpty())
314                 break;
315
316             QTextCharFormat textCharFormat = attr.value.value<QTextFormat>().toCharFormat();
317             QColor qcolor = textCharFormat.underlineColor();
318             Color color = makeRGBA(qcolor.red(), qcolor.green(), qcolor.blue(), qcolor.alpha());
319             int start = qMin(attr.start, (attr.start + attr.length));
320             int end = qMax(attr.start, (attr.start + attr.length));
321             underlines.append(CompositionUnderline(start, end, color, false));
322             break;
323         }
324         case QInputMethodEvent::Cursor:
325             if (attr.length)
326                 cursorPositionWithinComposition = attr.start;
327             break;
328         // Selection is handled further down.
329         default: break;
330         }
331     }
332
333     if (composition.isEmpty()) {
334         int selectionStart = -1;
335         int selectionLength = 0;
336         for (int i = 0; i < ev->attributes().size(); ++i) {
337             const QInputMethodEvent::Attribute& attr = ev->attributes().at(i);
338             if (attr.type == QInputMethodEvent::Selection) {
339                 selectionStart = attr.start;
340                 selectionLength = attr.length;
341
342                 ASSERT(selectionStart >= 0);
343                 ASSERT(selectionLength >= 0);
344                 break;
345             }
346         }
347
348         m_webPageProxy->confirmComposition(commit, selectionStart, selectionLength);
349     } else {
350         ASSERT(cursorPositionWithinComposition >= 0);
351         ASSERT(replacementStart >= 0);
352
353         m_webPageProxy->setComposition(composition, underlines,
354             cursorPositionWithinComposition, cursorPositionWithinComposition,
355             replacementStart, replacementLength);
356     }
357
358     ev->accept();
359 }
360
361 void QtWebPageEventHandler::handleTouchEvent(QTouchEvent* event)
362 {
363 #if ENABLE(TOUCH_EVENTS)
364     QTransform fromItemTransform = m_webPage->transformFromItem();
365     m_webPageProxy->handleTouchEvent(NativeWebTouchEvent(event, fromItemTransform));
366     event->accept();
367 #else
368     ASSERT_NOT_REACHED();
369     event->ignore();
370 #endif
371 }
372
373 void QtWebPageEventHandler::resetGestureRecognizers()
374 {
375     m_panGestureRecognizer.cancel();
376     m_pinchGestureRecognizer.cancel();
377     m_tapGestureRecognizer.cancel();
378 }
379
380 static void setInputPanelVisible(bool visible)
381 {
382     if (qApp->inputPanel()->visible() == visible)
383         return;
384
385     qApp->inputPanel()->setVisible(visible);
386 }
387
388 void QtWebPageEventHandler::inputPanelVisibleChanged()
389 {
390     if (!m_interactionEngine)
391         return;
392
393     // We only respond to the input panel becoming visible.
394     if (!m_webView->hasActiveFocus() || !qApp->inputPanel()->visible())
395         return;
396
397     const EditorState& editor = m_webPageProxy->editorState();
398     if (editor.isContentEditable)
399         m_interactionEngine->focusEditableArea(QRectF(editor.cursorRect), QRectF(editor.editorRect));
400 }
401
402 void QtWebPageEventHandler::updateTextInputState()
403 {
404     if (m_postponeTextInputStateChanged)
405         return;
406
407     const EditorState& editor = m_webPageProxy->editorState();
408
409     m_webView->setFlag(QQuickItem::ItemAcceptsInputMethod, editor.isContentEditable);
410
411     if (!m_webView->hasActiveFocus())
412         return;
413
414     qApp->inputPanel()->update(Qt::ImQueryInput | Qt::ImEnabled);
415
416     setInputPanelVisible(editor.isContentEditable);
417 }
418
419 void QtWebPageEventHandler::doneWithGestureEvent(const WebGestureEvent& event, bool wasEventHandled)
420 {
421     if (event.type() != WebEvent::GestureSingleTap)
422         return;
423
424     m_postponeTextInputStateChanged = false;
425
426     if (!wasEventHandled || !m_webView->hasActiveFocus())
427         return;
428
429     updateTextInputState();
430 }
431
432 #if ENABLE(TOUCH_EVENTS)
433 void QtWebPageEventHandler::doneWithTouchEvent(const NativeWebTouchEvent& event, bool wasEventHandled)
434 {
435     if (!m_interactionEngine)
436         return;
437
438     if (wasEventHandled || event.type() == WebEvent::TouchCancel) {
439         resetGestureRecognizers();
440         return;
441     }
442
443     const QTouchEvent* ev = event.nativeEvent();
444
445     switch (ev->type()) {
446     case QEvent::TouchBegin:
447         ASSERT(!m_interactionEngine->panGestureActive());
448         ASSERT(!m_interactionEngine->pinchGestureActive());
449         m_interactionEngine->touchBegin();
450
451         // The interaction engine might still be animating kinetic scrolling or a scale animation
452         // such as double-tap to zoom or the bounce back effect. A touch stops the kinetic scrolling
453         // where as it does not stop the scale animation.
454         // The gesture recognizer stops the kinetic scrolling animation if needed.
455         break;
456     case QEvent::TouchUpdate:
457         // The scale animation can only be interrupted by a pinch gesture, which will then take over.
458         if (m_interactionEngine->scaleAnimationActive() && m_pinchGestureRecognizer.isRecognized())
459             m_interactionEngine->interruptScaleAnimation();
460         break;
461     case QEvent::TouchEnd:
462         m_interactionEngine->touchEnd();
463         break;
464     default:
465         break;
466     }
467
468     // If the scale animation is active we don't pass the event to the recognizers. In the future
469     // we would want to queue the event here and repost then when the animation ends.
470     if (m_interactionEngine->scaleAnimationActive())
471         return;
472
473     const QList<QTouchEvent::TouchPoint>& touchPoints = ev->touchPoints();
474     const int touchPointCount = touchPoints.size();
475     qint64 eventTimestampMillis = ev->timestamp();
476     QList<QTouchEvent::TouchPoint> activeTouchPoints;
477     activeTouchPoints.reserve(touchPointCount);
478
479     for (int i = 0; i < touchPointCount; ++i) {
480         if (touchPoints[i].state() != Qt::TouchPointReleased)
481             activeTouchPoints << touchPoints[i];
482     }
483
484     const int activeTouchPointCount = activeTouchPoints.size();
485
486     if (!activeTouchPointCount) {
487         if (touchPointCount == 1) {
488             // No active touch points, one finger released.
489             if (!m_panGestureRecognizer.isRecognized())
490                 m_tapGestureRecognizer.update(ev->type(), touchPoints.first());
491             m_panGestureRecognizer.finish(touchPoints.first(), eventTimestampMillis);
492         } else
493             m_pinchGestureRecognizer.finish();
494
495         // Early return since this was a touch-end event.
496         return;
497     } else if (activeTouchPointCount == 1) {
498         // If the pinch gesture recognizer was previously in active state the content might
499         // be out of valid zoom boundaries, thus we need to finish the pinch gesture here.
500         // This will resume the content to valid zoom levels before the pan gesture is started.
501         m_pinchGestureRecognizer.finish();
502         m_panGestureRecognizer.update(activeTouchPoints.first(), eventTimestampMillis);
503     } else if (activeTouchPointCount == 2) {
504         m_panGestureRecognizer.cancel();
505         m_pinchGestureRecognizer.update(activeTouchPoints.first(), activeTouchPoints.last());
506     }
507
508     if (m_panGestureRecognizer.isRecognized() || m_pinchGestureRecognizer.isRecognized() || m_webView->isMoving())
509         m_tapGestureRecognizer.cancel();
510     else if (touchPointCount == 1)
511         m_tapGestureRecognizer.update(ev->type(), touchPoints.first());
512 }
513 #endif
514
515 void QtWebPageEventHandler::didFindZoomableArea(const IntPoint& target, const IntRect& area)
516 {
517     if (!m_interactionEngine)
518         return;
519
520     // FIXME: As the find method might not respond immediately during load etc,
521     // we should ignore all but the latest request.
522     m_interactionEngine->zoomToAreaGestureEnded(QPointF(target), QRectF(area));
523 }
524
525 void QtWebPageEventHandler::startDrag(const WebCore::DragData& dragData, PassRefPtr<ShareableBitmap> dragImage)
526 {
527     QImage dragQImage;
528     if (dragImage)
529         dragQImage = dragImage->createQImage();
530     else if (dragData.platformData() && dragData.platformData()->hasImage())
531         dragQImage = qvariant_cast<QImage>(dragData.platformData()->imageData());
532
533     DragOperation dragOperationMask = dragData.draggingSourceOperationMask();
534     QMimeData* mimeData = const_cast<QMimeData*>(dragData.platformData());
535     Qt::DropActions supportedDropActions = dragOperationToDropActions(dragOperationMask);
536
537     QPoint clientPosition;
538     QPoint globalPosition;
539     Qt::DropAction actualDropAction = Qt::IgnoreAction;
540
541     if (QWindow* window = m_webPage->canvas()) {
542         QDrag* drag = new QDrag(window);
543         drag->setPixmap(QPixmap::fromImage(dragQImage));
544         drag->setMimeData(mimeData);
545         actualDropAction = drag->exec(supportedDropActions);
546         globalPosition = QCursor::pos();
547         clientPosition = window->mapFromGlobal(globalPosition);
548     }
549
550     m_webPageProxy->dragEnded(clientPosition, globalPosition, dropActionToDragOperation(actualDropAction));
551 }
552
553 } // namespace WebKit
554
555 #include "moc_QtWebPageEventHandler.cpp"
556