From bf59e802514f3921aac139d758943b69990cdb66 Mon Sep 17 00:00:00 2001
From: "Md. Rifat Hasan Jihan" <31377578+RHJihan@users.noreply.github.com>
Date: Sat, 4 Jul 2026 01:13:15 +0600
Subject: [PATCH 1/5] implement recent emoji removal feature with long-press
delete option
---
.../java/helium314/keyboard/keyboard/Key.java | 6 +-
.../keyboard/emoji/DynamicGridKeyboard.java | 53 +++++++++++++---
.../keyboard/emoji/EmojiPageKeyboardView.java | 63 ++++++++++++++++++-
.../keyboard/emoji/EmojiPalettesView.java | 25 ++++++++
.../keyboard/emoji/EmojiSearchActivity.kt | 3 +
.../keyboard/emoji/EmojiViewCallback.java | 5 ++
.../keyboard_parser/floris/KeyCode.kt | 4 +-
app/src/main/res/values-bn/strings.xml | 2 +-
app/src/main/res/values/strings.xml | 1 +
9 files changed, 147 insertions(+), 15 deletions(-)
diff --git a/app/src/main/java/helium314/keyboard/keyboard/Key.java b/app/src/main/java/helium314/keyboard/keyboard/Key.java
index e54790d131..5c1d05a867 100644
--- a/app/src/main/java/helium314/keyboard/keyboard/Key.java
+++ b/app/src/main/java/helium314/keyboard/keyboard/Key.java
@@ -250,7 +250,11 @@ protected Key(@NonNull final Key key, @Nullable final PopupKeySpec[] popupKeys,
mY = key.mY;
mHitBox.set(key.mHitBox);
mPopupKeys = popupKeys;
- mPopupKeysColumnAndFlags = key.mPopupKeysColumnAndFlags;
+ // A key that is given popup keys needs at least one column to lay them out. The source key
+ // may report zero (e.g. emoji recent keys restored from settings), so ensure a valid count.
+ mPopupKeysColumnAndFlags = (popupKeys != null && (key.mPopupKeysColumnAndFlags & POPUP_KEYS_COLUMN_NUMBER_MASK) == 0)
+ ? (key.mPopupKeysColumnAndFlags & ~POPUP_KEYS_COLUMN_NUMBER_MASK) | 1
+ : key.mPopupKeysColumnAndFlags;
mBackgroundType = backgroundType;
mActionFlags = key.mActionFlags;
mKeyVisualAttributes = key.mKeyVisualAttributes;
diff --git a/app/src/main/java/helium314/keyboard/keyboard/emoji/DynamicGridKeyboard.java b/app/src/main/java/helium314/keyboard/keyboard/emoji/DynamicGridKeyboard.java
index f7ff4db83c..cffba1d873 100644
--- a/app/src/main/java/helium314/keyboard/keyboard/emoji/DynamicGridKeyboard.java
+++ b/app/src/main/java/helium314/keyboard/keyboard/emoji/DynamicGridKeyboard.java
@@ -167,6 +167,35 @@ public void removeAllKeys() {
}
}
+ public boolean isRecents() {
+ return mIsRecents;
+ }
+
+ public void removeKey(final Key usedKey) {
+ synchronized (mLock) {
+ boolean removed = false;
+ while (mGridKeys.remove(usedKey)) {
+ // Remove all keys matching the removed one.
+ removed = true;
+ }
+ if (!removed) {
+ return;
+ }
+ mCachedGridKeys = null;
+ updateKeyCoordinates();
+ }
+ if (mIsRecents) {
+ saveRecentKeys();
+ }
+ }
+
+ public void clearRecentKeys() {
+ removeAllKeys();
+ if (mIsRecents) {
+ saveRecentKeys();
+ }
+ }
+
private void addKey(final Key usedKey, final boolean addFirst) {
if (usedKey == null) {
return;
@@ -194,18 +223,22 @@ private void addKey(final Key usedKey, final boolean addFirst) {
while (mGridKeys.size() > mMaxKeyCount) {
mGridKeys.removeLast();
}
- int index = 0;
- for (final GridKey gridKey : mGridKeys) {
- while (mEmptyColumnIndices.contains(index % mColumnsNum)) {
- index++;
- }
- final int keyX0 = getKeyX0(index);
- final int keyY0 = getKeyY0(index);
- final int keyX1 = getKeyX1(index);
- final int keyY1 = getKeyY1(index);
- gridKey.updateCoordinates(keyX0, keyY0, keyX1, keyY1);
+ updateKeyCoordinates();
+ }
+ }
+
+ private void updateKeyCoordinates() {
+ int index = 0;
+ for (final GridKey gridKey : mGridKeys) {
+ while (mEmptyColumnIndices.contains(index % mColumnsNum)) {
index++;
}
+ final int keyX0 = getKeyX0(index);
+ final int keyY0 = getKeyY0(index);
+ final int keyX1 = getKeyX1(index);
+ final int keyY1 = getKeyY1(index);
+ gridKey.updateCoordinates(keyX0, keyY0, keyX1, keyY1);
+ index++;
}
}
diff --git a/app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiPageKeyboardView.java b/app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiPageKeyboardView.java
index 6142d41cb2..527c40f1fb 100644
--- a/app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiPageKeyboardView.java
+++ b/app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiPageKeyboardView.java
@@ -38,11 +38,14 @@
import helium314.keyboard.keyboard.PopupKeysKeyboard;
import helium314.keyboard.keyboard.PopupKeysKeyboardView;
import helium314.keyboard.keyboard.PopupKeysPanel;
+import helium314.keyboard.keyboard.internal.KeyboardIconsSet;
import helium314.keyboard.keyboard.internal.PopupKeySpec;
+import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode;
import helium314.keyboard.latin.R;
import helium314.keyboard.latin.common.CoordinateUtils;
import helium314.keyboard.latin.settings.Settings;
+import java.util.Locale;
import java.util.WeakHashMap;
/**
@@ -56,12 +59,20 @@ public final class EmojiPageKeyboardView extends KeyboardView implements
private static final long KEY_PRESS_DELAY_TIME = 250; // msec
private static final long KEY_RELEASE_DELAY_TIME = 30; // msec
+ // Single delete button shown when long-pressing a key in the recents category.
+ private static final PopupKeySpec[] REMOVE_RECENT_POPUP_KEYS = {
+ new PopupKeySpec(KeyboardIconsSet.PREFIX_ICON + KeyboardIconsSet.NAME_BIN
+ + "|!code/" + KeyCode.EMOJI_RECENT_REMOVE, false, Locale.ROOT)
+ };
+
private static final EmojiViewCallback EMPTY_EMOJI_VIEW_CALLBACK = new EmojiViewCallback() {
@Override
public void onPressKey(final Key key) {}
@Override
public void onReleaseKey(final Key key) {}
@Override
+ public void onRemoveRecentKey(final Key key) {}
+ @Override
public String getDescription(String emoji) {
return null;
}
@@ -200,6 +211,48 @@ private PopupKeysPanel showPopupKeysKeyboard(@NonNull final Key key) {
return mPopupKeysKeyboardView;
}
+ // Builds a single-key popup holding a delete button, used to remove a key from the recents category.
+ @NonNull
+ private PopupKeysPanel showRemoveKeyboard(@NonNull final Key key) {
+ Keyboard popupKeysKeyboard = mPopupKeysKeyboardCache.get(key);
+ if (popupKeysKeyboard == null) {
+ final Key removeKey = new DynamicGridKeyboard.GridKey(key, REMOVE_RECENT_POPUP_KEYS, null, key.getBackgroundType());
+ // The delete button is icon-only, so size the single-key popup explicitly instead of
+ // measuring a label (which would divide by a zero base width). Fall back to the most
+ // common key height so the width is always non-zero.
+ final Keyboard keyboard = getKeyboard();
+ final int keyWidth = Math.max(key.getHitBox().width(), keyboard.mMostCommonKeyHeight);
+ final int keyHeight = Math.max(key.getHitBox().height(), keyboard.mMostCommonKeyHeight);
+ final PopupKeysKeyboard.Builder builder = new PopupKeysKeyboard.Builder(
+ getContext(), removeKey, keyboard, true, keyWidth, keyHeight, newLabelPaint(removeKey));
+ popupKeysKeyboard = builder.build();
+ mPopupKeysKeyboardCache.put(key, popupKeysKeyboard);
+ }
+
+ mPopupKeysKeyboardView.setKeyboard(popupKeysKeyboard);
+ mPopupKeysKeyboardView.setVisibility(VISIBLE);
+ return mPopupKeysKeyboardView;
+ }
+
+ // Wraps the regular callback so that selecting the delete button removes the given recent key,
+ // while emoji descriptions keep being resolved through the original callback.
+ private EmojiViewCallback createRemoveRecentCallback(@NonNull final Key key) {
+ return new EmojiViewCallback() {
+ @Override
+ public void onPressKey(final Key pressedKey) {}
+ @Override
+ public void onReleaseKey(final Key releasedKey) {
+ mEmojiViewCallback.onRemoveRecentKey(key);
+ }
+ @Override
+ public void onRemoveRecentKey(final Key removedKey) {}
+ @Override
+ public String getDescription(final String emoji) {
+ return mEmojiViewCallback.getDescription(emoji);
+ }
+ };
+ }
+
private void dismissPopupKeysPanel() {
if (isShowingPopupKeysPanel()) {
mPopupKeysPanel.dismissPopupKeysPanel();
@@ -298,7 +351,13 @@ private void onLongPressed(final Key key) {
}
var descriptionPanel = showDescription(key);
- final PopupKeysPanel popupKeysPanel = showPopupKeysKeyboard(key);
+ // In the recents category, long-pressing a key offers a delete button to remove it from
+ // recents (similar to deleting a suggestion), instead of the skin tone variants panel.
+ final Keyboard keyboard = getKeyboard();
+ final boolean isRecents = keyboard instanceof DynamicGridKeyboard && ((DynamicGridKeyboard) keyboard).isRecents();
+ final PopupKeysPanel popupKeysPanel = isRecents ? showRemoveKeyboard(key) : showPopupKeysKeyboard(key);
+ // Route the delete button to recents removal, everything else to the regular callback.
+ final EmojiViewCallback callback = isRecents ? createRemoveRecentCallback(key) : mEmojiViewCallback;
final int x = mLastX;
final int y = mLastY;
@@ -314,7 +373,7 @@ private void onLongPressed(final Key key) {
: key.getX() + key.getWidth() / 2;
final int pointY = key.getY() - getKeyboard().mVerticalGap;
(popupKeysPanel != null? popupKeysPanel : descriptionPanel)
- .showPopupKeysPanel(this, this, pointX, pointY, mEmojiViewCallback);
+ .showPopupKeysPanel(this, this, pointX, pointY, callback);
}
if (popupKeysPanel != null) {
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 102668b26a..a2be55575b 100644
--- a/app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiPalettesView.java
+++ b/app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiPalettesView.java
@@ -246,6 +246,12 @@ private void addTab(LinearLayout host, EmojiCategory.Category category) {
host.addView(iconView);
iconView.setLayoutParams(new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f));
iconView.setOnClickListener(this);
+ if (category == EmojiCategory.Category.RECENTS) {
+ iconView.setOnLongClickListener(v -> {
+ clearRecentKeys();
+ return true;
+ });
+ }
}
@SuppressLint("ClickableViewAccessibility")
@@ -317,6 +323,25 @@ public void onReleaseKey(final Key key) {
mKeyboardActionListener.onCodeInput(KeyCode.ALPHA, NOT_A_COORDINATE, NOT_A_COORDINATE, false);
}
+ /**
+ * Called from {@link EmojiPageKeyboardView} through {@link EmojiViewCallback}
+ * interface to remove a single emoji from the recents category.
+ */
+ @Override
+ public void onRemoveRecentKey(final Key key) {
+ AudioAndHapticFeedbackManager.getInstance().performHapticAndAudioFeedback(KeyCode.NOT_SPECIFIED, this, HapticEvent.KEY_LONG_PRESS);
+ getRecentsKeyboard().removeKey(key);
+ if (initialized)
+ mPager.getAdapter().notifyItemChanged(mEmojiCategory.getRecentTabId());
+ }
+
+ private void clearRecentKeys() {
+ AudioAndHapticFeedbackManager.getInstance().performHapticAndAudioFeedback(KeyCode.NOT_SPECIFIED, this, HapticEvent.KEY_LONG_PRESS);
+ getRecentsKeyboard().clearRecentKeys();
+ if (initialized)
+ mPager.getAdapter().notifyItemChanged(mEmojiCategory.getRecentTabId());
+ }
+
@Override
public String getDescription(String emoji) {
if (sDictionaryFacilitator == null) {
diff --git a/app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiSearchActivity.kt b/app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiSearchActivity.kt
index fde84e0a1e..0914b27a3f 100644
--- a/app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiSearchActivity.kt
+++ b/app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiSearchActivity.kt
@@ -318,6 +318,9 @@ class EmojiSearchActivity : ComponentActivity() {
finish()
}
+ override fun onRemoveRecentKey(key: Key) {
+ }
+
override fun getDescription(emoji: String): String? = if (Settings.getValues().mShowEmojiDescriptions)
dictionaryFacilitator?.getWordProperty(getEmojiNeutralVersion(emoji))?.let {
if (it.mHasShortcuts) it.mShortcutTargets[0]?.mWord else null
diff --git a/app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiViewCallback.java b/app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiViewCallback.java
index 192d92fee4..8f8faded7c 100644
--- a/app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiViewCallback.java
+++ b/app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiViewCallback.java
@@ -22,6 +22,11 @@ public interface EmojiViewCallback {
*/
void onReleaseKey(Key key);
+ /**
+ * Called when a key in the recents category is long-pressed, to remove it from recents.
+ */
+ void onRemoveRecentKey(Key key);
+
/**
* Called from keyboard view to get an emoji description
*/
diff --git a/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/floris/KeyCode.kt b/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/floris/KeyCode.kt
index 11ded67f86..0eb4a9c6fc 100644
--- a/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/floris/KeyCode.kt
+++ b/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/floris/KeyCode.kt
@@ -106,6 +106,8 @@ object KeyCode {
const val CURRENCY_SLOT_5 = -805
const val CURRENCY_SLOT_6 = -806
+ const val EMOJI_RECENT_REMOVE = -10000 // remove a single emoji from the recents category
+
const val MULTIPLE_CODE_POINTS = -902
//const val DRAG_MARKER = -991
//const val NOOP = -999
@@ -208,7 +210,7 @@ object KeyCode {
MEDIA_PREVIOUS, VOL_UP, VOL_DOWN, MUTE, F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, BACK,
TIMESTAMP, CTRL_LEFT, CTRL_RIGHT, ALT_LEFT, ALT_RIGHT, META_LEFT, META_RIGHT, SEND_INTENT_ONE, SEND_INTENT_TWO,
SEND_INTENT_THREE, EMOJI_SEARCH, INLINE_EMOJI_SEARCH_DONE, META_LOCK,
- BACKGROUND_GATHERING, BACKGROUND_GATHERING_TEMP_OFF
+ BACKGROUND_GATHERING, BACKGROUND_GATHERING_TEMP_OFF, EMOJI_RECENT_REMOVE
-> this
// conversion
diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml
index 20094b6cac..089b1a5c87 100644
--- a/app/src/main/res/values-bn/strings.xml
+++ b/app/src/main/res/values-bn/strings.xml
@@ -96,7 +96,7 @@
The source key may report zero columns (e.g. emoji recent keys restored from settings), + * which cannot lay out popup keys. Callers that assign popup keys can pass an explicit column + * count here instead of relying on the source key.
+ * + * @param key the original key. + * @param popupKeys the popup keys that should be assigned to this key. + * @param labelHint the label hint that should be assigned to this key. + * @param backgroundType the background type that should be assigned to this key. + * @param popupKeysColumnNumber the number of columns to lay the popup keys out in. + */ + protected Key(@NonNull final Key key, @Nullable final PopupKeySpec[] popupKeys, + @Nullable final String labelHint, final int backgroundType, final int popupKeysColumnNumber) { // Final attributes. mCode = key.mCode; mLabel = key.mLabel; @@ -249,11 +267,8 @@ protected Key(@NonNull final Key key, @Nullable final PopupKeySpec[] popupKeys, mY = key.mY; mHitBox.set(key.mHitBox); mPopupKeys = popupKeys; - // A key that is given popup keys needs at least one column to lay them out. The source key - // may report zero (e.g. emoji recent keys restored from settings), so ensure a valid count. - mPopupKeysColumnAndFlags = (popupKeys != null && (key.mPopupKeysColumnAndFlags & POPUP_KEYS_COLUMN_NUMBER_MASK) == 0) - ? (key.mPopupKeysColumnAndFlags & ~POPUP_KEYS_COLUMN_NUMBER_MASK) | 1 - : key.mPopupKeysColumnAndFlags; + mPopupKeysColumnAndFlags = (key.mPopupKeysColumnAndFlags & ~POPUP_KEYS_COLUMN_NUMBER_MASK) + | (popupKeysColumnNumber & POPUP_KEYS_COLUMN_NUMBER_MASK); mBackgroundType = backgroundType; mActionFlags = key.mActionFlags; mKeyVisualAttributes = key.mKeyVisualAttributes; diff --git a/app/src/main/java/helium314/keyboard/keyboard/emoji/DynamicGridKeyboard.java b/app/src/main/java/helium314/keyboard/keyboard/emoji/DynamicGridKeyboard.java index 24f939cbfc..800d662fb6 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/emoji/DynamicGridKeyboard.java +++ b/app/src/main/java/helium314/keyboard/keyboard/emoji/DynamicGridKeyboard.java @@ -337,6 +337,11 @@ public GridKey(@NonNull final Key originalKey, @Nullable final PopupKeySpec[] po super(originalKey, popupKeys, labelHint, backgroundType); } + public GridKey(@NonNull final Key originalKey, @Nullable final PopupKeySpec[] popupKeys, + @Nullable final String labelHint, final int backgroundType, final int popupKeysColumnNumber) { + super(originalKey, popupKeys, labelHint, backgroundType, popupKeysColumnNumber); + } + public void updateCoordinates(final int x0, final int y0, final int x1, final int y1) { mCurrentX = x0; mCurrentY = y0; diff --git a/app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiPageKeyboardView.java b/app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiPageKeyboardView.java index 527c40f1fb..2f79b67c84 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiPageKeyboardView.java +++ b/app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiPageKeyboardView.java @@ -216,7 +216,10 @@ private PopupKeysPanel showPopupKeysKeyboard(@NonNull final Key key) { private PopupKeysPanel showRemoveKeyboard(@NonNull final Key key) { Keyboard popupKeysKeyboard = mPopupKeysKeyboardCache.get(key); if (popupKeysKeyboard == null) { - final Key removeKey = new DynamicGridKeyboard.GridKey(key, REMOVE_RECENT_POPUP_KEYS, null, key.getBackgroundType()); + // A recent key restored from settings can report zero popup-key columns, which cannot + // lay out popup keys. Give the remove popup one column per delete button it contains. + final Key removeKey = new DynamicGridKeyboard.GridKey(key, REMOVE_RECENT_POPUP_KEYS, null, + key.getBackgroundType(), REMOVE_RECENT_POPUP_KEYS.length); // The delete button is icon-only, so size the single-key popup explicitly instead of // measuring a label (which would divide by a zero base width). Fall back to the most // common key height so the width is always non-zero. From dafcbfbad1a08eae8857e4ecbeee9efc867b82ec Mon Sep 17 00:00:00 2001 From: "Md. Rifat Hasan Jihan" <31377578+RHJihan@users.noreply.github.com> Date: Mon, 20 Jul 2026 08:28:16 -0400 Subject: [PATCH 4/5] Fix build: wire recents removal/clear to RecentEmojis storage --- .../keyboard/emoji/DynamicGridKeyboard.java | 11 +++++++++-- .../keyboard/keyboard/emoji/RecentEmojis.kt | 17 +++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/helium314/keyboard/keyboard/emoji/DynamicGridKeyboard.java b/app/src/main/java/helium314/keyboard/keyboard/emoji/DynamicGridKeyboard.java index 800d662fb6..6e22f6c940 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/emoji/DynamicGridKeyboard.java +++ b/app/src/main/java/helium314/keyboard/keyboard/emoji/DynamicGridKeyboard.java @@ -182,14 +182,14 @@ public void removeKey(final Key usedKey) { updateKeyCoordinates(); } if (mIsRecents) { - saveRecentKeys(); + removeRecentKey(usedKey); } } public void clearRecentKeys() { removeAllKeys(); if (mIsRecents) { - saveRecentKeys(); + RecentEmojis.clear(); } } @@ -246,6 +246,13 @@ private void saveRecentKey(@Nullable Key key) { else RecentEmojis.addCodepoint(key.getCode()); } + private void removeRecentKey(@Nullable Key key) { + if (key == null) return; + String outputText = key.getOutputText(); + if (outputText != null) RecentEmojis.remove(outputText); + else RecentEmojis.removeCodepoint(key.getCode()); + } + private Key getKeyByCode(final Collection