Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qiostextresponder.mm
Go to the documentation of this file.
1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:significant reason:default
4
6
7#include "qiosglobal.h"
9#include "quiview.h"
10
11#include <QtCore/qscopedvaluerollback.h>
12
13#include <QtGui/qevent.h>
14#include <QtGui/qtextformat.h>
15#include <QtGui/private/qguiapplication_p.h>
16#include <QtGui/qpa/qplatformwindow.h>
17
18// -------------------------------------------------------------------------
19
20@interface QUITextPosition : UITextPosition
21
22@property (nonatomic) NSUInteger index;
23+ (instancetype)positionWithIndex:(NSUInteger)index;
24
25@end
26
27@implementation QUITextPosition
28
29+ (instancetype)positionWithIndex:(NSUInteger)index
30{
31 QUITextPosition *pos = [[QUITextPosition alloc] init];
32 pos.index = index;
33 return [pos autorelease];
34}
35
36@end
37
38// -------------------------------------------------------------------------
39
40@interface QUITextRange : UITextRange
41
42@property (nonatomic) NSRange range;
43+ (instancetype)rangeWithNSRange:(NSRange)range;
44
45@end
46
47@implementation QUITextRange
48
49+ (instancetype)rangeWithNSRange:(NSRange)nsrange
50{
51 QUITextRange *range = [[self alloc] init];
52 range.range = nsrange;
53 return [range autorelease];
54}
55
56- (UITextPosition *)start
57{
58 return [QUITextPosition positionWithIndex:self.range.location];
59}
60
61- (UITextPosition *)end
62{
63 return [QUITextPosition positionWithIndex:(self.range.location + self.range.length)];
64}
65
66- (NSRange)range
67{
68 return _range;
69}
70
71- (BOOL)isEmpty
72{
73 return (self.range.length == 0);
74}
75
76@end
77
78// -------------------------------------------------------------------------
79
80@interface WrapperView : UIView
81@end
82
83@implementation WrapperView
84
85- (instancetype)initWithView:(UIView *)view
86{
87 if (self = [self init]) {
88 [self addSubview:view];
89
90 self.autoresizingMask = view.autoresizingMask;
91
92 [self sizeToFit];
93 }
94
95 return self;
96}
97
98- (void)layoutSubviews
99{
100 UIView *view = [self.subviews firstObject];
101 view.frame = self.bounds;
102
103 // FIXME: During orientation changes the size and position
104 // of the view is not respected by the host view, even if
105 // we call sizeToFit or setNeedsLayout on the superview.
106}
107
108- (CGSize)sizeThatFits:(CGSize)size
109{
110 return [[self.subviews firstObject] sizeThatFits:size];
111}
112
113// By keeping the responder (QIOSTextInputResponder in this case)
114// retained, we ensure that all messages sent to the view during
115// its lifetime in a window hierarchy will be able to traverse the
116// responder chain.
117- (void)willMoveToWindow:(UIWindow *)window
118{
119 if (window)
120 [[self nextResponder] retain];
121 else
122 [[self nextResponder] autorelease];
123}
124
125@end
126
127// -------------------------------------------------------------------------
128
129@implementation QIOSTextResponder {
130 @public
131 QT_PREPEND_NAMESPACE(QIOSInputContext) *m_inputContext;
132 QT_PREPEND_NAMESPACE(QInputMethodQueryEvent) *m_configuredImeState;
133 BOOL m_inSendEventToFocusObject;
134}
135
136- (instancetype)initWithInputContext:(QT_PREPEND_NAMESPACE(QIOSInputContext) *)inputContext
137{
138 if (!(self = [self init]))
139 return self;
140
141 m_inputContext = inputContext;
142 m_configuredImeState = static_cast<QInputMethodQueryEvent*>(m_inputContext->imeState().currentState.clone());
143 m_inSendEventToFocusObject = NO;
144
145 return self;
146}
147
148- (void)dealloc
149{
150 delete m_configuredImeState;
151 [super dealloc];
152}
153
154- (QVariant)currentImeState:(Qt::InputMethodQuery)query
155{
156 return m_inputContext->imeState().currentState.value(query);
157}
158
159- (BOOL)canBecomeFirstResponder
160{
161 return YES;
162}
163
164- (BOOL)becomeFirstResponder
165{
166 FirstResponderCandidate firstResponderCandidate(self);
167
168 qImDebug() << "self:" << self << "first:" << [UIResponder qt_currentFirstResponder];
169
170 if (![super becomeFirstResponder]) {
171 qImDebug() << self << "was not allowed to become first responder";
172 return NO;
173 }
174
175 qImDebug() << self << "became first responder";
176
177 return YES;
178}
179
180- (BOOL)resignFirstResponder
181{
182 qImDebug() << "self:" << self << "first:" << [UIResponder qt_currentFirstResponder];
183
184 // Don't allow activation events of the window that we're doing text on behalf on
185 // to steal responder.
186 if (FirstResponderCandidate::currentCandidate() == [self nextResponder]) {
187 qImDebug("not allowing parent window to steal responder");
188 return NO;
189 }
190
191 if (![super resignFirstResponder])
192 return NO;
193
194 qImDebug() << self << "resigned first responder";
195
196 // Dismissing the keyboard will trigger resignFirstResponder, but so will
197 // a regular responder transfer to another window. In the former case, iOS
198 // will set the new first-responder to our next-responder, and in the latter
199 // case we'll have an active responder candidate.
200 if (![UIResponder qt_currentFirstResponder] && !FirstResponderCandidate::currentCandidate()) {
201 // No first responder set anymore, sync this with Qt by clearing the
202 // focus object.
203 m_inputContext->clearCurrentFocusObject();
204 } else if ([UIResponder qt_currentFirstResponder] == [self nextResponder]) {
205 // We have resigned the keyboard, and transferred first responder back to the parent view
206 Q_ASSERT(!FirstResponderCandidate::currentCandidate());
207 if ([self currentImeState:Qt::ImEnabled].toBool()) {
208 // The current focus object expects text input, but there
209 // is no keyboard to get input from. So we clear focus.
210 qImDebug("no keyboard available, clearing focus object");
211 m_inputContext->clearCurrentFocusObject();
212 }
213 } else {
214 // We've lost responder status because another Qt window was made active,
215 // another QIOSTextResponder was made first-responder, another UIView was
216 // made first-responder, or the first-responder was cleared globally. In
217 // either of these cases we don't have to do anything.
218 qImDebug("lost first responder, but not clearing focus object");
219 }
220
221 return YES;
222}
223
224- (UIResponder*)nextResponder
225{
226 // Make sure we have a handle/platform window before getting the winId().
227 // In the dtor of QIOSWindow the platform window is set to null before calling
228 // removeFromSuperview which will end up calling nextResponder. That means it's
229 // possible that we can get here while the window is being torn down.
230 return (qApp->focusWindow() && qApp->focusWindow()->handle()) ?
231 reinterpret_cast<QUIView *>(qApp->focusWindow()->handle()->winId()) : 0;
232}
233
234// -------------------------------------------------------------------------
235
236- (void)notifyInputDelegate:(Qt::InputMethodQueries)updatedProperties
237{
238 Q_UNUSED(updatedProperties);
239}
240
241- (BOOL)needsKeyboardReconfigure:(Qt::InputMethodQueries)updatedProperties
242{
243 if (updatedProperties & Qt::ImEnabled) {
244 qImDebug() << "Qt::ImEnabled has changed since text responder was configured, need reconfigure";
245 return YES;
246 }
247
248 if (updatedProperties & Qt::ImReadOnly) {
249 qImDebug() << "Qt::ImReadOnly has changed since text responder was configured, need reconfigure";
250 return YES;
251 }
252
253 return NO;
254}
255
256- (void)reset
257{
258 // Nothing to reset for read-only text fields
259}
260
261- (void)commit
262{
263 // Nothing to commit for read-only text fields
264}
265
266// -------------------------------------------------------------------------
267
268#ifndef QT_NO_SHORTCUT
269
270- (void)sendKeyPressRelease:(Qt::Key)key modifiers:(Qt::KeyboardModifiers)modifiers
271{
272 QScopedValueRollback<BOOL> rollback(m_inSendEventToFocusObject, true);
273 QWindowSystemInterface::handleKeyEvent(qApp->focusWindow(), QEvent::KeyPress, key, modifiers);
274 QWindowSystemInterface::handleKeyEvent(qApp->focusWindow(), QEvent::KeyRelease, key, modifiers);
275}
276
277- (void)sendShortcut:(QKeySequence::StandardKey)standardKey
278{
279 const QKeyCombination combination = QKeySequence(standardKey)[0];
280 [self sendKeyPressRelease:combination.key() modifiers:combination.keyboardModifiers()];
281}
282
283- (BOOL)hasSelection
284{
285 if (!QGuiApplication::focusObject())
286 return false;
287
288 QInputMethodQueryEvent query(Qt::ImAnchorPosition | Qt::ImCursorPosition);
289 QGuiApplication::sendEvent(QGuiApplication::focusObject(), &query);
290 int anchorPos = query.value(Qt::ImAnchorPosition).toInt();
291 int cursorPos = query.value(Qt::ImCursorPosition).toInt();
292 return anchorPos != cursorPos;
293}
294
295- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
296{
297 const bool isSelectAction =
298 action == @selector(select:) ||
299 action == @selector(selectAll:);
300
301 const bool isReadAction = action == @selector(copy:);
302
303 if (!isSelectAction && !isReadAction)
304 return [super canPerformAction:action withSender:sender];
305
306 const bool hasSelection = [self hasSelection];
307 return (!hasSelection && isSelectAction) || (hasSelection && isReadAction);
308}
309
310- (void)copy:(id)sender
311{
312 Q_UNUSED(sender);
313 [self sendShortcut:QKeySequence::Copy];
314}
315
316- (void)select:(id)sender
317{
318 Q_UNUSED(sender);
319 [self sendShortcut:QKeySequence::MoveToPreviousWord];
320 [self sendShortcut:QKeySequence::SelectNextWord];
321}
322
323- (void)selectAll:(id)sender
324{
325 Q_UNUSED(sender);
326 [self sendShortcut:QKeySequence::SelectAll];
327}
328
329#endif // QT_NO_SHORTCUT
330
331@end
332
333// -------------------------------------------------------------------------
334
335@implementation QIOSTextInputResponder {
336 QString m_markedText;
337 BOOL m_inSelectionChange;
338}
339
340- (instancetype)initWithInputContext:(QT_PREPEND_NAMESPACE(QIOSInputContext) *)inputContext
341{
342 if (!(self = [super initWithInputContext:inputContext]))
343 return self;
344
345 m_inSelectionChange = NO;
346
347 QVariantMap platformData = m_configuredImeState->value(Qt::ImPlatformData).toMap();
348 Qt::InputMethodHints hints = Qt::InputMethodHints(m_configuredImeState->value(Qt::ImHints).toUInt());
349 Qt::EnterKeyType enterKeyType = Qt::EnterKeyType(m_configuredImeState->value(Qt::ImEnterKeyType).toUInt());
350
351 switch (enterKeyType) {
352 case Qt::EnterKeyReturn:
353 self.returnKeyType = UIReturnKeyDefault;
354 break;
355 case Qt::EnterKeyDone:
356 self.returnKeyType = UIReturnKeyDone;
357 break;
358 case Qt::EnterKeyGo:
359 self.returnKeyType = UIReturnKeyGo;
360 break;
361 case Qt::EnterKeySend:
362 self.returnKeyType = UIReturnKeySend;
363 break;
364 case Qt::EnterKeySearch:
365 self.returnKeyType = UIReturnKeySearch;
366 break;
367 case Qt::EnterKeyNext:
368 self.returnKeyType = UIReturnKeyNext;
369 break;
370 default:
371 self.returnKeyType = (hints & Qt::ImhMultiLine) ? UIReturnKeyDefault : UIReturnKeyDone;
372 break;
373 }
374
375 self.secureTextEntry = BOOL(hints & Qt::ImhHiddenText);
376 self.autocorrectionType = (hints & Qt::ImhNoPredictiveText) ?
377 UITextAutocorrectionTypeNo : UITextAutocorrectionTypeDefault;
378 self.spellCheckingType = (hints & Qt::ImhNoPredictiveText) ?
379 UITextSpellCheckingTypeNo : UITextSpellCheckingTypeDefault;
380
381 if (hints & Qt::ImhUppercaseOnly)
382 self.autocapitalizationType = UITextAutocapitalizationTypeAllCharacters;
383 else if (hints & Qt::ImhNoAutoUppercase)
384 self.autocapitalizationType = UITextAutocapitalizationTypeNone;
385 else
386 self.autocapitalizationType = UITextAutocapitalizationTypeSentences;
387
388 if (hints & Qt::ImhUrlCharactersOnly)
389 self.keyboardType = UIKeyboardTypeURL;
390 else if (hints & Qt::ImhEmailCharactersOnly)
391 self.keyboardType = UIKeyboardTypeEmailAddress;
392 else if (hints & Qt::ImhDigitsOnly)
393 self.keyboardType = UIKeyboardTypeNumberPad;
394 else if (hints & Qt::ImhDialableCharactersOnly)
395 self.keyboardType = UIKeyboardTypePhonePad;
396 else if (hints & Qt::ImhLatinOnly)
397 self.keyboardType = UIKeyboardTypeASCIICapable;
398 else if (hints & (Qt::ImhPreferNumbers | Qt::ImhFormattedNumbersOnly))
399 self.keyboardType = UIKeyboardTypeNumbersAndPunctuation;
400 else if (hints & Qt::ImhDecimalNumbersOnly)
401 self.keyboardType = UIKeyboardTypeDecimalPad;
402 else
403 self.keyboardType = UIKeyboardTypeDefault;
404
405 if (UIView *inputView = static_cast<UIView *>(platformData.value(kImePlatformDataInputView).value<void *>()))
406 self.inputView = [[[WrapperView alloc] initWithView:inputView] autorelease];
407 if (UIView *accessoryView = static_cast<UIView *>(platformData.value(kImePlatformDataInputAccessoryView).value<void *>()))
408 self.inputAccessoryView = [[[WrapperView alloc] initWithView:accessoryView] autorelease];
409
410#if !defined(Q_OS_TVOS) && !defined(Q_OS_VISIONOS)
411 if (platformData.value(kImePlatformDataHideShortcutsBar).toBool()) {
412 // According to the docs, leadingBarButtonGroups/trailingBarButtonGroups should be set to nil to hide the shortcuts bar.
413 // However, starting with iOS 10, the API has been surrounded with NS_ASSUME_NONNULL, which contradicts this and causes
414 // compiler warnings. Still it is the way to go to really hide the space reserved for that.
415#pragma clang diagnostic push
416#pragma clang diagnostic ignored "-Wnonnull"
417 self.inputAssistantItem.leadingBarButtonGroups = nil;
418 self.inputAssistantItem.trailingBarButtonGroups = nil;
419#pragma clang diagnostic pop
420 }
421#endif
422
423 self.undoManager.groupsByEvent = NO;
424 [self rebuildUndoStack];
425
426 return self;
427}
428
429- (void)dealloc
430{
431 self.inputView = 0;
432 self.inputAccessoryView = 0;
433 [self.undoManager removeAllActions];
434
435 [super dealloc];
436}
437
438- (BOOL)needsKeyboardReconfigure:(Qt::InputMethodQueries)updatedProperties
439{
440 Qt::InputMethodQueries relevantProperties = updatedProperties;
441 if ((relevantProperties & Qt::ImEnabled)) {
442 // When switching on input-methods we need to consider hints and platform data
443 // as well, as the IM state that we were based on may have been invalidated when
444 // IM was switched off.
445
446 qImDebug("IM was turned on, we need to check hints and platform data as well");
447 relevantProperties |= (Qt::ImHints | Qt::ImPlatformData);
448 }
449
450 // Based on what we set up in initWithInputContext above
451 relevantProperties &= (Qt::ImHints | Qt::ImEnterKeyType | Qt::ImPlatformData);
452
453 if (!relevantProperties)
454 return [super needsKeyboardReconfigure:updatedProperties];
455
456 for (uint i = 0; i < (sizeof(Qt::ImQueryAll) * CHAR_BIT); ++i) {
457 if (Qt::InputMethodQuery property = Qt::InputMethodQuery(int(updatedProperties & (1 << i)))) {
458 if ([self currentImeState:property] != m_configuredImeState->value(property)) {
459 qImDebug() << property << "has changed since text responder was configured, need reconfigure";
460 return YES;
461 }
462 }
463 }
464
465 return [super needsKeyboardReconfigure:updatedProperties];
466}
467
468- (void)reset
469{
470 [self setMarkedText:@"" selectedRange:NSMakeRange(0, 0)];
471 [self notifyInputDelegate:Qt::ImSurroundingText];
472}
473
474- (void)commit
475{
476 [self unmarkText];
477 [self notifyInputDelegate:Qt::ImSurroundingText];
478}
479
480// -------------------------------------------------------------------------
481
482#ifndef QT_NO_SHORTCUT
483
484- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
485{
486 bool isEditAction = (action == @selector(cut:)
487 || action == @selector(copy:)
488 || action == @selector(paste:)
489 || action == @selector(delete:)
490 || action == @selector(toggleBoldface:)
491 || action == @selector(toggleItalics:)
492 || action == @selector(toggleUnderline:)
493 || action == @selector(undo)
494 || action == @selector(redo));
495
496 bool isSelectAction = (action == @selector(select:)
497 || action == @selector(selectAll:)
498 || action == @selector(paste:)
499 || action == @selector(undo)
500 || action == @selector(redo));
501
502 const bool unknownAction = !isEditAction && !isSelectAction;
503 const bool hasSelection = [self hasSelection];
504
505 if (unknownAction)
506 return [super canPerformAction:action withSender:sender];
507
508 QObject *focusObject = QGuiApplication::focusObject();
509 if (focusObject && focusObject->property("qt_im_readonly").toBool()) {
510 // exceptional menu items for read-only views: do include Copy, do not include Paste etc.
511 if (action == @selector(cut:)
512 || action == @selector(paste:)
513 || action == @selector(delete:))
514 return NO;
515 if (action == @selector(copy:))
516 return YES;
517 }
518 return (hasSelection && isEditAction) || (!hasSelection && isSelectAction);
519}
520
521- (void)cut:(id)sender
522{
523 Q_UNUSED(sender);
524 [self sendShortcut:QKeySequence::Cut];
525}
526
527- (void)paste:(id)sender
528{
529 Q_UNUSED(sender);
530 [self sendShortcut:QKeySequence::Paste];
531}
532
533- (void)delete:(id)sender
534{
535 Q_UNUSED(sender);
536 [self sendShortcut:QKeySequence::Delete];
537}
538
539- (void)toggleBoldface:(id)sender
540{
541 Q_UNUSED(sender);
542 [self sendShortcut:QKeySequence::Bold];
543}
544
545- (void)toggleItalics:(id)sender
546{
547 Q_UNUSED(sender);
548 [self sendShortcut:QKeySequence::Italic];
549}
550
551- (void)toggleUnderline:(id)sender
552{
553 Q_UNUSED(sender);
554 [self sendShortcut:QKeySequence::Underline];
555}
556
557// -------------------------------------------------------------------------
558
559- (void)undo
560{
561 [self sendShortcut:QKeySequence::Undo];
562 [self rebuildUndoStack];
563}
564
565- (void)redo
566{
567 [self sendShortcut:QKeySequence::Redo];
568 [self rebuildUndoStack];
569}
570
571- (void)registerRedo
572{
573 NSUndoManager *undoMgr = self.undoManager;
574 [undoMgr beginUndoGrouping];
575 [undoMgr registerUndoWithTarget:self selector:@selector(redo) object:nil];
576 [undoMgr endUndoGrouping];
577}
578
579- (void)rebuildUndoStack
580{
581 dispatch_async(dispatch_get_main_queue (), ^{
582 // Register dummy undo/redo operations to enable Cmd-Z and Cmd-Shift-Z
583 // Ensure we do this outside any undo/redo callback since NSUndoManager
584 // will treat registerUndoWithTarget as registering a redo when called
585 // from within a undo callback.
586 NSUndoManager *undoMgr = self.undoManager;
587 [undoMgr removeAllActions];
588
589 [undoMgr beginUndoGrouping];
590 [undoMgr registerUndoWithTarget:self selector:@selector(undo) object:nil];
591 [undoMgr endUndoGrouping];
592 [undoMgr beginUndoGrouping];
593 [undoMgr registerUndoWithTarget:self selector:@selector(undo) object:nil];
594 [undoMgr endUndoGrouping];
595
596 // Schedule operations that we immediately pop off to be able to schedule redos
597 [undoMgr beginUndoGrouping];
598 [undoMgr registerUndoWithTarget:self selector:@selector(registerRedo) object:nil];
599 [undoMgr endUndoGrouping];
600 [undoMgr beginUndoGrouping];
601 [undoMgr registerUndoWithTarget:self selector:@selector(registerRedo) object:nil];
602 [undoMgr endUndoGrouping];
603 [undoMgr undo];
604 [undoMgr undo];
605
606 // Note that, perhaps because of a bug in UIKit, the buttons on the shortcuts bar ends up
607 // disabled if a undo/redo callback doesn't lead to a [UITextInputDelegate textDidChange].
608 // And we only call that method if Qt made changes to the text. The effect is that the buttons
609 // become disabled when there is nothing more to undo (Qt didn't change anything upon receiving
610 // an undo request). This seems to be OK behavior, so we let it stay like that unless it shows
611 // to cause problems.
612
613 // QTBUG-63393: Having two operations on the rebuilt undo stack keeps the undo/redo widgets
614 // always enabled on the shortcut bar. This workaround was found by experimenting with
615 // removing the removeAllActions call, and is related to the unknown internal implementation
616 // details of how the shortcut bar updates the dimming of its buttons.
617 });
618}
619
620// -------------------------------------------------------------------------
621
622- (void)keyCommandTriggered:(UIKeyCommand *)keyCommand
623{
624 Qt::Key key = Qt::Key_unknown;
625 Qt::KeyboardModifiers modifiers = Qt::NoModifier;
626
627 if (keyCommand.input == UIKeyInputLeftArrow)
628 key = Qt::Key_Left;
629 else if (keyCommand.input == UIKeyInputRightArrow)
630 key = Qt::Key_Right;
631 else if (keyCommand.input == UIKeyInputUpArrow)
632 key = Qt::Key_Up;
633 else if (keyCommand.input == UIKeyInputDownArrow)
634 key = Qt::Key_Down;
635 else
636 Q_UNREACHABLE();
637
638 if (keyCommand.modifierFlags & UIKeyModifierAlternate)
639 modifiers |= Qt::AltModifier;
640 if (keyCommand.modifierFlags & UIKeyModifierShift)
641 modifiers |= Qt::ShiftModifier;
642 if (keyCommand.modifierFlags & UIKeyModifierCommand)
643 modifiers |= Qt::ControlModifier;
644
645 [self sendKeyPressRelease:key modifiers:modifiers];
646}
647
648- (void)addKeyCommandsToArray:(NSMutableArray<UIKeyCommand *> *)array key:(NSString *)key
649{
650 SEL s = @selector(keyCommandTriggered:);
651 [array addObject:[UIKeyCommand keyCommandWithInput:key modifierFlags:0 action:s]];
652 [array addObject:[UIKeyCommand keyCommandWithInput:key modifierFlags:UIKeyModifierShift action:s]];
653 [array addObject:[UIKeyCommand keyCommandWithInput:key modifierFlags:UIKeyModifierAlternate action:s]];
654 [array addObject:[UIKeyCommand keyCommandWithInput:key modifierFlags:UIKeyModifierAlternate|UIKeyModifierShift action:s]];
655 [array addObject:[UIKeyCommand keyCommandWithInput:key modifierFlags:UIKeyModifierCommand action:s]];
656 [array addObject:[UIKeyCommand keyCommandWithInput:key modifierFlags:UIKeyModifierCommand|UIKeyModifierShift action:s]];
657}
658
659- (NSArray<UIKeyCommand *> *)keyCommands
660{
661 // Since keyCommands is called for every key
662 // press/release, we cache the result
663 static dispatch_once_t once;
664 static NSMutableArray<UIKeyCommand *> *array;
665
666 dispatch_once(&once, ^{
667 // We let Qt move the cursor around when the arrow keys are being used. This
668 // is normally implemented through UITextInput, but since IM in Qt have poor
669 // support for moving the cursor vertically, and even less support for selecting
670 // text across multiple paragraphs, we do this through key events.
671 array = [NSMutableArray<UIKeyCommand *> new];
672 [self addKeyCommandsToArray:array key:UIKeyInputUpArrow];
673 [self addKeyCommandsToArray:array key:UIKeyInputDownArrow];
674 [self addKeyCommandsToArray:array key:UIKeyInputLeftArrow];
675 [self addKeyCommandsToArray:array key:UIKeyInputRightArrow];
676 });
677
678 return array;
679}
680
681#endif // QT_NO_SHORTCUT
682
683// -------------------------------------------------------------------------
684
685- (void)notifyInputDelegate:(Qt::InputMethodQueries)updatedProperties
686{
687 // As documented, we should not report textWillChange/textDidChange unless the text
688 // was changed externally. That will cause spell checking etc to fail. But we don't
689 // really know if the text/selection was changed by UITextInput or Qt/app when getting
690 // update calls from Qt. We therefore use a less ideal approach where we always assume
691 // that UITextView caused the change if we're currently processing an event sendt from it.
692 if (m_inSendEventToFocusObject)
693 return;
694
695 if (updatedProperties & (Qt::ImCursorPosition | Qt::ImAnchorPosition)) {
696 QScopedValueRollback<BOOL> rollback(m_inSelectionChange, true);
697 [self.inputDelegate selectionWillChange:self];
698 [self.inputDelegate selectionDidChange:self];
699 }
700
701 if (updatedProperties & Qt::ImSurroundingText) {
702 [self.inputDelegate textWillChange:self];
703 [self.inputDelegate textDidChange:self];
704 }
705}
706
707- (void)sendEventToFocusObject:(QEvent &)e
708{
709 QObject *focusObject = QGuiApplication::focusObject();
710 if (!focusObject)
711 return;
712
713 // While sending the event, we will receive back updateInputMethodWithQuery calls.
714 // Note that it would be more correct to post the event instead, but UITextInput expects
715 // callbacks to take effect immediately (it will query us for information after a callback).
716 QScopedValueRollback<BOOL> rollback(m_inSendEventToFocusObject);
717 m_inSendEventToFocusObject = YES;
718 QCoreApplication::sendEvent(focusObject, &e);
719}
720
721- (id<UITextInputTokenizer>)tokenizer
722{
723 return [[[UITextInputStringTokenizer alloc] initWithTextInput:self] autorelease];
724}
725
726- (UITextPosition *)beginningOfDocument
727{
728 return [QUITextPosition positionWithIndex:0];
729}
730
731- (UITextPosition *)endOfDocument
732{
733 QString surroundingText = [self currentImeState:Qt::ImSurroundingText].toString();
734 int endPosition = surroundingText.length() + m_markedText.length();
735 return [QUITextPosition positionWithIndex:endPosition];
736}
737
738- (void)setSelectedTextRange:(UITextRange *)range
739{
740 if (m_inSelectionChange) {
741 // After [UITextInputDelegate selectionWillChange], UIKit will cancel
742 // any ongoing auto correction (if enabled) and ask us to set an empty selection.
743 // This is contradictory to our current attempt to set a selection, so we ignore
744 // the callback. UIKit will be re-notified of the new selection after
745 // [UITextInputDelegate selectionDidChange].
746 return;
747 }
748
749 QUITextRange *r = static_cast<QUITextRange *>(range);
750 QList<QInputMethodEvent::Attribute> attrs;
751 attrs << QInputMethodEvent::Attribute(QInputMethodEvent::Selection, r.range.location, r.range.length, 0);
752 QInputMethodEvent e(m_markedText, attrs);
753 [self sendEventToFocusObject:e];
754}
755
756- (UITextRange *)selectedTextRange
757{
758 int cursorPos = [self currentImeState:Qt::ImCursorPosition].toInt();
759 int anchorPos = [self currentImeState:Qt::ImAnchorPosition].toInt();
760 return [QUITextRange rangeWithNSRange:NSMakeRange(qMin(cursorPos, anchorPos), qAbs(anchorPos - cursorPos))];
761}
762
763- (NSString *)textInRange:(UITextRange *)range
764{
765 QString text = [self currentImeState:Qt::ImSurroundingText].toString();
766 if (!m_markedText.isEmpty()) {
767 // [UITextInput textInRange] is sparsely documented, but it turns out that unconfirmed
768 // marked text should be seen as a part of the text document. This is different from
769 // ImSurroundingText, which excludes it.
770 int cursorPos = [self currentImeState:Qt::ImCursorPosition].toInt();
771 text = text.left(cursorPos) + m_markedText + text.mid(cursorPos);
772 }
773
774 int s = static_cast<QUITextPosition *>([range start]).index;
775 int e = static_cast<QUITextPosition *>([range end]).index;
776 return text.mid(s, e - s).toNSString();
777}
778
779- (void)setMarkedText:(NSString *)markedText selectedRange:(NSRange)selectedRange
780{
781 Q_UNUSED(selectedRange);
782
783 m_markedText = markedText ? QString::fromNSString(markedText) : QString();
784
785 static QTextCharFormat markedTextFormat;
786 if (markedTextFormat.isEmpty()) {
787 // There seems to be no way to query how the preedit text
788 // should be drawn. So we need to hard-code the color.
789 markedTextFormat.setBackground(QColor(206, 221, 238));
790 }
791
792 QList<QInputMethodEvent::Attribute> attrs;
793 attrs << QInputMethodEvent::Attribute(QInputMethodEvent::TextFormat, 0, markedText.length, markedTextFormat);
794 QInputMethodEvent e(m_markedText, attrs);
795 [self sendEventToFocusObject:e];
796}
797
798- (void)unmarkText
799{
800 if (m_markedText.isEmpty())
801 return;
802
803 QInputMethodEvent e;
804 e.setCommitString(m_markedText);
805 [self sendEventToFocusObject:e];
806
807 m_markedText.clear();
808}
809
810- (NSComparisonResult)comparePosition:(UITextPosition *)position toPosition:(UITextPosition *)other
811{
812 int p = static_cast<QUITextPosition *>(position).index;
813 int o = static_cast<QUITextPosition *>(other).index;
814 if (p > o)
815 return NSOrderedAscending;
816 else if (p < o)
817 return NSOrderedDescending;
818 return NSOrderedSame;
819}
820
821- (UITextRange *)markedTextRange
822{
823 return m_markedText.isEmpty() ? nil : [QUITextRange rangeWithNSRange:NSMakeRange(0, m_markedText.length())];
824}
825
826- (UITextRange *)textRangeFromPosition:(UITextPosition *)fromPosition toPosition:(UITextPosition *)toPosition
827{
828 int f = static_cast<QUITextPosition *>(fromPosition).index;
829 int t = static_cast<QUITextPosition *>(toPosition).index;
830 return [QUITextRange rangeWithNSRange:NSMakeRange(f, t - f)];
831}
832
833- (UITextPosition *)positionFromPosition:(UITextPosition *)position offset:(NSInteger)offset
834{
835 int p = static_cast<QUITextPosition *>(position).index;
836 const int posWithIndex = p + offset;
837 const int textLength = [self currentImeState:Qt::ImSurroundingText].toString().length();
838 if (posWithIndex < 0 || posWithIndex > textLength)
839 return nil;
840 return [QUITextPosition positionWithIndex:posWithIndex];
841}
842
843- (UITextPosition *)positionFromPosition:(UITextPosition *)position inDirection:(UITextLayoutDirection)direction offset:(NSInteger)offset
844{
845 int p = static_cast<QUITextPosition *>(position).index;
846
847 switch (direction) {
848 case UITextLayoutDirectionLeft:
849 return [QUITextPosition positionWithIndex:p - offset];
850 case UITextLayoutDirectionRight:
851 return [QUITextPosition positionWithIndex:p + offset];
852 default:
853 // Qt doesn't support getting the position above or below the current position, so
854 // for those cases we just return the current position, making it a no-op.
855 return position;
856 }
857}
858
859- (UITextPosition *)positionWithinRange:(UITextRange *)range farthestInDirection:(UITextLayoutDirection)direction
860{
861 NSRange r = static_cast<QUITextRange *>(range).range;
862 if (direction == UITextLayoutDirectionRight)
863 return [QUITextPosition positionWithIndex:r.location + r.length];
864 return [QUITextPosition positionWithIndex:r.location];
865}
866
867- (NSInteger)offsetFromPosition:(UITextPosition *)fromPosition toPosition:(UITextPosition *)toPosition
868{
869 int f = static_cast<QUITextPosition *>(fromPosition).index;
870 int t = static_cast<QUITextPosition *>(toPosition).index;
871 return t - f;
872}
873
874- (UIView *)textInputView
875{
876 auto *focusWindow = QGuiApplication::focusWindow();
877 if (!focusWindow)
878 return nil;
879
880 // iOS expects rects we return from other UITextInput methods
881 // to be relative to the view this method returns.
882 // Since QInputMethod returns rects relative to the top level
883 // QWindow, that is also the view we need to return.
884 Q_ASSERT(focusWindow->handle());
885 QPlatformWindow *topLevel = focusWindow->handle();
886 while (QPlatformWindow *p = topLevel->parent())
887 topLevel = p;
888 return reinterpret_cast<UIView *>(topLevel->winId());
889}
890
891- (CGRect)firstRectForRange:(UITextRange *)range
892{
893 QObject *focusObject = QGuiApplication::focusObject();
894 if (!focusObject)
895 return CGRectZero;
896
897 // Using a work-around to get the current rect until
898 // a better API is in place:
899 if (!m_markedText.isEmpty())
900 return CGRectZero;
901
902 int cursorPos = [self currentImeState:Qt::ImCursorPosition].toInt();
903 int anchorPos = [self currentImeState:Qt::ImAnchorPosition].toInt();
904
905 NSRange r = static_cast<QUITextRange*>(range).range;
906 QList<QInputMethodEvent::Attribute> attrs;
907 attrs << QInputMethodEvent::Attribute(QInputMethodEvent::Selection, r.location, 0, 0);
908 {
909 QInputMethodEvent e(m_markedText, attrs);
910 [self sendEventToFocusObject:e];
911 }
912 QRectF startRect = QPlatformInputContext::cursorRectangle();
913
914 attrs = QList<QInputMethodEvent::Attribute>();
915 attrs << QInputMethodEvent::Attribute(QInputMethodEvent::Selection, r.location + r.length, 0, 0);
916 {
917 QInputMethodEvent e(m_markedText, attrs);
918 [self sendEventToFocusObject:e];
919 }
920 QRectF endRect = QPlatformInputContext::cursorRectangle();
921
922 if (cursorPos != int(r.location + r.length) || cursorPos != anchorPos) {
923 attrs = QList<QInputMethodEvent::Attribute>();
924 attrs << QInputMethodEvent::Attribute(QInputMethodEvent::Selection, qMin(cursorPos, anchorPos), qAbs(cursorPos - anchorPos), 0);
925 QInputMethodEvent e(m_markedText, attrs);
926 [self sendEventToFocusObject:e];
927 }
928
929 return startRect.united(endRect).toCGRect();
930}
931
932- (NSArray<UITextSelectionRect *> *)selectionRectsForRange:(UITextRange *)range
933{
934 Q_UNUSED(range);
935 // This method is supposed to return a rectangle for each line with selection. Since we don't
936 // expose an API in Qt/IM for getting this information, and since we never seems to be getting
937 // a call from UIKit for this, we return an empty array until a need arise.
938 return [[NSArray<UITextSelectionRect *> new] autorelease];
939}
940
941- (CGRect)caretRectForPosition:(UITextPosition *)position
942{
943 Q_UNUSED(position);
944 // Assume for now that position is always the same as
945 // cursor index until a better API is in place:
946 return QPlatformInputContext::cursorRectangle().toCGRect();
947}
948
949- (void)replaceRange:(UITextRange *)range withText:(NSString *)text
950{
951 [self setSelectedTextRange:range];
952
953 QInputMethodEvent e;
954 e.setCommitString(QString::fromNSString(text));
955 [self sendEventToFocusObject:e];
956}
957
958- (void)setBaseWritingDirection:(NSWritingDirection)writingDirection forRange:(UITextRange *)range
959{
960 Q_UNUSED(writingDirection);
961 Q_UNUSED(range);
962 // Writing direction is handled by QLocale
963}
964
965- (NSWritingDirection)baseWritingDirectionForPosition:(UITextPosition *)position inDirection:(UITextStorageDirection)direction
966{
967 Q_UNUSED(position);
968 Q_UNUSED(direction);
969 if (QLocale::system().textDirection() == Qt::RightToLeft)
970 return NSWritingDirectionRightToLeft;
971 return NSWritingDirectionLeftToRight;
972}
973
974- (UITextRange *)characterRangeByExtendingPosition:(UITextPosition *)position inDirection:(UITextLayoutDirection)direction
975{
976 int p = static_cast<QUITextPosition *>(position).index;
977 if (direction == UITextLayoutDirectionLeft)
978 return [QUITextRange rangeWithNSRange:NSMakeRange(0, p)];
979 int l = [self currentImeState:Qt::ImSurroundingText].toString().length();
980 return [QUITextRange rangeWithNSRange:NSMakeRange(p, l - p)];
981}
982
983- (UITextPosition *)closestPositionToPoint:(CGPoint)point
984{
985 int textPos = QPlatformInputContext::queryFocusObject(Qt::ImCursorPosition, QPointF::fromCGPoint(point)).toInt();
986 return [QUITextPosition positionWithIndex:textPos];
987}
988
989- (UITextPosition *)closestPositionToPoint:(CGPoint)point withinRange:(UITextRange *)range
990{
991 // No API in Qt for determining this. Use sensible default instead:
992 Q_UNUSED(point);
993 Q_UNUSED(range);
994 return [QUITextPosition positionWithIndex:[self currentImeState:Qt::ImCursorPosition].toInt()];
995}
996
997- (UITextRange *)characterRangeAtPoint:(CGPoint)point
998{
999 // No API in Qt for determining this. Use sensible default instead:
1000 Q_UNUSED(point);
1001 return [QUITextRange rangeWithNSRange:NSMakeRange([self currentImeState:Qt::ImCursorPosition].toInt(), 0)];
1002}
1003
1004- (void)setMarkedTextStyle:(NSDictionary *)style
1005{
1006 Q_UNUSED(style);
1007 // No-one is going to change our style. If UIKit itself did that
1008 // it would be very welcome, since then we knew how to style marked
1009 // text instead of just guessing...
1010}
1011
1012#ifndef Q_OS_TVOS
1013- (NSDictionary *)textStylingAtPosition:(UITextPosition *)position inDirection:(UITextStorageDirection)direction
1014{
1015 Q_UNUSED(position);
1016 Q_UNUSED(direction);
1017
1018 QObject *focusObject = QGuiApplication::focusObject();
1019 if (!focusObject)
1020 return @{};
1021
1022 // Assume position is the same as the cursor for now. QInputMethodQueryEvent with Qt::ImFont
1023 // needs to be extended to take an extra position argument before this can be fully correct.
1024 QInputMethodQueryEvent e(Qt::ImFont);
1025 QCoreApplication::sendEvent(focusObject, &e);
1026 QFont qfont = qvariant_cast<QFont>(e.value(Qt::ImFont));
1027 UIFont *uifont = [UIFont fontWithName:qfont.family().toNSString() size:qfont.pointSize()];
1028 if (!uifont)
1029 return @{};
1030 return @{NSFontAttributeName: uifont};
1031}
1032#endif
1033
1034- (NSDictionary *)markedTextStyle
1035{
1036 return [NSDictionary dictionary];
1037}
1038
1039- (BOOL)hasText
1040{
1041 return YES;
1042}
1043
1044- (void)insertText:(NSString *)text
1045{
1046 QObject *focusObject = QGuiApplication::focusObject();
1047 if (!focusObject)
1048 return;
1049
1050 if ([text isEqualToString:@"\n"]) {
1051 [self sendKeyPressRelease:Qt::Key_Return modifiers:Qt::NoModifier];
1052
1053 // An onEnter handler of a TextInput might move to the next input by calling
1054 // nextInput.forceActiveFocus() which changes the focusObject.
1055 // In that case we don't want to hide the VKB.
1056 if (focusObject != QGuiApplication::focusObject()) {
1057 qImDebug() << "focusObject already changed, not resigning first responder.";
1058 return;
1059 }
1060
1061 if (self.returnKeyType == UIReturnKeyDone || self.returnKeyType == UIReturnKeyGo
1062 || self.returnKeyType == UIReturnKeySend || self.returnKeyType == UIReturnKeySearch)
1063 [self resignFirstResponder];
1064
1065 return;
1066 }
1067
1068 QInputMethodEvent e;
1069 e.setCommitString(QString::fromNSString(text));
1070 [self sendEventToFocusObject:e];
1071}
1072
1073- (void)deleteBackward
1074{
1075 // UITextInput selects the text to be deleted before calling this method. To avoid
1076 // drawing the selection, we flush after posting the key press/release.
1077 [self sendKeyPressRelease:Qt::Key_Backspace modifiers:Qt::NoModifier];
1078}
1079
1080@end
\inmodule QtCore
Definition qvariant.h:68
long NSInteger
Q_FORWARD_DECLARE_OBJC_CLASS(NSString)
#define qImDebug
Definition qiosglobal.h:21