Qt
Internal/Contributor docs for the Qt SDK. <b>Note:</b> These are NOT official API docs; those are found <a href='https://doc.qt.io/'>here</a>.
Loading...
Searching...
No Matches
qiostextinputoverlay.mm
Go to the documentation of this file.
1// Copyright (C) 2017 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
4#import <UIKit/UIGestureRecognizerSubclass.h>
5#import <UIKit/UITextView.h>
6
7#include <QtGui/QGuiApplication>
8#include <QtGui/QInputMethod>
9#include <QtGui/QStyleHints>
10
11#include <QtGui/private/qinputmethod_p.h>
12#include <QtCore/private/qobject_p.h>
13#include <QtCore/private/qcore_mac_p.h>
14
15#include "qiosglobal.h"
17
18typedef QPair<int, int> SelectionPair;
19typedef void (^Block)(void);
20
21static const CGFloat kKnobWidth = 10;
22
27
29{
32 int anchorPos = query.value(Qt::ImAnchorPosition).toInt();
33 int cursorPos = query.value(Qt::ImCursorPosition).toInt();
34 return qMakePair(anchorPos, cursorPos);
35}
36
37static bool hasSelection()
38{
40 return selection.first != selection.second;
41}
42
44{
45 [CATransaction begin];
46 [CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
47 block();
48 [CATransaction commit];
49}
50
51// -------------------------------------------------------------------------
56@interface QIOSEditMenu : NSObject
57@property (nonatomic, assign) BOOL visible;
58@property (nonatomic, readonly) BOOL isHiding;
59@property (nonatomic, readonly) BOOL shownByUs;
60@property (nonatomic, assign) BOOL reshowAfterHidden;
61@end
62
63@implementation QIOSEditMenu
64
65- (instancetype)init
66{
67 if (self = [super init]) {
68 NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
69
70 [center addObserverForName:UIMenuControllerWillHideMenuNotification
71 object:nil queue:nil usingBlock:^(NSNotification *) {
72 _isHiding = YES;
73 }];
74
75 [center addObserverForName:UIMenuControllerDidHideMenuNotification
76 object:nil queue:nil usingBlock:^(NSNotification *) {
77 _isHiding = NO;
78 _shownByUs = NO;
79 if (self.reshowAfterHidden) {
80 // To not abort an ongoing hide transition when showing the menu, you can set
81 // reshowAfterHidden to wait until the transition finishes before reshowing it.
82 self.reshowAfterHidden = NO;
83 dispatch_async(dispatch_get_main_queue (), ^{ self.visible = YES; });
84 }
85 }];
86 [center addObserverForName:UIKeyboardDidHideNotification object:nil queue:nil
87 usingBlock:^(NSNotification *) {
88 self.visible = NO;
89 }];
90
91 }
92
93 return self;
94}
95
96- (void)dealloc
97{
98 [[NSNotificationCenter defaultCenter] removeObserver:self name:nil object:nil];
99 [super dealloc];
100}
101
102- (BOOL)visible
103{
104 return [UIMenuController sharedMenuController].menuVisible;
105}
106
107- (void)setVisible:(BOOL)visible
108{
109 if (visible == self.visible)
110 return;
111
112 if (visible) {
113 // UIMenuController is a singleton that can be shown (and hidden) from anywhere.
114 // Try to keep track of whether or not is was shown by us (the gesture recognizers
115 // in this file) to avoid closing it if it was opened from elsewhere.
116 _shownByUs = YES;
117 // Note that the contents of the edit menu is decided by
118 // first responder, which is normally QIOSTextResponder.
121
122 CGRect targetRect = cr.united(ar).toCGRect();
123 UIView *focusView = reinterpret_cast<UIView *>(qApp->focusWindow()->winId());
124 [[UIMenuController sharedMenuController] setTargetRect:targetRect inView:focusView];
125 [[UIMenuController sharedMenuController] setMenuVisible:YES animated:YES];
126 } else {
127 [[UIMenuController sharedMenuController] setMenuVisible:NO animated:YES];
128 }
129}
130
131@end
132
133// -------------------------------------------------------------------------
134
135@interface QIOSLoupeLayer : CALayer
136@property (nonatomic, retain) UIView *targetView;
137@property (nonatomic, assign) CGPoint focalPoint;
138@property (nonatomic, assign) BOOL visible;
139@end
140
141@implementation QIOSLoupeLayer {
142 UIView *_snapshotView;
148}
149
150- (instancetype)initWithSize:(CGSize)size cornerRadius:(CGFloat)cornerRadius bottomOffset:(CGFloat)bottomOffset
151{
152 if (self = [super init]) {
153 _loupeOffset = bottomOffset + (size.height / 2);
154 _snapshotView = nil;
158 QObject::connect(&_updateTimer, &QTimer::timeout, [self](){ [self updateSnapshot]; });
159
160 // Create own geometry and outer shadow
161 self.frame = CGRectMake(0, 0, size.width, size.height);
162 self.cornerRadius = cornerRadius;
163 self.shadowColor = [[UIColor grayColor] CGColor];
164 self.shadowOffset = CGSizeMake(0, 1);
165 self.shadowRadius = 2.0;
166 self.shadowOpacity = 0.75;
167 self.transform = CATransform3DMakeScale(0, 0, 0);
168
169 // Create container view for the snapshots
170 _containerLayer = [[CALayer new] autorelease];
171 _containerLayer.frame = self.bounds;
172 _containerLayer.cornerRadius = cornerRadius;
173 _containerLayer.masksToBounds = YES;
174 [self addSublayer:_containerLayer];
175
176 // Create inner loupe shadow
177 const CGFloat inset = 30;
178 CALayer *topShadeLayer = [[CALayer new] autorelease];
179 topShadeLayer.frame = CGRectOffset(CGRectInset(self.bounds, -inset, -inset), 0, inset / 2);
180 topShadeLayer.borderWidth = inset / 2;
181 topShadeLayer.cornerRadius = cornerRadius;
182 topShadeLayer.borderColor = [[UIColor blackColor] CGColor];
183 topShadeLayer.shadowColor = [[UIColor blackColor] CGColor];
184 topShadeLayer.shadowOffset = CGSizeMake(0, 0);
185 topShadeLayer.shadowRadius = 15.0;
186 topShadeLayer.shadowOpacity = 0.6;
187 // Keep the shadow inside the loupe
188 CALayer *mask = [[CALayer new] autorelease];
189 mask.frame = CGRectOffset(self.bounds, inset, inset / 2);
190 mask.backgroundColor = [[UIColor blackColor] CGColor];
191 mask.cornerRadius = cornerRadius;
192 topShadeLayer.mask = mask;
193 [self addSublayer:topShadeLayer];
194
195 // Create border around the loupe. We need to do this in a separate
196 // layer (as opposed to on self) to not draw the border on top of
197 // overlapping external children (arrow).
198 CALayer *borderLayer = [[CALayer new] autorelease];
199 borderLayer.frame = self.bounds;
200 borderLayer.borderWidth = 0.75;
201 borderLayer.cornerRadius = cornerRadius;
202 borderLayer.borderColor = [[UIColor lightGrayColor] CGColor];
203 [self addSublayer:borderLayer];
204 }
205
206 return self;
207}
208
209- (void)dealloc
210{
211 _targetView = nil;
212 [super dealloc];
213}
214
215- (void)setVisible:(BOOL)visible
216{
217 if (_visible == visible)
218 return;
219
220 _visible = visible;
221
222 dispatch_async(dispatch_get_main_queue (), ^{
223 // Setting transform later, since CA will not perform an animation if
224 // changing values directly after init, and if the scale ends up empty.
225 self.transform = _visible ? CATransform3DMakeScale(1, 1, 1) : CATransform3DMakeScale(0.0, 0.0, 1);
226 });
227}
228
229- (void)updateSnapshot
230{
232 [self setNeedsDisplay];
233}
234
235- (void)setFocalPoint:(CGPoint)point
236{
237 _focalPoint = point;
238 [self updateSnapshot];
239
240 // Schedule a delayed update as well to ensure that we end up with a correct
241 // snapshot of the cursor, since QQuickRenderThread lags a bit behind
243}
244
245- (void)display
246{
247 // Take a snapshow of the target view, magnify the area around the focal
248 // point, and add the snapshow layer as a child of the container layer
249 // to make it look like a loupe. Then place this layer at the position of
250 // the focal point with the requested offset.
253 UIView *newSnapshot = [_targetView snapshotViewAfterScreenUpdates:NO];
254 [_snapshotView.layer removeFromSuperlayer];
255 [_snapshotView release];
256 _snapshotView = [newSnapshot retain];
257 [_containerLayer addSublayer:_snapshotView.layer];
259 }
260
261 self.position = CGPointMake(_focalPoint.x, _focalPoint.y - _loupeOffset);
262
263 const CGFloat loupeScale = 1.5;
264 CGFloat x = -(_focalPoint.x * loupeScale) + self.frame.size.width / 2;
265 CGFloat y = -(_focalPoint.y * loupeScale) + self.frame.size.height / 2;
266 CGFloat w = _targetView.frame.size.width * loupeScale;
267 CGFloat h = _targetView.frame.size.height * loupeScale;
268 _snapshotView.layer.frame = CGRectMake(x, y, w, h);
269 });
270}
271
272@end
273
274// -------------------------------------------------------------------------
275
276@interface QIOSHandleLayer : CALayer <CAAnimationDelegate>
277@property (nonatomic, assign) CGRect cursorRectangle;
278@property (nonatomic, assign) CGFloat handleScale;
279@property (nonatomic, assign) BOOL visible;
280@property (nonatomic, copy) Block onAnimationDidStop;
281@end
282
283@implementation QIOSHandleLayer {
284 CALayer *_handleCursorLayer;
287}
288
289@dynamic handleScale;
290
291- (instancetype)initWithKnobAtEdge:(Qt::Edge)selectionEdge
292{
293 if (self = [super init]) {
294 CGColorRef bgColor = [UIColor colorWithRed:0.1 green:0.4 blue:0.9 alpha:1].CGColor;
295 _selectionEdge = selectionEdge;
296 self.handleScale = 0;
297
298 _handleCursorLayer = [[CALayer new] autorelease];
299 _handleCursorLayer.masksToBounds = YES;
300 _handleCursorLayer.backgroundColor = bgColor;
301 [self addSublayer:_handleCursorLayer];
302
303 _handleKnobLayer = [[CALayer new] autorelease];
304 _handleKnobLayer.masksToBounds = YES;
305 _handleKnobLayer.backgroundColor = bgColor;
306 _handleKnobLayer.cornerRadius = kKnobWidth / 2;
307 [self addSublayer:_handleKnobLayer];
308 }
309 return self;
310}
311
312+ (BOOL)needsDisplayForKey:(NSString *)key
313{
314 if ([key isEqualToString:@"handleScale"])
315 return YES;
316 return [super needsDisplayForKey:key];
317}
318
319- (id<CAAction>)actionForKey:(NSString *)key
320{
321 if ([key isEqualToString:@"handleScale"]) {
322 if (_visible) {
323 // The handle should "bounce" in when becoming visible
324 CAKeyframeAnimation * animation = [CAKeyframeAnimation animationWithKeyPath:key];
325 [animation setDuration:0.5];
326 animation.values = @[@(0.0f), @(1.3f), @(1.3f), @(1.0f)];
327 animation.keyTimes = @[@(0.0f), @(0.3f), @(0.9f), @(1.0f)];
328 return animation;
329 } else {
330 CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:key];
331 [animation setDelegate:self];
332 animation.fromValue = [self valueForKey:key];
333 [animation setDuration:0.2];
334 return animation;
335 }
336 }
337 return [super actionForKey:key];
338}
339
340- (void)animationDidStop:(CAAnimation *)animation finished:(BOOL)flag
341{
343 Q_UNUSED(flag);
344 if (self.onAnimationDidStop)
345 self.onAnimationDidStop();
346}
347
348- (void)setVisible:(BOOL)visible
349{
350 if (visible == _visible)
351 return;
352
353 _visible = visible;
354
355 self.handleScale = visible ? 1 : 0;
356}
357
358- (void)setCursorRectangle:(CGRect)cursorRect
359{
360 if (CGRectEqualToRect(_cursorRectangle, cursorRect))
361 return;
362
363 _cursorRectangle = cursorRect;
364
366 [self setNeedsDisplay];
367 [self displayIfNeeded];
368 });
369}
370
371- (void)display
372{
373 CGFloat cursorWidth = 2;
374 CGPoint origin = _cursorRectangle.origin;
375 CGSize size = _cursorRectangle.size;
376 CGFloat scale = ((QIOSHandleLayer *)[self presentationLayer]).handleScale;
377 CGFloat edgeAdjustment = (_selectionEdge == Qt::LeftEdge) ? 0.5 - cursorWidth : -0.5;
378
379 CGFloat cursorX = origin.x + (size.width / 2) + edgeAdjustment;
380 CGFloat cursorY = origin.y;
381 CGFloat knobX = cursorX - (kKnobWidth - cursorWidth) / 2;
382 CGFloat knobY = origin.y + ((_selectionEdge == Qt::LeftEdge) ? -kKnobWidth : size.height);
383
384 _handleCursorLayer.frame = CGRectMake(cursorX, cursorY, cursorWidth, size.height);
385 _handleKnobLayer.frame = CGRectMake(knobX, knobY, kKnobWidth, kKnobWidth);
386 _handleCursorLayer.transform = CATransform3DMakeScale(1, scale, scale);
387 _handleKnobLayer.transform = CATransform3DMakeScale(scale, scale, scale);
388}
389
390@end
391
392// -------------------------------------------------------------------------
393
399@interface QIOSLoupeRecognizer : UIGestureRecognizer <UIGestureRecognizerDelegate>
400@property (nonatomic, assign) QPointF focalPoint;
401@property (nonatomic, assign) BOOL dragTriggersGesture;
402@property (nonatomic, readonly) UIView *focusView;
403@end
404
405@implementation QIOSLoupeRecognizer {
406 QIOSLoupeLayer *_loupeLayer;
412}
413
414- (instancetype)init
415{
416 if (self = [super initWithTarget:self action:@selector(gestureStateChanged)]) {
417 self.enabled = NO;
421 self.state = UIGestureRecognizerStateBegan;
422 });
423 }
424
425 return self;
426}
427
428- (void)setEnabled:(BOOL)enabled
429{
430 if (enabled == self.enabled)
431 return;
432
433 [super setEnabled:enabled];
434
435 if (enabled) {
436 _focusView = [reinterpret_cast<UIView *>(qApp->focusWindow()->winId()) retain];
437 _desktopView = [presentationWindow(nullptr).rootViewController.view retain];
439 [_desktopView addGestureRecognizer:self];
440 } else {
441 [_desktopView removeGestureRecognizer:self];
442 [_desktopView release];
443 _desktopView = nil;
444 [_focusView release];
445 _focusView = nil;
447 if (_loupeLayer) {
448 [_loupeLayer removeFromSuperlayer];
449 [_loupeLayer release];
450 _loupeLayer = nil;
451 }
452 }
453}
454
455- (void)gestureStateChanged
456{
457 switch (self.state) {
458 case UIGestureRecognizerStateBegan:
459 // Stop cursor blinking, and show the loupe
461 QGuiApplication::styleHints()->setCursorFlashTime(0);
462 if (!_loupeLayer)
463 [self createLoupe];
464 [self updateFocalPoint:QPointF::fromCGPoint(_lastTouchPoint)];
465 _loupeLayer.visible = YES;
467 break;
468 case UIGestureRecognizerStateChanged:
469 // Tell the sub class to move the loupe to the correct position
470 [self updateFocalPoint:QPointF::fromCGPoint(_lastTouchPoint)];
471 break;
472 case UIGestureRecognizerStateEnded:
473 // Restore cursor blinking, and hide the loupe
476 _loupeLayer.visible = NO;
477 break;
478 default:
479 _loupeLayer.visible = NO;
480 break;
481 }
482}
483
484- (void)createLoupe
485{
486 // We magnify the desktop view. But the loupe itself will be added as a child
487 // of the desktop view's parent, so it doesn't become a part of what we magnify.
488 _loupeLayer = [[self createLoupeLayer] retain];
489 _loupeLayer.targetView = _desktopView;
490 [_desktopView.superview.layer addSublayer:_loupeLayer];
491}
492
494{
495 return QPointF::fromCGPoint([_loupeLayer.targetView convertPoint:_loupeLayer.focalPoint toView:_focusView]);
496}
497
498- (void)setFocalPoint:(QPointF)point
499{
500 _loupeLayer.focalPoint = [_loupeLayer.targetView convertPoint:point.toCGPoint() fromView:_focusView];
501}
502
503- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
504{
505 [super touchesBegan:touches withEvent:event];
506 if ([event allTouches].count > 1) {
507 // We only support text selection with one finger
508 self.state = UIGestureRecognizerStateFailed;
509 return;
510 }
511
512 _firstTouchPoint = [static_cast<UITouch *>([touches anyObject]) locationInView:_focusView];
514
515 // If the touch point is accepted by the sub class (e.g touch on cursor), we start a
516 // press'n'hold timer that eventually will move the state to UIGestureRecognizerStateBegan.
517 if ([self acceptTouchesBegan:QPointF::fromCGPoint(_firstTouchPoint)])
519 else
520 self.state = UIGestureRecognizerStateFailed;
521}
522
523- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
524{
525 [super touchesMoved:touches withEvent:event];
526 _lastTouchPoint = [static_cast<UITouch *>([touches anyObject]) locationInView:_focusView];
527
528 if (self.state == UIGestureRecognizerStatePossible) {
529 // If the touch was moved too far before the timer triggered (meaning that this
530 // is a drag, not a press'n'hold), we should either fail, or trigger the gesture
531 // immediately, depending on self.dragTriggersGesture.
532 int startDragDistance = QGuiApplication::styleHints()->startDragDistance();
533 int dragDistance = hypot(_firstTouchPoint.x - _lastTouchPoint.x, _firstTouchPoint.y - _lastTouchPoint.y);
534 if (dragDistance > startDragDistance) {
536 self.state = self.dragTriggersGesture ? UIGestureRecognizerStateBegan : UIGestureRecognizerStateFailed;
537 }
538 } else {
539 self.state = UIGestureRecognizerStateChanged;
540 }
541}
542
543- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
544{
545 [super touchesEnded:touches withEvent:event];
547 _lastTouchPoint = [static_cast<UITouch *>([touches anyObject]) locationInView:_focusView];
548 self.state = self.state == UIGestureRecognizerStatePossible ? UIGestureRecognizerStateFailed : UIGestureRecognizerStateEnded;
549}
550
551- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
552{
553 [super touchesCancelled:touches withEvent:event];
555 _lastTouchPoint = [static_cast<UITouch *>([touches anyObject]) locationInView:_focusView];
556 self.state = UIGestureRecognizerStateCancelled;
557}
558
559// Methods implemented by subclasses:
560
561- (BOOL)acceptTouchesBegan:(QPointF)touchPoint
562{
563 Q_UNUSED(touchPoint);
564 Q_UNREACHABLE();
565 return NO;
566}
567
568- (QIOSLoupeLayer *)createLoupeLayer
569{
570 Q_UNREACHABLE();
571 return nullptr;
572}
573
574- (void)updateFocalPoint:(QPointF)touchPoint
575{
576 Q_UNUSED(touchPoint);
577 Q_UNREACHABLE();
578}
579
580@end
581
582// -------------------------------------------------------------------------
583
590@end
591
592@implementation QIOSCursorRecognizer
593
594- (QIOSLoupeLayer *)createLoupeLayer
595{
596 return [[[QIOSLoupeLayer alloc] initWithSize:CGSizeMake(120, 120) cornerRadius:60 bottomOffset:4] autorelease];
597}
598
599- (BOOL)acceptTouchesBegan:(QPointF)touchPoint
600{
602 return !hasSelection() && inputRect.contains(touchPoint);
603}
604
605- (void)updateFocalPoint:(QPointF)touchPoint
606{
607 self.focalPoint = touchPoint;
608
609 const int currentCursorPos = QInputMethod::queryFocusObject(Qt::ImCursorPosition, QVariant()).toInt();
610 const int newCursorPos = QPlatformInputContext::queryFocusObject(Qt::ImCursorPosition, touchPoint).toInt();
611 if (newCursorPos != currentCursorPos)
613}
614
615@end
616
617// -------------------------------------------------------------------------
618
625@end
626
627@implementation QIOSSelectionRecognizer {
628 CALayer *_clipRectLayer;
639}
640
641- (instancetype)init
642{
643 if (self = [super init]) {
644 self.delaysTouchesBegan = YES;
645 self.dragTriggersGesture = YES;
649 QObject::connect(&_updateSelectionTimer, &QTimer::timeout, [self](){ [self updateSelection]; });
650 }
651
652 return self;
653}
654
655- (void)setEnabled:(BOOL)enabled
656{
657 if (enabled == self.enabled)
658 return;
659
660 [super setEnabled:enabled];
661
662 if (enabled) {
663 // Create a layer that clips the handles inside the input field
664 _clipRectLayer = [CALayer new];
665 _clipRectLayer.masksToBounds = YES;
666 [self.focusView.layer addSublayer:_clipRectLayer];
667
668 // Create the handle layers, and add them to the clipped input rect layer
669 _cursorLayer = [[[QIOSHandleLayer alloc] initWithKnobAtEdge:Qt::RightEdge] autorelease];
670 _anchorLayer = [[[QIOSHandleLayer alloc] initWithKnobAtEdge:Qt::LeftEdge] autorelease];
671 bool selection = hasSelection();
674 [_clipRectLayer addSublayer:_cursorLayer];
675 [_clipRectLayer addSublayer:_anchorLayer];
676
677 // iOS text input will sometimes set a temporary text selection to perform operations
678 // such as backspace (select last character + cut selection). To avoid briefly showing
679 // the selection handles for such cases, and to avoid calling updateSelection when
680 // both handles and clip rectangle change, we use a timer to wait a cycle before we update.
681 // (Note that since QTimer::start is overloaded, we need some extra syntax for the connections).
687
688 [self updateSelection];
689 } else {
690 // Fade out the handles by setting visible to NO, and wait for the animations
691 // to finish before removing the clip rect layer, including the handles.
692 // Create a local variable to hold the clipRectLayer while the animation is
693 // ongoing to ensure that any subsequent calls to setEnabled does not interfere.
694 // Also, declare it as __block to stop it from being automatically retained, which
695 // would cause a cyclic dependency between clipRectLayer and the block.
696 __block CALayer *clipRectLayer = _clipRectLayer;
697 __block int handleCount = 2;
698 Block block = ^{
699 if (--handleCount == 0) {
700 [clipRectLayer removeFromSuperlayer];
701 [clipRectLayer release];
702 }
703 };
704
709
710 _clipRectLayer = 0;
711 _cursorLayer = 0;
712 _anchorLayer = 0;
714
718
721 }
722}
723
724- (QIOSLoupeLayer *)createLoupeLayer
725{
726 CGSize loupeSize = CGSizeMake(123, 33);
727 CGSize arrowSize = CGSizeMake(25, 12);
728 CGFloat loupeOffset = arrowSize.height + 20;
729
730 // Create loupe and arrow layers
731 QIOSLoupeLayer *loupeLayer = [[[QIOSLoupeLayer alloc] initWithSize:loupeSize cornerRadius:5 bottomOffset:loupeOffset] autorelease];
732 CAShapeLayer *arrowLayer = [[[CAShapeLayer alloc] init] autorelease];
733
734 // Build a triangular path to both draw and mask the arrow layer as a triangle
735 UIBezierPath *path = [[UIBezierPath new] autorelease];
736 [path moveToPoint:CGPointMake(0, 0)];
737 [path addLineToPoint:CGPointMake(arrowSize.width / 2, arrowSize.height)];
738 [path addLineToPoint:CGPointMake(arrowSize.width, 0)];
739
740 arrowLayer.frame = CGRectMake((loupeSize.width - arrowSize.width) / 2, loupeSize.height - 1, arrowSize.width, arrowSize.height);
741 arrowLayer.path = path.CGPath;
742 arrowLayer.backgroundColor = [[UIColor whiteColor] CGColor];
743 arrowLayer.strokeColor = [[UIColor lightGrayColor] CGColor];
744 arrowLayer.lineWidth = 0.75 * 2;
745 arrowLayer.fillColor = nil;
746
747 CAShapeLayer *mask = [[CAShapeLayer new] autorelease];
748 mask.frame = arrowLayer.bounds;
749 mask.path = path.CGPath;
750 arrowLayer.mask = mask;
751
752 [loupeLayer addSublayer:arrowLayer];
753
754 return loupeLayer;
755}
756
757- (BOOL)acceptTouchesBegan:(QPointF)touchPoint
758{
759 if (!hasSelection())
760 return NO;
761
762 // Accept the touch if it "overlaps" with any of the handles
763 const int handleRadius = 50;
764 QPointF cursorCenter = QPlatformInputContext::cursorRectangle().center();
765 QPointF anchorCenter = QPlatformInputContext::anchorRectangle().center();
766 QPointF cursorOffset = QPointF(cursorCenter.x() - touchPoint.x(), cursorCenter.y() - touchPoint.y());
767 QPointF anchorOffset = QPointF(anchorCenter.x() - touchPoint.x(), anchorCenter.y() - touchPoint.y());
768 double cursorDist = hypot(cursorOffset.x(), cursorOffset.y());
769 double anchorDist = hypot(anchorOffset.x(), anchorOffset.y());
770
771 if (cursorDist > handleRadius && anchorDist > handleRadius)
772 return NO;
773
774 if (cursorDist < anchorDist) {
775 _touchOffset = cursorOffset;
776 _dragOnCursor = YES;
777 _dragOnAnchor = NO;
778 } else {
779 _touchOffset = anchorOffset;
780 _dragOnCursor = NO;
781 _dragOnAnchor = YES;
782 }
783
784 return YES;
785}
786
787- (void)updateFocalPoint:(QPointF)touchPoint
788{
789 touchPoint += _touchOffset;
790
791 // Get the text position under the touch
793 int touchTextPos = QPlatformInputContext::queryFocusObject(Qt::ImCursorPosition, touchPoint).toInt();
794
795 // Ensure that the handles cannot be dragged past each other
796 if (_dragOnCursor)
797 selection.second = (touchTextPos > selection.first) ? touchTextPos : selection.first + 1;
798 else
799 selection.first = (touchTextPos < selection.second) ? touchTextPos : selection.second - 1;
800
801 // Set new selection
802 QList<QInputMethodEvent::Attribute> imAttributes;
803 imAttributes.append(QInputMethodEvent::Attribute(
805 QInputMethodEvent event(QString(), imAttributes);
806 QGuiApplication::sendEvent(qApp->focusObject(), &event);
807
808 // Move loupe to new position
809 QRectF handleRect = _dragOnCursor ?
811 QPlatformInputContext::anchorRectangle();
812 self.focalPoint = QPointF(touchPoint.x(), handleRect.center().y());
813}
814
816{
817 if (!hasSelection()) {
818 if (_cursorLayer.visible) {
821 }
824 return;
825 }
826
828 // Since the edit menu is hiding and this is the first selection thereafter, we
829 // assume that the selection came from the user tapping on a menu item. In that
830 // case, we reshow the menu after it has closed (but then with selection based
831 // menu items, as specified by first responder).
832 QIOSTextInputOverlay::s_editMenu.reshowAfterHidden = YES;
833 }
834
835 // Adjust handles and input rect to match the new selection
837 CGRect cursorRect = QPlatformInputContext::cursorRectangle().toCGRect();
838 CGRect anchorRect = QPlatformInputContext::anchorRectangle().toCGRect();
839
840 if (!_multiLine) {
841 // Resize the layer a bit bigger to ensure that the handles are
842 // not cut if if they are otherwise visible inside the clip rect.
843 int margin = kKnobWidth + 5;
844 inputRect.adjust(-margin / 2, -margin, margin / 2, margin);
845 }
846
847 executeBlockWithoutAnimation(^{ _clipRectLayer.frame = inputRect.toCGRect(); });
848 _cursorLayer.cursorRectangle = [self.focusView.layer convertRect:cursorRect toLayer:_clipRectLayer];
849 _anchorLayer.cursorRectangle = [self.focusView.layer convertRect:anchorRect toLayer:_clipRectLayer];
850 _cursorLayer.visible = YES;
851 _anchorLayer.visible = YES;
852}
853
854@end
855
856// -------------------------------------------------------------------------
857
863@interface QIOSTapRecognizer : UITapGestureRecognizer
864@end
865
866@implementation QIOSTapRecognizer {
867 int _cursorPosOnPress;
869 UIView *_focusView;
870}
871
872- (instancetype)init
873{
874 if (self = [super initWithTarget:self action:@selector(gestureStateChanged)]) {
875 self.enabled = NO;
876 }
877
878 return self;
879}
880
881- (void)setEnabled:(BOOL)enabled
882{
883 if (enabled == self.enabled)
884 return;
885
886 [super setEnabled:enabled];
887
888 if (enabled) {
889 _focusView = [reinterpret_cast<UIView *>(qApp->focusWindow()->winId()) retain];
890 [_focusView addGestureRecognizer:self];
891 } else {
892 [_focusView removeGestureRecognizer:self];
893 [_focusView release];
894 _focusView = nil;
895 }
896}
897
898- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
899{
900 [super touchesBegan:touches withEvent:event];
901
903 QPointF touchPos = QPointF::fromCGPoint([static_cast<UITouch *>([touches anyObject]) locationInView:_focusView]);
904 const bool touchInsideInputArea = inputRect.contains(touchPos);
905
906 if (touchInsideInputArea && hasSelection()) {
907 // When we have a selection and the user taps inside the input area, we stop
908 // tracking, and let Qt handle the event like normal. Unless the selection
909 // recogniser is triggered instead (if the touch is on top of the selection
910 // handles) this will typically result in Qt clearing the selection, which in
911 // turn will make the selection recogniser hide the menu.
912 self.state = UIGestureRecognizerStateFailed;
913 return;
914 }
915
917 // When the menu is visible and there is no selection, we should always
918 // hide it, regardless of where the user tapped on the screen. We achieve
919 // this by continue tracking so that we receive a touchesEnded call.
920 // But note, we only want to hide the menu, and not clear the selection.
921 // Only when the user taps inside the input area do we want to clear the
922 // selection as well. This is different from native behavior, but done so
923 // deliberately for cross-platform consistency. This will let the user click on
924 // e.g "Bold" and "Italic" buttons elsewhere in the UI to modify the selected text.
925 return;
926 }
927
928 if (!touchInsideInputArea) {
929 // If the menu is not showing, and the touch is outside the input
930 // area, there is nothing left for this recogniser to do.
931 self.state = UIGestureRecognizerStateFailed;
932 return;
933 }
934
935 // When no menu is showing, and the touch is inside the input
936 // area, we check if we should show it. We want to do so if
937 // the tap doesn't result in the cursor changing position.
938 _cursorPosOnPress = QInputMethod::queryFocusObject(Qt::ImCursorPosition, QVariant()).toInt();
939}
940
941- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
942{
944 _menuShouldBeVisible = false;
945 } else {
946 QPointF touchPos = QPointF::fromCGPoint([static_cast<UITouch *>([touches anyObject]) locationInView:_focusView]);
947 int cursorPosOnRelease = QPlatformInputContext::queryFocusObject(Qt::ImCursorPosition, touchPos).toInt();
948
949 if (cursorPosOnRelease == _cursorPosOnPress) {
950 // We've recognized a gesture to open the menu, but we don't know
951 // whether the user tapped a control that was overlaid our input
952 // area, since we don't do any granular hit-testing in touchesBegan.
953 // To ensure that the gesture doesn't eat touch events that should
954 // have reached another UI control we report the gesture as failed
955 // here, and then manually show the menu at the next runloop pass.
957 self.state = UIGestureRecognizerStateFailed;
958 dispatch_async(dispatch_get_main_queue(), ^{
960 });
961 } else {
962 // The menu is hidden, and the cursor will change position once
963 // Qt receive the touch release. We therefore fail so that we
964 // don't block the touch event from further processing.
965 self.state = UIGestureRecognizerStateFailed;
966 }
967 }
968
969 [super touchesEnded:touches withEvent:event];
970}
971
972- (void)gestureStateChanged
973{
974 if (self.state != UIGestureRecognizerStateEnded)
975 return;
976
978}
979
980@end
981
982// -------------------------------------------------------------------------
983
985
987
989 : m_cursorRecognizer(nullptr)
990 , m_selectionRecognizer(nullptr)
991 , m_openMenuOnTapRecognizer(nullptr)
992{
994 qWarning() << "text input overlays disabled in application extensions";
995 return;
996 }
997
998 connect(qApp, &QGuiApplication::focusObjectChanged, this, &QIOSTextInputOverlay::updateFocusObject);
999}
1000
1002{
1003 if (qApp)
1004 disconnect(qApp, 0, this, 0);
1005}
1006
1007void QIOSTextInputOverlay::updateFocusObject()
1008{
1009 // Destroy old recognizers since they were created with
1010 // dependencies to the old focus object (focus view).
1011 if (m_cursorRecognizer) {
1012 m_cursorRecognizer.enabled = NO;
1013 [m_cursorRecognizer release];
1014 m_cursorRecognizer = nullptr;
1015 }
1016 if (m_selectionRecognizer) {
1017 m_selectionRecognizer.enabled = NO;
1018 [m_selectionRecognizer release];
1019 m_selectionRecognizer = nullptr;
1020 }
1021 if (m_openMenuOnTapRecognizer) {
1022 m_openMenuOnTapRecognizer.enabled = NO;
1023 [m_openMenuOnTapRecognizer release];
1024 m_openMenuOnTapRecognizer = nullptr;
1025 }
1026
1027 if (s_editMenu) {
1028 [s_editMenu release];
1029 s_editMenu = nullptr;
1030 }
1031
1032 const QVariant hintsVariant = QGuiApplication::inputMethod()->queryFocusObject(Qt::ImHints, QVariant());
1033 const Qt::InputMethodHints hints = Qt::InputMethodHints(hintsVariant.toUInt());
1034 if (hints & Qt::ImhNoTextHandles)
1035 return;
1036
1037 // The focus object can emit selection updates (e.g from mouse drag), and
1038 // accept modifying it through IM when dragging on the handles, even if it
1039 // doesn't accept text input and IM in general (and hence return false from
1040 // inputMethodAccepted()). This is the case for read-only text fields.
1041 // Therefore, listen for selection changes also when the focus object
1042 // reports that it's ImReadOnly (which we take as a hint that it's actually
1043 // a text field, and that selections therefore might happen). But since
1044 // we have no guarantee that the focus object can actually accept new selections
1045 // through IM (and since we also need to respect if the input accepts selections
1046 // in the first place), we only support selections started by the text field (e.g from
1047 // mouse drag), even if we in theory could also start selections from a loupe.
1048
1049 const bool inputAccepted = platformInputContext()->inputMethodAccepted();
1050 const bool readOnly = QGuiApplication::inputMethod()->queryFocusObject(Qt::ImReadOnly, QVariant()).toBool();
1051
1052 if (inputAccepted || readOnly) {
1053 if (!(hints & Qt::ImhNoEditMenu))
1054 s_editMenu = [QIOSEditMenu new];
1055 m_selectionRecognizer = [QIOSSelectionRecognizer new];
1056 m_openMenuOnTapRecognizer = [QIOSTapRecognizer new];
1057 m_selectionRecognizer.enabled = YES;
1058 m_openMenuOnTapRecognizer.enabled = YES;
1059 }
1060
1061 if (inputAccepted) {
1062 m_cursorRecognizer = [QIOSCursorRecognizer new];
1063 m_cursorRecognizer.enabled = YES;
1064 }
1065}
1066
static bool sendEvent(QObject *receiver, QEvent *event)
Sends event event directly to receiver receiver, using the notify() function.
static QObject * focusObject()
Returns the QObject in currently active window that will be final receiver of events tied to focus,...
void focusObjectChanged(QObject *focusObject)
This signal is emitted when final receiver of events tied to focus is changed.
static QStyleHints * styleHints()
Returns the application's style hints.
static QInputMethod * inputMethod()
returns the input method.
static QIOSEditMenu * s_editMenu
The QInputMethodEvent class provides parameters for input method events.
Definition qevent.h:625
The QInputMethodQueryEvent class provides an event sent by the input context to input objects.
Definition qevent.h:679
The QInputMethod class provides access to the active text input method.
void anchorRectangleChanged()
static QVariant queryFocusObject(Qt::InputMethodQuery query, const QVariant &argument)
Send query to the current focus object with parameters argument and return the result.
void inputItemClipRectangleChanged()
void cursorRectangleChanged()
T & first()
Definition qlist.h:645
\inmodule QtCore Represents a handle to a signal-slot (or signal-functor) connection.
static QObjectPrivate * get(QObject *o)
Definition qobject_p.h:150
static QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *member, Qt::ConnectionType=Qt::AutoConnection)
\threadsafe
Definition qobject.cpp:2960
static bool disconnect(const QObject *sender, const char *signal, const QObject *receiver, const char *member)
\threadsafe
Definition qobject.cpp:3236
The QPlatformInputContext class abstracts the input method dependent data and composing state.
static QRectF anchorRectangle()
QPlatformInputContext::anchorRectangle.
static void setSelectionOnFocusObject(const QPointF &anchorPos, const QPointF &cursorPos)
QPlatformInputContext::setSelectionOnFocusObject.
static QVariant queryFocusObject(Qt::InputMethodQuery query, QPointF position)
QPlatformInputContext::queryFocusObject.
static QRectF inputItemClipRectangle()
QPlatformInputContext::inputItemClipRectangle.
static QRectF cursorRectangle()
QPlatformInputContext::cursorRectangle.
static QRectF inputItemRectangle()
QPlatformInputContext::inputItemRectangle.
\inmodule QtCore\reentrant
Definition qpoint.h:217
constexpr qreal x() const noexcept
Returns the x coordinate of this point.
Definition qpoint.h:343
constexpr qreal y() const noexcept
Returns the y coordinate of this point.
Definition qpoint.h:348
\inmodule QtCore\reentrant
Definition qrect.h:484
QRectF united(const QRectF &other) const noexcept
Definition qrect.h:852
constexpr int width() const noexcept
Returns the width.
Definition qsize.h:130
\macro QT_RESTRICTED_CAST_FROM_ASCII
Definition qstring.h:129
\inmodule QtCore
Definition qtimer.h:20
void setSingleShot(bool singleShot)
Definition qtimer.cpp:552
void start(int msec)
Starts or restarts the timer with a timeout interval of msec milliseconds.
Definition qtimer.cpp:241
void setInterval(int msec)
Definition qtimer.cpp:579
void stop()
Stops the timer.
Definition qtimer.cpp:267
void timeout(QPrivateSignal)
This signal is emitted when the timer times out.
\inmodule QtCore
Definition qvariant.h:65
QSize size
the size of the widget excluding any window frame
Definition qwidget.h:113
struct wl_display * display
Definition linuxdmabuf.h:41
Combined button and popup list for selecting options.
void updateSelection(int selStart, int selEnd, int candidatesStart, int candidatesEnd)
Definition qcompare.h:63
@ ImCursorPosition
@ ImReadOnly
@ ImAnchorPosition
@ ImHints
@ ImhNoEditMenu
@ ImhNoTextHandles
@ ImhMultiLine
@ LeftEdge
QString self
Definition language.cpp:58
static jboolean copy(JNIEnv *, jobject)
float CGFloat
bool qt_apple_isApplicationExtension()
Definition qcore_mac.mm:424
#define qApp
DBusConnection const char DBusError DBusBusType DBusError return DBusConnection DBusHandleMessageFunction void DBusFreeFunction return DBusConnection return DBusConnection return const char DBusError return DBusConnection DBusMessage dbus_uint32_t return DBusConnection dbus_bool_t DBusConnection DBusAddWatchFunction DBusRemoveWatchFunction DBusWatchToggledFunction void DBusFreeFunction return DBusConnection DBusDispatchStatusFunction void DBusFreeFunction DBusTimeout return DBusTimeout return DBusWatch return DBusWatch unsigned int return DBusError const DBusError return const DBusMessage return DBusMessage return DBusMessage return DBusMessage return DBusMessage return DBusMessage return DBusMessageIter int const void return DBusMessageIter DBusMessageIter return DBusMessageIter void DBusMessageIter void int return DBusMessage DBusMessageIter return DBusMessageIter return DBusMessageIter DBusMessageIter const char const char const char const char return DBusMessage return DBusMessage const char return DBusMessage dbus_bool_t return DBusMessage dbus_uint32_t return DBusMessage void
static QUIView * focusView()
UIView * _desktopView
QTimer _triggerStateBeganTimer
bool _dragOnCursor
static SelectionPair querySelection()
UIView * _focusView
BOOL _pendingSnapshotUpdate
bool _menuShouldBeVisible
QMetaObject::Connection _clipRectConnection
bool _multiLine
int _originalCursorFlashTime
static const CGFloat kKnobWidth
CALayer * _containerLayer
CALayer * _handleKnobLayer
CGPoint _firstTouchPoint
Qt::Edge _selectionEdge
QPointF _touchOffset
QMetaObject::Connection _anchorConnection
QTimer _updateTimer
CGFloat _loupeOffset
QIOSHandleLayer * _anchorLayer
QTimer _updateSelectionTimer
CGPoint _lastTouchPoint
QIOSHandleLayer * _cursorLayer
UIView * _loupeImageView
static bool hasSelection()
static void executeBlockWithoutAnimation(Block block)
QMetaObject::Connection _cursorConnection
QPair< int, int > SelectionPair
bool _dragOnAnchor
void(^ Block)(void)
static QPlatformInputContext * platformInputContext()
#define qWarning
Definition qlogging.h:166
GLint GLint GLint GLint GLint x
[0]
GLuint64 key
GLfloat GLfloat GLfloat w
[0]
GLint GLsizei GLsizei height
GLenum GLuint GLintptr GLsizeiptr size
[1]
GLenum GLuint id
[7]
GLenum GLenum GLsizei count
GLint GLint GLint GLint GLsizei GLsizei GLsizei GLboolean commit
GLenum GLenum GLsizei const GLuint GLboolean enabled
GLuint GLuint focalPoint
GLint GLsizei width
GLuint start
GLint first
GLint GLint GLint GLint GLint GLint GLint GLbitfield mask
GLint y
GLfloat GLfloat GLfloat GLfloat h
struct _cl_event * event
GLenum query
GLsizei const GLchar *const * path
GLenum GLenum GLenum GLenum GLenum scale
QT_BEGIN_NAMESPACE constexpr decltype(auto) qMakePair(T1 &&value1, T2 &&value2) noexcept(noexcept(std::make_pair(std::forward< T1 >(value1), std::forward< T2 >(value2))))
Definition qpair.h:19
#define Q_ASSERT(cond)
Definition qrandom.cpp:47
QtPrivate::QRegularExpressionMatchIteratorRangeBasedForIterator begin(const QRegularExpressionMatchIterator &iterator)
static QT_BEGIN_NAMESPACE void init(QTextBoundaryFinder::BoundaryType type, QStringView str, QCharAttributes *attributes)
#define Q_UNUSED(x)
settings setValue("DataPump/bgcolor", color)
QObject::connect nullptr
myObject disconnect()
[26]
QPropertyAnimation animation
[0]
QItemSelection * selection
[0]
QFrame frame
[0]