From 171f911237ba937b70f7dab01a1ff5cbaef7d48b Mon Sep 17 00:00:00 2001 From: Muhammad Rifqi Priyo Susanto Date: Sun, 7 Jan 2024 19:00:04 +0700 Subject: [PATCH] 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> --- .../net/minetest/minetest/GameActivity.java | 59 ++++++++++++--- src/client/game.cpp | 16 ++-- src/gui/guiFormSpecMenu.cpp | 62 ++++++++------- src/gui/guiFormSpecMenu.h | 2 +- src/gui/guiPasswordChange.cpp | 23 +++--- src/gui/guiPasswordChange.h | 2 +- src/gui/modalMenu.cpp | 45 +++++++---- src/gui/modalMenu.h | 7 +- src/porting_android.cpp | 74 ++++++++++++++---- src/porting_android.h | 75 ++++++++++++++----- 10 files changed, 259 insertions(+), 106 deletions(-) diff --git a/android/app/src/main/java/net/minetest/minetest/GameActivity.java b/android/app/src/main/java/net/minetest/minetest/GameActivity.java index caac0637d..f2ff09f3d 100644 --- a/android/app/src/main/java/net/minetest/minetest/GameActivity.java +++ b/android/app/src/main/java/net/minetest/minetest/GameActivity.java @@ -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; } diff --git a/src/client/game.cpp b/src/client/game.cpp index 622e8bb07..40fea4307 100644 --- a/src/client/game.cpp +++ b/src/client/game.cpp @@ -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"); diff --git a/src/gui/guiFormSpecMenu.cpp b/src/gui/guiFormSpecMenu.cpp index c53c65ded..b268574bf 100644 --- a/src/gui/guiFormSpecMenu.cpp +++ b/src/gui/guiFormSpecMenu.cpp @@ -3497,46 +3497,58 @@ void GUIFormSpecMenu::legacySortElements(std::list::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 diff --git a/src/gui/guiFormSpecMenu.h b/src/gui/guiFormSpecMenu.h index eb464747f..9f5685df6 100644 --- a/src/gui/guiFormSpecMenu.h +++ b/src/gui/guiFormSpecMenu.h @@ -286,7 +286,7 @@ public: core::rect getAbsoluteRect(); #ifdef __ANDROID__ - bool getAndroidUIInput(); + void getAndroidUIInput(); #endif protected: diff --git a/src/gui/guiPasswordChange.cpp b/src/gui/guiPasswordChange.cpp index e100643a4..b1264ddfe 100644 --- a/src/gui/guiPasswordChange.cpp +++ b/src/gui/guiPasswordChange.cpp @@ -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 diff --git a/src/gui/guiPasswordChange.h b/src/gui/guiPasswordChange.h index 452702add..8dc670cc2 100644 --- a/src/gui/guiPasswordChange.h +++ b/src/gui/guiPasswordChange.h @@ -45,7 +45,7 @@ public: bool OnEvent(const SEvent &event); #ifdef __ANDROID__ - bool getAndroidUIInput(); + void getAndroidUIInput(); #endif protected: diff --git a/src/gui/modalMenu.cpp b/src/gui/modalMenu.cpp index 8d46c70f3..b260f17d5 100644 --- a/src/gui/modalMenu.cpp +++ b/src/gui/modalMenu.cpp @@ -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 diff --git a/src/gui/modalMenu.h b/src/gui/modalMenu.h index bfa5d4ac8..9bb55ffec 100644 --- a/src/gui/modalMenu.h +++ b/src/gui/modalMenu.h @@ -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 +#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; }; diff --git a/src/porting_android.cpp b/src/porting_android.cpp index c8bdad5a0..43c2cea35 100644 --- a/src/porting_android.cpp +++ b/src/porting_android.cpp @@ -165,22 +165,41 @@ bool setSystemPaths() return true; } -void showInputDialog(const std::string &acceptButton, const std::string &hint, - const std::string ¤t, int editType) +void showTextInputDialog(const std::string &hint, const std::string ¤t, 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(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(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() { diff --git a/src/porting_android.h b/src/porting_android.h index d41cc8e4c..980f43ed1 100644 --- a/src/porting_android.h +++ b/src/porting_android.h @@ -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 @@ -30,22 +30,28 @@ with this program; if not, write to the Free Software Foundation, Inc., #include 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 ¤t, int editType); +void showTextInputDialog(const std::string &hint, const std::string ¤t, 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();