1/****************************************************************************
2**
3** Copyright (C) 2016 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
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 The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/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 3 as published by the Free Software
20** Foundation and appearing in the file LICENSE.LGPL3 included in the
21** packaging of this file. Please review the following information to
22** ensure the GNU Lesser General Public License version 3 requirements
23** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24**
25** GNU General Public License Usage
26** Alternatively, this file may be used under the terms of the GNU
27** General Public License version 2.0 or (at your option) the GNU General
28** Public license version 3 or any later version approved by the KDE Free
29** Qt Foundation. The licenses are as published by the Free Software
30** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31** included in the packaging of this file. Please review the following
32** information to ensure the GNU General Public License requirements will
33** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34** https://www.gnu.org/licenses/gpl-3.0.html.
35**
36** $QT_END_LICENSE$
37**
38****************************************************************************/
39
40#include "qiosinputcontext.h"
41
42#import <UIKit/UIGestureRecognizerSubclass.h>
43
44#include "qiosglobal.h"
45#include "qiosintegration.h"
46#include "qiosscreen.h"
47#include "qiostextresponder.h"
48#include "qiosviewcontroller.h"
49#include "qioswindow.h"
50#include "quiview.h"
51
52#include <QtCore/private/qcore_mac_p.h>
53
54#include <QGuiApplication>
55#include <QtGui/private/qwindow_p.h>
56
57// -------------------------------------------------------------------------
58
59static QUIView *focusView()
60{
61    return qApp->focusWindow() ?
62        reinterpret_cast<QUIView *>(qApp->focusWindow()->winId()) : 0;
63}
64
65// -------------------------------------------------------------------------
66
67@interface QIOSLocaleListener : NSObject
68@end
69
70@implementation QIOSLocaleListener
71
72- (instancetype)init
73{
74    if (self = [super init]) {
75        NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
76        [notificationCenter addObserver:self
77            selector:@selector(localeDidChange:)
78            name:NSCurrentLocaleDidChangeNotification object:nil];
79    }
80
81    return self;
82}
83
84- (void)dealloc
85{
86    [[NSNotificationCenter defaultCenter] removeObserver:self];
87    [super dealloc];
88}
89
90- (void)localeDidChange:(NSNotification *)notification
91{
92    Q_UNUSED(notification);
93    QIOSInputContext::instance()->emitLocaleChanged();
94}
95
96@end
97
98// -------------------------------------------------------------------------
99
100@interface QIOSKeyboardListener : UIGestureRecognizer <UIGestureRecognizerDelegate>
101@property BOOL hasDeferredScrollToCursor;
102@end
103
104@implementation QIOSKeyboardListener {
105    QT_PREPEND_NAMESPACE(QIOSInputContext) *m_context;
106}
107
108- (instancetype)initWithQIOSInputContext:(QT_PREPEND_NAMESPACE(QIOSInputContext) *)context
109{
110    if (self = [super initWithTarget:self action:@selector(gestureStateChanged:)]) {
111
112        m_context = context;
113
114        self.hasDeferredScrollToCursor = NO;
115
116        // UIGestureRecognizer
117        self.enabled = NO;
118        self.cancelsTouchesInView = NO;
119        self.delaysTouchesEnded = NO;
120
121#ifndef Q_OS_TVOS
122        NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
123
124        [notificationCenter addObserver:self
125            selector:@selector(keyboardWillShow:)
126            name:UIKeyboardWillShowNotification object:nil];
127        [notificationCenter addObserver:self
128            selector:@selector(keyboardWillOrDidChange:)
129            name:UIKeyboardDidShowNotification object:nil];
130        [notificationCenter addObserver:self
131            selector:@selector(keyboardWillHide:)
132            name:UIKeyboardWillHideNotification object:nil];
133        [notificationCenter addObserver:self
134            selector:@selector(keyboardWillOrDidChange:)
135            name:UIKeyboardDidHideNotification object:nil];
136        [notificationCenter addObserver:self
137            selector:@selector(keyboardDidChangeFrame:)
138            name:UIKeyboardDidChangeFrameNotification object:nil];
139#endif
140    }
141
142    return self;
143}
144
145- (void)dealloc
146{
147    [[NSNotificationCenter defaultCenter] removeObserver:self];
148
149    [super dealloc];
150}
151
152// -------------------------------------------------------------------------
153
154- (void)keyboardWillShow:(NSNotification *)notification
155{
156    [self keyboardWillOrDidChange:notification];
157
158    UIResponder *firstResponder = [UIResponder currentFirstResponder];
159    if (![firstResponder isKindOfClass:[QIOSTextInputResponder class]])
160        return;
161
162    // Enable hide-keyboard gesture
163    self.enabled = YES;
164
165    m_context->scrollToCursor();
166}
167
168- (void)keyboardWillHide:(NSNotification *)notification
169{
170    [self keyboardWillOrDidChange:notification];
171
172    if (self.state != UIGestureRecognizerStateBegan) {
173        // Only disable the gesture if the hiding of the keyboard was not caused by it.
174        // Otherwise we need to await the final touchEnd callback for doing some clean-up.
175        self.enabled = NO;
176    }
177    m_context->scroll(0);
178}
179
180- (void)keyboardDidChangeFrame:(NSNotification *)notification
181{
182    [self keyboardWillOrDidChange:notification];
183
184    // If the keyboard was visible and docked from before, this is just a geometry
185    // change (normally caused by an orientation change). In that case, update scroll:
186    if (m_context->isInputPanelVisible())
187        m_context->scrollToCursor();
188}
189
190- (void)keyboardWillOrDidChange:(NSNotification *)notification
191{
192    m_context->updateKeyboardState(notification);
193}
194
195// -------------------------------------------------------------------------
196
197- (BOOL)canPreventGestureRecognizer:(UIGestureRecognizer *)other
198{
199    Q_UNUSED(other);
200    return NO;
201}
202
203- (BOOL)canBePreventedByGestureRecognizer:(UIGestureRecognizer *)other
204{
205    Q_UNUSED(other);
206    return NO;
207}
208
209- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
210{
211    [super touchesBegan:touches withEvent:event];
212
213    Q_ASSERT(m_context->isInputPanelVisible());
214
215    if ([touches count] != 1)
216        self.state = UIGestureRecognizerStateFailed;
217}
218
219- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
220{
221    [super touchesMoved:touches withEvent:event];
222
223    if (self.state != UIGestureRecognizerStatePossible)
224        return;
225
226    CGPoint touchPoint = [[touches anyObject] locationInView:self.view];
227    if (CGRectContainsPoint(m_context->keyboardState().keyboardEndRect, touchPoint))
228        self.state = UIGestureRecognizerStateBegan;
229}
230
231- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
232{
233    [super touchesEnded:touches withEvent:event];
234
235    [self touchesEndedOrCancelled];
236}
237
238- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
239{
240    [super touchesCancelled:touches withEvent:event];
241
242    [self touchesEndedOrCancelled];
243}
244
245- (void)touchesEndedOrCancelled
246{
247    // Defer final state change until next runloop iteration, so that Qt
248    // has a chance to process the final touch events first, before we eg.
249    // scroll the view.
250    dispatch_async(dispatch_get_main_queue (), ^{
251        // iOS will transition from began to changed by itself
252        Q_ASSERT(self.state != UIGestureRecognizerStateBegan);
253
254        if (self.state == UIGestureRecognizerStateChanged)
255            self.state = UIGestureRecognizerStateEnded;
256        else
257            self.state = UIGestureRecognizerStateFailed;
258    });
259}
260
261- (void)gestureStateChanged:(id)sender
262{
263    Q_UNUSED(sender);
264
265    if (self.state == UIGestureRecognizerStateBegan) {
266        qImDebug("hide keyboard gesture was triggered");
267        UIResponder *firstResponder = [UIResponder currentFirstResponder];
268        Q_ASSERT([firstResponder isKindOfClass:[QIOSTextInputResponder class]]);
269        [firstResponder resignFirstResponder];
270    }
271}
272
273- (void)reset
274{
275    [super reset];
276
277    if (!m_context->isInputPanelVisible()) {
278        qImDebug("keyboard was hidden, disabling hide-keyboard gesture");
279        self.enabled = NO;
280    } else {
281        qImDebug("gesture completed without triggering");
282        if (self.hasDeferredScrollToCursor) {
283            qImDebug("applying deferred scroll to cursor");
284            m_context->scrollToCursor();
285        }
286    }
287
288    self.hasDeferredScrollToCursor = NO;
289}
290
291@end
292
293// -------------------------------------------------------------------------
294
295QT_BEGIN_NAMESPACE
296
297Qt::InputMethodQueries ImeState::update(Qt::InputMethodQueries properties)
298{
299    if (!properties)
300        return 0;
301
302    QInputMethodQueryEvent newState(properties);
303
304    // Update the focus object that the new state is based on
305    focusObject = qApp ? qApp->focusObject() : 0;
306
307    if (focusObject)
308        QCoreApplication::sendEvent(focusObject, &newState);
309
310    Qt::InputMethodQueries updatedProperties;
311    for (uint i = 0; i < (sizeof(Qt::ImQueryAll) * CHAR_BIT); ++i) {
312        if (Qt::InputMethodQuery property = Qt::InputMethodQuery(int(properties & (1 << i)))) {
313            if (newState.value(property) != currentState.value(property)) {
314                updatedProperties |= property;
315                currentState.setValue(property, newState.value(property));
316            }
317        }
318    }
319
320    return updatedProperties;
321}
322
323// -------------------------------------------------------------------------
324
325QIOSInputContext *QIOSInputContext::instance()
326{
327    return static_cast<QIOSInputContext *>(QIOSIntegration::instance()->inputContext());
328}
329
330QIOSInputContext::QIOSInputContext()
331    : QPlatformInputContext()
332    , m_localeListener([QIOSLocaleListener new])
333    , m_keyboardHideGesture([[QIOSKeyboardListener alloc] initWithQIOSInputContext:this])
334    , m_textResponder(0)
335{
336    if (isQtApplication()) {
337        QIOSScreen *iosScreen = static_cast<QIOSScreen*>(QGuiApplication::primaryScreen()->handle());
338        [iosScreen->uiWindow() addGestureRecognizer:m_keyboardHideGesture];
339    }
340
341    connect(qGuiApp, &QGuiApplication::focusWindowChanged, this, &QIOSInputContext::focusWindowChanged);
342}
343
344QIOSInputContext::~QIOSInputContext()
345{
346    [m_localeListener release];
347    [m_keyboardHideGesture.view removeGestureRecognizer:m_keyboardHideGesture];
348    [m_keyboardHideGesture release];
349
350    [m_textResponder release];
351}
352
353void QIOSInputContext::showInputPanel()
354{
355    // No-op, keyboard controlled fully by platform based on focus
356    qImDebug("can't show virtual keyboard without a focus object, ignoring");
357}
358
359void QIOSInputContext::hideInputPanel()
360{
361    if (![m_textResponder isFirstResponder]) {
362        qImDebug("QIOSTextInputResponder is not first responder, ignoring");
363        return;
364    }
365
366    if (qGuiApp->focusObject() != m_imeState.focusObject) {
367        qImDebug("current focus object does not match IM state, likely hiding from focusOut event, so ignoring");
368        return;
369    }
370
371    qImDebug("hiding VKB as requested by QInputMethod::hide()");
372    [m_textResponder resignFirstResponder];
373}
374
375void QIOSInputContext::clearCurrentFocusObject()
376{
377    if (QWindow *focusWindow = qApp->focusWindow())
378        static_cast<QWindowPrivate *>(QObjectPrivate::get(focusWindow))->clearFocusObject();
379}
380
381// -------------------------------------------------------------------------
382
383void QIOSInputContext::updateKeyboardState(NSNotification *notification)
384{
385#ifdef Q_OS_TVOS
386    Q_UNUSED(notification);
387#else
388    static CGRect currentKeyboardRect = CGRectZero;
389
390    KeyboardState previousState = m_keyboardState;
391
392    if (notification) {
393        NSDictionary *userInfo = [notification userInfo];
394
395        CGRect frameBegin = [[userInfo objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue];
396        CGRect frameEnd = [[userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
397
398        bool atEndOfKeyboardTransition = [notification.name rangeOfString:@"Did"].location != NSNotFound;
399
400        currentKeyboardRect = atEndOfKeyboardTransition ? frameEnd : frameBegin;
401
402        // The isInputPanelVisible() property is based on whether or not the virtual keyboard
403        // is visible on screen, and does not follow the logic of the iOS WillShow and WillHide
404        // notifications which are not emitted for undocked keyboards, and are buggy when dealing
405        // with input-accesosory-views. The reason for using frameEnd here (the future state),
406        // instead of the current state reflected in frameBegin, is that QInputMethod::isVisible()
407        // is documented to reflect the future state in the case of animated transitions.
408        m_keyboardState.keyboardVisible = CGRectIntersectsRect(frameEnd, [UIScreen mainScreen].bounds);
409
410        // Used for auto-scroller, and will be used for animation-signal in the future
411        m_keyboardState.keyboardEndRect = frameEnd;
412
413        if (m_keyboardState.animationCurve < 0) {
414            // We only set the animation curve the first time it has a valid value, since iOS will sometimes report
415            // an invalid animation curve even if the keyboard is animating, and we don't want to overwrite the
416            // curve in that case.
417            m_keyboardState.animationCurve = UIViewAnimationCurve([[userInfo objectForKey:UIKeyboardAnimationCurveUserInfoKey] integerValue]);
418        }
419
420        m_keyboardState.animationDuration = [[userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];
421        m_keyboardState.keyboardAnimating = m_keyboardState.animationDuration > 0 && !atEndOfKeyboardTransition;
422
423        qImDebug() << qPrintable(QString::fromNSString(notification.name)) << "from" << QRectF::fromCGRect(frameBegin) << "to" << QRectF::fromCGRect(frameEnd)
424                   << "(curve =" << m_keyboardState.animationCurve << "duration =" << m_keyboardState.animationDuration << "s)";
425    } else {
426        qImDebug("No notification to update keyboard state based on, just updating keyboard rect");
427    }
428
429    if (!focusView() || CGRectIsEmpty(currentKeyboardRect))
430        m_keyboardState.keyboardRect = QRectF();
431    else // QInputmethod::keyboardRectangle() is documented to be in window coordinates.
432        m_keyboardState.keyboardRect = QRectF::fromCGRect([focusView() convertRect:currentKeyboardRect fromView:nil]);
433
434    // Emit for all changed properties
435    if (m_keyboardState.keyboardVisible != previousState.keyboardVisible)
436        emitInputPanelVisibleChanged();
437    if (m_keyboardState.keyboardAnimating != previousState.keyboardAnimating)
438        emitAnimatingChanged();
439    if (m_keyboardState.keyboardRect != previousState.keyboardRect)
440        emitKeyboardRectChanged();
441#endif
442}
443
444bool QIOSInputContext::isInputPanelVisible() const
445{
446    return m_keyboardState.keyboardVisible;
447}
448
449bool QIOSInputContext::isAnimating() const
450{
451    return m_keyboardState.keyboardAnimating;
452}
453
454QRectF QIOSInputContext::keyboardRect() const
455{
456    return m_keyboardState.keyboardRect;
457}
458
459// -------------------------------------------------------------------------
460
461UIView *QIOSInputContext::scrollableRootView()
462{
463    if (!m_keyboardHideGesture.view)
464        return 0;
465
466    UIWindow *window = static_cast<UIWindow*>(m_keyboardHideGesture.view);
467    if (![window.rootViewController isKindOfClass:[QIOSViewController class]])
468        return 0;
469
470    return window.rootViewController.view;
471}
472
473void QIOSInputContext::scrollToCursor()
474{
475    if (!isQtApplication())
476        return;
477
478    if (m_keyboardHideGesture.state == UIGestureRecognizerStatePossible && m_keyboardHideGesture.numberOfTouches == 1) {
479        // Don't scroll to the cursor if the user is touching the screen and possibly
480        // trying to trigger the hide-keyboard gesture.
481        qImDebug("deferring scrolling to cursor as we're still waiting for a possible gesture");
482        m_keyboardHideGesture.hasDeferredScrollToCursor = YES;
483        return;
484    }
485
486    UIView *rootView = scrollableRootView();
487    if (!rootView)
488        return;
489
490    if (!focusView())
491        return;
492
493    if (rootView.window != focusView().window)
494        return;
495
496    // We only support auto-scroll for docked keyboards for now, so make sure that's the case
497    if (CGRectGetMaxY(m_keyboardState.keyboardEndRect) != CGRectGetMaxY([UIScreen mainScreen].bounds)) {
498        qImDebug("Keyboard not docked, ignoring request to scroll to reveal cursor");
499        return;
500    }
501
502    QWindow *focusWindow = qApp->focusWindow();
503    QRect cursorRect = qApp->inputMethod()->cursorRectangle().translated(focusWindow->geometry().topLeft()).toRect();
504
505    // We explicitly ask for the geometry of the screen instead of the availableGeometry,
506    // as we hide the status bar when scrolling the screen, so the available geometry will
507    // include the space taken by the status bar at the moment.
508    QRect screenGeometry = focusWindow->screen()->geometry();
509
510    if (!cursorRect.isNull()) {
511         // Add some padding so that the cursor does not end up directly above the keyboard
512        static const int kCursorRectPadding = 20;
513        cursorRect.adjust(0, -kCursorRectPadding, 0, kCursorRectPadding);
514
515        // Make sure the cursor rect is still within the screen geometry after padding
516        cursorRect &= screenGeometry;
517    }
518
519    QRect keyboardGeometry = QRectF::fromCGRect(m_keyboardState.keyboardEndRect).toRect();
520    QRect availableGeometry = (QRegion(screenGeometry) - keyboardGeometry).boundingRect();
521
522    if (!cursorRect.isNull() && !availableGeometry.contains(cursorRect)) {
523        qImDebug() << "cursor rect" << cursorRect << "not fully within" << availableGeometry;
524        int scrollToCenter = -(availableGeometry.center() - cursorRect.center()).y();
525        int scrollToBottom = focusWindow->screen()->geometry().bottom() - availableGeometry.bottom();
526        scroll(qMin(scrollToCenter, scrollToBottom));
527    } else {
528        scroll(0);
529    }
530}
531
532void QIOSInputContext::scroll(int y)
533{
534    Q_ASSERT(y >= 0);
535
536    UIView *rootView = scrollableRootView();
537    if (!rootView)
538        return;
539
540    if (qt_apple_isApplicationExtension()) {
541        qWarning() << "can't scroll root view in application extension";
542        return;
543    }
544
545    CATransform3D translationTransform = CATransform3DMakeTranslation(0.0, -y, 0.0);
546    if (CATransform3DEqualToTransform(translationTransform, rootView.layer.sublayerTransform))
547        return;
548
549    qImDebug() << "scrolling root view to y =" << -y;
550
551    QPointer<QIOSInputContext> self = this;
552    [UIView animateWithDuration:m_keyboardState.animationDuration delay:0
553        options:(m_keyboardState.animationCurve << 16) | UIViewAnimationOptionBeginFromCurrentState
554        animations:^{
555            // The sublayerTransform property of CALayer is not implicitly animated for a
556            // layer-backed view, even inside a UIView animation block, so we need to set up
557            // an explicit CoreAnimation animation. Since there is no predefined media timing
558            // function that matches the custom keyboard animation curve we cheat by asking
559            // the view for an animation of another property, which will give us an animation
560            // that matches the parameters we passed to [UIView animateWithDuration] above.
561            // The reason we ask for the animation of 'backgroundColor' is that it's a simple
562            // property that will not return a compound animation, like eg. bounds will.
563            NSObject *action = (NSObject*)[rootView actionForLayer:rootView.layer forKey:@"backgroundColor"];
564
565            CABasicAnimation *animation;
566            if ([action isKindOfClass:[CABasicAnimation class]]) {
567                animation = static_cast<CABasicAnimation*>(action);
568                animation.keyPath = @"sublayerTransform"; // Instead of backgroundColor
569            } else {
570                animation = [CABasicAnimation animationWithKeyPath:@"sublayerTransform"];
571            }
572
573            CATransform3D currentSublayerTransform = static_cast<CALayer *>([rootView.layer presentationLayer]).sublayerTransform;
574            animation.fromValue = [NSValue valueWithCATransform3D:currentSublayerTransform];
575            animation.toValue = [NSValue valueWithCATransform3D:translationTransform];
576            [rootView.layer addAnimation:animation forKey:@"AnimateSubLayerTransform"];
577            rootView.layer.sublayerTransform = translationTransform;
578
579            bool keyboardScrollIsActive = y != 0;
580
581            // Raise all known windows to above the status-bar if we're scrolling the screen,
582            // while keeping the relative window level between the windows the same.
583            NSArray<UIWindow *> *applicationWindows = [qt_apple_sharedApplication() windows];
584            static QHash<UIWindow *, UIWindowLevel> originalWindowLevels;
585            for (UIWindow *window in applicationWindows) {
586                if (keyboardScrollIsActive && !originalWindowLevels.contains(window))
587                    originalWindowLevels.insert(window, window.windowLevel);
588
589#ifndef Q_OS_TVOS
590                UIWindowLevel windowLevelAdjustment = keyboardScrollIsActive ? UIWindowLevelStatusBar : 0;
591#else
592                UIWindowLevel windowLevelAdjustment = 0;
593#endif
594                window.windowLevel = originalWindowLevels.value(window) + windowLevelAdjustment;
595
596                if (!keyboardScrollIsActive)
597                    originalWindowLevels.remove(window);
598            }
599        }
600        completion:^(BOOL){
601            if (self) {
602                // Scrolling the root view results in the keyboard being moved
603                // relative to the focus window, so we need to re-evaluate the
604                // keyboard rectangle.
605                updateKeyboardState();
606            }
607        }
608    ];
609}
610
611// -------------------------------------------------------------------------
612
613void QIOSInputContext::setFocusObject(QObject *focusObject)
614{
615    Q_UNUSED(focusObject);
616
617    qImDebug() << "new focus object =" << focusObject;
618
619    if (QPlatformInputContext::inputMethodAccepted()
620            && m_keyboardHideGesture.state == UIGestureRecognizerStateChanged) {
621        // A new focus object may be set as part of delivering touch events to
622        // application during the hide-keyboard gesture, but we don't want that
623        // to result in a new object getting focus and bringing the keyboard up
624        // again.
625        qImDebug() << "clearing focus object" << focusObject << "as hide-keyboard gesture is active";
626        clearCurrentFocusObject();
627        return;
628    } else if (focusObject == m_imeState.focusObject) {
629        qImDebug("same focus object as last update, skipping reset");
630        return;
631    }
632
633    reset();
634
635    if (isInputPanelVisible())
636        scrollToCursor();
637}
638
639void QIOSInputContext::focusWindowChanged(QWindow *focusWindow)
640{
641    Q_UNUSED(focusWindow);
642
643    qImDebug() << "new focus window =" << focusWindow;
644
645    reset();
646
647    // The keyboard rectangle depend on the focus window, so
648    // we need to re-evaluate the keyboard state.
649    updateKeyboardState();
650
651    if (isInputPanelVisible())
652        scrollToCursor();
653}
654
655/*!
656    Called by the input item to inform the platform input methods when there has been
657    state changes in editor's input method query attributes. When calling the function
658    \a queries parameter has to be used to tell what has changes, which input method
659    can use to make queries for attributes it's interested with QInputMethodQueryEvent.
660*/
661void QIOSInputContext::update(Qt::InputMethodQueries updatedProperties)
662{
663    qImDebug() << "fw =" << qApp->focusWindow() << "fo =" << qApp->focusObject();
664
665    // Changes to the focus object should always result in a call to setFocusObject(),
666    // triggering a reset() which will update all the properties based on the new
667    // focus object. We try to detect code paths that fail this assertion and smooth
668    // over the situation by doing a manual update of the focus object.
669    if (qApp->focusObject() != m_imeState.focusObject && updatedProperties != Qt::ImQueryAll) {
670        qWarning() << "stale focus object" << m_imeState.focusObject << ", doing manual update";
671        setFocusObject(qApp->focusObject());
672        return;
673    }
674
675    // Mask for properties that we are interested in and see if any of them changed
676    updatedProperties &= (Qt::ImEnabled | Qt::ImHints | Qt::ImQueryInput | Qt::ImEnterKeyType | Qt::ImPlatformData);
677
678    // Perform update first, so we can trust the value of inputMethodAccepted()
679    Qt::InputMethodQueries changedProperties = m_imeState.update(updatedProperties);
680
681    if (inputMethodAccepted()) {
682        if (!m_textResponder || [m_textResponder needsKeyboardReconfigure:changedProperties]) {
683            qImDebug("creating new text responder");
684            [m_textResponder autorelease];
685            m_textResponder = [[QIOSTextInputResponder alloc] initWithInputContext:this];
686        } else {
687            qImDebug("no need to reconfigure keyboard, just notifying input delegate");
688            [m_textResponder notifyInputDelegate:changedProperties];
689        }
690
691        if (![m_textResponder isFirstResponder]) {
692            qImDebug("IM enabled, making text responder first responder");
693            [m_textResponder becomeFirstResponder];
694        }
695
696        if (changedProperties & Qt::ImCursorRectangle)
697            scrollToCursor();
698    } else if ([m_textResponder isFirstResponder]) {
699        qImDebug("IM not enabled, resigning text responder as first responder");
700        [m_textResponder resignFirstResponder];
701    }
702}
703
704bool QIOSInputContext::inputMethodAccepted() const
705{
706    // The IM enablement state is based on the last call to update()
707    bool lastKnownImEnablementState = m_imeState.currentState.value(Qt::ImEnabled).toBool();
708
709#if !defined(QT_NO_DEBUG)
710    // QPlatformInputContext keeps a cached value of the current IM enablement state that is
711    // updated by QGuiApplication when the current focus object changes, or by QInputMethod's
712    // update() function. If the focus object changes, but the change is not propagated as
713    // a signal to QGuiApplication due to bugs in the widget/graphicsview/qml stack, we'll
714    // end up with a stale value for QPlatformInputContext::inputMethodAccepted(). To be on
715    // the safe side we always use our own cached value to decide if IM is enabled, and try
716    // to detect the case where the two values are out of sync.
717    if (lastKnownImEnablementState != QPlatformInputContext::inputMethodAccepted())
718        qWarning("QPlatformInputContext::inputMethodAccepted() does not match actual focus object IM enablement!");
719#endif
720
721    return lastKnownImEnablementState;
722}
723
724/*!
725    Called by the input item to reset the input method state.
726*/
727void QIOSInputContext::reset()
728{
729    qImDebug("updating Qt::ImQueryAll and unmarking text");
730
731    update(Qt::ImQueryAll);
732
733    [m_textResponder setMarkedText:@"" selectedRange:NSMakeRange(0, 0)];
734    [m_textResponder notifyInputDelegate:Qt::ImQueryInput];
735}
736
737/*!
738    Commits the word user is currently composing to the editor. The function is
739    mostly needed by the input methods with text prediction features and by the
740    methods where the script used for typing characters is different from the
741    script that actually gets appended to the editor. Any kind of action that
742    interrupts the text composing needs to flush the composing state by calling the
743    commit() function, for example when the cursor is moved elsewhere.
744*/
745void QIOSInputContext::commit()
746{
747    qImDebug("unmarking text");
748
749    [m_textResponder unmarkText];
750    [m_textResponder notifyInputDelegate:Qt::ImSurroundingText];
751}
752
753QLocale QIOSInputContext::locale() const
754{
755    return QLocale(QString::fromNSString([[NSLocale currentLocale] objectForKey:NSLocaleIdentifier]));
756}
757
758QT_END_NAMESPACE
759