Compare commits

..

40 commits

Author SHA1 Message Date
Eran Leshem
d2217f099a
Export DictionaryPackInstallBroadcastReceiver (#1756) 2025-06-29 20:02:29 +02:00
Eran Leshem
4f58e5d013
Support Latin (#1749) 2025-06-28 16:25:15 +02:00
Helium314
10a5eab3bc don't change insets when view is hidden by hardware keyboard
seems to happen only in some apps, e.g. simplemobiletools notes
issue only occurs when keyboard is suppressed after it has been shown and a new input view is created

fixes GH-1455
fixes GH-702
2025-06-28 16:20:54 +02:00
Helium314
8d876a15f0 fix crash in isShowingKeyboardId with hardware keyboard 2025-06-28 15:16:25 +02:00
Helium314
945a700b71 do the correct checks when loading blacklist file
fixes GH-1475
2025-06-28 14:11:00 +02:00
Helium314
53a899794e register OnSharedPreferenceChangeListener in LatinIME.onCreate
because it's unregistered in onDestroy
(registering twice is not an issue)

though maybe not unregistering would be more correct, as it's registered in app.onCreate?

fixes GH-1670
2025-06-24 06:27:51 +02:00
Eran Leshem
c2068224a0
Change settings search keyboard action to Search. (#1734) 2025-06-22 12:23:46 +02:00
Eran Leshem
9193c95c2b
Ignore spacer columns when calculating emoji page size (#1721) 2025-06-22 12:18:37 +02:00
Helium314
76ebf99921 deal with Android refusing to add word to user dictionary
fixes GH-1735
2025-06-22 12:11:20 +02:00
Helium314
77a728e390 don't pre-select unavailable locale when adding dictionary 2025-06-19 09:35:08 +02:00
Helium314
7b0c511857 add logging when hiding window
may help with GH-1710
2025-06-16 19:43:32 +02:00
Eran Leshem
e062efb3d4
Always default popup VisibleOffset to keyboard.mVerticalGap (#1722) 2025-06-16 19:14:47 +02:00
lurebat
d356f9f54b
Add broadcast intent keys (#1675)
This commit introduces three new keycodes: SEND_INTENT_ONE, SEND_INTENT_TWO, and SEND_INTENT_THREE.

When these keys are pressed, a broadcast intent is sent with the action `helium314.keyboard.latin.ACTION_SEND_INTENT`. The intent includes an extra `EXTRA_NUMBER` (integer) indicating which of the three keys was pressed (1, 2, or 3).

This functionality allows external applications to react to these specific key presses.
2025-06-16 18:55:15 +02:00
Helium314
9549389be7 use different method for hiding keyboard
fixes GH-1719
2025-06-15 21:09:53 +02:00
Helium314
2dc838798d use hasLabels for TLD popups
and add documentation
2025-06-15 19:49:32 +02:00
Helium314
24a2eddc1f avoid StringIndexOutOfBoundsException when initializing stringbuilder from other stringbuilder 2025-06-15 17:43:06 +02:00
Helium314
ef3191a2eb prepare for selecting text with cursor movement keys when shift is enabled manually
not enabled, as it's not working reliably
2025-06-15 17:41:05 +02:00
Helium314
e430d13c4a improvements to hardware keyboard handling 2025-06-14 12:53:46 +02:00
Helium314
9c97a6b9bf move some key press logic from LatinIme into KeyboardActionListener 2025-06-14 12:16:29 +02:00
Helium314
a37de668c0 enable hardware keyboard handling again, with improved handling 2025-06-14 11:29:26 +02:00
Helium314
63dad1549e redirect suggestion strip view code input to KeyboardActionListener 2025-06-13 20:57:49 +02:00
Helium314
f06a553d2c make sure selection start is before end
this should not be necessary according to documentation, but apparently there are apps (even by Google!) that can't deal with documented behavior
see comments in GH-1512
2025-06-13 20:49:53 +02:00
Eran Leshem
e9e3bdac17
Display emoji descriptions in popups (#1542) 2025-06-11 22:17:26 +02:00
Eran Leshem
d5cd18ecaa
Remove regional indicator symbol letters from emoji list (#1680) 2025-06-11 22:17:05 +02:00
Eran Leshem
871ac110ad
Move non-com global TLDs to end of list (#1668) 2025-06-11 22:16:39 +02:00
Helium314
49c9d77978 try more aggressive proguard settings 2025-06-11 22:15:49 +02:00
Eran Leshem
79726f1a9d
Fix onInlineSuggestionsResponse crash (#1700) 2025-06-11 21:59:36 +02:00
Helium314
f2ec441f45 update version and translations 2025-06-11 21:57:06 +02:00
Eran Leshem
62f82d15cf
Show incognito button in all toolbar modes (#1681) 2025-06-11 21:43:04 +02:00
Eran Leshem
f8d3795302
Disable emoji view animation according to system setting (#1696) 2025-06-11 21:29:06 +02:00
Helium314
9cec401e1e don't add single letter words with AddToPersonalDictionary setting
fixes GH-1605
2025-06-11 20:01:59 +02:00
Helium314
83ff9b3345 bring up keyboard when adding a new word in personal dictionary screen
fixes GH-1663
2025-06-11 19:43:42 +02:00
Eran Leshem
8ae241b032
Only show some suggestion settings if toolbar is in compatible mode. (#1693) 2025-06-11 17:24:40 +02:00
Helium314
80ba394b95 move some code from RichInputConnection to StringUtils
so we can easily add unit tests
and maybe improve the awkward behavior
2025-06-09 20:20:27 +02:00
Helium314
52744b7427 full reload after changing icon style
should fix GH-1686
2025-06-09 16:05:41 +02:00
Helium314
af5c41c83c add NO_LANGUAGE inside remember
should fix GH-1678
2025-06-08 21:42:33 +02:00
Helium314
e21168b1d3 merge labelFlags when when replacing comma & period
so e.g. dvorak z key doesn't get .com popup hint by default
2025-06-08 11:40:38 +02:00
Helium314
11f45a6209 show PREF_EMOJI_KEY_FIT setting only if PREF_EMOJI_FONT_SCALE is not 100% 2025-06-08 11:26:23 +02:00
Helium314
c8322dd4a2 make KeyCode.IME_HIDE_UI work
fixes GH-1272
2025-06-08 11:16:00 +02:00
Helium314
896e207c5f remove the fallback to original key type when replacing comma & period
relevant e.g. for dvorak in url fields, where otherwise the q has functional key background
2025-06-08 11:07:51 +02:00
71 changed files with 1139 additions and 600 deletions

View file

@ -12,13 +12,13 @@ android {
applicationId = "helium314.keyboard"
minSdk = 21
targetSdk = 35
versionCode = 3200
versionName = "3.2-beta1"
versionCode = 3201
versionName = "3.2"
ndk {
abiFilters.clear()
abiFilters.addAll(listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64"))
}
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
buildTypes {

View file

@ -136,32 +136,6 @@
®️
™️
🫟
🇦
🇧
🇨
🇩
🇪
🇫
🇬
🇭
🇮
🇯
🇰
🇱
🇲
🇳
🇴
🇵
🇶
🇷
🇸
🇹
🇺
🇻
🇼
🇽
🇾
🇿
#️⃣
*️⃣
0

View file

@ -139,6 +139,16 @@ class Event private constructor(
null, if (isKeyRepeat) FLAG_REPEAT else FLAG_NONE, null)
}
// A helper method to split the code point and the key code.
// todo: Ultimately, they should not be squashed into the same variable, and this method should be removed.
@JvmStatic
fun createSoftwareKeypressEvent(keyCodeOrCodePoint: Int, metaState: Int, keyX: Int, keyY: Int, isKeyRepeat: Boolean) =
if (keyCodeOrCodePoint <= 0) {
createSoftwareKeypressEvent(NOT_A_CODE_POINT, keyCodeOrCodePoint, metaState, keyX, keyY, isKeyRepeat)
} else {
createSoftwareKeypressEvent(keyCodeOrCodePoint, NOT_A_KEY_CODE, metaState, keyX, keyY, isKeyRepeat)
}
fun createHardwareKeypressEvent(codePoint: Int, keyCode: Int, metaState: Int, next: Event?, isKeyRepeat: Boolean): Event {
return Event(EVENT_TYPE_INPUT_KEYPRESS, null, codePoint, keyCode, metaState,
Constants.EXTERNAL_KEYBOARD_COORDINATE, Constants.EXTERNAL_KEYBOARD_COORDINATE,
@ -256,11 +266,9 @@ class Event private constructor(
source.mX, source.mY, source.mSuggestedWordInfo, source.mFlags or FLAG_COMBINING, source.mNextEvent)
}
fun createNotHandledEvent(): Event {
return Event(EVENT_TYPE_NOT_HANDLED, null, NOT_A_CODE_POINT, NOT_A_KEY_CODE, 0,
val notHandledEvent = Event(EVENT_TYPE_NOT_HANDLED, null, NOT_A_CODE_POINT, NOT_A_KEY_CODE, 0,
Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, null, FLAG_NONE, null)
}
}
// This method is private - to create a new event, use one of the create* utility methods.
init {

View file

@ -24,7 +24,8 @@ class HardwareKeyboardEventDecoder(val mDeviceId: Int) : HardwareEventDecoder {
// KeyEvent#getUnicodeChar() does not exactly returns a unicode char, but rather a value
// that includes both the unicode char in the lower 21 bits and flags in the upper bits,
// hence the name "codePointAndFlags". {@see KeyEvent#getUnicodeChar()} for more info.
val codePointAndFlags = keyEvent.unicodeChar
val codePointAndFlags = keyEvent.unicodeChar.takeIf { it != 0 }
?: Event.NOT_A_CODE_POINT // KeyEvent has 0 if no codePoint, but that's actually valid so we convert it to -1
// The keyCode is the abstraction used by the KeyEvent to represent different keys that
// do not necessarily map to a unicode character. This represents a physical key, like
// the key for 'A' or Space, but also Backspace or Ctrl or Caps Lock.
@ -48,6 +49,21 @@ class HardwareKeyboardEventDecoder(val mDeviceId: Int) : HardwareEventDecoder {
} else Event.createHardwareKeypressEvent(codePointAndFlags, keyCode, metaState, null, isKeyRepeat)
// If not Enter, then this is just a regular keypress event for a normal character
// that can be committed right away, taking into account the current state.
} else Event.createNotHandledEvent()
} else if (isDpadDirection(keyCode)) {
Event.createHardwareKeypressEvent(codePointAndFlags, keyCode, metaState, null, isKeyRepeat)
// } else if (KeyEvent.isModifierKey(keyCode)) {
// todo: we could synchronize meta state across HW and SW keyboard, but that's more work for little benefit (especially with shift & caps lock)
} else {
Event.notHandledEvent
}
}
companion object {
private fun isDpadDirection(keyCode: Int) = when (keyCode) {
KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_DPAD_DOWN, KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_DPAD_RIGHT,
KeyEvent.KEYCODE_DPAD_DOWN_LEFT, KeyEvent.KEYCODE_DPAD_DOWN_RIGHT, KeyEvent.KEYCODE_DPAD_UP_RIGHT,
KeyEvent.KEYCODE_DPAD_UP_LEFT -> true
else -> false
}
}
}

View file

@ -6,6 +6,8 @@
package helium314.keyboard.keyboard;
import android.view.KeyEvent;
import helium314.keyboard.latin.common.Constants;
import helium314.keyboard.latin.common.InputPointers;
@ -31,6 +33,12 @@ public interface KeyboardActionListener {
*/
void onReleaseKey(int primaryCode, boolean withSliding);
/** For handling hardware key presses. Returns whether the event was handled. */
boolean onKeyDown(int keyCode, KeyEvent keyEvent);
/** For handling hardware key presses. Returns whether the event was handled. */
boolean onKeyUp(int keyCode, KeyEvent keyEvent);
/**
* Send a key code to the listener.
*
@ -117,6 +125,10 @@ public interface KeyboardActionListener {
@Override
public void onReleaseKey(int primaryCode, boolean withSliding) {}
@Override
public boolean onKeyDown(int keyCode, KeyEvent keyEvent) { return false; }
@Override
public boolean onKeyUp(int keyCode, KeyEvent keyEvent) { return false; }
@Override
public void onCodeInput(int primaryCode, int x, int y, boolean isKeyRepeat) {}
@Override
public void onTextInput(String text) {}

View file

@ -1,16 +1,24 @@
package helium314.keyboard.keyboard
import android.text.InputType
import android.util.SparseArray
import android.view.KeyEvent
import android.view.inputmethod.InputMethodSubtype
import helium314.keyboard.event.Event
import helium314.keyboard.event.HangulEventDecoder
import helium314.keyboard.event.HardwareEventDecoder
import helium314.keyboard.event.HardwareKeyboardEventDecoder
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode
import helium314.keyboard.latin.EmojiAltPhysicalKeyDetector
import helium314.keyboard.latin.LatinIME
import helium314.keyboard.latin.RichInputMethodManager
import helium314.keyboard.latin.common.Constants
import helium314.keyboard.latin.common.InputPointers
import helium314.keyboard.latin.common.StringUtils
import helium314.keyboard.latin.common.combiningRange
import helium314.keyboard.latin.common.loopOverCodePoints
import helium314.keyboard.latin.common.loopOverCodePointsBackwards
import helium314.keyboard.latin.define.ProductionFlags
import helium314.keyboard.latin.inputlogic.InputLogic
import helium314.keyboard.latin.settings.Settings
import kotlin.math.abs
@ -19,6 +27,11 @@ import kotlin.math.min
class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inputLogic: InputLogic) : KeyboardActionListener {
private val connection = inputLogic.mConnection
private val emojiAltPhysicalKeyDetector by lazy { EmojiAltPhysicalKeyDetector(latinIME.resources) }
// We expect to have only one decoder in almost all cases, hence the default capacity of 1.
// If it turns out we need several, it will get grown seamlessly.
private val hardwareEventDecoders: SparseArray<HardwareEventDecoder> = SparseArray(1)
private val keyboardSwitcher = KeyboardSwitcher.getInstance()
private val settings = Settings.getInstance()
@ -58,9 +71,62 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp
keyboardSwitcher.onReleaseKey(primaryCode, withSliding, latinIME.currentAutoCapsState, latinIME.currentRecapitalizeState)
}
override fun onKeyUp(keyCode: Int, keyEvent: KeyEvent): Boolean {
emojiAltPhysicalKeyDetector.onKeyUp(keyEvent)
if (!ProductionFlags.IS_HARDWARE_KEYBOARD_SUPPORTED)
return false
val keyIdentifier = keyEvent.deviceId.toLong() shl 32 + keyEvent.keyCode
return inputLogic.mCurrentlyPressedHardwareKeys.remove(keyIdentifier)
}
override fun onKeyDown(keyCode: Int, keyEvent: KeyEvent): Boolean {
emojiAltPhysicalKeyDetector.onKeyDown(keyEvent)
if (!ProductionFlags.IS_HARDWARE_KEYBOARD_SUPPORTED)
return false
val event: Event
if (settings.current.mLocale.language == "ko") { // todo: this does not appear to be the right place
val subtype = keyboardSwitcher.keyboard?.mId?.mSubtype ?: RichInputMethodManager.getInstance().currentSubtype
event = HangulEventDecoder.decodeHardwareKeyEvent(subtype, keyEvent) {
getHardwareKeyEventDecoder(keyEvent.deviceId).decodeHardwareKey(keyEvent)
}
} else {
event = getHardwareKeyEventDecoder(keyEvent.deviceId).decodeHardwareKey(keyEvent)
}
if (event.isHandled) {
inputLogic.onCodeInput(
settings.current, event,
keyboardSwitcher.getKeyboardShiftMode(), // TODO: this is not necessarily correct for a hardware keyboard right now
keyboardSwitcher.getCurrentKeyboardScript(),
latinIME.mHandler
)
return true
}
return false
}
override fun onCodeInput(primaryCode: Int, x: Int, y: Int, isKeyRepeat: Boolean) {
when (primaryCode) {
KeyCode.TOGGLE_AUTOCORRECT -> return Settings.getInstance().toggleAutoCorrect()
KeyCode.TOGGLE_INCOGNITO_MODE -> return Settings.getInstance().toggleAlwaysIncognitoMode()
}
val mkv = keyboardSwitcher.mainKeyboardView
latinIME.onCodeInput(primaryCode, metaState, mkv.getKeyX(x), mkv.getKeyY(y), isKeyRepeat)
// checking if the character is a combining accent
val event = if (primaryCode in combiningRange) { // todo: should this be done later, maybe in inputLogic?
Event.createSoftwareDeadEvent(primaryCode, 0, metaState, mkv.getKeyX(x), mkv.getKeyY(y), null)
} else {
// todo:
// setting meta shift should only be done for arrow and similar cursor movement keys
// should only be enabled once it works more reliably (currently depends on app for some reason)
// if (mkv.keyboard?.mId?.isAlphabetShiftedManually == true)
// Event.createSoftwareKeypressEvent(primaryCode, metaState or KeyEvent.META_SHIFT_ON, mkv.getKeyX(x), mkv.getKeyY(y), isKeyRepeat)
// else Event.createSoftwareKeypressEvent(primaryCode, metaState, mkv.getKeyX(x), mkv.getKeyY(y), isKeyRepeat)
Event.createSoftwareKeypressEvent(primaryCode, metaState, mkv.getKeyX(x), mkv.getKeyY(y), isKeyRepeat)
}
latinIME.onEvent(event)
}
override fun onTextInput(text: String?) = latinIME.onTextInput(text)
@ -257,4 +323,13 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp
}
return -min(-actualSteps, text.length)
}
private fun getHardwareKeyEventDecoder(deviceId: Int): HardwareEventDecoder {
hardwareEventDecoders.get(deviceId)?.let { return it }
// TODO: create the decoder according to the specification
val newDecoder = HardwareKeyboardEventDecoder(deviceId)
hardwareEventDecoders.put(deviceId, newDecoder)
return newDecoder
}
}

View file

@ -184,6 +184,11 @@ public final class KeyboardId {
|| mElementId == ELEMENT_ALPHABET_AUTOMATIC_SHIFTED || mElementId == ELEMENT_ALPHABET_MANUAL_SHIFTED;
}
public boolean isAlphabetShiftedManually() {
return mElementId == ELEMENT_ALPHABET_SHIFT_LOCKED || mElementId == ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED
|| mElementId == ELEMENT_ALPHABET_MANUAL_SHIFTED;
}
public boolean isNumberLayout() {
return mElementId == ELEMENT_NUMBER || mElementId == ELEMENT_NUMPAD
|| mElementId == ELEMENT_PHONE || mElementId == ELEMENT_PHONE_SYMBOLS;

View file

@ -138,7 +138,7 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
return false;
}
public void loadKeyboard(final EditorInfo editorInfo, final SettingsValues settingsValues,
private void loadKeyboard(final EditorInfo editorInfo, final SettingsValues settingsValues,
final int currentAutoCapsState, final int currentRecapitalizeState) {
final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder(
mThemeContext, editorInfo);
@ -527,6 +527,10 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
if (mCurrentInputView == null)
return;
mEmojiPalettesView.clearKeyboardCache();
reloadMainKeyboard();
}
public void reloadMainKeyboard() {
loadKeyboard(mLatinIME.getCurrentInputEditorInfo(), Settings.getValues(),
mLatinIME.getCurrentAutoCapsState(), mLatinIME.getCurrentRecapitalizeState());
}
@ -597,7 +601,10 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
if (mKeyboardView == null || !mKeyboardView.isShown()) {
return false;
}
int activeKeyboardId = mKeyboardView.getKeyboard().mId.mElementId;
final Keyboard keyboard = mKeyboardView.getKeyboard();
if (keyboard == null) // may happen when using hardware keyboard
return false;
int activeKeyboardId = keyboard.mId.mElementId;
for (int keyboardId : keyboardIds) {
if (activeKeyboardId == keyboardId) {
return true;

View file

@ -360,25 +360,21 @@ public final class MainKeyboardView extends KeyboardView implements DrawingProxy
public void onKeyPressed(@NonNull final Key key, final boolean withPreview) {
key.onPressed();
invalidateKey(key);
if (withPreview && !key.noKeyPreview()) {
final Keyboard keyboard = getKeyboard();
if (keyboard == null) {
return;
}
mKeyPreviewDrawParams.setVisibleOffset(-keyboard.mVerticalGap);
if (withPreview && !key.noKeyPreview() && mKeyPreviewDrawParams.isPopupEnabled()) {
showKeyPreview(key);
}
}
private void showKeyPreview(@NonNull final Key key) {
final Keyboard keyboard = getKeyboard();
if (keyboard == null) {
return;
}
final KeyPreviewDrawParams previewParams = mKeyPreviewDrawParams;
if (!previewParams.isPopupEnabled()) {
previewParams.setVisibleOffset(-keyboard.mVerticalGap);
return;
}
locatePreviewPlacerView();
getLocationInWindow(mOriginCoords);
mKeyPreviewChoreographer.placeAndShowKeyPreview(key, keyboard.mIconsSet, getKeyDrawParams(),
mKeyPreviewChoreographer.placeAndShowKeyPreview(key, getKeyboard().mIconsSet, getKeyDrawParams(),
KeyboardSwitcher.getInstance().getWrapperView().getWidth(), mOriginCoords, mDrawingPreviewPlacerView);
}

View file

@ -12,15 +12,15 @@ import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import helium314.keyboard.accessibility.AccessibilityUtils;
import helium314.keyboard.accessibility.PopupKeysKeyboardAccessibilityDelegate;
import helium314.keyboard.keyboard.emoji.OnKeyEventListener;
import helium314.keyboard.keyboard.emoji.EmojiViewCallback;
import helium314.keyboard.keyboard.internal.KeyDrawParams;
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode;
import helium314.keyboard.latin.R;
@ -39,7 +39,7 @@ public class PopupKeysKeyboardView extends KeyboardView implements PopupKeysPane
protected final KeyDetector mKeyDetector;
private Controller mController = EMPTY_CONTROLLER;
protected KeyboardActionListener mListener;
protected OnKeyEventListener mKeyEventListener;
protected EmojiViewCallback mEmojiViewCallback;
private int mOriginX;
private int mOriginY;
private Key mCurrentKey;
@ -122,7 +122,7 @@ public class PopupKeysKeyboardView extends KeyboardView implements PopupKeysPane
public void showPopupKeysPanel(final View parentView, final Controller controller,
final int pointX, final int pointY, final KeyboardActionListener listener) {
mListener = listener;
mKeyEventListener = null;
mEmojiViewCallback = null;
showPopupKeysPanelInternal(parentView, controller, pointX, pointY);
}
@ -131,9 +131,9 @@ public class PopupKeysKeyboardView extends KeyboardView implements PopupKeysPane
*/
@Override
public void showPopupKeysPanel(final View parentView, final Controller controller,
final int pointX, final int pointY, final OnKeyEventListener listener) {
final int pointX, final int pointY, final EmojiViewCallback emojiViewCallback) {
mListener = null;
mKeyEventListener = listener;
mEmojiViewCallback = emojiViewCallback;
showPopupKeysPanelInternal(parentView, controller, pointX, pointY);
}
@ -157,6 +157,9 @@ public class PopupKeysKeyboardView extends KeyboardView implements PopupKeysPane
mOriginX = x + container.getPaddingLeft();
mOriginY = y + container.getPaddingTop();
var center = panelX + getMeasuredWidth() / 2;
// This is needed for cases where there's also a long text popup above this keyboard
controller.setLayoutGravity(center < pointX? Gravity.RIGHT : center > pointX? Gravity.LEFT : Gravity.CENTER_HORIZONTAL);
controller.onShowPopupKeysPanel(this);
final PopupKeysKeyboardAccessibilityDelegate accessibilityDelegate = mAccessibilityDelegate;
if (accessibilityDelegate != null
@ -222,8 +225,8 @@ public class PopupKeysKeyboardView extends KeyboardView implements PopupKeysPane
false /* isKeyRepeat */);
}
}
} else if (mKeyEventListener != null) {
mKeyEventListener.onReleaseKey(key);
} else if (mEmojiViewCallback != null) {
mEmojiViewCallback.onReleaseKey(key);
}
}
@ -314,28 +317,4 @@ public class PopupKeysKeyboardView extends KeyboardView implements PopupKeysPane
}
return super.onHoverEvent(event);
}
private View getContainerView() {
return (View)getParent();
}
@Override
public void showInParent(final ViewGroup parentView) {
removeFromParent();
parentView.addView(getContainerView());
}
@Override
public void removeFromParent() {
final View containerView = getContainerView();
final ViewGroup currentParent = (ViewGroup)containerView.getParent();
if (currentParent != null) {
currentParent.removeView(containerView);
}
}
@Override
public boolean isShowingInParent() {
return (getContainerView().getParent() != null);
}
}

View file

@ -8,10 +8,17 @@ package helium314.keyboard.keyboard;
import android.view.View;
import android.view.ViewGroup;
import helium314.keyboard.keyboard.emoji.OnKeyEventListener;
import helium314.keyboard.keyboard.emoji.EmojiViewCallback;
public interface PopupKeysPanel {
interface Controller {
/**
* Set the layout gravity.
* @param layoutGravity requested by the popup
*/
default void setLayoutGravity(int layoutGravity) {
}
/**
* Add the {@link PopupKeysPanel} to the target view.
* @param panel the panel to be shown.
@ -59,19 +66,18 @@ public interface PopupKeysPanel {
* Initializes the layout and event handling of this {@link PopupKeysPanel} and calls the
* controller's onShowPopupKeysPanel to add the panel's container view.
* Same as {@link PopupKeysPanel#showPopupKeysPanel(View, Controller, int, int, KeyboardActionListener)},
* but with a {@link OnKeyEventListener}.
* but with a {@link EmojiViewCallback}.
*
* @param parentView the parent view of this {@link PopupKeysPanel}
* @param controller the controller that can dismiss this {@link PopupKeysPanel}
* @param pointX x coordinate of this {@link PopupKeysPanel}
* @param pointY y coordinate of this {@link PopupKeysPanel}
* @param listener the listener that will receive keyboard action from this
* {@link PopupKeysPanel}.
* @param emojiViewCallback to receive keyboard actions from this {@link PopupKeysPanel}.
*/
// TODO: Currently the PopupKeysPanel is inside a container view that is added to the parent.
// Consider the simpler approach of placing the PopupKeysPanel itself into the parent view.
void showPopupKeysPanel(View parentView, Controller controller, int pointX,
int pointY, OnKeyEventListener listener);
int pointY, EmojiViewCallback emojiViewCallback);
/**
* Dismisses the popup keys panel and calls the controller's onDismissPopupKeysPanel to remove
@ -127,20 +133,35 @@ public interface PopupKeysPanel {
*/
int translateY(int y);
default View getContainerView() {
return (View) ((View) this).getParent();
}
/**
* Show this {@link PopupKeysPanel} in the parent view.
*
* @param parentView the {@link ViewGroup} that hosts this {@link PopupKeysPanel}.
*/
void showInParent(ViewGroup parentView);
default void showInParent(ViewGroup parentView) {
removeFromParent();
parentView.addView(getContainerView());
}
/**
* Remove this {@link PopupKeysPanel} from the parent view.
*/
void removeFromParent();
default void removeFromParent() {
final View containerView = getContainerView();
final ViewGroup currentParent = (ViewGroup)containerView.getParent();
if (currentParent != null) {
currentParent.removeView(containerView);
}
}
/**
* Return whether the panel is currently being shown.
*/
boolean isShowingInParent();
default boolean isShowingInParent() {
return getContainerView().getParent() != null;
}
}

View file

@ -0,0 +1,121 @@
/*
* Copyright (C) 2011 The Android Open Source Project
* modified
* SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
*/
package helium314.keyboard.keyboard;
import android.content.Context;
import android.graphics.Typeface;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.widget.TextView;
import helium314.keyboard.keyboard.emoji.EmojiViewCallback;
import helium314.keyboard.keyboard.internal.KeyDrawParams;
import helium314.keyboard.latin.R;
import helium314.keyboard.latin.common.ColorType;
import helium314.keyboard.latin.common.CoordinateUtils;
import helium314.keyboard.latin.settings.Settings;
/**
* A view that displays popup text.
*/
public class PopupTextView extends TextView implements PopupKeysPanel {
private final int[] mCoordinates = CoordinateUtils.newInstance();
private final Typeface mTypeface;
private Controller mController = EMPTY_CONTROLLER;
private int mOriginX;
private int mOriginY;
private Key mKey;
private EmojiViewCallback mEmojiViewCallback;
public PopupTextView(final Context context, final AttributeSet attrs) {
this(context, attrs, R.attr.popupKeysKeyboardViewStyle);
}
public PopupTextView(final Context context, final AttributeSet attrs,
final int defStyle) {
super(context, attrs, defStyle);
mTypeface = Settings.getInstance().getCustomTypeface();
}
public void setKeyDrawParams(Key key, KeyDrawParams drawParams) {
mKey = key;
Settings.getValues().mColors.setBackground(this, ColorType.KEY_PREVIEW_BACKGROUND);
setTextColor(drawParams.mPreviewTextColor);
setTextSize(TypedValue.COMPLEX_UNIT_PX, key.selectHintTextSize(drawParams) << 1);
setTypeface(mTypeface == null ? key.selectTypeface(drawParams) : mTypeface);
}
@Override
public void showPopupKeysPanel(final View parentView, final Controller controller,
final int pointX, final int pointY, final KeyboardActionListener listener) {
showPopupKeysPanelInternal(parentView, controller, pointX, pointY);
}
@Override
public void showPopupKeysPanel(final View parentView, final Controller controller,
final int pointX, final int pointY, final EmojiViewCallback emojiViewCallback) {
mEmojiViewCallback = emojiViewCallback;
showPopupKeysPanelInternal(parentView, controller, pointX, pointY);
}
private void showPopupKeysPanelInternal(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.
final int x = pointX - getMeasuredWidth() / 2 - container.getPaddingLeft() - getPaddingLeft();
final int y = pointY - container.getMeasuredHeight() + container.getPaddingBottom()
+ getPaddingBottom();
parentView.getLocationInWindow(mCoordinates);
// Ensure the horizontal position of the panel does not extend past the parentView edges.
final int maxX = parentView.getMeasuredWidth() - container.getMeasuredWidth();
final int panelX = Math.max(0, Math.min(maxX, x)) + CoordinateUtils.x(mCoordinates);
final int panelY = y + CoordinateUtils.y(mCoordinates);
container.setX(panelX);
container.setY(panelY);
mOriginX = x + container.getPaddingLeft();
mOriginY = y + container.getPaddingTop();
controller.setLayoutGravity(Gravity.NO_GRAVITY);
controller.onShowPopupKeysPanel(this);
}
@Override
public void onDownEvent(final int x, final int y, final int pointerId, final long eventTime) {
}
@Override
public void onMoveEvent(final int x, final int y, final int pointerId, final long eventTime) {
}
@Override
public void onUpEvent(final int x, final int y, final int pointerId, final long eventTime) {
mEmojiViewCallback.onReleaseKey(mKey);
}
@Override
public void dismissPopupKeysPanel() {
if (!isShowingInParent()) {
return;
}
mController.onDismissPopupKeysPanel();
}
@Override
public int translateX(final int x) {
return x - mOriginX;
}
@Override
public int translateY(final int y) {
return y - mOriginY;
}
}

View file

@ -112,12 +112,12 @@ final class DynamicGridKeyboard extends Keyboard {
}
public int getDynamicOccupiedHeight() {
final int row = (mGridKeys.size() - 1) / mColumnsNum + 1;
final int row = (mGridKeys.size() - 1) / getOccupiedColumnCount() + 1;
return row * mVerticalStep;
}
public int getColumnsCount() {
return mColumnsNum;
public int getOccupiedColumnCount() {
return mColumnsNum - mEmptyColumnIndices.size();
}
public void addPendingKey(final Key usedKey) {

View file

@ -324,7 +324,7 @@ final class EmojiCategory {
final DynamicGridKeyboard tempKeyboard = new DynamicGridKeyboard(mPrefs,
mLayoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS),
0, 0, ResourceUtils.getKeyboardWidth(mContext, Settings.getValues()));
return MAX_LINE_COUNT_PER_PAGE * tempKeyboard.getColumnsCount();
return MAX_LINE_COUNT_PER_PAGE * tempKeyboard.getOccupiedColumnCount();
}
private static final Comparator<Key> EMOJI_KEY_COMPARATOR = (lhs, rhs) -> {

View file

@ -13,6 +13,9 @@ import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.Gravity;
import android.widget.LinearLayout;
import helium314.keyboard.keyboard.PopupTextView;
import helium314.keyboard.latin.utils.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
@ -53,14 +56,18 @@ public final class EmojiPageKeyboardView extends KeyboardView implements
private static final long KEY_PRESS_DELAY_TIME = 250; // msec
private static final long KEY_RELEASE_DELAY_TIME = 30; // msec
private static final OnKeyEventListener EMPTY_LISTENER = new OnKeyEventListener() {
private static final EmojiViewCallback EMPTY_EMOJI_VIEW_CALLBACK = new EmojiViewCallback() {
@Override
public void onPressKey(final Key key) {}
@Override
public void onReleaseKey(final Key key) {}
@Override
public String getDescription(String emoji) {
return null;
}
};
private OnKeyEventListener mListener = EMPTY_LISTENER;
private EmojiViewCallback mEmojiViewCallback = EMPTY_EMOJI_VIEW_CALLBACK;
private final KeyDetector mKeyDetector = new KeyDetector();
private KeyboardAccessibilityDelegate<EmojiPageKeyboardView> mAccessibilityDelegate;
@ -74,6 +81,8 @@ public final class EmojiPageKeyboardView extends KeyboardView implements
// More keys keyboard
private final View mPopupKeysKeyboardContainer;
private final PopupTextView mDescriptionView;
private final PopupKeysKeyboardView mPopupKeysKeyboardView;
private final WeakHashMap<Key, Keyboard> mPopupKeysKeyboardCache = new WeakHashMap<>();
private final boolean mConfigShowPopupKeysKeyboardAtTouchedPoint;
private final ViewGroup mPopupKeysPlacerView;
@ -102,6 +111,8 @@ public final class EmojiPageKeyboardView extends KeyboardView implements
final LayoutInflater inflater = LayoutInflater.from(getContext());
mPopupKeysKeyboardContainer = inflater.inflate(popupKeysKeyboardLayoutId, null);
mDescriptionView = mPopupKeysKeyboardContainer.findViewById(R.id.description_view);
mPopupKeysKeyboardView = mPopupKeysKeyboardContainer.findViewById(R.id.popup_keys_keyboard_view);
}
@Override
@ -146,8 +157,8 @@ public final class EmojiPageKeyboardView extends KeyboardView implements
}
}
public void setOnKeyEventListener(final OnKeyEventListener listener) {
mListener = listener;
public void setEmojiViewCallback(final EmojiViewCallback emojiViewCallback) {
mEmojiViewCallback = emojiViewCallback;
}
/**
@ -169,7 +180,8 @@ public final class EmojiPageKeyboardView extends KeyboardView implements
}
@Nullable
public PopupKeysPanel showPopupKeysKeyboard(@NonNull final Key key, final int lastX, final int lastY) {
private PopupKeysPanel showPopupKeysKeyboard(@NonNull final Key key) {
mPopupKeysKeyboardView.setVisibility(GONE);
final PopupKeySpec[] popupKeys = key.getPopupKeys();
if (popupKeys == null) {
return null;
@ -182,21 +194,9 @@ public final class EmojiPageKeyboardView extends KeyboardView implements
mPopupKeysKeyboardCache.put(key, popupKeysKeyboard);
}
final View container = mPopupKeysKeyboardContainer;
final PopupKeysKeyboardView popupKeysKeyboardView = container.findViewById(R.id.popup_keys_keyboard_view);
popupKeysKeyboardView.setKeyboard(popupKeysKeyboard);
container.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
final int[] lastCoords = CoordinateUtils.newCoordinateArray(1, lastX, lastY);
// The popup keys keyboard is usually horizontally aligned with the center of the parent key.
// If showPopupKeysKeyboardAtTouchedPoint 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 = mConfigShowPopupKeysKeyboardAtTouchedPoint
? CoordinateUtils.x(lastCoords)
: key.getX() + key.getWidth() / 2;
final int pointY = key.getY();
popupKeysKeyboardView.showPopupKeysPanel(this, this, pointX, pointY, mListener);
return popupKeysKeyboardView;
mPopupKeysKeyboardView.setKeyboard(popupKeysKeyboard);
mPopupKeysKeyboardView.setVisibility(VISIBLE);
return mPopupKeysKeyboardView;
}
private void dismissPopupKeysPanel() {
@ -209,6 +209,17 @@ public final class EmojiPageKeyboardView extends KeyboardView implements
return mPopupKeysPanel != null;
}
@Override
public void setLayoutGravity(int layoutGravity) {
var layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
layoutParams.gravity = mDescriptionView.getMeasuredWidth() > mPopupKeysKeyboardView.getMeasuredWidth()?
layoutGravity : Gravity.CENTER_HORIZONTAL;
mPopupKeysKeyboardContainer.setLayoutParams(layoutParams);
mDescriptionView.setLayoutParams(layoutParams);
mPopupKeysKeyboardView.setLayoutParams(layoutParams);
}
@Override
public void onShowPopupKeysPanel(final PopupKeysPanel panel) {
// install placer view only when needed instead of when this
@ -290,9 +301,11 @@ public final class EmojiPageKeyboardView extends KeyboardView implements
return;
}
var descriptionPanel = showDescription(key);
final PopupKeysPanel popupKeysPanel = showPopupKeysKeyboard(key);
final int x = mLastX;
final int y = mLastY;
final PopupKeysPanel popupKeysPanel = showPopupKeysKeyboard(key, x, y);
if (popupKeysPanel != null) {
final int translatedX = popupKeysPanel.translateX(x);
final int translatedY = popupKeysPanel.translateY(y);
@ -301,6 +314,34 @@ public final class EmojiPageKeyboardView extends KeyboardView implements
// want any scroll to append during this entire input.
disallowParentInterceptTouchEvent(true);
}
if (popupKeysPanel != null || descriptionPanel != null) {
mPopupKeysKeyboardContainer.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
final int[] lastCoords = CoordinateUtils.newCoordinateArray(1, x, y);
// The popup keys keyboard is usually horizontally aligned with the center of the parent key.
// If showPopupKeysKeyboardAtTouchedPoint 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 = mConfigShowPopupKeysKeyboardAtTouchedPoint
? CoordinateUtils.x(lastCoords)
: key.getX() + key.getWidth() / 2;
final int pointY = key.getY() - getKeyboard().mVerticalGap;
(popupKeysPanel != null? popupKeysPanel : descriptionPanel)
.showPopupKeysPanel(this, this, pointX, pointY, mEmojiViewCallback);
}
}
private PopupKeysPanel showDescription(Key key) {
mDescriptionView.setVisibility(GONE);
var description = mEmojiViewCallback.getDescription(key.getLabel());
if (description == null) {
return null;
}
mDescriptionView.setText(description);
mDescriptionView.setKeyDrawParams(key, getKeyDrawParams());
mDescriptionView.setVisibility(VISIBLE);
return mDescriptionView;
}
private void registerPress(final Key key) {
@ -318,7 +359,7 @@ public final class EmojiPageKeyboardView extends KeyboardView implements
releasedKey.onReleased();
invalidateKey(releasedKey);
if (withKeyRegistering) {
mListener.onReleaseKey(releasedKey);
mEmojiViewCallback.onReleaseKey(releasedKey);
}
}
@ -326,7 +367,7 @@ public final class EmojiPageKeyboardView extends KeyboardView implements
mPendingKeyDown = null;
pressedKey.onPressed();
invalidateKey(pressedKey);
mListener.onPressKey(pressedKey);
mEmojiViewCallback.onPressKey(pressedKey);
}
public void releaseCurrentKey(final boolean withKeyRegistering) {

View file

@ -7,7 +7,6 @@
package helium314.keyboard.keyboard.emoji;
import helium314.keyboard.latin.utils.Log;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -23,13 +22,13 @@ final class EmojiPalettesAdapter extends RecyclerView.Adapter<EmojiPalettesAdapt
private static final boolean DEBUG_PAGER = false;
private final int mCategoryId;
private final OnKeyEventListener mListener;
private final EmojiViewCallback mEmojiViewCallback;
private final EmojiCategory mEmojiCategory;
public EmojiPalettesAdapter(final EmojiCategory emojiCategory, int categoryId, final OnKeyEventListener listener) {
public EmojiPalettesAdapter(final EmojiCategory emojiCategory, int categoryId, final EmojiViewCallback emojiViewCallback) {
mEmojiCategory = emojiCategory;
mCategoryId = categoryId;
mListener = listener;
mEmojiViewCallback = emojiViewCallback;
}
@NonNull
@ -38,6 +37,7 @@ final class EmojiPalettesAdapter extends RecyclerView.Adapter<EmojiPalettesAdapt
final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
final EmojiPageKeyboardView keyboardView = (EmojiPageKeyboardView)inflater.inflate(
R.layout.emoji_keyboard_page, parent, false);
keyboardView.setEmojiViewCallback(mEmojiViewCallback);
return new ViewHolder(keyboardView);
}
@ -50,7 +50,6 @@ final class EmojiPalettesAdapter extends RecyclerView.Adapter<EmojiPalettesAdapt
final Keyboard keyboard =
mEmojiCategory.getKeyboardFromAdapterPosition(mCategoryId, position);
holder.getKeyboardView().setKeyboard(keyboard);
holder.getKeyboardView().setOnKeyEventListener(mListener);
}
@Override

View file

@ -38,12 +38,16 @@ import helium314.keyboard.keyboard.internal.KeyDrawParams;
import helium314.keyboard.keyboard.internal.KeyVisualAttributes;
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode;
import helium314.keyboard.latin.AudioAndHapticFeedbackManager;
import helium314.keyboard.latin.DictionaryFactory;
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.Colors;
import helium314.keyboard.latin.settings.Settings;
import helium314.keyboard.latin.settings.SettingsValues;
import helium314.keyboard.latin.utils.DictionaryInfoUtils;
import helium314.keyboard.latin.utils.ResourceUtils;
import static helium314.keyboard.latin.common.Constants.NOT_A_COORDINATE;
@ -60,7 +64,7 @@ import static helium314.keyboard.latin.common.Constants.NOT_A_COORDINATE;
* Because of the above reasons, this class doesn't extend {@link KeyboardView}.
*/
public final class EmojiPalettesView extends LinearLayout
implements View.OnClickListener, OnKeyEventListener {
implements View.OnClickListener, EmojiViewCallback {
private static final class PagerViewHolder extends RecyclerView.ViewHolder {
private long mCategoryId;
@ -71,7 +75,7 @@ public final class EmojiPalettesView extends LinearLayout
private final class PagerAdapter extends RecyclerView.Adapter<PagerViewHolder> {
private boolean mInitialized;
private Map<Integer, RecyclerView> mViews = new HashMap<>(mEmojiCategory.getShownCategories().size());
private final Map<Integer, RecyclerView> mViews = new HashMap<>(mEmojiCategory.getShownCategories().size());
private PagerAdapter(ViewPager2 pager) {
setHasStableIds(true);
@ -179,15 +183,14 @@ public final class EmojiPalettesView extends LinearLayout
}
}
private static SingleDictionaryFacilitator sDictionaryFacilitator;
private boolean initialized = false;
private final Colors mColors;
private final EmojiLayoutParams mEmojiLayoutParams;
private LinearLayout mTabStrip;
private EmojiCategoryPageIndicatorView mEmojiCategoryPageIndicatorView;
private KeyboardActionListener mKeyboardActionListener = KeyboardActionListener.EMPTY_LISTENER;
private final EmojiCategory mEmojiCategory;
private ViewPager2 mPager;
@ -254,9 +257,7 @@ public final class EmojiPalettesView extends LinearLayout
mEmojiLayoutParams.setEmojiListProperties(mPager);
mEmojiCategoryPageIndicatorView = findViewById(R.id.emoji_category_page_id_view);
mEmojiLayoutParams.setCategoryPageIdViewProperties(mEmojiCategoryPageIndicatorView);
setCurrentCategoryId(mEmojiCategory.getCurrentCategoryId(), true);
mEmojiCategoryPageIndicatorView.setColors(mColors.get(ColorType.EMOJI_CATEGORY_SELECTED), mColors.get(ColorType.STRIP_BACKGROUND));
initialized = true;
}
@ -280,8 +281,7 @@ public final class EmojiPalettesView extends LinearLayout
}
/**
* Called from {@link EmojiPageKeyboardView} through
* {@link helium314.keyboard.keyboard.emoji.OnKeyEventListener}
* Called from {@link EmojiPageKeyboardView} through {@link EmojiViewCallback}
* interface to handle touch events from non-View-based elements such as Emoji buttons.
*/
@Override
@ -291,10 +291,9 @@ public final class EmojiPalettesView extends LinearLayout
}
/**
* Called from {@link EmojiPageKeyboardView} through
* {@link helium314.keyboard.keyboard.emoji.OnKeyEventListener}
* Called from {@link EmojiPageKeyboardView} through {@link EmojiViewCallback}
* 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)}.
* This may be called without any prior call to {@link EmojiViewCallback#onPressKey(Key)}.
*/
@Override
public void onReleaseKey(final Key key) {
@ -310,6 +309,20 @@ public final class EmojiPalettesView extends LinearLayout
mKeyboardActionListener.onCodeInput(KeyCode.ALPHA, NOT_A_COORDINATE, NOT_A_COORDINATE, false);
}
@Override
public String getDescription(String emoji) {
if (sDictionaryFacilitator == null) {
return null;
}
var wordProperty = sDictionaryFacilitator.getWordProperty(emoji);
if (wordProperty == null || ! wordProperty.mHasShortcuts) {
return null;
}
return wordProperty.mShortcutTargets.get(0).mWord;
}
public void setHardwareAcceleratedDrawingEnabled(final boolean enabled) {
if (!enabled) return;
// TODO: Should use LAYER_TYPE_SOFTWARE when hardware acceleration is off?
@ -324,6 +337,7 @@ public final class EmojiPalettesView extends LinearLayout
final KeyDrawParams params = new KeyDrawParams();
params.updateParams(mEmojiLayoutParams.getBottomRowKeyboardHeight(), keyVisualAttr);
setupSidePadding();
initDictionaryFacilitator();
}
private void addRecentKey(final Key key) {
@ -403,7 +417,7 @@ public final class EmojiPalettesView extends LinearLayout
if (mPager.getScrollState() != ViewPager2.SCROLL_STATE_DRAGGING) {
// Not swiping
mPager.setCurrentItem(mEmojiCategory.getTabIdFromCategoryId(
mEmojiCategory.getCurrentCategoryId()), ! initial);
mEmojiCategory.getCurrentCategoryId()), ! initial && ! isAnimationsDisabled());
}
if (Settings.getValues().mSecondaryStripVisible) {
@ -418,6 +432,11 @@ public final class EmojiPalettesView extends LinearLayout
}
}
private boolean isAnimationsDisabled() {
return android.provider.Settings.Global.getFloat(getContext().getContentResolver(),
android.provider.Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f) == 0.0f;
}
public void clearKeyboardCache() {
if (!initialized) {
return;
@ -425,5 +444,27 @@ public final class EmojiPalettesView extends LinearLayout
mEmojiCategory.clearKeyboardCache();
mPager.getAdapter().notifyDataSetChanged();
closeDictionaryFacilitator();
}
private void initDictionaryFacilitator() {
if (Settings.getValues().mShowEmojiDescriptions) {
var locale = RichInputMethodManager.getInstance().getCurrentSubtype().getLocale();
if (sDictionaryFacilitator == null || ! sDictionaryFacilitator.isForLocale(locale)) {
closeDictionaryFacilitator();
var dictFile = DictionaryInfoUtils.getCachedDictForLocaleAndType(locale, "emoji", getContext());
var dictionary = dictFile != null? DictionaryFactory.getDictionary(dictFile, locale) : null;
sDictionaryFacilitator = dictionary != null? new SingleDictionaryFacilitator(dictionary) : null;
}
} else {
closeDictionaryFacilitator();
}
}
private static void closeDictionaryFacilitator() {
if (sDictionaryFacilitator != null) {
sDictionaryFacilitator.closeDictionaries();
sDictionaryFacilitator = null;
}
}
}

View file

@ -5,10 +5,10 @@ package helium314.keyboard.keyboard.emoji;
import helium314.keyboard.keyboard.Key;
/**
* Interface to handle touch events from non-View-based elements
* such as Emoji buttons.
* Interface to handle callbacks from child elements
* such as Emoji buttons and keyboard views.
*/
public interface OnKeyEventListener {
public interface EmojiViewCallback {
/**
* Called when a key is pressed by the user
@ -17,8 +17,13 @@ public interface OnKeyEventListener {
/**
* Called when a key is released.
* This may be called without any prior call to {@link OnKeyEventListener#onPressKey(Key)},
* This may be called without any prior call to {@link EmojiViewCallback#onPressKey(Key)},
* for example when a key from a popup keys keyboard is selected by releasing touch on it.
*/
void onReleaseKey(Key key);
/**
* Called from keyboard view to get an emoji description
*/
String getDescription(String emoji);
}

View file

@ -229,13 +229,15 @@ class KeyboardParser(private val params: KeyboardParams, private val context: Co
return
// replace comma / period if 2 keys in normal bottom row
if (baseKeys.last().size == 2) {
val newComma = baseKeys.last()[0]
functionalKeysBottom.replaceFirst(
{ it.label == KeyLabel.COMMA || it.groupId == KeyData.GROUP_COMMA},
{ baseKeys.last()[0].copy(newGroupId = 1, newType = baseKeys.last()[0].type ?: it.type) }
{ newComma.copy(newGroupId = 1, newType = newComma.type, newLabelFlags = it.labelFlags or newComma.labelFlags) }
)
val newPeriod = baseKeys.last()[1]
functionalKeysBottom.replaceFirst(
{ it.label == KeyLabel.PERIOD || it.groupId == KeyData.GROUP_PERIOD},
{ baseKeys.last()[1].copy(newGroupId = 2, newType = baseKeys.last()[1].type ?: it.type) }
{ newPeriod.copy(newGroupId = 2, newType = newPeriod.type, newLabelFlags = it.labelFlags or newPeriod.labelFlags) }
)
baseKeys.removeAt(baseKeys.lastIndex)
}

View file

@ -45,7 +45,7 @@ class LocaleKeyboardInfos(dataStream: InputStream?, locale: Locale) {
"mns" -> Key.LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO
else -> 0
}
val tlds = getLocaleTlds(locale)
val tlds = mutableListOf(Key.POPUP_KEYS_HAS_LABELS)
init {
readStream(dataStream, false, true)
@ -84,7 +84,7 @@ class LocaleKeyboardInfos(dataStream: InputStream?, locale: Locale) {
READER_MODE_EXTRA_KEYS -> if (!onlyPopupKeys) addExtraKey(line.split(colonSpaceRegex, 2))
READER_MODE_LABELS -> if (!onlyPopupKeys) addLabel(line.split(colonSpaceRegex, 2))
READER_MODE_NUMBER_ROW -> localizedNumberKeys = line.splitOnWhitespace()
READER_MODE_TLD -> SpacedTokens(line).forEach { tlds.add(".$it") }
READER_MODE_TLD -> tlds.addAll(SpacedTokens(line).map { ".$it" })
}
}
}
@ -156,6 +156,16 @@ class LocaleKeyboardInfos(dataStream: InputStream?, locale: Locale) {
}
}
fun addLocaleTlds(locale: Locale) {
tlds.add(0, comTld)
val ccLower = locale.country.lowercase()
if (ccLower.isNotEmpty() && locale.language != SubtypeLocaleUtils.NO_LANGUAGE) {
specialCountryTlds[ccLower]?.let { tlds.addAll(SpacedTokens(it)) } ?: tlds.add(".$ccLower")
}
if ((locale.language != "en" && euroLocales.matches(locale.language)) || euroCountries.matches(locale.country))
tlds.add(".eu")
tlds.addAll(SpacedTokens(otherDefaultTlds))
}
}
private fun addFixedColumnOrder(popupKeys: MutableCollection<String>) {
@ -205,6 +215,7 @@ private fun createLocaleKeyTexts(context: Context, params: KeyboardParams, popup
POPUP_KEYS_MORE -> lkt.addFile(context.assets.open("$LOCALE_TEXTS_FOLDER/more_popups_more.txt"), false)
POPUP_KEYS_ALL -> lkt.addFile(context.assets.open("$LOCALE_TEXTS_FOLDER/more_popups_all.txt"), false)
}
lkt.addLocaleTlds(params.mId.locale)
return lkt
}
@ -220,28 +231,6 @@ private fun getStreamForLocale(locale: Locale, context: Context) =
}
}
private fun getLocaleTlds(locale: Locale): LinkedHashSet<String> {
val tlds = getDefaultTlds(locale)
val ccLower = locale.country.lowercase()
if (ccLower.isEmpty() || locale.language == SubtypeLocaleUtils.NO_LANGUAGE)
return tlds
specialCountryTlds.forEach {
if (ccLower != it.first) return@forEach
tlds.addAll(SpacedTokens(it.second))
return@getLocaleTlds tlds
}
tlds.add(".$ccLower")
return tlds
}
private fun getDefaultTlds(locale: Locale): LinkedHashSet<String> {
val tlds = linkedSetOf<String>()
tlds.addAll(SpacedTokens(defaultTlds))
if ((locale.language != "en" && euroLocales.matches(locale.language)) || euroCountries.matches(locale.country))
tlds.add(".eu")
return tlds
}
fun clearCache() = localeKeyboardInfosCache.clear()
// cache the texts, so they don't need to be read over and over
@ -341,7 +330,7 @@ const val POPUP_KEYS_NORMAL = "normal"
private const val LOCALE_TEXTS_FOLDER = "locale_key_texts"
// either tld is not simply lowercase ISO 3166-1 code, or there are multiple according to some list
private val specialCountryTlds = listOf(
private val specialCountryTlds = hashMapOf<String, String>(
"bd" to ".bd .com.bd",
"bq" to ".bq .an .nl",
"bl" to ".bl .gp .fr",
@ -351,4 +340,5 @@ private val specialCountryTlds = listOf(
"mf" to ".mf .gp .fr",
"tl" to ".tl .tp",
)
private const val defaultTlds = ".com .gov .edu .org .net"
private const val comTld = ".com"
private const val otherDefaultTlds = ".gov .edu .org .net"

View file

@ -83,7 +83,7 @@ object KeyCode {
const val LANGUAGE_SWITCH = -227
//const val IME_SHOW_UI = -231
//const val IME_HIDE_UI = -232
const val IME_HIDE_UI = -232
const val VOICE_INPUT = -233
//const val TOGGLE_SMARTBAR_VISIBILITY = -241
@ -175,6 +175,12 @@ object KeyCode {
const val META_LEFT = -10048
const val META_RIGHT = -10049
// Intents
const val SEND_INTENT_ONE = -20000
const val SEND_INTENT_TWO = -20001
const val SEND_INTENT_THREE = -20002
/** to make sure a FlorisBoard code works when reading a JSON layout */
fun Int.checkAndConvertCode(): Int = if (this > 0) this else when (this) {
// working
@ -183,14 +189,14 @@ object KeyCode {
REDO, ARROW_DOWN, ARROW_UP, ARROW_RIGHT, ARROW_LEFT, CLIPBOARD_COPY, CLIPBOARD_PASTE, CLIPBOARD_SELECT_ALL,
CLIPBOARD_SELECT_WORD, TOGGLE_INCOGNITO_MODE, TOGGLE_AUTOCORRECT, MOVE_START_OF_LINE, MOVE_END_OF_LINE,
MOVE_START_OF_PAGE, MOVE_END_OF_PAGE, SHIFT, CAPS_LOCK, MULTIPLE_CODE_POINTS, UNSPECIFIED, CTRL, ALT,
FN, CLIPBOARD_CLEAR_HISTORY, NUMPAD,
FN, CLIPBOARD_CLEAR_HISTORY, NUMPAD, IME_HIDE_UI,
// heliboard only
SYMBOL_ALPHA, TOGGLE_ONE_HANDED_MODE, SWITCH_ONE_HANDED_MODE, SPLIT_LAYOUT, SHIFT_ENTER,
ACTION_NEXT, ACTION_PREVIOUS, NOT_SPECIFIED, CLIPBOARD_COPY_ALL, WORD_LEFT, WORD_RIGHT, PAGE_UP,
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
TIMESTAMP, CTRL_LEFT, CTRL_RIGHT, ALT_LEFT, ALT_RIGHT, META_LEFT, META_RIGHT, SEND_INTENT_ONE, SEND_INTENT_TWO, SEND_INTENT_THREE,
-> this
// conversion
@ -210,12 +216,54 @@ object KeyCode {
// todo: there are many more keys, see near https://developer.android.com/reference/android/view/KeyEvent#KEYCODE_0
/**
* Convert a keyCode / codePoint to a KeyEvent.KEYCODE_<xxx>.
* Convert an internal keyCode to a KeyEvent.KEYCODE_<xxx>.
* Positive codes are passed through, unknown negative codes result in KeyEvent.KEYCODE_UNKNOWN.
* To be uses for fake hardware key press.
*/
@JvmStatic fun keyCodeToKeyEventCode(keyCode: Int) = when (keyCode) {
ARROW_UP -> KeyEvent.KEYCODE_DPAD_UP
ARROW_RIGHT -> KeyEvent.KEYCODE_DPAD_RIGHT
ARROW_DOWN -> KeyEvent.KEYCODE_DPAD_DOWN
ARROW_LEFT -> KeyEvent.KEYCODE_DPAD_LEFT
MOVE_START_OF_LINE -> KeyEvent.KEYCODE_MOVE_HOME
MOVE_END_OF_LINE -> KeyEvent.KEYCODE_MOVE_END
TAB -> KeyEvent.KEYCODE_TAB
PAGE_UP -> KeyEvent.KEYCODE_PAGE_UP
PAGE_DOWN -> KeyEvent.KEYCODE_PAGE_DOWN
ESCAPE -> KeyEvent.KEYCODE_ESCAPE
INSERT -> KeyEvent.KEYCODE_INSERT
SLEEP -> KeyEvent.KEYCODE_SLEEP
MEDIA_PLAY -> KeyEvent.KEYCODE_MEDIA_PLAY
MEDIA_PAUSE -> KeyEvent.KEYCODE_MEDIA_PAUSE
MEDIA_PLAY_PAUSE -> KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
MEDIA_NEXT -> KeyEvent.KEYCODE_MEDIA_NEXT
MEDIA_PREVIOUS -> KeyEvent.KEYCODE_MEDIA_PREVIOUS
VOL_UP -> KeyEvent.KEYCODE_VOLUME_UP
VOL_DOWN -> KeyEvent.KEYCODE_VOLUME_DOWN
MUTE -> KeyEvent.KEYCODE_VOLUME_MUTE
BACK -> KeyEvent.KEYCODE_BACK
F1 -> KeyEvent.KEYCODE_F1
F2 -> KeyEvent.KEYCODE_F2
F3 -> KeyEvent.KEYCODE_F3
F4 -> KeyEvent.KEYCODE_F4
F5 -> KeyEvent.KEYCODE_F5
F6 -> KeyEvent.KEYCODE_F6
F7 -> KeyEvent.KEYCODE_F7
F8 -> KeyEvent.KEYCODE_F8
F9 -> KeyEvent.KEYCODE_F9
F10 -> KeyEvent.KEYCODE_F10
F11 -> KeyEvent.KEYCODE_F11
F12 -> KeyEvent.KEYCODE_F12
else -> if (keyCode < 0) KeyEvent.KEYCODE_UNKNOWN else keyCode
}
// todo: there are many more keys, see near https://developer.android.com/reference/android/view/KeyEvent#KEYCODE_0
/**
* Convert a codePoint to a KeyEvent.KEYCODE_<xxx>.
* Fallback to KeyEvent.KEYCODE_UNKNOWN.
* To be uses for fake hardware key press.
* */
fun Int.toKeyEventCode(): Int = if (this > 0)
when (this.toChar().uppercaseChar()) {
*/
@JvmStatic fun codePointToKeyEventCode(codePoint: Int): Int = when (codePoint.toChar().uppercaseChar()) {
'/' -> KeyEvent.KEYCODE_SLASH
'\\' -> KeyEvent.KEYCODE_BACKSLASH
';' -> KeyEvent.KEYCODE_SEMICOLON
@ -269,40 +317,4 @@ object KeyCode {
'Z' -> KeyEvent.KEYCODE_Z
else -> KeyEvent.KEYCODE_UNKNOWN
}
else when (this) {
ARROW_UP -> KeyEvent.KEYCODE_DPAD_UP
ARROW_RIGHT -> KeyEvent.KEYCODE_DPAD_RIGHT
ARROW_DOWN -> KeyEvent.KEYCODE_DPAD_DOWN
ARROW_LEFT -> KeyEvent.KEYCODE_DPAD_LEFT
MOVE_START_OF_LINE -> KeyEvent.KEYCODE_MOVE_HOME
MOVE_END_OF_LINE -> KeyEvent.KEYCODE_MOVE_END
TAB -> KeyEvent.KEYCODE_TAB
PAGE_UP -> KeyEvent.KEYCODE_PAGE_UP
PAGE_DOWN -> KeyEvent.KEYCODE_PAGE_DOWN
ESCAPE -> KeyEvent.KEYCODE_ESCAPE
INSERT -> KeyEvent.KEYCODE_INSERT
SLEEP -> KeyEvent.KEYCODE_SLEEP
MEDIA_PLAY -> KeyEvent.KEYCODE_MEDIA_PLAY
MEDIA_PAUSE -> KeyEvent.KEYCODE_MEDIA_PAUSE
MEDIA_PLAY_PAUSE -> KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
MEDIA_NEXT -> KeyEvent.KEYCODE_MEDIA_NEXT
MEDIA_PREVIOUS -> KeyEvent.KEYCODE_MEDIA_PREVIOUS
VOL_UP -> KeyEvent.KEYCODE_VOLUME_UP
VOL_DOWN -> KeyEvent.KEYCODE_VOLUME_DOWN
MUTE -> KeyEvent.KEYCODE_VOLUME_MUTE
BACK -> KeyEvent.KEYCODE_BACK
F1 -> KeyEvent.KEYCODE_F1
F2 -> KeyEvent.KEYCODE_F2
F3 -> KeyEvent.KEYCODE_F3
F4 -> KeyEvent.KEYCODE_F4
F5 -> KeyEvent.KEYCODE_F5
F6 -> KeyEvent.KEYCODE_F6
F7 -> KeyEvent.KEYCODE_F7
F8 -> KeyEvent.KEYCODE_F8
F9 -> KeyEvent.KEYCODE_F9
F10 -> KeyEvent.KEYCODE_F10
F11 -> KeyEvent.KEYCODE_F11
F12 -> KeyEvent.KEYCODE_F12
else -> KeyEvent.KEYCODE_UNKNOWN
}
}

View file

@ -11,6 +11,7 @@ import java.util.Locale;
import helium314.keyboard.latin.SuggestedWords.SuggestedWordInfo;
import helium314.keyboard.latin.common.ComposedData;
import helium314.keyboard.latin.makedict.WordProperty;
import helium314.keyboard.latin.settings.SettingsValuesForSuggestion;
/**
@ -177,6 +178,10 @@ public abstract class Dictionary {
};
}
public WordProperty getWordProperty(final String word, final boolean isBeginningOfSentence) {
return null;
}
/**
* Not a true dictionary. A placeholder used to indicate suggestions that don't come from any
* real dictionary.

View file

@ -381,6 +381,7 @@ class DictionaryFacilitatorImpl : DictionaryFacilitator {
}
private fun addToPersonalDictionaryIfInvalidButInHistory(word: String) {
if (word.length <= 1) return
val dictionaryGroup = clearlyPreferredDictionaryGroup ?: return
val userDict = dictionaryGroup.getSubDict(Dictionary.TYPE_USER) ?: return
val userHistoryDict = dictionaryGroup.getSubDict(Dictionary.TYPE_USER_HISTORY) ?: return
@ -396,7 +397,10 @@ class DictionaryFacilitatorImpl : DictionaryFacilitator {
// This is not too bad, but it delays adding in case a user wants to fill a dictionary using this functionality
if (userHistoryDict.getFrequency(word) > 120) {
scope.launch {
UserDictionary.Words.addWord(userDict.mContext, word, 250, null, dictionaryGroup.locale)
// adding can throw IllegalArgumentException: Unknown URL content://user_dictionary/words
// https://stackoverflow.com/q/41474623 https://github.com/AnySoftKeyboard/AnySoftKeyboard/issues/490
// apparently some devices don't have a dictionary? or it's just sporadic hiccups?
runCatching { UserDictionary.Words.addWord(userDict.mContext, word, 250, null, dictionaryGroup.locale) }
}
}
}
@ -730,7 +734,7 @@ private class DictionaryGroup(
else {
val file = File(context.filesDir.absolutePath + File.separator + "blacklists" + File.separator + locale.toLanguageTag() + ".txt")
if (file.isDirectory) file.delete() // this apparently was an issue in some versions
if (file.parentFile?.mkdirs() == true) file
if (file.parentFile?.exists() == true || file.parentFile?.mkdirs() == true) file
else null
}

View file

@ -64,10 +64,26 @@ object DictionaryFactory {
* if the dictionary type already exists in [dicts], the [file] is skipped
*/
private fun checkAndAddDictionaryToListNewType(file: File, dicts: MutableList<Dictionary>, locale: Locale) {
if (!file.isFile) return
val header = DictionaryInfoUtils.getDictionaryFileHeaderOrNull(file) ?: return killDictionary(file)
val dictionary = getDictionary(file, locale) ?: return
if (dicts.any { it.mDictType == dictionary.mDictType }) {
dictionary.close()
return
}
dicts.add(dictionary)
}
@JvmStatic
fun getDictionary(
file: File,
locale: Locale
): Dictionary? {
if (!file.isFile) return null
val header = DictionaryInfoUtils.getDictionaryFileHeaderOrNull(file)
if (header == null) {
killDictionary(file)
return null
}
val dictType = header.mIdString.split(":").first()
if (dicts.any { it.mDictType == dictType }) return
val readOnlyBinaryDictionary = ReadOnlyBinaryDictionary(
file.absolutePath, 0, file.length(), false, locale, dictType
)
@ -75,14 +91,13 @@ object DictionaryFactory {
if (readOnlyBinaryDictionary.isValidDictionary) {
if (locale.language == "ko") {
// Use KoreanDictionary for Korean locale
dicts.add(KoreanDictionary(readOnlyBinaryDictionary))
} else {
dicts.add(readOnlyBinaryDictionary)
return KoreanDictionary(readOnlyBinaryDictionary)
}
return readOnlyBinaryDictionary
}
} else {
readOnlyBinaryDictionary.close()
killDictionary(file)
}
return null
}
private fun killDictionary(file: File) {

View file

@ -23,7 +23,7 @@ import java.util.List;
/**
* A class for detecting Emoji-Alt physical key.
*/
final class EmojiAltPhysicalKeyDetector {
public final class EmojiAltPhysicalKeyDetector {
private static final String TAG = "EmojiAltPhysKeyDetector";
private static final boolean DEBUG = false;

View file

@ -4,6 +4,7 @@ package helium314.keyboard.latin;
import helium314.keyboard.event.HangulCombiner;
import helium314.keyboard.latin.common.ComposedData;
import helium314.keyboard.latin.makedict.WordProperty;
import helium314.keyboard.latin.settings.SettingsValuesForSuggestion;
import java.text.Normalizer;
@ -72,6 +73,11 @@ public class KoreanDictionary extends Dictionary {
return mDictionary.getMaxFrequencyOfExactMatches(processInput(word));
}
@Override
public WordProperty getWordProperty(String word, boolean isBeginningOfSentence) {
return mDictionary.getWordProperty(processInput(word), isBeginningOfSentence);
}
@Override
protected boolean same(char[] word, int length, String typedWord) {
word = processInput(new String(word)).toCharArray();

View file

@ -26,7 +26,6 @@ import android.os.Process;
import android.text.InputType;
import android.util.PrintWriterPrinter;
import android.util.Printer;
import android.util.SparseArray;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.View;
@ -51,9 +50,6 @@ import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode;
import helium314.keyboard.latin.common.InsetsOutlineProvider;
import helium314.keyboard.dictionarypack.DictionaryPackConstants;
import helium314.keyboard.event.Event;
import helium314.keyboard.event.HangulEventDecoder;
import helium314.keyboard.event.HardwareEventDecoder;
import helium314.keyboard.event.HardwareKeyboardEventDecoder;
import helium314.keyboard.event.InputTransaction;
import helium314.keyboard.keyboard.Keyboard;
import helium314.keyboard.keyboard.KeyboardId;
@ -68,7 +64,6 @@ import helium314.keyboard.latin.common.InputPointers;
import helium314.keyboard.latin.common.LocaleUtils;
import helium314.keyboard.latin.common.ViewOutlineProviderUtilsKt;
import helium314.keyboard.latin.define.DebugFlags;
import helium314.keyboard.latin.define.ProductionFlags;
import helium314.keyboard.latin.inputlogic.InputLogic;
import helium314.keyboard.latin.personalization.PersonalizationHelper;
import helium314.keyboard.latin.settings.Settings;
@ -134,9 +129,6 @@ public class LatinIME extends InputMethodService implements
private final DictionaryFacilitator mDictionaryFacilitator =
DictionaryFacilitatorProvider.getDictionaryFacilitator(false);
final InputLogic mInputLogic = new InputLogic(this, this, mDictionaryFacilitator);
// We expect to have only one decoder in almost all cases, hence the default capacity of 1.
// If it turns out we need several, it will get grown seamlessly.
final SparseArray<HardwareEventDecoder> mHardwareEventDecoders = new SparseArray<>(1);
// TODO: Move these {@link View}s to {@link KeyboardSwitcher}.
private View mInputView;
@ -146,7 +138,6 @@ public class LatinIME extends InputMethodService implements
private RichInputMethodManager mRichImm;
final KeyboardSwitcher mKeyboardSwitcher;
private final SubtypeState mSubtypeState = new SubtypeState();
private EmojiAltPhysicalKeyDetector mEmojiAltPhysicalKeyDetector;
private final StatsUtilsManager mStatsUtilsManager;
// Working variable for {@link #startShowingInputView()} and
// {@link #onEvaluateInputViewShown()}.
@ -279,9 +270,7 @@ public class LatinIME extends InputMethodService implements
msg.arg2 /* remainingTries */, this /* handler */)) {
// If we were able to reset the caches, then we can reload the keyboard.
// Otherwise, we'll do it when we can.
latinIme.mKeyboardSwitcher.loadKeyboard(latinIme.getCurrentInputEditorInfo(),
settingsValues, latinIme.getCurrentAutoCapsState(),
latinIme.getCurrentRecapitalizeState());
latinIme.mKeyboardSwitcher.reloadMainKeyboard();
}
break;
case MSG_WAIT_FOR_DICTIONARY_LOAD:
@ -637,6 +626,7 @@ public class LatinIME extends InputMethodService implements
@Override
public void onCreate() {
mSettings.startListener();
KeyboardIconsSet.Companion.getInstance().loadIcons(this);
mRichImm = RichInputMethodManager.getInstance();
AudioAndHapticFeedbackManager.init(this);
@ -664,7 +654,8 @@ public class LatinIME extends InputMethodService implements
final IntentFilter newDictFilter = new IntentFilter();
newDictFilter.addAction(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION);
ContextCompat.registerReceiver(this, mDictionaryPackInstallReceiver, newDictFilter, ContextCompat.RECEIVER_NOT_EXPORTED);
// RECEIVER_EXPORTED is necessary because apparently Android 15 (and others?) don't recognize if the sender and receiver are the same app, see https://github.com/Helium314/HeliBoard/pull/1756
ContextCompat.registerReceiver(this, mDictionaryPackInstallReceiver, newDictFilter, ContextCompat.RECEIVER_EXPORTED);
final IntentFilter dictDumpFilter = new IntentFilter();
dictDumpFilter.addAction(DictionaryDumpBroadcastReceiver.DICTIONARY_DUMP_INTENT_ACTION);
@ -1071,7 +1062,7 @@ public class LatinIME extends InputMethodService implements
if (isDifferentTextField) {
mainKeyboardView.closing();
suggest.setAutoCorrectionThreshold(currentSettingsValues.mAutoCorrectionThreshold);
switcher.loadKeyboard(editorInfo, currentSettingsValues, getCurrentAutoCapsState(), getCurrentRecapitalizeState());
switcher.reloadMainKeyboard();
if (needToCallLoadKeyboardLater) {
// If we need to call loadKeyboard again later, we need to save its state now. The
// later call will be done in #retryResetCaches.
@ -1119,6 +1110,7 @@ public class LatinIME extends InputMethodService implements
@Override
public void onWindowHidden() {
super.onWindowHidden();
Log.i(TAG, "onWindowHidden");
final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
if (mainKeyboardView != null) {
mainKeyboardView.closing();
@ -1175,8 +1167,12 @@ public class LatinIME extends InputMethodService implements
if (isInputViewShown()
&& mInputLogic.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd,
composingSpanStart, composingSpanEnd, settingsValues)) {
mKeyboardSwitcher.requestUpdatingShiftState(getCurrentAutoCapsState(),
getCurrentRecapitalizeState());
// we don't want to update a manually set shift state if selection changed towards one side
// because this may end the manual shift, which is unwanted in case of shift + arrow keys for changing selection
// todo: this is not fully implemented yet, and maybe should be behind a setting
if (mKeyboardSwitcher.getKeyboard() != null && mKeyboardSwitcher.getKeyboard().mId.isAlphabetShiftedManually()
&& !((oldSelEnd == newSelEnd && oldSelStart != newSelStart) || (oldSelEnd != newSelEnd && oldSelStart == newSelStart)))
mKeyboardSwitcher.requestUpdatingShiftState(getCurrentAutoCapsState(), getCurrentRecapitalizeState());
}
}
@ -1217,6 +1213,7 @@ public class LatinIME extends InputMethodService implements
@Override
public void hideWindow() {
Log.i(TAG, "hideWindow");
if (hasSuggestionStripView() && mSettings.getCurrent().mToolbarMode == ToolbarMode.EXPANDABLE)
mSuggestionStripView.setToolbarVisibility(false);
mKeyboardSwitcher.onHideWindow();
@ -1229,6 +1226,12 @@ public class LatinIME extends InputMethodService implements
super.hideWindow();
}
@Override
public void requestHideSelf(int flags) {
super.requestHideSelf(flags);
Log.i(TAG, "requestHideSelf: " + flags);
}
@Override
public void onDisplayCompletions(final CompletionInfo[] applicationSpecifiedCompletions) {
if (DebugFlags.DEBUG_ENABLED) {
@ -1280,8 +1283,8 @@ public class LatinIME extends InputMethodService implements
if (isImeSuppressedByHardwareKeyboard() && !visibleKeyboardView.isShown()) {
// If there is a hardware keyboard and a visible software keyboard view has been hidden,
// no visual element will be shown on the screen.
outInsets.contentTopInsets = inputHeight;
outInsets.visibleTopInsets = inputHeight;
// for some reason setting contentTopInsets and visibleTopInsets broke somewhere along the
// way from OpenBoard to HeliBoard (GH-702, GH-1455), but not setting anything seems to work
mInsetsUpdater.setInsets(outInsets);
return;
}
@ -1386,6 +1389,10 @@ public class LatinIME extends InputMethodService implements
@RequiresApi(api = Build.VERSION_CODES.R)
public boolean onInlineSuggestionsResponse(InlineSuggestionsResponse response) {
Log.d(TAG,"onInlineSuggestionsResponse called");
if (Settings.getValues().mSuggestionStripHiddenPerUserSettings) {
return false;
}
final List<InlineSuggestion> inlineSuggestions = response.getInlineSuggestions();
if (inlineSuggestions.isEmpty()) {
return false;
@ -1535,25 +1542,7 @@ public class LatinIME extends InputMethodService implements
// Implementation of {@link SuggestionStripView.Listener}.
@Override
public void onCodeInput(final int codePoint, final int x, final int y, final boolean isKeyRepeat) {
onCodeInput(codePoint, 0, x, y, isKeyRepeat);
}
public void onCodeInput(final int codePoint, final int metaState, final int x, final int y, final boolean isKeyRepeat) {
if (codePoint < 0) {
switch (codePoint) {
case KeyCode.TOGGLE_AUTOCORRECT -> {mSettings.toggleAutoCorrect(); return; }
case KeyCode.TOGGLE_INCOGNITO_MODE -> {mSettings.toggleAlwaysIncognitoMode(); return; }
}
}
final Event event;
// checking if the character is a combining accent
if (0x300 <= codePoint && codePoint <= 0x35b) {
event = Event.createSoftwareDeadEvent(codePoint, 0, metaState, x, y, null);
} else {
event = createSoftwareKeypressEvent(codePoint, metaState, x, y, isKeyRepeat);
}
onEvent(event);
mKeyboardActionListener.onCodeInput(codePoint, x, y, isKeyRepeat);
}
// This method is public for testability of LatinIME, but also in the future it should
@ -1570,24 +1559,6 @@ public class LatinIME extends InputMethodService implements
mKeyboardSwitcher.onEvent(event, getCurrentAutoCapsState(), getCurrentRecapitalizeState());
}
// A helper method to split the code point and the key code. Ultimately, they should not be
// squashed into the same variable, and this method should be removed.
// public for testing, as we don't want to copy the same logic into test code
@NonNull
public static Event createSoftwareKeypressEvent(final int keyCodeOrCodePoint, final int metaState,
final int keyX, final int keyY, final boolean isKeyRepeat) {
final int keyCode;
final int codePoint;
if (keyCodeOrCodePoint <= 0) {
keyCode = keyCodeOrCodePoint;
codePoint = Event.NOT_A_CODE_POINT;
} else {
keyCode = Event.NOT_A_KEY_CODE;
codePoint = keyCodeOrCodePoint;
}
return Event.createSoftwareKeypressEvent(codePoint, keyCode, metaState, keyX, keyY, isKeyRepeat);
}
public void onTextInput(final String rawText) {
// TODO: have the keyboard pass the correct key code when we need it.
final Event event = Event.createSoftwareTextEvent(rawText, KeyCode.MULTIPLE_CODE_POINTS);
@ -1759,8 +1730,7 @@ public class LatinIME extends InputMethodService implements
loadSettings();
if (mKeyboardSwitcher.getMainKeyboardView() != null) {
// Reload keyboard because the current language has been changed.
mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettings.getCurrent(),
getCurrentAutoCapsState(), getCurrentRecapitalizeState());
mKeyboardSwitcher.reloadMainKeyboard();
}
}
@ -1828,63 +1798,18 @@ public class LatinIME extends InputMethodService implements
feedbackManager.performAudioFeedback(code);
}
private HardwareEventDecoder getHardwareKeyEventDecoder(final int deviceId) {
final HardwareEventDecoder decoder = mHardwareEventDecoders.get(deviceId);
if (null != decoder) return decoder;
// TODO: create the decoder according to the specification
final HardwareEventDecoder newDecoder = new HardwareKeyboardEventDecoder(deviceId);
mHardwareEventDecoders.put(deviceId, newDecoder);
return newDecoder;
}
// Hooks for hardware keyboard
@Override
public boolean onKeyDown(final int keyCode, final KeyEvent keyEvent) {
if (mEmojiAltPhysicalKeyDetector == null) {
mEmojiAltPhysicalKeyDetector = new EmojiAltPhysicalKeyDetector(
getApplicationContext().getResources());
}
mEmojiAltPhysicalKeyDetector.onKeyDown(keyEvent);
if (!ProductionFlags.IS_HARDWARE_KEYBOARD_SUPPORTED) {
return super.onKeyDown(keyCode, keyEvent);
}
final Event event;
if (mRichImm.getCurrentSubtypeLocale().getLanguage().equals("ko")) {
final RichInputMethodSubtype subtype = mKeyboardSwitcher.getKeyboard() == null
? mRichImm.getCurrentSubtype()
: mKeyboardSwitcher.getKeyboard().mId.mSubtype;
event = HangulEventDecoder.decodeHardwareKeyEvent(subtype, keyEvent,
() -> getHardwareKeyEventDecoder(keyEvent.getDeviceId()).decodeHardwareKey(keyEvent));
} else {
event = getHardwareKeyEventDecoder(keyEvent.getDeviceId()).decodeHardwareKey(keyEvent);
}
// If the event is not handled by LatinIME, we just pass it to the parent implementation.
// If it's handled, we return true because we did handle it.
if (event.isHandled()) {
mInputLogic.onCodeInput(mSettings.getCurrent(), event,
mKeyboardSwitcher.getKeyboardShiftMode(),
// TODO: this is not necessarily correct for a hardware keyboard right now
mKeyboardSwitcher.getCurrentKeyboardScript(),
mHandler);
if (mKeyboardActionListener.onKeyDown(keyCode, keyEvent))
return true;
}
return super.onKeyDown(keyCode, keyEvent);
}
@Override
public boolean onKeyUp(final int keyCode, final KeyEvent keyEvent) {
if (mEmojiAltPhysicalKeyDetector == null) {
mEmojiAltPhysicalKeyDetector = new EmojiAltPhysicalKeyDetector(
getApplicationContext().getResources());
}
mEmojiAltPhysicalKeyDetector.onKeyUp(keyEvent);
if (!ProductionFlags.IS_HARDWARE_KEYBOARD_SUPPORTED) {
return super.onKeyUp(keyCode, keyEvent);
}
final long keyIdentifier = (long) keyEvent.getDeviceId() << 32 + keyEvent.getKeyCode();
if (mInputLogic.mCurrentlyPressedHardwareKeys.remove(keyIdentifier)) {
if (mKeyboardActionListener.onKeyUp(keyCode, keyEvent))
return true;
}
return super.onKeyUp(keyCode, keyEvent);
}

View file

@ -10,6 +10,7 @@ import com.android.inputmethod.latin.BinaryDictionary;
import helium314.keyboard.latin.SuggestedWords.SuggestedWordInfo;
import helium314.keyboard.latin.common.ComposedData;
import helium314.keyboard.latin.makedict.WordProperty;
import helium314.keyboard.latin.settings.SettingsValuesForSuggestion;
import java.util.ArrayList;
@ -107,6 +108,18 @@ public final class ReadOnlyBinaryDictionary extends Dictionary {
return NOT_A_PROBABILITY;
}
@Override
public WordProperty getWordProperty(String word, boolean isBeginningOfSentence) {
if (mLock.readLock().tryLock()) {
try {
return mBinaryDictionary.getWordProperty(word, isBeginningOfSentence);
} finally {
mLock.readLock().unlock();
}
}
return null;
}
@Override
public void close() {
mLock.writeLock().lock();

View file

@ -40,8 +40,6 @@ import helium314.keyboard.latin.settings.SpacingAndPunctuations;
import helium314.keyboard.latin.utils.CapsModeUtils;
import helium314.keyboard.latin.utils.DebugLogUtils;
import helium314.keyboard.latin.utils.NgramContextUtils;
import helium314.keyboard.latin.utils.ScriptUtils;
import helium314.keyboard.latin.utils.SpannableStringUtils;
import helium314.keyboard.latin.utils.StatsUtils;
import helium314.keyboard.latin.utils.TextRange;
@ -441,7 +439,7 @@ public final class RichInputConnection implements PrivateCommandPerformer {
// test for this explicitly)
if (INVALID_CURSOR_POSITION != mExpectedSelStart
&& (cachedLength >= n || cachedLength >= mExpectedSelStart)) {
final StringBuilder s = new StringBuilder(mCommittedTextBeforeComposingText);
final StringBuilder s = new StringBuilder(mCommittedTextBeforeComposingText.toString());
// We call #toString() here to create a temporary object.
// In some situations, this method is called on a worker thread, and it's possible
// the main thread touches the contents of mComposingText while this worker thread
@ -718,8 +716,13 @@ public final class RichInputConnection implements PrivateCommandPerformer {
if (start < 0 || end < 0) {
return false;
}
if (start > end) {
mExpectedSelStart = end;
mExpectedSelEnd = start;
} else {
mExpectedSelStart = start;
mExpectedSelEnd = end;
}
if (isConnected()) {
final boolean isIcValid = mIC.setSelection(start, end);
if (!isIcValid) {
@ -825,15 +828,6 @@ public final class RichInputConnection implements PrivateCommandPerformer {
return NgramContextUtils.getNgramContextFromNthPreviousWord(prev, spacingAndPunctuations, n);
}
private static boolean isPartOfCompositionForScript(final int codePoint,
final SpacingAndPunctuations spacingAndPunctuations, final String script) {
// We always consider word connectors part of compositions.
return spacingAndPunctuations.isWordConnector(codePoint)
// Otherwise, it's part of composition if it's part of script and not a separator.
|| (!spacingAndPunctuations.isWordSeparator(codePoint)
&& ScriptUtils.isLetterPartOfScript(codePoint, script));
}
/**
* Returns the text surrounding the cursor.
*
@ -860,90 +854,7 @@ public final class RichInputConnection implements PrivateCommandPerformer {
if (before == null || after == null) {
return null;
}
// Going backward, find the first breaking point (separator)
int startIndexInBefore = before.length();
int endIndexInAfter = -1;
while (startIndexInBefore > 0) {
final int codePoint = Character.codePointBefore(before, startIndexInBefore);
if (!isPartOfCompositionForScript(codePoint, spacingAndPunctuations, script)) {
if (Character.isWhitespace(codePoint) || !spacingAndPunctuations.mCurrentLanguageHasSpaces)
break;
// continue to the next whitespace and see whether this contains a sometimesWordConnector
for (int i = startIndexInBefore - 1; i >= 0; i--) {
final char c = before.charAt(i);
if (spacingAndPunctuations.isSometimesWordConnector(c)) {
// if yes -> whitespace is the index
startIndexInBefore = Math.max(StringUtils.charIndexOfLastWhitespace(before), 0);
final int firstSpaceAfter = StringUtils.charIndexOfFirstWhitespace(after);
endIndexInAfter = firstSpaceAfter == -1 ? after.length() : firstSpaceAfter -1;
break;
} else if (Character.isWhitespace(c)) {
// if no, just break normally
break;
}
}
break;
}
--startIndexInBefore;
if (Character.isSupplementaryCodePoint(codePoint)) {
--startIndexInBefore;
}
}
// Find last word separator after the cursor
if (endIndexInAfter == -1) {
while (++endIndexInAfter < after.length()) {
final int codePoint = Character.codePointAt(after, endIndexInAfter);
if (!isPartOfCompositionForScript(codePoint, spacingAndPunctuations, script)) {
if (Character.isWhitespace(codePoint) || !spacingAndPunctuations.mCurrentLanguageHasSpaces)
break;
// continue to the next whitespace and see whether this contains a sometimesWordConnector
for (int i = endIndexInAfter; i < after.length(); i++) {
final char c = after.charAt(i);
if (spacingAndPunctuations.isSometimesWordConnector(c)) {
// if yes -> whitespace is next to the index
startIndexInBefore = Math.max(StringUtils.charIndexOfLastWhitespace(before), 0);
final int firstSpaceAfter = StringUtils.charIndexOfFirstWhitespace(after);
endIndexInAfter = firstSpaceAfter == -1 ? after.length() : firstSpaceAfter - 1;
break;
} else if (Character.isWhitespace(c)) {
// if no, just break normally
break;
}
}
break;
}
if (Character.isSupplementaryCodePoint(codePoint)) {
++endIndexInAfter;
}
}
}
// strip stuff before "//" (i.e. ignore http and other protocols)
final String beforeConsideringStart = before.subSequence(startIndexInBefore, before.length()).toString();
final int protocolEnd = beforeConsideringStart.lastIndexOf("//");
if (protocolEnd != -1)
startIndexInBefore += protocolEnd + 1;
// we don't want the end characters to be word separators
while (endIndexInAfter > 0 && spacingAndPunctuations.isWordSeparator(after.charAt(endIndexInAfter - 1))) {
--endIndexInAfter;
}
while (startIndexInBefore < before.length() && spacingAndPunctuations.isWordSeparator(before.charAt(startIndexInBefore))) {
++startIndexInBefore;
}
final boolean hasUrlSpans =
SpannableStringUtils.hasUrlSpans(before, startIndexInBefore, before.length())
|| SpannableStringUtils.hasUrlSpans(after, 0, endIndexInAfter);
// We don't use TextUtils#concat because it copies all spans without respect to their
// nature. If the text includes a PARAGRAPH span and it has been split, then
// TextUtils#concat will crash when it tries to concat both sides of it.
return new TextRange(
SpannableStringUtils.concatWithNonParagraphSuggestionSpansOnly(before, after),
startIndexInBefore, before.length() + endIndexInAfter, before.length(),
hasUrlSpans);
return StringUtilsKt.getTouchedWordRange(before, after, script, spacingAndPunctuations);
}
public boolean isCursorTouchingWord(final SpacingAndPunctuations spacingAndPunctuations,
@ -956,19 +867,7 @@ public final class RichInputConnection implements PrivateCommandPerformer {
// a composing region should always count as a word
return true;
}
final String textBeforeCursor = mCommittedTextBeforeComposingText.toString();
int indexOfCodePointInJavaChars = textBeforeCursor.length();
int consideredCodePoint = 0 == indexOfCodePointInJavaChars ? Constants.NOT_A_CODE
: textBeforeCursor.codePointBefore(indexOfCodePointInJavaChars);
// Search for the first non word-connector char
if (spacingAndPunctuations.isWordConnector(consideredCodePoint)) {
indexOfCodePointInJavaChars -= Character.charCount(consideredCodePoint);
consideredCodePoint = 0 == indexOfCodePointInJavaChars ? Constants.NOT_A_CODE
: textBeforeCursor.codePointBefore(indexOfCodePointInJavaChars);
}
return !(Constants.NOT_A_CODE == consideredCodePoint
|| spacingAndPunctuations.isWordSeparator(consideredCodePoint)
|| spacingAndPunctuations.isWordConnector(consideredCodePoint));
return StringUtilsKt.endsWithWordCodepoint(mCommittedTextBeforeComposingText.toString(), spacingAndPunctuations);
}
public boolean isCursorFollowedByWordCharacter(

View file

@ -7,6 +7,7 @@ import helium314.keyboard.keyboard.Keyboard
import helium314.keyboard.keyboard.KeyboardSwitcher
import helium314.keyboard.latin.DictionaryFacilitator.DictionaryInitializationListener
import helium314.keyboard.latin.common.ComposedData
import helium314.keyboard.latin.makedict.WordProperty
import helium314.keyboard.latin.settings.SettingsValuesForSuggestion
import helium314.keyboard.latin.utils.SuggestionResults
import java.util.Locale
@ -47,6 +48,8 @@ class SingleDictionaryFacilitator(private val dict: Dictionary) : DictionaryFaci
return suggestionResults
}
fun getWordProperty(word: String): WordProperty? = dict.getWordProperty(word, false)
// ------------ dummy functionality ----------------
override fun setValidSpellingWordReadCache(cache: LruCache<String, Boolean>) {}

View file

@ -12,3 +12,5 @@ object Links {
const val CUSTOM_LAYOUTS = "$GITHUB/discussions/categories/custom-layout"
const val CUSTOM_COLORS = "$GITHUB/discussions/categories/custom-colors"
}
val combiningRange = 0x300..0x35b

View file

@ -6,13 +6,18 @@ import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode
import helium314.keyboard.latin.common.StringUtils.mightBeEmoji
import helium314.keyboard.latin.common.StringUtils.newSingleCodePointString
import helium314.keyboard.latin.settings.SpacingAndPunctuations
import helium314.keyboard.latin.utils.ScriptUtils
import helium314.keyboard.latin.utils.SpacedTokens
import helium314.keyboard.latin.utils.SpannableStringUtils
import helium314.keyboard.latin.utils.TextRange
import java.math.BigInteger
import java.util.Locale
import kotlin.math.max
fun CharSequence.codePointAt(offset: Int) = Character.codePointAt(this, offset)
fun CharSequence.codePointBefore(offset: Int) = Character.codePointBefore(this, offset)
/** Loops over the codepoints in [text]. Exits when [loop] returns true */
inline fun loopOverCodePoints(text: CharSequence, loop: (cp: Int, charCount: Int) -> Boolean) {
var offset = 0
while (offset < text.length) {
@ -23,6 +28,7 @@ inline fun loopOverCodePoints(text: CharSequence, loop: (cp: Int, charCount: Int
}
}
/** Loops backwards over the codepoints in [text]. Exits when [loop] returns true */
inline fun loopOverCodePointsBackwards(text: CharSequence, loop: (cp: Int, charCount: Int) -> Boolean) {
var offset = text.length
while (offset > 0) {
@ -88,6 +94,111 @@ fun getFullEmojiAtEnd(text: CharSequence): String {
return s.substring(offset)
}
/**
* Returns whether the [text] does not end with word separator, ignoring all word connectors.
* If the [text] is empty (after ignoring word connectors), the method returns false.
*/
// todo: this returns true on numbers, why isn't Character.isLetter(code) used?
fun endsWithWordCodepoint(text: String, spacingAndPunctuations: SpacingAndPunctuations): Boolean {
if (text.isEmpty()) return false
var codePoint = 0 // initial value irrelevant since length is always > 0
loopOverCodePointsBackwards(text) { cp, _ ->
codePoint = cp
!spacingAndPunctuations.isWordConnector(cp)
}
// codePoint might still be a wordConnector (if text consists of wordConnectors)
return !spacingAndPunctuations.isWordConnector(codePoint) && !spacingAndPunctuations.isWordSeparator(codePoint)
}
// todo: simplify... maybe compare with original code?
fun getTouchedWordRange(before: CharSequence, after: CharSequence, script: String, spacingAndPunctuations: SpacingAndPunctuations): TextRange {
// Going backward, find the first breaking point (separator)
var startIndexInBefore = before.length
var endIndexInAfter = -1 // todo: clarify why might we want to set it when checking before
loopOverCodePointsBackwards(before) { codePoint, cpLength ->
if (!isPartOfCompositionForScript(codePoint, spacingAndPunctuations, script)) {
if (Character.isWhitespace(codePoint) || !spacingAndPunctuations.mCurrentLanguageHasSpaces)
return@loopOverCodePointsBackwards true
// continue to the next whitespace and see whether this contains a sometimesWordConnector
for (i in startIndexInBefore - 1 downTo 0) {
val c = before[i]
if (spacingAndPunctuations.isSometimesWordConnector(c.code)) {
// if yes -> whitespace is the index
startIndexInBefore = max(StringUtils.charIndexOfLastWhitespace(before).toDouble(), 0.0).toInt()
val firstSpaceAfter = StringUtils.charIndexOfFirstWhitespace(after)
endIndexInAfter = if (firstSpaceAfter == -1) after.length else firstSpaceAfter - 1
return@loopOverCodePointsBackwards true
} else if (Character.isWhitespace(c)) {
// if no, just break normally
return@loopOverCodePointsBackwards true
}
}
return@loopOverCodePointsBackwards true
}
startIndexInBefore -= cpLength
false
}
// Find last word separator after the cursor
if (endIndexInAfter == -1) {
endIndexInAfter = 0
loopOverCodePoints(after) { codePoint, cpLength ->
if (!isPartOfCompositionForScript(codePoint, spacingAndPunctuations, script)) {
if (Character.isWhitespace(codePoint) || !spacingAndPunctuations.mCurrentLanguageHasSpaces)
return@loopOverCodePoints true
// continue to the next whitespace and see whether this contains a sometimesWordConnector
for (i in endIndexInAfter..<after.length) {
val c = after[i]
if (spacingAndPunctuations.isSometimesWordConnector(c.code)) {
// if yes -> whitespace is next to the index
startIndexInBefore = max(StringUtils.charIndexOfLastWhitespace(before), 0)
val firstSpaceAfter = StringUtils.charIndexOfFirstWhitespace(after)
endIndexInAfter = if (firstSpaceAfter == -1) after.length else firstSpaceAfter - 1
return@loopOverCodePoints true
} else if (Character.isWhitespace(c)) {
// if no, just break normally
return@loopOverCodePoints true
}
}
return@loopOverCodePoints true
}
endIndexInAfter += cpLength
false
}
}
// strip text before "//" (i.e. ignore http and other protocols)
val beforeConsideringStart = before.substring(startIndexInBefore, before.length)
val protocolEnd = beforeConsideringStart.lastIndexOf("//")
if (protocolEnd != -1) startIndexInBefore += protocolEnd + 1
// we don't want the end characters to be word separators
while (endIndexInAfter > 0 && spacingAndPunctuations.isWordSeparator(after[endIndexInAfter - 1].code)) {
--endIndexInAfter
}
while (startIndexInBefore < before.length && spacingAndPunctuations.isWordSeparator(before[startIndexInBefore].code)) {
++startIndexInBefore
}
val hasUrlSpans = SpannableStringUtils.hasUrlSpans(before, startIndexInBefore, before.length)
|| SpannableStringUtils.hasUrlSpans(after, 0, endIndexInAfter)
// We don't use TextUtils#concat because it copies all spans without respect to their
// nature. If the text includes a PARAGRAPH span and it has been split, then
// TextUtils#concat will crash when it tries to concat both sides of it.
return TextRange(
SpannableStringUtils.concatWithNonParagraphSuggestionSpansOnly(before, after),
startIndexInBefore, before.length + endIndexInAfter, before.length,
hasUrlSpans
)
}
// actually this should not be in STRING Utils, but only used for getTouchedWordRange
private fun isPartOfCompositionForScript(codePoint: Int, spacingAndPunctuations: SpacingAndPunctuations, script: String) =
spacingAndPunctuations.isWordConnector(codePoint) // We always consider word connectors part of compositions.
// Otherwise, it's part of composition if it's part of script and not a separator.
|| (!spacingAndPunctuations.isWordSeparator(codePoint) && ScriptUtils.isLetterPartOfScript(codePoint, script))
/** split the string on the first of consecutive space only, further consecutive spaces are added to the next split */
fun String.splitOnFirstSpacesOnly(): List<String> {
val out = mutableListOf<String>()

View file

@ -7,11 +7,7 @@
package helium314.keyboard.latin.define
object ProductionFlags {
const val IS_HARDWARE_KEYBOARD_SUPPORTED = false
// todo: make it work
// was set to true in hangul branch (and there is the hangul hardware event decoder in latinIme)
// but disabled again because this breaks ctrl+c / ctrl+v, and most likely other things
// so it looks like the HardwareKeyboardEventDecoder needs some work before it's ready
const val IS_HARDWARE_KEYBOARD_SUPPORTED = true
/**
* Include all suggestions from all dictionaries in

View file

@ -49,6 +49,7 @@ import helium314.keyboard.latin.settings.SpacingAndPunctuations;
import helium314.keyboard.latin.suggestions.SuggestionStripViewAccessor;
import helium314.keyboard.latin.utils.AsyncResultHolder;
import helium314.keyboard.latin.utils.InputTypeUtils;
import helium314.keyboard.latin.utils.IntentUtils;
import helium314.keyboard.latin.utils.Log;
import helium314.keyboard.latin.utils.RecapitalizeStatus;
import helium314.keyboard.latin.utils.ScriptUtils;
@ -89,6 +90,7 @@ public final class InputLogic {
private int mDeleteCount;
private long mLastKeyTime;
// todo: this is not used, so either remove it or do something with it
public final TreeSet<Long> mCurrentlyPressedHardwareKeys = new TreeSet<>();
// Keeps track of most recently inserted text (multi-character key) for reverting
@ -399,7 +401,11 @@ public final class InputLogic {
// Stop the last recapitalization, if started.
mRecapitalizeStatus.stop();
mWordBeingCorrectedByCursor = null;
return true;
// we do not return true if
final boolean oneSidedSelectionMove = hasOrHadSelection
&& ((oldSelEnd == newSelEnd && oldSelStart != newSelStart) || (oldSelEnd != newSelEnd && oldSelStart == newSelStart));
return !oneSidedSelectionMove;
}
public boolean moveCursorByAndReturnIfInsideComposingWord(int distance) {
@ -643,7 +649,8 @@ public final class InputLogic {
*/
private void handleFunctionalEvent(final Event event, final InputTransaction inputTransaction,
final String currentKeyboardScript, final LatinIME.UIHandler handler) {
switch (event.getMKeyCode()) {
final int keyCode = event.getMKeyCode();
switch (keyCode) {
case KeyCode.DELETE:
handleBackspaceEvent(event, inputTransaction, currentKeyboardScript);
// Backspace is a functional key, but it affects the contents of the editor.
@ -686,7 +693,7 @@ public final class InputLogic {
case KeyCode.SHIFT_ENTER:
// todo: try using sendDownUpKeyEventWithMetaState() and remove the key code maybe
final Event tmpEvent = Event.createSoftwareKeypressEvent(Constants.CODE_ENTER,
event.getMKeyCode(), 0, event.getMX(), event.getMY(), event.isKeyRepeat());
keyCode, 0, event.getMX(), event.getMY(), event.isKeyRepeat());
handleNonSpecialCharacterEvent(tmpEvent, inputTransaction, handler);
// Shift + Enter is treated as a functional key but it results in adding a new
// line, so that does affect the contents of the editor.
@ -717,37 +724,43 @@ public final class InputLogic {
if (mConnection.hasSelection()) {
mConnection.copyText(true);
// fake delete keypress to remove the text
final Event backspaceEvent = LatinIME.createSoftwareKeypressEvent(KeyCode.DELETE, 0,
final Event backspaceEvent = Event.createSoftwareKeypressEvent(KeyCode.DELETE, 0,
event.getMX(), event.getMY(), event.isKeyRepeat());
handleBackspaceEvent(backspaceEvent, inputTransaction, currentKeyboardScript);
inputTransaction.setDidAffectContents();
}
break;
case KeyCode.WORD_LEFT:
sendDownUpKeyEventWithMetaState(ScriptUtils.isScriptRtl(currentKeyboardScript)?
KeyEvent.KEYCODE_DPAD_RIGHT : KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.META_CTRL_ON);
sendDownUpKeyEventWithMetaState(
ScriptUtils.isScriptRtl(currentKeyboardScript) ? KeyEvent.KEYCODE_DPAD_RIGHT : KeyEvent.KEYCODE_DPAD_LEFT,
KeyEvent.META_CTRL_ON | event.getMMetaState());
break;
case KeyCode.WORD_RIGHT:
sendDownUpKeyEventWithMetaState(ScriptUtils.isScriptRtl(currentKeyboardScript)?
KeyEvent.KEYCODE_DPAD_LEFT : KeyEvent.KEYCODE_DPAD_RIGHT, KeyEvent.META_CTRL_ON);
sendDownUpKeyEventWithMetaState(
ScriptUtils.isScriptRtl(currentKeyboardScript) ? KeyEvent.KEYCODE_DPAD_LEFT : KeyEvent.KEYCODE_DPAD_RIGHT,
KeyEvent.META_CTRL_ON | event.getMMetaState());
break;
case KeyCode.MOVE_START_OF_PAGE:
final int selectionEnd = mConnection.getExpectedSelectionEnd();
sendDownUpKeyEventWithMetaState(KeyEvent.KEYCODE_MOVE_HOME, KeyEvent.META_CTRL_ON);
if (mConnection.getExpectedSelectionStart() > 0 && mConnection.getExpectedSelectionEnd() == selectionEnd) {
// unchanged, and we're not at the top -> try a different method (necessary for compose fields)
mConnection.setSelection(0, 0);
final int selectionEnd1 = mConnection.getExpectedSelectionEnd();
final int selectionStart1 = mConnection.getExpectedSelectionStart();
sendDownUpKeyEventWithMetaState(KeyEvent.KEYCODE_MOVE_HOME, KeyEvent.META_CTRL_ON | event.getMMetaState());
if (mConnection.getExpectedSelectionStart() == selectionStart1 && mConnection.getExpectedSelectionEnd() == selectionEnd1) {
// unchanged -> try a different method (necessary for compose fields)
final int newEnd = (event.getMMetaState() & KeyEvent.META_SHIFT_MASK) != 0 ? selectionEnd1 : 0;
mConnection.setSelection(0, newEnd);
}
break;
case KeyCode.MOVE_END_OF_PAGE:
final int selectionStart = mConnection.getExpectedSelectionEnd();
sendDownUpKeyEventWithMetaState(KeyEvent.KEYCODE_MOVE_END, KeyEvent.META_CTRL_ON);
if (mConnection.getExpectedSelectionStart() == selectionStart) {
final int selectionStart2 = mConnection.getExpectedSelectionStart();
final int selectionEnd2 = mConnection.getExpectedSelectionEnd();
sendDownUpKeyEventWithMetaState(KeyEvent.KEYCODE_MOVE_END, KeyEvent.META_CTRL_ON | event.getMMetaState());
if (mConnection.getExpectedSelectionStart() == selectionStart2 && mConnection.getExpectedSelectionEnd() == selectionEnd2) {
// unchanged, try fallback e.g. for compose fields that don't care about ctrl + end
// we just move to a very large index, and hope the field is prepared to deal with this
// getting the actual length of the text for setting the correct position can be tricky for some apps...
try {
mConnection.setSelection(Integer.MAX_VALUE, Integer.MAX_VALUE);
final int newStart = (event.getMMetaState() & KeyEvent.META_SHIFT_MASK) != 0 ? selectionStart2 : Integer.MAX_VALUE;
mConnection.setSelection(newStart, Integer.MAX_VALUE);
} catch (Exception e) {
// better catch potential errors and just do nothing in this case
Log.i(TAG, "error when trying to move cursor to last position: " + e);
@ -766,6 +779,11 @@ public final class InputLogic {
case KeyCode.TIMESTAMP:
mLatinIME.onTextInput(TimestampKt.getTimestamp(mLatinIME));
break;
case KeyCode.SEND_INTENT_ONE, KeyCode.SEND_INTENT_TWO, KeyCode.SEND_INTENT_THREE:
IntentUtils.handleSendIntentKey(mLatinIME, event.getMKeyCode());
case KeyCode.IME_HIDE_UI:
mLatinIME.requestHideSelf(0);
break;
case KeyCode.VOICE_INPUT:
// switching to shortcut IME, shift state, keyboard,... is handled by LatinIME,
// {@link KeyboardSwitcher#onEvent(Event)}, or {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}.
@ -774,23 +792,20 @@ public final class InputLogic {
case KeyCode.CAPS_LOCK, KeyCode.EMOJI, KeyCode.TOGGLE_ONE_HANDED_MODE, KeyCode.SWITCH_ONE_HANDED_MODE:
break;
default:
if (KeyCode.INSTANCE.isModifier(event.getMKeyCode()))
return; // continuation of previous switch case, but modifiers are in a separate place
if (event.getMMetaState() != 0) {
// need to convert codepoint to KeyEvent.KEYCODE_<xxx>
final int codeToConvert = event.getMKeyCode() < 0 ? event.getMKeyCode() : event.getMCodePoint();
int keyEventCode = KeyCode.INSTANCE.toKeyEventCode(codeToConvert);
if (keyEventCode != KeyEvent.KEYCODE_UNKNOWN)
sendDownUpKeyEventWithMetaState(keyEventCode, event.getMMetaState());
return; // never crash if user inputs sth we don't have a KeyEvent.KEYCODE for
} else if (event.getMKeyCode() < 0) {
int keyEventCode = KeyCode.INSTANCE.toKeyEventCode(event.getMKeyCode());
if (KeyCode.INSTANCE.isModifier(keyCode))
return; // continuation of previous switch case above, but modifiers are held in a separate place
final int keyEventCode = keyCode > 0
? keyCode
: event.getMCodePoint() >= 0 ? KeyCode.codePointToKeyEventCode(event.getMCodePoint())
: KeyCode.keyCodeToKeyEventCode(keyCode);
if (keyEventCode != KeyEvent.KEYCODE_UNKNOWN) {
sendDownUpKeyEvent(keyEventCode);
sendDownUpKeyEventWithMetaState(keyEventCode, event.getMMetaState());
return;
}
}
throw new RuntimeException("Unknown key code : " + event.getMKeyCode());
// unknown event
Log.e(TAG, "unknown event, key code: "+keyCode+", meta: "+event.getMMetaState());
if (DebugFlags.DEBUG_ENABLED)
throw new RuntimeException("Unknown event");
}
}

View file

@ -58,6 +58,7 @@ object Defaults {
const val PREF_VIBRATE_ON = false
const val PREF_VIBRATE_IN_DND_MODE = false
const val PREF_SOUND_ON = false
const val PREF_SHOW_EMOJI_DESCRIPTIONS = true
@JvmField
var PREF_POPUP_ON = true
const val PREF_AUTO_CORRECTION = true

View file

@ -67,6 +67,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
public static final String PREF_VIBRATE_ON = "vibrate_on";
public static final String PREF_VIBRATE_IN_DND_MODE = "vibrate_in_dnd_mode";
public static final String PREF_SOUND_ON = "sound_on";
public static final String PREF_SHOW_EMOJI_DESCRIPTIONS = "show_emoji_descriptions";
public static final String PREF_POPUP_ON = "popup_on";
public static final String PREF_AUTO_CORRECTION = "auto_correction";
public static final String PREF_MORE_AUTO_CORRECTION = "more_auto_correction";

View file

@ -54,6 +54,7 @@ public class SettingsValues {
public final boolean mVibrateOn;
public final boolean mVibrateInDndMode;
public final boolean mSoundOn;
public final boolean mShowEmojiDescriptions;
public final boolean mKeyPreviewPopupOn;
public final boolean mShowsVoiceInputKey;
public final boolean mLanguageSwitchKeyToOtherImes;
@ -169,6 +170,7 @@ public class SettingsValues {
mVibrateOn = Settings.readVibrationEnabled(prefs);
mVibrateInDndMode = prefs.getBoolean(Settings.PREF_VIBRATE_IN_DND_MODE, Defaults.PREF_VIBRATE_IN_DND_MODE);
mSoundOn = prefs.getBoolean(Settings.PREF_SOUND_ON, Defaults.PREF_SOUND_ON);
mShowEmojiDescriptions = prefs.getBoolean(Settings.PREF_SHOW_EMOJI_DESCRIPTIONS, Defaults.PREF_SHOW_EMOJI_DESCRIPTIONS);
mKeyPreviewPopupOn = prefs.getBoolean(Settings.PREF_POPUP_ON, Defaults.PREF_POPUP_ON);
mSlidingKeyInputPreviewEnabled = prefs.getBoolean(
DebugSettings.PREF_SLIDING_KEY_INPUT_PREVIEW, Defaults.PREF_SLIDING_KEY_INPUT_PREVIEW);

View file

@ -116,6 +116,7 @@ class SuggestionStripView(context: Context, attrs: AttributeSet?, defStyle: Int)
private val toolbarArrowIcon = KeyboardIconsSet.instance.getNewDrawable(KeyboardIconsSet.NAME_TOOLBAR_KEY, context)
private val defaultToolbarBackground: Drawable = toolbarExpandKey.background
private val enabledToolKeyBackground = GradientDrawable()
private var direction = 1 // 1 if LTR, -1 if RTL
init {
val colors = Settings.getValues().mColors
@ -171,7 +172,6 @@ class SuggestionStripView(context: Context, attrs: AttributeSet?, defStyle: Int)
private lateinit var listener: Listener
private var suggestedWords = SuggestedWords.getEmptyInstance()
private var startIndexOfMoreSuggestions = 0
private var direction = 1 // 1 if LTR, -1 if RTL
private var isExternalSuggestionVisible = false // Required to disable the more suggestions if other suggestions are visible
private val layoutHelper = SuggestionStripLayoutHelper(context, attrs, defStyle, wordViews, dividerViews, debugInfoViews)
private val moreSuggestionsView = moreSuggestionsContainer.findViewById<MoreSuggestionsView>(R.id.more_suggestions_view).apply {
@ -234,6 +234,7 @@ class SuggestionStripView(context: Context, attrs: AttributeSet?, defStyle: Int)
context, suggestedWords, suggestionsStrip, this
)
isExternalSuggestionVisible = false
updateKeys();
}
fun setExternalSuggestionView(view: View?) {

View file

@ -101,6 +101,10 @@ object DictionaryInfoUtils {
return absoluteDirectoryName
}
@JvmStatic
fun getCachedDictForLocaleAndType(locale: Locale, type: String, context: Context): File? =
getCachedDictsForLocale(locale, context).firstOrNull { it.name.substringBefore("_") == type }
fun getCachedDictsForLocale(locale: Locale, context: Context) =
getCacheDirectoryForLocale(locale, context)?.let { File(it).listFiles() }.orEmpty()

View file

@ -0,0 +1,26 @@
package helium314.keyboard.latin.utils
import android.content.Context
import android.content.Intent
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode
import helium314.keyboard.latin.inputlogic.InputLogic
import helium314.keyboard.latin.utils.Log.i
object IntentUtils {
val TAG: String = InputLogic::class.java.simpleName
private val ACTION_SEND_INTENT = "helium314.keyboard.latin.ACTION_SEND_INTENT"
private val EXTRA_NUMBER = "EXTRA_NUMBER"
@JvmStatic
fun handleSendIntentKey(context: Context, mKeyCode: Int) {
val intentNumber = (KeyCode.SEND_INTENT_ONE + 1) - mKeyCode;
val intent: Intent = Intent(ACTION_SEND_INTENT).apply {
putExtra(EXTRA_NUMBER, intentNumber)
}
context.sendBroadcast(intent)
i(TAG, "Sent broadcast for intent number: $intentNumber")
}
}

View file

@ -7,9 +7,13 @@
package helium314.keyboard.latin.utils;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.SuggestionSpan;
import androidx.annotation.NonNull;
import java.util.Arrays;
import java.util.Objects;
/**
* Represents a range of text, relative to the current cursor position.
@ -95,6 +99,28 @@ public final class TextRange {
return writeIndex == readIndex ? spans : Arrays.copyOfRange(spans, 0, writeIndex);
}
@Override
public boolean equals(Object other) {
if (!(other instanceof TextRange textRange)) return false;
return mWordAtCursorStartIndex == textRange.mWordAtCursorStartIndex
&& mWordAtCursorEndIndex == textRange.mWordAtCursorEndIndex
&& mCursorIndex == textRange.mCursorIndex
&& mHasUrlSpans == textRange.mHasUrlSpans
&& TextUtils.equals(mTextAtCursor, textRange.mTextAtCursor)
&& TextUtils.equals(mWord, textRange.mWord);
}
@Override
public int hashCode() {
return Objects.hash(mTextAtCursor, mWordAtCursorStartIndex, mWordAtCursorEndIndex, mCursorIndex, mWord, mHasUrlSpans);
}
@NonNull
@Override
public String toString() {
return mTextAtCursor + ", " + mWord + ", " + mCursorIndex;
}
public TextRange(final CharSequence textAtCursor, final int wordAtCursorStartIndex,
final int wordAtCursorEndIndex, final int cursorIndex, final boolean hasUrlSpans) {
if (wordAtCursorStartIndex < 0 || cursorIndex < wordAtCursorStartIndex

View file

@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
@ -45,6 +46,7 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import helium314.keyboard.latin.R
@ -241,7 +243,8 @@ fun ExpandableSearchField(
}) { CloseIcon(android.R.string.cancel) } },
singleLine = true,
colors = colors,
textStyle = contentTextDirectionStyle
textStyle = contentTextDirectionStyle,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search)
)
}
}

View file

@ -47,12 +47,12 @@ fun NewDictionaryDialog(
} else if (header != null) {
val ctx = LocalContext.current
val dictLocale = header.mLocaleString.constructLocale()
var locale by remember { mutableStateOf(mainLocale ?: dictLocale) }
val enabledLanguages = SubtypeSettings.getEnabledSubtypes().map { it.locale().language }
val comparer = compareBy<Locale>({ it != mainLocale }, { it != dictLocale }, { it.language !in enabledLanguages }, { it.script() != dictLocale.script() })
val locales = SubtypeSettings.getAvailableSubtypeLocales()
.filter { it.script() == dictLocale.script() || it.script() == mainLocale?.script() }
.sortedWith(comparer)
var locale by remember { mutableStateOf(mainLocale ?: dictLocale.takeIf { it in locales } ?: locales.first()) }
val cacheDir = DictionaryInfoUtils.getCacheDirectoryForLocale(locale, ctx)
val dictFile = File(cacheDir, header.mIdString.substringBefore(":") + "_" + DictionaryInfoUtils.USER_DICTIONARY_SUFFIX)
val type = header.mIdString.substringBefore(":")

View file

@ -79,7 +79,8 @@ fun AppearanceScreen(
SettingsWithoutKey.CUSTOM_FONT,
Settings.PREF_FONT_SCALE,
Settings.PREF_EMOJI_FONT_SCALE,
Settings.PREF_EMOJI_KEY_FIT,
if (prefs.getFloat(Settings.PREF_EMOJI_FONT_SCALE, Defaults.PREF_EMOJI_FONT_SCALE) != 1f)
Settings.PREF_EMOJI_KEY_FIT else null,
if (prefs.getInt(Settings.PREF_EMOJI_MAX_SDK, Defaults.PREF_EMOJI_MAX_SDK) >= 24)
Settings.PREF_EMOJI_SKIN_TONE else null,
)
@ -109,6 +110,7 @@ fun createAppearanceSettings(context: Context) = listOf(
prefs.edit().remove(Settings.PREF_THEME_COLORS_NIGHT).apply()
}
KeyboardIconsSet.needsReload = true // only relevant for Settings.PREF_CUSTOM_ICON_NAMES
KeyboardSwitcher.getInstance().setThemeNeedsReload()
}
},
Setting(context, Settings.PREF_ICON_STYLE, R.string.icon_style) { setting ->

View file

@ -57,8 +57,11 @@ fun DictionaryScreen(
val enabledLanguages = SubtypeSettings.getEnabledSubtypes(true).map { it.locale().language }
val cachedDictFolders = DictionaryInfoUtils.getCacheDirectories(ctx).map { it.name }
val comparer = compareBy<Locale>({ it.language !in enabledLanguages }, { it.toLanguageTag() !in cachedDictFolders}, { it.displayName })
val dictionaryLocales = remember { getDictionaryLocales(ctx).sortedWith(comparer).toMutableList() }
dictionaryLocales.add(0, Locale(SubtypeLocaleUtils.NO_LANGUAGE))
val dictionaryLocales = remember {
getDictionaryLocales(ctx).sortedWith(comparer).toMutableList().apply {
add(0, Locale(SubtypeLocaleUtils.NO_LANGUAGE))
}
}
var selectedLocale: Locale? by remember { mutableStateOf(null) }
var showAddDictDialog by remember { mutableStateOf(false) }
val dictPicker = dictionaryFilePicker(selectedLocale)

View file

@ -20,12 +20,15 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
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.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@ -83,6 +86,11 @@ fun PersonalDictionaryScreen(
)
if (selectedWord != null) {
val selWord = selectedWord!!
val focusRequester = remember { FocusRequester() }
LaunchedEffect(selectedWord) {
if (selWord.word == "" && selWord.weight == null && selWord.shortcut == null)
focusRequester.requestFocus() // user clicked add word
}
var newWord by remember { mutableStateOf(selWord) }
var newLocale by remember { mutableStateOf(locale) }
val wordValid = (newWord.word == selWord.word && locale == newLocale) || !doesWordExist(newWord.word, newLocale, ctx)
@ -119,7 +127,7 @@ fun PersonalDictionaryScreen(
TextField(
value = newWord.word,
onValueChange = { newWord = newWord.copy(word = it) },
modifier = Modifier.fillMaxWidth(),
modifier = Modifier.fillMaxWidth().focusRequester(focusRequester),
singleLine = true
)
Row(

View file

@ -57,6 +57,7 @@ fun PreferencesScreen(
Settings.PREF_SOUND_ON,
if (prefs.getBoolean(Settings.PREF_SOUND_ON, Defaults.PREF_SOUND_ON))
Settings.PREF_KEYPRESS_SOUND_VOLUME else null,
Settings.PREF_SHOW_EMOJI_DESCRIPTIONS,
R.string.settings_category_additional_keys,
Settings.PREF_SHOW_NUMBER_ROW,
if (SubtypeSettings.getEnabledSubtypes(true).any { it.locale().language in localesWithLocalizedNumberRow })
@ -111,6 +112,14 @@ fun createPreferencesSettings(context: Context) = listOf(
Setting(context, Settings.PREF_SOUND_ON, R.string.sound_on_keypress) {
SwitchPreference(it, Defaults.PREF_SOUND_ON)
},
Setting(
context, Settings.PREF_SHOW_EMOJI_DESCRIPTIONS, R.string.show_emoji_descriptions,
R.string.show_emoji_descriptions_summary
) {
SwitchPreference(it, Defaults.PREF_SHOW_EMOJI_DESCRIPTIONS) {
KeyboardSwitcher.getInstance().reloadKeyboard()
}
},
Setting(context, Settings.PREF_ENABLE_CLIPBOARD_HISTORY,
R.string.enable_clipboard_history, R.string.enable_clipboard_history_summary)
{

View file

@ -24,6 +24,7 @@ import helium314.keyboard.latin.settings.Defaults
import helium314.keyboard.latin.settings.Settings
import helium314.keyboard.latin.utils.JniUtils
import helium314.keyboard.latin.utils.Log
import helium314.keyboard.latin.utils.ToolbarMode
import helium314.keyboard.latin.utils.getActivity
import helium314.keyboard.latin.utils.prefs
import helium314.keyboard.settings.NextScreenIcon
@ -49,7 +50,8 @@ fun TextCorrectionScreen(
if ((b?.value ?: 0) < 0)
Log.v("irrelevant", "stupid way to trigger recomposition on preference change")
val autocorrectEnabled = prefs.getBoolean(Settings.PREF_AUTO_CORRECTION, Defaults.PREF_AUTO_CORRECTION)
val suggestionsEnabled = prefs.getBoolean(Settings.PREF_SHOW_SUGGESTIONS, Defaults.PREF_SHOW_SUGGESTIONS)
val suggestionsVisible = Settings.readToolbarMode(prefs) in setOf(ToolbarMode.SUGGESTION_STRIP, ToolbarMode.EXPANDABLE)
val suggestionsEnabled = suggestionsVisible && prefs.getBoolean(Settings.PREF_SHOW_SUGGESTIONS, Defaults.PREF_SHOW_SUGGESTIONS)
val gestureEnabled = JniUtils.sHaveGestureLib && prefs.getBoolean(Settings.PREF_GESTURE_INPUT, Defaults.PREF_GESTURE_INPUT)
val items = listOf(
SettingsWithoutKey.EDIT_PERSONAL_DICTIONARY,
@ -69,7 +71,7 @@ fun TextCorrectionScreen(
if (gestureEnabled) Settings.PREF_AUTOSPACE_AFTER_GESTURE_TYPING else null,
Settings.PREF_SHIFT_REMOVES_AUTOSPACE,
R.string.settings_category_suggestions,
Settings.PREF_SHOW_SUGGESTIONS,
if (suggestionsVisible) Settings.PREF_SHOW_SUGGESTIONS else null,
if (suggestionsEnabled) Settings.PREF_ALWAYS_SHOW_SUGGESTIONS else null,
if (suggestionsEnabled && prefs.getBoolean(Settings.PREF_ALWAYS_SHOW_SUGGESTIONS, Defaults.PREF_ALWAYS_SHOW_SUGGESTIONS))
Settings.PREF_ALWAYS_SHOW_SUGGESTIONS_EXCEPT_WEB_TEXT else null,

View file

@ -10,6 +10,15 @@
android:layout_height="wrap_content"
android:orientation="vertical"
>
<helium314.keyboard.keyboard.PopupTextView
android:id="@+id/description_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingHorizontal="8dp"
android:paddingBottom="4dp"
android:visibility="gone"
android:maxLines="1" android:ellipsize="end"
style="?attr/popupKeysKeyboardViewStyle" />
<helium314.keyboard.keyboard.PopupKeysKeyboardView
android:id="@+id/popup_keys_keyboard_view"
android:layout_width="wrap_content"

View file

@ -426,4 +426,13 @@
<string name="use_apps_dict_summary">Namen der installierten Apps für Vorschläge und Korrekturen verwenden</string>
<string name="prefs_emoji_skin_tone">Standard-Emoji-Hautton</string>
<string name="prefs_emoji_skin_tone_neutral">Neutral</string>
<string name="toolbar_mode_expandable">Symbolleiste Tasten und Vorschläge</string>
<string name="toolbar_mode_toolbar_keys">Symbolleiste nur Tasten</string>
<string name="popup_order_and_hint_source">Popup Tasten Reihenfolge und Vorschlag Quelle</string>
<string name="toolbar_mode_hidden">Versteckt</string>
<string name="toolbar_hiding_global">Auch Zwischenablage und Emoji Symbolleisten verstecken</string>
<string name="subtype_with_layout_generic">%1$s (%2$s)</string>
<string name="landscape">Querformat</string>
<string name="toolbar_mode_suggestion_strip">Nur Vorschläge</string>
<string name="toolbar_mode">Symbolleiste Modus</string>
</resources>

View file

@ -1,10 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2008 The Android Open Source Project
modified
SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
--><resources>
<string name="use_contacts_dict">"Look up contact names"</string>
<string name="vibrate_on_keypress">"Vibrate on keypress"</string>
<string name="sound_on_keypress">"Sound on keypress"</string>
@ -109,4 +108,29 @@
<string name="label_search_key">"Search"</string>
<string name="label_pause_key">"Pause"</string>
<string name="label_wait_key">"Wait"</string>
<string name="split_spacer_scale_landscape">Split distance (landscape)</string>
<string name="spell_checker_service_name">HeliBoard Spell Checker</string>
<string name="ime_settings">HeliBoard Settings</string>
<string name="android_spell_checker_settings">HeliBoard Spell Checker Settings</string>
<string name="split_spacer_scale">Split distance</string>
<string name="show_emoji_key">Emoji key</string>
<string name="switch_language">Switch Language</string>
<string name="settings_category_input">Input</string>
<string name="settings_category_additional_keys">Additional keys</string>
<string name="settings_category_suggestions">Suggestions</string>
<string name="vibrate_in_dnd_mode">Vibrate in do not disturb mode</string>
<string name="settings_category_correction">Corrections</string>
<string name="enable_split_keyboard_landscape">Enable split keyboard (landscape)</string>
<string name="disable_personalized_dicts_message">Warning: Disabling this setting will clear learned data</string>
<string name="settings_category_clipboard_history">Clipboard history</string>
<string name="settings_category_space">Space</string>
<string name="settings_category_experimental">Experimental</string>
<string name="settings_category_miscellaneous">Uncategorised</string>
<string name="language_switch_key_switch_both">Switch both</string>
<string name="language_switch_key_behavior">Language switch key behavior</string>
<string name="abbreviation_unit_minutes">%s min</string>
<string name="settings_no_limit">No limit</string>
<string name="add_to_personal_dictionary">Add learnt words to personal dictionary</string>
<string name="use_apps_dict">Look up app names</string>
<string name="use_apps_dict_summary">Use names of installed apps for suggestions and corrections</string>
</resources>

View file

@ -124,7 +124,7 @@ Nouveau dictionnaire:
<string name="dictionary_file_wrong_script">Erreur: script non compatible avec ce clavier</string>
<string name="dictionary_file_wrong_locale_ok">"Utiliser quand même"</string>
<string name="dictionary_load_error">"Erreur de chargement du fichier dictionnaire"</string>
<string name="dictionary_available">"Dictionnaire disponible"</string>
<string name="dictionary_available">Dictionnaires disponibles</string>
<string name="last_update">"Dernière mise à jour"</string>
<string name="delete">"Supprimer"</string>
<string name="version_text">Version %s</string>

View file

@ -428,11 +428,19 @@
<string name="timestamp_format_title">Formato per il tasto data/ora</string>
<string name="subtype_dag">Dagbani</string>
<string name="subtype_st">Sesotho</string>
<string name="prefs_emoji_key_fit">Scala la dimensione dei tasti con la dimensione dei caratteri</string>
<string name="prefs_emoji_key_fit">Ridimensiona il tasto emoji in base alla dimensione dei caratteri</string>
<string name="subtype_with_layout_generic">%1$s (%2$s)</string>
<string name="backup_restored">Backup ripristinato</string>
<string name="use_apps_dict">Suggerisci nomi delle app installate</string>
<string name="use_apps_dict_summary">Aggiunge i nomi delle app installate alla lista di suggerimenti e correzioni</string>
<string name="prefs_emoji_skin_tone">Tonalità di pelle predefinita per le emoji</string>
<string name="prefs_emoji_skin_tone_neutral">Neutro (giallo)</string>
<string name="toolbar_mode_expandable">Tasti funzione e suggerimenti</string>
<string name="toolbar_mode_toolbar_keys">Solo tasti funzione</string>
<string name="toolbar_mode_hidden">Nascosta</string>
<string name="toolbar_hiding_global">Nascondi anche le barre degli appunti e delle emoji</string>
<string name="landscape">Orizzontale</string>
<string name="popup_order_and_hint_source">Ordine dei caratteri popup e fonti dei suggerimenti</string>
<string name="toolbar_mode_suggestion_strip">Solo suggerimenti</string>
<string name="toolbar_mode">Modalità</string>
</resources>

View file

@ -17,6 +17,10 @@
<string name="vibrate_in_dnd_mode">Vibrate in do not disturb mode</string>
<!-- Option to play back sound on keypress in soft keyboard -->
<string name="sound_on_keypress">Sound on keypress</string>
<!-- Option to show emoji descriptions on long press -->
<string name="show_emoji_descriptions">Show emoji description on long press</string>
<!-- Description for option to show emoji descriptions on long press -->
<string name="show_emoji_descriptions_summary">Requires an emoji dictionary</string>
<!-- Option to control whether or not to show a popup with a larger font on each key press. -->
<string name="popup_on_keypress">Popup on keypress</string>
<!-- Settings screen title for preferences-->

View file

@ -79,6 +79,7 @@
kn_IN: Kannada (India)/kannada
kn_IN: Kannada Extended (India)/kannada
ky: Kyrgyz/russian
la: Latin
lg: Luganda/luganda # This is a preliminary keyboard layout.
ln: Lingala/lingala # This is a preliminary keyboard layout.
lo_LA: Lao (Laos)/lao
@ -828,6 +829,15 @@
android:imeSubtypeMode="keyboard"
android:imeSubtypeExtraValue="KeyboardLayoutSet=MAIN:qwertz,AsciiCapable,EmojiCapable"
/>
<subtype android:icon="@drawable/ic_ime_switcher"
android:label="@string/subtype_generic"
android:subtypeId="0x37885a0c"
android:imeSubtypeLocale="la"
android:languageTag="la"
android:imeSubtypeMode="keyboard"
android:imeSubtypeExtraValue="AsciiCapable,SupportTouchPositionCorrection,EmojiCapable"
android:isAsciiCapable="true"
/>
<subtype android:icon="@drawable/ic_ime_switcher"
android:label="@string/subtype_generic"
android:subtypeId="0x1ec2b4c9"

View file

@ -92,6 +92,9 @@
<subtype
android:label="@string/subtype_generic"
android:subtypeLocale="kn" />
<subtype
android:label="@string/subtype_generic"
android:subtypeLocale="la" />
<subtype
android:label="@string/subtype_generic"
android:subtypeLocale="lb" />

View file

@ -4,9 +4,13 @@ package helium314.keyboard.latin
import androidx.test.core.app.ApplicationProvider
import helium314.keyboard.ShadowInputMethodManager2
import helium314.keyboard.latin.common.StringUtils
import helium314.keyboard.latin.common.endsWithWordCodepoint
import helium314.keyboard.latin.common.getFullEmojiAtEnd
import helium314.keyboard.latin.common.getTouchedWordRange
import helium314.keyboard.latin.common.nonWordCodePointAndNoSpaceBeforeCursor
import helium314.keyboard.latin.settings.SpacingAndPunctuations
import helium314.keyboard.latin.utils.ScriptUtils
import helium314.keyboard.latin.utils.TextRange
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@ -60,6 +64,54 @@ class StringUtilsTest {
assert(nonWordCodePointAndNoSpaceBeforeCursor("th.is", sp))
}
@Test fun `is word-like at end`() {
val sp = SpacingAndPunctuations(ApplicationProvider.getApplicationContext<App>().resources, false)
assert(!endsWithWordCodepoint("", sp))
assert(endsWithWordCodepoint("don'", sp))
assert(!endsWithWordCodepoint("hello!", sp))
assert(!endsWithWordCodepoint("when ", sp))
assert(endsWithWordCodepoint("3-", sp)) // todo: this seems wrong
assert(endsWithWordCodepoint("5'", sp)) // todo: this seems wrong
assert(endsWithWordCodepoint("1", sp)) // todo: this seems wrong
assert(endsWithWordCodepoint("a-", sp))
assert(!endsWithWordCodepoint("--", sp))
}
@Test fun `get touched text range`() {
val sp = SpacingAndPunctuations(ApplicationProvider.getApplicationContext<App>().resources, false)
val spUrl = SpacingAndPunctuations(ApplicationProvider.getApplicationContext<App>().resources, true)
val script = ScriptUtils.SCRIPT_LATIN
checkTextRange("blabla this is v", "ery good", sp, script, 15, 19)
checkTextRange(".hel", "lo...", sp, script, 1, 6)
checkTextRange("(hi", ")", sp, script, 1, 3)
checkTextRange("", "word", sp, script, 0, 4)
checkTextRange("mail: blorb@", "florb.com or", sp, script, 12, 17)
checkTextRange("mail: blorb@", "florb.com or", spUrl, script, 6, 21)
checkTextRange("mail: blor", "b@florb.com or", sp, script, 6, 11)
checkTextRange("mail: blor", "b@florb.com or", spUrl, script, 6, 21)
checkTextRange("mail: blorb@f", "lorb.com or", sp, script, 12, 17)
checkTextRange("mail: blorb@f", "lorb.com or", spUrl, script, 6, 21)
checkTextRange("http://exam", "ple.com", sp, script, 7, 14)
checkTextRange("http://exam", "ple.com", spUrl, script, 7, 18)
checkTextRange("http://example.", "com", sp, script, 15, 18)
checkTextRange("http://example.", "com", spUrl, script, 7, 18)
checkTextRange("htt", "p://example.com", sp, script, 0, 4)
checkTextRange("htt", "p://example.com", spUrl, script, 0, 18)
checkTextRange("http:/", "/example.com", sp, script, 6, 6)
checkTextRange("http:/", "/example.com", spUrl, script, 0, 18)
checkTextRange("..", ".", spUrl, script, 2, 2)
checkTextRange("...", "", spUrl, script, 3, 3)
// todo: these are bad cases of url detection
// also: sometimesWordConnectors are for URL and should be named accordingly
checkTextRange("@@@", "@@@", spUrl, script, 0, 6)
checkTextRange("a...", "", spUrl, script, 0, 4)
checkTextRange("@@@", "", spUrl, script, 0, 3)
}
@Test fun detectEmojisAtEnd() {
assertEquals("", getFullEmojiAtEnd("\uD83C\uDF83 "))
assertEquals("", getFullEmojiAtEnd("a"))
@ -87,4 +139,10 @@ class StringUtilsTest {
// could help towards fully fixing https://github.com/Helium314/HeliBoard/issues/22
// though this might be tricky, as some emojis will show as one on new Android versions, and
// as two on older versions
private fun checkTextRange(before: String, after: String, sp: SpacingAndPunctuations, script: String, wordStart: Int, WordEnd: Int) {
val got = getTouchedWordRange(before, after, script, sp)
val wanted = TextRange(before + after, wordStart, WordEnd, before.length, false)
assertEquals(wanted, got)
}
}

View file

@ -0,0 +1,10 @@
* إضافة أوضاع شريط الأدوات (تسمح بإخفاء شريط الأدوات)
* إضافة بعض متغيرات الرموز التعبيرية المفقودة
* تحسين شاشة النوع الفرعي وحوار القاموس
* إصلاح الألوان عند فرض الوضع الداكن
* نقل معظم إعدادات مقياس الوضع الرأسي/الأفقي إلى حوار
* إزالة ترجمات السلاسل الموسومة على أنها غير قابلة للترجمة
* إصلاح اتجاه سهم الشاشة التالية للغات اليمين إلى اليسار
* إصلاح التحميل الصحيح للغة العبرية على أندرويد 15
* توفير لوحة مفاتيح أساسية على الأقل عندما لا تعمل المكتبة على الإطلاق
* إصلاحات أخطاء طفيفة

View file

@ -0,0 +1,10 @@
* إضافة أوضاع شريط الأدوات (تسمح بإخفاء شريط الأدوات)
* إضافة بعض متغيرات الرموز التعبيرية المفقودة
* تحسين شاشة النوع الفرعي وحوار القاموس
* إصلاح الألوان عند فرض الوضع الداكن
* نقل معظم إعدادات مقياس الوضع الرأسي/الأفقي إلى حوار
* إزالة ترجمات السلاسل الموسومة على أنها غير قابلة للترجمة
* إصلاح اتجاه سهم الشاشة التالية للغات اليمين إلى اليسار
* إصلاح التحميل الصحيح للغة العبرية على أندرويد 15
* توفير لوحة مفاتيح أساسية على الأقل عندما لا تعمل المكتبة على الإطلاق
* إصلاحات أخطاء طفيفة

View file

@ -0,0 +1,10 @@
* af. modes de barra d'eines (permet ocultar-la)
* af. alguns emojis
* millorar pantalla de subtipus i el diàleg del dicc.
* corr. colors en forçar el mode fosc
* moure la majoria de la configuració d'escala vert/horiz a un diàleg
* elim. les trads. de les cadenes marcades com a no traduïbles
* corr. direcció de fletxa de la pantalla següent a idiomes RTL
* corr. la càrrega correcta de la config. regional hebrea a Android 15
* tenir almenys un teclat bàsic quan la biblio. no funciona
* corr. etc

View file

@ -0,0 +1,10 @@
* af. modes de barra d'eines (permet ocultar-la)
* af. alguns emojis
* millorar pantalla de subtipus i el diàleg del dicc.
* corr. colors en forçar el mode fosc
* moure la majoria de la configuració d'escala vert/horiz a un diàleg
* elim. les trads. de les cadenes marcades com a no traduïbles
* corr. direcció de fletxa de la pantalla següent a idiomes RTL
* corr. la càrrega correcta de la config. regional hebrea a Android 15
* tenir almenys un teclat bàsic quan la biblio. no funciona
* corr. etc

View file

@ -0,0 +1,10 @@
* přidány režimy panelu nástrojů (umožňuje skrýt p. n.)
* přidány chybějící varianty emodži
* vylepšena obrazovka podtypů a dialog slovníku
* opraveny barvy při vynuceném tmavém režimu
* přesun většiny nastavení měřítka do dialogu
* odstraněny překlady řetězců označených jako nepřekládatelné
* opraven směr šipky „dále“ pro jazyky RTL
* opraveno správné načítání hebrejského jazyka na Androidu 15
* zajištění alespoň základní klávesnice, když knihovna vůbec nefunguje
* drobné opravy chyb

View file

@ -0,0 +1,10 @@
* přidány režimy panelu nástrojů (umožňuje skrýt p. n.)
* přidány chybějící varianty emodži
* vylepšena obrazovka podtypů a dialog slovníku
* opraveny barvy při vynuceném tmavém režimu
* přesun většiny nastavení měřítka do dialogu
* odstraněny překlady řetězců označených jako nepřekládatelné
* opraven směr šipky „dále“ pro jazyky RTL
* opraveno správné načítání hebrejského jazyka na Androidu 15
* zajištění alespoň základní klávesnice, když knihovna vůbec nefunguje
* drobné opravy chyb

View file

@ -0,0 +1,10 @@
* add toolbar modes (allows hiding toolbar)
* add some missing emoji variants
* improve subtype screen and dictionary dialog
* fix colors when forcing dark mode
* move most of the portrait / landscape scale settings into a dialog
* remove translations of strings marked as non-translatable
* fix next-screen arrow direction for RTL languages
* fix proper loading of Hebrew locale on Android 15
* have at least a basic keyboard when library doesn't work at all
* minor bug fixes

View file

@ -0,0 +1,10 @@
* הוספת מצבי סרגל כלים (מאפשר הסתרת סרגלי כלים)
* הוספת גווני עור חסרים לחלק מהאמוג'ים
* שיפור מסך הכתב התחתי ודו-שיח המילון
* תיקון הצבעים בעת כפיית מצב כהה
* העברת מרבית הגדרות קנה-המידה של מצב אנכי / אופקי לדו-שיח
* הסרת תרגומי מחרוזות המסומנות כבלתי ניתנות לתרגום
* תיקון כיווני החץ למעבר למסך הבא עבור שםות הנכתבות מימין לשמאל
* תיקון טעינה נכונה של הגדרות השפה העברית באנדרואיד 15
* הצגת מקלדת בסיסית לכל הפחות כאשר ספריה איננה עובדת כלל
* תיקוני שגיאות שוליות

View file

@ -0,0 +1,10 @@
* הוספת מצבי סרגל כלים (מאפשר הסתרת סרגלי כלים)
* הוספת גווני עור חסרים לחלק מהאמוג'ים
* שיפור מסך הכתב התחתי ודו-שיח המילון
* תיקון הצבעים בעת כפיית מצב כהה
* העברת מרבית הגדרות קנה-המידה של מצב אנכי / אופקי לדו-שיח
* הסרת תרגומי מחרוזות המסומנות כבלתי ניתנות לתרגום
* תיקון כיווני החץ למעבר למסך הבא עבור שםות הנכתבות מימין לשמאל
* תיקון טעינה נכונה של הגדרות השפה העברית באנדרואיד 15
* הצגת מקלדת בסיסית לכל הפחות כאשר ספריה איננה עובדת כלל
* תיקוני שגיאות שוליות

View file

@ -0,0 +1,10 @@
* добавлены режимы панели инструментов (появилась возможность скрывать панель)
* добавлены некоторые недостающие варианты эмодзи
* улучшены экран выбора подтипа и диалог словаря
* исправлены цвета при принудительном тёмном режиме
* большинство настроек масштабирования для портретного и ландшафтного режимов перенесены в отдельный диалог
* удалены переводы строк, отмеченных как не подлежащие переводу
* исправлено направление стрелки перехода на следующий экран для языков с письмом справа налево (RTL)
* исправлена корректная загрузка иврита на Android 15
* обеспечена работа хотя бы базовой клавиатуры, если библиотека не функционирует вовсе
* мелкие исправления ошибок

View file

@ -0,0 +1,10 @@
* добавлены режимы панели инструментов (появилась возможность скрывать панель)
* добавлены некоторые недостающие варианты эмодзи
* улучшены экран выбора подтипа и диалог словаря
* исправлены цвета при принудительном тёмном режиме
* большинство настроек масштабирования для портретного и ландшафтного режимов перенесены в отдельный диалог
* удалены переводы строк, отмеченных как не подлежащие переводу
* исправлено направление стрелки перехода на следующий экран для языков с письмом справа налево (RTL)
* исправлена корректная загрузка иврита на Android 15
* обеспечена работа хотя бы базовой клавиатуры, если библиотека не функционирует вовсе
* мелкие исправления ошибок

View file

@ -73,7 +73,7 @@ If the layout has exactly 2 keys in the bottom row, these keys will replace comm
* `0.1` for phones
* `0.09` for tablets
* If the sum of widths in a row is greater than 1, keys are rescaled to fit on the screen
* `labelFlags`: allows specific effects, see [here](app/src/main/res/values/attrs.xml#L251-L287) in the section _keyLabelFlags_ for names and numeric values
* `labelFlags`: allows specific effects, see [here](app/src/main/res/values/attrs.xml#L250-L282) in the section _keyLabelFlags_ for names and numeric values
* Since json does not support hexadecimal-values, you have to use the decimal values in the comments in the same line.
* In case you want to apply multiple flags, you will need to combine them using [bitwise OR](https://en.wikipedia.org/wiki/Bitwise_operation#OR). In most cases this means you can just add the individual values, only exceptions are `fontDefault`, `followKeyLabelRatio`, `followKeyHintLabelRatio`, and `autoScale`.
@ -106,6 +106,12 @@ Usually the label is what is displayed on the key. However, there are some speci
You can also specify special key codes like `a|!code/key_action_previous` or `abc|!code/-10043`, but it's cleaner to use a json layout and specify the code explicitly. Note that when specifying a code in the label, and a code in a json layout, the code in the label will be ignored.
* It's also possible to specify an icon, like `!icon/previous_key|!code/key_action_previous`.
* You can find available icon names in [KeyboardIconsSet](/app/src/main/java/helium314/keyboard/keyboard/internal/KeyboardIconsSet.kt). You can also use toolbar key icons using the uppercase name of the [toolbar key](/app/src/main/java/helium314/keyboard/latin/utils/ToolbarUtils.kt#L109), e.g. `!icon/redo`
* There are some further special labels to be used in popup keys (i.e. one of the popup keys should have the label)
* `!noPanelAutoPopupKey!`: no popups are shown, a long press will result in the first normal popup of the key being selected
* `!needsDividers!`: dividers are shown between popup keys
* `!hasLabels!`: reduces text size in popup keys for nicer display of labels instead of letters
* `!autoColumnOrder!`: use with a number, e.g. _!autoColumnOrder!4_ will result in 4 popup columns
* `!fixedColumnOrder!`: use with a number, e.g. _!fixedColumnOrder!4_ will result in 4 popup columns. Keys will not be re-ordered if the result is a single line.
## Adding new layouts / languages
* You need a layout file in one of the formats above, and add it to [layouts](app/src/main/assets/layouts)

View file

@ -33,36 +33,6 @@ class EmojiData {
}
private fun onEmojiInserted(group: EmojiGroup, emoji: EmojiSpec): Boolean {
// Unicode RGI does not include letter symbols but Android supports them, so we inject them manually.
if (emoji.codes contentEquals RAW_CPS_KEYCAP_HASH) {
insertEmoji(group, intArrayOf(CP_REGIONAL_INDICATOR_SYMBOL_LETTER_A), 2.0f, "regional indicator symbol letter a")
insertEmoji(group, intArrayOf(CP_REGIONAL_INDICATOR_SYMBOL_LETTER_B), 2.0f, "regional indicator symbol letter b")
insertEmoji(group, intArrayOf(CP_REGIONAL_INDICATOR_SYMBOL_LETTER_C), 2.0f, "regional indicator symbol letter c")
insertEmoji(group, intArrayOf(CP_REGIONAL_INDICATOR_SYMBOL_LETTER_D), 2.0f, "regional indicator symbol letter d")
insertEmoji(group, intArrayOf(CP_REGIONAL_INDICATOR_SYMBOL_LETTER_E), 2.0f, "regional indicator symbol letter e")
insertEmoji(group, intArrayOf(CP_REGIONAL_INDICATOR_SYMBOL_LETTER_F), 2.0f, "regional indicator symbol letter f")
insertEmoji(group, intArrayOf(CP_REGIONAL_INDICATOR_SYMBOL_LETTER_G), 2.0f, "regional indicator symbol letter g")
insertEmoji(group, intArrayOf(CP_REGIONAL_INDICATOR_SYMBOL_LETTER_H), 2.0f, "regional indicator symbol letter h")
insertEmoji(group, intArrayOf(CP_REGIONAL_INDICATOR_SYMBOL_LETTER_I), 2.0f, "regional indicator symbol letter i")
insertEmoji(group, intArrayOf(CP_REGIONAL_INDICATOR_SYMBOL_LETTER_J), 2.0f, "regional indicator symbol letter j")
insertEmoji(group, intArrayOf(CP_REGIONAL_INDICATOR_SYMBOL_LETTER_K), 2.0f, "regional indicator symbol letter k")
insertEmoji(group, intArrayOf(CP_REGIONAL_INDICATOR_SYMBOL_LETTER_L), 2.0f, "regional indicator symbol letter l")
insertEmoji(group, intArrayOf(CP_REGIONAL_INDICATOR_SYMBOL_LETTER_M), 2.0f, "regional indicator symbol letter m")
insertEmoji(group, intArrayOf(CP_REGIONAL_INDICATOR_SYMBOL_LETTER_N), 2.0f, "regional indicator symbol letter n")
insertEmoji(group, intArrayOf(CP_REGIONAL_INDICATOR_SYMBOL_LETTER_O), 2.0f, "regional indicator symbol letter o")
insertEmoji(group, intArrayOf(CP_REGIONAL_INDICATOR_SYMBOL_LETTER_P), 2.0f, "regional indicator symbol letter p")
insertEmoji(group, intArrayOf(CP_REGIONAL_INDICATOR_SYMBOL_LETTER_Q), 2.0f, "regional indicator symbol letter q")
insertEmoji(group, intArrayOf(CP_REGIONAL_INDICATOR_SYMBOL_LETTER_R), 2.0f, "regional indicator symbol letter r")
insertEmoji(group, intArrayOf(CP_REGIONAL_INDICATOR_SYMBOL_LETTER_S), 2.0f, "regional indicator symbol letter s")
insertEmoji(group, intArrayOf(CP_REGIONAL_INDICATOR_SYMBOL_LETTER_T), 2.0f, "regional indicator symbol letter t")
insertEmoji(group, intArrayOf(CP_REGIONAL_INDICATOR_SYMBOL_LETTER_U), 2.0f, "regional indicator symbol letter u")
insertEmoji(group, intArrayOf(CP_REGIONAL_INDICATOR_SYMBOL_LETTER_V), 2.0f, "regional indicator symbol letter v")
insertEmoji(group, intArrayOf(CP_REGIONAL_INDICATOR_SYMBOL_LETTER_W), 2.0f, "regional indicator symbol letter w")
insertEmoji(group, intArrayOf(CP_REGIONAL_INDICATOR_SYMBOL_LETTER_X), 2.0f, "regional indicator symbol letter x")
insertEmoji(group, intArrayOf(CP_REGIONAL_INDICATOR_SYMBOL_LETTER_Y), 2.0f, "regional indicator symbol letter y")
insertEmoji(group, intArrayOf(CP_REGIONAL_INDICATOR_SYMBOL_LETTER_Z), 2.0f, "regional indicator symbol letter z")
}
// Some multi-skin-tone variants use a different base code than their non-multi-skin-tone counterparts,
// so they don't get grouped. We drop them here, to prevent each variant from being displayed separately.
return ! hasMultipleSkinModifiers(emoji.codes)
@ -118,9 +88,6 @@ class EmojiData {
}
companion object {
private val RAW_CPS_KEYCAP_HASH = intArrayOf(0x0023, 0xFE0F, 0x20E3)
const val CP_NUL = 0x0000
private const val CP_ZWJ = 0x200D
@ -136,34 +103,5 @@ class EmojiData {
private const val CP_WHITE_HAIR = 0x1F9B3
private const val CP_BARLD = 0x1F9B2
private const val CP_VARIANT_SELECTOR = 0xFE0F
private const val CP_REGIONAL_INDICATOR_SYMBOL_LETTER_A = 0x1F1E6
private const val CP_REGIONAL_INDICATOR_SYMBOL_LETTER_B = 0x1F1E7
private const val CP_REGIONAL_INDICATOR_SYMBOL_LETTER_C = 0x1F1E8
private const val CP_REGIONAL_INDICATOR_SYMBOL_LETTER_D = 0x1F1E9
private const val CP_REGIONAL_INDICATOR_SYMBOL_LETTER_E = 0x1F1EA
private const val CP_REGIONAL_INDICATOR_SYMBOL_LETTER_F = 0x1F1EB
private const val CP_REGIONAL_INDICATOR_SYMBOL_LETTER_G = 0x1F1EC
private const val CP_REGIONAL_INDICATOR_SYMBOL_LETTER_H = 0x1F1ED
private const val CP_REGIONAL_INDICATOR_SYMBOL_LETTER_I = 0x1F1EE
private const val CP_REGIONAL_INDICATOR_SYMBOL_LETTER_J = 0x1F1EF
private const val CP_REGIONAL_INDICATOR_SYMBOL_LETTER_K = 0x1F1F0
private const val CP_REGIONAL_INDICATOR_SYMBOL_LETTER_L = 0x1F1F1
private const val CP_REGIONAL_INDICATOR_SYMBOL_LETTER_M = 0x1F1F2
private const val CP_REGIONAL_INDICATOR_SYMBOL_LETTER_N = 0x1F1F3
private const val CP_REGIONAL_INDICATOR_SYMBOL_LETTER_O = 0x1F1F4
private const val CP_REGIONAL_INDICATOR_SYMBOL_LETTER_P = 0x1F1F5
private const val CP_REGIONAL_INDICATOR_SYMBOL_LETTER_Q = 0x1F1F6
private const val CP_REGIONAL_INDICATOR_SYMBOL_LETTER_R = 0x1F1F7
private const val CP_REGIONAL_INDICATOR_SYMBOL_LETTER_S = 0x1F1F8
private const val CP_REGIONAL_INDICATOR_SYMBOL_LETTER_T = 0x1F1F9
private const val CP_REGIONAL_INDICATOR_SYMBOL_LETTER_U = 0x1F1FA
private const val CP_REGIONAL_INDICATOR_SYMBOL_LETTER_V = 0x1F1FB
private const val CP_REGIONAL_INDICATOR_SYMBOL_LETTER_W = 0x1F1FC
private const val CP_REGIONAL_INDICATOR_SYMBOL_LETTER_X = 0x1F1FD
private const val CP_REGIONAL_INDICATOR_SYMBOL_LETTER_Y = 0x1F1FE
private const val CP_REGIONAL_INDICATOR_SYMBOL_LETTER_Z = 0x1F1FF
}
}

View file

@ -3866,31 +3866,5 @@ U+1F532 # black square button
U+1F3C1 # chequered flag
U+1F6A9 # triangular flag
U+1F38C # crossed flags
U+1F1E6 # regional indicator symbol letter a
U+1F1E7 # regional indicator symbol letter b
U+1F1E8 # regional indicator symbol letter c
U+1F1E9 # regional indicator symbol letter d
U+1F1EA # regional indicator symbol letter e
U+1F1EB # regional indicator symbol letter f
U+1F1EC # regional indicator symbol letter g
U+1F1ED # regional indicator symbol letter h
U+1F1EE # regional indicator symbol letter i
U+1F1EF # regional indicator symbol letter j
U+1F1F0 # regional indicator symbol letter k
U+1F1F1 # regional indicator symbol letter l
U+1F1F2 # regional indicator symbol letter m
U+1F1F3 # regional indicator symbol letter n
U+1F1F4 # regional indicator symbol letter o
U+1F1F5 # regional indicator symbol letter p
U+1F1F6 # regional indicator symbol letter q
U+1F1F7 # regional indicator symbol letter r
U+1F1F8 # regional indicator symbol letter s
U+1F1F9 # regional indicator symbol letter t
U+1F1FA # regional indicator symbol letter u
U+1F1FB # regional indicator symbol letter v
U+1F1FC # regional indicator symbol letter w
U+1F1FD # regional indicator symbol letter x
U+1F1FE # regional indicator symbol letter y
U+1F1FF # regional indicator symbol letter z
# Above emojis are supported from Android 4.4 (API level 19)