From 2079be88d3798b33c673284301ddf5a47679c884 Mon Sep 17 00:00:00 2001 From: "pdroidandroid@gmail.com" Date: Sun, 6 Dec 2020 00:06:06 +0100 Subject: [PATCH] Extended 'more keys' support to emoji keyboards --- .../openboard/inputmethod/keyboard/Key.java | 125 ++++++- .../keyboard/MoreKeysKeyboardView.java | 46 ++- .../inputmethod/keyboard/MoreKeysPanel.java | 20 + .../keyboard/emoji/DynamicGridKeyboard.java | 19 +- .../keyboard/emoji/EmojiPageKeyboardView.java | 353 +++++++++++++++--- .../keyboard/emoji/EmojiPalettesAdapter.java | 4 +- .../keyboard/emoji/EmojiPalettesView.java | 7 +- .../keyboard/emoji/OnKeyEventListener.java | 22 ++ .../keyboard/internal/KeyboardBuilder.java | 23 +- .../internal/MoreCodesArrayParser.java | 60 +++ app/src/main/res/values/attrs.xml | 1 + 11 files changed, 596 insertions(+), 84 deletions(-) create mode 100644 app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/OnKeyEventListener.java create mode 100644 app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/MoreCodesArrayParser.java diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/Key.java b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/Key.java index 5a1b01cd7..414072594 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/Key.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/Key.java @@ -22,6 +22,7 @@ import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.text.TextUtils; +import android.util.Log; import org.dslul.openboard.inputmethod.keyboard.internal.KeyDrawParams; import org.dslul.openboard.inputmethod.keyboard.internal.KeySpecParser; import org.dslul.openboard.inputmethod.keyboard.internal.KeyStyle; @@ -208,8 +209,7 @@ public class Key implements Comparable { private boolean mEnabled = true; /** - * Constructor for a key on MoreKeyKeyboard, on MoreSuggestions, - * and in a . + * Constructor for a key on MoreKeyKeyboard and on MoreSuggestions. */ public Key(@Nullable final String label, final int iconId, final int code, @Nullable final String outputText, @Nullable final String hintLabel, @@ -241,6 +241,82 @@ public class Key implements Comparable { mHashCode = computeHashCode(this); } + /** + * Constructor for a key in a . + */ + public Key(@Nullable final String label, final int code, @Nullable final String outputText, + @Nullable final String hintLabel, @Nullable final String moreKeySpecs, + final int labelFlags, final int backgroundType, final int x, final int y, + final int width, final int height, final KeyboardParams params) { + mWidth = width - params.mHorizontalGap; + mHeight = height - params.mVerticalGap; + mHorizontalGap = params.mHorizontalGap; + mVerticalGap = params.mVerticalGap; + mHintLabel = hintLabel; + mLabelFlags = labelFlags; + mBackgroundType = backgroundType; + + if (moreKeySpecs != null) { + String[] moreKeys = MoreKeySpec.splitKeySpecs(moreKeySpecs); + // Get maximum column order number and set a relevant mode value. + int moreKeysColumnAndFlags = MORE_KEYS_MODE_MAX_COLUMN_WITH_AUTO_ORDER + | params.mMaxMoreKeysKeyboardColumn; + int value; + if ((value = MoreKeySpec.getIntValue(moreKeys, MORE_KEYS_AUTO_COLUMN_ORDER, -1)) > 0) { + // Override with fixed column order number and set a relevant mode value. + moreKeysColumnAndFlags = MORE_KEYS_MODE_FIXED_COLUMN_WITH_AUTO_ORDER + | (value & MORE_KEYS_COLUMN_NUMBER_MASK); + } + if ((value = MoreKeySpec.getIntValue(moreKeys, MORE_KEYS_FIXED_COLUMN_ORDER, -1)) > 0) { + // Override with fixed column order number and set a relevant mode value. + moreKeysColumnAndFlags = MORE_KEYS_MODE_FIXED_COLUMN_WITH_FIXED_ORDER + | (value & MORE_KEYS_COLUMN_NUMBER_MASK); + } + if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_HAS_LABELS)) { + moreKeysColumnAndFlags |= MORE_KEYS_FLAGS_HAS_LABELS; + } + if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_NEEDS_DIVIDERS)) { + moreKeysColumnAndFlags |= MORE_KEYS_FLAGS_NEEDS_DIVIDERS; + } + if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_NO_PANEL_AUTO_MORE_KEY)) { + moreKeysColumnAndFlags |= MORE_KEYS_FLAGS_NO_PANEL_AUTO_MORE_KEY; + } + mMoreKeysColumnAndFlags = moreKeysColumnAndFlags; + + moreKeys = MoreKeySpec.insertAdditionalMoreKeys(moreKeys, null); + int actionFlags = 0; + if (moreKeys != null) { + actionFlags |= ACTION_FLAGS_ENABLE_LONG_PRESS; + mMoreKeys = new MoreKeySpec[moreKeys.length]; + for (int i = 0; i < moreKeys.length; i++) { + mMoreKeys[i] = new MoreKeySpec(moreKeys[i], false, Locale.getDefault()); + } + } else { + mMoreKeys = null; + } + mActionFlags = actionFlags; + } else { + // TODO: Pass keyActionFlags as an argument. + mActionFlags = ACTION_FLAGS_NO_KEY_PREVIEW; + mMoreKeys = null; + mMoreKeysColumnAndFlags = 0; + } + + mLabel = label; + mOptionalAttributes = OptionalAttributes.newInstance(outputText, CODE_UNSPECIFIED, + ICON_UNDEFINED, 0 /* visualInsetsLeft */, 0 /* visualInsetsRight */); + mCode = code; + mEnabled = (code != CODE_UNSPECIFIED); + mIconId = KeyboardIconsSet.ICON_UNDEFINED; + // Horizontal gap is divided equally to both sides of the key. + mX = x + mHorizontalGap / 2; + mY = y; + mHitBox.set(x, y, x + width + 1, y + height); + mKeyVisualAttributes = null; + + mHashCode = computeHashCode(this); + } + /** * Create a key with the given top-left coordinate and extract its attributes from a key * specification string, Key attribute array, key style, and etc. @@ -410,9 +486,35 @@ public class Key implements Comparable { * Copy constructor for DynamicGridKeyboard.GridKey. * * @param key the original key. + * @param moreKeys the more keys that should be assigned to this key. + * @param labelHint the label hint that should be assigned to this key. + * @param backgroundType the background type that should be assigned to this key. */ - protected Key(@Nonnull final Key key) { - this(key, key.mMoreKeys); + protected Key(@Nonnull final Key key, @Nullable final MoreKeySpec[] moreKeys, + @Nullable final String labelHint, final int backgroundType) { + // Final attributes. + mCode = key.mCode; + mLabel = key.mLabel; + mHintLabel = labelHint; + mLabelFlags = key.mLabelFlags; + mIconId = key.mIconId; + mWidth = key.mWidth; + mHeight = key.mHeight; + mHorizontalGap = key.mHorizontalGap; + mVerticalGap = key.mVerticalGap; + mX = key.mX; + mY = key.mY; + mHitBox.set(key.mHitBox); + mMoreKeys = moreKeys; + mMoreKeysColumnAndFlags = key.mMoreKeysColumnAndFlags; + mBackgroundType = backgroundType; + mActionFlags = key.mActionFlags; + mKeyVisualAttributes = key.mKeyVisualAttributes; + mOptionalAttributes = key.mOptionalAttributes; + mHashCode = key.mHashCode; + // Key state. + mPressed = key.mPressed; + mEnabled = key.mEnabled; } private Key(@Nonnull final Key key, @Nullable final MoreKeySpec[] moreKeys) { @@ -826,6 +928,21 @@ public class Key implements Comparable { return iconSet.getIconDrawable(getIconId()); } + /** + * Gets the background type of this key. + * @return Background type. + * @see Key#BACKGROUND_TYPE_EMPTY + * @see Key#BACKGROUND_TYPE_NORMAL + * @see Key#BACKGROUND_TYPE_FUNCTIONAL + * @see Key#BACKGROUND_TYPE_STICKY_OFF + * @see Key#BACKGROUND_TYPE_STICKY_ON + * @see Key#BACKGROUND_TYPE_ACTION + * @see Key#BACKGROUND_TYPE_SPACEBAR + */ + public int getBackgroundType() { + return mBackgroundType; + } + /** * Gets the width of the key in pixels, excluding the gap. * @return The width of the key in pixels, excluding the gap. diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/MoreKeysKeyboardView.java b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/MoreKeysKeyboardView.java index 320df3142..f65651c9b 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/MoreKeysKeyboardView.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/MoreKeysKeyboardView.java @@ -28,6 +28,7 @@ import android.view.ViewGroup; import org.dslul.openboard.inputmethod.accessibility.AccessibilityUtils; import org.dslul.openboard.inputmethod.accessibility.MoreKeysKeyboardAccessibilityDelegate; +import org.dslul.openboard.inputmethod.keyboard.emoji.OnKeyEventListener; import org.dslul.openboard.inputmethod.keyboard.internal.KeyDrawParams; import org.dslul.openboard.inputmethod.latin.R; import org.dslul.openboard.inputmethod.latin.common.Constants; @@ -44,6 +45,7 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel protected final KeyDetector mKeyDetector; private Controller mController = EMPTY_CONTROLLER; protected KeyboardActionListener mListener; + protected OnKeyEventListener mKeyEventListener; private int mOriginX; private int mOriginY; private Key mCurrentKey; @@ -118,11 +120,31 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel } } + /** + * {@inheritDoc} + */ @Override public void showMoreKeysPanel(final View parentView, final Controller controller, final int pointX, final int pointY, final KeyboardActionListener listener) { - mController = controller; mListener = listener; + mKeyEventListener = null; + showMoreKeysPanelInternal(parentView, controller, pointX, pointY); + } + + /** + * {@inheritDoc} + */ + @Override + public void showMoreKeysPanel(final View parentView, final Controller controller, + final int pointX, final int pointY, final OnKeyEventListener listener) { + mListener = null; + mKeyEventListener = listener; + showMoreKeysPanelInternal(parentView, controller, pointX, pointY); + } + + private void showMoreKeysPanelInternal(final View parentView, final Controller controller, + final int pointX, final int pointY) { + mController = controller; final View container = getContainerView(); // The coordinates of panel's left-top corner in parentView's coordinate system. // We need to consider background drawable paddings. @@ -193,16 +215,20 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel * Performs the specific action for this panel when the user presses a key on the panel. */ protected void onKeyInput(final Key key, final int x, final int y) { - final int code = key.getCode(); - if (code == Constants.CODE_OUTPUT_TEXT) { - mListener.onTextInput(mCurrentKey.getOutputText()); - } else if (code != Constants.CODE_UNSPECIFIED) { - if (getKeyboard().hasProximityCharsCorrection(code)) { - mListener.onCodeInput(code, x, y, false /* isKeyRepeat */); - } else { - mListener.onCodeInput(code, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, - false /* isKeyRepeat */); + if (mListener != null) { + final int code = key.getCode(); + if (code == Constants.CODE_OUTPUT_TEXT) { + mListener.onTextInput(mCurrentKey.getOutputText()); + } else if (code != Constants.CODE_UNSPECIFIED) { + if (getKeyboard().hasProximityCharsCorrection(code)) { + mListener.onCodeInput(code, x, y, false /* isKeyRepeat */); + } else { + mListener.onCodeInput(code, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, + false /* isKeyRepeat */); + } } + } else if (mKeyEventListener != null) { + mKeyEventListener.onReleaseKey(key); } } diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/MoreKeysPanel.java b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/MoreKeysPanel.java index 62e7b195b..0291241db 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/MoreKeysPanel.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/MoreKeysPanel.java @@ -18,6 +18,7 @@ package org.dslul.openboard.inputmethod.keyboard; import android.view.View; import android.view.ViewGroup; +import org.dslul.openboard.inputmethod.keyboard.emoji.OnKeyEventListener; public interface MoreKeysPanel { interface Controller { @@ -63,6 +64,25 @@ public interface MoreKeysPanel { void showMoreKeysPanel(View parentView, Controller controller, int pointX, int pointY, KeyboardActionListener listener); + /** + * + * Initializes the layout and event handling of this {@link MoreKeysPanel} and calls the + * controller's onShowMoreKeysPanel to add the panel's container view. + * Same as {@link MoreKeysPanel#showMoreKeysPanel(View, Controller, int, int, KeyboardActionListener)}, + * but with a {@link OnKeyEventListener}. + * + * @param parentView the parent view of this {@link MoreKeysPanel} + * @param controller the controller that can dismiss this {@link MoreKeysPanel} + * @param pointX x coordinate of this {@link MoreKeysPanel} + * @param pointY y coordinate of this {@link MoreKeysPanel} + * @param listener the listener that will receive keyboard action from this + * {@link MoreKeysPanel}. + */ + // TODO: Currently the MoreKeysPanel is inside a container view that is added to the parent. + // Consider the simpler approach of placing the MoreKeysPanel itself into the parent view. + void showMoreKeysPanel(View parentView, Controller controller, int pointX, + int pointY, OnKeyEventListener listener); + /** * Dismisses the more keys panel and calls the controller's onDismissMoreKeysPanel to remove * the panel's container view. diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/DynamicGridKeyboard.java b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/DynamicGridKeyboard.java index 939a0cfa7..528c5f1ac 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/DynamicGridKeyboard.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/DynamicGridKeyboard.java @@ -22,9 +22,12 @@ import android.util.Log; import org.dslul.openboard.inputmethod.keyboard.Key; import org.dslul.openboard.inputmethod.keyboard.Keyboard; +import org.dslul.openboard.inputmethod.keyboard.internal.MoreKeySpec; import org.dslul.openboard.inputmethod.latin.settings.Settings; import org.dslul.openboard.inputmethod.latin.utils.JsonUtils; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; @@ -105,7 +108,16 @@ final class DynamicGridKeyboard extends Keyboard { } synchronized (mLock) { mCachedGridKeys = null; - final GridKey key = new GridKey(usedKey); + // When a key is added to recents keyboard, we don't want to keep its more keys + // neither its hint label. Also, we make sure its background type is matching our keyboard + // if key comes from another keyboard (ie. a {@link MoreKeysKeyboard}). + final boolean dropMoreKeys = mIsRecents; + // Check if hint was a more emoji indicator and prevent its copy if more keys aren't copied + final boolean dropHintLabel = dropMoreKeys && "\u25E5".equals(usedKey.getHintLabel()); + final GridKey key = new GridKey(usedKey, + dropMoreKeys ? null : usedKey.getMoreKeys(), + dropHintLabel ? null : usedKey.getHintLabel(), + mIsRecents ? Key.BACKGROUND_TYPE_EMPTY : usedKey.getBackgroundType()); while (mGridKeys.remove(key)) { // Remove duplicate keys. } @@ -227,8 +239,9 @@ final class DynamicGridKeyboard extends Keyboard { private int mCurrentX; private int mCurrentY; - public GridKey(final Key originalKey) { - super(originalKey); + public GridKey(@Nonnull final Key originalKey, @Nullable final MoreKeySpec[] moreKeys, + @Nullable final String labelHint, final int backgroundType) { + super(originalKey, moreKeys, labelHint, backgroundType); } public void updateCoordinates(final int x0, final int y0, final int x1, final int y1) { diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/EmojiPageKeyboardView.java b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/EmojiPageKeyboardView.java index b6b6b1a04..7ccf2beb4 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/EmojiPageKeyboardView.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/EmojiPageKeyboardView.java @@ -17,19 +17,37 @@ package org.dslul.openboard.inputmethod.keyboard.emoji; import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; import android.os.Handler; import android.util.AttributeSet; -import android.view.GestureDetector; +import android.util.Log; +import android.view.LayoutInflater; import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; +import android.widget.FrameLayout; import org.dslul.openboard.inputmethod.accessibility.AccessibilityUtils; import org.dslul.openboard.inputmethod.accessibility.KeyboardAccessibilityDelegate; import org.dslul.openboard.inputmethod.keyboard.Key; import org.dslul.openboard.inputmethod.keyboard.KeyDetector; import org.dslul.openboard.inputmethod.keyboard.Keyboard; import org.dslul.openboard.inputmethod.keyboard.KeyboardView; +import org.dslul.openboard.inputmethod.keyboard.MoreKeysKeyboard; +import org.dslul.openboard.inputmethod.keyboard.MoreKeysKeyboardView; +import org.dslul.openboard.inputmethod.keyboard.MoreKeysPanel; +import org.dslul.openboard.inputmethod.keyboard.internal.MoreKeySpec; import org.dslul.openboard.inputmethod.latin.R; +import org.dslul.openboard.inputmethod.latin.common.CoordinateUtils; +import org.dslul.openboard.inputmethod.latin.settings.Settings; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.WeakHashMap; /** * This is an extended {@link KeyboardView} class that hosts an emoji page keyboard. @@ -37,15 +55,12 @@ import org.dslul.openboard.inputmethod.latin.R; */ // TODO: Implement key popup preview. final class EmojiPageKeyboardView extends KeyboardView implements - GestureDetector.OnGestureListener { + MoreKeysPanel.Controller { + private static final String TAG = "EmojiPageKeyboardView"; + private static final boolean LOG = true; private static final long KEY_PRESS_DELAY_TIME = 250; // msec private static final long KEY_RELEASE_DELAY_TIME = 30; // msec - public interface OnKeyEventListener { - void onPressKey(Key key); - void onReleaseKey(Key key); - } - private static final OnKeyEventListener EMPTY_LISTENER = new OnKeyEventListener() { @Override public void onPressKey(final Key key) {} @@ -55,9 +70,25 @@ final class EmojiPageKeyboardView extends KeyboardView implements private OnKeyEventListener mListener = EMPTY_LISTENER; private final KeyDetector mKeyDetector = new KeyDetector(); - private final GestureDetector mGestureDetector; private KeyboardAccessibilityDelegate mAccessibilityDelegate; + // Touch inputs + private int mPointerId = MotionEvent.INVALID_POINTER_ID; + private int mLastX, mLastY; + private Key mCurrentKey; + private Runnable mPendingKeyDown; + private Runnable mPendingLongPress; + private final Handler mHandler; + + // More keys keyboard + private final View mMoreKeysKeyboardContainer; + private final WeakHashMap mMoreKeysKeyboardCache = new WeakHashMap<>(); + private final boolean mConfigShowMoreKeysKeyboardAtTouchedPoint; + private final ViewGroup mMoreKeysPlacerView; + // More keys panel (used by more keys keyboard view) + // TODO: Consider extending to support multiple more keys panels + private MoreKeysPanel mMoreKeysPanel; + public EmojiPageKeyboardView(final Context context, final AttributeSet attrs) { this(context, attrs, R.attr.keyboardViewStyle); } @@ -65,9 +96,74 @@ final class EmojiPageKeyboardView extends KeyboardView implements public EmojiPageKeyboardView(final Context context, final AttributeSet attrs, final int defStyle) { super(context, attrs, defStyle); - mGestureDetector = new GestureDetector(context, this); - mGestureDetector.setIsLongpressEnabled(false /* isLongpressEnabled */); mHandler = new Handler(); + + mMoreKeysPlacerView = new FrameLayout(context, attrs); + + final TypedArray keyboardViewAttr = context.obtainStyledAttributes(attrs, + R.styleable.MainKeyboardView, defStyle, R.style.MainKeyboardView); + final int moreKeysKeyboardLayoutId = keyboardViewAttr.getResourceId( + R.styleable.MainKeyboardView_moreKeysKeyboardLayout, 0); + mConfigShowMoreKeysKeyboardAtTouchedPoint = keyboardViewAttr.getBoolean( + R.styleable.MainKeyboardView_showMoreKeysKeyboardAtTouchedPoint, false); + keyboardViewAttr.recycle(); + + final LayoutInflater inflater = LayoutInflater.from(getContext()); + mMoreKeysKeyboardContainer = inflater.inflate(moreKeysKeyboardLayoutId, null); + } + + @Override + public void setHardwareAcceleratedDrawingEnabled(final boolean enabled) { + super.setHardwareAcceleratedDrawingEnabled(enabled); + if (!enabled) return; + final Paint layerPaint = new Paint(); + layerPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER)); + mMoreKeysPlacerView.setLayerType(LAYER_TYPE_HARDWARE, layerPaint); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + installMoreKeysPlacerView(); + } + + private void installMoreKeysPlacerView() { + final View rootView = getRootView(); + if (rootView == null) { + Log.w(TAG, "Cannot find root view"); + return; + } + final ViewGroup windowContentView = rootView.findViewById(android.R.id.content); + // Note: It'd be very weird if we get null by android.R.id.content. + if (windowContentView == null) { + Log.w(TAG, "Cannot find android.R.id.content view to add DrawingPreviewPlacerView"); + return; + } + + windowContentView.addView(mMoreKeysPlacerView); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mMoreKeysPlacerView.removeAllViews(); + uninstallMoreKeysPlacerView(); + } + + private void uninstallMoreKeysPlacerView() { + final View rootView = getRootView(); + if (rootView == null) { + Log.w(TAG, "Cannot find root view"); + return; + } + final ViewGroup windowContentView = rootView.findViewById(android.R.id.content); + // Note: It'd be very weird if we get null by android.R.id.content. + if (windowContentView == null) { + Log.w(TAG, "Cannot find android.R.id.content view to add DrawingPreviewPlacerView"); + return; + } + + windowContentView.removeView(mMoreKeysPlacerView); } public void setOnKeyEventListener(final OnKeyEventListener listener) { @@ -81,6 +177,7 @@ final class EmojiPageKeyboardView extends KeyboardView implements public void setKeyboard(final Keyboard keyboard) { super.setKeyboard(keyboard); mKeyDetector.setKeyboard(keyboard, 0 /* correctionX */, 0 /* correctionY */); + mMoreKeysKeyboardCache.clear(); if (AccessibilityUtils.Companion.getInstance().isAccessibilityEnabled()) { if (mAccessibilityDelegate == null) { mAccessibilityDelegate = new KeyboardAccessibilityDelegate<>(this, mKeyDetector); @@ -91,12 +188,82 @@ final class EmojiPageKeyboardView extends KeyboardView implements } } + @Nullable + public MoreKeysPanel showMoreKeysKeyboard(@Nonnull final Key key, final int lastX, final int lastY) { + final MoreKeySpec[] moreKeys = key.getMoreKeys(); + if (moreKeys == null) { + return null; + } + Keyboard moreKeysKeyboard = mMoreKeysKeyboardCache.get(key); + if (moreKeysKeyboard == null) { + final MoreKeysKeyboard.Builder builder = new MoreKeysKeyboard.Builder( + getContext(), key, getKeyboard(), + true, key.getWidth(), key.getHeight(), // TODO This is cheating + newLabelPaint(key)); + moreKeysKeyboard = builder.build(); + mMoreKeysKeyboardCache.put(key, moreKeysKeyboard); + } + + final View container = mMoreKeysKeyboardContainer; + final MoreKeysKeyboardView moreKeysKeyboardView = + container.findViewById(R.id.more_keys_keyboard_view); + moreKeysKeyboardView.setKeyboard(moreKeysKeyboard); + container.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + + final int[] lastCoords = CoordinateUtils.newCoordinateArray(1, lastX, lastY); + // The more keys keyboard is usually horizontally aligned with the center of the parent key. + // If showMoreKeysKeyboardAtTouchedPoint is true and the key preview is disabled, the more + // keys keyboard is placed at the touch point of the parent key. + final int pointX = mConfigShowMoreKeysKeyboardAtTouchedPoint + ? CoordinateUtils.x(lastCoords) + : key.getX() + key.getWidth() / 2; + final int pointY = key.getY(); + moreKeysKeyboardView.showMoreKeysPanel(this, this, + pointX, pointY, mListener); + return moreKeysKeyboardView; + } + + @Override + public void onShowMoreKeysPanel(final MoreKeysPanel panel) { + // Dismiss another {@link MoreKeysPanel} that may be being showed. + onDismissMoreKeysPanel(); + panel.showInParent(mMoreKeysPlacerView); + mMoreKeysPanel = panel; + } + + public boolean isShowingMoreKeysPanel() { + return mMoreKeysPanel != null; + } + + @Override + public void onCancelMoreKeysPanel() { + // Nothing to do + } + + @Override + public void onDismissMoreKeysPanel() { + if (isShowingMoreKeysPanel()) { + mMoreKeysPanel.removeFromParent(); + mMoreKeysPanel = null; + } + } + + private void dismissMoreKeysPanel() { + if (isShowingMoreKeysPanel()) { + mMoreKeysPanel.dismissMoreKeysPanel(); + } + } + @Override public boolean dispatchPopulateAccessibilityEvent(final AccessibilityEvent event) { // Don't populate accessibility event with all Emoji keys. return true; } + private int getLongPressTimeout() { + return Settings.getInstance().getCurrent().mKeyLongpressTimeout; + } + /** * {@inheritDoc} */ @@ -116,28 +283,66 @@ final class EmojiPageKeyboardView extends KeyboardView implements */ @Override public boolean onTouchEvent(final MotionEvent e) { - if (mGestureDetector.onTouchEvent(e)) { - return true; + switch (e.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + mPointerId = e.getPointerId(0); + return onDown(e); + case MotionEvent.ACTION_UP: + return onUp(e); + case MotionEvent.ACTION_MOVE: + return onMove(e); + case MotionEvent.ACTION_CANCEL: + return onCancel(e); + default: + return false; } - final Key key = getKey(e); - if (key != null && key != mCurrentKey) { - releaseCurrentKey(false /* withKeyRegistering */); - } - return true; } - // {@link GestureEnabler#OnGestureListener} methods. - private Key mCurrentKey; - private Runnable mPendingKeyDown; - private final Handler mHandler; - - private Key getKey(final MotionEvent e) { - final int index = e.getActionIndex(); - final int x = (int)e.getX(index); - final int y = (int)e.getY(index); + private Key getKey(final int x, final int y) { return mKeyDetector.detectHitKey(x, y); } + private void onLongPressed(final Key key) { + if (isShowingMoreKeysPanel()) { + return; + } + + if (key == null) { + if (LOG) Log.d(TAG, "Long press ignored because detected key is null"); + return; + } + + final int x = mLastX; + final int y = mLastY; + final MoreKeysPanel moreKeysPanel = showMoreKeysKeyboard(key, x, y); + if (moreKeysPanel != null) { + final int translatedX = moreKeysPanel.translateX(x); + final int translatedY = moreKeysPanel.translateY(y); + moreKeysPanel.onDownEvent(translatedX, translatedY, mPointerId, 0 /* nor used for now */); + } + } + + private void registerPress(final Key key) { + // Do not trigger key-down effect right now in case this is actually a fling action. + mPendingKeyDown = new Runnable() { + @Override + public void run() { + callListenerOnPressKey(key); + } + }; + mHandler.postDelayed(mPendingKeyDown, KEY_PRESS_DELAY_TIME); + } + + private void registerLongPress(final Key key) { + mPendingLongPress = new Runnable() { + @Override + public void run() { + onLongPressed(key); + } + }; + mHandler.postDelayed(mPendingLongPress, getLongPressTimeout()); + } + void callListenerOnReleaseKey(final Key releasedKey, final boolean withKeyRegistering) { releasedKey.onReleased(); invalidateKey(releasedKey); @@ -164,40 +369,48 @@ final class EmojiPageKeyboardView extends KeyboardView implements mCurrentKey = null; } - @Override + public void cancelLongPress() { + mHandler.removeCallbacks(mPendingLongPress); + mPendingLongPress = null; + } + public boolean onDown(final MotionEvent e) { - final Key key = getKey(e); + final int x = (int) e.getX(); + final int y = (int) e.getY(); + final Key key = getKey(x, y); releaseCurrentKey(false /* withKeyRegistering */); mCurrentKey = key; if (key == null) { return false; } - // Do not trigger key-down effect right now in case this is actually a fling action. - mPendingKeyDown = new Runnable() { - @Override - public void run() { - callListenerOnPressKey(key); - } - }; - mHandler.postDelayed(mPendingKeyDown, KEY_PRESS_DELAY_TIME); - return false; + registerPress(key); + + registerLongPress(key); + + mLastX = x; + mLastY = y; + return true; } - @Override - public void onShowPress(final MotionEvent e) { - // User feedback is done at {@link #onDown(MotionEvent)}. - } - - @Override - public boolean onSingleTapUp(final MotionEvent e) { - final Key key = getKey(e); + public boolean onUp(final MotionEvent e) { + final int x = (int) e.getX(); + final int y = (int) e.getY(); + final Key key = getKey(x, y); final Runnable pendingKeyDown = mPendingKeyDown; final Key currentKey = mCurrentKey; releaseCurrentKey(false /* withKeyRegistering */); if (key == null) { return false; } - if (key == currentKey && pendingKeyDown != null) { + + final boolean isShowingMoreKeysPanel = isShowingMoreKeysPanel(); + if (isShowingMoreKeysPanel) { + final long eventTime = e.getEventTime(); + final int translatedX = mMoreKeysPanel.translateX(x); + final int translatedY = mMoreKeysPanel.translateY(y); + mMoreKeysPanel.onUpEvent(translatedX, translatedY, mPointerId, eventTime); + dismissMoreKeysPanel(); + } else if (key == currentKey && pendingKeyDown != null) { pendingKeyDown.run(); // Trigger key-release event a little later so that a user can see visual feedback. mHandler.postDelayed(new Runnable() { @@ -209,25 +422,47 @@ final class EmojiPageKeyboardView extends KeyboardView implements } else { callListenerOnReleaseKey(key, true /* withRegistering */); } + + cancelLongPress(); return true; } - @Override - public boolean onScroll(final MotionEvent e1, final MotionEvent e2, final float distanceX, - final float distanceY) { + public boolean onCancel(final MotionEvent e) { releaseCurrentKey(false /* withKeyRegistering */); - return false; + dismissMoreKeysPanel(); + cancelLongPress(); + return true; } - @Override - public boolean onFling(final MotionEvent e1, final MotionEvent e2, final float velocityX, - final float velocityY) { - releaseCurrentKey(false /* withKeyRegistering */); - return false; - } + 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 isShowingMoreKeysPanel = isShowingMoreKeysPanel(); - @Override - public void onLongPress(final MotionEvent e) { - // Long press detection of {@link #mGestureDetector} is disabled and not used. + // Touched key has changed, release previous key's callbacks and + // re-register them for the new key. + if (key != mCurrentKey && !isShowingMoreKeysPanel) { + releaseCurrentKey(false /* withKeyRegistering */); + mCurrentKey = key; + if (key == null) { + return false; + } + registerPress(key); + + cancelLongPress(); + registerLongPress(key); + } + + if (isShowingMoreKeysPanel) { + final long eventTime = e.getEventTime(); + final int translatedX = mMoreKeysPanel.translateX(x); + final int translatedY = mMoreKeysPanel.translateY(y); + mMoreKeysPanel.onMoveEvent(translatedX, translatedY, mPointerId, eventTime); + } + + mLastX = x; + mLastY = y; + return true; } } diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/EmojiPalettesAdapter.java b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/EmojiPalettesAdapter.java index 09cdc9674..c34b3c7a7 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/EmojiPalettesAdapter.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/EmojiPalettesAdapter.java @@ -34,14 +34,14 @@ final class EmojiPalettesAdapter extends RecyclerView.Adapter mActiveKeyboardViews = new SparseArray<>(); private final EmojiCategory mEmojiCategory; private int mActivePosition = 0; public EmojiPalettesAdapter(final EmojiCategory emojiCategory, - final EmojiPageKeyboardView.OnKeyEventListener listener) { + final OnKeyEventListener listener) { mEmojiCategory = emojiCategory; mListener = listener; mRecentsKeyboard = mEmojiCategory.getKeyboard(EmojiCategory.ID_RECENTS, 0); diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/EmojiPalettesView.java b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/EmojiPalettesView.java index 4bdb5f0d2..342a8ea05 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/EmojiPalettesView.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/EmojiPalettesView.java @@ -66,7 +66,7 @@ import static org.dslul.openboard.inputmethod.latin.common.Constants.NOT_A_COORD */ public final class EmojiPalettesView extends LinearLayout implements OnTabChangeListener, View.OnClickListener, View.OnTouchListener, - EmojiPageKeyboardView.OnKeyEventListener{ + OnKeyEventListener{ private final int mFunctionalKeyBackgroundId; private final int mSpacebarBackgroundId; private final boolean mCategoryIndicatorEnabled; @@ -325,7 +325,7 @@ public final class EmojiPalettesView extends LinearLayout /** * Called from {@link EmojiPageKeyboardView} through - * {@link org.dslul.openboard.inputmethod.keyboard.emoji.EmojiPageKeyboardView.OnKeyEventListener} + * {@link org.dslul.openboard.inputmethod.keyboard.emoji.OnKeyEventListener} * interface to handle touch events from non-View-based elements such as Emoji buttons. */ @Override @@ -336,8 +336,9 @@ public final class EmojiPalettesView extends LinearLayout /** * Called from {@link EmojiPageKeyboardView} through - * {@link org.dslul.openboard.inputmethod.keyboard.emoji.EmojiPageKeyboardView.OnKeyEventListener} + * {@link org.dslul.openboard.inputmethod.keyboard.emoji.OnKeyEventListener} * interface to handle touch events from non-View-based elements such as Emoji buttons. + * This may be called without any prior call to {@link OnKeyEventListener#onPressKey(Key)}. */ @Override public void onReleaseKey(final Key key) { diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/OnKeyEventListener.java b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/OnKeyEventListener.java new file mode 100644 index 000000000..3a7b8f523 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/OnKeyEventListener.java @@ -0,0 +1,22 @@ +package org.dslul.openboard.inputmethod.keyboard.emoji; + +import org.dslul.openboard.inputmethod.keyboard.Key; + +/** + * Interface to handle touch events from non-View-based elements + * such as Emoji buttons. + */ +public interface OnKeyEventListener { + + /** + * Called when a key is pressed by the user + */ + void onPressKey(Key key); + + /** + * Called when a key is released. + * This may be called without any prior call to {@link OnKeyEventListener#onPressKey(Key)}, + * for example when a key from a more keys keyboard is selected by releasing touch on it. + */ + void onReleaseKey(Key key); +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/KeyboardBuilder.java b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/KeyboardBuilder.java index 6c2437c6d..bf5ed81ec 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/KeyboardBuilder.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/KeyboardBuilder.java @@ -403,6 +403,8 @@ public class KeyboardBuilder { R.styleable.Keyboard_GridRows_codesArray, 0); final int textsArrayId = gridRowAttr.getResourceId( R.styleable.Keyboard_GridRows_textsArray, 0); + final int moreCodesArrayId = gridRowAttr.getResourceId( + R.styleable.Keyboard_GridRows_moreCodesArray, 0); gridRowAttr.recycle(); if (codesArrayId == 0 && textsArrayId == 0) { throw new XmlParseUtils.ParseException( @@ -412,9 +414,19 @@ public class KeyboardBuilder { throw new XmlParseUtils.ParseException( "Both codesArray and textsArray attributes specifed", parser); } + if (textsArrayId != 0 && moreCodesArrayId != 0) { + throw new XmlParseUtils.ParseException( + "moreCodesArray is not compatible with textsArray", parser); + } final String[] array = mResources.getStringArray( codesArrayId != 0 ? codesArrayId : textsArrayId); + final String[] arrayMore = moreCodesArrayId != 0 ? + mResources.getStringArray(moreCodesArrayId) : null; final int counts = array.length; + if (arrayMore != null && counts != arrayMore.length) { + throw new XmlParseUtils.ParseException( + "Inconsistent array size between codesArray and moreKeysArray", parser); + } final float keyWidth = gridRows.getKeyWidth(null, 0.0f); final int numColumns = (int)(mParams.mOccupiedWidth / keyWidth); for (int index = 0; index < counts; index += numColumns) { @@ -429,6 +441,7 @@ public class KeyboardBuilder { final int code; final String outputText; final int supportedMinSdkVersion; + final String moreKeySpecs; if (codesArrayId != 0) { final String codeArraySpec = array[i]; label = CodesArrayParser.parseLabel(codeArraySpec); @@ -436,6 +449,8 @@ public class KeyboardBuilder { outputText = CodesArrayParser.parseOutputText(codeArraySpec); supportedMinSdkVersion = CodesArrayParser.getMinSupportSdkVersion(codeArraySpec); + moreKeySpecs = MoreCodesArrayParser.parseKeySpecs( + arrayMore != null ? arrayMore[i] : null); } else { final String textArraySpec = array[i]; // TODO: Utilize KeySpecParser or write more generic TextsArrayParser. @@ -443,6 +458,7 @@ public class KeyboardBuilder { code = Constants.CODE_OUTPUT_TEXT; outputText = textArraySpec + (char)Constants.CODE_SPACE; supportedMinSdkVersion = 0; + moreKeySpecs = null; } if (Build.VERSION.SDK_INT < supportedMinSdkVersion) { continue; @@ -454,9 +470,10 @@ public class KeyboardBuilder { final int y = row.getKeyY(); final int width = (int)keyWidth; final int height = row.getRowHeight(); - final Key key = new Key(label, KeyboardIconsSet.ICON_UNDEFINED, code, outputText, - null /* hintLabel */, labelFlags, backgroundType, x, y, width, height, - mParams.mHorizontalGap, mParams.mVerticalGap); + final String hintLabel = moreKeySpecs != null ? "\u25E5" : null; + final KeyboardParams params = mParams; + final Key key = new Key(label, code, outputText, hintLabel, moreKeySpecs, + labelFlags, backgroundType, x, y, width, height, params); endKey(key); row.advanceXPos(keyWidth); } diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/MoreCodesArrayParser.java b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/MoreCodesArrayParser.java new file mode 100644 index 000000000..155991a40 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/MoreCodesArrayParser.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dslul.openboard.inputmethod.keyboard.internal; + +import org.dslul.openboard.inputmethod.latin.common.StringUtils; + +import javax.annotation.Nullable; + +/** + * The string parser of moreCodesArray specification for . The attribute moreCodesArray is an + * array of string. + * The more codes array specification is semicolon separated "codes array specification" each of which represents one + * "more key". + * Each element of the array defines a sequence of key labels specified as hexadecimal strings + * representing code points separated by a vertical bar. + * + */ +public final class MoreCodesArrayParser { + // Constants for parsing. + private static final char SEMICOLON = ';'; + private static final String SEMICOLON_REGEX = StringUtils.newSingleCodePointString(SEMICOLON); + + private MoreCodesArrayParser() { + // This utility class is not publicly instantiable. + } + + public static String parseKeySpecs(@Nullable String codeArraySpecs) { + if (codeArraySpecs == null) { + return null; + } + + final StringBuilder sb = new StringBuilder(); + for (final String codeArraySpec : codeArraySpecs.split(SEMICOLON_REGEX)) { + final String label = CodesArrayParser.parseLabel(codeArraySpec); + final String outputText = CodesArrayParser.parseOutputText(codeArraySpec); + + sb.append(label).append("|").append(outputText); + sb.append(","); + } + + // Remove last comma + if (sb.length() > 0) sb.deleteCharAt(sb.length() - 1); + + return sb.toString(); + } +} diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index cfa27898e..6c7e165df 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -291,6 +291,7 @@ +