Skip to content
21 changes: 20 additions & 1 deletion app/src/main/java/helium314/keyboard/keyboard/Key.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>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.</p>
*
* @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;
Expand All @@ -249,7 +267,8 @@ protected Key(@NonNull final Key key, @Nullable final PopupKeySpec[] popupKeys,
mY = key.mY;
mHitBox.set(key.mHitBox);
mPopupKeys = popupKeys;
mPopupKeysColumnAndFlags = key.mPopupKeysColumnAndFlags;
mPopupKeysColumnAndFlags = (key.mPopupKeysColumnAndFlags & ~POPUP_KEYS_COLUMN_NUMBER_MASK)
| (popupKeysColumnNumber & POPUP_KEYS_COLUMN_NUMBER_MASK);
mBackgroundType = backgroundType;
mActionFlags = key.mActionFlags;
mKeyVisualAttributes = key.mKeyVisualAttributes;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,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) {
removeRecentKey(usedKey);
}
}

public void clearRecentKeys() {
removeAllKeys();
if (mIsRecents) {
RecentEmojis.clear();
}
}

private void addKey(final Key usedKey, final boolean addFirst) {
if (usedKey == null) {
return;
Expand Down Expand Up @@ -191,18 +220,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++;
}
}

Expand All @@ -213,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<DynamicGridKeyboard> keyboards,
final int code) {
for (final DynamicGridKeyboard keyboard : keyboards) {
Expand Down Expand Up @@ -304,6 +344,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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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;
}
Expand Down Expand Up @@ -200,6 +211,51 @@ 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) {
// 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.
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();
Expand Down Expand Up @@ -298,7 +354,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;
Expand All @@ -314,7 +376,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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
val pref = prefs.getString(Settings.PREF_RECENT_EMOJIS, Defaults.PREF_RECENT_EMOJIS)
if (pref.isNullOrEmpty()) return mutableListOf()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,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)
Expand Down Expand Up @@ -211,7 +212,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, DPAD,
BACKGROUND_GATHERING, BACKGROUND_GATHERING_TEMP_OFF, DPAD, EMOJI_RECENT_REMOVE
-> this

KEY_REPEAT if (longPress) -> this
Expand Down
Loading