7#import <UIKit/UIGestureRecognizerSubclass.h>
17#include <QtCore/private/qcore_mac_p.h>
19#include <QGuiApplication>
20#include <QtGui/private/qwindow_p.h>
22#include <QtCore/qpointer.h>
26static QUIView *focusView()
28 return qApp->focusWindow() ?
29 reinterpret_cast<QUIView *>(qApp->focusWindow()->winId()) : 0;
34@interface QIOSLocaleListener : NSObject
37@implementation QIOSLocaleListener
41 if (self = [super init]) {
42 NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
43 [notificationCenter addObserver:self
44 selector:@selector(localeDidChange:)
45 name:NSCurrentLocaleDidChangeNotification object:nil];
53 [[NSNotificationCenter defaultCenter] removeObserver:self];
57- (
void)localeDidChange:(NSNotification *)notification
59 Q_UNUSED(notification);
60 QIOSInputContext::instance()->emitLocaleChanged();
67@interface QIOSKeyboardListener : UIGestureRecognizer <UIGestureRecognizerDelegate>
68@property BOOL hasDeferredScrollToCursor;
71@implementation QIOSKeyboardListener {
72 QT_PREPEND_NAMESPACE(QIOSInputContext) *m_context;
75- (instancetype)initWithQIOSInputContext:(QT_PREPEND_NAMESPACE(QIOSInputContext) *)context
77 if (self = [super initWithTarget:self action:@selector(gestureStateChanged:)]) {
81 self.hasDeferredScrollToCursor = NO;
85 self.cancelsTouchesInView = NO;
86 self.delaysTouchesEnded = NO;
89 NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
91 [notificationCenter addObserver:self
92 selector:@selector(keyboardWillShow:)
93 name:UIKeyboardWillShowNotification object:nil];
94 [notificationCenter addObserver:self
95 selector:@selector(keyboardWillOrDidChange:)
96 name:UIKeyboardDidShowNotification object:nil];
97 [notificationCenter addObserver:self
98 selector:@selector(keyboardWillHide:)
99 name:UIKeyboardWillHideNotification object:nil];
100 [notificationCenter addObserver:self
101 selector:@selector(keyboardWillOrDidChange:)
102 name:UIKeyboardDidHideNotification object:nil];
103 [notificationCenter addObserver:self
104 selector:@selector(keyboardDidChangeFrame:)
105 name:UIKeyboardDidChangeFrameNotification object:nil];
114 [[NSNotificationCenter defaultCenter] removeObserver:self];
121- (
void)keyboardWillShow:(NSNotification *)notification
123 [self keyboardWillOrDidChange:notification];
125 UIResponder *firstResponder = [UIResponder qt_currentFirstResponder];
126 if (![firstResponder isKindOfClass:[QIOSTextInputResponder
class]])
130 self.enabled = m_context->isInputPanelVisible();
132 m_context->scrollToCursor();
135- (
void)keyboardWillHide:(NSNotification *)notification
137 [self keyboardWillOrDidChange:notification];
139 if (self.state != UIGestureRecognizerStateBegan) {
144 m_context->scroll(0);
147- (
void)keyboardDidChangeFrame:(NSNotification *)notification
149 [self keyboardWillOrDidChange:notification];
153 if (m_context->isInputPanelVisible())
154 m_context->scrollToCursor();
157- (
void)keyboardWillOrDidChange:(NSNotification *)notification
159 m_context->updateKeyboardState(notification);
164- (BOOL)canPreventGestureRecognizer:(UIGestureRecognizer *)other
170- (BOOL)canBePreventedByGestureRecognizer:(UIGestureRecognizer *)other
176- (
void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
178 [super touchesBegan:touches withEvent:event];
180 if (!m_context->isInputPanelVisible()) {
181 qImDebug(
"keyboard was hidden by sliding it down, disabling hide-keyboard gesture");
186 if ([touches count] != 1)
187 self.state = UIGestureRecognizerStateFailed;
190- (
void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
192 [super touchesMoved:touches withEvent:event];
194 if (self.state != UIGestureRecognizerStatePossible)
197 CGPoint touchPoint = [[touches anyObject] locationInView:self.view];
198 if (CGRectContainsPoint(m_context->keyboardState().keyboardEndRect, touchPoint))
199 self.state = UIGestureRecognizerStateBegan;
202- (
void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
204 [super touchesEnded:touches withEvent:event];
206 [self touchesEndedOrCancelled];
209- (
void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
211 [super touchesCancelled:touches withEvent:event];
213 [self touchesEndedOrCancelled];
216- (
void)touchesEndedOrCancelled
221 dispatch_async(dispatch_get_main_queue (), ^{
223 Q_ASSERT(self.state != UIGestureRecognizerStateBegan);
225 if (self.state == UIGestureRecognizerStateChanged)
226 self.state = UIGestureRecognizerStateEnded;
228 self.state = UIGestureRecognizerStateFailed;
232- (
void)gestureStateChanged:(id)sender
236 if (self.state == UIGestureRecognizerStateBegan) {
237 qImDebug(
"hide keyboard gesture was triggered");
238 UIResponder *firstResponder = [UIResponder qt_currentFirstResponder];
239 Q_ASSERT([firstResponder isKindOfClass:[QIOSTextInputResponder
class]]);
240 [firstResponder resignFirstResponder];
248 if (!m_context->isInputPanelVisible()) {
249 qImDebug(
"keyboard was hidden, disabling hide-keyboard gesture");
252 qImDebug(
"gesture completed without triggering");
253 if (self.hasDeferredScrollToCursor) {
254 qImDebug(
"applying deferred scroll to cursor");
255 m_context->scrollToCursor();
259 self.hasDeferredScrollToCursor = NO;
268Qt::InputMethodQueries ImeState::update(Qt::InputMethodQueries properties)
273 QInputMethodQueryEvent newState(properties);
276 focusObject = qApp ? qApp->focusObject() : 0;
279 QCoreApplication::sendEvent(focusObject, &newState);
281 Qt::InputMethodQueries updatedProperties;
282 for (uint i = 0; i < (
sizeof(Qt::ImQueryAll) * CHAR_BIT); ++i) {
283 if (Qt::InputMethodQuery property = Qt::InputMethodQuery(
int(properties & (1 << i)))) {
284 if (newState.value(property) != currentState.value(property)) {
285 updatedProperties |= property;
286 currentState.setValue(property, newState.value(property));
291 return updatedProperties;
302 : QPlatformInputContext()
303 , m_localeListener([QIOSLocaleListener
new])
304 , m_keyboardHideGesture([[QIOSKeyboardListener alloc] initWithQIOSInputContext:
this])
307 Q_ASSERT(!qGuiApp->focusWindow());
308 connect(qGuiApp, &QGuiApplication::focusWindowChanged,
this, &QIOSInputContext::focusWindowChanged);
313 [m_localeListener release];
314 [m_keyboardHideGesture.view removeGestureRecognizer:m_keyboardHideGesture];
315 [m_keyboardHideGesture release];
317 [m_textResponder release];
323 qImDebug(
"can't show virtual keyboard without a focus object, ignoring");
328 if (![m_textResponder isFirstResponder]) {
329 qImDebug(
"QIOSTextInputResponder is not first responder, ignoring");
333 if (qGuiApp->focusObject() != m_imeState.focusObject) {
334 qImDebug(
"current focus object does not match IM state, likely hiding from focusOut event, so ignoring");
338 qImDebug(
"hiding VKB as requested by QInputMethod::hide()");
339 [m_textResponder resignFirstResponder];
344 if (QWindow *focusWindow = qApp->focusWindow())
345 static_cast<QWindowPrivate *>(QObjectPrivate::get(focusWindow))->clearFocusObject();
352#if defined(Q_OS_TVOS) || defined(Q_OS_VISIONOS)
353 Q_UNUSED(notification);
355 static CGRect currentKeyboardRect = CGRectZero;
357 KeyboardState previousState = m_keyboardState;
360 NSDictionary *userInfo = [notification userInfo];
362 CGRect frameBegin = [[userInfo objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue];
363 CGRect frameEnd = [[userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
365 bool atEndOfKeyboardTransition = [notification.name rangeOfString:@
"Did"].location != NSNotFound;
367 currentKeyboardRect = atEndOfKeyboardTransition ? frameEnd : frameBegin;
375 m_keyboardState.keyboardVisible = !CGRectIsEmpty(UIScreen.mainScreen.bounds) &&
376 !CGRectIsEmpty(frameEnd) && CGRectIntersectsRect(frameEnd, UIScreen.mainScreen.bounds);
379 m_keyboardState.keyboardEndRect = frameEnd;
381 if (m_keyboardState.animationCurve < 0) {
385 m_keyboardState.animationCurve = UIViewAnimationCurve([[userInfo objectForKey:UIKeyboardAnimationCurveUserInfoKey] integerValue]);
388 m_keyboardState.animationDuration = [[userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];
389 m_keyboardState.keyboardAnimating = m_keyboardState.animationDuration > 0 && !atEndOfKeyboardTransition;
391 qImDebug() <<
qPrintable(QString::fromNSString(notification.name)) <<
"from" << QRectF::fromCGRect(frameBegin) <<
"to" << QRectF::fromCGRect(frameEnd)
392 <<
"(curve =" << m_keyboardState.animationCurve <<
"duration =" << m_keyboardState.animationDuration <<
"s)";
394 qImDebug(
"No notification to update keyboard state based on, just updating keyboard rect");
397 if (!focusView() || CGRectIsEmpty(currentKeyboardRect))
398 m_keyboardState.keyboardRect = QRectF();
400 m_keyboardState.keyboardRect = QRectF::fromCGRect([focusView() convertRect:currentKeyboardRect fromView:nil]);
403 if (m_keyboardState.keyboardVisible != previousState.keyboardVisible)
404 emitInputPanelVisibleChanged();
405 if (m_keyboardState.keyboardAnimating != previousState.keyboardAnimating)
406 emitAnimatingChanged();
407 if (m_keyboardState.keyboardRect != previousState.keyboardRect)
408 emitKeyboardRectChanged();
414 return m_keyboardState.keyboardVisible;
419 return m_keyboardState.keyboardAnimating;
424 return m_keyboardState.keyboardRect;
431 if (!m_keyboardHideGesture.view)
434 UIWindow *window =
static_cast<UIWindow*>(m_keyboardHideGesture.view);
435 if (![window.rootViewController isKindOfClass:[QIOSViewController
class]])
438 return window.rootViewController.view;
443#if !defined(Q_OS_VISIONOS)
447 if (m_keyboardHideGesture.state == UIGestureRecognizerStatePossible && m_keyboardHideGesture.numberOfTouches == 1) {
450 qImDebug(
"deferring scrolling to cursor as we're still waiting for a possible gesture");
451 m_keyboardHideGesture.hasDeferredScrollToCursor = YES;
455 UIView *rootView = scrollableRootView();
462 if (rootView.window != focusView().window)
466 if (CGRectGetMaxY(m_keyboardState.keyboardEndRect) != CGRectGetMaxY([UIScreen mainScreen].bounds)) {
467 qImDebug(
"Keyboard not docked, ignoring request to scroll to reveal cursor");
471 QPlatformWindow *focusWindow = qApp->focusWindow()->handle();
472 QRect windowCurosorRect = QPlatformInputContext::cursorRectangle().toRect();
473 QRect cursorRect = QRect(focusWindow->mapToGlobal(windowCurosorRect.topLeft()), windowCurosorRect.size());
478 QRect screenGeometry = focusWindow->screen()->geometry();
480 if (!cursorRect.isNull()) {
482 static const int kCursorRectPadding = 20;
483 cursorRect.adjust(0, -kCursorRectPadding, 0, kCursorRectPadding);
486 cursorRect &= screenGeometry;
489 QRect keyboardGeometry = QRectF::fromCGRect(m_keyboardState.keyboardEndRect).toRect();
490 QRect availableGeometry = (QRegion(screenGeometry) - keyboardGeometry).boundingRect();
492 if (!cursorRect.isNull() && !availableGeometry.contains(cursorRect)) {
493 qImDebug() <<
"cursor rect" << cursorRect <<
"not fully within" << availableGeometry;
494 int scrollToCenter = -(availableGeometry.center() - cursorRect.center()).y();
495 int scrollToBottom = focusWindow->screen()->geometry().bottom() - availableGeometry.bottom();
496 scroll(qMin(scrollToCenter, scrollToBottom)
);
507 UIView *rootView = scrollableRootView();
511 if (qt_apple_isApplicationExtension()) {
512 qWarning() <<
"can't scroll root view in application extension";
516 CATransform3D translationTransform = CATransform3DMakeTranslation(0.0, -y, 0.0);
517 if (CATransform3DEqualToTransform(translationTransform, rootView.layer.sublayerTransform))
520 qImDebug() <<
"scrolling root view to y =" << -y;
522 QPointer<QIOSInputContext> self =
this;
523 [UIView animateWithDuration:m_keyboardState.animationDuration delay:0
524 options:(m_keyboardState.animationCurve << 16) | UIViewAnimationOptionBeginFromCurrentState
534 NSObject *action = (NSObject*)[rootView actionForLayer:rootView.layer forKey:@
"backgroundColor"];
536 CABasicAnimation *animation;
537 if ([action isKindOfClass:[CABasicAnimation
class]]) {
538 animation =
static_cast<CABasicAnimation*>(action);
539 animation.keyPath = @
"sublayerTransform";
541 animation = [CABasicAnimation animationWithKeyPath:@
"sublayerTransform"];
544 CATransform3D currentSublayerTransform =
static_cast<CALayer *>([rootView.layer presentationLayer]).sublayerTransform;
545 animation.fromValue = [NSValue valueWithCATransform3D:currentSublayerTransform];
546 animation.toValue = [NSValue valueWithCATransform3D:translationTransform];
547 [rootView.layer addAnimation:animation forKey:@
"AnimateSubLayerTransform"];
548 rootView.layer.sublayerTransform = translationTransform;
550 bool keyboardScrollIsActive = y != 0;
554 NSArray<UIWindow *> *applicationWindows = [qt_apple_sharedApplication() windows];
555 static QHash<UIWindow *, UIWindowLevel> originalWindowLevels;
556 for (UIWindow *window in applicationWindows) {
557 if (keyboardScrollIsActive && !originalWindowLevels.contains(window))
558 originalWindowLevels.insert(window, window.windowLevel);
561 UIWindowLevel windowLevelAdjustment = keyboardScrollIsActive ? UIWindowLevelStatusBar : 0;
563 UIWindowLevel windowLevelAdjustment = 0;
565 window.windowLevel = originalWindowLevels.value(window) + windowLevelAdjustment;
567 if (!keyboardScrollIsActive)
568 originalWindowLevels.remove(window);
576 updateKeyboardState();
586 Q_UNUSED(focusObject);
588 qImDebug() <<
"new focus object =" << focusObject;
590 if (QPlatformInputContext::inputMethodAccepted()
591 && m_keyboardHideGesture.state == UIGestureRecognizerStateChanged) {
596 qImDebug() <<
"clearing focus object" << focusObject <<
"as hide-keyboard gesture is active";
599 }
else if (focusObject == m_imeState.focusObject) {
600 qImDebug(
"same focus object as last update, skipping reset");
612 qImDebug() <<
"new focus window =" << focusWindow;
617 [m_keyboardHideGesture.view removeGestureRecognizer:m_keyboardHideGesture];
618 [focusView().window addGestureRecognizer:m_keyboardHideGesture];
623 updateKeyboardState();
630
631
632
633
634
637 qImDebug() <<
"fw =" << qApp->focusWindow() <<
"fo =" << qApp->focusObject();
643 if (qApp->focusObject() != m_imeState.focusObject && updatedProperties != Qt::ImQueryAll) {
644 qCWarning(lcQpaInputMethods).verbosity(0) <<
"Updating input context" << updatedProperties
645 <<
"with last reported focus object" << m_imeState.focusObject
646 <<
"but qGuiApp reports" << qApp->focusObject()
647 <<
"which means someone failed to call QPlatformInputContext::setFocusObject()";
648 setFocusObject(qApp->focusObject());
653 updatedProperties &= (Qt::ImEnabled | Qt::ImHints | Qt::ImQueryInput | Qt::ImEnterKeyType | Qt::ImPlatformData | Qt::ImReadOnly);
656 Qt::InputMethodQueries changedProperties = m_imeState.update(updatedProperties);
658 const bool inputIsReadOnly = m_imeState.currentState.value(Qt::ImReadOnly).toBool();
661 if (!m_textResponder || [m_textResponder needsKeyboardReconfigure:changedProperties]) {
662 [m_textResponder autorelease];
663 if (inputIsReadOnly) {
664 qImDebug(
"creating new read-only text responder");
665 m_textResponder = [[QIOSTextResponder alloc] initWithInputContext:
this];
667 qImDebug(
"creating new read/write text responder");
668 m_textResponder = [[QIOSTextInputResponder alloc] initWithInputContext:
this];
671 qImDebug(
"no need to reconfigure keyboard, just notifying input delegate");
672 [m_textResponder notifyInputDelegate:changedProperties];
675 if (![m_textResponder isFirstResponder]) {
676 qImDebug(
"IM enabled, making text responder first responder");
677 [m_textResponder becomeFirstResponder];
680 if (changedProperties & Qt::ImCursorRectangle)
682 }
else if ([m_textResponder isFirstResponder]) {
683 qImDebug(
"IM not enabled, resigning text responder as first responder");
684 [m_textResponder resignFirstResponder];
691 bool lastKnownImEnablementState = m_imeState.currentState.value(Qt::ImEnabled).toBool();
693#if !defined(QT_NO_DEBUG)
701 if (lastKnownImEnablementState != QPlatformInputContext::inputMethodAccepted())
702 qWarning(
"QPlatformInputContext::inputMethodAccepted() does not match actual focus object IM enablement!");
705 return lastKnownImEnablementState;
709
710
713 qImDebug(
"releasing text responder");
726 const auto oldResponder = m_textResponder;
727 [m_textResponder reset];
728 [m_textResponder autorelease];
729 m_textResponder =
nullptr;
731 update(Qt::ImQueryAll);
736 if ([oldResponder isFirstResponder]) {
737 qImDebug(
"IM not enabled, resigning autoreleased text responder as first responder");
738 [oldResponder resignFirstResponder];
743
744
745
746
747
748
749
753 [m_textResponder commit];
758 return QLocale(QString::fromNSString([[NSLocale currentLocale] objectForKey:NSLocaleIdentifier]));
void setFocusObject(QObject *object) override
This virtual method gets called to notify updated focus to object.
void clearCurrentFocusObject()
QRectF keyboardRect() const override
This function can be reimplemented to return virtual keyboard rectangle in currently active window co...
void updateKeyboardState(NSNotification *notification=nullptr)
static QIOSInputContext * instance()
QLocale locale() const override
void reset() override
Method to be called when input method needs to be reset.
bool isAnimating() const override
This function can be reimplemented to return true whenever input method is animating shown or hidden.
void showInputPanel() override
Request to show input panel.
void focusWindowChanged(QWindow *focusWindow)
void hideInputPanel() override
Request to hide input panel.
bool isInputPanelVisible() const override
Returns input panel visibility status.
bool inputMethodAccepted() const
void update(Qt::InputMethodQueries) override
Notification on editor updates.
static QIOSIntegration * instance()
#define qPrintable(string)
connect(manager, &QNetworkAccessManager::finished, this, &MyClass::replyFinished)