Migrate functional key layouts to json (#778)

Now the functional key layouts should be (mostly) compatible to FlorisBoard
Not yet customizable, this is a large step towards customizable functional key layouts
This commit is contained in:
Helium314 2024-05-11 15:41:00 +02:00 committed by GitHub
parent 691ae017bc
commit 34d8bd16f0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 931 additions and 712 deletions

View file

@ -0,0 +1,21 @@
[
[
{ "label": "shift", "width": 0.15 },
{ "type": "placeholder" },
{ "label": "delete", "width": 0.15 }
],
[
{ "label": "symbol_alpha", "width": 0.15 },
{ "$": "variation_selector",
"default": { "label": "comma" },
"email": { "label": "@", "groupId": 1, "type": "function" },
"uri": { "label": "/", "groupId": 1, "type": "function" }
},
{ "label": "language_switch" },
{ "label": "emoji" },
{ "label": "numpad" },
{ "label": "space" },
{ "label": "period" },
{ "label": "action", "width": 0.15 }
]
]

View file

@ -0,0 +1,36 @@
[
[
{ "type": "placeholder" },
{ "label": "delete", "width": 0.1 }
],
[
{ "type": "placeholder" }
],
[
{ "type": "placeholder" },
{ "label": "action", "width": 0.1 }
],
[
{ "label": "shift", "width": 0.1 },
{ "type": "placeholder" },
{ "label": "shift" }
],
[
{ "label": "symbol_alpha" },
{ "$": "variation_selector",
"default": { "label": "comma" },
"email": { "label": "@", "groupId": 1, "type": "function" },
"uri": { "label": "/", "groupId": 1, "type": "function" }
},
{ "label": "language_switch" },
{ "label": "emoji" },
{ "label": "numpad" },
{ "label": "space" },
{ "label": "period" },
{ "$": "variation_selector",
"default": { "label": "emoji" },
"email": { "label": "com" },
"uri": { "label": "com" }
}
]
]

View file

@ -29,7 +29,7 @@ internal class KeyCodeDescriptionMapper private constructor() {
put(KeyCode.SETTINGS, R.string.spoken_description_settings)
put(KeyCode.SHIFT, R.string.spoken_description_shift)
put(KeyCode.VOICE_INPUT, R.string.spoken_description_mic)
put(KeyCode.ALPHA_SYMBOL, R.string.spoken_description_to_symbol)
put(KeyCode.SYMBOL_ALPHA, R.string.spoken_description_to_symbol)
put(Constants.CODE_TAB, R.string.spoken_description_tab)
put(KeyCode.LANGUAGE_SWITCH, R.string.spoken_description_language_switch)
put(KeyCode.ACTION_NEXT, R.string.spoken_description_action_next)
@ -58,7 +58,7 @@ internal class KeyCodeDescriptionMapper private constructor() {
*/
fun getDescriptionForKey(context: Context, keyboard: Keyboard?, key: Key, shouldObscure: Boolean): String? {
val code = key.code
if (code == KeyCode.ALPHA_SYMBOL || code == KeyCode.SYMBOL || code == KeyCode.ALPHA) {
if (code == KeyCode.SYMBOL_ALPHA || code == KeyCode.SYMBOL || code == KeyCode.ALPHA) {
val description = getDescriptionForSwitchAlphaSymbol(context, keyboard)
if (description != null) {
return description

View file

@ -308,17 +308,17 @@ public class Key implements Comparable<Key> {
final float horizontalGapFloat = isSpacer() ? 0 : (keyParams.mKeyboardParams.mRelativeHorizontalGap * keyParams.mKeyboardParams.mOccupiedWidth);
mHorizontalGap = Math.round(horizontalGapFloat);
mVerticalGap = Math.round(keyParams.mKeyboardParams.mRelativeVerticalGap * keyParams.mKeyboardParams.mOccupiedHeight);
mWidth = Math.round(keyParams.mFullWidth - horizontalGapFloat);
mWidth = Math.round(keyParams.mAbsoluteWidth - horizontalGapFloat);
// height is always rounded down, because rounding up may make the keyboard too high to fit, leading to issues
mHeight = (int) (keyParams.mFullHeight - keyParams.mKeyboardParams.mVerticalGap);
mHeight = (int) (keyParams.mAbsoluteHeight - keyParams.mKeyboardParams.mVerticalGap);
if (!isSpacer() && (mWidth == 0 || mHeight == 0)) {
throw new IllegalStateException("key needs positive width and height");
}
// Horizontal gap is divided equally to both sides of the key.
mX = Math.round(keyParams.xPos + horizontalGapFloat / 2);
mY = Math.round(keyParams.yPos);
mHitBox.set(Math.round(keyParams.xPos), Math.round(keyParams.yPos), Math.round(keyParams.xPos + keyParams.mFullWidth) + 1,
Math.round(keyParams.yPos + keyParams.mFullHeight));
mHitBox.set(Math.round(keyParams.xPos), Math.round(keyParams.yPos), Math.round(keyParams.xPos + keyParams.mAbsoluteWidth) + 1,
Math.round(keyParams.yPos + keyParams.mAbsoluteHeight));
mHashCode = computeHashCode(this);
}
@ -504,7 +504,7 @@ public class Key implements Comparable<Key> {
return this instanceof Spacer;
}
public final boolean isActionKey() {
public final boolean hasActionKeyBackground() {
return mBackgroundType == BACKGROUND_TYPE_ACTION;
}
@ -513,7 +513,7 @@ public class Key implements Comparable<Key> {
}
public final boolean isModifier() {
return mCode == KeyCode.SHIFT || mCode == KeyCode.ALPHA_SYMBOL || mCode == KeyCode.ALPHA || mCode == KeyCode.SYMBOL;
return mCode == KeyCode.SHIFT || mCode == KeyCode.SYMBOL_ALPHA || mCode == KeyCode.ALPHA || mCode == KeyCode.SYMBOL;
}
public final boolean isRepeatable() {
@ -906,7 +906,7 @@ public class Key implements Comparable<Key> {
final Drawable background;
if (isAccentColored()) {
background = actionKeyBackground;
} else if (isFunctional()) {
} else if (hasFunctionalBackground()) {
background = functionalKeyBackground;
} else if (mBackgroundType == BACKGROUND_TYPE_SPACEBAR) {
background = spacebarBackground;
@ -919,7 +919,7 @@ public class Key implements Comparable<Key> {
}
public final boolean isAccentColored() {
if (isActionKey()) return true;
if (hasActionKeyBackground()) return true;
final String iconName = KeyboardIconsSet.getIconName(getIconId());
return iconName.equals(KeyboardIconsSet.NAME_NEXT_KEY)
|| iconName.equals(KeyboardIconsSet.NAME_PREVIOUS_KEY)
@ -927,7 +927,7 @@ public class Key implements Comparable<Key> {
|| iconName.equals(KeyboardIconsSet.NAME_EMOJI_ACTION_KEY);
}
public boolean isFunctional() {
public boolean hasFunctionalBackground() {
return mBackgroundType == BACKGROUND_TYPE_FUNCTIONAL
|| mBackgroundType == BACKGROUND_TYPE_STICKY_OFF
|| mBackgroundType == BACKGROUND_TYPE_STICKY_ON;
@ -954,12 +954,12 @@ public class Key implements Comparable<Key> {
// params for building
public boolean isSpacer;
private final KeyboardParams mKeyboardParams; // for reading gaps and keyboard width / height
public float mRelativeWidth;
public float mRelativeHeight; // also should allow negative values, indicating absolute height is defined
public float mWidth;
public float mHeight; // also should allow negative values, indicating absolute height is defined
// params that may change
public float mFullWidth;
public float mFullHeight;
public float mAbsoluteWidth;
public float mAbsoluteHeight;
public float xPos;
public float yPos;
@ -977,10 +977,10 @@ public class Key implements Comparable<Key> {
@Nullable public final OptionalAttributes mOptionalAttributes;
public final boolean mEnabled;
public static KeyParams newSpacer(final KeyboardParams params, final float relativeWidth) {
public static KeyParams newSpacer(final KeyboardParams params, final float width) {
final KeyParams spacer = new KeyParams(params);
spacer.mRelativeWidth = relativeWidth;
spacer.mRelativeHeight = params.mDefaultRelativeRowHeight;
spacer.mWidth = width;
spacer.mHeight = params.mDefaultRowHeight;
return spacer;
}
@ -989,18 +989,18 @@ public class Key implements Comparable<Key> {
return new Key(this);
}
public void setDimensionsFromRelativeSize(final float newX, final float newY) {
if (mRelativeHeight == 0)
mRelativeHeight = mKeyboardParams.mDefaultRelativeRowHeight;
if (!isSpacer && mRelativeWidth == 0)
mRelativeWidth = mKeyboardParams.mDefaultRelativeKeyWidth;
if (mRelativeHeight < 0)
public void setAbsoluteDimensions(final float newX, final float newY) {
if (mHeight == 0)
mHeight = mKeyboardParams.mDefaultRowHeight;
if (!isSpacer && mWidth == 0)
throw new IllegalStateException("width = 0 should have been evaluated already");
if (mHeight < 0)
// todo (later): deal with it properly when it needs to be adjusted, i.e. when changing popupKeys or moreSuggestions
throw new IllegalStateException("can't (yet) deal with absolute height");
xPos = newX;
yPos = newY;
mFullWidth = mRelativeWidth * mKeyboardParams.mBaseWidth;
mFullHeight = mRelativeHeight * mKeyboardParams.mBaseHeight;
mAbsoluteWidth = mWidth * mKeyboardParams.mBaseWidth;
mAbsoluteHeight = mHeight * mKeyboardParams.mBaseHeight;
}
private static int getPopupKeysColumnAndFlagsAndSetNullInArray(final KeyboardParams params, final String[] popupKeys) {
@ -1052,7 +1052,7 @@ public class Key implements Comparable<Key> {
@NonNull final String keySpec, // key text or some special string for KeySpecParser, e.g. "!icon/shift_key|!code/key_shift" (avoid using !text, should be removed)
final int code,
@NonNull final KeyboardParams params,
final float relativeWidth,
final float width,
final int labelFlags,
final int backgroundType,
@Nullable final PopupSet<?> popupSet
@ -1060,8 +1060,8 @@ public class Key implements Comparable<Key> {
mKeyboardParams = params;
mBackgroundType = backgroundType;
mLabelFlags = labelFlags;
mRelativeWidth = relativeWidth;
mRelativeHeight = params.mDefaultRelativeRowHeight;
mWidth = width;
mHeight = params.mDefaultRowHeight;
mIconId = KeySpecParser.getIconId(keySpec);
final boolean needsToUpcase = needsToUpcase(mLabelFlags, params.mId.mElementId);
@ -1144,9 +1144,9 @@ public class Key implements Comparable<Key> {
}
// action flags don't need to be specified, they can be deduced from the key
if (backgroundType == BACKGROUND_TYPE_SPACEBAR
if (mCode == Constants.CODE_SPACE
|| mCode == KeyCode.LANGUAGE_SWITCH
|| (mCode == KeyCode.ALPHA_SYMBOL && !params.mId.isAlphabetKeyboard())
|| (mCode == KeyCode.SYMBOL_ALPHA && !params.mId.isAlphabetKeyboard())
)
actionFlags |= ACTION_FLAGS_ENABLE_LONG_PRESS;
if (mCode <= Constants.CODE_SPACE && mCode != KeyCode.MULTIPLE_CODE_POINTS)
@ -1237,8 +1237,8 @@ public class Key implements Comparable<Key> {
public KeyParams(final KeyParams keyParams) {
xPos = keyParams.xPos;
yPos = keyParams.yPos;
mRelativeWidth = keyParams.mRelativeWidth;
mRelativeHeight = keyParams.mRelativeHeight;
mWidth = keyParams.mWidth;
mHeight = keyParams.mHeight;
isSpacer = keyParams.isSpacer;
mKeyboardParams = keyParams.mKeyboardParams;
mEnabled = keyParams.mEnabled;
@ -1248,8 +1248,8 @@ public class Key implements Comparable<Key> {
mHintLabel = keyParams.mHintLabel;
mLabelFlags = keyParams.mLabelFlags;
mIconId = keyParams.mIconId;
mFullWidth = keyParams.mFullWidth;
mFullHeight = keyParams.mFullHeight;
mAbsoluteWidth = keyParams.mAbsoluteWidth;
mAbsoluteHeight = keyParams.mAbsoluteHeight;
mPopupKeys = keyParams.mPopupKeys;
mPopupKeysColumnAndFlags = keyParams.mPopupKeysColumnAndFlags;
mBackgroundType = keyParams.mBackgroundType;

View file

@ -149,6 +149,10 @@ public final class KeyboardId {
return elementId < ELEMENT_SYMBOLS;
}
public boolean isAlphaOrSymbolKeyboard() {
return mElementId <= ELEMENT_SYMBOLS_SHIFTED;
}
public boolean isAlphabetKeyboard() {
return isAlphabetKeyboard(mElementId);
}

View file

@ -472,7 +472,7 @@ public class KeyboardView extends View {
blendAlpha(paint, params.mAnimAlpha);
final float labelCharHeight = TypefaceUtils.getReferenceCharHeight(paint);
final float labelCharWidth = TypefaceUtils.getReferenceCharWidth(paint);
final boolean isFunctionalKeyAndRoundedStyle = mColors.getThemeStyle().equals(STYLE_ROUNDED) && key.isFunctional();
final boolean isFunctionalKeyAndRoundedStyle = mColors.getThemeStyle().equals(STYLE_ROUNDED) && key.hasFunctionalBackground();
final float hintX, hintBaseline;
if (key.hasHintLabel()) {
// The hint label is placed just right of the key label. Used mainly on
@ -555,7 +555,7 @@ public class KeyboardView extends View {
if (key.getBackgroundType() == Key.BACKGROUND_TYPE_SPACEBAR)
hintX = keyWidth + hintBaseline + labelCharWidth * 0.1f;
else
hintX = key.isFunctional() || key.isActionKey() ? keyWidth / 2.0f : keyWidth - mKeyHintLetterPadding - labelCharWidth / 2.0f;
hintX = key.hasFunctionalBackground() || key.hasActionKeyBackground() ? keyWidth / 2.0f : keyWidth - mKeyHintLetterPadding - labelCharWidth / 2.0f;
} else {
hintX = keyWidth - mKeyHintLetterPadding - TypefaceUtils.getReferenceCharWidth(paint) / 2.0f;
}

View file

@ -540,7 +540,7 @@ public final class MainKeyboardView extends KeyboardView implements DrawingProxy
mPopupKeysKeyboardCache.put(key, popupKeysKeyboard);
}
final View container = key.isActionKey() ? mPopupKeysKeyboardForActionContainer
final View container = key.hasActionKeyBackground() ? mPopupKeysKeyboardForActionContainer
: mPopupKeysKeyboardContainer;
final PopupKeysKeyboardView popupKeysKeyboardView =
container.findViewById(R.id.popup_keys_keyboard_view);

View file

@ -1107,7 +1107,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element,
return;
}
}
if (code == KeyCode.ALPHA_SYMBOL && Settings.getInstance().getCurrent().mLongPressSymbolsForNumpad) {
if (code == KeyCode.SYMBOL_ALPHA && Settings.getInstance().getCurrent().mLongPressSymbolsForNumpad) {
sListener.onCodeInput(KeyCode.NUMPAD, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false);
return;
}

View file

@ -23,7 +23,7 @@ public final class PopupKeysKeyboard extends Keyboard {
PopupKeysKeyboard(final PopupKeysKeyboardParams params) {
super(params);
mDefaultKeyCoordX = params.getDefaultKeyCoordX() + params.mDefaultKeyWidth / 2;
mDefaultKeyCoordX = params.getDefaultKeyCoordX() + params.mDefaultAbsoluteKeyWidth / 2;
}
public int getDefaultCoordX() {
@ -71,8 +71,8 @@ public final class PopupKeysKeyboard extends Keyboard {
throw new IllegalArgumentException("Keyboard is too small to hold popup keys: "
+ parentKeyboardWidth + " " + keyWidth + " " + numKeys + " " + numColumn);
}
mDefaultKeyWidth = keyWidth;
mDefaultRowHeight = rowHeight;
mDefaultAbsoluteKeyWidth = keyWidth;
mDefaultAbsoluteRowHeight = rowHeight;
mNumRows = (numKeys + numColumn - 1) / numColumn;
final int numColumns = isPopupKeysFixedColumn ? Math.min(numKeys, numColumn)
@ -116,10 +116,10 @@ public final class PopupKeysKeyboard extends Keyboard {
mTopRowAdjustment = isPopupKeysFixedOrder ? getFixedOrderTopRowAdjustment()
: getAutoOrderTopRowAdjustment();
mDividerWidth = dividerWidth;
mColumnWidth = mDefaultKeyWidth + mDividerWidth;
mColumnWidth = mDefaultAbsoluteKeyWidth + mDividerWidth;
mBaseWidth = mOccupiedWidth = mNumColumns * mColumnWidth - mDividerWidth;
// Need to subtract the bottom row's gutter only.
mBaseHeight = mOccupiedHeight = mNumRows * mDefaultRowHeight - mVerticalGap
mBaseHeight = mOccupiedHeight = mNumRows * mDefaultAbsoluteRowHeight - mVerticalGap
+ mTopPadding + mBottomPadding;
}
@ -227,7 +227,7 @@ public final class PopupKeysKeyboard extends Keyboard {
}
public int getY(final int row) {
return (mNumRows - 1 - row) * mDefaultRowHeight + mTopPadding;
return (mNumRows - 1 - row) * mDefaultAbsoluteRowHeight + mTopPadding;
}
public void markAsEdgeKey(final Key key, final int row) {
@ -287,8 +287,8 @@ public final class PopupKeysKeyboard extends Keyboard {
final float padding = context.getResources().getDimension(
R.dimen.config_popup_keys_keyboard_key_horizontal_padding)
+ (key.hasLabelsInPopupKeys()
? mParams.mDefaultKeyWidth * LABEL_PADDING_RATIO : 0.0f);
keyWidth = getMaxKeyWidth(key, mParams.mDefaultKeyWidth, padding, paintToMeasure);
? mParams.mDefaultAbsoluteKeyWidth * LABEL_PADDING_RATIO : 0.0f);
keyWidth = getMaxKeyWidth(key, mParams.mDefaultAbsoluteKeyWidth, padding, paintToMeasure);
rowHeight = keyboard.mMostCommonKeyHeight;
}
final int dividerWidth;
@ -342,9 +342,9 @@ public final class PopupKeysKeyboard extends Keyboard {
// left of the default position.
if (params.mDividerWidth > 0 && pos != 0) {
final int dividerX = (pos > 0) ? x - params.mDividerWidth
: x + params.mDefaultKeyWidth;
: x + params.mDefaultAbsoluteKeyWidth;
final Key divider = new PopupKeyDivider(
params, dividerX, y, params.mDividerWidth, params.mDefaultRowHeight);
params, dividerX, y, params.mDividerWidth, params.mDefaultAbsoluteRowHeight);
params.onAddKey(divider);
}
}

View file

@ -111,12 +111,12 @@ open class KeyboardBuilder<KP : KeyboardParams>(protected val mContext: Context,
fillGapsWithSpacers(row)
var currentX = mParams.mLeftPadding.toFloat()
row.forEach {
it.setDimensionsFromRelativeSize(currentX, currentY)
it.setAbsoluteDimensions(currentX, currentY)
if (DebugFlags.DEBUG_ENABLED)
Log.d(TAG, "setting size and position for ${it.mLabel}, ${it.mCode}: x ${currentX.toInt()}, w ${it.mFullWidth.toInt()}")
currentX += it.mFullWidth
Log.d(TAG, "setting size and position for ${it.mLabel}, ${it.mCode}: x ${currentX.toInt()}, w ${it.mAbsoluteWidth.toInt()}")
currentX += it.mAbsoluteWidth
}
currentY += row.first().mFullHeight
currentY += row.first().mAbsoluteHeight
}
}
@ -140,7 +140,7 @@ open class KeyboardBuilder<KP : KeyboardParams>(protected val mContext: Context,
i++
currentX += currentKeyXPos - currentX
}
currentX += row[i].mFullWidth
currentX += row[i].mAbsoluteWidth
i++
}
if (currentX < mParams.mOccupiedWidth) {
@ -161,48 +161,48 @@ open class KeyboardBuilder<KP : KeyboardParams>(protected val mContext: Context,
for (row in keysInRows) {
fillGapsWithSpacers(row)
val y = row.first().yPos // all have the same y, so this is fine
val relativeWidthSum = row.sumOf { it.mRelativeWidth } // sum up relative widths
val relativeWidthSum = row.sumOf { it.mWidth } // sum up relative widths
val spacer = KeyParams.newSpacer(mParams, spacerRelativeWidth)
// insert spacer before first key that starts right of the center (also consider gap)
var insertIndex = row.indexOfFirst { it.xPos + it.mFullWidth / 3 > mParams.mOccupiedWidth / 2 }
var insertIndex = row.indexOfFirst { it.xPos + it.mAbsoluteWidth / 3 > mParams.mOccupiedWidth / 2 }
.takeIf { it > -1 } ?: (row.size / 2) // fallback should never be needed, but better than having an error
if (row.any { it.mCode == Constants.CODE_SPACE }) {
val spaceLeft = row.single { it.mCode == Constants.CODE_SPACE }
reduceSymbolAndActionKeyWidth(row)
insertIndex = row.indexOf(spaceLeft) + 1
val widthBeforeSpace = row.subList(0, insertIndex - 1).sumOf { it.mRelativeWidth }
val widthAfterSpace = row.subList(insertIndex, row.size).sumOf { it.mRelativeWidth }
val spaceLeftWidth = (maxWidthBeforeSpacer - widthBeforeSpace).coerceAtLeast(mParams.mDefaultRelativeKeyWidth)
val spaceRightWidth = (maxWidthAfterSpacer - widthAfterSpace).coerceAtLeast(mParams.mDefaultRelativeKeyWidth)
val spacerWidth = spaceLeft.mRelativeWidth + spacerRelativeWidth - spaceLeftWidth - spaceRightWidth
val widthBeforeSpace = row.subList(0, insertIndex - 1).sumOf { it.mWidth }
val widthAfterSpace = row.subList(insertIndex, row.size).sumOf { it.mWidth }
val spaceLeftWidth = (maxWidthBeforeSpacer - widthBeforeSpace).coerceAtLeast(mParams.mDefaultKeyWidth)
val spaceRightWidth = (maxWidthAfterSpacer - widthAfterSpace).coerceAtLeast(mParams.mDefaultKeyWidth)
val spacerWidth = spaceLeft.mWidth + spacerRelativeWidth - spaceLeftWidth - spaceRightWidth
if (spacerWidth > 0.05f) {
// only insert if the spacer has a reasonable width
val spaceRight = KeyParams(spaceLeft)
spaceLeft.mRelativeWidth = spaceLeftWidth
spaceRight.mRelativeWidth = spaceRightWidth
spacer.mRelativeWidth = spacerWidth
spaceLeft.mWidth = spaceLeftWidth
spaceRight.mWidth = spaceRightWidth
spacer.mWidth = spacerWidth
row.add(insertIndex, spaceRight)
row.add(insertIndex, spacer)
} else {
// otherwise increase space width, so other keys are resized properly
spaceLeft.mRelativeWidth += spacerWidth
spaceLeft.mWidth += spacerWidth
}
} else {
val widthBeforeSpacer = row.subList(0, insertIndex).sumOf { it.mRelativeWidth }
val widthAfterSpacer = row.subList(insertIndex, row.size).sumOf { it.mRelativeWidth }
val widthBeforeSpacer = row.subList(0, insertIndex).sumOf { it.mWidth }
val widthAfterSpacer = row.subList(insertIndex, row.size).sumOf { it.mWidth }
maxWidthBeforeSpacer = maxWidthBeforeSpacer.coerceAtLeast(widthBeforeSpacer)
maxWidthAfterSpacer = maxWidthAfterSpacer.coerceAtLeast(widthAfterSpacer)
row.add(insertIndex, spacer)
}
// re-calculate relative widths
val relativeWidthSumNew = row.sumOf { it.mRelativeWidth }
val relativeWidthSumNew = row.sumOf { it.mWidth }
val widthFactor = relativeWidthSum / relativeWidthSumNew
// re-calculate absolute sizes and positions
var currentX = 0f
row.forEach {
it.mRelativeWidth *= widthFactor
it.setDimensionsFromRelativeSize(currentX, y)
currentX += it.mFullWidth
it.mWidth *= widthFactor
it.setAbsoluteDimensions(currentX, y)
currentX += it.mAbsoluteWidth
}
}
}
@ -211,19 +211,19 @@ open class KeyboardBuilder<KP : KeyboardParams>(protected val mContext: Context,
// todo: this assumes fixed layout for symbols keys, which will change soon!
private fun reduceSymbolAndActionKeyWidth(row: ArrayList<KeyParams>) {
val spaceKey = row.first { it.mCode == Constants.CODE_SPACE }
val symbolKey = row.firstOrNull { it.mCode == KeyCode.ALPHA_SYMBOL }
val symbolKeyWidth = symbolKey?.mRelativeWidth ?: 0f
if (symbolKeyWidth > mParams.mDefaultRelativeKeyWidth) {
val widthToChange = symbolKey!!.mRelativeWidth - mParams.mDefaultRelativeKeyWidth
symbolKey.mRelativeWidth -= widthToChange
spaceKey.mRelativeWidth += widthToChange
val symbolKey = row.firstOrNull { it.mCode == KeyCode.SYMBOL_ALPHA }
val symbolKeyWidth = symbolKey?.mWidth ?: 0f
if (symbolKeyWidth > mParams.mDefaultKeyWidth) {
val widthToChange = symbolKey!!.mWidth - mParams.mDefaultKeyWidth
symbolKey.mWidth -= widthToChange
spaceKey.mWidth += widthToChange
}
val actionKey = row.firstOrNull { it.mBackgroundType == Key.BACKGROUND_TYPE_ACTION }
val actionKeyWidth = actionKey?.mRelativeWidth ?: 0f
if (actionKeyWidth > mParams.mDefaultRelativeKeyWidth * 1.1f) { // allow it to stay a little wider
val widthToChange = actionKey!!.mRelativeWidth - mParams.mDefaultRelativeKeyWidth * 1.1f
actionKey.mRelativeWidth -= widthToChange
spaceKey.mRelativeWidth += widthToChange
val actionKeyWidth = actionKey?.mWidth ?: 0f
if (actionKeyWidth > mParams.mDefaultKeyWidth * 1.1f) { // allow it to stay a little wider
val widthToChange = actionKey!!.mWidth - mParams.mDefaultKeyWidth * 1.1f
actionKey.mWidth -= widthToChange
spaceKey.mWidth += widthToChange
}
}

View file

@ -58,7 +58,7 @@ public final class KeyboardCodesSet {
Constants.CODE_SPACE,
KeyCode.SHIFT,
KeyCode.CAPS_LOCK,
KeyCode.ALPHA_SYMBOL,
KeyCode.SYMBOL_ALPHA,
KeyCode.ALPHA,
KeyCode.SYMBOL,
KeyCode.MULTIPLE_CODE_POINTS,

View file

@ -19,7 +19,6 @@ import helium314.keyboard.keyboard.KeyboardId;
import helium314.keyboard.keyboard.internal.keyboard_parser.LocaleKeyboardInfos;
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode;
import helium314.keyboard.latin.R;
import helium314.keyboard.latin.common.Constants;
import helium314.keyboard.latin.settings.Settings;
import helium314.keyboard.latin.utils.ResourceUtils;
@ -55,13 +54,13 @@ public class KeyboardParams {
@Nullable
public KeyVisualAttributes mKeyVisualAttributes;
public float mDefaultRelativeRowHeight;
public float mDefaultRelativeKeyWidth;
public float mDefaultRowHeight;
public float mDefaultKeyWidth;
public float mRelativeHorizontalGap;
public float mRelativeVerticalGap;
// relative values multiplied with baseHeight / baseWidth
public int mDefaultRowHeight;
public int mDefaultKeyWidth;
public int mDefaultAbsoluteRowHeight;
public int mDefaultAbsoluteKeyWidth;
public int mHorizontalGap;
public int mVerticalGap;
@ -227,9 +226,9 @@ public class KeyboardParams {
mBaseWidth = mOccupiedWidth - mLeftPadding - mRightPadding;
final float defaultKeyWidthFactor = context.getResources().getInteger(R.integer.config_screen_metrics) > 2
? 0.9f : 1f;
mDefaultRelativeKeyWidth = keyAttr.getFraction(R.styleable.Keyboard_Key_keyWidth,
mDefaultKeyWidth = keyAttr.getFraction(R.styleable.Keyboard_Key_keyWidth,
1, 1, defaultKeyWidthFactor / DEFAULT_KEYBOARD_COLUMNS);
mDefaultKeyWidth = (int) (mDefaultRelativeKeyWidth * mBaseWidth);
mDefaultAbsoluteKeyWidth = (int) (mDefaultKeyWidth * mBaseWidth);
// todo: maybe settings should not be accessed from here?
if (Settings.getInstance().getCurrent().mNarrowKeyGaps) {
@ -250,13 +249,13 @@ public class KeyboardParams {
mVerticalGap = (int) (mRelativeVerticalGap * height);
mBaseHeight = mOccupiedHeight - mTopPadding - mBottomPadding + mVerticalGap;
mDefaultRelativeRowHeight = ResourceUtils.getDimensionOrFraction(keyboardAttr,
mDefaultRowHeight = ResourceUtils.getDimensionOrFraction(keyboardAttr,
R.styleable.Keyboard_rowHeight, 1, 1f / DEFAULT_KEYBOARD_ROWS);
if (mDefaultRelativeRowHeight > 1) { // can be absolute size, in that case will be > 1
mDefaultRowHeight = (int) mDefaultRelativeRowHeight;
mDefaultRelativeRowHeight *= -1; // make it negative when it's absolute
if (mDefaultRowHeight > 1) { // can be absolute size, in that case will be > 1
mDefaultAbsoluteRowHeight = (int) mDefaultRowHeight;
mDefaultRowHeight *= -1; // make it negative when it's absolute
} else {
mDefaultRowHeight = (int) (mDefaultRelativeRowHeight * mBaseHeight);
mDefaultAbsoluteRowHeight = (int) (mDefaultRowHeight * mBaseHeight);
}
mKeyVisualAttributes = KeyVisualAttributes.newInstance(keyAttr);

View file

@ -422,7 +422,7 @@ public final class KeyboardState {
onPressShift();
} else if (code == KeyCode.CAPS_LOCK) {
// Nothing to do here. See {@link #onReleaseKey(int,boolean)}.
} else if (code == KeyCode.ALPHA_SYMBOL) {
} else if (code == KeyCode.SYMBOL_ALPHA) {
onPressAlphaSymbol(autoCapsFlags, recapitalizeMode);
} else if (code == KeyCode.SYMBOL) {
// don't start sliding, causes issues with fully customizable layouts
@ -463,7 +463,7 @@ public final class KeyboardState {
onReleaseShift(withSliding, autoCapsFlags, recapitalizeMode);
} else if (code == KeyCode.CAPS_LOCK) {
setShiftLocked(!mAlphabetShiftState.isShiftLocked());
} else if (code == KeyCode.ALPHA_SYMBOL) {
} else if (code == KeyCode.SYMBOL_ALPHA) {
onReleaseAlphaSymbol(withSliding, autoCapsFlags, recapitalizeMode);
} else if (code == KeyCode.SYMBOL) {
onReleaseSymbol(withSliding, autoCapsFlags, recapitalizeMode);
@ -700,7 +700,7 @@ public final class KeyboardState {
switch (mSwitchState) {
case SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL:
if (code == KeyCode.ALPHA_SYMBOL) {
if (code == KeyCode.SYMBOL_ALPHA) {
// Detected only the mode change key has been pressed, and then released.
if (mMode == MODE_ALPHABET) {
mSwitchState = SWITCH_STATE_ALPHA;

View file

@ -70,7 +70,7 @@ public final class PopupKeySpec {
public Key buildKey(final int x, final int y, final int labelFlags,
@NonNull final KeyboardParams params) {
return new Key(mLabel, mIconId, mCode, mOutputText, null /* hintLabel */, labelFlags,
Key.BACKGROUND_TYPE_NORMAL, x, y, params.mDefaultKeyWidth, params.mDefaultRowHeight,
Key.BACKGROUND_TYPE_NORMAL, x, y, params.mDefaultAbsoluteKeyWidth, params.mDefaultAbsoluteRowHeight,
params.mHorizontalGap, params.mVerticalGap);
}

View file

@ -9,7 +9,6 @@ import helium314.keyboard.keyboard.KeyboardId
import helium314.keyboard.keyboard.internal.KeyboardParams
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode
import helium314.keyboard.latin.R
import helium314.keyboard.latin.common.Constants
import helium314.keyboard.latin.common.StringUtils
import helium314.keyboard.latin.settings.Settings
import helium314.keyboard.latin.utils.ResourceUtils
@ -44,20 +43,20 @@ class EmojiParser(private val params: KeyboardParams, private val context: Conte
// determine key width for default settings (no number row, no one-handed mode, 100% height and bottom padding scale)
// this is a bit long, but ensures that emoji size stays the same, independent of these settings
val defaultKeyWidth = (ResourceUtils.getDefaultKeyboardWidth(context.resources) - params.mLeftPadding - params.mRightPadding) * params.mDefaultRelativeKeyWidth
val defaultKeyWidth = (ResourceUtils.getDefaultKeyboardWidth(context.resources) - params.mLeftPadding - params.mRightPadding) * params.mDefaultKeyWidth
val keyWidth = defaultKeyWidth * sqrt(Settings.getInstance().current.mKeyboardHeightScale)
val defaultKeyboardHeight = ResourceUtils.getDefaultKeyboardHeight(context.resources, false)
val defaultBottomPadding = context.resources.getFraction(R.fraction.config_keyboard_bottom_padding_holo, defaultKeyboardHeight, defaultKeyboardHeight)
val emojiKeyboardHeight = ResourceUtils.getDefaultKeyboardHeight(context.resources, false) * 0.75f + params.mVerticalGap - defaultBottomPadding - context.resources.getDimensionPixelSize(R.dimen.config_emoji_category_page_id_height)
val keyHeight = emojiKeyboardHeight * params.mDefaultRelativeRowHeight * Settings.getInstance().current.mKeyboardHeightScale // still apply height scale to key
val keyHeight = emojiKeyboardHeight * params.mDefaultRowHeight * Settings.getInstance().current.mKeyboardHeightScale // still apply height scale to key
emojiArray.forEachIndexed { i, codeArraySpec ->
val keyParams = parseEmojiKey(codeArraySpec, popupEmojisArray?.get(i)?.takeIf { it.isNotEmpty() }) ?: return@forEachIndexed
keyParams.xPos = currentX
keyParams.yPos = currentY
keyParams.mFullWidth = keyWidth
keyParams.mFullHeight = keyHeight
currentX += keyParams.mFullWidth
keyParams.mAbsoluteWidth = keyWidth
keyParams.mAbsoluteHeight = keyHeight
currentX += keyParams.mAbsoluteWidth
row.add(keyParams)
}
return arrayListOf(row)

View file

@ -9,18 +9,17 @@ import androidx.annotation.StringRes
import helium314.keyboard.keyboard.Key
import helium314.keyboard.keyboard.Key.KeyParams
import helium314.keyboard.keyboard.KeyboardId
import helium314.keyboard.keyboard.KeyboardTheme
import helium314.keyboard.keyboard.internal.KeyboardIconsSet
import helium314.keyboard.keyboard.internal.KeyboardParams
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyData
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyLabel
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyType
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.SimplePopups
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.TextKeyData
import helium314.keyboard.latin.R
import helium314.keyboard.latin.common.Constants
import helium314.keyboard.latin.common.LocaleUtils.constructLocale
import helium314.keyboard.latin.common.isEmoji
import helium314.keyboard.latin.common.splitOnWhitespace
import helium314.keyboard.latin.define.DebugFlags
import helium314.keyboard.latin.settings.Settings
import helium314.keyboard.latin.spellcheck.AndroidSpellCheckerService
@ -31,7 +30,10 @@ import helium314.keyboard.latin.utils.POPUP_KEYS_NUMBER
import helium314.keyboard.latin.utils.ScriptUtils
import helium314.keyboard.latin.utils.ScriptUtils.script
import helium314.keyboard.latin.utils.getLayoutFile
import helium314.keyboard.latin.utils.removeFirst
import helium314.keyboard.latin.utils.replaceFirst
import helium314.keyboard.latin.utils.runInLocale
import helium314.keyboard.latin.utils.splitAt
import helium314.keyboard.latin.utils.sumOf
import java.io.File
@ -65,103 +67,260 @@ abstract class KeyboardParser(private val params: KeyboardParams, private val co
params.mTouchPositionCorrection.load(context.resources.getStringArray(infos.touchPositionCorrectionData))
val baseKeys: MutableList<List<KeyData>> = parseCoreLayout(layoutContent)
val keysInRows: ArrayList<ArrayList<KeyParams>>
if (params.mId.mElementId <= KeyboardId.ELEMENT_SYMBOLS_SHIFTED) {
keysInRows = createAlphaSymbolRows(baseKeys)
val keysInRows = if (params.mId.isAlphaOrSymbolKeyboard) {
createAlphaSymbolRows(baseKeys)
} else if (params.mId.isNumberLayout) {
keysInRows = createNumericRows(baseKeys)
createNumericRows(baseKeys)
} else {
throw(UnsupportedOperationException("creating KeyboardId ${params.mId.mElementId} not supported"))
}
// rescale height if we have more than 4 rows
val heightRescale = if (keysInRows.size > 4) 4f / keysInRows.size else 1f
if (heightRescale != 1f) {
keysInRows.forEach { row -> row.forEach { it.mRelativeHeight *= heightRescale } }
keysInRows.forEach { row -> row.forEach { it.mHeight *= heightRescale } }
}
return keysInRows
}
// this should be ready for customizable functional layouts, but needs cleanup
// todo (later): remove this as part of adding a cache for parsed layouts
private fun getFunctionalKeyLayoutText(): String {
if (!params.mId.isAlphaOrSymbolKeyboard) throw IllegalStateException("functional key layout only for aloha and symbol layouts")
val layouts = Settings.getLayoutsDir(context).list() ?: emptyArray()
if (params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS_SHIFTED) {
if ("functional_keys_symbols_shifted.json" in layouts)
return getLayoutFile("functional_keys_symbols_shifted.json", context).readText()
}
if (!params.mId.isAlphabetKeyboard) {
if ("functional_keys_symbols.json" in layouts)
return getLayoutFile("functional_keys_symbols.json", context).readText()
}
if ("functional_keys.json" in layouts)
return getLayoutFile("functional_keys.json", context).readText()
val fileName = if (Settings.getInstance().isTablet) "functional_keys_tablet.json" else "functional_keys.json"
return context.readAssetsLayoutFile(fileName)
}
private fun createAlphaSymbolRows(baseKeys: MutableList<List<KeyData>>): ArrayList<ArrayList<KeyParams>> {
addNumberRowOrPopupKeys(baseKeys)
if (params.mId.isAlphabetKeyboard)
addSymbolPopupKeys(baseKeys)
val bottomRow = getBottomRowAndAdjustBaseKeys(baseKeys) // not really fast, but irrelevant compared to the loop
if (params.mId.mNumberRowEnabled)
baseKeys.add(
0,
params.mLocaleKeyboardInfos.getNumberRow()
.map { it.copy(newLabelFlags = Key.LABEL_FLAGS_DISABLE_HINT_LABEL or defaultLabelFlags) })
val keysInRows = ArrayList<ArrayList<KeyParams>>()
val functionalKeys = parseFunctionalKeys(R.string.key_def_functional)
val functionalKeysTop = parseFunctionalKeys(R.string.key_def_functional_top_row)
val allFunctionalKeys = JsonKeyboardParser(params, context).parseCoreLayout(getFunctionalKeyLayoutText())
adjustBottomFunctionalRowAndBaseKeys(allFunctionalKeys, baseKeys)
// todo: this loop could use some performance improvements
baseKeys.forEachIndexed { i, it ->
val row: List<KeyData> = if (i == baseKeys.lastIndex && isTablet()) {
if (allFunctionalKeys.none { it.singleOrNull()?.isKeyPlaceholder() == true })
// add a placeholder so splitAt does what we really want
allFunctionalKeys.add(0, listOf(TextKeyData(type = KeyType.PLACEHOLDER)))
val (functionalKeysTop, functionalKeysBottom) = allFunctionalKeys.splitAt { it.singleOrNull()?.isKeyPlaceholder() == true }
// offset for bottom, relevant for getting correct functional key rows
val bottomIndexOffset = baseKeys.size - functionalKeysBottom.size
val functionalKeys = mutableListOf<Pair<List<KeyParams>, List<KeyParams>>>()
val baseKeyParams = baseKeys.mapIndexed { i, it ->
val row: List<KeyData> = if (i == baseKeys.lastIndex - 1 && Settings.getInstance().isTablet) {
// add bottom row extra keys
// todo (later): this can make very customized layouts look awkward
// decide when to (not) add it
// when not adding, consider that punctuation popup keys should not remove those keys!
val tabletExtraKeys = params.mLocaleKeyboardInfos.getTabletExtraKeys(params.mId.mElementId)
tabletExtraKeys.first + it + tabletExtraKeys.second
} else {
it
}
// parse functional keys for this row (if any)
val offset = baseKeys.size - functionalKeys.size
val functionalKeysDefs = if (i >= offset) functionalKeys[i - offset] // functional keys are aligned to bottom
else emptyList<String>() to emptyList()
val outerFunctionalKeyDefs = if (i == 0 && functionalKeysTop.isNotEmpty()) functionalKeysTop.first() // top row
else emptyList<String>() to emptyList()
// if we have a top row and top row entries from normal functional key defs, use top row as outer keys
val functionalKeysLeft = outerFunctionalKeyDefs.first.map { getFunctionalKeyParams(it) } + functionalKeysDefs.first.map { getFunctionalKeyParams(it) }
val functionalKeysRight = functionalKeysDefs.second.map { getFunctionalKeyParams(it) } + outerFunctionalKeyDefs.second.map { getFunctionalKeyParams(it) }
val paramsRow = ArrayList<KeyParams>(functionalKeysLeft)
// determine key width, maybe scale factor for keys, and spacers to add
val usedKeyWidth = params.mDefaultRelativeKeyWidth * row.size
val functionalKeyWidth = (functionalKeysLeft.sumOf { it.mRelativeWidth }) + (functionalKeysRight.sumOf { it.mRelativeWidth })
val availableWidth = 1f - functionalKeyWidth
var keyWidth: Float
val spacerWidth: Float
if (availableWidth - usedKeyWidth > 0.0001f) { // don't add spacers if only a tiny bit is empty
// width available, add spacer
keyWidth = params.mDefaultRelativeKeyWidth
spacerWidth = (availableWidth - usedKeyWidth) / 2
} else {
// need more width, re-scale
spacerWidth = 0f
keyWidth = availableWidth / row.size
}
if (spacerWidth != 0f) {
paramsRow.add(KeyParams.newSpacer(params, spacerWidth))
}
if (keyWidth < params.mDefaultRelativeKeyWidth * 0.82 && spacerWidth == 0f) {
// keys are very narrow, also rescale the functional keys to make keys a little wider
// 0.82 is just some guess for "too narrow"
val allKeyScale = 1f / (functionalKeyWidth + row.size * params.mDefaultRelativeKeyWidth)
keyWidth = params.mDefaultRelativeKeyWidth * allKeyScale
functionalKeysLeft.forEach { it.mRelativeWidth *= allKeyScale }
functionalKeysRight.forEach { it.mRelativeWidth *= allKeyScale }
}
// build list of functional keys of same size as baseKeys
val functionalKeysFromTop = functionalKeysTop.getOrNull(i) ?: emptyList()
val functionalKeysFromBottom = functionalKeysBottom.getOrNull(i - bottomIndexOffset) ?: emptyList()
functionalKeys.add(getFunctionalKeysBySide(functionalKeysFromTop, functionalKeysFromBottom))
for (key in row) {
row.map { key ->
val extraFlags = if (key.label.length > 2 && key.label.codePointCount(0, key.label.length) > 2 && !isEmoji(key.label))
Key.LABEL_FLAGS_AUTO_X_SCALE
else 0
val keyData = key.compute(params)
if (DebugFlags.DEBUG_ENABLED)
Log.d(TAG, "adding key ${keyData.label}, ${keyData.code}")
val keyParams = keyData.toKeyParams(params, keyWidth, defaultLabelFlags or extraFlags)
paramsRow.add(keyParams)
Log.d(TAG, "adding key ${key.label}, ${key.code}")
key.toKeyParams(params, defaultLabelFlags or extraFlags)
}
if (spacerWidth != 0f) {
paramsRow.add(KeyParams.newSpacer(params, spacerWidth))
}
functionalKeysRight.forEach { paramsRow.add(it) }
keysInRows.add(paramsRow)
}
resizeLastRowIfNecessaryForAlignment(keysInRows)
keysInRows.add(bottomRow)
if (params.mId.mNumberRowEnabled)
keysInRows.add(0, getNumberRow())
return setReasonableWidths(baseKeyParams, functionalKeys)
}
/** interprets key width -1, adjusts row size to nicely fit on screen, adds spacers if necessary */
private fun setReasonableWidths(bassKeyParams: List<List<KeyParams>>, functionalKeys: List<Pair<List<KeyParams>, List<KeyParams>>>): ArrayList<ArrayList<KeyParams>> {
val keysInRows = ArrayList<ArrayList<KeyParams>>()
// expand width = -1 keys and make sure rows fit on screen, insert spacers if necessary
bassKeyParams.forEachIndexed { i, keys ->
val (functionalKeysLeft, functionalKeysRight) = functionalKeys[i]
// sum up width, excluding -1 elements (put those in a separate list)
val varWidthKeys = mutableListOf<KeyParams>()
var totalWidth = 0f
val allKeys = (functionalKeysLeft + keys + functionalKeysRight)
allKeys.forEach {
if (it.mWidth == -1f) varWidthKeys.add(it)
else totalWidth += it.mWidth
}
// set width for varWidthKeys
if (varWidthKeys.isNotEmpty()) {
val width = if (totalWidth + varWidthKeys.size * params.mDefaultKeyWidth > 1)
params.mDefaultKeyWidth // never go below default width
else (1f - totalWidth) / varWidthKeys.size // split remaining space evenly
varWidthKeys.forEach { it.mWidth = width }
// re-calculate total width
totalWidth = allKeys.sumOf { it.mWidth }
}
// re-scale total width, or add spacers (or do nothing if totalWidth is near 1)
if (totalWidth < 0.9999f) { // add spacers
val spacerWidth = (1f - totalWidth) / 2
val paramsRow = ArrayList<KeyParams>(functionalKeysLeft + KeyParams.newSpacer(params, spacerWidth) + keys +
KeyParams.newSpacer(params, spacerWidth) + functionalKeysRight)
keysInRows.add(paramsRow)
} else {
if (totalWidth > 1.0001f) { // re-scale total width
val normalKeysWith = keys.sumOf { it.mWidth }
val functionalKeysWidth = totalWidth - normalKeysWith
val scaleFactor = (1f - functionalKeysWidth) / normalKeysWith
// re-scale normal keys if factor is > 0.82, otherwise re-scale all keys
if (scaleFactor > 0.82f) keys.forEach { it.mWidth *= scaleFactor }
else allKeys.forEach { it.mWidth /= totalWidth }
}
keysInRows.add(ArrayList(allKeys))
}
}
// adjust last normal row key widths to be aligned with row above, assuming a reasonably close-to-default alpha / symbol layout
// like in original layouts, e.g. for nordic and swiss layouts
if (!params.mId.isAlphaOrSymbolKeyboard || bassKeyParams.size < 3 || bassKeyParams.last().isNotEmpty())
return keysInRows
val lastNormalRow = bassKeyParams[bassKeyParams.lastIndex - 1]
val rowAboveLast = bassKeyParams[bassKeyParams.lastIndex - 2]
val lastNormalRowKeyWidth = lastNormalRow.first().mWidth
val rowAboveLastNormalRowKeyWidth = rowAboveLast.first().mWidth
if (lastNormalRowKeyWidth <= rowAboveLastNormalRowKeyWidth + 0.0001f // no need
|| lastNormalRowKeyWidth / rowAboveLastNormalRowKeyWidth > 1.1f // don't resize on large size difference
|| lastNormalRow.any { it.isSpacer } || rowAboveLast.any { it.isSpacer } // annoying to deal with, and probably no resize wanted anyway
|| lastNormalRow.any { it.mWidth != lastNormalRowKeyWidth } || rowAboveLast.any { it.mWidth != rowAboveLastNormalRowKeyWidth })
return keysInRows
val numberOfKeysInLast = lastNormalRow.count { it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL }
val widthBefore = numberOfKeysInLast * lastNormalRowKeyWidth
val widthAfter = numberOfKeysInLast * rowAboveLastNormalRowKeyWidth
val spacerWidth = (widthBefore - widthAfter) / 2
// resize keys
lastNormalRow.forEach { if (it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL) it.mWidth = rowAboveLastNormalRowKeyWidth }
// add spacers
val lastNormalFullRow = keysInRows[keysInRows.lastIndex - 1]
lastNormalFullRow.add(lastNormalFullRow.indexOfFirst { it == lastNormalRow.first() }, KeyParams.newSpacer(params, spacerWidth))
lastNormalFullRow.add(lastNormalFullRow.indexOfLast { it == lastNormalRow.last() } + 1, KeyParams.newSpacer(params, spacerWidth))
return keysInRows
}
/**
* adds / removes keys to the bottom row
* assumes a close-to-default bottom row consisting only of functional keys
* does nothing if not isAlphaOrSymbolKeyboard or assumptions not met
* adds an empty row to baseKeys, to have a baseKey row for the bottom functional row
*/
private fun adjustBottomFunctionalRowAndBaseKeys(allFunctionalKeys: MutableList<List<KeyData>>, baseKeys: MutableList<List<KeyData>>) {
val functionalKeysBottom = allFunctionalKeys.lastOrNull()?.toMutableList() ?: return
if (!params.mId.isAlphaOrSymbolKeyboard || functionalKeysBottom.isEmpty() || functionalKeysBottom.any { it.isKeyPlaceholder() })
return
if (true /* Settings.getInstance().current.mSingleFunctionalLayout */) { // todo with the customizable functional layout
// remove unwanted keys (emoji, numpad, language switch)
if (!Settings.getInstance().current.mShowsEmojiKey || !params.mId.isAlphabetKeyboard)
functionalKeysBottom.removeFirst { it.label == KeyLabel.EMOJI }
if (!Settings.getInstance().current.isLanguageSwitchKeyEnabled || !params.mId.isAlphabetKeyboard)
functionalKeysBottom.removeFirst { it.label == KeyLabel.LANGUAGE_SWITCH }
if (params.mId.mElementId != KeyboardId.ELEMENT_SYMBOLS)
functionalKeysBottom.removeFirst { it.label == KeyLabel.NUMPAD }
}
// replace comma / period if 2 keys in normal bottom row
if (baseKeys.last().size == 2) {
Log.i("test", "$functionalKeysBottom")
functionalKeysBottom.replaceFirst(
{ it.label == KeyLabel.COMMA || it.groupId == KeyData.GROUP_COMMA},
{ baseKeys.last()[0].copy(newGroupId = 1, newType = baseKeys.last()[0].type ?: it.type) }
)
functionalKeysBottom.replaceFirst(
{ it.label == KeyLabel.PERIOD || it.groupId == KeyData.GROUP_PERIOD},
{ baseKeys.last()[1].copy(newGroupId = 2, newType = baseKeys.last()[1].type ?: it.type) }
)
Log.i("test", "$functionalKeysBottom")
baseKeys.removeLast()
}
// add those extra keys depending on layout (remove later)
val spaceIndex = functionalKeysBottom.indexOfFirst { it.label == KeyLabel.SPACE && it.width <= 0 } // 0 or -1
if (spaceIndex >= 0) {
if (params.mLocaleKeyboardInfos.hasZwnjKey && params.mId.isAlphabetKeyboard) {
// add zwnj key next to space
functionalKeysBottom.add(spaceIndex + 1, TextKeyData(label = KeyLabel.ZWNJ))
} else if (params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS) {
// add / key next to space, todo (later): not any more, but keep it so this PR can be released without too many people complaining
functionalKeysBottom.add(spaceIndex + 1, TextKeyData(label = "/", type = KeyType.FUNCTION))
} else if (params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS_SHIFTED) {
// add < and > keys next to space, todo (later): not any more, but keep it so this PR can be released without too many people complaining
val key1 = TextKeyData(
label = "<",
popup = SimplePopups(listOf("!fixedColumnOrder!3", "", "", "«")),
labelFlags = Key.LABEL_FLAGS_HAS_POPUP_HINT,
type = KeyType.FUNCTION
)
val key2 = TextKeyData(
label = ">",
popup = SimplePopups(listOf("!fixedColumnOrder!3", "", "", "»")),
labelFlags = Key.LABEL_FLAGS_HAS_POPUP_HINT,
type = KeyType.FUNCTION
)
functionalKeysBottom.add(spaceIndex + 1, key2)
functionalKeysBottom.add(spaceIndex, key1)
}
}
allFunctionalKeys[allFunctionalKeys.lastIndex] = functionalKeysBottom
baseKeys.add(emptyList())
}
// ideally we would get all functional keys in a nice list of pairs from the start, but at least it works...
private fun getFunctionalKeysBySide(functionalKeysFromTop: List<KeyData>, functionalKeysFromBottom: List<KeyData>): Pair<List<KeyParams>, List<KeyParams>> {
val (functionalKeysFromTopLeft, functionalKeysFromTopRight) = functionalKeysFromTop.splitAt { it.isKeyPlaceholder() }
val (functionalKeysFromBottomLeft, functionalKeysFromBottomRight) = functionalKeysFromBottom.splitAt { it.isKeyPlaceholder() }
// functional keys from top rows are the outermost, if there are some in the same row
functionalKeysFromTopLeft.addAll(functionalKeysFromBottomLeft)
functionalKeysFromBottomRight.addAll(functionalKeysFromTopRight)
val functionalKeysLeft = functionalKeysFromTopLeft.mapNotNull { it.processFunctionalKeys()?.toKeyParams(params) }
val functionalKeysRight = functionalKeysFromBottomRight.mapNotNull { it.processFunctionalKeys()?.toKeyParams(params) }
return functionalKeysLeft to functionalKeysRight
}
// this is not nice in here, but otherwise we'd need context, and defaultLabelFlags and infos for toKeyParams
// improve it later, but currently this messy way is still ok
private fun KeyData.processFunctionalKeys(): KeyData? {
if (label == KeyLabel.PERIOD) {
// todo: why defaultLabelFlags exactly here? is this for armenian or bengali period labels? try removing also check in holo theme
return copy(newLabelFlags = labelFlags or defaultLabelFlags)
}
if (label == KeyLabel.SHIFT && !infos.hasShiftKey) return null
if (label != KeyLabel.ACTION) return this
return copy(
// todo: evaluating the label should actually only happen in toKeyParams
// this label change already makes it necessary to provide the background in here too, because toKeyParams can't use action as label
newLabel = "${getActionKeyLabel()}|${getActionKeyCode()}",
newPopup = popup.merge(getActionKeyPopupKeys()?.let { SimplePopups(it) }),
// the label change is messing with toKeyParams, so we need to supply the appropriate BG type here
newType = type ?: KeyType.ENTER_EDITING
)
}
private fun addNumberRowOrPopupKeys(baseKeys: MutableList<List<KeyData>>) {
if (!params.mId.mNumberRowEnabled && params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS) {
// replace first symbols row with number row, but use the labels as popupKeys
@ -194,7 +353,7 @@ abstract class KeyboardParser(private val params: KeyboardParams, private val co
else SimpleKeyboardParser(params, context, false)
parser.parseCoreLayout(getLayoutFile(layoutName, context).readText())
} else {
SimpleKeyboardParser(params, context, false).parseCoreLayout(context.readAssetsFile("layouts/$layoutName.txt"))
SimpleKeyboardParser(params, context, false).parseCoreLayout(context.readAssetsLayoutFile("$layoutName.txt"))
}
layout.forEachIndexed { i, row ->
val baseRow = baseKeys.getOrNull(i) ?: return@forEachIndexed
@ -204,34 +363,6 @@ abstract class KeyboardParser(private val params: KeyboardParams, private val co
}
}
// resize keys in last row if they are wider than keys in the row above
// this is done so the keys align with the keys above, like in original layouts
// e.g. for nordic and swiss layouts
private fun resizeLastRowIfNecessaryForAlignment(keysInRows: ArrayList<ArrayList<KeyParams>>) {
if (keysInRows.size < 3)
return
val lastRow = keysInRows.last()
val rowAboveLast = keysInRows[keysInRows.lastIndex - 1]
if (lastRow.any { it.isSpacer } || rowAboveLast.any { it.isSpacer })
return // annoying to deal with, and probably no resize needed anyway
val lastNormalRowKeyWidth = lastRow.first { it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL }.mRelativeWidth
val rowAboveLastNormalRowKeyWidth = rowAboveLast.first { it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL }.mRelativeWidth
if (lastNormalRowKeyWidth <= rowAboveLastNormalRowKeyWidth + 0.0001f)
return // no need
if (lastNormalRowKeyWidth / rowAboveLastNormalRowKeyWidth > 1.1f)
return // don't resize on large size difference
if (lastRow.any { it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL && it.mRelativeWidth != lastNormalRowKeyWidth })
return // normal keys have different width, don't deal with this
val numberOfNormalKeys = lastRow.count { it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL }
val widthBefore = numberOfNormalKeys * lastNormalRowKeyWidth
val widthAfter = numberOfNormalKeys * rowAboveLastNormalRowKeyWidth
val spacerWidth = (widthBefore - widthAfter) / 2
// resize keys and add spacers
lastRow.forEach { if (it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL) it.mRelativeWidth = rowAboveLastNormalRowKeyWidth }
lastRow.add(lastRow.indexOfFirst { it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL }, KeyParams.newSpacer(params, spacerWidth))
lastRow.add(lastRow.indexOfLast { it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL } + 1, KeyParams.newSpacer(params, spacerWidth))
}
private fun createNumericRows(baseKeys: MutableList<List<KeyData>>): ArrayList<ArrayList<KeyParams>> {
val keysInRows = ArrayList<ArrayList<KeyParams>>()
if (context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE && params.mId.mElementId != KeyboardId.ELEMENT_NUMPAD) {
@ -245,16 +376,15 @@ abstract class KeyboardParser(private val params: KeyboardParams, private val co
row.forEach { key ->
var keyParams: KeyParams? = null
// try parsing a functional key
// todo: note that this is ignoring code on those keys, if any
val functionalKeyName = when (key.label) {
// todo (later): maybe add special popupKeys for phone and number layouts?
"." -> if (params.mId.mElementId == KeyboardId.ELEMENT_NUMPAD) "period" else "."
"," -> if (params.mId.mElementId == KeyboardId.ELEMENT_NUMPAD) "comma" else ","
"." -> if (params.mId.mElementId == KeyboardId.ELEMENT_NUMPAD) KeyLabel.PERIOD else "."
"," -> if (params.mId.mElementId == KeyboardId.ELEMENT_NUMPAD) KeyLabel.COMMA else ","
else -> key.label
}
if (functionalKeyName.length > 1 && key.type != KeyType.NUMERIC) { // todo: why exception for numeric?
if (functionalKeyName.length > 1 && key.type != KeyType.NUMERIC) {
try {
keyParams = getFunctionalKeyParams(functionalKeyName)
keyParams = key.copy(newLabel = functionalKeyName).processFunctionalKeys()!!.toKeyParams(params)
} catch (_: Throwable) {} // just use normal label
}
if (keyParams == null) {
@ -264,11 +394,11 @@ abstract class KeyboardParser(private val params: KeyboardParams, private val co
KeyboardId.ELEMENT_PHONE_SYMBOLS -> 0
else -> Key.LABEL_FLAGS_FOLLOW_KEY_LARGE_LETTER_RATIO
}
key.compute(params).toKeyParams(params, 0.17f, labelFlags or defaultLabelFlags)
key.toKeyParams(params, labelFlags or defaultLabelFlags)
} else if (key.label.length == 1 && (params.mId.mElementId == KeyboardId.ELEMENT_PHONE || params.mId.mElementId == KeyboardId.ELEMENT_NUMBER))
key.compute(params).toKeyParams(params, additionalLabelFlags = Key.LABEL_FLAGS_FOLLOW_KEY_LARGE_LETTER_RATIO or defaultLabelFlags)
key.toKeyParams(params, additionalLabelFlags = Key.LABEL_FLAGS_FOLLOW_KEY_LARGE_LETTER_RATIO or defaultLabelFlags)
else
key.compute(params).toKeyParams(params, additionalLabelFlags = defaultLabelFlags)
key.toKeyParams(params, additionalLabelFlags = defaultLabelFlags)
}
if (key.type != KeyType.NUMERIC && keyParams.mBackgroundType != Key.BACKGROUND_TYPE_ACTION)
keyParams.mBackgroundType = Key.BACKGROUND_TYPE_FUNCTIONAL
@ -296,256 +426,24 @@ abstract class KeyboardParser(private val params: KeyboardParams, private val co
// make those keys same width as numeric keys except in numpad layout
// but determine from row size instead of from elementId, in case user wants to adjust numpad layout
if (row.size == baseKeys[0].size) {
paramsRow.getOrNull(n - 1)?.mRelativeWidth = paramsRow[n].mRelativeWidth
paramsRow.getOrNull(n + 1)?.mRelativeWidth = paramsRow[n].mRelativeWidth
paramsRow.getOrNull(n - 1)?.mWidth = paramsRow[n].mWidth
paramsRow.getOrNull(n + 1)?.mWidth = paramsRow[n].mWidth
} else if (row.size == baseKeys[0].size + 2) {
// numpad last row -> make sure the keys next to 0 fit nicely
paramsRow.getOrNull(n - 1)?.mRelativeWidth = paramsRow[n].mRelativeWidth * 0.55f
paramsRow.getOrNull(n - 2)?.mRelativeWidth = paramsRow[n].mRelativeWidth * 0.45f
paramsRow.getOrNull(n + 1)?.mRelativeWidth = paramsRow[n].mRelativeWidth * 0.55f
paramsRow.getOrNull(n + 2)?.mRelativeWidth = paramsRow[n].mRelativeWidth * 0.45f
paramsRow.getOrNull(n - 1)?.mWidth = paramsRow[n].mWidth * 0.55f
paramsRow.getOrNull(n - 2)?.mWidth = paramsRow[n].mWidth * 0.45f
paramsRow.getOrNull(n + 1)?.mWidth = paramsRow[n].mWidth * 0.55f
paramsRow.getOrNull(n + 2)?.mWidth = paramsRow[n].mWidth * 0.45f
}
}
}
val widthSum = paramsRow.sumOf { it.mRelativeWidth }
paramsRow.forEach { it.mRelativeWidth /= widthSum }
val widthSum = paramsRow.sumOf { it.mWidth }
paramsRow.forEach { it.mWidth /= widthSum }
keysInRows.add(paramsRow)
}
return keysInRows
}
private fun parseFunctionalKeys(@StringRes id: Int): List<Pair<List<String>, List<String>>> =
context.getString(id).split("\n").mapNotNull { line ->
if (line.isBlank()) return@mapNotNull null
val p = line.split(";")
splitFunctionalKeyDefs(p.first()) to splitFunctionalKeyDefs(p.last())
}
private fun splitFunctionalKeyDefs(def: String): List<String> {
if (def.isBlank()) return emptyList()
return def.split(",").filter { infos.hasShiftKey || !it.trim().startsWith("shift") }
}
private fun getBottomRowAndAdjustBaseKeys(baseKeys: MutableList<List<KeyData>>): ArrayList<KeyParams> {
val adjustableKeyCount = when (params.mId.mElementId) {
KeyboardId.ELEMENT_SYMBOLS -> 3
KeyboardId.ELEMENT_SYMBOLS_SHIFTED -> 4
else -> 2 // must be alphabet, parser doesn't work for other elementIds
}
val adjustedKeys = if (baseKeys.last().size == adjustableKeyCount) baseKeys.last()
else null
if (adjustedKeys != null)
baseKeys.removeLast()
val bottomRow = ArrayList<KeyParams>()
context.getString(R.string.key_def_bottom_row).split(",").forEach {
val key = it.trim().splitOnWhitespace().first()
val adjustKey = when (key) {
"comma" -> adjustedKeys?.first()
"period" -> adjustedKeys?.last()
else -> null
}
val keyParams = getFunctionalKeyParams(it, adjustKey?.label, adjustKey?.popup?.getPopupKeyLabels(params))
if (key == "space") { // add the extra keys around space
if (params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS) {
bottomRow.add(getFunctionalKeyParams(FunctionalKey.NUMPAD))
bottomRow.add(keyParams)
bottomRow.add(KeyParams(
adjustedKeys?.get(1)?.label ?: "/",
params,
params.mDefaultRelativeKeyWidth,
defaultLabelFlags,
Key.BACKGROUND_TYPE_FUNCTIONAL,
adjustedKeys?.get(1)?.popup
))
} else if (params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS_SHIFTED) {
bottomRow.add(KeyParams(
(adjustedKeys?.get(1)?.label ?: "<").rtlLabel(params),
params,
params.mDefaultRelativeKeyWidth,
defaultLabelFlags or Key.LABEL_FLAGS_HAS_POPUP_HINT,
Key.BACKGROUND_TYPE_FUNCTIONAL,
adjustedKeys?.get(1)?.popup ?: SimplePopups(listOf("!fixedColumnOrder!3", "", "", "«"))
))
bottomRow.add(keyParams)
bottomRow.add(KeyParams(
(adjustedKeys?.get(2)?.label ?: ">").rtlLabel(params),
params,
params.mDefaultRelativeKeyWidth,
defaultLabelFlags or Key.LABEL_FLAGS_HAS_POPUP_HINT,
Key.BACKGROUND_TYPE_FUNCTIONAL,
adjustedKeys?.get(2)?.popup ?: SimplePopups(listOf("!fixedColumnOrder!3", "", "", "»"))
))
} else { // alphabet
if (params.mId.mLanguageSwitchKeyEnabled)
bottomRow.add(getFunctionalKeyParams(FunctionalKey.LANGUAGE_SWITCH))
if (params.mId.mEmojiKeyEnabled)
bottomRow.add(getFunctionalKeyParams(FunctionalKey.EMOJI))
bottomRow.add(keyParams)
if (params.mLocaleKeyboardInfos.hasZwnjKey)
bottomRow.add(getFunctionalKeyParams(FunctionalKey.ZWNJ))
}
} else {
bottomRow.add(keyParams)
}
}
// set space width
val space = bottomRow.first { it.mBackgroundType == Key.BACKGROUND_TYPE_SPACEBAR }
space.mRelativeWidth = 1f - bottomRow.filter { it != space }.sumOf { it.mRelativeWidth }
return bottomRow
}
private fun getNumberRow(): ArrayList<KeyParams> =
params.mLocaleKeyboardInfos.getNumberRow().mapTo(ArrayList()) {
it.toKeyParams(params, additionalLabelFlags = Key.LABEL_FLAGS_DISABLE_HINT_LABEL or defaultLabelFlags)
}
private fun getFunctionalKeyParams(def: String, label: String? = null, popupKeys: Collection<String>? = null): KeyParams {
val split = def.trim().splitOnWhitespace()
val key = FunctionalKey.valueOf(split[0].uppercase())
val width = if (split.size == 2) split[1].substringBefore("%").toFloat() / 100f
else params.mDefaultRelativeKeyWidth
return getFunctionalKeyParams(key, width, label, popupKeys)
}
private fun getFunctionalKeyParams(key: FunctionalKey, relativeWidth: Float? = null, label: String? = null, popupKeys: Collection<String>? = null): KeyParams {
// for comma and period: label will override default, popupKeys will be appended
val width = relativeWidth ?: params.mDefaultRelativeKeyWidth
return when (key) {
FunctionalKey.SYMBOL_ALPHA -> KeyParams(
if (params.mId.isAlphabetKeyboard) getToSymbolLabel() else params.mLocaleKeyboardInfos.labelAlphabet,
KeyCode.ALPHA_SYMBOL,
params,
width,
Key.LABEL_FLAGS_PRESERVE_CASE or Key.LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR,
Key.BACKGROUND_TYPE_FUNCTIONAL,
null
)
FunctionalKey.SYMBOL -> KeyParams(
getToSymbolLabel(),
KeyCode.SYMBOL,
params,
width,
Key.LABEL_FLAGS_PRESERVE_CASE or Key.LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR,
Key.BACKGROUND_TYPE_FUNCTIONAL,
null
)
FunctionalKey.ALPHA -> KeyParams(
params.mLocaleKeyboardInfos.labelAlphabet,
KeyCode.ALPHA,
params,
width,
Key.LABEL_FLAGS_PRESERVE_CASE or Key.LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR,
Key.BACKGROUND_TYPE_FUNCTIONAL,
null
)
FunctionalKey.COMMA -> KeyParams(
label ?: getCommaLabel(),
params,
width,
Key.LABEL_FLAGS_HAS_POPUP_HINT, // previously only if normal comma, but always is more correct
if (label?.first()?.isLetter() == true) Key.BACKGROUND_TYPE_NORMAL // mimic behavior of old dvorak and halmak layouts
else Key.BACKGROUND_TYPE_FUNCTIONAL,
SimplePopups(popupKeys?.let { getCommaPopupKeys() + it } ?: getCommaPopupKeys())
)
FunctionalKey.PERIOD -> KeyParams(
label ?: getPeriodLabel(),
params,
width,
Key.LABEL_FLAGS_HAS_POPUP_HINT or defaultLabelFlags,
if (label?.first()?.isLetter() == true) Key.BACKGROUND_TYPE_NORMAL
else Key.BACKGROUND_TYPE_FUNCTIONAL,
SimplePopups(popupKeys?.let { getPunctuationPopupKeys() + it } ?: getPunctuationPopupKeys())
)
FunctionalKey.SPACE -> KeyParams(
getSpaceLabel(),
params,
width, // will not be used for normal space (only in number layouts)
if (params.mId.isNumberLayout) Key.LABEL_FLAGS_ALIGN_ICON_TO_BOTTOM else 0,
Key.BACKGROUND_TYPE_SPACEBAR,
null
)
FunctionalKey.ACTION -> KeyParams(
"${getActionKeyLabel()}|${getActionKeyCode()}",
params,
width,
Key.LABEL_FLAGS_PRESERVE_CASE
or Key.LABEL_FLAGS_AUTO_X_SCALE
or Key.LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO
or Key.LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR
or Key.LABEL_FLAGS_HAS_POPUP_HINT
or KeyboardTheme.getThemeActionAndEmojiKeyLabelFlags(params.mThemeId),
Key.BACKGROUND_TYPE_ACTION,
getActionKeyPopupKeys()?.let { SimplePopups(it) }
)
FunctionalKey.DELETE -> KeyParams(
"!icon/delete_key|!code/key_delete",
params,
width,
0,
Key.BACKGROUND_TYPE_FUNCTIONAL,
null
)
FunctionalKey.SHIFT -> KeyParams(
"${getShiftLabel()}|!code/key_shift",
params,
width,
Key.LABEL_FLAGS_PRESERVE_CASE or if (!params.mId.isAlphabetKeyboard) Key.LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR else 0,
// todo (later): possibly the whole stickyOn/Off stuff can be removed, currently it should only have a very slight effect in holo
if (params.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED || params.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED)
Key.BACKGROUND_TYPE_STICKY_ON
else Key.BACKGROUND_TYPE_STICKY_OFF,
if (params.mId.isAlphabetKeyboard) SimplePopups(listOf("!noPanelAutoPopupKey!", " |!code/key_capslock")) else null // why the alphabet popup keys actually?
)
FunctionalKey.EMOJI -> KeyParams(
"!icon/emoji_normal_key|!code/key_emoji",
params,
width,
KeyboardTheme.getThemeActionAndEmojiKeyLabelFlags(params.mThemeId),
Key.BACKGROUND_TYPE_FUNCTIONAL,
null
)
// tablet layout has an emoji key that changes to com key in url / mail
FunctionalKey.EMOJI_COM -> if (params.mId.mMode == KeyboardId.MODE_URL || params.mId.mMode == KeyboardId.MODE_EMAIL)
getFunctionalKeyParams(FunctionalKey.COM, width)
else getFunctionalKeyParams(FunctionalKey.EMOJI, width)
FunctionalKey.COM -> KeyParams(
// todo (later): label and popupKeys could be in localeKeyTexts, handled similar to currency key
// better not in the text files, because it should be handled per country
".com",
params,
width,
Key.LABEL_FLAGS_AUTO_X_SCALE or Key.LABEL_FLAGS_FONT_NORMAL or Key.LABEL_FLAGS_HAS_POPUP_HINT or Key.LABEL_FLAGS_PRESERVE_CASE,
Key.BACKGROUND_TYPE_FUNCTIONAL,
SimplePopups(listOf(Key.POPUP_KEYS_HAS_LABELS, ".net", ".org", ".gov", ".edu"))
)
FunctionalKey.LANGUAGE_SWITCH -> KeyParams(
"!icon/language_switch_key|!code/key_language_switch",
params,
width,
0,
Key.BACKGROUND_TYPE_FUNCTIONAL,
null
)
FunctionalKey.NUMPAD -> KeyParams(
"!icon/numpad_key|!code/key_numpad",
params,
width,
0,
Key.BACKGROUND_TYPE_FUNCTIONAL,
null
)
FunctionalKey.ZWNJ -> KeyParams(
"!icon/zwnj_key|\u200C",
params,
width,
Key.LABEL_FLAGS_HAS_POPUP_HINT,
// this may not be a good place to make this choice, but probably it's fine (though reading from settings here is not good)
if (Settings.getInstance().current.mColors.hasKeyBorders) Key.BACKGROUND_TYPE_SPACEBAR else Key.BACKGROUND_TYPE_NORMAL,
SimplePopups(listOf("!icon/zwj_key|\u200D"))
)
}
}
private fun getActionKeyLabel(): String {
if (params.mId.isMultiLine && (params.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED || params.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED))
return "!icon/enter_key"
@ -627,7 +525,7 @@ abstract class KeyboardParser(private val params: KeyboardParams, private val co
}
// remove emoji shortcut on enter in tablet mode (like original, because bottom row always has an emoji key)
// (probably not necessary, but whatever)
if (isTablet() && popupKeys.remove("!icon/emoji_action_key|!code/key_emoji")) {
if (Settings.getInstance().isTablet && popupKeys.remove("!icon/emoji_action_key|!code/key_emoji")) {
val i = popupKeys.indexOfFirst { it.startsWith(Key.POPUP_KEYS_FIXED_COLUMN_ORDER) }
if (i > -1) {
val n = popupKeys[i].substringAfter(Key.POPUP_KEYS_FIXED_COLUMN_ORDER).toIntOrNull()
@ -662,84 +560,6 @@ abstract class KeyboardParser(private val params: KeyboardParams, private val co
return runInLocale(context, locale) { it.getString(id) }
}
private fun getToSymbolLabel() =
if (params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS || params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS_SHIFTED)
params.mLocaleKeyboardInfos.labelAlphabet
else params.mLocaleKeyboardInfos.labelSymbol
private fun getShiftLabel(): String {
val elementId = params.mId.mElementId
if (elementId == KeyboardId.ELEMENT_SYMBOLS_SHIFTED)
return params.mLocaleKeyboardInfos.labelSymbol
if (elementId == KeyboardId.ELEMENT_SYMBOLS)
return params.mLocaleKeyboardInfos.getShiftSymbolLabel(isTablet())
if (elementId == KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED || elementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED
|| elementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED || elementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED)
return "!icon/shift_key_shifted"
return "!icon/shift_key"
}
private fun getPeriodLabel(): String {
if (params.mId.isNumberLayout) return "."
if (params.mId.isAlphabetKeyboard || params.mId.locale.language in listOf("ar", "fa")) // todo: this exception is not so great...
return params.mLocaleKeyboardInfos.labelPeriod
return "."
}
private fun getCommaLabel(): String {
if (params.mId.mMode == KeyboardId.MODE_URL && params.mId.isAlphabetKeyboard)
return "/"
if (params.mId.mMode == KeyboardId.MODE_EMAIL && params.mId.isAlphabetKeyboard)
return "\\@"
if (params.mId.isNumberLayout)
return ","
return params.mLocaleKeyboardInfos.labelComma
}
private fun getCommaPopupKeys(): List<String> {
val keys = mutableListOf<String>()
if (!params.mId.mDeviceLocked)
keys.add("!icon/clipboard_normal_key|!code/key_clipboard")
if (!params.mId.mEmojiKeyEnabled && !params.mId.isNumberLayout)
keys.add("!icon/emoji_normal_key|!code/key_emoji")
if (!params.mId.mLanguageSwitchKeyEnabled)
keys.add("!icon/language_switch_key|!code/key_language_switch")
if (!params.mId.mOneHandedModeEnabled)
keys.add("!icon/start_onehanded_mode_key|!code/key_start_onehanded")
if (!params.mId.mDeviceLocked)
keys.add("!icon/settings_key|!code/key_settings")
return keys
}
private fun getPunctuationPopupKeys(): List<String> {
if (params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS || params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS_SHIFTED)
return listOf("")
if (params.mId.isNumberLayout)
return listOf(":", "", ";", "", "π", "", "°", "^")
val popupKeys = params.mLocaleKeyboardInfos.getPopupKeys("punctuation")!!.toMutableList()
if (params.mId.mSubtype.isRtlSubtype) {
for (i in popupKeys.indices)
popupKeys[i] = popupKeys[i].rtlLabel(params) // for parentheses
}
if (isTablet() && popupKeys.contains("!") && popupKeys.contains("?")) {
// remove ! and ? keys and reduce number in autoColumnOrder
// this makes use of removal of empty popupKeys in PopupKeySpec.insertAdditionalPopupKeys
popupKeys[popupKeys.indexOf("!")] = ""
popupKeys[popupKeys.indexOf("?")] = ""
val columns = popupKeys[0].substringAfter(Key.POPUP_KEYS_AUTO_COLUMN_ORDER).toIntOrNull()
if (columns != null)
popupKeys[0] = "${Key.POPUP_KEYS_AUTO_COLUMN_ORDER}${columns - 1}"
}
return popupKeys
}
private fun getSpaceLabel(): String =
if (params.mId.mElementId <= KeyboardId.ELEMENT_SYMBOLS_SHIFTED)
"!icon/space_key|!code/key_space"
else "!icon/space_key_for_number_layout|!code/key_space"
private fun isTablet() = context.resources.getInteger(R.integer.config_screen_metrics) >= 3
companion object {
private const val TAG = "KeyboardParser"
@ -755,15 +575,15 @@ abstract class KeyboardParser(private val params: KeyboardParams, private val co
}
val layoutFileNames = context.assets.list("layouts")!!
if (layoutFileNames.contains("$layoutName.json")) {
return JsonKeyboardParser(params, context).parseLayoutString(context.readAssetsFile("layouts${File.separator}$layoutName.json"))
return JsonKeyboardParser(params, context).parseLayoutString(context.readAssetsLayoutFile("$layoutName.json"))
}
if (layoutFileNames.contains("$layoutName.txt")) {
return SimpleKeyboardParser(params, context).parseLayoutString(context.readAssetsFile("layouts${File.separator}$layoutName.txt"))
return SimpleKeyboardParser(params, context).parseLayoutString(context.readAssetsLayoutFile("$layoutName.txt"))
}
throw IllegalStateException("can't parse layout $layoutName with id ${params.mId} and elementId ${params.mId.mElementId}")
}
private fun Context.readAssetsFile(name: String) = assets.open(name).reader().readText()
private fun Context.readAssetsLayoutFile(name: String) = assets.open("layouts${File.separator}$name").reader().readText()
private fun getLayoutFileName(params: KeyboardParams, context: Context, overrideElementId: Int? = null): String {
var checkForCustom = true
@ -815,10 +635,6 @@ abstract class KeyboardParser(private val params: KeyboardParams, private val co
}
}
protected enum class FunctionalKey {
EMOJI, LANGUAGE_SWITCH, COM, EMOJI_COM, ACTION, DELETE, PERIOD, COMMA, SPACE, SHIFT, NUMPAD, SYMBOL, ALPHA, SYMBOL_ALPHA, ZWNJ
}
}
// todo: actually this should be in some separate file

View file

@ -14,7 +14,7 @@ object KeyCode {
const val INTERNAL_FLORIS_MIN = -9999
const val INTERNAL_FLORIS_MAX = -1
val INTERNAL_FLORIS = INTERNAL_FLORIS_MIN..INTERNAL_FLORIS_MAX
val INTERNAL_FLORIS = INTERNAL_FLORIS_MIN..INTERNAL_FLORIS_MAX // do NOT add key codes in this range
val INTERNAL_HELI = -19999..-10000 // for keys exclusive to this app
val CURRENCY = CURRENCY_SLOT_6..CURRENCY_SLOT_1
}
@ -122,7 +122,7 @@ object KeyCode {
const val CJK_SPACE = 12288
// heliboard only codes
const val ALPHA_SYMBOL = -10001
const val SYMBOL_ALPHA = -10001
const val START_ONE_HANDED_MODE = -10002
const val STOP_ONE_HANDED_MODE = -10003
const val SWITCH_ONE_HANDED_MODE = -10004
@ -132,8 +132,8 @@ object KeyCode {
// Code value representing the code is not specified.
const val NOT_SPECIFIED = -10008 // todo: not sure if there is need to have the "old" unspecified keyCode different, just test it and maybe merge
const val CLIPBOARD_COPY_ALL = -10009
const val PAGE_UP = -10010
const val PAGE_DOWN = -10011
const val PAGE_UP = -10010
const val PAGE_DOWN = -10011
/** to make sure a FlorisBoard code works when reading a JSON layout */
fun Int.checkAndConvertCode(): Int = if (this > 0) this else when (this) {
@ -145,8 +145,8 @@ object KeyCode {
SHIFT, CAPS_LOCK, MULTIPLE_CODE_POINTS, UNSPECIFIED,
// heliboard only
ALPHA_SYMBOL, START_ONE_HANDED_MODE, STOP_ONE_HANDED_MODE, SWITCH_ONE_HANDED_MODE, SHIFT_ENTER,
ACTION_NEXT, ACTION_PREVIOUS, NOT_SPECIFIED
SYMBOL_ALPHA, START_ONE_HANDED_MODE, STOP_ONE_HANDED_MODE, SWITCH_ONE_HANDED_MODE, SHIFT_ENTER,
ACTION_NEXT, ACTION_PREVIOUS, NOT_SPECIFIED, CLIPBOARD_COPY_ALL, PAGE_UP, PAGE_DOWN
-> this
// conversion
@ -156,26 +156,4 @@ object KeyCode {
else -> throw IllegalStateException("key code $this not yet supported")
}
/** to make sure a FlorisBoard label works when reading a JSON layout */
// resulting special labels should be names of FunctionalKey enum, case insensitive
fun String.convertFlorisLabel(): String = when (this) {
"view_characters" -> "alpha"
"view_symbols" -> "symbol"
"view_numeric_advanced" -> "numpad"
"view_phone" -> "alpha" // phone keyboard is treated like alphabet, just with different layout
"view_phone2" -> "symbols" // phone symbols
"ime_ui_mode_media" -> "emoji"
"ime_ui_mode_clipboard" -> "clipboard" // todo: is this supported? when yes -> add to readme, and add a test
"ime_ui_mode_text" -> "alpha"
"currency_slot_1" -> "$$$"
"currency_slot_2" -> "$$$1"
"currency_slot_3" -> "$$$2"
"currency_slot_4" -> "$$$3"
"currency_slot_5" -> "$$$4"
"currency_slot_6" -> "$$$5"
"enter" -> "action"
"half_space" -> "zwnj"
else -> this
}
}

View file

@ -0,0 +1,48 @@
package helium314.keyboard.keyboard.internal.keyboard_parser.floris
/** labels for functional / special keys */
object KeyLabel {
const val EMOJI = "emoji"
const val COM = "com"
const val LANGUAGE_SWITCH = "language_switch"
const val ACTION = "action"
const val DELETE = "delete"
const val SHIFT = "shift"
const val NUMPAD = "numpad"
const val SYMBOL = "symbol"
const val ALPHA = "alpha"
const val SYMBOL_ALPHA = "symbol_alpha"
const val PERIOD = "period"
const val COMMA = "comma"
const val SPACE = "space"
const val ZWNJ = "zwnj"
const val CURRENCY = "$$$"
const val CURRENCY1 = "$$$1"
const val CURRENCY2 = "$$$2"
const val CURRENCY3 = "$$$3"
const val CURRENCY4 = "$$$4"
const val CURRENCY5 = "$$$5"
/** to make sure a FlorisBoard label works when reading a JSON layout */
// resulting special labels should be names of FunctionalKey enum, case insensitive
fun String.convertFlorisLabel(): String = when (this) {
"view_characters" -> ALPHA
"view_symbols" -> SYMBOL
"view_numeric_advanced" -> NUMPAD
"view_phone" -> ALPHA // phone keyboard is treated like alphabet, just with different layout
"view_phone2" -> SYMBOL // phone symbols
"ime_ui_mode_media" -> EMOJI
"ime_ui_mode_clipboard" -> "clipboard" // todo: is this supported? when yes -> add to readme, and add a test
"ime_ui_mode_text" -> ALPHA
"currency_slot_1" -> CURRENCY
"currency_slot_2" -> CURRENCY1
"currency_slot_3" -> CURRENCY2
"currency_slot_4" -> CURRENCY3
"currency_slot_5" -> CURRENCY4
"currency_slot_6" -> CURRENCY5
"enter" -> ACTION
"half_space" -> ZWNJ
else -> this
}
}

View file

@ -21,18 +21,16 @@ import kotlinx.serialization.encoding.Encoder
*/
@Serializable(with = KeyTypeSerializer::class)
enum class KeyType {
// todo: implement the effect on background
// also, how to get that specific space bar background?
CHARACTER, // default
ENTER_EDITING, // enter/insert/delete, gets functional key background (if not action key)
ENTER_EDITING, // should be enter/insert/delete, but always gets action key background
FUNCTION, // f1..., gets functional key background
LOCK, // scroll lock, num lock, caps lock, gets functional key background
LOCK, // scroll lock, num lock, caps lock, gets sticky on/off background, which currently is the same as functional background
MODIFIER, // alt, ctrl, shift, gets functional key background
NAVIGATION, // home, page up, page down, tab, arrows, geta default background
SYSTEM_GUI, // esc, print, pause, meta, (keyboard layout switch), geta functional background
NUMERIC, // numpad keys, get larger letter and larger width
PLACEHOLDER, // other keys go here, e.g. in shift, placeholder, delete the placeholder gets (typically) replaced by the bottom keyboard row
UNSPECIFIED; // treated like default
NAVIGATION, // home, page up, page down, tab, arrows, gets space background because it'S still the most suitable type
SYSTEM_GUI, // esc, print, pause, meta, (keyboard layout switch), gets functional background
NUMERIC, // numpad keys, get larger letter and larger width in number layouts, and default background
PLACEHOLDER, // spacer, or actual placeholder when used in functional key layouts
UNSPECIFIED; // empty background
override fun toString(): String {
return super.toString().lowercase()
@ -40,7 +38,13 @@ enum class KeyType {
companion object {
fun fromString(string: String): KeyType {
return valueOf(string.uppercase())
// resolve alternative names
return when (string) {
"space" -> NAVIGATION
"action" -> ENTER_EDITING
"shift" -> LOCK
else -> valueOf(string.uppercase())
}
}
}
}

View file

@ -6,30 +6,49 @@
package helium314.keyboard.keyboard.internal.keyboard_parser.floris
import kotlinx.serialization.Serializable
import helium314.keyboard.keyboard.internal.KeySpecParser
import helium314.keyboard.keyboard.internal.KeyboardParams
import helium314.keyboard.latin.utils.addCollections
// only the constructor and name remain from FlorisBoard
// we don't care about the difference between main and relevant (at least for now)
@Serializable
open class PopupSet<T : AbstractKeyData>(
open val main: T? = null,
open val relevant: List<T>? = null
open val relevant: Collection<T>? = null
) {
// get labels of all popup keys
open fun getPopupKeyLabels(params: KeyboardParams): Collection<String>? {
if (main == null && relevant == null) return null
val popupKeys = mutableListOf<String>()
main?.getPopupLabel(params)?.let { popupKeys.add(it) }
relevant?.let { popupKeys.addAll(it.map { it.getPopupLabel(params) }) }
main?.compute(params)?.getPopupLabel(params)?.let { popupKeys.add(it) }
relevant?.let { popupKeys.addAll(it.mapNotNull { it.compute(params)?.getPopupLabel(params) }) }
if (popupKeys.isEmpty()) return null
return popupKeys
}
open fun isEmpty(): Boolean = main == null && relevant.isNullOrEmpty()
var numberIndex: Int? = null
var symbol: String? = null // maybe list of keys?
fun <U : AbstractKeyData> merge(other: PopupSet<U>?): PopupSet<out AbstractKeyData> {
if (other == null || other.isEmpty()) return this
if (this.isEmpty()) return other
if (this is SimplePopups) {
if (other is SimplePopups)
return SimplePopups(addCollections(popupKeys, other.popupKeys))
return PopupSet(other.main, addCollections(popupKeys?.map { it.toTextKey() }, other.relevant))
} else if (other is SimplePopups) {
return PopupSet(main, addCollections(relevant, other.popupKeys?.map { it.toTextKey() }))
}
val newMain = if (main == null) other.main else main
val newRelevant = addCollections(relevant, other.relevant)
if (main != null && other.main != null)
return PopupSet(newMain, addCollections(listOf(other.main!!), newRelevant))
return PopupSet(newMain, newRelevant)
}
}
class SimplePopups(val popupKeys: Collection<String>?) : PopupSet<AbstractKeyData>() {
override fun getPopupKeyLabels(params: KeyboardParams) = popupKeys
override fun isEmpty(): Boolean = popupKeys.isNullOrEmpty()
}

View file

@ -9,12 +9,16 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import helium314.keyboard.keyboard.Key
import helium314.keyboard.keyboard.KeyboardId
import helium314.keyboard.keyboard.KeyboardTheme
import helium314.keyboard.keyboard.internal.KeyboardIconsSet
import helium314.keyboard.keyboard.internal.KeyboardParams
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode.checkAndConvertCode
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode.convertFlorisLabel
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyLabel.convertFlorisLabel
import helium314.keyboard.keyboard.internal.keyboard_parser.rtlLabel
import helium314.keyboard.latin.common.Constants
import helium314.keyboard.latin.common.StringUtils
import helium314.keyboard.latin.settings.Settings
// taken from FlorisBoard, small modifications (see also KeyData)
// internal keys removed (currently no plan to support them)
@ -32,13 +36,17 @@ import helium314.keyboard.latin.common.StringUtils
* @property popup The popups for ths key. Can also dynamically be provided via popup extensions.
*/
sealed interface KeyData : AbstractKeyData {
val type: KeyType
val type: KeyType?
val code: Int
val label: String
val groupId: Int
val popup: PopupSet<AbstractKeyData> // not nullable because can't add number otherwise
val popup: PopupSet<out AbstractKeyData> // not nullable because can't add number otherwise
val width: Float // in percent of keyboard width, 0 is default (depends on key), -1 is fill (like space bar)
val labelFlags: Int
fun copy(newType: KeyType? = type, newCode: Int = code, newLabel: String = label, newGroupId: Int = groupId,
newPopup: PopupSet<out AbstractKeyData> = popup, newWidth: Float = width, newLabelFlags: Int = labelFlags): KeyData
// groups (currently) not supported
companion object {
/**
@ -49,15 +57,15 @@ sealed interface KeyData : AbstractKeyData {
/**
* Constant for the Left modifier key group. Any key belonging to this group will get the
* popups specified for "~left" in the popup mapping.
* popups specified for the comma key.
*/
const val GROUP_LEFT: Int = 1
const val GROUP_COMMA: Int = 1
/**
* Constant for the right modifier key group. Any key belonging to this group will get the
* popups specified for "~right" in the popup mapping.
* popups specified for the period key.
*/
const val GROUP_RIGHT: Int = 2
const val GROUP_PERIOD: Int = 2
/**
* Constant for the enter modifier key group. Any key belonging to this group will get the
@ -70,100 +78,276 @@ sealed interface KeyData : AbstractKeyData {
* popups specified for "~kana" in the popup mapping.
*/
const val GROUP_KANA: Int = 97
private fun getShiftLabel(params: KeyboardParams) = when (params.mId.mElementId) {
KeyboardId.ELEMENT_SYMBOLS_SHIFTED -> params.mLocaleKeyboardInfos.labelSymbol
KeyboardId.ELEMENT_SYMBOLS -> params.mLocaleKeyboardInfos.getShiftSymbolLabel(Settings.getInstance().isTablet)
KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED, KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED,
KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED, KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED -> "!icon/${KeyboardIconsSet.NAME_SHIFT_KEY_SHIFTED}"
else -> "!icon/${KeyboardIconsSet.NAME_SHIFT_KEY}"
}
// todo (later): try avoiding this weirdness
// maybe just remove it and if users want it they can use custom functional layouts?
// but it has been like this "forever" and actually seems to make sense
private fun getPeriodLabel(params: KeyboardParams): String {
if (params.mId.isNumberLayout) return "."
if (params.mId.isAlphabetKeyboard || params.mId.locale.language in listOf("ar", "fa"))
return params.mLocaleKeyboardInfos.labelPeriod
return "."
}
private fun getSpaceLabel(params: KeyboardParams): String =
if (params.mId.mElementId <= KeyboardId.ELEMENT_SYMBOLS_SHIFTED)
"!icon/space_key|!code/key_space"
else "!icon/space_key_for_number_layout|!code/key_space"
private fun getCommaPopupKeys(params: KeyboardParams): List<String> {
val keys = mutableListOf<String>()
if (!params.mId.mDeviceLocked)
keys.add("!icon/clipboard_normal_key|!code/key_clipboard")
if (!params.mId.mEmojiKeyEnabled && !params.mId.isNumberLayout)
keys.add("!icon/emoji_normal_key|!code/key_emoji")
if (!params.mId.mLanguageSwitchKeyEnabled)
keys.add("!icon/language_switch_key|!code/key_language_switch")
if (!params.mId.mOneHandedModeEnabled)
keys.add("!icon/start_onehanded_mode_key|!code/key_start_onehanded")
if (!params.mId.mDeviceLocked)
keys.add("!icon/settings_key|!code/key_settings")
return keys
}
private fun getPunctuationPopupKeys(params: KeyboardParams): List<String> {
if (params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS || params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS_SHIFTED)
return listOf("")
if (params.mId.isNumberLayout)
return listOf(":", "", ";", "", "π", "", "°", "^")
val popupKeys = params.mLocaleKeyboardInfos.getPopupKeys("punctuation")!!.toMutableList()
if (params.mId.mSubtype.isRtlSubtype) {
for (i in popupKeys.indices)
popupKeys[i] = popupKeys[i].rtlLabel(params) // for parentheses
}
if (Settings.getInstance().isTablet && popupKeys.contains("!") && popupKeys.contains("?")) {
// remove ! and ? keys and reduce number in autoColumnOrder
// this makes use of removal of empty popupKeys in PopupKeySpec.insertAdditionalPopupKeys
popupKeys[popupKeys.indexOf("!")] = ""
popupKeys[popupKeys.indexOf("?")] = ""
val columns = popupKeys[0].substringAfter(Key.POPUP_KEYS_AUTO_COLUMN_ORDER).toIntOrNull()
if (columns != null)
popupKeys[0] = "${Key.POPUP_KEYS_AUTO_COLUMN_ORDER}${columns - 1}"
}
return popupKeys
}
}
// make it non-nullable for simplicity, and to reflect current implementations
override fun compute(params: KeyboardParams): KeyData {
require(groupId <= GROUP_ENTER) { "only groups up to GROUP_ENTER are supported" }
require(label.isNotEmpty() || type == KeyType.PLACEHOLDER || code != KeyCode.UNSPECIFIED) { "non-placeholder key has no code and no label" }
val newLabel = label.convertFlorisLabel()
val newCode = code.checkAndConvertCode()
// resolve currency keys
if (newLabel.startsWith("$$$") || newCode in KeyCode.Spec.CURRENCY) {
val currencyKey = params.mLocaleKeyboardInfos.currencyKey
val currencyCodeAsString = if (newCode in KeyCode.Spec.CURRENCY) {
when (newCode) {
KeyCode.CURRENCY_SLOT_1 -> "|" + currencyKey.first
KeyCode.CURRENCY_SLOT_2 -> "|" + currencyKey.second[0]
KeyCode.CURRENCY_SLOT_3 -> "|" + currencyKey.second[1]
KeyCode.CURRENCY_SLOT_4 -> "|" + currencyKey.second[2]
KeyCode.CURRENCY_SLOT_5 -> "|" + currencyKey.second[3]
KeyCode.CURRENCY_SLOT_6 -> "|" + currencyKey.second[4]
else -> ""
}
} else ""
if (newLabel == "$$$") {
val finalLabel = currencyKey.first + currencyCodeAsString
// the flag is to match old parser, but why is it there for main currency key and not for others?
return TextKeyData(type, KeyCode.UNSPECIFIED, finalLabel, groupId, SimplePopups(currencyKey.second), labelFlags or Key.LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO)
}
val n = newLabel.substringAfter("$$$").toIntOrNull()
if (n != null && n <= 5 && n > 0) {
val finalLabel = currencyKey.second[n - 1] + currencyCodeAsString
return TextKeyData(type, KeyCode.UNSPECIFIED, finalLabel, groupId, popup, labelFlags)
}
}
if (newCode != code || newLabel != label)
return TextKeyData(type, newCode, newLabel, groupId, popup, labelFlags).compute(params)
return copy(newCode = newCode, newLabel = newLabel)
return this
}
fun isSpaceKey(): Boolean {
return type == KeyType.CHARACTER && (code == Constants.CODE_SPACE || code == KeyCode.CJK_SPACE
|| code == KeyCode.ZWNJ || code == KeyCode.KESHIDA)
return code == Constants.CODE_SPACE || code == KeyCode.CJK_SPACE || code == KeyCode.ZWNJ || code == KeyCode.KESHIDA
}
fun toKeyParams(params: KeyboardParams, width: Float = params.mDefaultRelativeKeyWidth, additionalLabelFlags: Int = 0): Key.KeyParams {
// todo: remove checks here, do only when reading json layouts
// numeric keys are assigned a higher width in number layouts
require(type == KeyType.CHARACTER || type == KeyType.NUMERIC) { "only KeyType CHARACTER or NUMERIC is supported" }
// allow GROUP_ENTER negative codes so original florisboard number layouts can be used, bu actually it's ignored
require(groupId == GROUP_DEFAULT || groupId == GROUP_ENTER) { "currently only GROUP_DEFAULT or GROUP_ENTER is supported" }
require(code != KeyCode.UNSPECIFIED || label.isNotEmpty()) { "key has no code and no label" }
fun isKeyPlaceholder() = type == KeyType.PLACEHOLDER && code == KeyCode.UNSPECIFIED && width == 0f
return if (code == KeyCode.UNSPECIFIED || code == KeyCode.MULTIPLE_CODE_POINTS) {
/** this expects that codes and labels are already converted from FlorisBoard values, usually through compute */
fun toKeyParams(params: KeyboardParams, additionalLabelFlags: Int = 0): Key.KeyParams {
if (type == KeyType.PLACEHOLDER) return Key.KeyParams.newSpacer(params, width)
val newWidth = if (width == 0f) getDefaultWidth(params) else width
val newCode: Int
val newLabel: String
if (code in KeyCode.Spec.CURRENCY) {
// special treatment necessary, because we may need to encode it in the label
// (currency is a string, so might have more than 1 codepoint)
newCode = 0
val l = processLabel(params)
newLabel = when (code) {
// consider currency codes for label
KeyCode.CURRENCY_SLOT_1 -> "$l|${params.mLocaleKeyboardInfos.currencyKey.first}"
KeyCode.CURRENCY_SLOT_2 -> "$l|${params.mLocaleKeyboardInfos.currencyKey.second[0]}"
KeyCode.CURRENCY_SLOT_3 -> "$l|${params.mLocaleKeyboardInfos.currencyKey.second[1]}"
KeyCode.CURRENCY_SLOT_4 -> "$l|${params.mLocaleKeyboardInfos.currencyKey.second[2]}"
KeyCode.CURRENCY_SLOT_5 -> "$l|${params.mLocaleKeyboardInfos.currencyKey.second[3]}"
KeyCode.CURRENCY_SLOT_6 -> "$l|${params.mLocaleKeyboardInfos.currencyKey.second[4]}"
else -> throw IllegalStateException("code in currency range, but not in currency range?")
}
} else {
newCode = processCode()
newLabel = processLabel(params)
}
val newLabelFlags = labelFlags or additionalLabelFlags or getAdditionalLabelFlags(params)
val newPopupKeys = popup.merge(getAdditionalPopupKeys(params))
val background = when (type) {
KeyType.CHARACTER, KeyType.NUMERIC -> Key.BACKGROUND_TYPE_NORMAL
KeyType.FUNCTION, KeyType.MODIFIER, KeyType.SYSTEM_GUI -> Key.BACKGROUND_TYPE_FUNCTIONAL
KeyType.PLACEHOLDER, KeyType.UNSPECIFIED -> Key.BACKGROUND_TYPE_EMPTY
KeyType.NAVIGATION -> Key.BACKGROUND_TYPE_SPACEBAR
KeyType.ENTER_EDITING -> Key.BACKGROUND_TYPE_ACTION
KeyType.LOCK -> getShiftBackground(params)
null -> getDefaultBackground(params)
}
return if (newCode == KeyCode.UNSPECIFIED || newCode == KeyCode.MULTIPLE_CODE_POINTS) {
// code will be determined from label if possible (i.e. label is single code point)
// but also longer labels should work without issues, also for MultiTextKeyData
if (this is MultiTextKeyData) {
val outputText = String(codePoints, 0, codePoints.size)
Key.KeyParams(
"$label|$outputText",
code,
"$newLabel|$outputText",
newCode,
params,
width,
labelFlags or additionalLabelFlags,
Key.BACKGROUND_TYPE_NORMAL, // todo (when supported): determine type
popup,
newWidth,
newLabelFlags,
background,
newPopupKeys,
)
} else {
Key.KeyParams(
label.rtlLabel(params), // todo (when supported): convert special labels to keySpec
newLabel.rtlLabel(params), // todo (when supported): convert special labels to keySpec
params,
width,
labelFlags or additionalLabelFlags,
Key.BACKGROUND_TYPE_NORMAL, // todo (when supported): determine type
popup,
newWidth,
newLabelFlags,
background,
newPopupKeys,
)
}
} else {
Key.KeyParams(
label.ifEmpty { StringUtils.newSingleCodePointString(code) },
code,
newLabel.ifEmpty { StringUtils.newSingleCodePointString(newCode) },
newCode,
params,
width,
labelFlags or additionalLabelFlags,
Key.BACKGROUND_TYPE_NORMAL,
popup,
newWidth,
newLabelFlags,
background,
newPopupKeys,
)
}
}
private fun getDefaultBackground(params: KeyboardParams): Int {
// functional keys
when (label) { // or use code?
KeyLabel.SYMBOL_ALPHA, KeyLabel.SYMBOL, KeyLabel.ALPHA, KeyLabel.COMMA, KeyLabel.PERIOD, KeyLabel.DELETE,
KeyLabel.EMOJI, KeyLabel.COM, KeyLabel.LANGUAGE_SWITCH, KeyLabel.NUMPAD -> return Key.BACKGROUND_TYPE_FUNCTIONAL
KeyLabel.SPACE, KeyLabel.ZWNJ -> return Key.BACKGROUND_TYPE_SPACEBAR
KeyLabel.ACTION -> return Key.BACKGROUND_TYPE_ACTION
KeyLabel.SHIFT -> return getShiftBackground(params)
}
if (type == KeyType.PLACEHOLDER) return Key.BACKGROUND_TYPE_EMPTY
return Key.BACKGROUND_TYPE_NORMAL
}
// todo (later): possibly the whole stickyOn/Off stuff can be removed, currently it should only have a very slight effect in holo
// but iirc there is some attempt in reviving the sticky thing, right?
private fun getShiftBackground(params: KeyboardParams): Int {
return if (params.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED
|| params.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED) Key.BACKGROUND_TYPE_STICKY_ON
else Key.BACKGROUND_TYPE_STICKY_OFF
}
private fun getDefaultWidth(params: KeyboardParams): Float {
return if (label == KeyLabel.SPACE && params.mId.isAlphaOrSymbolKeyboard) -1f
else if (type == KeyType.NUMERIC && params.mId.isNumberLayout) 0.17f // todo (later) consider making this -1?
else params.mDefaultKeyWidth
}
// todo (later): encoding the code in the label should be avoided, because we know it already
private fun processLabel(params: KeyboardParams): String = when (label) {
KeyLabel.SYMBOL_ALPHA -> if (params.mId.isAlphabetKeyboard) params.mLocaleKeyboardInfos.labelSymbol else params.mLocaleKeyboardInfos.labelAlphabet
KeyLabel.SYMBOL -> params.mLocaleKeyboardInfos.labelSymbol
KeyLabel.ALPHA -> params.mLocaleKeyboardInfos.labelAlphabet
KeyLabel.COMMA -> params.mLocaleKeyboardInfos.labelComma
KeyLabel.PERIOD -> getPeriodLabel(params)
KeyLabel.SPACE -> getSpaceLabel(params)
// KeyLabel.ACTION -> "${getActionKeyLabel(params)}|${getActionKeyCode(params)}" would need context
KeyLabel.DELETE -> "!icon/delete_key|!code/key_delete"
KeyLabel.SHIFT -> "${getShiftLabel(params)}|!code/key_shift"
KeyLabel.EMOJI -> "!icon/emoji_normal_key|!code/key_emoji"
// todo (later): label and popupKeys for .com should be in localeKeyTexts, handled similar to currency key
KeyLabel.COM -> ".com"
KeyLabel.LANGUAGE_SWITCH -> "!icon/language_switch_key|!code/key_language_switch"
KeyLabel.NUMPAD -> "!icon/numpad_key|!code/key_numpad"
KeyLabel.ZWNJ -> "!icon/zwnj_key|\u200C"
KeyLabel.CURRENCY -> params.mLocaleKeyboardInfos.currencyKey.first
KeyLabel.CURRENCY1 -> params.mLocaleKeyboardInfos.currencyKey.second[0]
KeyLabel.CURRENCY2 -> params.mLocaleKeyboardInfos.currencyKey.second[1]
KeyLabel.CURRENCY3 -> params.mLocaleKeyboardInfos.currencyKey.second[2]
KeyLabel.CURRENCY4 -> params.mLocaleKeyboardInfos.currencyKey.second[3]
KeyLabel.CURRENCY5 -> params.mLocaleKeyboardInfos.currencyKey.second[4]
else -> label
}
private fun processCode(): Int {
if (code != KeyCode.UNSPECIFIED) return code
return when (label) {
KeyLabel.SYMBOL_ALPHA -> KeyCode.SYMBOL_ALPHA
KeyLabel.SYMBOL -> KeyCode.SYMBOL
KeyLabel.ALPHA -> KeyCode.ALPHA
else -> code
}
}
// todo (later): add explanations / reasoning, often this is just taken from conversion from AOSP layouts
private fun getAdditionalLabelFlags(params: KeyboardParams): Int {
return when (label) {
KeyLabel.ALPHA, KeyLabel.SYMBOL_ALPHA, KeyLabel.SYMBOL -> Key.LABEL_FLAGS_PRESERVE_CASE or Key.LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR
KeyLabel.PERIOD, KeyLabel.COMMA -> Key.LABEL_FLAGS_HAS_POPUP_HINT // todo: period also has defaultLabelFlags -> when is this relevant?
KeyLabel.ACTION -> {
Key.LABEL_FLAGS_PRESERVE_CASE or Key.LABEL_FLAGS_AUTO_X_SCALE or
Key.LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO or Key.LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR or
Key.LABEL_FLAGS_HAS_POPUP_HINT or KeyboardTheme.getThemeActionAndEmojiKeyLabelFlags(params.mThemeId)
}
KeyLabel.SPACE -> if (params.mId.isNumberLayout) Key.LABEL_FLAGS_ALIGN_ICON_TO_BOTTOM else 0
KeyLabel.SHIFT -> Key.LABEL_FLAGS_PRESERVE_CASE or if (!params.mId.isAlphabetKeyboard) Key.LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR else 0
KeyLabel.EMOJI -> KeyboardTheme.getThemeActionAndEmojiKeyLabelFlags(params.mThemeId)
KeyLabel.COM -> Key.LABEL_FLAGS_AUTO_X_SCALE or Key.LABEL_FLAGS_FONT_NORMAL or Key.LABEL_FLAGS_HAS_POPUP_HINT or Key.LABEL_FLAGS_PRESERVE_CASE
KeyLabel.ZWNJ -> Key.LABEL_FLAGS_HAS_POPUP_HINT
KeyLabel.CURRENCY -> Key.LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO
else -> 0
}
}
private fun getAdditionalPopupKeys(params: KeyboardParams): PopupSet<AbstractKeyData>? {
if (groupId == GROUP_COMMA) return SimplePopups(getCommaPopupKeys(params))
if (groupId == GROUP_PERIOD) return SimplePopups(getPunctuationPopupKeys(params))
// if (groupId == GROUP_ENTER) return getActionKeyPopupKeys(params)?.let { SimplePopups(it) }
return when (label) {
KeyLabel.COMMA -> SimplePopups(getCommaPopupKeys(params))
KeyLabel.PERIOD -> SimplePopups(getPunctuationPopupKeys(params))
// KeyLabel.ACTION -> getActionKeyPopupKeys(params)?.let { SimplePopups(it) }
KeyLabel.SHIFT -> {
if (params.mId.isAlphabetKeyboard) SimplePopups(
listOf(
"!noPanelAutoPopupKey!",
" |!code/key_capslock"
)
) else null // why the alphabet popup keys actually?
}
KeyLabel.COM -> SimplePopups(listOf(Key.POPUP_KEYS_HAS_LABELS, ".net", ".org", ".gov", ".edu"))
KeyLabel.ZWNJ -> SimplePopups(listOf("!icon/zwj_key|\u200D"))
// only add currency popups if there are none defined on the key
KeyLabel.CURRENCY -> if (popup.isEmpty()) SimplePopups(params.mLocaleKeyboardInfos.currencyKey.second) else null
else -> null
}
}
}
/**
* Data class which describes a single key and its attributes.
*
* @property type The type of the key. Some actions require both [code] and [type] to match in order
* to be successfully executed. Defaults to [KeyType.CHARACTER].
* to be successfully executed. Defaults to null.
* @property code The UTF-8 encoded code of the character. The code defined here is used as the
* data passed to the system. Defaults to 0.
* @property label The string used to display the key in the UI. Is not used for the actual data
@ -173,11 +357,12 @@ sealed interface KeyData : AbstractKeyData {
@Serializable
@SerialName("text_key")
class TextKeyData(
override val type: KeyType = KeyType.CHARACTER,
override val type: KeyType? = null,
override val code: Int = KeyCode.UNSPECIFIED,
override val label: String = "",
override val groupId: Int = KeyData.GROUP_DEFAULT,
override val popup: PopupSet<AbstractKeyData> = PopupSet(),
override val popup: PopupSet<out AbstractKeyData> = SimplePopups(null),
override val width: Float = 0f,
override val labelFlags: Int = 0
) : KeyData {
override fun asString(isForDisplay: Boolean): String {
@ -197,6 +382,16 @@ class TextKeyData(
return "${TextKeyData::class.simpleName} { type=$type code=$code label=\"$label\" groupId=$groupId }"
}
override fun copy(
newType: KeyType?,
newCode: Int,
newLabel: String,
newGroupId: Int,
newPopup: PopupSet<out AbstractKeyData>,
newWidth: Float,
newLabelFlags: Int
) = TextKeyData(newType, newCode, newLabel, newGroupId, newPopup, newWidth, newLabelFlags)
}
// AutoTextKeyData is just for converting case with shift, which HeliBoard always does anyway
@ -204,11 +399,12 @@ class TextKeyData(
@Serializable
@SerialName("auto_text_key")
class AutoTextKeyData(
override val type: KeyType = KeyType.CHARACTER,
override val type: KeyType? = null,
override val code: Int = KeyCode.UNSPECIFIED,
override val label: String = "",
override val groupId: Int = KeyData.GROUP_DEFAULT,
override val popup: PopupSet<AbstractKeyData> = PopupSet(),
override val popup: PopupSet<out AbstractKeyData> = SimplePopups(null),
override val width: Float = 0f,
override val labelFlags: Int = 0
) : KeyData {
@ -228,16 +424,28 @@ class AutoTextKeyData(
override fun toString(): String {
return "${AutoTextKeyData::class.simpleName} { type=$type code=$code label=\"$label\" groupId=$groupId }"
}
override fun copy(
newType: KeyType?,
newCode: Int,
newLabel: String,
newGroupId: Int,
newPopup: PopupSet<out AbstractKeyData>,
newWidth: Float,
newLabelFlags: Int
) = AutoTextKeyData(newType, newCode, newLabel, newGroupId, newPopup, newWidth, newLabelFlags)
}
@Serializable
@SerialName("multi_text_key")
class MultiTextKeyData(
override val type: KeyType = KeyType.CHARACTER,
override val type: KeyType? = null,
val codePoints: IntArray = intArrayOf(),
override val label: String = "",
override val groupId: Int = KeyData.GROUP_DEFAULT,
override val popup: PopupSet<AbstractKeyData> = PopupSet(),
override val popup: PopupSet<out AbstractKeyData> = SimplePopups(null),
override val width: Float = 0f,
override val labelFlags: Int = 0
) : KeyData {
@Transient override val code: Int = KeyCode.MULTIPLE_CODE_POINTS
@ -263,6 +471,17 @@ class MultiTextKeyData(
override fun toString(): String {
return "${MultiTextKeyData::class.simpleName} { type=$type code=$code label=\"$label\" groupId=$groupId }"
}
override fun copy(
newType: KeyType?,
newCode: Int,
newLabel: String,
newGroupId: Int,
newPopup: PopupSet<out AbstractKeyData>,
newWidth: Float,
newLabelFlags: Int
) = MultiTextKeyData(newType, codePoints, newLabel, newGroupId, newPopup, newWidth, newLabelFlags)
}
fun String.toTextKey(popupKeys: Collection<String>? = null, labelFlags: Int = 0): TextKeyData =

View file

@ -201,7 +201,7 @@ public final class Constants {
switch (code) {
case KeyCode.SHIFT: return "shift";
case KeyCode.CAPS_LOCK: return "capslock";
case KeyCode.ALPHA_SYMBOL: return "alpha_symbol";
case KeyCode.SYMBOL_ALPHA: return "symbol_alpha";
case KeyCode.ALPHA: return "alpha";
case KeyCode.SYMBOL: return "symbol";
case KeyCode.MULTIPLE_CODE_POINTS: return "text";

View file

@ -490,7 +490,7 @@ public final class InputLogic {
}
if (!inputTransaction.didAutoCorrect() && processedEvent.getMKeyCode() != KeyCode.SHIFT
&& processedEvent.getMKeyCode() != KeyCode.CAPS_LOCK
&& processedEvent.getMKeyCode() != KeyCode.ALPHA_SYMBOL
&& processedEvent.getMKeyCode() != KeyCode.SYMBOL_ALPHA
&& processedEvent.getMKeyCode() != KeyCode.ALPHA
&& processedEvent.getMKeyCode() != KeyCode.SYMBOL)
mLastComposedWord.deactivate();
@ -775,7 +775,7 @@ public final class InputLogic {
// We need to switch to the shortcut IME. This is handled by LatinIME since the
// input logic has no business with IME switching.
case KeyCode.CAPS_LOCK:
case KeyCode.ALPHA_SYMBOL:
case KeyCode.SYMBOL_ALPHA:
case KeyCode.ALPHA:
case KeyCode.SYMBOL:
case KeyCode.NUMPAD:

View file

@ -706,4 +706,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
return wrapper;
}
public boolean isTablet() {
return mContext.getResources().getInteger(R.integer.config_screen_metrics) >= 3;
}
}

View file

@ -19,7 +19,6 @@ import helium314.keyboard.keyboard.internal.KeyboardParams;
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode;
import helium314.keyboard.latin.R;
import helium314.keyboard.latin.SuggestedWords;
import helium314.keyboard.latin.common.Constants;
import helium314.keyboard.latin.utils.TypefaceUtils;
public final class MoreSuggestions extends Keyboard {
@ -87,7 +86,7 @@ public final class MoreSuggestions extends Keyboard {
mNumRows = row + 1;
mBaseWidth = mOccupiedWidth = Math.max(
minWidth, calcurateMaxRowWidth(fromIndex, index));
mBaseHeight = mOccupiedHeight = mNumRows * mDefaultRowHeight + mVerticalGap;
mBaseHeight = mOccupiedHeight = mNumRows * mDefaultAbsoluteRowHeight + mVerticalGap;
return index - fromIndex;
}
@ -138,7 +137,7 @@ public final class MoreSuggestions extends Keyboard {
public int getY(final int index) {
final int row = mRowNumbers[index];
return (mNumRows -1 - row) * mDefaultRowHeight + mTopPadding;
return (mNumRows -1 - row) * mDefaultAbsoluteRowHeight + mTopPadding;
}
public int getWidth(final int index) {
@ -185,7 +184,7 @@ public final class MoreSuggestions extends Keyboard {
mParams.mId = parentKeyboard.mId;
readAttributes(xmlId);
mParams.mVerticalGap = mParams.mTopPadding = parentKeyboard.mVerticalGap / 2;
mPaneView.updateKeyboardGeometry(mParams.mDefaultRowHeight);
mPaneView.updateKeyboardGeometry(mParams.mDefaultAbsoluteRowHeight);
final int count = mParams.layout(suggestedWords, fromIndex, maxWidth, minWidth, maxRow,
mPaneView.newLabelPaint(null /* key */), mResources);
mFromIndex = fromIndex;
@ -218,7 +217,7 @@ public final class MoreSuggestions extends Keyboard {
final int numColumnInRow = params.getNumColumnInRow(index);
if (columnNumber < numColumnInRow - 1) {
final Divider divider = new Divider(params, params.mDivider, x + width, y,
params.mDividerWidth, params.mDefaultRowHeight);
params.mDividerWidth, params.mDefaultAbsoluteRowHeight);
params.onAddKey(divider);
}
}
@ -234,7 +233,7 @@ public final class MoreSuggestions extends Keyboard {
super(word /* label */, KeyboardIconsSet.ICON_UNDEFINED, KeyCode.MULTIPLE_CODE_POINTS,
word /* outputText */, info, 0 /* labelFlags */, Key.BACKGROUND_TYPE_NORMAL,
params.getX(index), params.getY(index), params.getWidth(index),
params.mDefaultRowHeight, params.mHorizontalGap, params.mVerticalGap);
params.mDefaultAbsoluteRowHeight, params.mHorizontalGap, params.mVerticalGap);
mSuggestedWordIndex = index;
}
}

View file

@ -17,3 +17,40 @@ fun CharSequence.getStringResourceOrName(prefix: String, context: Context): Char
val resId = context.resources.getIdentifier(prefix + this, "string", context.packageName)
return if (resId == 0) this else context.getString(resId)
}
/**
* Splits the collection into a pair of lists on the first match of [condition], discarding the element first matching the condition.
* If [condition] is not met, all elements are in the first list.
*/
fun <T> Collection<T>.splitAt(condition: (T) -> Boolean): Pair<MutableList<T>, MutableList<T>> {
var conditionMet = false
val first = mutableListOf<T>()
val second = mutableListOf<T>()
forEach {
if (conditionMet) {
second.add(it)
} else {
conditionMet = condition(it)
if (!conditionMet)
first.add(it)
}
}
return first to second
}
// like plus, but for nullable collections
fun <T> addCollections(a: Collection<T>?, b: Collection<T>?): Collection<T>? {
if (a.isNullOrEmpty()) return b
if (b.isNullOrEmpty()) return a
return a + b
}
fun <T> MutableList<T>.removeFirst(predicate: (T) -> Boolean) {
val i = indexOfFirst(predicate)
if (i >= 0) removeAt(i)
}
fun <T> MutableList<T>.replaceFirst(predicate: (T) -> Boolean, with: (T) -> T) {
val i = indexOfFirst(predicate)
if (i >= 0) this[i] = with(this[i])
}

View file

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="key_def_functional">"
;action 10%
shift 10%; shift"</string>
<string name="key_def_functional_top_row" translatable="false">";delete 10%"</string>
<string name="key_def_bottom_row">"symbol_alpha, comma, space, period, emoji_com"</string>
</resources>

View file

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--
Functional key definition: use newline to separate rows, keys before semicolon are on
the left side, keys after are on the right side. Use comma to separate keys on the same side.
Valid key names are at the bottom of SimpleLayoutParser (todo: location may change)
Width (in percent of keyboard width) can be appended to the key name, defaults to default key width
Applied from bottom
-->
<string name="key_def_functional" translatable="false">"shift 15%; delete 15%"</string>
<!--
Functional key definitions for a single row, on top of the keyboard.
This is to consider that some keyboards have more than 3 rows (relevant for tablet layout)
-->
<string name="key_def_functional_top_row" translatable="false">""</string>
<!--
Bottom row definition is similar to functional key definition, but only one row, and not
split into two groups. Space bar will be adjusted in code for language and emoji keys,
and other keys in symbol layouts
-->
<string name="key_def_bottom_row" translatable="false">"symbol_alpha 15%, comma, space, period, action 15%"</string>
</resources>

View file

@ -50,6 +50,12 @@ class ParserTest {
ShadowLog.stream = System.out
}
// todo: add more tests
// (popup) keys with label and code
// (popup) keys with icon
// (popup) keys with that are essentially toolbar keys (yes, this should work at some point!)
// correct background type, depending on key type and maybe sth else
@Test fun simpleParser() {
val params = KeyboardParams()
params.mId = KeyboardLayoutSet.getFakeKeyboardId(KeyboardId.ELEMENT_ALPHABET)
@ -124,10 +130,11 @@ f""", // no newline at the end
params.mId = KeyboardLayoutSet.getFakeKeyboardId(KeyboardId.ELEMENT_ALPHABET)
params.mPopupKeyTypes.add(POPUP_KEYS_LAYOUT)
addLocaleKeyTextsToParams(latinIME, params, POPUP_KEYS_NORMAL)
data class Expected(val label: String, val text: String?, val code: Int, val popups: List<String>?)
data class Expected(val label: String?, val text: String?, val code: Int, val popups: List<String>? = null)
val expected = listOf(
Expected("a", null, 'a'.code, null),
Expected("a", null, 'a'.code, null),
Expected("a", null, 'b'.code, listOf("b")), // todo: should also check whether code is "a"
Expected("$", null, '$'.code, listOf("£", "", "¢", "¥", "")),
Expected("$", null, '¥'.code, listOf("£", "", "¢", "¥", "")),
Expected("i", null, 105, null),
@ -137,11 +144,15 @@ f""", // no newline at the end
Expected(".", null, '.'.code, listOf(">")),
Expected("'", null, '\''.code, listOf("!", "\"")),
Expected("9", null, '9'.code, null), // todo (later): also should have different background or whatever is related to type
Expected("", null, -7, null), // todo: expect an icon
Expected("?123", null, -207, null),
Expected("", null, ' '.code, null),
Expected(null, null, -7, null), // todo: expect an icon
Expected("?123", "?123", -202, null),
Expected(null, null, ' '.code, null),
Expected("(", null, '('.code, listOf("<", "[", "{")),
Expected("$", null, '$'.code, listOf("£", "", "", "¢", "¥")),
Expected("a", null, ' '.code, null),
Expected("a", null, ' '.code, null),
Expected(null, null, KeyCode.CLIPBOARD, null), // todo: expect an icon
Expected(null, null, KeyCode.MULTIPLE_CODE_POINTS, null), // todo: this works here, but crashes on phone
Expected("p", null, 'p'.code, null),
)
val layoutString = """
@ -149,6 +160,7 @@ f""", // no newline at the end
[
{ "$": "auto_text_key" "label": "a" },
{ "$": "text_key" "label": "a" },
{ "$": "text_key" "label": "a|b", "popup": { "main": { "label": "b|a" } } },
{ "label": "$$$" },
{ "label": "$$$", code: -805 },
{ "$": "case_selector",
@ -228,6 +240,10 @@ f""", // no newline at the end
{ "code": -805, "label": "currency_slot_5" }
]
} },
{ "code": 32, "label": "a|!code/key_delete" },
{ "code": 32, "label": "a|b" },
{ "label": "!icon/clipboard_action_key|!code/key_clipboard" },
{ "label": "!icon/clipboard_action_key" },
{ "label": "p" }
],
[
@ -254,11 +270,9 @@ f""", // no newline at the end
""".trimIndent()
val keys = JsonKeyboardParser(params, latinIME).parseCoreLayout(layoutString)
keys.first().forEachIndexed { index, keyData ->
println("key ${keyData.label}: code ${keyData.code}, popups: ${keyData.popup.getPopupKeyLabels(params)}")
if (keyData.type == KeyType.ENTER_EDITING || keyData.type == KeyType.SYSTEM_GUI) return@forEachIndexed // todo: currently not accepted, but should be (see below)
println("data: key ${keyData.label}: code ${keyData.code}, popups: ${keyData.popup.getPopupKeyLabels(params)}")
val keyParams = keyData.toKeyParams(params)
println("key ${keyParams.mLabel}: code ${keyParams.mCode}, popups: ${keyParams.mPopupKeys?.toList()}")
if (keyParams.outputText == "space") return@forEachIndexed // todo: only works for numeric layouts... idea: parse space anywhere, and otherwise only if special type
println("params: key ${keyParams.mLabel}: code ${keyParams.mCode}, popups: ${keyParams.mPopupKeys?.toList()}")
assertEquals(expected[index].label, keyParams.mLabel)
assertEquals(expected[index].code, keyParams.mCode)
assertEquals(expected[index].popups?.sorted(), keyParams.mPopupKeys?.mapNotNull { it.mLabel }?.sorted()) // todo (later): what's wrong with order?

View file

@ -1,36 +1,17 @@
A compilation of information about the layout formats usable in this app.
There are two distinct formats:
* the _simple_ format is a text file with one key per line, and two consecutive line breaks indicating a switch to the next row, [example](app/src/main/assets/layouts/qwerty.txt)
* the _simple_ format is a text file with one key label per line, and two consecutive line breaks indicating a switch to the next row, [example](app/src/main/assets/layouts/qwerty.txt)
* the _json_ format taken from [FlorisBoard](https://github.com/florisboard/florisboard/blob/master/CONTRIBUTING.md#adding-the-layout), but only "normal" keys are supported (i.e. no action keys and similar), [example](app/src/main/assets/layouts/azerty.json)
## General notes
Adding too many keys or too long texts will make the keyboard look awkward or broken, and even crash the app under some specific conditions.
Adding too many keys or too long texts will make the keyboard look awkward or broken, and even crash the app under some specific conditions (popup keys are especially prone for this).
There are some sanity checks when adding a layout to avoid such issues, but they do not cover all possible cases.
Further there is no check whether the layout actually contains characters of the selected language.
If you use an external glide typing library, you likely will have issues if your layout contains duplicate keys, or keys with text longer than a single letter.
There are special key labels that are intended for internal use only, but can (currently) be set on custom layouts too. An example is `!icon/previous_key|!code/key_action_previous`, so it's unlikely you will stumble upon issues here when not intentionally provoking it.
One special label that might be wanted though is `$$$`, which will be replaced by the local currency. `$$$1` - `$$$5` will be replaced by currencies available on long-pressing the currency key.
If you want different key label and use text, set the label to [label]|[text], e.g. `aa|bb` will show `aa`, but pressing the key will input `bb`.
Some special key labels will be implemented, most are already working in the (currently experimental) customization of number layouts (numpad and similar). Some keys have two names for compatibility to FlorisBoard layouts.
* _alpha_ / _view_characters_: switch to alphabet keyboard (or main phone keyboard in case of phone layout)
* _symbol_ / _view_symbols_: switch to symbol keyboard (or phone symbols keyboard in case of phone layout)
* _symbol_alpha_: toggle alpha / symbol keyboard
* _numpad_ / _view_numeric_advanced_: switch to numpad layout
* _emoji_: switch to emoji view
* _com_: display common TLDs (.com and similar)
* _emoji_com_: emoji key, but in URL and email fields it's a com key
* _language_switch_: language switch key
* _action_ / _enter_: the action (enter) key
* _delete_: delete key
* _shift_: shift key, will change label when in symbols layout
* _period_: `.` key with punctuation popups, will adapt to language-specific period
* _comma_: `,` key with special popups, will adapt to language-specific comma, or display `/` in URL fields and `@` in email fields
* _space_: space key, with icon when using a number layout
* _zwnj_: Zero-width non-joiner (automatically added next to space in alphabet layout for some languages)
If the layout has exactly 2 keys in the bottom row, these keys will replace comma and period keys. More exactly: the first key will replace the first functional key with `"groupId": 1` in the bottom row, and the second key with replace the first key with `"groupId": 2`.
## Simple format
* One key per line
@ -44,8 +25,68 @@ Some special key labels will be implemented, most are already working in the (cu
* There is no need for specifying a `code`, it will be determined from the label automatically
* You can still specify it, but it's only necessary if you want key label and code to be different (please avoid contributing layout with unnecessary codes to HeliBoard)
* Note that not all _special codes_ (negative numbers) from FlorisBoard are supported
* You can add the numeric value of a _labelFlag_ to a key for some specific effects, see [here](app/src/main/res/values/attrs.xml) in the section _keyLabelFlags_ for names and numeric values.
* More details on the formal will be provided. For now you can check other layouts, often you just need to copy lines and change the labels.
* Key classes: specified with `$`, usually you can omit them in HeliBoard
* `text_key`: normal key, default
* `auto_text_key`: used in FlorisBoard for a key that changes text case when shift is enabled, HeliBoard does that anyway unless disabled with a _labelFlag_
* `multi_text_key`: key with an array of code points, e.g. `{ "$": "multi_text_key", "codePoints": [2509, 2480], "label": "্র" }`
* there are also selector classes, which allow to change keys conditionally, see the [dvorak layout](https://github.com/Helium314/HeliBoard/blob/main/app/src/main/assets/layouts/dvorak.json) for an example:
* `case_selector`: keys for `lower` and `upper` (both mandatory), similar to `shift_state_selector`
* `shift_state_selector`: keys for `unshifted`, `shifted`, `shiftedManual`, `shiftedAutomatic`, `capsLock`, `manualOrLocked`, `default` (all opttional)
* `variation_selector`: keys for `datetime`, `time`, `date`, `password`, `normal`, `uri`, `email`, `default` (all opttional)
* `layout_direction_selector`: keys for `ltr` and `rtl` (both mandatory)
### Properties
* A (non-selector) key can have the following properties:
* `type`: only specific values, HeliBoard mostly uses this to determine background color and type, determined automatically by default
* `character`: normal key color
* `function`: functional key color
* `space`: space bar color
* `action`: action key color
* `unspecified`: no background color
* `placeholder`: no background color, no label, and pressing the key does nothing
* There are some more values, but they do nothing
* `code`: code point that is entered when the key is pressed, determined from the label by default, not available for `multi_text_key`
* There are special negative values available, e.g. the ones used by functional keys, see [KeyCode.kt](/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/floris/KeyCode.kt). There are several not yet supported key codes in there, you can see in the function `checkAndConvertCode` which ones are working.
* `codePoints`: when multiple code points should be entered, only available for `multi_text_key`
* `label`: text to display on the key, determined from code if empty
* There are some special values, see the [label section](#labels)
* `groupId`: which additional popup keys to show, `0` is default and does not add anything, `1` adds the comma popup keys, and `2` adds the period popup keys
* `popup`: list of keys to add in the popup, e.g. `"label": ")", "popup": {"relevant": [{ "label": "." }]}` is a `)` key with a `.` popup
* Note that in popup keys, properties are ignored with the exception of `$`, `code`, `codePoints`, and `label`
* When specifying a _selector_ key class in a popup key, it will be evaluated correctly (e.g. for changing popups dependent on shift state)
* `width`: width of the key in units of screen width, e.g. a key with `"width": 0.1` has a width of 10% of the screen, defaults to `0`
* A special value is `-1`, which means the key expands to the available space not already used by other keys (e.g. the space bar)
* `0` is interpreted as follows
* `-1` on the `space` key in alphabet or symbols layouts
* `0.17` for keys with `"type": numeric` in number layouts
* Otherwise the default width is used, which is `0.1` for phones and `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) in the section _keyLabelFlags_ for names and numeric values
## Labels
In the simple format you only specify labels, in json layouts you do it explicitly via the `label` property.
Usually the label is what is displayed on the key. However, there are some special labels:
* Currency keys
* `$$$` will be replaced by the local currency, depending on your current layout language. If you define a key with `$$$` without defining popup keys, it will get the first 4 additional currencies (see below) as popup
* `$$$1` - `$$$5` will be replaced by currencies available on long-pressing the currency key
* Functional keys (incomplete list)
* _alpha_: switch to alphabet keyboard (or main phone keyboard in case of phone layout)
* _symbol_: switch to symbol keyboard (or phone symbols keyboard in case of phone layout)
* _symbol_alpha_: toggle alpha / symbol keyboard
* _numpad_: switch to numpad layout
* _emoji_: switch to emoji view
* _com_: display common TLDs (.com and similar, currently not localized)
* _language_switch_: language switch key
* _action_: the action (enter) key
* _delete_: delete key
* _shift_: shift key, will change label when in symbols layout
* _period_: `.` key with punctuation popups, will adapt to language-specific period
* _comma_: `,` key with special popups, will adapt to language-specific comma, or display `/` in URL fields and `@` in email fields
* _space_: space key, with icon when using a number layout
* _zwnj_: Zero-width non-joiner (automatically added next to space in alphabet layout for some languages)
* If you want different key label and use text, set the label to [label]|[text], e.g. `aa|bb` will show `aa`, but pressing the key will input `bb`.
You can also specify special key codes like `a|!code/key_action_previous`, 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 together with a code `!icon/previous_key|!code/key_action_previous`, but this is not fully supported yet.
## 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)
@ -66,3 +107,15 @@ Some special key labels will be implemented, most are already working in the (cu
* If you add a new language for which Android does not have a display name, it will be displayed using the language tag
* Avoiding this currently is more complicated than necessary: add the language tag to [LocaleUtils.getLocaleDisplayNameInSystemLocale](/app/src/main/java/helium314/keyboard/latin/common/LocaleUtils.kt#L181) to have an exception, and add a string named `subtype_<langage tag, but with _ instead of ->` to [`strings.xml`](/app/src/main/res/values/strings.xml). Further you may need to add a `subtype_in_root_locale_<language tag>` to [donottranslate.xml](/app/src/main/res/values/donottranslate.xml), and add the language tag to `subtype_locale_exception_keys` and `subtype_locale_displayed_in_root_locale`.
* If a newly added language does not use latin script, please update the default scripts method `Locale.script` in [ScriptUtils](app/src/main/java/helium314/keyboard/latin/utils/ScriptUtils.kt)
## Functional key layouts
This is not yet customizable, but will be soon!
Mostly customizing functional keys works like other layouts, with some specific adjustments:
* you can either have a single layout for functional keys (default), or separate layouts for symbols and shift symbols
* when using a single layout
* emoji and language switch keys will only show in alphabet layout and when the option is enabled
* numpad key will only show in symbols layout
* otherwise the layout will be shown as it is in the layout file
* use keys with `"type": "placeholder"` for
* separating left and right functional keys (e.g. shift and delete in default layout)
* separating top and bottom rows in case you want to have functional key rows aligned to the top of the keyboard