Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CodenameOne/src/com/codename1/ui/TextArea.java
Original file line number Diff line number Diff line change
Expand Up @@ -2311,6 +2311,25 @@ public ActionListener getDoneListener() {
/// Sets a Done listener on the TextField - notice this listener will be called
/// only on supported platforms that supports done action on the keyboard
///
/// The listener also affects which action the keyboard offers. On Android a
/// single line field with a listener shows Done even when another field
/// follows it, because Next would never fire the listener. To choose the
/// action yourself set the client property `"nextButton"` or `"doneButton"`
/// to any non-null value; that works with every input constraint, numeric
/// ones included:
///
/// ```java
/// TextField quantity = new TextField();
/// quantity.setConstraint(TextArea.NUMERIC);
/// quantity.putClientProperty("nextButton", Boolean.TRUE);
/// ```
///
/// A field that asks for Next moves to the following field without calling
/// this listener, and its keyboard usually has no Done to call it with. Set
/// the listener on a field that offers Done, typically the last one.
/// A field nothing follows offers Done even with `"nextButton"`, and
/// `"doneButton"` wins when both are set.
///
/// #### Parameters
///
/// - `l`: the listener
Expand Down
22 changes: 14 additions & 8 deletions Ports/Android/src/com/codename1/impl/android/InPlaceEditView.java
Original file line number Diff line number Diff line change
Expand Up @@ -1004,7 +1004,17 @@ public boolean onActionItemClicked(ActionMode mode,
if(textArea.getClientProperty("goButton") != null) {
mEditText.setImeOptions(ime | EditorInfo.IME_ACTION_GO);
} else {
if(textArea.getDoneListener() != null){
// "doneButton" and "nextButton" choose the action explicitly, ahead
// of the defaults below. Without them a Done listener forces Done even
// when a next field exists, because the listener only fires for Done:
// that default must not change, or every listener on a field that is
// not the last one stops firing. Done wins if both are set, and a Next
// with nowhere to go falls back to the defaults.
if (textArea.getClientProperty("doneButton") != null) {
mEditText.setImeOptions(ime | EditorInfo.IME_ACTION_DONE);
} else if (textArea.getClientProperty("nextButton") != null && nextDown != null) {
mEditText.setImeOptions(ime | EditorInfo.IME_ACTION_NEXT);
} else if(textArea.getDoneListener() != null){
mEditText.setImeOptions(ime | EditorInfo.IME_ACTION_DONE);
} else if (nextDown != null) {
mEditText.setImeOptions(ime | EditorInfo.IME_ACTION_NEXT);
Expand Down Expand Up @@ -1219,7 +1229,6 @@ private synchronized void endEditing(int reason, boolean forceVKBOpen, boolean f
//if (cursorTimer != null) {
// cursorTimer.cancel();
//}
System.out.println("-------In endEditing");
if (!mIsEditing || mEditText == null) {
return;
}
Expand All @@ -1235,9 +1244,6 @@ private synchronized void endEditing(int reason, boolean forceVKBOpen, boolean f


boolean leaveKeyboardShowing = impl.isAsyncEditMode();
System.out.println("Next flag: "+isNextActionFlagSet);
System.out.println("Next cmp: "+getNextComponent(mEditText.mTextArea));
System.out.println("Reason : "+reason+" IME Action: "+REASON_IME_ACTION);
if (reason == REASON_IME_ACTION && isNextActionFlagSet) {
if (getNextComponent(mEditText.mTextArea) instanceof TextArea) {
leaveKeyboardShowing = true;
Expand All @@ -1252,15 +1258,15 @@ private synchronized void endEditing(int reason, boolean forceVKBOpen, boolean f
if (forceVKBClose) {
leaveKeyboardShowing = false;
}
System.out.println("-----LEAVE KEYBOARD SHOWING: "+leaveKeyboardShowing);
if (!leaveKeyboardShowing || actionCode == EditorInfo.IME_ACTION_DONE || actionCode == EditorInfo.IME_ACTION_SEARCH || actionCode == EditorInfo.IME_ACTION_SEND || actionCode == EditorInfo.IME_ACTION_GO) {
System.out.println("Hiding virtual keyboard");
showVirtualKeyboard(false);
}
int imo = mEditText.getImeOptions() & 0xf; // Get rid of flags
// Next is deliberately absent: it moves on, it does not finish, so a field
// whose action is Next never fires its Done listener from the keyboard.
if (reason == REASON_IME_ACTION
&& ((TextArea) mEditText.mTextArea).getDoneListener() != null
&& (actionCode == EditorInfo.IME_ACTION_DONE)|| actionCode == EditorInfo.IME_ACTION_SEARCH || actionCode == EditorInfo.IME_ACTION_SEND || actionCode == EditorInfo.IME_ACTION_GO) {
&& (actionCode == EditorInfo.IME_ACTION_DONE || actionCode == EditorInfo.IME_ACTION_SEARCH || actionCode == EditorInfo.IME_ACTION_SEND || actionCode == EditorInfo.IME_ACTION_GO)) {
((TextArea) mEditText.mTextArea).fireDoneEvent(keyEvent);
}

Expand Down
53 changes: 43 additions & 10 deletions Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -987,6 +987,39 @@ void cn1_setStyleDoneButton(CN1_THREAD_STATE_MULTI_ARG UIBarButtonItem* btn) {
#endif



#if !TARGET_OS_OSX && !TARGET_OS_WATCH
// A keyboard with no Return key: the toolbar is the only place its Next or Done
// can live.
static BOOL cn1IsPadKeyboard(UIKeyboardType type) {
return type == UIKeyboardTypeDecimalPad
|| type == UIKeyboardTypePhonePad
|| type == UIKeyboardTypeNumberPad;
}

static BOOL cn1ExplicitKeyboardAction(void) {
#ifndef NEW_CODENAME_ONE_VM
return com_codename1_impl_ios_TextEditUtil_isExplicitKeyboardAction__();
#else
return com_codename1_impl_ios_TextEditUtil_isExplicitKeyboardAction___R_boolean(CN1_THREAD_GET_STATE_PASS_SINGLE_ARG);
#endif
}

// Whether such a keyboard gets the toolbar on this device. An iPad's docked
// number pad is a full keyboard with a Return key, so it is left alone; its
// floating keyboard is not, and nothing here can tell the two apart, so a field
// that chose its action with "nextButton"/"doneButton" gets the toolbar on an
// iPad whichever keyboard is up -- beside a Return key when it is the docked one.
static BOOL cn1PadKeyboardNeedsToolbar(UIKeyboardType type) {
return cn1IsPadKeyboard(type) && (!isIPad() || cn1ExplicitKeyboardAction());
}

// The developer's choice (showToolbar), minus landscape -- except where the
// toolbar is the keyboard's only action key. Unused on tvOS, which has no toolbar.
__attribute__((unused)) static BOOL cn1EditToolbarAllowed(BOOL showToolbar, BOOL landscape, UIKeyboardType type) {
return showToolbar && (!landscape || cn1PadKeyboardNeedsToolbar(type));
}
#endif
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,
Expand All @@ -1000,10 +1033,12 @@ void cn1_setStyleDoneButton(CN1_THREAD_STATE_MULTI_ARG UIBarButtonItem* btn) {
#if TARGET_OS_OSX
#else
#if !TARGET_OS_WATCH
// don't show toolbar in iOS 8 in landscape since there is just no room for that...
if(isIOS8() && displayHeight < displayWidth) {
showToolbar = NO;
}
// In landscape there is little room for the toolbar, so it is left out -- but
// only for a keyboard that has a Return key. This used to be unconditional, and
// isIOS8() is true of every device now, so a number pad in landscape had neither
// a Return key nor a toolbar: no Next, no Done, no way to leave the field from
// the keyboard. See cn1EditToolbarAllowed.
__attribute__((unused)) const BOOL cn1LandscapeEdit = isIOS8() && displayHeight < displayWidth;
//CN1Log(@"Java_com_codename1_impl_ios_IOSImplementation_editStringAtImpl");
currentlyEditingMaxLength = maxSize;
// Honored by the UITextView shouldChangeTextInRange: delegate (METALView) to
Expand Down Expand Up @@ -1191,13 +1226,11 @@ void cn1_setStyleDoneButton(CN1_THREAD_STATE_MULTI_ARG UIBarButtonItem* btn) {
name:UITextFieldTextDidChangeNotification
object:utf];

if ((utf.keyboardType == UIKeyboardTypeDecimalPad
|| utf.keyboardType == UIKeyboardTypePhonePad
|| utf.keyboardType == UIKeyboardTypeNumberPad
|| (utf.returnKeyType == UIReturnKeyNext && isVKBAlwaysOpen())) && !isIPad()) {
if (cn1PadKeyboardNeedsToolbar(utf.keyboardType)
|| (utf.returnKeyType == UIReturnKeyNext && isVKBAlwaysOpen() && !isIPad())) {
//add navigation toolbar to the top of the keyboard
#if !TARGET_OS_TV
if(showToolbar) {
if(cn1EditToolbarAllowed(showToolbar, cn1LandscapeEdit, utf.keyboardType)) {
#ifndef CN1_USE_ARC
UIToolbar *toolbar = [[[UIToolbar alloc] init] autorelease];
#else
Expand Down Expand Up @@ -1396,7 +1429,7 @@ void cn1_setStyleDoneButton(CN1_THREAD_STATE_MULTI_ARG UIBarButtonItem* btn) {
}

#if !TARGET_OS_TV
if(showToolbar) {
if(cn1EditToolbarAllowed(showToolbar, cn1LandscapeEdit, utv.keyboardType)) {
//add navigation toolbar to the top of the keyboard
#ifndef CN1_USE_ARC
UIToolbar *toolbar = [[[UIToolbar alloc] init] autorelease];
Expand Down
7 changes: 5 additions & 2 deletions Ports/iOSPort/nativeSources/METALView.m
Original file line number Diff line number Diff line change
Expand Up @@ -1852,10 +1852,13 @@ - (BOOL)presentFramebuffer
*/
-(void) keyboardDoneClicked {
if(editingComponent != nil) {
// -3, not the -2 that Return and Next send: IOSImplementation.editingUpdate
// treats the two alike except for a field that asked for Next explicitly,
// where only this button fires the Done listener.
if([editingComponent isKindOfClass:[UITextView class]]) {
stringEdit(YES, -2, ((UITextView*)editingComponent).text);
stringEdit(YES, -3, ((UITextView*)editingComponent).text);
} else {
stringEdit(YES, -2, ((UITextField*)editingComponent).text);
stringEdit(YES, -3, ((UITextField*)editingComponent).text);
}
if(isVKBAlwaysOpen()) {
com_codename1_impl_ios_IOSImplementation_foldKeyboard__(CN1_THREAD_GET_STATE_PASS_SINGLE_ARG);
Expand Down
20 changes: 16 additions & 4 deletions Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java
Original file line number Diff line number Diff line change
Expand Up @@ -1633,11 +1633,18 @@ public void run() {
// field, or in a window-only application left a stale destination in place.
// getNextComponent is Form-only but is defined as exactly this call, and
// getTabIterator is on TopLevelContainer.
// "doneButton" and "nextButton" choose the keyboard action explicitly. Done
// is expressed as "there is no next field": the native side derives the Return
// key, the toolbar button and whether Return advances from that one answer, so
// no second path can disagree with it. Next is what a field with a successor
// gets anyway; setting it only changes whether advancing fires the Done
// listener (see editingUpdate) and whether an iPad gets the toolbar.
if (parentTop != null) {
TextEditUtil.setNextEditComponent(parentTop.getTabIterator(cmp).getNext());
TextEditUtil.setNextEditComponent(TextEditUtil.nextFor(cmp, parentTop.getTabIterator(cmp).getNext()));
} else if (current != null) {
TextEditUtil.setNextEditComponent(current.getNextComponent(cmp));
TextEditUtil.setNextEditComponent(TextEditUtil.nextFor(cmp, current.getNextComponent(cmp)));
}
TextEditUtil.setExplicitKeyboardAction(TextEditUtil.choosesKeyboardAction(cmp));
Display.getInstance().callSerially(new Runnable() {
@Override
public void run() {
Expand Down Expand Up @@ -1763,11 +1770,16 @@ public static void editingUpdate(final String s, final int cursorPositon, final
public void run() {
if(instance.currentEditing != null) {
if(finished) {
editNext = cursorPositon == -2;
// -2 is Return or the toolbar's Next, -3 the toolbar's Done. They
// were one code, and still behave as one, except for a field that
// asked for Next explicitly: there Next moves on without finishing,
// so only Done fires the listener -- which is what Android does.
editNext = TextEditUtil.endedOnKeyboardAction(cursorPositon);
boolean firesDone = TextEditUtil.firesDoneEvent(cursorPositon, instance.currentEditing);
synchronized(EDITING_LOCK) {
instance.currentEditing.setText(s);
Display.getInstance().onEditingComplete(instance.currentEditing, s);
if(editNext && instance.currentEditing != null && instance.currentEditing instanceof TextArea) {
if(firesDone && instance.currentEditing instanceof TextArea) {
((TextArea)instance.currentEditing).fireDoneEvent();
}
Component cmp = instance.currentEditing;
Expand Down
90 changes: 90 additions & 0 deletions Ports/iOSPort/src/com/codename1/impl/ios/TextEditUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,96 @@ public static void setNextEditComponent(Component cmp) {
nextEditedComponent = cmp;
}

/**
* The cursor position native code reports when editing ended on the Return
* key or the toolbar's Next button.
*/
static final int ENDED_BY_RETURN_OR_NEXT = -2;

/**
* The cursor position native code reports when editing ended on the
* toolbar's Done button (METALView keyboardDoneClicked).
*/
static final int ENDED_BY_DONE = -3;

/**
* Whether native code reported that the edit ended on a keyboard action, as
* opposed to a touch elsewhere or a programmatic stop. Both codes move to the
* next component where there is one, as the single code they used to be did.
*/
static boolean endedOnKeyboardAction(int endCode) {
return endCode == ENDED_BY_RETURN_OR_NEXT || endCode == ENDED_BY_DONE;
}

/**
* Whether the component chose its keyboard action with the "nextButton" or
* "doneButton" client property.
*/
static boolean choosesKeyboardAction(Component cmp) {
return cmp.getClientProperty("doneButton") != null || cmp.getClientProperty("nextButton") != null;
}

/**
* The component the keyboard moves to from cmp. "doneButton" answers null
* whatever follows: the native side derives the Return key, the toolbar
* button and whether Return advances from "is there a next component", so
* saying there is none is the one place Done has to be decided.
*
* @param cmp the component about to be edited
* @param following the component after it in the tab order, or null
*/
static Component nextFor(Component cmp, Component following) {
return cmp.getClientProperty("doneButton") != null ? null : following;
}

/**
* Whether ending the edit of a component fires its Done listener. Every
* keyboard ending does, as it always has, with one exception: a field that
* asked for Next explicitly and has somewhere to go moves on without
* finishing, which is what Android does with its Next action. The toolbar's
* Done button still fires, but such a field has one only beside Next on a
* number, decimal or phone pad in async edit mode: a listener that must run
* belongs on a field that offers Done.
*
* @param endCode the cursor position native code reported
* @param edited the component whose edit ended, possibly null
*/
static boolean firesDoneEvent(int endCode, Component edited) {
if (edited == null) {
return false;
}
if (endCode == ENDED_BY_DONE) {
return true;
}
if (endCode != ENDED_BY_RETURN_OR_NEXT) {
return false;
}
return edited.getClientProperty("nextButton") == null || isLastEditComponent();
}

private static boolean explicitKeyboardAction;

/**
* Records whether the component about to be edited chose its keyboard action
* with the "nextButton" or "doneButton" client property.
*
* @param explicit true if it did
*/
public static void setExplicitKeyboardAction(boolean explicit) {
explicitKeyboardAction = explicit;
}

/**
* Callback for native code. An explicit action is the developer saying the
* field must offer that action, so the toolbar carrying it is shown on an
* iPad too, where it is otherwise left out.
*
* @return true if the edited component chose its keyboard action explicitly
*/
public static boolean isExplicitKeyboardAction() {
return explicitKeyboardAction;
}

/**
* @return if the currently edited textarea is the last component on the
* form
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/
package com.codenameone.developerguide.snippets;

import com.codename1.ui.Dialog;
import com.codename1.ui.TextArea;
import com.codename1.ui.TextField;

/// Snippets for choosing the virtual keyboard's Next or Done action, included
/// by the developer guide's virtual keyboard section.
public class KeyboardActionSnippets {
void explicitActions() {
// tag::keyboard-action-next-done[]
TextField quantity = new TextField("", "Quantity", 6, TextArea.NUMERIC);
TextField price = new TextField("", "Price", 8, TextArea.DECIMAL);

// Numeric, and offering Next even if the field has a Done listener
quantity.putClientProperty("nextButton", Boolean.TRUE);

// Offers Done even if another field follows it, and calls the listener
price.putClientProperty("doneButton", Boolean.TRUE);
price.setDoneListener(e -> Dialog.show("Saved", price.getText(), "OK", null));
// end::keyboard-action-next-done[]
}
}
Loading
Loading