diff --git a/app/src/main/java/helium314/keyboard/keyboard/KeyboardActionListener.java b/app/src/main/java/helium314/keyboard/keyboard/KeyboardActionListener.java index a71414fa30..181b5c481b 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/KeyboardActionListener.java +++ b/app/src/main/java/helium314/keyboard/keyboard/KeyboardActionListener.java @@ -13,30 +13,27 @@ import helium314.keyboard.latin.common.InputPointers; public interface KeyboardActionListener { + + enum Direction { + UP, DOWN, LEFT, RIGHT + } + /** * Called when the user presses a key. This is sent before the {@link #onCodeInput} is called. * For keys that repeat, this is only called once. - * - * @param primaryCode the unicode of the key being pressed. If the touch is not on a valid key, - * the value will be zero. - * @param repeatCount how many times the key was repeated. Zero if it is the first press. - * @param isSinglePointer true if pressing has occurred while no other key is being pressed. - * @param hapticEvent the type of haptic feedback to perform. */ void onPressKey(int primaryCode, int repeatCount, boolean isSinglePointer, HapticEvent hapticEvent); void onLongPressKey(int primaryCode); /** - * Called when the user releases a key. This is sent after the {@link #onCodeInput} is called. - * For keys that repeat, this is only called once. - * - * @param primaryCode the code of the key that was released - * @param withSliding true if releasing has occurred because the user slid finger from the key - * to other key without releasing the finger. + * Called when the user releases a key. */ void onReleaseKey(int primaryCode, boolean withSliding); + void onMoveFocus(Direction direction); + void onPressFocusedKey(); + /** For handling hardware key presses. Returns whether the event was handled. */ boolean onKeyDown(int keyCode, KeyEvent keyEvent); @@ -45,68 +42,33 @@ public interface KeyboardActionListener { /** * Send a key code to the listener. - * - * @param primaryCode this is the code of the key that was pressed - * @param x x-coordinate pixel of touched event. If onCodeInput is not called by - * {@link PointerTracker} or so, the value should be - * {@link Constants#NOT_A_COORDINATE}. If it's called on insertion from the - * suggestion strip, it should be {@link Constants#SUGGESTION_STRIP_COORDINATE}. - * @param y y-coordinate pixel of touched event. If #onCodeInput is not called by - * {@link PointerTracker} or so, the value should be - * {@link Constants#NOT_A_COORDINATE}.If it's called on insertion from the - * suggestion strip, it should be {@link Constants#SUGGESTION_STRIP_COORDINATE}. - * @param isKeyRepeat true if this is a key repeat, false otherwise */ - // TODO: change this to send an Event object instead void onCodeInput(int primaryCode, int x, int y, boolean isKeyRepeat); - /** - * Sends a string of characters to the listener. - * - * @param text the string of characters to be registered. - */ + /** Sends a string of characters to the listener. */ void onTextInput(String text); - /** - * Called when user started batch input. - */ + /** Called when user started batch input. */ void onStartBatchInput(); - /** - * Sends the ongoing batch input points data. - * @param batchPointers the batch input points representing the user input - */ + /** Sends the ongoing batch input points data. */ void onUpdateBatchInput(InputPointers batchPointers); - /** - * Sends the final batch input points data. - * - * @param batchPointers the batch input points representing the user input - */ + /** Sends the final batch input points data. */ void onEndBatchInput(InputPointers batchPointers); void onCancelBatchInput(); - /** - * Called when user released a finger outside any key. - */ + /** Called when user released a finger outside any key. */ void onCancelInput(); - /** - * Called when user finished sliding key input. - */ + /** Called when user finished sliding key input. */ void onFinishSlidingInput(); - /** - * Send a non-"code input" custom request to the listener. - * @return true if the request has been consumed, false otherwise. - */ + /** Send a non-"code input" custom request to the listener. */ boolean onCustomRequest(int requestCode); - /** - * Called when the user performs a horizontal or vertical swipe gesture - * on the space bar. - */ + /** Swipes on space bar etc. */ boolean onHorizontalSpaceSwipe(int steps); boolean onVerticalSpaceSwipe(int steps); void onEndSpaceSwipe(); @@ -125,55 +87,82 @@ public interface KeyboardActionListener { int SWIPE_HIDE_KEYBOARD = 4; class Adapter implements KeyboardActionListener { + @Override public void onPressKey(int primaryCode, int repeatCount, boolean isSinglePointer, HapticEvent hapticEvent) {} + @Override public void onLongPressKey(int primaryCode) {} + @Override public void onReleaseKey(int primaryCode, boolean withSliding) {} + @Override public boolean onKeyDown(int keyCode, KeyEvent keyEvent) { return false; } + @Override public boolean onKeyUp(int keyCode, KeyEvent keyEvent) { return false; } + @Override public void onCodeInput(int primaryCode, int x, int y, boolean isKeyRepeat) {} + @Override public void onTextInput(String text) {} + @Override public void onStartBatchInput() {} + @Override public void onUpdateBatchInput(InputPointers batchPointers) {} + @Override public void onEndBatchInput(InputPointers batchPointers) {} + @Override public void onCancelBatchInput() {} + @Override public void onCancelInput() {} + @Override public void onFinishSlidingInput() {} + @Override public boolean onCustomRequest(int requestCode) { return false; } + @Override public boolean onHorizontalSpaceSwipe(int steps) { return false; } + @Override public boolean onVerticalSpaceSwipe(int steps) { return false; } + @Override public boolean toggleNumpad(boolean withSliding, boolean forceReturnToAlpha) { return false; } + @Override public void onEndSpaceSwipe() {} + @Override public void onMoveDeletePointer(int steps) {} + @Override public void onUpWithDeletePointerActive() {} + @Override public void resetMetaState() {} + + @Override + public void onMoveFocus(Direction direction) {} + + @Override + public void onPressFocusedKey() {} } } diff --git a/app/src/main/java/helium314/keyboard/keyboard/KeyboardActionListenerImpl.kt b/app/src/main/java/helium314/keyboard/keyboard/KeyboardActionListenerImpl.kt index 814a7733ac..73050b1e0c 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/KeyboardActionListenerImpl.kt +++ b/app/src/main/java/helium314/keyboard/keyboard/KeyboardActionListenerImpl.kt @@ -60,7 +60,18 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp override fun onReleaseKey(primaryCode: Int, withSliding: Boolean) { metaOnReleaseKey(primaryCode) - keyboardSwitcher.onReleaseKey(primaryCode, withSliding, latinIME.currentAutoCapsState, latinIME.currentRecapitalizeState) + keyboardSwitcher.onReleaseKey(primaryCode, withSliding, + latinIME.currentAutoCapsState, latinIME.currentRecapitalizeState) + } + + override fun onMoveFocus(direction: KeyboardActionListener.Direction) { + val kv = KeyboardView.getActiveKeyboardView() ?: return + kv.moveFocus(direction) + } + + override fun onPressFocusedKey() { + val kv = KeyboardView.getActiveKeyboardView() ?: return + kv.pressFocusedKey() } override fun onKeyUp(keyCode: Int, keyEvent: KeyEvent): Boolean { diff --git a/app/src/main/java/helium314/keyboard/keyboard/KeyboardView.java b/app/src/main/java/helium314/keyboard/keyboard/KeyboardView.java index 07f39ffd48..48cd476dc9 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/KeyboardView.java +++ b/app/src/main/java/helium314/keyboard/keyboard/KeyboardView.java @@ -27,6 +27,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import helium314.keyboard.event.HapticEvent; import helium314.keyboard.keyboard.emoji.EmojiPageKeyboardView; import helium314.keyboard.keyboard.internal.KeyDrawParams; import helium314.keyboard.keyboard.internal.KeyVisualAttributes; @@ -40,8 +41,12 @@ import helium314.keyboard.latin.suggestions.MoreSuggestions; import helium314.keyboard.latin.suggestions.MoreSuggestionsView; import helium314.keyboard.latin.utils.TypefaceUtils; +import helium314.keyboard.keyboard.KeyboardActionListener.Direction; import java.util.HashSet; +import java.util.List; +import java.util.ArrayList; +import java.util.Collections; /** A view that renders a virtual {@link Keyboard}. */ // todo: this ThemeStyle-dependent stuff really should not be in here! @@ -68,6 +73,11 @@ public class KeyboardView extends View { private float mKeyScaleForText; protected float mFontSizeMultiplier; + @Nullable + protected Key mFocusedKey; + @Nullable + protected static KeyboardActionListener sGlobalKeyboardActionListener; + // The maximum key label width in the proportion to the key width. private static final float MAX_LABEL_RATIO = 0.90f; @@ -75,9 +85,22 @@ public class KeyboardView extends View { // TODO: Consider having a dummy keyboard object to make this @NonNull @Nullable private Keyboard mKeyboard; + + @Nullable + private static KeyboardView sActiveKeyboardView; + + @Nullable + public static KeyboardView getActiveKeyboardView() { + return sActiveKeyboardView; + } + @NonNull private final KeyDrawParams mKeyDrawParams = new KeyDrawParams(); + public static void setGlobalKeyboardActionListener(@Nullable KeyboardActionListener listener) { + sGlobalKeyboardActionListener = listener; + } + // Drawing /** True if all keys should be drawn */ private boolean mInvalidateAllKeys; @@ -181,7 +204,6 @@ public void setKeyboard(@NonNull final Keyboard keyboard) { } else if (keyboard instanceof PopupKeysKeyboard) { mColors.setBackground(this, ColorType.POPUP_KEYS_BACKGROUND); } else { - // actual background color/drawable is applied to main_keyboard_frame setBackgroundColor(Color.TRANSPARENT); } @@ -193,11 +215,21 @@ public void setKeyboard(@NonNull final Keyboard keyboard) { invalidateAllKeys(); requestLayout(); mFontSizeMultiplier = mKeyboard.mId.isEmojiKeyboard() - // In the case of EmojiKeyFit, the size of emojis is taken care of by the size of the keys - ? (Settings.getValues().mEmojiKeyFit? 1 : Settings.getValues().mFontSizeMultiplierEmoji) - : Settings.getValues().mFontSizeMultiplier; + ? (Settings.getValues().mEmojiKeyFit ? 1 : Settings.getValues().mFontSizeMultiplierEmoji) + : Settings.getValues().mFontSizeMultiplier; } + @Override + protected void onVisibilityChanged(@NonNull View changedView, int visibility) { + super.onVisibilityChanged(changedView, visibility); + if (changedView == this) { + if (visibility == VISIBLE) { + sActiveKeyboardView = this; + } else if (sActiveKeyboardView == this) { + sActiveKeyboardView = null; + } + } + } /** * Returns the current keyboard being displayed by this view. * @return the currently attached keyboard @@ -345,6 +377,14 @@ private void onDrawKey(@NonNull final Key key, @NonNull final Canvas canvas, } onDrawKeyTopVisuals(key, canvas, paint, params); + if (key == mFocusedKey) { + Paint highlightPaint = new Paint(paint); + highlightPaint.setStyle(Paint.Style.STROKE); + highlightPaint.setStrokeWidth(3.0f); + highlightPaint.setColor(Color.YELLOW); + canvas.drawRect(0, 0, key.getDrawWidth(), key.getHeight(), highlightPaint); + } + canvas.translate(-keyDrawX, -keyDrawY); } @@ -606,6 +646,137 @@ public void invalidateKey(@Nullable final Key key) { invalidate(x, y, x + key.getWidth(), y + key.getHeight()); } + public void pressFocusedKey() { + if (mFocusedKey == null) return; + + int code = mFocusedKey.getCode(); + if (code == 0) return; + + int x = mFocusedKey.getX() + mFocusedKey.getWidth() / 2; + int y = mFocusedKey.getY() + mFocusedKey.getHeight() / 2; + + if (sGlobalKeyboardActionListener != null) { + sGlobalKeyboardActionListener.onPressKey(code, 0, true, HapticEvent.KEY_PRESS); + sGlobalKeyboardActionListener.onCodeInput(code, x, y, false); + sGlobalKeyboardActionListener.onReleaseKey(code, false); + } + } + + public void moveFocus(final Direction direction) { + final Keyboard keyboard = getKeyboard(); + if (keyboard == null) { + return; + } + + final List allKeys = keyboard.getSortedKeys(); + if (allKeys.isEmpty()) return; + + final ArrayList> rows = new ArrayList<>(); + final int rowTolerance = keyboard.mMostCommonKeyHeight / 2; + + for (Key k : allKeys) { + if (k.isSpacer()) continue; + if (!k.isEnabled()) continue; + + final int cy = k.getY() + k.getHeight() / 2; + + ArrayList targetRow = null; + for (ArrayList row : rows) { + Key rowKey = row.get(0); + int rowCy = rowKey.getY() + rowKey.getHeight() / 2; + if (Math.abs(cy - rowCy) <= rowTolerance) { + targetRow = row; + break; + } + } + if (targetRow == null) { + targetRow = new ArrayList<>(); + rows.add(targetRow); + } + targetRow.add(k); + } + + if (rows.isEmpty()) return; + + Collections.sort(rows, (r1, r2) -> { + int y1 = r1.get(0).getY(); + int y2 = r2.get(0).getY(); + return Integer.compare(y1, y2); + }); + for (ArrayList row : rows) { + Collections.sort(row, (k1, k2) -> Integer.compare(k1.getX(), k2.getX())); + } + + if (mFocusedKey == null || !allKeys.contains(mFocusedKey)) { + mFocusedKey = rows.get(0).get(0); + invalidateAllKeys(); + return; + } + + int currentRow = -1; + int currentCol = -1; + for (int r = 0; r < rows.size(); r++) { + ArrayList row = rows.get(r); + int col = row.indexOf(mFocusedKey); + if (col != -1) { + currentRow = r; + currentCol = col; + break; + } + } + + if (currentRow == -1) { + mFocusedKey = rows.get(0).get(0); + invalidateAllKeys(); + return; + } + + int newRow = currentRow; + int newCol = currentCol; + + switch (direction) { + case LEFT: + newCol = Math.max(0, currentCol - 1); + break; + case RIGHT: + newCol = Math.min(rows.get(currentRow).size() - 1, currentCol + 1); + break; + case UP: + newRow = Math.max(0, currentRow - 1); + newCol = findClosestColumn(rows.get(newRow), mFocusedKey); + break; + case DOWN: + newRow = Math.min(rows.size() - 1, currentRow + 1); + newCol = findClosestColumn(rows.get(newRow), mFocusedKey); + break; + } + + Key newKey = rows.get(newRow).get(newCol); + if (newKey != mFocusedKey) { + invalidateKey(mFocusedKey); + mFocusedKey = newKey; + invalidateKey(mFocusedKey); + } + } + + private int findClosestColumn(ArrayList row, Key reference) { + int refCx = reference.getX() + reference.getWidth() / 2; + int bestIndex = 0; + int bestDist = Integer.MAX_VALUE; + + for (int i = 0; i < row.size(); i++) { + Key k = row.get(i); + int cx = k.getX() + k.getWidth() / 2; + int dist = Math.abs(cx - refCx); + if (dist < bestDist) { + bestDist = dist; + bestIndex = i; + } + } + return bestIndex; + } + + @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); @@ -639,5 +810,4 @@ private void setKeyIconColor(Key key, Drawable icon, Keyboard keyboard) { mColors.setColor(icon, ColorType.KEY_TEXT); } } - } diff --git a/app/src/main/java/helium314/keyboard/keyboard/MainKeyboardView.java b/app/src/main/java/helium314/keyboard/keyboard/MainKeyboardView.java index 7c1e7e4480..6131f6bce7 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/MainKeyboardView.java +++ b/app/src/main/java/helium314/keyboard/keyboard/MainKeyboardView.java @@ -31,6 +31,7 @@ import helium314.keyboard.accessibility.AccessibilityUtils; import helium314.keyboard.accessibility.MainKeyboardAccessibilityDelegate; import helium314.keyboard.compat.ConfigurationCompatKt; +import helium314.keyboard.event.HapticEvent; import helium314.keyboard.keyboard.internal.DrawingPreviewPlacerView; import helium314.keyboard.keyboard.internal.DrawingProxy; import helium314.keyboard.keyboard.internal.GestureFloatingTextDrawingPreview; @@ -282,8 +283,10 @@ public void setLanguageOnSpacebarAnimAlpha(final int alpha) { public void setKeyboardActionListener(final KeyboardActionListener listener) { mKeyboardActionListener = listener; PointerTracker.setKeyboardActionListener(listener); + KeyboardView.setGlobalKeyboardActionListener(listener); } + // TODO: We should reconsider which coordinate system should be used to represent keyboard event. public int getKeyX(final int x) { return Constants.isValidCoordinate(x) ? mKeyDetector.getTouchX(x) : x; @@ -842,6 +845,29 @@ private void drawLanguageOnSpacebar(final Key key, final Canvas canvas, final Pa paint.setTextScaleX(1.0f); } + @Override + public void pressFocusedKey() { + if (mFocusedKey == null || mKeyboardActionListener == null) { + return; + } + + final Key key = mFocusedKey; + final int code = key.getCode(); + if (code == 0) return; + + final int centerX = key.getX() + key.getWidth() / 2; + final int centerY = key.getY() + key.getHeight() / 2; + final int keyX = getKeyX(centerX); + final int keyY = getKeyY(centerY); + + mKeyboardActionListener.onPressKey(code, 0, true, HapticEvent.KEY_PRESS); + mKeyboardActionListener.onCodeInput(code, keyX, keyY, false); + mKeyboardActionListener.onReleaseKey(code, false); + + invalidateKey(key); + } + + @Override public void deallocateMemory() { super.deallocateMemory(); diff --git a/app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiPalettesAdapter.java b/app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiPalettesAdapter.java index 5c259e2354..45aa534c63 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiPalettesAdapter.java +++ b/app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiPalettesAdapter.java @@ -6,47 +6,64 @@ package helium314.keyboard.keyboard.emoji; -import helium314.keyboard.latin.utils.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; - -import helium314.keyboard.keyboard.Keyboard; -import helium314.keyboard.latin.R; +import android.view.KeyEvent; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; -final class EmojiPalettesAdapter extends RecyclerView.Adapter{ - private static final String TAG = EmojiPalettesAdapter.class.getSimpleName(); - private static final boolean DEBUG_PAGER = false; +import helium314.keyboard.keyboard.Keyboard; +import helium314.keyboard.latin.R; +final class EmojiPalettesAdapter extends RecyclerView.Adapter { private final int mCategoryId; private final EmojiViewCallback mEmojiViewCallback; private final EmojiCategory mEmojiCategory; + private final FocusRequestHandler mFocusHandler; + + public interface FocusRequestHandler { + boolean requestFocusOnTab(int index); + boolean requestFocusOnBottomRow(); + } - public EmojiPalettesAdapter(final EmojiCategory emojiCategory, int categoryId, final EmojiViewCallback emojiViewCallback) { + public EmojiPalettesAdapter(final EmojiCategory emojiCategory, + int categoryId, + final EmojiViewCallback emojiViewCallback, + final FocusRequestHandler focusHandler) { mEmojiCategory = emojiCategory; mCategoryId = categoryId; mEmojiViewCallback = emojiViewCallback; + mFocusHandler = focusHandler; } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { final LayoutInflater inflater = LayoutInflater.from(parent.getContext()); - final EmojiPageKeyboardView keyboardView = (EmojiPageKeyboardView)inflater.inflate( - R.layout.emoji_keyboard_page, parent, false); + final EmojiPageKeyboardView keyboardView = (EmojiPageKeyboardView) inflater.inflate( + R.layout.emoji_keyboard_page, parent, false); keyboardView.setEmojiViewCallback(mEmojiViewCallback); + keyboardView.setFocusable(true); + keyboardView.setFocusableInTouchMode(true); + keyboardView.setOnKeyListener((v, keyCode, event) -> { + if (event.getAction() == KeyEvent.ACTION_DOWN) { + if (keyCode == KeyEvent.KEYCODE_DPAD_UP) { + return mFocusHandler.requestFocusOnTab(0); + } else if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { + return mFocusHandler.requestFocusOnBottomRow(); + } + } + return false; + }); + return new ViewHolder(keyboardView); } - @Override - public void onBindViewHolder(@NonNull EmojiPalettesAdapter.ViewHolder holder, int position) { - if (DEBUG_PAGER) { - Log.d(TAG, "instantiate item: " + position); - } + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { final Keyboard keyboard = mEmojiCategory.getKeyboardFromAdapterPosition(mCategoryId, position); holder.getKeyboardView().setKeyboard(keyboard); } @@ -65,7 +82,7 @@ public int getItemCount() { static class ViewHolder extends RecyclerView.ViewHolder { private final EmojiPageKeyboardView customView; - public ViewHolder(View v) { + public ViewHolder(@NonNull View v) { super(v); customView = (EmojiPageKeyboardView) v; } @@ -75,4 +92,3 @@ public EmojiPageKeyboardView getKeyboardView() { } } } - diff --git a/app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiPalettesView.java b/app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiPalettesView.java index de8d103235..b2f8027ace 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiPalettesView.java +++ b/app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiPalettesView.java @@ -95,6 +95,12 @@ public void onPageSelected(int position) { }); } + public RecyclerView getRecyclerViewForCurrent() { + if (mPager == null) return null; + final int pos = mPager.getCurrentItem(); + return mViews.get(pos); + } + @Override public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) { recyclerView.setItemViewCacheSize(mEmojiCategory.getShownCategories().size()); @@ -129,8 +135,28 @@ public void onBindViewHolder(PagerViewHolder holder, int position) { holder.mCategoryId = getItemId(position); var recyclerView = getRecyclerView(holder.itemView); mViews.put(position, recyclerView); - recyclerView.setAdapter(new EmojiPalettesAdapter(mEmojiCategory, (int) holder.mCategoryId, - EmojiPalettesView.this)); + EmojiPalettesAdapter.FocusRequestHandler focusHandler = new EmojiPalettesAdapter.FocusRequestHandler() { + @Override + public boolean requestFocusOnTab(int tabIndex) { + return EmojiPalettesView.this.requestFocusOnTab(tabIndex); + } + + @Override + public boolean requestFocusOnBottomRow() { + return EmojiPalettesView.this.requestFocusOnBottomRow(); + } + }; + + recyclerView.setAdapter(new EmojiPalettesAdapter( + mEmojiCategory, + (int) holder.mCategoryId, + EmojiPalettesView.this, + focusHandler + )); + + recyclerView.setFocusable(true); + recyclerView.setFocusableInTouchMode(true); + recyclerView.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); if (! mInitialized) { recyclerView.scrollToPosition(mEmojiCategory.getCurrentCategoryPageId()); @@ -242,8 +268,65 @@ private void addTab(final LinearLayout host, final int categoryId) { host.addView(iconView); iconView.setLayoutParams(new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f)); iconView.setOnClickListener(this); + + // -------- NEW: make tab focusable and handle DPAD_DOWN ---------- + iconView.setFocusable(true); + iconView.setFocusableInTouchMode(true); + iconView.setOnKeyListener((v, keyCode, event) -> { + if (event.getAction() == android.view.KeyEvent.ACTION_DOWN) { + if (keyCode == android.view.KeyEvent.KEYCODE_DPAD_DOWN) { + // when user presses down on a tab -> go to grid + requestFocusOnGrid(); + return true; + } + // allow left/right on the tab strip itself (system will move focus to adjacent tab) + } + return false; + }); + } + + public boolean requestFocusOnTab(final int tabIndex) { + if (mTabStrip == null || tabIndex < 0 || tabIndex >= mTabStrip.getChildCount()) return false; + View tab = mTabStrip.getChildAt(tabIndex); + if (tab == null) return false; + return tab.requestFocus(); + } + + public boolean requestFocusOnGrid() { + if (mPager == null || mPager.getAdapter() == null) return false; + PagerAdapter adapter = (PagerAdapter) mPager.getAdapter(); + RecyclerView rv = adapter.getRecyclerViewForCurrent(); + if (rv == null) return false; + + // try to focus the first completely visible child; fallback to recyclerView itself + RecyclerView.LayoutManager lm = rv.getLayoutManager(); + if (lm instanceof LinearLayoutManager) { + LinearLayoutManager llm = (LinearLayoutManager) lm; + int pos = llm.findFirstCompletelyVisibleItemPosition(); + if (pos == RecyclerView.NO_POSITION) pos = llm.findFirstVisibleItemPosition(); + if (pos != RecyclerView.NO_POSITION) { + View child = llm.findViewByPosition(pos); + if (child != null && child.isFocusable()) { + child.requestFocus(); + return true; + } + } + } + + // fallback + rv.requestFocus(); + return true; } + public boolean requestFocusOnBottomRow() { + MainKeyboardView keyboardView = findViewById(R.id.bottom_row_keyboard); + if (keyboardView == null) return false; + keyboardView.setFocusable(true); + keyboardView.setFocusableInTouchMode(true); + return keyboardView.requestFocus(); + } + + @SuppressLint("ClickableViewAccessibility") public void initialize() { // needs to be delayed for access to EmojiTabStrip, which is not a child of this view if (initialized) return; @@ -333,16 +416,49 @@ public void setHardwareAcceleratedDrawingEnabled(final boolean enabled) { } public void startEmojiPalettes(final KeyVisualAttributes keyVisualAttr, - final EditorInfo editorInfo, final KeyboardActionListener keyboardActionListener) { + final EditorInfo editorInfo, + final KeyboardActionListener keyboardActionListener) { initialize(); - setupBottomRowKeyboard(editorInfo, keyboardActionListener); final KeyDrawParams params = new KeyDrawParams(); params.updateParams(mEmojiLayoutParams.getBottomRowKeyboardHeight(), keyVisualAttr); setupSidePadding(); initDictionaryFacilitator(); + post(() -> { + boolean focused = false; + + if (mTabStrip != null && mTabStrip.getChildCount() > 0) { + focused = requestFocusOnTab(0); + } + + if (!focused) { + focused = requestFocusOnGrid(); + } + + if (!focused) { + requestFocusOnBottomRow(); + } + }); + + if (mTabStrip != null) { + for (int i = 0; i < mTabStrip.getChildCount(); i++) { + View tab = mTabStrip.getChildAt(i); + if (tab != null) { + tab.setFocusable(true); + tab.setFocusableInTouchMode(true); + tab.setOnKeyListener((v, keyCode, event) -> { + if (event.getAction() == android.view.KeyEvent.ACTION_DOWN + && keyCode == android.view.KeyEvent.KEYCODE_DPAD_DOWN) { + return requestFocusOnGrid(); + } + return false; + }); + } + } + } } + private void addRecentKey(final Key key) { if (Settings.getValues().mIncognitoModeEnabled) { // We do not want to log recent keys while being in incognito @@ -363,6 +479,16 @@ private void setupBottomRowKeyboard(final EditorInfo editorInfo, final KeyboardA final KeyboardLayoutSet kls = KeyboardLayoutSet.Builder.buildEmojiClipBottomRow(getContext(), editorInfo); final Keyboard keyboard = kls.getKeyboard(KeyboardId.ELEMENT_EMOJI_BOTTOM_ROW); keyboardView.setKeyboard(keyboard); + keyboardView.setOnKeyListener((v, keyCode, event) -> { + if (event.getAction() == android.view.KeyEvent.ACTION_DOWN) { + if (keyCode == android.view.KeyEvent.KEYCODE_DPAD_UP) { + requestFocusOnGrid(); + return true; + } + } + return false; + }); + } private void setupSidePadding() { diff --git a/app/src/main/java/helium314/keyboard/latin/LatinIME.java b/app/src/main/java/helium314/keyboard/latin/LatinIME.java index afbe7b4a68..5b037462d3 100644 --- a/app/src/main/java/helium314/keyboard/latin/LatinIME.java +++ b/app/src/main/java/helium314/keyboard/latin/LatinIME.java @@ -41,6 +41,7 @@ import helium314.keyboard.event.HapticEvent; import helium314.keyboard.keyboard.KeyboardActionListener; import helium314.keyboard.keyboard.KeyboardActionListenerImpl; +import helium314.keyboard.keyboard.KeyboardView; import helium314.keyboard.keyboard.internal.KeyboardIconsSet; import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode; import helium314.keyboard.latin.common.InsetsOutlineProvider; @@ -131,6 +132,10 @@ public class LatinIME extends InputMethodService implements private InsetsOutlineProvider mInsetsUpdater; private SuggestionStripView mSuggestionStripView; + public enum Direction { + UP, DOWN, LEFT, RIGHT + } + private RichInputMethodManager mRichImm; final KeyboardSwitcher mKeyboardSwitcher; private final SubtypeState mSubtypeState = new SubtypeState((InputMethodSubtype subtype) -> { switchToSubtype(subtype); return Unit.INSTANCE; }); @@ -1627,12 +1632,55 @@ public void hapticAndAudioFeedback(final int code, final int repeatCount, // Hooks for hardware keyboard @Override - public boolean onKeyDown(final int keyCode, final KeyEvent keyEvent) { - if (mKeyboardActionListener.onKeyDown(keyCode, keyEvent)) + public boolean onKeyDown(final int keyCode, final KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (isInputViewShown()) { + final MainKeyboardView mkv = KeyboardSwitcher.getInstance().getMainKeyboardView(); + if (mkv != null && mkv.isShowingPopupKeysPanel()) { + mkv.onCancelPopupKeysPanel(); + } else { + requestHideSelf(0); + } + return true; + } + return super.onKeyDown(keyCode, event); + } + + if (!isInputViewShown()) { + return super.onKeyDown(keyCode, event); + } + + final View kv = + KeyboardSwitcher.getInstance().getVisibleKeyboardView(); + final boolean canHandle = (kv != null && kv.isShown()); + + if (canHandle) { + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_LEFT: + mKeyboardActionListener.onMoveFocus(KeyboardActionListener.Direction.LEFT); + return true; + case KeyEvent.KEYCODE_DPAD_RIGHT: + mKeyboardActionListener.onMoveFocus(KeyboardActionListener.Direction.RIGHT); + return true; + case KeyEvent.KEYCODE_DPAD_UP: + mKeyboardActionListener.onMoveFocus(KeyboardActionListener.Direction.UP); + return true; + case KeyEvent.KEYCODE_DPAD_DOWN: + mKeyboardActionListener.onMoveFocus(KeyboardActionListener.Direction.DOWN); + return true; + case KeyEvent.KEYCODE_DPAD_CENTER: + case KeyEvent.KEYCODE_ENTER: + mKeyboardActionListener.onPressFocusedKey(); + return true; + } + } + + if (mKeyboardActionListener.onKeyDown(keyCode, event)) return true; - return super.onKeyDown(keyCode, keyEvent); + return super.onKeyDown(keyCode, event); } + @Override public boolean onKeyUp(final int keyCode, final KeyEvent keyEvent) { if (mKeyboardActionListener.onKeyUp(keyCode, keyEvent))