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