diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MainActivity.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MainActivity.java index 2cf7b8ba08..c70e5a88e4 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MainActivity.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MainActivity.java @@ -110,6 +110,15 @@ public class MainActivity extends BaseActivity implements ControlButtonMenuListe private RectF inputAreaRect; private int imeHeight; private boolean hasOngoingImeAnimation; + private static volatile boolean sChatLikelyOpen; + + // When the mouse re-grabs, every screen (chat included) is guaranteed closed. + private final GrabListener mChatStateGrabListener = isGrabbing -> { + if (isGrabbing) { + sChatLikelyOpen = false; + refreshImeTranslation(); + } + }; MinecraftProfile minecraftProfile; @@ -138,6 +147,7 @@ public void onCreate(Bundle savedInstanceState) { initLayout(R.layout.activity_basemain); CallbackBridge.addGrabListener(touchpad); CallbackBridge.addGrabListener(minecraftGLView); + CallbackBridge.addGrabListener(mChatStateGrabListener); if (Tools.hasTouchController(new File(gameDirPath)) || LauncherPreferences.PREF_FORCE_ENABLE_TOUCHCONTROLLER) { TouchControllerUtils.initialize(this, touchControllerInputView); @@ -383,6 +393,7 @@ protected void onDestroy() { super.onDestroy(); CallbackBridge.removeGrabListener(touchpad); CallbackBridge.removeGrabListener(minecraftGLView); + CallbackBridge.removeGrabListener(mChatStateGrabListener); ContextExecutor.clearActivity(); } @@ -734,6 +745,32 @@ public void updateInputAreaRect(@Nullable RectF rect) { refreshImeTranslation(); } + /** + * Tracks whether the in-game chat is open, so keyboard panning only kicks in + * when the chat input actually needs to be revealed above the IME. + * Chat open keys only count from gameplay (mouse grabbed), while the close + * keys or the mouse grabbing back (any screen closes) mark it closed. + * Both key lists are user-configurable from the control settings. + */ + public static void trackChatStateKey(int keycode, boolean isDown) { + if (!isDown) return; + if (LauncherPreferences.PREF_CHAT_PAN_CLOSE_KEYS.contains(keycode)) { + sChatLikelyOpen = false; + } else if (LauncherPreferences.PREF_CHAT_PAN_OPEN_KEYS.contains(keycode) && CallbackBridge.isGrabbing()) { + sChatLikelyOpen = true; + } + } + + /** + * Char-input variant of the above for software keyboards, which commit + * characters instead of raw keycodes. GLFW keycodes for printable keys + * match their ASCII values, with letters using the uppercase code. + */ + public static void trackChatStateChar(char character) { + int keycode = character >= 'a' && character <= 'z' ? character - ('a' - 'A') : character; + trackChatStateKey(keycode, true); + } + private void refreshImeTranslation() { if (imeHeight == 0) { // Early exit @@ -744,7 +781,7 @@ private void refreshImeTranslation() { int inputAreaBottom; if (inputAreaRect != null) { inputAreaBottom = (int) inputAreaRect.bottom; - } else if (LauncherPreferences.PREF_KEYBOARD_PANNING) { + } else if (LauncherPreferences.PREF_KEYBOARD_PANNING && sChatLikelyOpen) { inputAreaBottom = contentFrame.getHeight(); } else { contentFrame.setTranslationY(0); diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/buttons/ControlButton.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/buttons/ControlButton.java index 54f21b2c25..a133b204c0 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/buttons/ControlButton.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/buttons/ControlButton.java @@ -194,6 +194,7 @@ public void sendKeyPresses(boolean isDown){ setActivated(isDown); for(int keycode : mProperties.keycodes){ if(keycode >= GLFW_KEY_UNKNOWN){ + MainActivity.trackChatStateKey(keycode, isDown); sendKeyPress(keycode, EfficientAndroidLWJGLKeycode.getLwjglChar(keycode), CallbackBridge.getCurrentMods(), isDown); CallbackBridge.setModifiers(keycode, isDown); }else{ diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/keyboard/TouchCharInput.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/keyboard/TouchCharInput.java index 5e22c59d4e..8f5f3983b1 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/keyboard/TouchCharInput.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/keyboard/TouchCharInput.java @@ -13,6 +13,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import net.kdt.pojavlaunch.LwjglGlfwKeycode; +import net.kdt.pojavlaunch.MainActivity; import net.kdt.pojavlaunch.MinecraftGLSurface; import net.kdt.pojavlaunch.R; @@ -117,6 +119,7 @@ public void disable(){ /** Send the enter key. */ private void sendEnter(){ + MainActivity.trackChatStateKey(LwjglGlfwKeycode.GLFW_KEY_ENTER, true); mCharacterSender.sendEnter(); clear(); } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/KeyboardPanKeysPreference.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/KeyboardPanKeysPreference.java new file mode 100644 index 0000000000..2ec8c6ef74 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/KeyboardPanKeysPreference.java @@ -0,0 +1,140 @@ +package net.kdt.pojavlaunch.prefs; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.Spinner; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.preference.Preference; + +import net.kdt.pojavlaunch.EfficientAndroidLWJGLKeycode; +import net.kdt.pojavlaunch.R; + +import java.util.ArrayList; +import java.util.List; + +/** + * Name-based picker for the keyboard panning chat key lists, similar to the + * key mapping spinner of the control editor. The stored value stays a + * comma-separated string of GLFW key codes, parsed by LauncherPreferences. + */ +public class KeyboardPanKeysPreference extends Preference { + + private static final String ANDROID_NS = "http://schemas.android.com/apk/res/android"; + private final String mDefaultValue; + + public KeyboardPanKeysPreference(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + mDefaultValue = attrs.getAttributeValue(ANDROID_NS, "defaultValue"); + setOnPreferenceClickListener(preference -> { + showKeyPickerDialog(); + return true; + }); + } + + @Override + public void onAttachedToHierarchy(@NonNull androidx.preference.PreferenceManager preferenceManager) { + super.onAttachedToHierarchy(preferenceManager); + refreshSummary(); + } + + private void showKeyPickerDialog() { + Context ctx = getContext(); + List keys = parseKeyCodes(getPersistedString(mDefaultValue)); + + View dialogView = LayoutInflater.from(ctx).inflate(R.layout.dialog_keyboard_pan_keys, null); + LinearLayout keyList = dialogView.findViewById(R.id.chatPanKeys_list); + Spinner spinner = dialogView.findViewById(R.id.chatPanKeys_spinner); + Button addButton = dialogView.findViewById(R.id.chatPanKeys_addButton); + + ArrayAdapter adapter = new ArrayAdapter<>(ctx, R.layout.item_centered_textview, + EfficientAndroidLWJGLKeycode.generateKeyName()); + spinner.setAdapter(adapter); + + rebuildKeyList(keyList, keys); + + addButton.setOnClickListener(v -> { + int keycode = EfficientAndroidLWJGLKeycode.getValueByIndex(spinner.getSelectedItemPosition()); + if (!keys.contains(keycode)) { + keys.add(keycode); + rebuildKeyList(keyList, keys); + } + }); + + new AlertDialog.Builder(ctx) + .setTitle(getTitle()) + .setView(dialogView) + .setPositiveButton(android.R.string.ok, (dialog, which) -> { + persistString(joinKeyCodes(keys)); + refreshSummary(); + }) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } + + /** Re-creates one tappable row per key; tapping a key removes it */ + private void rebuildKeyList(LinearLayout container, List keys) { + container.removeAllViews(); + int padding = (int) (12 * getContext().getResources().getDisplayMetrics().density); + for (Integer key : keys) { + TextView keyView = new TextView(container.getContext()); + keyView.setText(getKeyName(key)); + keyView.setGravity(Gravity.CENTER); + keyView.setPadding(0, padding, 0, padding); + keyView.setOnClickListener(v -> { + keys.remove(key); + rebuildKeyList(container, keys); + }); + container.addView(keyView); + } + } + + private void refreshSummary() { + List keys = parseKeyCodes(getPersistedString(mDefaultValue)); + if (keys.isEmpty()) { + setSummary(R.string.mcl_setting_chat_pan_keys_none); + return; + } + StringBuilder nameList = new StringBuilder(); + for (int key : keys) { + if (nameList.length() > 0) nameList.append(", "); + nameList.append(getKeyName(key)); + } + setSummary(nameList.toString()); + } + + private static String getKeyName(int keycode) { + return EfficientAndroidLWJGLKeycode.generateKeyName()[EfficientAndroidLWJGLKeycode.getIndexByValue(keycode)]; + } + + private static List parseKeyCodes(String stored) { + List keys = new ArrayList<>(); + if (stored != null) { + for (String part : stored.split(",")) { + try { + keys.add(Integer.valueOf(part.trim())); + } catch (NumberFormatException ignored) { + } + } + } + return keys; + } + + private static String joinKeyCodes(List keys) { + StringBuilder joined = new StringBuilder(); + for (int key : keys) { + if (joined.length() > 0) joined.append(','); + joined.append(key); + } + return joined.toString(); + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/LauncherPreferences.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/LauncherPreferences.java index f73aaf6382..bdc375b8d3 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/LauncherPreferences.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/LauncherPreferences.java @@ -20,7 +20,9 @@ import java.io.File; import java.io.IOException; +import java.util.HashSet; import java.util.LinkedHashMap; +import java.util.Set; public class LauncherPreferences { public static final String PREF_KEY_CURRENT_PROFILE = "currentProfile"; @@ -74,6 +76,8 @@ public class LauncherPreferences { public static boolean PREF_MOUSE_GRAB_FORCE = false; public static boolean PREF_KEYBOARD_PANNING = true; + public static Set PREF_CHAT_PAN_OPEN_KEYS = null; + public static Set PREF_CHAT_PAN_CLOSE_KEYS = null; public static void loadPreferences(Context ctx) { @@ -118,6 +122,10 @@ public static void loadPreferences(Context ctx) { PREF_TOUCHCONTROLLER_VIBRATE_LENGTH = DEFAULT_PREF.getInt("touchControllerVibrateLength", 100); PREF_MOUSE_GRAB_FORCE = DEFAULT_PREF.getBoolean("always_grab_mouse", false); PREF_KEYBOARD_PANNING = DEFAULT_PREF.getBoolean("keyboardPanning", true); + PREF_CHAT_PAN_OPEN_KEYS = parseKeyList(DEFAULT_PREF.getString("chatPanOpenKeys", null), + LwjglGlfwKeycode.GLFW_KEY_T, LwjglGlfwKeycode.GLFW_KEY_SLASH); + PREF_CHAT_PAN_CLOSE_KEYS = parseKeyList(DEFAULT_PREF.getString("chatPanCloseKeys", null), + LwjglGlfwKeycode.GLFW_KEY_ESCAPE, LwjglGlfwKeycode.GLFW_KEY_ENTER, LwjglGlfwKeycode.GLFW_KEY_KP_ENTER); // User may have deleted their default control String userDefCtrl = DEFAULT_PREF.getString("defaultCtrl", Tools.CTRLDEF_FILE); @@ -143,6 +151,27 @@ public static void loadPreferences(Context ctx) { } } + /** + * Parses a comma separated list of GLFW key codes into a set. + * Falls back to the given defaults when the preference is unset or empty, + * and skips anything that is not a plain number. + */ + private static Set parseKeyList(String value, int... defaultKeys) { + Set keys = new HashSet<>(); + if (value != null) { + for (String part : value.split(",")) { + try { + keys.add(Integer.parseInt(part.trim())); + } catch (NumberFormatException ignored) { + } + } + } + if (keys.isEmpty()) { + for (int key : defaultKeys) keys.add(key); + } + return keys; + } + /** * This functions aims at finding the best default RAM amount, * according to the RAM amount of the physical device. diff --git a/app_pojavlauncher/src/main/java/org/libsdl/app/SDLInputConnection.java b/app_pojavlauncher/src/main/java/org/libsdl/app/SDLInputConnection.java index c425f22889..e0b229b79e 100644 --- a/app_pojavlauncher/src/main/java/org/libsdl/app/SDLInputConnection.java +++ b/app_pojavlauncher/src/main/java/org/libsdl/app/SDLInputConnection.java @@ -12,6 +12,9 @@ import android.view.inputmethod.BaseInputConnection; import android.widget.EditText; +import net.kdt.pojavlaunch.LwjglGlfwKeycode; +import net.kdt.pojavlaunch.MainActivity; + class SDLInputConnection extends BaseInputConnection { protected EditText mEditText; @@ -49,6 +52,9 @@ public boolean sendKeyEvent(KeyEvent event) { // // return super.sendKeyEvent(event); if(KeyEvent.ACTION_DOWN == event.getAction()){ + if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) { + MainActivity.trackChatStateKey(LwjglGlfwKeycode.GLFW_KEY_ENTER, true); + } SDLActivity.onNativeKeyDown(event.getKeyCode()); } else SDLActivity.onNativeKeyUp(event.getKeyCode()); return true; @@ -123,12 +129,14 @@ protected void updateText() { int codePoint = pendingText.codePointAt(offset); if (codePoint == '\n') { if (SDLActivity.onNativeSoftReturnKey()) { + MainActivity.trackChatStateKey(LwjglGlfwKeycode.GLFW_KEY_ENTER, true); return; } } /* Higher code points don't generate simulated scancodes */ if (codePoint > 0 && codePoint < 128) { nativeGenerateScancodeForUnichar((char)codePoint); + MainActivity.trackChatStateChar((char)codePoint); } offset += Character.charCount(codePoint); } diff --git a/app_pojavlauncher/src/main/res/layout/dialog_keyboard_pan_keys.xml b/app_pojavlauncher/src/main/res/layout/dialog_keyboard_pan_keys.xml new file mode 100644 index 0000000000..30285b9e88 --- /dev/null +++ b/app_pojavlauncher/src/main/res/layout/dialog_keyboard_pan_keys.xml @@ -0,0 +1,32 @@ + + + + + + + + + +