Android: Add selection dialog (drop down/combo box) (#13814)

- The handling of IGUIComboBox uses the new setAndSendSelected() method.
- getDialogState() is now getInputDialogState() and returns the state of the input dialog.
- getLastDialogType() is added and returns current/last shown dialog's type.
- getInputDialogState() now returns an enum instead of int.
- getAndroidUIInput() now returns void instead of bool.
- New data types (enum) are added:
  (1) GameActivity.DialogType (Java) and porting::AndroidDialogType (C++)
  (2) GameActivity.DialogState (Java) and porting::AndroidDialogState (C++)
- When showing a text input dialog, there is no custom accept button text any more.
- showDialog()/showDialogUI() for text input is now showTextInputDialog()/showTextInputDialogUI().
- showInputDialog()/showDialogUI() for text input is now showTextInputDialog()/showTextInputDialogUI().
- getDialogValue()/getInputDialogValue() is now getDialogMessage()/getInputDialogMessage().


Co-authored-by: Gregor Parzefall <82708541+grorp@users.noreply.github.com>
This commit is contained in:
Muhammad Rifqi Priyo Susanto 2024-01-07 19:00:04 +07:00 committed by GitHub
parent bd42cc2c77
commit 171f911237
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 259 additions and 106 deletions

@ -51,8 +51,13 @@ public class GameActivity extends NativeActivity {
System.loadLibrary("minetest");
}
private int messageReturnCode = -1;
enum DialogType { TEXT_INPUT, SELECTION_INPUT }
enum DialogState { DIALOG_SHOWN, DIALOG_INPUTTED, DIALOG_CANCELED }
private DialogType lastDialogType = DialogType.TEXT_INPUT;
private DialogState inputDialogState = DialogState.DIALOG_CANCELED;
private String messageReturnValue = "";
private int selectionReturnValue = 0;
@Override
public void onCreate(Bundle savedInstanceState) {
@ -85,11 +90,17 @@ public class GameActivity extends NativeActivity {
// Ignore the back press so Minetest can handle it
}
public void showDialog(String acceptButton, String hint, String current, int editType) {
runOnUiThread(() -> showDialogUI(hint, current, editType));
public void showTextInputDialog(String hint, String current, int editType) {
runOnUiThread(() -> showTextInputDialogUI(hint, current, editType));
}
private void showDialogUI(String hint, String current, int editType) {
public void showSelectionInputDialog(String[] optionList, int selectedIdx) {
runOnUiThread(() -> showSelectionInputDialogUI(optionList, selectedIdx));
}
private void showTextInputDialogUI(String hint, String current, int editType) {
lastDialogType = DialogType.TEXT_INPUT;
inputDialogState = DialogState.DIALOG_SHOWN;
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
LinearLayout container = new LinearLayout(this);
container.setOrientation(LinearLayout.VERTICAL);
@ -114,7 +125,7 @@ public class GameActivity extends NativeActivity {
// For multi-line, do not submit the text after pressing Enter key
if (keyCode == KeyEvent.KEYCODE_ENTER && editType != 1) {
imm.hideSoftInputFromWindow(editText.getWindowToken(), 0);
messageReturnCode = 0;
inputDialogState = DialogState.DIALOG_INPUTTED;
messageReturnValue = editText.getText().toString();
alertDialog.dismiss();
return true;
@ -128,29 +139,55 @@ public class GameActivity extends NativeActivity {
doneButton.setText(R.string.ime_dialog_done);
doneButton.setOnClickListener((view -> {
imm.hideSoftInputFromWindow(editText.getWindowToken(), 0);
messageReturnCode = 0;
inputDialogState = DialogState.DIALOG_INPUTTED;
messageReturnValue = editText.getText().toString();
alertDialog.dismiss();
}));
}
alertDialog.setOnCancelListener(dialog -> {
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
inputDialogState = DialogState.DIALOG_CANCELED;
messageReturnValue = current;
messageReturnCode = -1;
});
alertDialog.show();
editText.requestFocusTryShow();
}
public int getDialogState() {
return messageReturnCode;
public void showSelectionInputDialogUI(String[] optionList, int selectedIdx) {
lastDialogType = DialogType.SELECTION_INPUT;
inputDialogState = DialogState.DIALOG_SHOWN;
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setSingleChoiceItems(optionList, selectedIdx, (dialog, selection) -> {
inputDialogState = DialogState.DIALOG_INPUTTED;
selectionReturnValue = selection;
dialog.dismiss();
});
builder.setOnCancelListener(dialog -> {
inputDialogState = DialogState.DIALOG_CANCELED;
selectionReturnValue = selectedIdx;
});
AlertDialog alertDialog = builder.create();
alertDialog.show();
}
public String getDialogValue() {
messageReturnCode = -1;
public int getLastDialogType() {
return lastDialogType.ordinal();
}
public int getInputDialogState() {
return inputDialogState.ordinal();
}
public String getDialogMessage() {
inputDialogState = DialogState.DIALOG_CANCELED;
return messageReturnValue;
}
public int getDialogSelection() {
inputDialogState = DialogState.DIALOG_CANCELED;
return selectionReturnValue;
}
public float getDensity() {
return getResources().getDisplayMetrics().density;
}

@ -2277,7 +2277,7 @@ void Game::openConsole(float scale, const wchar_t *line)
assert(scale > 0.0f && scale <= 1.0f);
#ifdef __ANDROID__
porting::showInputDialog(gettext("ok"), "", "", 2);
porting::showTextInputDialog("", "", 2);
m_android_chat_open = true;
#else
if (gui_chat_console->isOpenInhibited())
@ -2293,15 +2293,19 @@ void Game::openConsole(float scale, const wchar_t *line)
#ifdef __ANDROID__
void Game::handleAndroidChatInput()
{
if (m_android_chat_open && porting::getInputDialogState() == 0) {
std::string text = porting::getInputDialogValue();
client->typeChatMessage(utf8_to_wide(text));
m_android_chat_open = false;
// It has to be a text input
if (m_android_chat_open && porting::getLastInputDialogType() == porting::TEXT_INPUT) {
porting::AndroidDialogState dialogState = porting::getInputDialogState();
if (dialogState == porting::DIALOG_INPUTTED) {
std::string text = porting::getInputDialogMessage();
client->typeChatMessage(utf8_to_wide(text));
}
if (dialogState != porting::DIALOG_SHOWN)
m_android_chat_open = false;
}
}
#endif
void Game::toggleFreeMove()
{
bool free_move = !g_settings->getBool("free_move");

@ -3497,46 +3497,58 @@ void GUIFormSpecMenu::legacySortElements(std::list<IGUIElement *>::iterator from
}
#ifdef __ANDROID__
bool GUIFormSpecMenu::getAndroidUIInput()
void GUIFormSpecMenu::getAndroidUIInput()
{
if (!hasAndroidUIInput())
return false;
porting::AndroidDialogState dialogState = getAndroidUIInputState();
if (dialogState == porting::DIALOG_SHOWN) {
return;
} else if (dialogState == porting::DIALOG_CANCELED) {
m_jni_field_name.clear();
return;
}
// still waiting
if (porting::getInputDialogState() == -1)
return true;
porting::AndroidDialogType dialog_type = porting::getLastInputDialogType();
std::string fieldname = m_jni_field_name;
m_jni_field_name.clear();
for (const FieldSpec &field : m_fields) {
if (field.fname != fieldname)
continue;
continue; // Iterate until found
IGUIElement *element = getElementFromId(field.fid, true);
if (!element || element->getType() != irr::gui::EGUIET_EDIT_BOX)
return false;
if (!element)
return;
gui::IGUIEditBox *editbox = (gui::IGUIEditBox *)element;
std::string text = porting::getInputDialogValue();
editbox->setText(utf8_to_wide(text).c_str());
auto element_type = element->getType();
if (dialog_type == porting::TEXT_INPUT && element_type == irr::gui::EGUIET_EDIT_BOX) {
gui::IGUIEditBox *editbox = (gui::IGUIEditBox *)element;
std::string text = porting::getInputDialogMessage();
editbox->setText(utf8_to_wide(text).c_str());
bool enter_after_edit = false;
auto iter = field_enter_after_edit.find(fieldname);
if (iter != field_enter_after_edit.end()) {
enter_after_edit = iter->second;
}
if (enter_after_edit && editbox->getParent()) {
SEvent enter;
enter.EventType = EET_GUI_EVENT;
enter.GUIEvent.Caller = editbox;
enter.GUIEvent.Element = nullptr;
enter.GUIEvent.EventType = gui::EGET_EDITBOX_ENTER;
editbox->getParent()->OnEvent(enter);
bool enter_after_edit = false;
auto iter = field_enter_after_edit.find(fieldname);
if (iter != field_enter_after_edit.end()) {
enter_after_edit = iter->second;
}
if (enter_after_edit && editbox->getParent()) {
SEvent enter;
enter.EventType = EET_GUI_EVENT;
enter.GUIEvent.Caller = editbox;
enter.GUIEvent.Element = nullptr;
enter.GUIEvent.EventType = gui::EGET_EDITBOX_ENTER;
editbox->getParent()->OnEvent(enter);
}
} else if (dialog_type == porting::SELECTION_INPUT &&
element_type == irr::gui::EGUIET_COMBO_BOX) {
auto dropdown = (gui::IGUIComboBox *) element;
int selected = porting::getInputDialogSelection();
dropdown->setAndSendSelected(selected);
}
return; // Early-return after found
}
return false;
}
#endif

@ -286,7 +286,7 @@ public:
core::rect<s32> getAbsoluteRect();
#ifdef __ANDROID__
bool getAndroidUIInput();
void getAndroidUIInput();
#endif
protected:

@ -264,14 +264,19 @@ std::string GUIPasswordChange::getNameByID(s32 id)
}
#ifdef __ANDROID__
bool GUIPasswordChange::getAndroidUIInput()
void GUIPasswordChange::getAndroidUIInput()
{
if (!hasAndroidUIInput())
return false;
porting::AndroidDialogState dialogState = getAndroidUIInputState();
if (dialogState == porting::DIALOG_SHOWN) {
return;
} else if (dialogState == porting::DIALOG_CANCELED) {
m_jni_field_name.clear();
return;
}
// still waiting
if (porting::getInputDialogState() == -1)
return true;
// It has to be a text input
if (porting::getLastInputDialogType() != porting::TEXT_INPUT)
return;
gui::IGUIElement *e = nullptr;
if (m_jni_field_name == "old_password")
@ -283,10 +288,10 @@ bool GUIPasswordChange::getAndroidUIInput()
m_jni_field_name.clear();
if (!e || e->getType() != irr::gui::EGUIET_EDIT_BOX)
return false;
return;
std::string text = porting::getInputDialogValue();
std::string text = porting::getInputDialogMessage();
e->setText(utf8_to_wide(text).c_str());
return false;
return;
}
#endif

@ -45,7 +45,7 @@ public:
bool OnEvent(const SEvent &event);
#ifdef __ANDROID__
bool getAndroidUIInput();
void getAndroidUIInput();
#endif
protected:

@ -268,11 +268,34 @@ bool GUIModalMenu::preprocessEvent(const SEvent &event)
if (((gui::IGUIEditBox *)hovered)->isPasswordBox())
type = 3;
porting::showInputDialog(gettext("OK"), "",
wide_to_utf8(((gui::IGUIEditBox *)hovered)->getText()), type);
porting::showTextInputDialog("",
wide_to_utf8(((gui::IGUIEditBox *) hovered)->getText()), type);
return retval;
}
}
if (event.EventType == EET_GUI_EVENT) {
if (event.GUIEvent.EventType == gui::EGET_LISTBOX_OPENED) {
gui::IGUIComboBox *dropdown = (gui::IGUIComboBox *) event.GUIEvent.Caller;
std::string field_name = getNameByID(dropdown->getID());
if (field_name.empty())
return false;
m_jni_field_name = field_name;
s32 selected_idx = dropdown->getSelected();
s32 option_size = dropdown->getItemCount();
std::string list_of_options[option_size];
for (s32 i = 0; i < option_size; i++) {
list_of_options[i] = wide_to_utf8(dropdown->getItem(i));
}
porting::showComboBoxDialog(list_of_options, option_size, selected_idx);
return true; // Prevent the Irrlicht dropdown from opening.
}
}
#endif
// Convert touch events into mouse events.
@ -347,22 +370,12 @@ bool GUIModalMenu::preprocessEvent(const SEvent &event)
}
#ifdef __ANDROID__
bool GUIModalMenu::hasAndroidUIInput()
porting::AndroidDialogState GUIModalMenu::getAndroidUIInputState()
{
// no dialog shown
// No dialog is shown
if (m_jni_field_name.empty())
return false;
return porting::DIALOG_CANCELED;
// still waiting
if (porting::getInputDialogState() == -1)
return true;
// no value abort dialog processing
if (porting::getInputDialogState() != 0) {
m_jni_field_name.clear();
return false;
}
return true;
return porting::getInputDialogState();
}
#endif

@ -22,6 +22,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "irrlichttypes_extrabloated.h"
#include "irr_ptr.h"
#include "util/string.h"
#ifdef __ANDROID__
#include <porting_android.h>
#endif
enum class PointerType {
Mouse,
@ -59,8 +62,8 @@ public:
virtual bool OnEvent(const SEvent &event) { return false; };
virtual bool pausesGame() { return false; } // Used for pause menu
#ifdef __ANDROID__
virtual bool getAndroidUIInput() { return false; }
bool hasAndroidUIInput();
virtual void getAndroidUIInput() {};
porting::AndroidDialogState getAndroidUIInputState();
#endif
PointerType getPointerType() { return m_pointer_type; };

@ -165,22 +165,41 @@ bool setSystemPaths()
return true;
}
void showInputDialog(const std::string &acceptButton, const std::string &hint,
const std::string &current, int editType)
void showTextInputDialog(const std::string &hint, const std::string &current, int editType)
{
jmethodID showdialog = jnienv->GetMethodID(nativeActivity, "showDialog",
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)V");
jmethodID showdialog = jnienv->GetMethodID(nativeActivity, "showTextInputDialog",
"(Ljava/lang/String;Ljava/lang/String;I)V");
FATAL_ERROR_IF(showdialog == nullptr,
"porting::showInputDialog unable to find Java showDialog method");
"porting::showTextInputDialog unable to find Java showTextInputDialog method");
jstring jacceptButton = jnienv->NewStringUTF(acceptButton.c_str());
jstring jhint = jnienv->NewStringUTF(hint.c_str());
jstring jcurrent = jnienv->NewStringUTF(current.c_str());
jint jeditType = editType;
jnienv->CallVoidMethod(app_global->activity->clazz, showdialog,
jacceptButton, jhint, jcurrent, jeditType);
jhint, jcurrent, jeditType);
}
void showComboBoxDialog(const std::string optionList[], s32 listSize, s32 selectedIdx)
{
jmethodID showdialog = jnienv->GetMethodID(nativeActivity, "showSelectionInputDialog",
"([Ljava/lang/String;I)V");
FATAL_ERROR_IF(showdialog == nullptr,
"porting::showComboBoxDialog unable to find Java showSelectionInputDialog method");
jclass jStringClass = jnienv->FindClass("java/lang/String");
jobjectArray jOptionList = jnienv->NewObjectArray(listSize, jStringClass, NULL);
jint jselectedIdx = selectedIdx;
for (s32 i = 0; i < listSize; i ++) {
jnienv->SetObjectArrayElement(jOptionList, i,
jnienv->NewStringUTF(optionList[i].c_str()));
}
jnienv->CallVoidMethod(app_global->activity->clazz, showdialog, jOptionList,
jselectedIdx);
}
void openURIAndroid(const char *url)
@ -207,30 +226,53 @@ void shareFileAndroid(const std::string &path)
jnienv->CallVoidMethod(app_global->activity->clazz, url_open, jurl);
}
int getInputDialogState()
AndroidDialogType getLastInputDialogType()
{
jmethodID dialogstate = jnienv->GetMethodID(nativeActivity,
"getDialogState", "()I");
jmethodID lastdialogtype = jnienv->GetMethodID(nativeActivity,
"getLastDialogType", "()I");
FATAL_ERROR_IF(dialogstate == nullptr,
"porting::getInputDialogState unable to find Java getDialogState method");
FATAL_ERROR_IF(lastdialogtype == nullptr,
"porting::getLastInputDialogType unable to find Java getLastDialogType method");
return jnienv->CallIntMethod(app_global->activity->clazz, dialogstate);
int dialogType = jnienv->CallIntMethod(app_global->activity->clazz, lastdialogtype);
return static_cast<AndroidDialogType>(dialogType);
}
std::string getInputDialogValue()
AndroidDialogState getInputDialogState()
{
jmethodID inputdialogstate = jnienv->GetMethodID(nativeActivity,
"getInputDialogState", "()I");
FATAL_ERROR_IF(inputdialogstate == nullptr,
"porting::getInputDialogState unable to find Java getInputDialogState method");
int dialogState = jnienv->CallIntMethod(app_global->activity->clazz, inputdialogstate);
return static_cast<AndroidDialogState>(dialogState);
}
std::string getInputDialogMessage()
{
jmethodID dialogvalue = jnienv->GetMethodID(nativeActivity,
"getDialogValue", "()Ljava/lang/String;");
"getDialogMessage", "()Ljava/lang/String;");
FATAL_ERROR_IF(dialogvalue == nullptr,
"porting::getInputDialogValue unable to find Java getDialogValue method");
"porting::getInputDialogMessage unable to find Java getDialogMessage method");
jobject result = jnienv->CallObjectMethod(app_global->activity->clazz,
dialogvalue);
return readJavaString((jstring) result);
}
int getInputDialogSelection()
{
jmethodID dialogvalue = jnienv->GetMethodID(nativeActivity, "getDialogSelection", "()I");
FATAL_ERROR_IF(dialogvalue == nullptr,
"porting::getInputDialogSelection unable to find Java getDialogSelection method");
return jnienv->CallIntMethod(app_global->activity->clazz, dialogvalue);
}
#ifndef SERVER
float getDisplayDensity()
{

@ -20,7 +20,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#pragma once
#ifndef __ANDROID__
#error this include has to be included on android port only!
#error This header has to be included on Android port only!
#endif
#include <jni.h>
@ -30,22 +30,28 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include <string>
namespace porting {
// java app
// Java app
extern android_app *app_global;
// java <-> c++ interaction interface
// Java <-> C++ interaction interface
extern JNIEnv *jnienv;
/**
* show text input dialog in java
* @param acceptButton text to display on accept button
* @param hint hint to show
* @param current initial value to display
* @param editType type of texfield
* (1==multiline text input; 2==single line text input; 3=password field)
* Show a text input dialog in Java
* @param hint Hint to be shown
* @param current Initial value to be displayed
* @param editType Type of the text field
* (1 = multi-line text input; 2 = single-line text input; 3 = password field)
*/
void showInputDialog(const std::string &acceptButton,
const std::string &hint, const std::string &current, int editType);
void showTextInputDialog(const std::string &hint, const std::string &current, int editType);
/**
* Show a selection dialog in Java
* @param optionList The list of options
* @param listSize Size of the list
* @param selectedIdx Selected index
*/
void showComboBoxDialog(const std::string optionList[], s32 listSize, s32 selectedIdx);
/**
* Opens a share intent to the file at path
@ -54,17 +60,48 @@ void showInputDialog(const std::string &acceptButton,
*/
void shareFileAndroid(const std::string &path);
/**
* WORKAROUND for not working callbacks from java -> c++
* get current state of input dialog
/*
* Types of Android input dialog:
* 1. Text input (single/multi-line text and password field)
* 2. Selection input (combo box)
*/
int getInputDialogState();
enum AndroidDialogType { TEXT_INPUT, SELECTION_INPUT };
/**
* WORKAROUND for not working callbacks from java -> c++
* get text in current input dialog
/*
* WORKAROUND for not working callbacks from Java -> C++
* Get the type of the last input dialog
*/
std::string getInputDialogValue();
AndroidDialogType getLastInputDialogType();
/*
* States of Android input dialog:
* 1. The dialog is currently shown.
* 2. The dialog has its input sent.
* 3. The dialog is canceled/dismissed.
*/
enum AndroidDialogState { DIALOG_SHOWN, DIALOG_INPUTTED, DIALOG_CANCELED };
/*
* WORKAROUND for not working callbacks from Java -> C++
* Get the state of the input dialog
*/
AndroidDialogState getInputDialogState();
/*
* WORKAROUND for not working callbacks from Java -> C++
* Get the text in the current/last input dialog
* This function clears the dialog state (set to canceled). Make sure to save
* the dialog state before calling this function.
*/
std::string getInputDialogMessage();
/*
* WORKAROUND for not working callbacks from Java -> C++
* Get the selection in the current/last input dialog
* This function clears the dialog state (set to canceled). Make sure to save
* the dialog state before calling this function.
*/
int getInputDialogSelection();
#ifndef SERVER
float getDisplayDensity();