From 9a300e32e6fdc9effbbe15b06685ba31db3929a8 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sun, 3 May 2026 19:57:44 +0300 Subject: [PATCH] iOS: opt-in iosReturnExitsEditing client property for multi-line TextArea (#4854) Adds a guarded client property `iosReturnExitsEditing` that, when set on a multi-line TextArea, makes the iOS keyboard's Return key act as Done -- it exits editing (firing the Done listener) instead of inserting a newline. This mirrors the iOS Reminders task-title field. The Return key is relabeled to "Done" via UIReturnKeyDone while the flag is set. The flag is wired through the existing editStringAt native call (mirroring the blockCopyPaste/showToolbar pattern), gated to multi-line TextAreas only, and defaults to off so existing behavior is unchanged. The interception lives in textView:shouldChangeTextInRange:replacementText: in both EAGLView and METALView and only fires for an exact "\n" replacement, leaving pasted multi-line text untouched. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../nativeSources/CodenameOne_GLViewController.m | 14 +++++++++++++- Ports/iOSPort/nativeSources/EAGLView.m | 8 ++++++++ Ports/iOSPort/nativeSources/IOSNative.m | 8 ++++---- Ports/iOSPort/nativeSources/METALView.m | 8 ++++++++ .../com/codename1/impl/ios/IOSImplementation.java | 14 ++++++++++---- .../src/com/codename1/impl/ios/IOSNative.java | 7 ++++--- 6 files changed, 47 insertions(+), 12 deletions(-) diff --git a/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m b/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m index 8e423af2b0..3c28444e20 100644 --- a/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m +++ b/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m @@ -226,6 +226,7 @@ - (void)layoutSubviews { int orientationLock = 0; int upsideDownMultiplier = -1; int currentlyEditingMaxLength; +BOOL currentlyReturnExitsEditing = NO; #ifndef CN1_USE_ARC NSAutoreleasePool *globalCodenameOnePool; @@ -357,13 +358,17 @@ void cn1_setStyleDoneButton(CN1_THREAD_STATE_MULTI_ARG UIBarButtonItem* btn) { void Java_com_codename1_impl_ios_IOSImplementation_editStringAtImpl (CN1_THREAD_STATE_MULTI_ARG int x, int y, int w, int h, void* font, int isSingleLine, int rows, int maxSize, int constraint, const char* str, int len, BOOL forceSlideUp, - int color, JAVA_LONG imagePeer, int padTop, int padBottom, int padLeft, int padRight, NSString* hintString, int hintColor, BOOL showToolbar, BOOL blockCopyPaste, int alignment, int verticalAlignment) { + int color, JAVA_LONG imagePeer, int padTop, int padBottom, int padLeft, int padRight, NSString* hintString, int hintColor, BOOL showToolbar, BOOL blockCopyPaste, int alignment, int verticalAlignment, BOOL returnExitsEditing) { // don't show toolbar in iOS 8 in landscape since there is just no room for that... if(isIOS8() && displayHeight < displayWidth) { showToolbar = NO; } //CN1Log(@"Java_com_codename1_impl_ios_IOSImplementation_editStringAtImpl"); currentlyEditingMaxLength = maxSize; + // Honored by the UITextView shouldChangeTextInRange: delegate (EAGLView/METALView) to + // intercept Return on multi-line text areas when the iosReturnExitsEditing client + // property is set on the editing component. + currentlyReturnExitsEditing = returnExitsEditing && !isSingleLine; dispatch_sync(dispatch_get_main_queue(), ^{ if(editingComponent != nil) { [editingComponent resignFirstResponder]; @@ -615,6 +620,13 @@ void cn1_setStyleDoneButton(CN1_THREAD_STATE_MULTI_ARG UIBarButtonItem* btn) { } utv.text = [NSString stringWithUTF8String:str]; utv.delegate = [[CodenameOne_GLViewController instance] eaglView]; + + // When iosReturnExitsEditing is set on a multi-line TextArea, present the + // Return key as "Done" -- the actual exit-on-return is enforced by the + // shouldChangeTextInRange: delegate, which intercepts a "\n" replacement. + if (currentlyReturnExitsEditing) { + utv.returnKeyType = UIReturnKeyDone; + } // Apply constraints for multiline text view // INITIAL_CAPS_WORD diff --git a/Ports/iOSPort/nativeSources/EAGLView.m b/Ports/iOSPort/nativeSources/EAGLView.m index c59c4a8bbd..ff1e334306 100644 --- a/Ports/iOSPort/nativeSources/EAGLView.m +++ b/Ports/iOSPort/nativeSources/EAGLView.m @@ -376,12 +376,20 @@ -(void)textFieldDidChange { } extern int currentlyEditingMaxLength; +extern BOOL currentlyReturnExitsEditing; - (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string { NSUInteger newLength = (textField.text.length - range.length) + string.length; return (newLength <= currentlyEditingMaxLength); } -(BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { + // iosReturnExitsEditing: treat a Return keypress on a multi-line text view as Done. + // Only intercept a single "\n" replacement so pasted text containing newlines is + // unaffected. + if (currentlyReturnExitsEditing && [text isEqualToString:@"\n"]) { + [self keyboardDoneClicked]; + return NO; + } NSUInteger newLength = (textView.text.length - range.length) + text.length; return (newLength <= currentlyEditingMaxLength); } diff --git a/Ports/iOSPort/nativeSources/IOSNative.m b/Ports/iOSPort/nativeSources/IOSNative.m index 18d5c9dfb9..e47513549d 100644 --- a/Ports/iOSPort/nativeSources/IOSNative.m +++ b/Ports/iOSPort/nativeSources/IOSNative.m @@ -344,7 +344,7 @@ JAVA_OBJECT fromNSString(NSString* str) extern void Java_com_codename1_impl_ios_IOSImplementation_editStringAtImpl (CN1_THREAD_STATE_MULTI_ARG int x, int y, int w, int h, void* peer, int isSingleLine, int rows, int maxSize, int constraint, const char* str, int len, BOOL dialogHeight, int color, JAVA_LONG imagePeer, - int padTop, int padBottom, int padLeft, int padRight, NSString* hintString, int hintColor, BOOL showToolbar, BOOL blockCopyPaste, int alignment, int verticalAlignment); + int padTop, int padBottom, int padLeft, int padRight, NSString* hintString, int hintColor, BOOL showToolbar, BOOL blockCopyPaste, int alignment, int verticalAlignment, BOOL returnExitsEditing); extern void Java_com_codename1_impl_ios_IOSImplementation_resetAffineGlobal(); @@ -552,11 +552,11 @@ BOOL getBooleanClientProperty(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT o, NSString } #endif -void com_codename1_impl_ios_IOSNative_editStringAt___int_int_int_int_long_boolean_int_int_int_java_lang_String_boolean_int_long_int_int_int_int_java_lang_String_int_boolean_boolean_int_int(CN1_THREAD_STATE_MULTI_ARG +void com_codename1_impl_ios_IOSNative_editStringAt___int_int_int_int_long_boolean_int_int_int_java_lang_String_boolean_int_long_int_int_int_int_java_lang_String_int_boolean_boolean_int_int_boolean(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_INT n1, JAVA_INT n2, JAVA_INT n3, JAVA_INT n4, JAVA_LONG n5, JAVA_BOOLEAN n6, JAVA_INT n7, JAVA_INT n8, JAVA_INT n9, JAVA_OBJECT n10, JAVA_BOOLEAN forceSlide, JAVA_INT color, JAVA_LONG imagePeer, JAVA_INT padTop, JAVA_INT padBottom, JAVA_INT padLeft, JAVA_INT padRight, JAVA_OBJECT hint, JAVA_INT hintColor, JAVA_BOOLEAN showToolbar, JAVA_BOOLEAN blockCopyPaste, - JAVA_INT alignment, JAVA_INT verticalAlignment) + JAVA_INT alignment, JAVA_INT verticalAlignment, JAVA_BOOLEAN returnExitsEditing) { POOL_BEGIN(); const char* chr = stringToUTF8(CN1_THREAD_STATE_PASS_ARG n10); @@ -564,7 +564,7 @@ void com_codename1_impl_ios_IOSNative_editStringAt___int_int_int_int_long_boolea char cc[l]; memcpy(cc, chr, l); Java_com_codename1_impl_ios_IOSImplementation_editStringAtImpl(CN1_THREAD_STATE_PASS_ARG n1, n2, n3, n4, n5, n6, n7, n8, n9, cc, 0, forceSlide, color, imagePeer, - padTop, padBottom, padLeft, padRight, toNSString(CN1_THREAD_STATE_PASS_ARG hint), hintColor, showToolbar, blockCopyPaste, alignment, verticalAlignment); + padTop, padBottom, padLeft, padRight, toNSString(CN1_THREAD_STATE_PASS_ARG hint), hintColor, showToolbar, blockCopyPaste, alignment, verticalAlignment, returnExitsEditing); POOL_END(); } extern float scaleValue; diff --git a/Ports/iOSPort/nativeSources/METALView.m b/Ports/iOSPort/nativeSources/METALView.m index ea036c3eda..d686c8039a 100644 --- a/Ports/iOSPort/nativeSources/METALView.m +++ b/Ports/iOSPort/nativeSources/METALView.m @@ -272,12 +272,20 @@ -(void)textFieldDidChange { } extern int currentlyEditingMaxLength; +extern BOOL currentlyReturnExitsEditing; - (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string { NSUInteger newLength = (textField.text.length - range.length) + string.length; return (newLength <= currentlyEditingMaxLength); } -(BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { + // iosReturnExitsEditing: treat a Return keypress on a multi-line text view as Done. + // Only intercept a single "\n" replacement so pasted text containing newlines is + // unaffected. + if (currentlyReturnExitsEditing && [text isEqualToString:@"\n"]) { + [self keyboardDoneClicked]; + return NO; + } NSUInteger newLength = (textView.text.length - range.length) + text.length; return (newLength <= currentlyEditingMaxLength); } diff --git a/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java b/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java index 7a930b52e7..8187fe8ddc 100644 --- a/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java +++ b/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java @@ -1020,23 +1020,29 @@ public void run() { } if ( currentEditing != null ){ int align = currentEditing.getStyle().getAlignment(); + // iosReturnExitsEditing: opt-in client property that makes Return on a + // multi-line TextArea exit editing (firing the Done listener) instead of + // inserting a newline -- mirrors iOS Reminders task-title behavior. + boolean returnExitsEditing = Boolean.TRUE.equals(cmp.getClientProperty("iosReturnExitsEditing")) + && !currentEditing.isSingleLineTextArea(); nativeInstance.editStringAt(x, y, w, h, fnt.peer, currentEditing.isSingleLineTextArea(), currentEditing.getRows(), maxSize, constraint, text, forceSlideUp, - stl.getFgColor(), 0,//peer, + stl.getFgColor(), 0,//peer, pt, pb, pl, - pr, + pr, hint, hintColor, - showToolbar, + showToolbar, Boolean.TRUE.equals(cmp.getClientProperty("blockCopyPaste")), DefaultLookAndFeel.reverseAlignForBidi(cmp, align), - currentEditing.getVerticalAlignment()); + currentEditing.getVerticalAlignment(), + returnExitsEditing); } } }); diff --git a/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java b/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java index 056a5a2ed5..010801bd65 100644 --- a/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java +++ b/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java @@ -50,10 +50,11 @@ public final class IOSNative { native boolean isPainted(); native int getDisplayWidth(); native int getDisplayHeight(); - native void editStringAt(int x, int y, int w, int h, long peer, boolean singleLine, - int rows, int maxSize, int constraint, String text, boolean forceSlideUp, + native void editStringAt(int x, int y, int w, int h, long peer, boolean singleLine, + int rows, int maxSize, int constraint, String text, boolean forceSlideUp, int color, long imagePeer, int padTop, int padBottom, int padLeft, int padRight, - String hint, int hintColor, boolean showToolbar, boolean blockCopyPaste, int alignment, int verticalAlignment); + String hint, int hintColor, boolean showToolbar, boolean blockCopyPaste, int alignment, int verticalAlignment, + boolean returnExitsEditing); native void resizeNativeTextView(int x, int y, int w, int h, int padTop, int padRight, int padBottom, int padLeft); native void flushBuffer(long peer, int x, int y, int width, int height); native void imageRgbToIntArray(long imagePeer, int[] arr, int x, int y, int width, int height, int imgWidth, int imgHeight);