diff --git a/Libraries/Text/Text/RCTTextView.m b/Libraries/Text/Text/RCTTextView.m index 9b8331207d1e19..178b9c918c8e5e 100644 --- a/Libraries/Text/Text/RCTTextView.m +++ b/Libraries/Text/Text/RCTTextView.m @@ -17,6 +17,7 @@ #import // [macOS] #import +#import #import @@ -208,7 +209,6 @@ - (void)drawRect:(CGRect)rect return; } - NSLayoutManager *layoutManager = _textStorage.layoutManagers.firstObject; NSTextContainer *textContainer = layoutManager.textContainers.firstObject; @@ -407,36 +407,65 @@ - (void)handleLongPress:(UILongPressGestureRecognizer *)gesture } #else // [macOS +- (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) { + + 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:@""]; + [[RCTTouchHandler touchHandlerForView:self] cancelTouchWithEvent:event]; + [_textView rightMouseDown:event]; +} - for (NSMenuItem *fieldEditorMenuItem in fieldEditorMenu.itemArray) { - if (fieldEditorMenuItem.action == @selector(copy:)) { - NSMenuItem *item = [fieldEditorMenuItem copy]; +- (void)mouseDown:(NSEvent *)event +{ + if (!self.selectable) { + [super mouseDown:event]; + return; + } - item.target = self; - [menu addItem:item]; + // Double/triple-clicks should be forwarded to the NSTextView. + BOOL shouldForward = event.clickCount > 1; - break; - } - } + if (!shouldForward) { + // Peek at next event to know if a selection should begin. + NSEvent *nextEvent = [self.window nextEventMatchingMask:NSEventMaskLeftMouseUp | NSEventMaskLeftMouseDragged + untilDate:[NSDate distantFuture] + inMode:NSEventTrackingRunLoopMode + dequeue:NO]; + shouldForward = nextEvent.type == NSEventTypeLeftMouseDragged; + } - RCTAssert(menu.numberOfItems > 0, @"Unable to create context menu with \"Copy\" item"); + 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]; - if (menu.numberOfItems > 0) { - [NSMenu popUpContextMenu:menu withEvent:event forView:self]; + // 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 // macOS] @@ -533,8 +562,6 @@ - (void)copy:(id)sender UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; pasteboard.items = @[item]; #else // [macOS - [_textView copy:sender]; - NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; [pasteboard clearContents]; [pasteboard setData:rtf forType:NSPasteboardTypeRTFD]; diff --git a/Libraries/Text/TextInput/Singleline/RCTUITextField.m b/Libraries/Text/TextInput/Singleline/RCTUITextField.m index e3dff048f729f5..f4bc8764f5b7ba 100644 --- a/Libraries/Text/TextInput/Singleline/RCTUITextField.m +++ b/Libraries/Text/TextInput/Singleline/RCTUITextField.m @@ -12,7 +12,7 @@ #import #import // [macOS] #import - +#import // [TODO(macOS GH#774) #if TARGET_OS_OSX // [macOS @@ -436,7 +436,7 @@ - (CGRect)editingRectForBounds:(CGRect)bounds #else // [macOS -#pragma mark - NSTextViewDelegate methods +#pragma mark - NSTextFieldDelegate methods - (void)textDidChange:(NSNotification *)notification { @@ -473,6 +473,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 // macOS] #pragma mark - Overrides diff --git a/React/Base/RCTTouchHandler.h b/React/Base/RCTTouchHandler.h index a1e6c8074f96dc..93c4e8c3deb469 100644 --- a/React/Base/RCTTouchHandler.h +++ b/React/Base/RCTTouchHandler.h @@ -19,8 +19,13 @@ - (void)detachFromView:(RCTUIView *)view; // [macOS] - (void)cancel; + #if TARGET_OS_OSX // [macOS -- (void)willShowMenuWithEvent:(NSEvent*)event; ++ (instancetype)touchHandlerForEvent:(NSEvent *)event; ++ (instancetype)touchHandlerForView:(NSView *)view; + +- (void)willShowMenuWithEvent:(NSEvent *)event; +- (void)cancelTouchWithEvent:(NSEvent *)event; #endif // macOS] @end diff --git a/React/Base/RCTTouchHandler.m b/React/Base/RCTTouchHandler.m index 3bfefc35db3880..5a409ec769a408 100644 --- a/React/Base/RCTTouchHandler.m +++ b/React/Base/RCTTouchHandler.m @@ -577,12 +577,42 @@ - (void)cancel } #if TARGET_OS_OSX // [macOS -- (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 // macOS] #pragma mark - UIGestureRecognizerDelegate