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