From 53d9d5920a249e177af5a759f26c70d398e7bbed Mon Sep 17 00:00:00 2001 From: Shawn Dempsey Date: Fri, 11 Nov 2022 15:18:23 -0800 Subject: [PATCH 1/5] Forward mouse clicks to NSTextView to prevent swallowing of events Change HitTest to forward events correctly Add MouseDown & forward events correctly Add touch handler static methods --- Libraries/Text/Text/RCTTextView.m | 51 +++++++++++++++++++++++++++++++ React/Base/RCTTouchHandler.h | 7 ++++- React/Base/RCTTouchHandler.m | 34 ++++++++++++++++++++- 3 files changed, 90 insertions(+), 2 deletions(-) diff --git a/Libraries/Text/Text/RCTTextView.m b/Libraries/Text/Text/RCTTextView.m index 56973f2305fa29..60e9c32bf1adcf 100644 --- a/Libraries/Text/Text/RCTTextView.m +++ b/Libraries/Text/Text/RCTTextView.m @@ -17,6 +17,8 @@ #import // TODO(OSS Candidate ISS#2710739) #import +#import +#import #import @@ -407,6 +409,18 @@ - (void)handleLongPress:(UILongPressGestureRecognizer *)gesture } #else // [TODO(macOS GH#774) +- (NSView *)hitTest:(NSPoint)point +{ + // We will forward mouse click events to the NSTextView ourselves to prevent NSTextView from swallowing events that may be handled in JS (e.g. long press). + NSView *hitView = [super hitTest:point]; + NSEventType eventType = NSApp.currentEvent.type; + BOOL isMouseClickEvent = NSEvent.pressedMouseButtons > 0; + BOOL isMouseMoveEventType = eventType == NSEventTypeMouseMoved || eventType == NSEventTypeMouseEntered || eventType == NSEventTypeMouseExited || eventType == NSEventTypeCursorUpdate; + BOOL isMouseMoveEvent = !isMouseClickEvent && isMouseMoveEventType; + BOOL isTextViewClick = hitView && hitView == _textView && !isMouseMoveEvent; + return isTextViewClick ? self : hitView; +} + - (void)rightMouseDown:(NSEvent *)event { if (_selectable == NO) { @@ -439,6 +453,43 @@ - (void)rightMouseDown:(NSEvent *)event } } } + +- (void)mouseDown:(NSEvent *)event +{ + if (!self.selectable) { + [super mouseDown:event]; + return; + } + + // Double/triple-clicks should be forwarded to the NSTextView. + BOOL shouldForward = event.clickCount > 1; + + if (!shouldForward) { + // Peek at next event to know if a selection should begin. + NSEvent *nextEvent = [self.window nextEventMatchingMask:NSLeftMouseUpMask | NSLeftMouseDraggedMask + untilDate:[NSDate distantFuture] + inMode:NSEventTrackingRunLoopMode + dequeue:NO]; + shouldForward = nextEvent.type == NSLeftMouseDragged; + } + + if (shouldForward) { + NSView *contentView = self.window.contentView; + // -[NSView hitTest:] takes coordinates in a view's superview coordinate system. + NSPoint point = [contentView.superview convertPoint:event.locationInWindow fromView:nil]; + + // Start selection if we're still selectable and hit-testable. + if (self.selectable && [contentView hitTest:point] == self) { + [[RCTTouchHandler touchHandlerForView:self] cancelTouchWithEvent:event]; + [self.window makeFirstResponder:_textView]; + [_textView mouseDown:event]; + } + } else { + // Clear selection for single clicks. + _textView.selectedRange = NSMakeRange(NSNotFound, 0); + } +} + #endif // ]TODO(macOS GH#774) #pragma mark - Selection diff --git a/React/Base/RCTTouchHandler.h b/React/Base/RCTTouchHandler.h index ab50e8af2f03be..7b0048c8497772 100644 --- a/React/Base/RCTTouchHandler.h +++ b/React/Base/RCTTouchHandler.h @@ -19,8 +19,13 @@ - (void)detachFromView:(RCTUIView *)view; // TODO(macOS ISS#3536887) - (void)cancel; + #if TARGET_OS_OSX // [TODO(macOS GH#774) -- (void)willShowMenuWithEvent:(NSEvent*)event; ++ (instancetype)touchHandlerForEvent:(NSEvent *)event; ++ (instancetype)touchHandlerForView:(NSView *)view; + +- (void)willShowMenuWithEvent:(NSEvent *)event; +- (void)cancelTouchWithEvent:(NSEvent *)event; #endif // ]TODO(macOS GH#774) @end diff --git a/React/Base/RCTTouchHandler.m b/React/Base/RCTTouchHandler.m index 4459fc0dd4e9be..f49dec8faff85a 100644 --- a/React/Base/RCTTouchHandler.m +++ b/React/Base/RCTTouchHandler.m @@ -576,12 +576,44 @@ - (void)cancel } #if TARGET_OS_OSX // [TODO(macOS GH#774) -- (void)willShowMenuWithEvent:(NSEvent*)event + ++ (instancetype)touchHandlerForEvent:(NSEvent *)event { + // // The window's frame view must be used for hit testing against `locationInWindow` + NSView *hitView = [event.window.contentView.superview hitTest:event.locationInWindow]; + return [self touchHandlerForView:hitView]; +} + ++ (instancetype)touchHandlerForView:(NSView *)view { + if ([view isKindOfClass:[RCTRootView class]]) { + // The RCTTouchHandler is attached to the contentView. + view = ((RCTRootView *)view).contentView; + } + + while (view) { + for (NSGestureRecognizer *gestureRecognizer in view.gestureRecognizers) { + if ([gestureRecognizer isKindOfClass:[self class]]) { + return (RCTTouchHandler *)gestureRecognizer; + } + } + + view = view.superview; + } + + return nil; +} + + - (void)willShowMenuWithEvent:(NSEvent*)event { if (event.type == NSEventTypeRightMouseDown) { [self interactionsEnded:[NSSet setWithObject:event] withEvent:event]; } } + +- (void)cancelTouchWithEvent:(NSEvent*)event +{ + [self interactionsCancelled:[NSSet setWithObject:event] withEvent:event]; +} + #endif // ]TODO(macOS GH#774) #pragma mark - UIGestureRecognizerDelegate From c19fd47c9e8ab6ceedccda6dd08d67e65af5b2dd Mon Sep 17 00:00:00 2001 From: Shawn Dempsey Date: Wed, 16 Nov 2022 14:39:17 -0800 Subject: [PATCH 2/5] Show the NSTextView contextMenu on right click --- Libraries/Text/Text/RCTTextView.m | 34 ++++--------------- .../TextInput/Singleline/RCTUITextField.m | 13 +++++-- React/Base/RCTTouchHandler.m | 4 +-- 3 files changed, 20 insertions(+), 31 deletions(-) diff --git a/Libraries/Text/Text/RCTTextView.m b/Libraries/Text/Text/RCTTextView.m index 60e9c32bf1adcf..92a0799340cc3f 100644 --- a/Libraries/Text/Text/RCTTextView.m +++ b/Libraries/Text/Text/RCTTextView.m @@ -18,7 +18,6 @@ #import #import -#import #import @@ -413,45 +412,26 @@ - (NSView *)hitTest:(NSPoint)point { // We will forward mouse click events to the NSTextView ourselves to prevent NSTextView from swallowing events that may be handled in JS (e.g. long press). NSView *hitView = [super hitTest:point]; + NSEventType eventType = NSApp.currentEvent.type; BOOL isMouseClickEvent = NSEvent.pressedMouseButtons > 0; BOOL isMouseMoveEventType = eventType == NSEventTypeMouseMoved || eventType == NSEventTypeMouseEntered || eventType == NSEventTypeMouseExited || eventType == NSEventTypeCursorUpdate; BOOL isMouseMoveEvent = !isMouseClickEvent && isMouseMoveEventType; - BOOL isTextViewClick = hitView && hitView == _textView && !isMouseMoveEvent; + BOOL isTextViewClick = (hitView && hitView == _textView) && !isMouseMoveEvent; + return isTextViewClick ? self : hitView; } - (void)rightMouseDown:(NSEvent *)event { - if (_selectable == NO) { + + if (self.selectable == NO) { [super rightMouseDown:event]; return; } - NSText *fieldEditor = [self.window fieldEditor:YES forObject:self]; - NSMenu *fieldEditorMenu = [fieldEditor menuForEvent:event]; - - RCTAssert(fieldEditorMenu, @"Unable to obtain fieldEditor's context menu"); - - if (fieldEditorMenu) { - NSMenu *menu = [[NSMenu alloc] initWithTitle:@""]; - - for (NSMenuItem *fieldEditorMenuItem in fieldEditorMenu.itemArray) { - if (fieldEditorMenuItem.action == @selector(copy:)) { - NSMenuItem *item = [fieldEditorMenuItem copy]; - - item.target = self; - [menu addItem:item]; - break; - } - } - - RCTAssert(menu.numberOfItems > 0, @"Unable to create context menu with \"Copy\" item"); - - if (menu.numberOfItems > 0) { - [NSMenu popUpContextMenu:menu withEvent:event forView:self]; - } - } + [[RCTTouchHandler touchHandlerForView:self] cancelTouchWithEvent:event]; + [_textView rightMouseDown:event]; } - (void)mouseDown:(NSEvent *)event diff --git a/Libraries/Text/TextInput/Singleline/RCTUITextField.m b/Libraries/Text/TextInput/Singleline/RCTUITextField.m index 25e705018a992d..f56fde1542b4c5 100644 --- a/Libraries/Text/TextInput/Singleline/RCTUITextField.m +++ b/Libraries/Text/TextInput/Singleline/RCTUITextField.m @@ -12,7 +12,7 @@ #import #import // TODO(OSS Candidate ISS#2710739) #import - +#import #if TARGET_OS_OSX // [TODO(macOS GH#774) @@ -430,7 +430,7 @@ - (CGRect)editingRectForBounds:(CGRect)bounds #else // [TODO(macOS GH#774) -#pragma mark - NSTextViewDelegate methods +#pragma mark - NSTextFieldDelegate methods - (void)textDidChange:(NSNotification *)notification { @@ -467,6 +467,15 @@ - (BOOL)textView:(NSTextView *)aTextView shouldChangeTextInRange:(NSRange)aRange return NO; } +- (NSMenu *)textView:(NSTextView *)view menu:(NSMenu *)menu forEvent:(NSEvent *)event atIndex:(NSUInteger)charIndex +{ + if (menu) { + [[RCTTouchHandler touchHandlerForView:self] willShowMenuWithEvent:event]; + } + + return menu; +} + #endif // ]TODO(macOS GH#774) #pragma mark - Overrides diff --git a/React/Base/RCTTouchHandler.m b/React/Base/RCTTouchHandler.m index f49dec8faff85a..2b5168bbba99ef 100644 --- a/React/Base/RCTTouchHandler.m +++ b/React/Base/RCTTouchHandler.m @@ -602,14 +602,14 @@ + (instancetype)touchHandlerForView:(NSView *)view { return nil; } - - (void)willShowMenuWithEvent:(NSEvent*)event +- (void)willShowMenuWithEvent:(NSEvent *)event { if (event.type == NSEventTypeRightMouseDown) { [self interactionsEnded:[NSSet setWithObject:event] withEvent:event]; } } -- (void)cancelTouchWithEvent:(NSEvent*)event +- (void)cancelTouchWithEvent:(NSEvent *)event { [self interactionsCancelled:[NSSet setWithObject:event] withEvent:event]; } From 6a81ed20a5295255cc1ea1aed1135ed42a074d91 Mon Sep 17 00:00:00 2001 From: Shawn Dempsey Date: Thu, 17 Nov 2022 11:40:37 -0800 Subject: [PATCH 3/5] No need to forward copy to nstextview since it will have first responder upon click --- Libraries/Text/Text/RCTTextView.m | 2 -- 1 file changed, 2 deletions(-) diff --git a/Libraries/Text/Text/RCTTextView.m b/Libraries/Text/Text/RCTTextView.m index 92a0799340cc3f..19f93213393076 100644 --- a/Libraries/Text/Text/RCTTextView.m +++ b/Libraries/Text/Text/RCTTextView.m @@ -564,8 +564,6 @@ - (void)copy:(id)sender UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; pasteboard.items = @[item]; #elif TARGET_OS_OSX - [_textView copy:sender]; - NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; [pasteboard clearContents]; [pasteboard setData:rtf forType:NSPasteboardTypeRTFD]; From 9256d2b7c6e4f8a072a76573b0d6924d00d9defc Mon Sep 17 00:00:00 2001 From: Shawn Dempsey Date: Fri, 18 Nov 2022 14:38:18 -0800 Subject: [PATCH 4/5] Fix Event mask deprecation warnings --- Libraries/Text/Text/RCTTextView.m | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Libraries/Text/Text/RCTTextView.m b/Libraries/Text/Text/RCTTextView.m index 19f93213393076..8c76ba6a3723cb 100644 --- a/Libraries/Text/Text/RCTTextView.m +++ b/Libraries/Text/Text/RCTTextView.m @@ -209,7 +209,6 @@ - (void)drawRect:(CGRect)rect return; } - NSLayoutManager *layoutManager = _textStorage.layoutManagers.firstObject; NSTextContainer *textContainer = layoutManager.textContainers.firstObject; @@ -446,11 +445,11 @@ - (void)mouseDown:(NSEvent *)event if (!shouldForward) { // Peek at next event to know if a selection should begin. - NSEvent *nextEvent = [self.window nextEventMatchingMask:NSLeftMouseUpMask | NSLeftMouseDraggedMask + NSEvent *nextEvent = [self.window nextEventMatchingMask:NSEventMaskLeftMouseUp | NSEventMaskLeftMouseDragged untilDate:[NSDate distantFuture] inMode:NSEventTrackingRunLoopMode dequeue:NO]; - shouldForward = nextEvent.type == NSLeftMouseDragged; + shouldForward = nextEvent.type == NSEventTypeLeftMouseDragged; } if (shouldForward) { From ed28d6747eba348ab0d6cfbb7ff6aa7678c27df5 Mon Sep 17 00:00:00 2001 From: Shawn Dempsey Date: Thu, 15 Dec 2022 16:50:42 -0800 Subject: [PATCH 5/5] Fix missing macOS tag & extra comment --- Libraries/Text/TextInput/Singleline/RCTUITextField.m | 2 +- React/Base/RCTTouchHandler.m | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Libraries/Text/TextInput/Singleline/RCTUITextField.m b/Libraries/Text/TextInput/Singleline/RCTUITextField.m index f56fde1542b4c5..12136ab80374f5 100644 --- a/Libraries/Text/TextInput/Singleline/RCTUITextField.m +++ b/Libraries/Text/TextInput/Singleline/RCTUITextField.m @@ -12,7 +12,7 @@ #import #import // TODO(OSS Candidate ISS#2710739) #import -#import +#import // [TODO(macOS GH#774) #if TARGET_OS_OSX // [TODO(macOS GH#774) diff --git a/React/Base/RCTTouchHandler.m b/React/Base/RCTTouchHandler.m index 2b5168bbba99ef..1fcc3447c7929f 100644 --- a/React/Base/RCTTouchHandler.m +++ b/React/Base/RCTTouchHandler.m @@ -578,7 +578,7 @@ - (void)cancel #if TARGET_OS_OSX // [TODO(macOS GH#774) + (instancetype)touchHandlerForEvent:(NSEvent *)event { - // // The window's frame view must be used for hit testing against `locationInWindow` + // The window's frame view must be used for hit testing against `locationInWindow` NSView *hitView = [event.window.contentView.superview hitTest:event.locationInWindow]; return [self touchHandlerForView:hitView]; }