iOS: keep keyboard rect in sync
[qt:qtbase.git] / src / plugins / platforms / ios / qiosinputcontext.mm
1 /****************************************************************************
2 **
3 ** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
4 ** Contact: http://www.qt-project.org/legal
5 **
6 ** This file is part of the plugins of the Qt Toolkit.
7 **
8 ** $QT_BEGIN_LICENSE:LGPL$
9 ** Commercial License Usage
10 ** Licensees holding valid commercial Qt licenses may use this file in
11 ** accordance with the commercial license agreement provided with the
12 ** Software or, alternatively, in accordance with the terms contained in
13 ** a written agreement between you and Digia.  For licensing terms and
14 ** conditions see http://qt.digia.com/licensing.  For further information
15 ** use the contact form at http://qt.digia.com/contact-us.
16 **
17 ** GNU Lesser General Public License Usage
18 ** Alternatively, this file may be used under the terms of the GNU Lesser
19 ** General Public License version 2.1 as published by the Free Software
20 ** Foundation and appearing in the file LICENSE.LGPL included in the
21 ** packaging of this file.  Please review the following information to
22 ** ensure the GNU Lesser General Public License version 2.1 requirements
23 ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
24 **
25 ** In addition, as a special exception, Digia gives you certain additional
26 ** rights.  These rights are described in the Digia Qt LGPL Exception
27 ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
28 **
29 ** GNU General Public License Usage
30 ** Alternatively, this file may be used under the terms of the GNU
31 ** General Public License version 3.0 as published by the Free Software
32 ** Foundation and appearing in the file LICENSE.GPL included in the
33 ** packaging of this file.  Please review the following information to
34 ** ensure the GNU General Public License version 3.0 requirements will be
35 ** met: http://www.gnu.org/copyleft/gpl.html.
36 **
37 **
38 ** $QT_END_LICENSE$
39 **
40 ****************************************************************************/
41
42 #include "qiosinputcontext.h"
43
44 #import <UIKit/UIGestureRecognizerSubclass.h>
45
46 #include "qiosglobal.h"
47 #include "qioswindow.h"
48 #include "quiview.h"
49 #include <QGuiApplication>
50
51 @interface QIOSKeyboardListener : UIGestureRecognizer {
52 @public
53     QIOSInputContext *m_context;
54     BOOL m_keyboardVisible;
55     BOOL m_keyboardVisibleAndDocked;
56     BOOL m_ignoreKeyboardChanges;
57     QRectF m_keyboardRect;
58     QRectF m_keyboardEndRect;
59     NSTimeInterval m_duration;
60     UIViewAnimationCurve m_curve;
61     UIViewController *m_viewController;
62 }
63 @end
64
65 @implementation QIOSKeyboardListener
66
67 - (id)initWithQIOSInputContext:(QIOSInputContext *)context
68 {
69     self = [super initWithTarget:self action:@selector(gestureTriggered)];
70     if (self) {
71         m_context = context;
72         m_keyboardVisible = NO;
73         m_keyboardVisibleAndDocked = NO;
74         m_ignoreKeyboardChanges = NO;
75         m_duration = 0;
76         m_curve = UIViewAnimationCurveEaseOut;
77         m_viewController = 0;
78
79         if (isQtApplication()) {
80             // Get the root view controller that is on the same screen as the keyboard:
81             for (UIWindow *uiWindow in [[UIApplication sharedApplication] windows]) {
82                 if (uiWindow.screen == [UIScreen mainScreen]) {
83                     m_viewController = [uiWindow.rootViewController retain];
84                     break;
85                 }
86             }
87             Q_ASSERT(m_viewController);
88
89             // Attach 'hide keyboard' gesture to the window, but keep it disabled when the
90             // keyboard is not visible. Note that we never trigger the gesture the way it is intended
91             // since we don't want to cancel touch events and interrupt flicking etc. Instead we use
92             // the gesture framework more as an event filter and hide the keyboard silently.
93             self.enabled = NO;
94             self.delaysTouchesEnded = NO;
95             [m_viewController.view.window addGestureRecognizer:self];
96         }
97
98         [[NSNotificationCenter defaultCenter]
99             addObserver:self
100             selector:@selector(keyboardWillShow:)
101             name:@"UIKeyboardWillShowNotification" object:nil];
102         [[NSNotificationCenter defaultCenter]
103             addObserver:self
104             selector:@selector(keyboardWillHide:)
105             name:@"UIKeyboardWillHideNotification" object:nil];
106         [[NSNotificationCenter defaultCenter]
107             addObserver:self
108             selector:@selector(keyboardDidChangeFrame:)
109             name:@"UIKeyboardDidChangeFrameNotification" object:nil];
110     }
111     return self;
112 }
113
114 - (void) dealloc
115 {
116     [m_viewController.view.window removeGestureRecognizer:self];
117     [m_viewController release];
118
119     [[NSNotificationCenter defaultCenter]
120         removeObserver:self
121         name:@"UIKeyboardWillShowNotification" object:nil];
122     [[NSNotificationCenter defaultCenter]
123         removeObserver:self
124         name:@"UIKeyboardWillHideNotification" object:nil];
125     [[NSNotificationCenter defaultCenter]
126         removeObserver:self
127         name:@"UIKeyboardDidChangeFrameNotification" object:nil];
128     [super dealloc];
129 }
130
131 - (QRectF) getKeyboardRect:(NSNotification *)notification
132 {
133     // For Qt applications we rotate the keyboard rect to align with the screen
134     // orientation (which is the interface orientation of the root view controller).
135     // For hybrid apps we follow native behavior, and return the rect unmodified:
136     CGRect keyboardFrame = [[[notification userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
137     if (isQtApplication()) {
138         UIView *view = m_viewController.view;
139         return fromCGRect(CGRectOffset([view convertRect:keyboardFrame fromView:view.window], 0, -view.bounds.origin.y));
140     } else {
141         return fromCGRect(keyboardFrame);
142     }
143 }
144
145 - (void) keyboardDidChangeFrame:(NSNotification *)notification
146 {
147     Q_UNUSED(notification);
148     [self handleKeyboardRectChanged];
149
150     // If the keyboard was visible and docked from before, this is just a geometry
151     // change (normally caused by an orientation change). In that case, update scroll:
152     if (m_keyboardVisibleAndDocked)
153         m_context->scrollToCursor();
154 }
155
156 - (void) keyboardWillShow:(NSNotification *)notification
157 {
158     if (m_ignoreKeyboardChanges)
159         return;
160     // Note that UIKeyboardWillShowNotification is only sendt when the keyboard is docked.
161     m_keyboardVisibleAndDocked = YES;
162     m_keyboardEndRect = [self getKeyboardRect:notification];
163     self.enabled = YES;
164     if (!m_duration) {
165         m_duration = [[notification.userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];
166         m_curve = UIViewAnimationCurve([[notification.userInfo objectForKey:UIKeyboardAnimationCurveUserInfoKey] integerValue] << 16);
167     }
168     m_context->scrollToCursor();
169 }
170
171 - (void) keyboardWillHide:(NSNotification *)notification
172 {
173     if (m_ignoreKeyboardChanges)
174         return;
175     // Note that UIKeyboardWillHideNotification is also sendt when the keyboard is undocked.
176     m_keyboardVisibleAndDocked = NO;
177     m_keyboardEndRect = [self getKeyboardRect:notification];
178     self.enabled = NO;
179     m_context->scroll(0);
180 }
181
182 - (void) handleKeyboardRectChanged
183 {
184     QRectF rect = m_keyboardEndRect;
185     rect.moveTop(rect.y() + m_viewController.view.bounds.origin.y);
186     if (m_keyboardRect != rect) {
187         m_keyboardRect = rect;
188         m_context->emitKeyboardRectChanged();
189     }
190
191     BOOL visible = m_keyboardEndRect.intersects(fromCGRect([UIScreen mainScreen].bounds));
192     if (m_keyboardVisible != visible) {
193         m_keyboardVisible = visible;
194         m_context->emitInputPanelVisibleChanged();
195     }
196 }
197
198 - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
199 {
200     QPointF p = fromCGPoint([[touches anyObject] locationInView:m_viewController.view]);
201     if (m_keyboardRect.contains(p))
202         m_context->hideInputPanel();
203
204     [super touchesMoved:touches withEvent:event];
205 }
206
207 @end
208
209 QIOSInputContext::QIOSInputContext()
210     : QPlatformInputContext()
211     , m_keyboardListener([[QIOSKeyboardListener alloc] initWithQIOSInputContext:this])
212     , m_focusView(0)
213     , m_hasPendingHideRequest(false)
214 {
215     if (isQtApplication())
216         connect(qGuiApp->inputMethod(), &QInputMethod::cursorRectangleChanged, this, &QIOSInputContext::cursorRectangleChanged);
217     connect(qGuiApp, &QGuiApplication::focusWindowChanged, this, &QIOSInputContext::focusWindowChanged);
218 }
219
220 QIOSInputContext::~QIOSInputContext()
221 {
222     [m_keyboardListener release];
223     [m_focusView release];
224 }
225
226 QRectF QIOSInputContext::keyboardRect() const
227 {
228     return m_keyboardListener->m_keyboardRect;
229 }
230
231 void QIOSInputContext::showInputPanel()
232 {
233     // Documentation tells that one should call (and recall, if necessary) becomeFirstResponder/resignFirstResponder
234     // to show/hide the keyboard. This is slightly inconvenient, since there exist no API to get the current first
235     // responder. Rather than searching for it from the top, we let the active QIOSWindow tell us which view to use.
236     // Note that Qt will forward keyevents to whichever QObject that needs it, regardless of which UIView the input
237     // actually came from. So in this respect, we're undermining iOS' responder chain.
238     m_hasPendingHideRequest = false;
239     [m_focusView becomeFirstResponder];
240 }
241
242 void QIOSInputContext::hideInputPanel()
243 {
244     // Delay hiding the keyboard for cases where the user is transferring focus between
245     // 'line edits'. In that case the 'line edit' that lost focus will close the input
246     // panel, just to see that the new 'line edit' will open it again:
247     m_hasPendingHideRequest = true;
248     dispatch_async(dispatch_get_main_queue(), ^{
249         if (m_hasPendingHideRequest)
250             [m_focusView resignFirstResponder];
251     });
252 }
253
254 bool QIOSInputContext::isInputPanelVisible() const
255 {
256     return m_keyboardListener->m_keyboardVisible;
257 }
258
259 void QIOSInputContext::setFocusObject(QObject *focusObject)
260 {
261     if (!focusObject || !m_focusView || !m_focusView.isFirstResponder) {
262         scroll(0);
263         return;
264     }
265
266     reset();
267
268     if (m_keyboardListener->m_keyboardVisibleAndDocked)
269         scrollToCursor();
270 }
271
272 void QIOSInputContext::focusWindowChanged(QWindow *focusWindow)
273 {
274     QUIView *view = focusWindow ? reinterpret_cast<QUIView *>(focusWindow->handle()->winId()) : 0;
275     if ([m_focusView isFirstResponder])
276         [view becomeFirstResponder];
277     [m_focusView release];
278     m_focusView = [view retain];
279
280     if (view.window != m_keyboardListener->m_viewController.view)
281         scroll(0);
282 }
283
284 void QIOSInputContext::cursorRectangleChanged()
285 {
286     if (!m_keyboardListener->m_keyboardVisibleAndDocked)
287         return;
288
289     // Check if the cursor has changed position inside the input item. Since
290     // qApp->inputMethod()->cursorRectangle() will also change when the input item
291     // itself moves, we need to ask the focus object for ImCursorRectangle:
292     static QPoint prevCursor;
293     QInputMethodQueryEvent queryEvent(Qt::ImCursorRectangle);
294     QCoreApplication::sendEvent(qApp->focusObject(), &queryEvent);
295     QPoint cursor = queryEvent.value(Qt::ImCursorRectangle).toRect().topLeft();
296     if (cursor != prevCursor)
297         scrollToCursor();
298     prevCursor = cursor;
299 }
300
301 void QIOSInputContext::scrollToCursor()
302 {
303     if (!isQtApplication() || !m_focusView)
304         return;
305
306     UIView *view = m_keyboardListener->m_viewController.view;
307     if (view.window != m_focusView.window)
308         return;
309
310     const int margin = 20;
311     QRectF translatedCursorPos = qApp->inputMethod()->cursorRectangle();
312     translatedCursorPos.translate(m_focusView.qwindow->geometry().topLeft());
313     qreal keyboardY = m_keyboardListener->m_keyboardEndRect.y();
314     int statusBarY = qGuiApp->primaryScreen()->availableGeometry().y();
315
316     scroll((translatedCursorPos.bottomLeft().y() < keyboardY - margin) ? 0
317         : qMin(view.bounds.size.height - keyboardY, translatedCursorPos.y() - statusBarY - margin));
318 }
319
320 void QIOSInputContext::scroll(int y)
321 {
322     // Scroll the view the same way a UIScrollView
323     // works: by changing bounds.origin:
324     UIView *view = m_keyboardListener->m_viewController.view;
325     if (y == view.bounds.origin.y)
326         return;
327
328     CGRect newBounds = view.bounds;
329     newBounds.origin.y = y;
330     QPointer<QIOSInputContext> self = this;
331     [UIView animateWithDuration:m_keyboardListener->m_duration delay:0
332         options:m_keyboardListener->m_curve
333         animations:^{ view.bounds = newBounds; }
334         completion:^(BOOL){
335             if (self)
336                 [m_keyboardListener handleKeyboardRectChanged];
337         }
338     ];
339 }
340
341 void QIOSInputContext::update(Qt::InputMethodQueries query)
342 {
343     [m_focusView updateInputMethodWithQuery:query];
344 }
345
346 void QIOSInputContext::reset()
347 {
348     // Since the call to reset will cause a 'keyboardWillHide'
349     // notification to be sendt, we block keyboard nofifications to avoid artifacts:
350     m_keyboardListener->m_ignoreKeyboardChanges = true;
351     [m_focusView reset];
352     m_keyboardListener->m_ignoreKeyboardChanges = false;
353 }
354
355 void QIOSInputContext::commit()
356 {
357     [m_focusView commit];
358 }
359