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 +