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 @@ প্রচ্ছন্ন বৈশিষ্ট্য অপরিদৃষ্ট হতে পারে এমন বৈশিষ্ট্যের বর্ণনা ডিভাইস সুরক্ষিত স্টোরেজ - ► বাংলা চন্দ্রবিন্দু (ঁ) বোতামে দীর্ঘ চাপে সচরাচর ব্যবহৃত হয় না এমন বর্ণ যেমন: ৠ, ৡ, ৺, ৰ, ৱ ইত্যাদি টাইপ করা যাবে। \'র\' এবং \'ত\' বোতামে দীর্ঘ চাপে যথাক্রমে \'র‍্য\' এবং \'ৎ\' টাইপ করা যাবে। এছাড়াও দাঁড়ি (।) বোতামে দীর্ঘ চাপে <i>ZWNJ</i> এবং <i>ZWJ</i> টাইপ করা যাবে। <br> <br>► পিন করে রাখা টুলবার বোতামে দীর্ঘ চাপ দিলে তা অতিরিক্ত কাজ করবে: <br> \n\t• ক্লিপবোর্ড &#65515; পেস্ট <br> \n\t• বামে/ডানে স্থানান্তর &#65515; সম্পূর্ণ বামে/ডানে স্থানান্তর <br> \n\t• উপরে/নিচে স্থানান্তর &#65515; পৃষ্ঠার উপরে/নিচে <br> \n\t• শব্দের ডানে/বামে &#65515; লাইনের শুরু/শেষ <br> \n\t• পাতার উপরে/নিচে &#65515; পাতার শুরু/শেষ <br> \n\t• অনুলিপি &#65515; কর্তন <br> \n\t• শব্দ নির্বাচন &#8596; সমস্ত নির্বাচন <br> \n\t• পূর্বাবস্থা &#8596; পুনরাবৃত্তি <br> <br> \n► পরামর্শ ফলকের টুলবারের কোনো বোতামে দীর্ঘ চাপ দিলে তা পরামর্শ ফলকে আবদ্ধ হবে। <br> <br> \n► কমা বোতামে দীর্ঘ চাপ দিলে ক্লিপবোর্ড দর্শন, ইমোজি দর্শন, একক হাত মোড, সেটিংস অথবা ভাষা পরিবর্তন বোতাম উপলব্ধ হবে:<br> \n\t• সংশ্লিষ্ট বোতাম সক্ষম থাকলে ইমোজি দর্শন বা ভাষা পরিবর্তন বোতাম দৃশ্যমান থাকবে না; <br> \n\t• কিছু লেআউটের জন্য কমা বোতাম নয়, কিন্তু একই জায়গার বোতামটি, যেমন: Dvorak লেআউটে \'q\' বোতাম। <br> <br> \n► ছদ্মবেশী মোড সক্রিয় থাকলে শব্দ শিখন বন্ধ থাকবে, এবং সাম্প্রতিকে কোনো ইমোজি যুক্ত হবে না। <br> <br> \n► ছদ্মবেশী বোতামে চাপ দিলে টুলবার উপলব্ধ হবে। <br> <br> \n► স্লাইডিং কি ইনপুট: বড়ো হাতের অক্ষর লেখার জন্য শিফট বোতাম থেকে অন্য বোতামে অভিস্পর্শ করুন: <br> \n\t• সিম্বল কিবোর্ড থেকে সিম্বল এবং সম্পর্কিত অক্ষর লেখার জন্য \'?123\' বোতামেও এটি কাজ করে।<br> <br> \n► শিফট অথবা প্রতীক বোতাম চেপে ধরে এক বা একাধিক বোতাম চাপার পর শিফট অথবা প্রতীক বোতাম ছেড়ে দিলে তা পূর্ববর্তী কিবোর্ডে নিয়ে যাবে। <br> <br> \n► পরামর্শ ফলকে কোনো পরামর্শে দীর্ঘ চাপ দিলে আরও পরামর্শ, বিলোপ করার জন্য অপসারণ বোতাম প্রদর্শিত হবে। <br> <br> \n► আরও পরামর্শ খুলতে কোনো পরামর্শ থেকে উপরে অভিস্পর্শ করুন এবং নির্দিষ্ট পরামর্শ নির্বাচনের জন্য ছেড়ে দিন। <br> <br> \n► ক্লিপবোর্ড ইতিহাসে কোনো অন্তর্ভুক্তিতে দীর্ঘ চাপ দিয়ে তা পিন করা যাবে (আনপিন না করা পর্যন্ত তা ক্লিপবোর্ডে থাকবে)<br> <br> \n► ক্লিপবোর্ড দর্শনে বাম পাশে অভিস্পর্শ করলে অন্তর্ভুক্তি অপসারণ হবে (যদি সেটি পিন করা না থাকে)। <br> <br> \n► লেখা সিলেক্ট করে শিফট চাপলে তা বড়ো হাতের অক্ষর, ছোটো হাতের অক্ষর এবং সব অক্ষর বড়ো হাতের হবে। <br> <br> \n► আপনি ফাইল এক্সপ্লোরারে খোলার মাধ্যমে অভিধান সংযুক্ত করতে পারেন: <br> \n\t• এটি কেবল <i>content-uris</i> এর সাথে কাজ করে, <i>file-uris</i> এর সাথে নয়। অর্থাৎ কিছু ফাইল এক্সপ্লোরারের সাথে নাও কাজ করতে পারে। <br> <br> \n► রুট উপলব্ধতাসহ ম্যানুয়াল ব্যাকআপ করা ব্যবহারকারীদের জন্য: <br> \nঅ্যান্ড্রয়েড ৭ থেকে শেয়ারড্ প্রেফারেন্স ফাইল ডিফল্ট জায়গা নয়। কারণ অ্যাপ %s ব্যবহার করছে। ডিভাইস আনলকের আগে সেটিংস খোলার জন্য এটি প্রয়োজনীয়, যেমন: বুট করার সময়। <br> \nফাইলটি /data/user_de/0/package_id/shared_prefs/ থাকে। যদিও এটা ডিভাইস এবং অ্যান্ড্রয়েড সংস্করণের উপরে নির্ভর করে। <br> <br> \n<i><b>কেবল ডিবাগ এপিকে</b></i> <br> <br> \n► পরামর্শে দীর্ঘ চাপ দিলে তার উৎস-অভিধান দেখাবে। <br> <br> \n► ডিবাগ সেটিংস উচ্চতর সেটিংসে উপলব্ধ। যদিও লগ ফাইলে অভিধান অবমুক্ত করা ছাড়া এর ব্যবহারযোগ্যতা সীমিত। <br> \n\t• রিলিজ এপিকে এর জন্য <i>সম্পর্কে</i> এর সংস্করণে কয়েকবার চাপ দিতে হবে। তাহলে <i>উচ্চতর</i>-এ ডিবাগ সেটিংস উপলব্ধ হবে। <br> \n\t• <i>পরামর্শ তথ্য প্রদর্শন</i> চালু করলে পরামর্শের উপরে কিছু ছোটো নম্বর থাকবে যা অভ্যন্তরীণ হিসাব ও উৎস অভিধান নির্দেশ করে। <br> <br> \n► অ্যাপ্লিকেশন ক্র্যাশের পরে সেটিংস খুললে আপনি ক্র্যাশ লগ চান কি না তা জিজ্ঞেস করা হবে৷ <br> <br> \n► বহুভাষী টাইপিং ব্যবহারের সময় স্পেসবার বর্তমান ভাষা নির্ধারণের জন্য দৃঢ়তা মান দেখাবে। <br> <br> \n► পরামর্শের উপরে কিছু ছোটো নম্বর থাকবে যা অভ্যন্তরীণ হিসাব ও উৎস অভিধান নির্দেশ করে (নিষ্ক্রিয় করা যেতে পারে)। + ► বাংলা চন্দ্রবিন্দু (ঁ) বোতামে দীর্ঘ চাপে সচরাচর ব্যবহৃত হয় না এমন বর্ণ যেমন: ৠ, ৡ, ৺, ৰ, ৱ ইত্যাদি টাইপ করা যাবে। \'র\' এবং \'ত\' বোতামে দীর্ঘ চাপে যথাক্রমে \'র‍্য\' এবং \'ৎ\' টাইপ করা যাবে। এছাড়াও দাঁড়ি (।) বোতামে দীর্ঘ চাপে <i>ZWNJ</i> এবং <i>ZWJ</i> টাইপ করা যাবে। <br> <br>► পিন করে রাখা টুলবার বোতামে দীর্ঘ চাপ দিলে তা অতিরিক্ত কাজ করবে: <br> \n\t• ক্লিপবোর্ড &#65515; পেস্ট <br> \n\t• বামে/ডানে স্থানান্তর &#65515; সম্পূর্ণ বামে/ডানে স্থানান্তর <br> \n\t• উপরে/নিচে স্থানান্তর &#65515; পৃষ্ঠার উপরে/নিচে <br> \n\t• শব্দের ডানে/বামে &#65515; লাইনের শুরু/শেষ <br> \n\t• পাতার উপরে/নিচে &#65515; পাতার শুরু/শেষ <br> \n\t• অনুলিপি &#65515; কর্তন <br> \n\t• শব্দ নির্বাচন &#8596; সমস্ত নির্বাচন <br> \n\t• পূর্বাবস্থা &#8596; পুনরাবৃত্তি <br> <br> \n► পরামর্শ ফলকের টুলবারের কোনো বোতামে দীর্ঘ চাপ দিলে তা পরামর্শ ফলকে আবদ্ধ হবে। <br> <br> \n► কমা বোতামে দীর্ঘ চাপ দিলে ক্লিপবোর্ড দর্শন, ইমোজি দর্শন, একক হাত মোড, সেটিংস অথবা ভাষা পরিবর্তন বোতাম উপলব্ধ হবে:<br> \n\t• সংশ্লিষ্ট বোতাম সক্ষম থাকলে ইমোজি দর্শন বা ভাষা পরিবর্তন বোতাম দৃশ্যমান থাকবে না; <br> \n\t• কিছু লেআউটের জন্য কমা বোতাম নয়, কিন্তু একই জায়গার বোতামটি, যেমন: Dvorak লেআউটে \'q\' বোতাম। <br> <br> \n► ছদ্মবেশী মোড সক্রিয় থাকলে শব্দ শিখন বন্ধ থাকবে, এবং সাম্প্রতিকে কোনো ইমোজি যুক্ত হবে না। <br> <br> \n► ছদ্মবেশী বোতামে চাপ দিলে টুলবার উপলব্ধ হবে। <br> <br> \n► স্লাইডিং কি ইনপুট: বড়ো হাতের অক্ষর লেখার জন্য শিফট বোতাম থেকে অন্য বোতামে অভিস্পর্শ করুন: <br> \n\t• সিম্বল কিবোর্ড থেকে সিম্বল এবং সম্পর্কিত অক্ষর লেখার জন্য \'?123\' বোতামেও এটি কাজ করে।<br> <br> \n► শিফট অথবা প্রতীক বোতাম চেপে ধরে এক বা একাধিক বোতাম চাপার পর শিফট অথবা প্রতীক বোতাম ছেড়ে দিলে তা পূর্ববর্তী কিবোর্ডে নিয়ে যাবে। <br> <br> \n► পরামর্শ ফলকে কোনো পরামর্শে দীর্ঘ চাপ দিলে আরও পরামর্শ, বিলোপ করার জন্য অপসারণ বোতাম প্রদর্শিত হবে। <br> <br> \n► আরও পরামর্শ খুলতে কোনো পরামর্শ থেকে উপরে অভিস্পর্শ করুন এবং নির্দিষ্ট পরামর্শ নির্বাচনের জন্য ছেড়ে দিন। <br> <br> \n► ক্লিপবোর্ড ইতিহাসে কোনো অন্তর্ভুক্তিতে দীর্ঘ চাপ দিয়ে তা পিন করা যাবে (আনপিন না করা পর্যন্ত তা ক্লিপবোর্ডে থাকবে)<br> <br> \n► ক্লিপবোর্ড দর্শনে বাম পাশে অভিস্পর্শ করলে অন্তর্ভুক্তি অপসারণ হবে (যদি সেটি পিন করা না থাকে)। <br> <br> \n► সাম্প্রতিক ইমোজি বিভাগে কোনো ইমোজির ওপর দীর্ঘ চাপ দিলে সেটি অপসারণের জন্য একটি বিলোপ বোতাম দেখাবে। আবার সাম্প্রতিক ইমোজি বিভাগের ট্যাবে দীর্ঘ চাপ দিলে সব সাম্প্রতিক ইমোজি মুছে যাবে। <br> <br> \n► লেখা সিলেক্ট করে শিফট চাপলে তা বড়ো হাতের অক্ষর, ছোটো হাতের অক্ষর এবং সব অক্ষর বড়ো হাতের হবে। <br> <br> \n► আপনি ফাইল এক্সপ্লোরারে খোলার মাধ্যমে অভিধান সংযুক্ত করতে পারেন: <br> \n\t• এটি কেবল <i>content-uris</i> এর সাথে কাজ করে, <i>file-uris</i> এর সাথে নয়। অর্থাৎ কিছু ফাইল এক্সপ্লোরারের সাথে নাও কাজ করতে পারে। <br> <br> \n► রুট উপলব্ধতাসহ ম্যানুয়াল ব্যাকআপ করা ব্যবহারকারীদের জন্য: <br> \nঅ্যান্ড্রয়েড ৭ থেকে শেয়ারড্ প্রেফারেন্স ফাইল ডিফল্ট জায়গা নয়। কারণ অ্যাপ %s ব্যবহার করছে। ডিভাইস আনলকের আগে সেটিংস খোলার জন্য এটি প্রয়োজনীয়, যেমন: বুট করার সময়। <br> \nফাইলটি /data/user_de/0/package_id/shared_prefs/ থাকে। যদিও এটা ডিভাইস এবং অ্যান্ড্রয়েড সংস্করণের উপরে নির্ভর করে। <br> <br> \n<i><b>কেবল ডিবাগ এপিকে</b></i> <br> <br> \n► পরামর্শে দীর্ঘ চাপ দিলে তার উৎস-অভিধান দেখাবে। <br> <br> \n► ডিবাগ সেটিংস উচ্চতর সেটিংসে উপলব্ধ। যদিও লগ ফাইলে অভিধান অবমুক্ত করা ছাড়া এর ব্যবহারযোগ্যতা সীমিত। <br> \n\t• রিলিজ এপিকে এর জন্য <i>সম্পর্কে</i> এর সংস্করণে কয়েকবার চাপ দিতে হবে। তাহলে <i>উচ্চতর</i>-এ ডিবাগ সেটিংস উপলব্ধ হবে। <br> \n\t• <i>পরামর্শ তথ্য প্রদর্শন</i> চালু করলে পরামর্শের উপরে কিছু ছোটো নম্বর থাকবে যা অভ্যন্তরীণ হিসাব ও উৎস অভিধান নির্দেশ করে। <br> <br> \n► অ্যাপ্লিকেশন ক্র্যাশের পরে সেটিংস খুললে আপনি ক্র্যাশ লগ চান কি না তা জিজ্ঞেস করা হবে৷ <br> <br> \n► বহুভাষী টাইপিং ব্যবহারের সময় স্পেসবার বর্তমান ভাষা নির্ধারণের জন্য দৃঢ়তা মান দেখাবে। <br> <br> \n► পরামর্শের উপরে কিছু ছোটো নম্বর থাকবে যা অভ্যন্তরীণ হিসাব ও উৎস অভিধান নির্দেশ করে (নিষ্ক্রিয় করা যেতে পারে)। সার্বিয়ান (ল্যাটিন) %s (প্রথাগত) %s (অক্ষর) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dc29125207..31968d6e9b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -842,6 +842,7 @@ New dictionary: \n► Swipe up from a suggestion to open more suggestions, and release on the suggestion to select it. <br> <br> \n► Long-press an entry in the clipboard history to pin it (keep it in clipboard until you unpin). <br> <br> \n► Swipe left in clipboard view to remove an entry (except when it\\\'s pinned) <br> <br> +\n► Long-press an emoji in the recent category to show a delete button that removes it, or long-press the recent category tab to clear all recent emojis. <br> <br> \n► Select text and press shift to switch between uppercase, lowercase and capitalize words. <br> <br> \n► You can add dictionaries by opening them in a file explorer: <br> \n\t• This only works with <i>content-uris</i> and not with <i>file-uris</i>, meaning that it may not work with some file explorers. <br> <br> From 128c19fceb6df58087c357ca9bb4a6019ee8264b Mon Sep 17 00:00:00 2001 From: "Md. Rifat Hasan Jihan" <31377578+RHJihan@users.noreply.github.com> Date: Mon, 20 Jul 2026 13:03:00 +0600 Subject: [PATCH 2/5] Update strings.xml to update hidden_features_message --- app/src/main/res/values/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 06f57fad04..1bb4df0308 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -845,6 +845,7 @@ New dictionary: \n► Swipe up from a suggestion to open more suggestions, and release on the suggestion to select it. <br> <br> \n► Long-press an entry in the clipboard history to pin it (keep it in clipboard until you unpin). <br> <br> \n► Swipe left in clipboard view to remove an entry (except when it\'s pinned) <br> <br> +\n► Long-press an emoji in the recent category to show a delete button that removes it, or long-press the recent category tab to clear all recent emojis. <br> <br> \n► Select text and press shift to switch between uppercase, lowercase and capitalize words. <br> <br> \n► You can add dictionaries by opening them in a file explorer: <br> \n\t• This only works with <i>content-uris</i> and not with <i>file-uris</i>, meaning that it may not work with some file explorers. <br> <br> From 6358557ede31db39c2a7140f95380a672a06e5b2 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 3/5] Confine emoji recents popup-column workaround to the remove-key path --- .../java/helium314/keyboard/keyboard/Key.java | 25 +++++++++++++++---- .../keyboard/emoji/DynamicGridKeyboard.java | 5 ++++ .../keyboard/emoji/EmojiPageKeyboardView.java | 5 +++- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/helium314/keyboard/keyboard/Key.java b/app/src/main/java/helium314/keyboard/keyboard/Key.java index 51046f2034..7a1e4733a3 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/Key.java +++ b/app/src/main/java/helium314/keyboard/keyboard/Key.java @@ -234,6 +234,24 @@ public Key(@Nullable final String label, @Nullable final String iconName, final */ protected Key(@NonNull final Key key, @Nullable final PopupKeySpec[] popupKeys, @Nullable final String labelHint, final int backgroundType) { + this(key, popupKeys, labelHint, backgroundType, key.getPopupKeysColumnNumber()); + } + + /** + * Copy constructor that overrides the number of popup key columns. + * + *

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 keyboards, final int code) { for (final DynamicGridKeyboard keyboard : keyboards) { diff --git a/app/src/main/java/helium314/keyboard/keyboard/emoji/RecentEmojis.kt b/app/src/main/java/helium314/keyboard/keyboard/emoji/RecentEmojis.kt index 7e1af2488d..37b67c56e4 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/emoji/RecentEmojis.kt +++ b/app/src/main/java/helium314/keyboard/keyboard/emoji/RecentEmojis.kt @@ -33,6 +33,23 @@ object RecentEmojis { if (emoji > 0) add(StringUtils.newSingleCodePointString(emoji)) } + @JvmStatic + fun remove(emoji: String) { + if (emoji.isEmpty()) return + val recents = get() + if (recents.removeAll { it == emoji }) set(recents) + } + + @JvmStatic + fun removeCodepoint(emoji: Int) { + if (emoji > 0) remove(StringUtils.newSingleCodePointString(emoji)) + } + + @JvmStatic + fun clear() { + set(emptyList()) + } + fun get(): MutableList { val pref = prefs.getString(Settings.PREF_RECENT_EMOJIS, Defaults.PREF_RECENT_EMOJIS) if (pref.isNullOrEmpty()) return mutableListOf() From a5bb900e3db9f2c4cc7e2d3d96682a4ef82c7075 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 5/5] Group EMOJI_RECENT_REMOVE with the other heliboard-only codes --- .../keyboard/internal/keyboard_parser/floris/KeyCode.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 c3da8ca785..b9bb03819d 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,8 +106,6 @@ 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 @@ -129,6 +127,7 @@ object KeyCode { const val CJK_SPACE = 12288 // heliboard only codes + const val EMOJI_RECENT_REMOVE = -10000 // remove a single emoji from the recents category const val SYMBOL_ALPHA = -10001 const val TOGGLE_ONE_HANDED_MODE = -10002 const val TOGGLE_ONE_HANDED_MODE_2 = -10003 // does the same as TOGGLE_ONE_HANDED_MODE (used to be start & stop)