diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 5508a883c4..9e60fcde3c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -84,6 +84,10 @@ SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
+
+
diff --git a/app/src/main/assets/layouts/emoji_bottom/emoji_bottom_row.json b/app/src/main/assets/layouts/emoji_bottom/emoji_bottom_row.json
index 2a7c8f6b17..cde4e61b4c 100644
--- a/app/src/main/assets/layouts/emoji_bottom/emoji_bottom_row.json
+++ b/app/src/main/assets/layouts/emoji_bottom/emoji_bottom_row.json
@@ -1,6 +1,7 @@
[
[
{ "label": "alpha", "width": 0.15 },
+ { "$": "keyboard_state_selector", "emojiSearchAvailable": { "label": "emoji_search", "width": 0.15 }},
{ "label": "space", "width": -1 },
{ "label": "delete", "width": 0.15 }
]
diff --git a/app/src/main/assets/layouts/emoji_bottom/emoji_bottom_row_with_action.json b/app/src/main/assets/layouts/emoji_bottom/emoji_bottom_row_with_action.json
index 4827b365d4..831a5b84cd 100644
--- a/app/src/main/assets/layouts/emoji_bottom/emoji_bottom_row_with_action.json
+++ b/app/src/main/assets/layouts/emoji_bottom/emoji_bottom_row_with_action.json
@@ -1,6 +1,7 @@
[
[
{ "label": "alpha", "width": 0.15 },
+ { "$": "keyboard_state_selector", "emojiSearchAvailable": { "label": "emoji_search", "width": 0.15 }},
{ "label": "space", "width": -1 },
{ "label": "delete", "width": 0.15 },
{ "label": "action", "width": 0.15 }
diff --git a/app/src/main/java/helium314/keyboard/accessibility/KeyCodeDescriptionMapper.kt b/app/src/main/java/helium314/keyboard/accessibility/KeyCodeDescriptionMapper.kt
index cd1841ce67..64311e9313 100644
--- a/app/src/main/java/helium314/keyboard/accessibility/KeyCodeDescriptionMapper.kt
+++ b/app/src/main/java/helium314/keyboard/accessibility/KeyCodeDescriptionMapper.kt
@@ -35,6 +35,7 @@ internal class KeyCodeDescriptionMapper private constructor() {
put(KeyCode.ACTION_NEXT, R.string.spoken_description_action_next)
put(KeyCode.ACTION_PREVIOUS, R.string.spoken_description_action_previous)
put(KeyCode.EMOJI, R.string.spoken_description_emoji)
+ put(KeyCode.EMOJI_SEARCH, R.string.spoken_description_search)
// Because the upper-case and lower-case mappings of the following letters is depending on
// the locale, the upper case descriptions should be defined here. The lower case
// descriptions are handled in {@link #getSpokenLetterDescriptionId(Context,int)}.
diff --git a/app/src/main/java/helium314/keyboard/keyboard/Key.java b/app/src/main/java/helium314/keyboard/keyboard/Key.java
index c5de708515..2585d50418 100644
--- a/app/src/main/java/helium314/keyboard/keyboard/Key.java
+++ b/app/src/main/java/helium314/keyboard/keyboard/Key.java
@@ -1167,7 +1167,8 @@ public KeyParams(
// fallthrough
case KeyCode.SHIFT, Constants.CODE_ENTER, KeyCode.SHIFT_ENTER, KeyCode.ALPHA, Constants.CODE_SPACE, KeyCode.NUMPAD,
KeyCode.SYMBOL, KeyCode.SYMBOL_ALPHA, KeyCode.LANGUAGE_SWITCH, KeyCode.EMOJI, KeyCode.CLIPBOARD,
- KeyCode.MOVE_START_OF_LINE, KeyCode.MOVE_END_OF_LINE, KeyCode.MOVE_START_OF_PAGE, KeyCode.MOVE_END_OF_PAGE:
+ KeyCode.MOVE_START_OF_LINE, KeyCode.MOVE_END_OF_LINE, KeyCode.MOVE_START_OF_PAGE, KeyCode.MOVE_END_OF_PAGE,
+ KeyCode.EMOJI_SEARCH:
actionFlags |= ACTION_FLAGS_NO_KEY_PREVIEW; // no preview even if icon!
}
if (mCode == KeyCode.SETTINGS || mCode == KeyCode.LANGUAGE_SWITCH)
diff --git a/app/src/main/java/helium314/keyboard/keyboard/KeyboardId.java b/app/src/main/java/helium314/keyboard/keyboard/KeyboardId.java
index 3c5de6925e..dfce3b1125 100644
--- a/app/src/main/java/helium314/keyboard/keyboard/KeyboardId.java
+++ b/app/src/main/java/helium314/keyboard/keyboard/KeyboardId.java
@@ -62,7 +62,7 @@ public final class KeyboardId {
public static final int ELEMENT_EMOJI_CATEGORY13 = 23;
public static final int ELEMENT_EMOJI_CATEGORY14 = 24;
public static final int ELEMENT_EMOJI_CATEGORY15 = 25;
- public static final int ELEMENT_EMOJI_CATEGORY16 = 26;
+ public static final int ELEMENT_EMOJI_CATEGORY16 = 26; // Emoji search
public static final int ELEMENT_CLIPBOARD = 27;
public static final int ELEMENT_NUMPAD = 28;
public static final int ELEMENT_EMOJI_BOTTOM_ROW = 29;
@@ -84,6 +84,7 @@ public final class KeyboardId {
public final boolean mIsSplitLayout;
public final boolean mOneHandedModeEnabled;
public final KeyboardLayoutSet.InternalAction mInternalAction;
+ public final boolean mEmojiSearchAvailable;
private final int mHashCode;
@@ -105,6 +106,7 @@ public KeyboardId(final int elementId, final KeyboardLayoutSet.Params params) {
mIsSplitLayout = params.mIsSplitLayoutEnabled;
mOneHandedModeEnabled = params.mOneHandedModeEnabled;
mInternalAction = params.mInternalAction;
+ mEmojiSearchAvailable = params.mEmojiSearchAvailable;
mHashCode = computeHashCode(this);
}
@@ -229,7 +231,7 @@ public int hashCode() {
@Override
public String toString() {
- return String.format(Locale.ROOT, "[%s %s:%s %dx%d %s %s%s%s%s%s%s%s%s%s%s%s]",
+ return String.format(Locale.ROOT, "[%s %s:%s %dx%d %s %s%s%s%s%s%s%s%s%s%s%s%s%s]",
elementIdToName(mElementId),
mSubtype.getLocale(),
mSubtype.getExtraValueOf(KEYBOARD_LAYOUT_SET),
@@ -245,7 +247,9 @@ public String toString() {
(mLanguageSwitchKeyEnabled ? " languageSwitchKeyEnabled" : ""),
(mEmojiKeyEnabled ? " emojiKeyEnabled" : ""),
(isMultiLine() ? " isMultiLine" : ""),
- (mIsSplitLayout ? " isSplitLayout" : "")
+ (mIsSplitLayout ? " isSplitLayout" : ""),
+ (mInternalAction != null ? " internalAction=" + mInternalAction : ""),
+ (mEmojiSearchAvailable ? " emojiSearchAvailable" : "")
);
}
diff --git a/app/src/main/java/helium314/keyboard/keyboard/KeyboardLayoutSet.java b/app/src/main/java/helium314/keyboard/keyboard/KeyboardLayoutSet.java
index 5c293b4e6e..2c7ba8a771 100644
--- a/app/src/main/java/helium314/keyboard/keyboard/KeyboardLayoutSet.java
+++ b/app/src/main/java/helium314/keyboard/keyboard/KeyboardLayoutSet.java
@@ -21,6 +21,7 @@
import helium314.keyboard.latin.RichInputMethodManager;
import helium314.keyboard.latin.RichInputMethodSubtype;
import helium314.keyboard.latin.settings.Settings;
+import helium314.keyboard.latin.utils.DictionaryInfoUtils;
import helium314.keyboard.latin.utils.InputTypeUtils;
import helium314.keyboard.latin.utils.Log;
import helium314.keyboard.latin.utils.ResourceUtils;
@@ -99,6 +100,7 @@ public static final class Params {
// and the required ProductionFlags are enabled.
boolean mIsSplitLayoutEnabled;
InternalAction mInternalAction;
+ boolean mEmojiSearchAvailable;
}
public static void onSystemLocaleChanged() {
@@ -221,6 +223,7 @@ public Builder(final Context context, @Nullable final EditorInfo ei) {
public static KeyboardLayoutSet buildEmojiClipBottomRow(final Context context, @Nullable final EditorInfo ei) {
final Builder builder = new Builder(context, ei);
builder.mParams.mMode = KeyboardId.MODE_TEXT;
+ builder.mParams.mEmojiSearchAvailable = ! DictionaryInfoUtils.getLocalesWithEmojiDicts(context).isEmpty();
final int width = ResourceUtils.getKeyboardWidth(context, Settings.getValues());
// actually the keyboard does not have full height, but at this point we use it to get correct key heights
final int height = ResourceUtils.getKeyboardHeight(context.getResources(), Settings.getValues());
diff --git a/app/src/main/java/helium314/keyboard/keyboard/KeyboardSwitcher.java b/app/src/main/java/helium314/keyboard/keyboard/KeyboardSwitcher.java
index eb29a0c6c6..58a9050311 100644
--- a/app/src/main/java/helium314/keyboard/keyboard/KeyboardSwitcher.java
+++ b/app/src/main/java/helium314/keyboard/keyboard/KeyboardSwitcher.java
@@ -114,6 +114,9 @@ public void updateKeyboardTheme(@NonNull Context displayContext) {
settings.loadSettings(displayContext, settings.getCurrent().mLocale, settings.getCurrent().mInputAttributes);
if (mKeyboardView != null)
mLatinIME.setInputView(onCreateInputView(displayContext, mIsHardwareAcceleratedDrawingEnabled));
+ } else if (mCurrentInputView != null && mLatinIME.hasSuggestionStripView()
+ == (Settings.getValues().mToolbarMode == ToolbarMode.HIDDEN || mLatinIME.isEmojiSearch())) {
+ mLatinIME.updateSuggestionStripView(mCurrentInputView);
}
}
@@ -317,7 +320,7 @@ private void setMainKeyboardFrame(
@NonNull final SettingsValues settingsValues,
@NonNull final KeyboardSwitchState toggleState) {
final int visibility = isImeSuppressedByHardwareKeyboard(settingsValues, toggleState) ? View.GONE : View.VISIBLE;
- final int stripVisibility = settingsValues.mToolbarMode == ToolbarMode.HIDDEN ? View.GONE : View.VISIBLE;
+ final int stripVisibility = mLatinIME.hasSuggestionStripView()? View.VISIBLE : View.GONE;
mStripContainer.setVisibility(stripVisibility);
PointerTracker.switchTo(mKeyboardView);
mKeyboardView.setVisibility(visibility);
@@ -639,6 +642,10 @@ public boolean isShowingStripContainer() {
return mStripContainer.isShown();
}
+ public EmojiPalettesView getEmojiPalettesView() {
+ return mEmojiPalettesView;
+ }
+
public View getVisibleKeyboardView() {
if (isShowingEmojiPalettes()) {
return mEmojiPalettesView;
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 eea566a720..4ec57359f9 100644
--- a/app/src/main/java/helium314/keyboard/keyboard/emoji/DynamicGridKeyboard.java
+++ b/app/src/main/java/helium314/keyboard/keyboard/emoji/DynamicGridKeyboard.java
@@ -43,6 +43,7 @@ final class DynamicGridKeyboard extends Keyboard {
private final int mVerticalStep;
private final int mColumnsNum;
private final int mMaxKeyCount;
+ private final boolean mFixedRowCount;
private final boolean mIsRecents;
private final ArrayDeque mGridKeys = new ArrayDeque<>();
private final ArrayDeque mPendingKeys = new ArrayDeque<>();
@@ -50,8 +51,18 @@ final class DynamicGridKeyboard extends Keyboard {
private List mCachedGridKeys;
private final ArrayList mEmptyColumnIndices = new ArrayList<>(4);
- public DynamicGridKeyboard(final SharedPreferences prefs, final Keyboard templateKeyboard,
+ public static DynamicGridKeyboard ofKeyCount(final SharedPreferences prefs, final Keyboard templateKeyboard,
final int maxKeyCount, final int categoryId, final int width) {
+ return new DynamicGridKeyboard(prefs, templateKeyboard, maxKeyCount, categoryId, width, false);
+ }
+
+ public static DynamicGridKeyboard ofRowCount(final SharedPreferences prefs, final Keyboard templateKeyboard,
+ final int maxRowCount, final int categoryId, final int width) {
+ return new DynamicGridKeyboard(prefs, templateKeyboard, maxRowCount, categoryId, width, true);
+ }
+
+ private DynamicGridKeyboard(final SharedPreferences prefs, final Keyboard templateKeyboard,
+ final int maxCount, final int categoryId, final int width, boolean fixedRowCount) {
super(templateKeyboard);
// todo: would be better to keep them final and not require width, but how to properly set width of the template keyboard?
// an alternative would be to always create the templateKeyboard with full width
@@ -69,7 +80,8 @@ public DynamicGridKeyboard(final SharedPreferences prefs, final Keyboard templat
mColumnsNum = mBaseWidth / mHorizontalStep;
if (spacerWidth > 0)
setSpacerColumns(spacerWidth);
- mMaxKeyCount = maxKeyCount;
+ mMaxKeyCount = fixedRowCount? maxCount * getOccupiedColumnCount() : maxCount;
+ mFixedRowCount = fixedRowCount;
mIsRecents = categoryId == EmojiCategory.ID_RECENTS;
mPrefs = prefs;
}
@@ -111,8 +123,10 @@ private Key getTemplateKey(final int code) {
throw new RuntimeException("Can't find template key: code=" + code);
}
- public int getDynamicOccupiedHeight() {
- final int row = (mGridKeys.size() - 1) / getOccupiedColumnCount() + 1;
+ // height is dynamic if we don't have a fixed row count
+ int getOccupiedHeight() {
+ final int count = mFixedRowCount ? mMaxKeyCount : mGridKeys.size();
+ final int row = (count - 1) / getOccupiedColumnCount() + 1;
return row * mVerticalStep;
}
@@ -146,6 +160,13 @@ public void addKeyLast(final Key usedKey) {
addKey(usedKey, false);
}
+ public void removeAllKeys() {
+ synchronized (mLock) {
+ mGridKeys.clear();
+ mCachedGridKeys = null;
+ }
+ }
+
private void addKey(final Key usedKey, final boolean addFirst) {
if (usedKey == null) {
return;
diff --git a/app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiCategory.java b/app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiCategory.java
index 413a6b8f62..06ed06b93b 100644
--- a/app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiCategory.java
+++ b/app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiCategory.java
@@ -292,7 +292,7 @@ public DynamicGridKeyboard getKeyboard(final int categoryId, final int id) {
final int currentWidth = ResourceUtils.getKeyboardWidth(mContext, Settings.getValues());
if (categoryId == EmojiCategory.ID_RECENTS) {
- final DynamicGridKeyboard kbd = new DynamicGridKeyboard(mPrefs,
+ final DynamicGridKeyboard kbd = DynamicGridKeyboard.ofKeyCount(mPrefs,
mLayoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS),
mMaxRecentsKeyCount, categoryId, currentWidth);
mCategoryKeyboardMap.put(categoryKeyboardMapKey, kbd);
@@ -305,7 +305,7 @@ public DynamicGridKeyboard getKeyboard(final int categoryId, final int id) {
final Key[][] sortedKeysPages = sortKeysGrouped(
keyboard.getSortedKeys(), keyCountPerPage);
for (int pageId = 0; pageId < sortedKeysPages.length; ++pageId) {
- final DynamicGridKeyboard tempKeyboard = new DynamicGridKeyboard(mPrefs,
+ final DynamicGridKeyboard tempKeyboard = DynamicGridKeyboard.ofKeyCount(mPrefs,
mLayoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS),
keyCountPerPage, categoryId, currentWidth);
for (final Key emojiKey : sortedKeysPages[pageId]) {
@@ -321,7 +321,7 @@ public DynamicGridKeyboard getKeyboard(final int categoryId, final int id) {
}
private int computeMaxKeyCountPerPage() {
- final DynamicGridKeyboard tempKeyboard = new DynamicGridKeyboard(mPrefs,
+ final DynamicGridKeyboard tempKeyboard = DynamicGridKeyboard.ofKeyCount(mPrefs,
mLayoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS),
0, 0, ResourceUtils.getKeyboardWidth(mContext, Settings.getValues()));
return MAX_LINE_COUNT_PER_PAGE * tempKeyboard.getOccupiedColumnCount();
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 66cfe0d795..6142d41cb2 100644
--- a/app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiPageKeyboardView.java
+++ b/app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiPageKeyboardView.java
@@ -113,6 +113,7 @@ public EmojiPageKeyboardView(final Context context, final AttributeSet attrs,
mPopupKeysKeyboardContainer = inflater.inflate(popupKeysKeyboardLayoutId, null);
mDescriptionView = mPopupKeysKeyboardContainer.findViewById(R.id.description_view);
mPopupKeysKeyboardView = mPopupKeysKeyboardContainer.findViewById(R.id.popup_keys_keyboard_view);
+ setFitsSystemWindows(false);
}
@Override
@@ -120,7 +121,7 @@ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final Keyboard keyboard = getKeyboard();
if (keyboard instanceof DynamicGridKeyboard) {
final int width = keyboard.mOccupiedWidth + getPaddingLeft() + getPaddingRight();
- final int occupiedHeight = ((DynamicGridKeyboard) keyboard).getDynamicOccupiedHeight();
+ final int occupiedHeight = ((DynamicGridKeyboard) keyboard).getOccupiedHeight();
final int height = occupiedHeight + getPaddingTop() + getPaddingBottom();
setMeasuredDimension(width, height);
return;
@@ -437,23 +438,25 @@ public boolean onMove(final MotionEvent e) {
final int x = (int)e.getX();
final int y = (int)e.getY();
final Key key = getKey(x, y);
- final boolean isShowingPopupKeysPanel = isShowingPopupKeysPanel();
+ final boolean isShowingPopupKeyboard = isShowingPopupKeysPanel() && mPopupKeysKeyboardView.getVisibility() == VISIBLE;
// Touched key has changed, release previous key's callbacks and
// re-register them for the new key.
- if (key != mCurrentKey && !isShowingPopupKeysPanel) {
+ if (key != mCurrentKey && !isShowingPopupKeyboard) {
releaseCurrentKey(false);
mCurrentKey = key;
+ cancelLongPress();
+ if (isShowingPopupKeysPanel()) {
+ onCancelPopupKeysPanel();
+ }
if (key == null) {
return false;
}
registerPress(key);
-
- cancelLongPress();
registerLongPress(key);
}
- if (isShowingPopupKeysPanel) {
+ if (isShowingPopupKeyboard) {
final long eventTime = e.getEventTime();
final int translatedX = mPopupKeysPanel.translateX(x);
final int translatedY = mPopupKeysPanel.translateY(y);
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 534483c984..67d29c0733 100644
--- a/app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiPalettesView.java
+++ b/app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiPalettesView.java
@@ -38,6 +38,7 @@
import helium314.keyboard.keyboard.PointerTracker;
import helium314.keyboard.keyboard.internal.KeyDrawParams;
import helium314.keyboard.keyboard.internal.KeyVisualAttributes;
+import helium314.keyboard.keyboard.internal.keyboard_parser.EmojiParserKt;
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode;
import helium314.keyboard.latin.AudioAndHapticFeedbackManager;
import helium314.keyboard.latin.dictionary.Dictionary;
@@ -318,7 +319,7 @@ public String getDescription(String emoji) {
return null;
}
- var wordProperty = sDictionaryFacilitator.getWordProperty(emoji);
+ var wordProperty = sDictionaryFacilitator.getWordProperty(EmojiParserKt.getEmojiNeutralVersion(emoji));
if (wordProperty == null || ! wordProperty.mHasShortcuts) {
return null;
}
@@ -343,17 +344,18 @@ public void startEmojiPalettes(final KeyVisualAttributes keyVisualAttr,
initDictionaryFacilitator();
}
- private void addRecentKey(final Key key) {
+ void addRecentKey(final Key key) {
if (Settings.getValues().mIncognitoModeEnabled) {
// We do not want to log recent keys while being in incognito
return;
}
- if (mEmojiCategory.isInRecentTab()) {
+ if (getVisibility() == VISIBLE && mEmojiCategory.isInRecentTab()) {
getRecentsKeyboard().addPendingKey(key);
return;
}
getRecentsKeyboard().addKeyFirst(key);
- mPager.getAdapter().notifyItemChanged(mEmojiCategory.getRecentTabId());
+ if (initialized)
+ mPager.getAdapter().notifyItemChanged(mEmojiCategory.getRecentTabId());
}
private void setupBottomRowKeyboard(final EditorInfo editorInfo, final KeyboardActionListener keyboardActionListener) {
diff --git a/app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiSearchActivity.kt b/app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiSearchActivity.kt
new file mode 100644
index 0000000000..ddb8046e02
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/keyboard/emoji/EmojiSearchActivity.kt
@@ -0,0 +1,405 @@
+// SPDX-License-Identifier: GPL-3.0-only
+package helium314.keyboard.keyboard.emoji
+
+import android.R.string.cancel
+import android.content.Context
+import android.content.Intent
+import android.content.res.Configuration
+import android.os.Bundle
+import android.os.Handler
+import android.view.ViewGroup
+import android.view.inputmethod.EditorInfo
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.exclude
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.safeDrawing
+import androidx.compose.foundation.layout.windowInsetsPadding
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.foundation.text.selection.LocalTextSelectionColors
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextFieldDefaults
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.lerp
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.input.PlatformImeOptions
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.text.input.VisualTransformation
+import androidx.compose.ui.text.intl.Locale
+import androidx.compose.ui.text.intl.LocaleList
+import androidx.compose.ui.text.style.TextDirection
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.viewinterop.AndroidView
+import helium314.keyboard.keyboard.Key
+import helium314.keyboard.keyboard.KeyboardId
+import helium314.keyboard.keyboard.KeyboardLayoutSet
+import helium314.keyboard.keyboard.KeyboardSwitcher
+import helium314.keyboard.keyboard.KeyboardTheme
+import helium314.keyboard.keyboard.internal.KeyboardBuilder
+import helium314.keyboard.keyboard.internal.KeyboardParams
+import helium314.keyboard.keyboard.internal.keyboard_parser.EMOJI_HINT_LABEL
+import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode
+import helium314.keyboard.keyboard.internal.keyboard_parser.getCode
+import helium314.keyboard.keyboard.internal.keyboard_parser.getEmojiDefaultVersion
+import helium314.keyboard.keyboard.internal.keyboard_parser.getEmojiKeyDimensions
+import helium314.keyboard.keyboard.internal.keyboard_parser.getEmojiNeutralVersion
+import helium314.keyboard.keyboard.internal.keyboard_parser.getEmojiPopupSpec
+import helium314.keyboard.latin.LatinIME
+import helium314.keyboard.latin.R
+import helium314.keyboard.latin.RichInputMethodManager
+import helium314.keyboard.latin.RichInputMethodSubtype
+import helium314.keyboard.latin.SingleDictionaryFacilitator
+import helium314.keyboard.latin.common.ColorType
+import helium314.keyboard.latin.common.splitOnWhitespace
+import helium314.keyboard.latin.dictionary.Dictionary
+import helium314.keyboard.latin.dictionary.DictionaryFactory
+import helium314.keyboard.latin.settings.Settings
+import helium314.keyboard.latin.utils.DictionaryInfoUtils
+import helium314.keyboard.latin.utils.Log
+import helium314.keyboard.latin.utils.ResourceUtils
+import helium314.keyboard.latin.utils.prefs
+import helium314.keyboard.settings.CloseIcon
+import helium314.keyboard.settings.SearchIcon
+import kotlin.properties.Delegates
+
+private const val TAG = "emoji-search"
+
+/**
+ * This activity is displayed in a gap created for it above the keyboard and below the host app, and disables the host app.
+ */
+class EmojiSearchActivity : ComponentActivity() {
+ private val colors = Settings.getValues().mColors
+ private var imeOpened = false
+ private var firstSearchDone = false
+ private var screenHeight by Delegates.notNull()
+ private lateinit var hintLocales: LocaleList
+ private lateinit var emojiPageKeyboardView: EmojiPageKeyboardView
+ private lateinit var keyboardParams: KeyboardParams
+ private var keyWidth by Delegates.notNull()
+ private var keyHeight by Delegates.notNull()
+ private var firstKey: Key? = null
+ private var pressedKey: Key? = null
+ private var imeVisible = false
+ private var imeClosed = false
+
+ private val closer = Runnable {
+ if (!imeVisible) {
+ Log.d(TAG, "IME closed")
+ imeClosed = true
+ cancel()
+ }
+ }
+
+ @OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ init()
+ enableEdgeToEdge()
+ setContent {
+ LocalContext.current.setTheme(KeyboardTheme.getKeyboardTheme(this).mStyleId)
+ Surface(modifier = Modifier.fillMaxSize(), color = Color(0x80000000)) {
+ var heightDp by remember { mutableStateOf(0.dp) }
+ Column(modifier = Modifier.fillMaxSize().clickable(onClick = { cancel() })
+ .windowInsetsPadding(WindowInsets.safeDrawing.exclude(WindowInsets(bottom = heightDp))),
+ verticalArrangement = Arrangement.Bottom
+ ) {
+ val localDensity = LocalDensity.current
+ var heightPx by remember { mutableIntStateOf(0) }
+ Column(modifier = Modifier.wrapContentHeight().background(Color(colors.get(ColorType.MAIN_BACKGROUND)))
+ .clickable(false) {}.onGloballyPositioned {
+ val bottom = it.localToScreen(Offset(0f, it.size.height.toFloat())).y.toInt()
+ imeVisible = bottom < screenHeight - 100
+ Log.d(TAG, "imeVisible: $imeVisible, firstSearchDone: $firstSearchDone, imeOpened: $imeOpened, " +
+ "bottom: $bottom, keyboardState: ${KeyboardSwitcher.getInstance().keyboardSwitchState}")
+ if (imeOpened && !imeVisible) {
+ Handler(this@EmojiSearchActivity.mainLooper).postDelayed(closer, 200)
+ }
+ if (imeOpened && !isAlphaKeyboard()) {
+ cancel()
+ return@onGloballyPositioned
+ }
+ if (imeVisible && firstSearchDone && isAlphaKeyboard()) {
+ Log.d(TAG, "IME opened in onGloballyPositioned")
+ imeOpened = true
+ Handler(this@EmojiSearchActivity.mainLooper).removeCallbacks(closer)
+ }
+ heightPx = it.size.height
+ heightDp = with(localDensity) { it.size.height.toDp() }
+ }) {
+ Row(modifier = Modifier.fillMaxWidth().height(30.dp)) {
+ IconButton(onClick = { cancel() }) {
+ Icon(painter = painterResource(R.drawable.ic_arrow_back),
+ stringResource(R.string.spoken_description_action_previous),
+ tint = Color(colors.get(ColorType.EMOJI_KEY_TEXT)))
+ }
+ Text(text = stringResource(R.string.emoji_search_title), fontSize = 18.sp,
+ color = Color(colors.get(ColorType.EMOJI_KEY_TEXT)),
+ modifier = Modifier.fillMaxWidth().align(Alignment.CenterVertically))
+ }
+ key(emojiPageKeyboardView) {
+ AndroidView({ emojiPageKeyboardView }, modifier = Modifier.wrapContentHeight().fillMaxWidth())
+ }
+ val focusRequester = remember { FocusRequester() }
+ var text by remember { mutableStateOf(TextFieldValue(searchText, selection = TextRange(searchText.length))) }
+ val textFieldColors = TextFieldDefaults.colors().copy(
+ unfocusedContainerColor = Color(colors.get(ColorType.FUNCTIONAL_KEY_BACKGROUND)),
+ unfocusedTextColor = Color(colors.get(ColorType.FUNCTIONAL_KEY_TEXT)),
+ cursorColor = Color(colors.get(ColorType.FUNCTIONAL_KEY_TEXT)),
+ unfocusedLeadingIconColor = Color(colors.get(ColorType.FUNCTIONAL_KEY_TEXT)),
+ unfocusedTrailingIconColor = Color(colors.get(ColorType.FUNCTIONAL_KEY_TEXT)),
+ unfocusedPlaceholderColor = lerp(Color(colors.get(ColorType.FUNCTIONAL_KEY_BACKGROUND)),
+ Color(colors.get(ColorType.FUNCTIONAL_KEY_TEXT)), 0.5f))
+ CompositionLocalProvider(LocalTextSelectionColors provides textFieldColors.textSelectionColors) {
+ BasicTextField(
+ value = text,
+ modifier = Modifier.fillMaxWidth().heightIn(20.dp, 30.dp).focusRequester(focusRequester),
+ textStyle = TextStyle(textDirection = TextDirection.Content, color = textFieldColors.unfocusedTextColor),
+ onValueChange = {
+ text = it
+ search(it.text)
+ },
+ enabled = true,
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done,
+ hintLocales = hintLocales,
+ platformImeOptions = PlatformImeOptions(encodePrivateImeOptions(PrivateImeOptions(heightPx)))),
+ keyboardActions = KeyboardActions(onDone = {
+ if (Settings.getValues().mAutoCorrectEnabled) pressedKey = firstKey
+ finish()
+ }),
+ singleLine = true,
+ cursorBrush = SolidColor(textFieldColors.cursorColor)
+ ) {
+ TextFieldDefaults.DecorationBox(
+ value = text.text,
+ colors = textFieldColors,
+
+ /**
+ * This is the reason for not using [androidx.compose.material3.TextField],
+ * which uses [TextFieldDefaults.contentPaddingWithoutLabel]
+ */
+ contentPadding = PaddingValues(2.dp),
+ visualTransformation = VisualTransformation.None,
+ innerTextField = it,
+ placeholder = { Text(stringResource(R.string.search_field_placeholder)) },
+ leadingIcon = { SearchIcon() },
+ trailingIcon = {
+ IconButton(onClick = {
+ text = TextFieldValue()
+ search("")
+ }) { CloseIcon(cancel) }
+ },
+ singleLine = true,
+ enabled = true,
+ interactionSource = MutableInteractionSource(),
+ )
+ }
+ }
+ LaunchedEffect(Unit) { focusRequester.requestFocus() }
+ }
+ }
+ }
+ }
+ }
+
+ override fun onEnterAnimationComplete() {
+ Log.d(TAG, "onEnterAnimationComplete")
+ search(searchText)
+ Log.d(TAG, "initial search done")
+ }
+
+ override fun onConfigurationChanged(newConfig: Configuration) {
+ super.onConfigurationChanged(newConfig)
+ init()
+ imeVisible = false
+ imeOpened = false
+ firstSearchDone = false
+ search(searchText)
+ }
+
+ override fun onStop() {
+ val intent = Intent(this, LatinIME::class.java).setAction(EMOJI_SEARCH_DONE_ACTION)
+ .putExtra(IME_CLOSED_KEY, imeClosed)
+ pressedKey?.let {
+ intent.putExtra(EMOJI_KEY, if (it.code == KeyCode.MULTIPLE_CODE_POINTS)
+ it.getOutputText()
+ else
+ Character.toString(it.code))
+
+ KeyboardSwitcher.getInstance().emojiPalettesView.addRecentKey(it)
+ }
+ startService(intent)
+ super.onStop()
+ }
+
+ private fun init() {
+ Log.d(TAG, "init start")
+ @Suppress("DEPRECATION")
+ screenHeight = windowManager.defaultDisplay.height
+ Log.d(TAG, "screenHeight: $screenHeight")
+ hintLocales = LocaleList(DictionaryInfoUtils.getLocalesWithEmojiDicts(this).map { Locale(it.toLanguageTag()) })
+ val keyboardWidth = ResourceUtils.getKeyboardWidth(this, Settings.getValues())
+ val layoutSet = KeyboardLayoutSet.Builder(this, null).setSubtype(RichInputMethodSubtype.emojiSubtype)
+ .setKeyboardGeometry(keyboardWidth, EmojiLayoutParams(resources).emojiKeyboardHeight).build()
+
+ // Initialize default versions and popup specs
+ layoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_CATEGORY2)
+
+ val keyboard = DynamicGridKeyboard.ofRowCount(prefs(), layoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS),
+ if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) 1 else 2,
+ KeyboardId.ELEMENT_EMOJI_CATEGORY16, keyboardWidth)
+ val builder = KeyboardBuilder(this, KeyboardParams())
+ builder.load(keyboard.mId)
+ keyboardParams = builder.mParams
+ val (width, height) = getEmojiKeyDimensions(keyboardParams, this)
+ keyWidth = width
+ keyHeight = height
+ emojiPageKeyboardView = EmojiPageKeyboardView(this, null)
+ emojiPageKeyboardView.setKeyboard(keyboard)
+ emojiPageKeyboardView.layoutParams =
+ ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
+ emojiPageKeyboardView.background = null
+ colors.setBackground(emojiPageKeyboardView, ColorType.MAIN_BACKGROUND)
+ emojiPageKeyboardView.setPadding(0, 10, 0, 10)
+
+ emojiPageKeyboardView.setEmojiViewCallback(object : EmojiViewCallback {
+ override fun onPressKey(key: Key) {
+ }
+
+ override fun onReleaseKey(key: Key) {
+ pressedKey = key
+ finish()
+ }
+
+ override fun getDescription(emoji: String): String? = if (Settings.getValues().mShowEmojiDescriptions)
+ dictionaryFacilitator?.getWordProperty(getEmojiNeutralVersion(emoji))?.let {
+ if (it.mHasShortcuts) it.mShortcutTargets[0]?.mWord else null
+ } else null
+ })
+ KeyboardSwitcher.getInstance().setAlphabetKeyboard()
+ Log.d(TAG, "init end")
+ }
+
+ private fun isAlphaKeyboard() = KeyboardSwitcher.getInstance().keyboardSwitchState !in
+ setOf(KeyboardSwitcher.KeyboardSwitchState.EMOJI, KeyboardSwitcher.KeyboardSwitchState.CLIPBOARD)
+
+ private fun search(text: String) {
+ initDictionaryFacilitator(this)
+ if (dictionaryFacilitator == null) {
+ cancel()
+ return
+ }
+
+ if (firstSearchDone && text == searchText) {
+ return
+ }
+
+ if (KeyboardSwitcher.getInstance().keyboard == null) {
+ /** Avoid crash in [SingleDictionaryFacilitator.getSuggestions] */
+ return
+ }
+
+ val keyboard = emojiPageKeyboardView.keyboard as DynamicGridKeyboard
+ keyboard.removeAllKeys()
+ firstKey = null
+ pressedKey = null
+ dictionaryFacilitator!!.getSuggestions(text.splitOnWhitespace()).filter { it.isEmoji }.forEach {
+ val emoji = getEmojiDefaultVersion(it.word)
+ val popupSpec = getEmojiPopupSpec(emoji)
+ val keyParams = Key.KeyParams(emoji, emoji.getCode(), if (popupSpec != null) EMOJI_HINT_LABEL else null, popupSpec,
+ Key.LABEL_FLAGS_FONT_NORMAL, keyboardParams)
+ keyParams.mAbsoluteWidth = keyWidth
+ keyParams.mAbsoluteHeight = keyHeight
+ val key = keyParams.createKey()
+ keyboard.addKeyLast(key)
+ if (firstKey == null) firstKey = key
+ }
+ emojiPageKeyboardView.invalidate()
+
+ searchText = text
+ firstSearchDone = true
+ if (imeVisible && !imeOpened) {
+ Log.d(TAG, "IME opened in search")
+ imeOpened = true
+ }
+ }
+
+ private fun cancel() {
+ finish()
+ }
+
+ @JvmRecord
+ data class PrivateImeOptions(val height: Int)
+
+ companion object {
+ const val EMOJI_SEARCH_DONE_ACTION: String = "EMOJI_SEARCH_DONE"
+ const val IME_CLOSED_KEY: String = "IME_CLOSED"
+ const val EMOJI_KEY: String = "EMOJI"
+ private const val PRIVATE_IME_OPTIONS_PREFIX: String = "helium314.keyboard.keyboard.emoji.search"
+ private var dictionaryFacilitator: SingleDictionaryFacilitator? = null
+ private var searchText: String = ""
+
+ fun decodePrivateImeOptions(editorInfo: EditorInfo?): PrivateImeOptions = PrivateImeOptions(
+ editorInfo?.privateImeOptions?.takeIf { it.startsWith(PRIVATE_IME_OPTIONS_PREFIX) }
+ ?.let { it.substring(PRIVATE_IME_OPTIONS_PREFIX.length + 1, it.indexOf(',')) }?.toInt() ?: 0)
+
+ fun closeDictionaryFacilitator() {
+ dictionaryFacilitator?.closeDictionaries()
+ dictionaryFacilitator = null
+ }
+
+ private fun encodePrivateImeOptions(privateImeOptions: PrivateImeOptions) =
+ "$PRIVATE_IME_OPTIONS_PREFIX.${privateImeOptions.height},"
+
+ private fun initDictionaryFacilitator(context: Context) {
+ val locale = RichInputMethodManager.getInstance().currentSubtype.locale
+ if (dictionaryFacilitator?.isForLocale(locale) != true) {
+ dictionaryFacilitator?.closeDictionaries()
+ dictionaryFacilitator = DictionaryInfoUtils.getCachedDictForLocaleAndType(locale, Dictionary.TYPE_EMOJI, context)
+ ?.let { DictionaryFactory.getDictionary(it, locale) }?.let { SingleDictionaryFacilitator(it) }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/helium314/keyboard/keyboard/internal/KeyboardCodesSet.java b/app/src/main/java/helium314/keyboard/keyboard/internal/KeyboardCodesSet.java
index 7671784e05..9ce3265a0b 100644
--- a/app/src/main/java/helium314/keyboard/keyboard/internal/KeyboardCodesSet.java
+++ b/app/src/main/java/helium314/keyboard/keyboard/internal/KeyboardCodesSet.java
@@ -55,7 +55,8 @@ public static int getCode(final String name) {
"key_toggle_onehanded",
"key_start_onehanded", // keep name to avoid breaking custom layouts
"key_stop_onehanded", // keep name to avoid breaking custom layouts
- "key_switch_onehanded"
+ "key_switch_onehanded",
+ "key_emoji_search"
};
private static final int[] DEFAULT = {
@@ -81,7 +82,8 @@ public static int getCode(final String name) {
KeyCode.TOGGLE_ONE_HANDED_MODE,
KeyCode.TOGGLE_ONE_HANDED_MODE,
KeyCode.TOGGLE_ONE_HANDED_MODE,
- KeyCode.SWITCH_ONE_HANDED_MODE
+ KeyCode.SWITCH_ONE_HANDED_MODE,
+ KeyCode.EMOJI_SEARCH
};
static {
diff --git a/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/EmojiParser.kt b/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/EmojiParser.kt
index f4662fb1a2..730094fba4 100644
--- a/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/EmojiParser.kt
+++ b/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/EmojiParser.kt
@@ -17,6 +17,7 @@ import helium314.keyboard.latin.settings.Settings
import helium314.keyboard.latin.utils.ResourceUtils
import helium314.keyboard.latin.utils.prefs
import java.util.Collections
+import kotlin.let
import kotlin.math.sqrt
class EmojiParser(private val params: KeyboardParams, private val context: Context) {
@@ -44,18 +45,25 @@ class EmojiParser(private val params: KeyboardParams, private val context: Conte
context.assets.open("emoji/$emojiFileName").reader().use { it.readLines() }
}
val defaultSkinTone = context.prefs().getString(Settings.PREF_EMOJI_SKIN_TONE, Defaults.PREF_EMOJI_SKIN_TONE)!!
- if (params.mId.mElementId == KeyboardId.ELEMENT_EMOJI_CATEGORY2 && defaultSkinTone != "") {
- // adjust PEOPLE_AND_BODY if we have a non-yellow default skin tone
- val modifiedLines = emojiLines.map { line ->
- val split = line.splitOnWhitespace().toMutableList()
- // find the line containing the skin tone, and swap with first
- val foundIndex = split.indexOfFirst { it.contains(defaultSkinTone) }
- if (foundIndex > 0) {
- Collections.swap(split, 0, foundIndex)
+ if (params.mId.mElementId == KeyboardId.ELEMENT_EMOJI_CATEGORY2) {
+ emojiDefaultVersions.clear()
+ emojiNeutralVersions.clear()
+ emojiPopupSpecs.clear()
+ if (defaultSkinTone != "") {
+ // adjust PEOPLE_AND_BODY if we have a non-yellow default skin tone
+ val modifiedLines = emojiLines.map { line ->
+ val split = line.splitOnWhitespace().toMutableList()
+ // find the line containing the skin tone, and swap with first
+ val foundIndex = split.indexOfFirst { it.contains(defaultSkinTone) }
+ if (foundIndex > 0) {
+ emojiDefaultVersions[split[0]] = split[foundIndex]
+ emojiNeutralVersions[split[foundIndex]] = split[0]
+ Collections.swap(split, 0, foundIndex)
+ }
+ split.joinToString(" ")
}
- split.joinToString(" ")
+ return parseLines(modifiedLines)
}
- return parseLines(modifiedLines)
}
return parseLines(emojiLines)
}
@@ -65,21 +73,7 @@ class EmojiParser(private val params: KeyboardParams, private val context: Conte
var currentX = params.mLeftPadding.toFloat()
val currentY = params.mTopPadding.toFloat() // no need to ever change, assignment to rows into rows is done in DynamicGridKeyboard
- // determine key width for default settings (no number row, no one-handed mode, 100% height and bottom padding scale)
- // this is a bit long, but ensures that emoji size stays the same, independent of these settings
- // we also ignore side padding for key width, and prefer fewer keys per row over narrower keys
- val defaultKeyWidth = ResourceUtils.getDefaultKeyboardWidth(context) * params.mDefaultKeyWidth
- var keyWidth = defaultKeyWidth * sqrt(Settings.getValues().mKeyboardHeightScale)
- val defaultKeyboardHeight = ResourceUtils.getDefaultKeyboardHeight(context.resources, false)
- val defaultBottomPadding = context.resources.getFraction(R.fraction.config_keyboard_bottom_padding_holo, defaultKeyboardHeight, defaultKeyboardHeight)
- val emojiKeyboardHeight = defaultKeyboardHeight * 0.75f + params.mVerticalGap - defaultBottomPadding - context.resources.getDimensionPixelSize(R.dimen.config_emoji_category_page_id_height)
- var keyHeight = emojiKeyboardHeight * params.mDefaultRowHeight * Settings.getValues().mKeyboardHeightScale // still apply height scale to key
-
- if (Settings.getValues().mEmojiKeyFit) {
- keyWidth *= Settings.getValues().mFontSizeMultiplierEmoji
- keyHeight *= Settings.getValues().mFontSizeMultiplierEmoji
- }
-
+ val (keyWidth, keyHeight) = getEmojiKeyDimensions(params, context)
lines.forEach { line ->
val keyParams = parseEmojiKeyNew(line) ?: return@forEach
@@ -104,6 +98,7 @@ class EmojiParser(private val params: KeyboardParams, private val context: Conte
if (SupportedEmojis.isUnsupported(label)) return null
val popupKeysSpec = split.drop(1).filterNot { SupportedEmojis.isUnsupported(it) }
.takeIf { it.isNotEmpty() }?.joinToString(",")
+ popupKeysSpec?.let { emojiPopupSpecs[label] = popupKeysSpec }
return KeyParams(
label,
label.getCode(),
@@ -113,10 +108,40 @@ class EmojiParser(private val params: KeyboardParams, private val context: Conte
params
)
}
+}
+
+fun getEmojiKeyDimensions(params: KeyboardParams, context: Context): Pair {
+ // determine key width for default settings (no number row, no one-handed mode, 100% height and bottom padding scale)
+ // this is a bit long, but ensures that emoji size stays the same, independent of these settings
+ // we also ignore side padding for key width, and prefer fewer keys per row over narrower keys
+ val defaultKeyWidth = ResourceUtils.getDefaultKeyboardWidth(context) * params.mDefaultKeyWidth
+ var keyWidth = defaultKeyWidth * sqrt(Settings.getValues().mKeyboardHeightScale)
+ val defaultKeyboardHeight = ResourceUtils.getDefaultKeyboardHeight(context.resources, false)
+ val defaultBottomPadding = context.resources.getFraction(
+ R.fraction.config_keyboard_bottom_padding_holo, defaultKeyboardHeight, defaultKeyboardHeight
+ )
+ val emojiKeyboardHeight = defaultKeyboardHeight * 0.75f + params.mVerticalGap - defaultBottomPadding -
+ context.resources.getDimensionPixelSize(R.dimen.config_emoji_category_page_id_height)
+ var keyHeight =
+ emojiKeyboardHeight * params.mDefaultRowHeight * Settings.getValues().mKeyboardHeightScale // still apply height scale to key
- private fun String.getCode(): Int =
- if (StringUtils.codePointCount(this) != 1) KeyCode.MULTIPLE_CODE_POINTS
- else Character.codePointAt(this, 0)
+ if (Settings.getValues().mEmojiKeyFit) {
+ keyWidth *= Settings.getValues().mFontSizeMultiplierEmoji
+ keyHeight *= Settings.getValues().mFontSizeMultiplierEmoji
+ }
+ return Pair(keyWidth, keyHeight)
}
+fun String.getCode(): Int =
+ if (StringUtils.codePointCount(this) != 1) KeyCode.MULTIPLE_CODE_POINTS
+ else Character.codePointAt(this, 0)
+
const val EMOJI_HINT_LABEL = "◥"
+
+private val emojiDefaultVersions: MutableMap = mutableMapOf()
+private val emojiNeutralVersions: MutableMap = mutableMapOf()
+private val emojiPopupSpecs: MutableMap = mutableMapOf()
+
+fun getEmojiDefaultVersion(emoji: String): String = emojiDefaultVersions[emoji] ?: emoji
+fun getEmojiNeutralVersion(emoji: String): String = emojiNeutralVersions[emoji] ?: emoji
+fun getEmojiPopupSpec(emoji: String): String? = emojiPopupSpecs[emoji]
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 9a748163bc..a47ac62c3b 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
@@ -176,6 +176,7 @@ object KeyCode {
const val ALT_RIGHT = -10047
const val META_LEFT = -10048
const val META_RIGHT = -10049
+ const val EMOJI_SEARCH = -10050
const val INLINE_EMOJI_SEARCH_DONE = -10051
@@ -200,7 +201,7 @@ object KeyCode {
PAGE_DOWN, META, TAB, ESCAPE, INSERT, SLEEP, MEDIA_PLAY, MEDIA_PAUSE, MEDIA_PLAY_PAUSE, MEDIA_NEXT,
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, INLINE_EMOJI_SEARCH_DONE, META_LOCK
+ SEND_INTENT_THREE, EMOJI_SEARCH, INLINE_EMOJI_SEARCH_DONE, META_LOCK
-> this
// conversion
diff --git a/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/floris/KeyData.kt b/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/floris/KeyData.kt
index 7c8b0736d4..4e73ab6e0c 100644
--- a/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/floris/KeyData.kt
+++ b/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/floris/KeyData.kt
@@ -210,6 +210,7 @@ class KeyboardStateSelector(
val moreSymbols: AbstractKeyData? = null,
val alphabet: AbstractKeyData? = null,
val default: AbstractKeyData? = null,
+ val emojiSearchAvailable: AbstractKeyData? = null,
) : AbstractKeyData {
override fun compute(params: KeyboardParams): KeyData? {
if (params.mId.mEmojiKeyEnabled)
@@ -222,6 +223,8 @@ class KeyboardStateSelector(
moreSymbols?.compute(params)?.let { return it }
if (params.mId.isAlphabetKeyboard)
alphabet?.compute(params)?.let { return it }
+ if (params.mId.mEmojiSearchAvailable)
+ emojiSearchAvailable?.compute(params)?.let { return it }
return default?.compute(params)
}
diff --git a/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/floris/KeyLabel.kt b/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/floris/KeyLabel.kt
index a473255c86..d31d9cc092 100644
--- a/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/floris/KeyLabel.kt
+++ b/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/floris/KeyLabel.kt
@@ -41,6 +41,7 @@ object KeyLabel {
const val TAB = "tab"
const val ESCAPE = "esc"
const val TIMESTAMP = "timestamp"
+ const val EMOJI_SEARCH = "emoji_search"
/** to make sure a FlorisBoard label works when reading a JSON layout */
// resulting special labels should be names of FunctionalKey enum, case insensitive
@@ -110,6 +111,7 @@ object KeyLabel {
CTRL, ALT, FN, META, ESCAPE -> label.uppercase(Locale.US)
TAB -> "!icon/tab_key|!code/${KeyCode.TAB}"
TIMESTAMP -> "⌚"
+ EMOJI_SEARCH -> "!icon/search_key|!code/key_emoji_search"
else -> if (label in toolbarKeyStrings.values)
"!icon/$label|!code/${getCodeForToolbarKey(ToolbarKey.valueOf(label.uppercase(Locale.US)))}"
else label
diff --git a/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/floris/TextKeyData.kt b/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/floris/TextKeyData.kt
index 07f3a37382..2eab73d699 100644
--- a/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/floris/TextKeyData.kt
+++ b/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/floris/TextKeyData.kt
@@ -405,7 +405,7 @@ sealed interface KeyData : AbstractKeyData {
when (label) { // or use code?
KeyLabel.SYMBOL_ALPHA, KeyLabel.SYMBOL, KeyLabel.ALPHA, KeyLabel.COMMA, KeyLabel.PERIOD, KeyLabel.DELETE,
KeyLabel.COM, KeyLabel.LANGUAGE_SWITCH, KeyLabel.NUMPAD, KeyLabel.CTRL, KeyLabel.ALT,
- KeyLabel.FN, KeyLabel.META, toolbarKeyStrings[ToolbarKey.EMOJI] -> return Key.BACKGROUND_TYPE_FUNCTIONAL
+ KeyLabel.FN, KeyLabel.META, KeyLabel.EMOJI_SEARCH, toolbarKeyStrings[ToolbarKey.EMOJI] -> return Key.BACKGROUND_TYPE_FUNCTIONAL
KeyLabel.SPACE, KeyLabel.ZWNJ -> return Key.BACKGROUND_TYPE_SPACEBAR
KeyLabel.ACTION -> return Key.BACKGROUND_TYPE_ACTION
KeyLabel.SHIFT -> return Key.BACKGROUND_TYPE_FUNCTIONAL
diff --git a/app/src/main/java/helium314/keyboard/latin/LatinIME.java b/app/src/main/java/helium314/keyboard/latin/LatinIME.java
index c44fb7bc69..051371b759 100644
--- a/app/src/main/java/helium314/keyboard/latin/LatinIME.java
+++ b/app/src/main/java/helium314/keyboard/latin/LatinIME.java
@@ -42,6 +42,7 @@
import helium314.keyboard.keyboard.KeyboardActionListener;
import helium314.keyboard.keyboard.KeyboardActionListenerImpl;
import helium314.keyboard.keyboard.emoji.EmojiPalettesView;
+import helium314.keyboard.keyboard.emoji.EmojiSearchActivity;
import helium314.keyboard.keyboard.internal.KeyboardIconsSet;
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode;
import helium314.keyboard.latin.common.InsetsOutlineProvider;
@@ -670,7 +671,9 @@ private void resetDictionaryFacilitator(@NonNull final Locale locale) {
mDictionaryFacilitator.resetDictionaries(this, mDictionaryFacilitator.getMainLocale(),
settingsValues.mUseContactsDictionary, settingsValues.mUseAppsDictionary,
settingsValues.mUsePersonalizedDicts, true, "", this);
+ mKeyboardSwitcher.setThemeNeedsReload(); // necessary for emoji search
EmojiPalettesView.closeDictionaryFacilitator();
+ EmojiSearchActivity.Companion.closeDictionaryFacilitator();
}
// used for debug
@@ -747,7 +750,11 @@ public void setInputView(final View view) {
mInputView = view;
mInsetsUpdater = ViewOutlineProviderUtilsKt.setInsetsOutlineProvider(view);
KtxKt.updateSoftInputWindowLayoutParameters(this, mInputView);
- mSuggestionStripView = mSettings.getCurrent().mToolbarMode == ToolbarMode.HIDDEN?
+ updateSuggestionStripView(view);
+ }
+
+ public void updateSuggestionStripView(View view) {
+ mSuggestionStripView = mSettings.getCurrent().mToolbarMode == ToolbarMode.HIDDEN || isEmojiSearch()?
null : view.findViewById(R.id.suggestion_strip_view);
if (hasSuggestionStripView()) {
mSuggestionStripView.setRtl(mRichImm.getCurrentSubtype().isRtlSubtype());
@@ -1177,7 +1184,7 @@ public void onComputeInsets(final InputMethodService.Insets outInsets) {
return;
}
final int stripHeight = mKeyboardSwitcher.isShowingStripContainer() ? mKeyboardSwitcher.getStripContainer().getHeight() : 0;
- final int visibleTopY = inputHeight - visibleKeyboardView.getHeight() - stripHeight;
+ int visibleTopY = inputHeight - visibleKeyboardView.getHeight() - stripHeight;
if (hasSuggestionStripView()) {
mSuggestionStripView.setMoreSuggestionsHeight(visibleTopY);
@@ -1194,6 +1201,10 @@ public void onComputeInsets(final InputMethodService.Insets outInsets) {
outInsets.touchableInsets = InputMethodService.Insets.TOUCHABLE_INSETS_REGION;
outInsets.touchableRegion.set(touchLeft, touchTop, touchRight, touchBottom);
}
+
+ // Has to be subtracted after calculating touchableRegion
+ visibleTopY -= getEmojiSearchActivityHeight();
+
outInsets.contentTopInsets = visibleTopY;
outInsets.visibleTopInsets = visibleTopY;
mInsetsUpdater.setInsets(outInsets);
@@ -1441,7 +1452,7 @@ private void showGesturePreviewAndSetSuggestions(@NonNull final SuggestedWords s
dismissGestureFloatingPreviewText /* dismissDelayed */);
}
- private boolean hasSuggestionStripView() {
+ public boolean hasSuggestionStripView() {
return null != mSuggestionStripView;
}
@@ -1700,6 +1711,39 @@ void launchSettings() {
startActivity(intent);
}
+ public void launchEmojiSearch() {
+ Log.d("emoji-search", "before activity launch");
+ startActivity(new Intent().setClass(this, EmojiSearchActivity.class)
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_MULTIPLE_TASK));
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (intent != null && EmojiSearchActivity.EMOJI_SEARCH_DONE_ACTION.equals(intent.getAction()) && ! isEmojiSearch()) {
+ if (intent.getBooleanExtra(EmojiSearchActivity.IME_CLOSED_KEY, false)) {
+ requestHideSelf(0);
+ } else {
+ mHandler.postDelayed(() -> KeyboardSwitcher.getInstance().setEmojiKeyboard(), 100);
+ if (intent.hasExtra(EmojiSearchActivity.EMOJI_KEY)) {
+ onTextInput(intent.getStringExtra(EmojiSearchActivity.EMOJI_KEY));
+ }
+ }
+
+ stopSelf(startId); // Allow the service to be destroyed when unbound
+ return START_NOT_STICKY;
+ }
+
+ return super.onStartCommand(intent, flags, startId);
+ }
+
+ public boolean isEmojiSearch() {
+ return getEmojiSearchActivityHeight() > 0;
+ }
+
+ private int getEmojiSearchActivityHeight() {
+ return EmojiSearchActivity.Companion.decodePrivateImeOptions(getCurrentInputEditorInfo()).height();
+ }
+
public void dumpDictionaryForDebug(final String dictName) {
if (!mDictionaryFacilitator.isActive()) {
resetDictionaryFacilitatorIfNecessary();
diff --git a/app/src/main/java/helium314/keyboard/latin/common/Colors.kt b/app/src/main/java/helium314/keyboard/latin/common/Colors.kt
index 848cca69a8..d0039fc4d9 100644
--- a/app/src/main/java/helium314/keyboard/latin/common/Colors.kt
+++ b/app/src/main/java/helium314/keyboard/latin/common/Colors.kt
@@ -281,7 +281,7 @@ class DynamicColors(context: Context, override val themeStyle: String, override
KEY_ICON, POPUP_KEY_ICON, ONE_HANDED_MODE_BUTTON, EMOJI_CATEGORY, TOOL_BAR_KEY, FUNCTIONAL_KEY_TEXT -> keyText
KEY_HINT_TEXT -> keyHintText
SPACE_BAR_TEXT -> spaceBarText
- FUNCTIONAL_KEY_BACKGROUND -> functionalKey
+ FUNCTIONAL_KEY_BACKGROUND -> if (!isNight) functionalKey else doubleAdjustedKeyBackground
SPACE_BAR_BACKGROUND -> spaceBar
MORE_SUGGESTIONS_WORD_BACKGROUND, MAIN_BACKGROUND -> background
KEY_BACKGROUND -> keyBackground
diff --git a/app/src/main/java/helium314/keyboard/latin/common/Constants.java b/app/src/main/java/helium314/keyboard/latin/common/Constants.java
index b61586bb2b..cb4eb408b7 100644
--- a/app/src/main/java/helium314/keyboard/latin/common/Constants.java
+++ b/app/src/main/java/helium314/keyboard/latin/common/Constants.java
@@ -233,6 +233,7 @@ public static String printableCode(final int code) {
case KeyCode.SWITCH_ONE_HANDED_MODE: return "switchOneHandedMode";
case KeyCode.SPLIT_LAYOUT: return "splitLayout";
case KeyCode.NUMPAD: return "numpad";
+ case KeyCode.EMOJI_SEARCH: return "emojiSearch";
default:
if (code < CODE_SPACE) return String.format("\\u%02X", code);
if (code < 0x100) return String.format("%c", code);
diff --git a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java
index d949e7155d..d155cbbe51 100644
--- a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java
+++ b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java
@@ -804,6 +804,10 @@ private void handleFunctionalEvent(final Event event, final InputTransaction inp
case KeyCode.TIMESTAMP:
mLatinIME.onTextInput(TimestampKt.getTimestamp(mLatinIME));
break;
+ case KeyCode.EMOJI_SEARCH:
+ commitTyped(Settings.getValues(), LastComposedWord.NOT_A_SEPARATOR);
+ mLatinIME.launchEmojiSearch();
+ break;
case KeyCode.SEND_INTENT_ONE, KeyCode.SEND_INTENT_TWO, KeyCode.SEND_INTENT_THREE:
IntentUtils.handleSendIntentKey(mLatinIME, event.getKeyCode());
case KeyCode.IME_HIDE_UI:
@@ -2750,8 +2754,7 @@ private static boolean isValidInlineEmojiSearchPreviousChar(int charBeforeBefore
}
public void updateEmojiDictionary(Locale locale) {
- //todo: disable if in full emoji search mode
- if (Settings.getValues().mInlineEmojiSearch && Settings.getValues().needsToLookupSuggestions()) {
+ if (Settings.getValues().mInlineEmojiSearch && Settings.getValues().needsToLookupSuggestions() && ! mLatinIME.isEmojiSearch()) {
if (mEmojiDictionaryFacilitator == null || ! mEmojiDictionaryFacilitator.isForLocale(locale)) {
closeEmojiDictionary();
var dictFile = DictionaryInfoUtils.getCachedDictForLocaleAndType(locale, "emoji", mLatinIME);
diff --git a/app/src/main/java/helium314/keyboard/latin/suggestions/SuggestionStripView.kt b/app/src/main/java/helium314/keyboard/latin/suggestions/SuggestionStripView.kt
index cff038f758..856fbe2a1e 100644
--- a/app/src/main/java/helium314/keyboard/latin/suggestions/SuggestionStripView.kt
+++ b/app/src/main/java/helium314/keyboard/latin/suggestions/SuggestionStripView.kt
@@ -61,6 +61,7 @@ import helium314.keyboard.latin.utils.removePinnedKey
import helium314.keyboard.latin.utils.setToolbarButtonsActivatedStateOnPrefChange
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.math.min
+import androidx.core.view.isGone
@SuppressLint("InflateParams")
class SuggestionStripView(context: Context, attrs: AttributeSet?, defStyle: Int) :
@@ -145,7 +146,7 @@ class SuggestionStripView(context: Context, attrs: AttributeSet?, defStyle: Int)
enabledToolKeyBackground.gradientType = GradientDrawable.RADIAL_GRADIENT
enabledToolKeyBackground.gradientRadius = resources.getDimensionPixelSize(R.dimen.config_suggestions_strip_height) / 2.1f
- val mToolbarMode = Settings.getValues().mToolbarMode
+ val mToolbarMode = if (isGone) ToolbarMode.HIDDEN else Settings.getValues().mToolbarMode
if (mToolbarMode == ToolbarMode.TOOLBAR_KEYS) {
setToolbarVisibility(true)
}
@@ -159,7 +160,7 @@ class SuggestionStripView(context: Context, attrs: AttributeSet?, defStyle: Int)
toolbar.addView(button)
}
}
- if (!Settings.getValues().mSuggestionStripHiddenPerUserSettings) {
+ if (!isGone && !Settings.getValues().mSuggestionStripHiddenPerUserSettings) {
for (pinnedKey in getPinnedToolbarKeys(context.prefs())) {
val button = createToolbarKey(context, pinnedKey)
button.layoutParams = toolbarKeyLayoutParams
diff --git a/app/src/main/java/helium314/keyboard/settings/dialogs/DictionaryDialog.kt b/app/src/main/java/helium314/keyboard/settings/dialogs/DictionaryDialog.kt
index 0ff2057e84..dad4094ea5 100644
--- a/app/src/main/java/helium314/keyboard/settings/dialogs/DictionaryDialog.kt
+++ b/app/src/main/java/helium314/keyboard/settings/dialogs/DictionaryDialog.kt
@@ -42,6 +42,7 @@ import java.io.File
import java.util.Locale
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalResources
+import helium314.keyboard.dictionarypack.DictionaryPackConstants
@Composable
fun DictionaryDialog(
@@ -130,13 +131,18 @@ private fun DictionaryDetails(dict: File) {
modifier = Modifier.padding(start = 10.dp, top = 0.dp, end = 10.dp, bottom = 12.dp)
)
}
- if (showDeleteDialog)
+ if (showDeleteDialog) {
+ val context = LocalContext.current
ConfirmationDialog(
onDismissRequest = { showDeleteDialog = false },
confirmButtonText = stringResource(R.string.remove),
- onConfirmed = { dict.delete() },
- content = { Text(stringResource(R.string.remove_dictionary_message, type))}
+ onConfirmed = {
+ dict.delete()
+ context.sendBroadcast(Intent(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION))
+ },
+ content = { Text(stringResource(R.string.remove_dictionary_message, type)) }
)
+ }
}
@Preview
diff --git a/app/src/main/res/layout/input_view.xml b/app/src/main/res/layout/input_view.xml
index 0bd874eeef..52faeca364 100644
--- a/app/src/main/res/layout/input_view.xml
+++ b/app/src/main/res/layout/input_view.xml
@@ -8,8 +8,7 @@
+ android:layout_height="wrap_content">
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index bf59d2d6bd..baf1d79f03 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -918,6 +918,10 @@ New dictionary:
Custom subtype
Landscape
+
+ Search emoji
+
+ Search
diff --git a/app/src/main/res/values/themes-common.xml b/app/src/main/res/values/themes-common.xml
index 6d2ede1177..b53f419cd5 100644
--- a/app/src/main/res/values/themes-common.xml
+++ b/app/src/main/res/values/themes-common.xml
@@ -114,4 +114,9 @@
- true
- none
+