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.SETTINGS, R.string.spoken_description_settings)
put(KeyCode.SHIFT, R.string.spoken_description_shift) put(KeyCode.SHIFT, R.string.spoken_description_shift)
put(KeyCode.VOICE_INPUT, R.string.spoken_description_mic) 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(Constants.CODE_TAB, R.string.spoken_description_tab)
put(KeyCode.LANGUAGE_SWITCH, R.string.spoken_description_language_switch) put(KeyCode.LANGUAGE_SWITCH, R.string.spoken_description_language_switch)
put(KeyCode.ACTION_NEXT, R.string.spoken_description_action_next) 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? { fun getDescriptionForKey(context: Context, keyboard: Keyboard?, key: Key, shouldObscure: Boolean): String? {
val code = key.code 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) val description = getDescriptionForSwitchAlphaSymbol(context, keyboard)
if (description != null) { if (description != null) {
return description 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); final float horizontalGapFloat = isSpacer() ? 0 : (keyParams.mKeyboardParams.mRelativeHorizontalGap * keyParams.mKeyboardParams.mOccupiedWidth);
mHorizontalGap = Math.round(horizontalGapFloat); mHorizontalGap = Math.round(horizontalGapFloat);
mVerticalGap = Math.round(keyParams.mKeyboardParams.mRelativeVerticalGap * keyParams.mKeyboardParams.mOccupiedHeight); 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 // 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)) { if (!isSpacer() && (mWidth == 0 || mHeight == 0)) {
throw new IllegalStateException("key needs positive width and height"); throw new IllegalStateException("key needs positive width and height");
} }
// Horizontal gap is divided equally to both sides of the key. // Horizontal gap is divided equally to both sides of the key.
mX = Math.round(keyParams.xPos + horizontalGapFloat / 2); mX = Math.round(keyParams.xPos + horizontalGapFloat / 2);
mY = Math.round(keyParams.yPos); mY = Math.round(keyParams.yPos);
mHitBox.set(Math.round(keyParams.xPos), Math.round(keyParams.yPos), Math.round(keyParams.xPos + keyParams.mFullWidth) + 1, mHitBox.set(Math.round(keyParams.xPos), Math.round(keyParams.yPos), Math.round(keyParams.xPos + keyParams.mAbsoluteWidth) + 1,
Math.round(keyParams.yPos + keyParams.mFullHeight)); Math.round(keyParams.yPos + keyParams.mAbsoluteHeight));
mHashCode = computeHashCode(this); mHashCode = computeHashCode(this);
} }
@ -504,7 +504,7 @@ public class Key implements Comparable<Key> {
return this instanceof Spacer; return this instanceof Spacer;
} }
public final boolean isActionKey() { public final boolean hasActionKeyBackground() {
return mBackgroundType == BACKGROUND_TYPE_ACTION; return mBackgroundType == BACKGROUND_TYPE_ACTION;
} }
@ -513,7 +513,7 @@ public class Key implements Comparable<Key> {
} }
public final boolean isModifier() { 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() { public final boolean isRepeatable() {
@ -906,7 +906,7 @@ public class Key implements Comparable<Key> {
final Drawable background; final Drawable background;
if (isAccentColored()) { if (isAccentColored()) {
background = actionKeyBackground; background = actionKeyBackground;
} else if (isFunctional()) { } else if (hasFunctionalBackground()) {
background = functionalKeyBackground; background = functionalKeyBackground;
} else if (mBackgroundType == BACKGROUND_TYPE_SPACEBAR) { } else if (mBackgroundType == BACKGROUND_TYPE_SPACEBAR) {
background = spacebarBackground; background = spacebarBackground;
@ -919,7 +919,7 @@ public class Key implements Comparable<Key> {
} }
public final boolean isAccentColored() { public final boolean isAccentColored() {
if (isActionKey()) return true; if (hasActionKeyBackground()) return true;
final String iconName = KeyboardIconsSet.getIconName(getIconId()); final String iconName = KeyboardIconsSet.getIconName(getIconId());
return iconName.equals(KeyboardIconsSet.NAME_NEXT_KEY) return iconName.equals(KeyboardIconsSet.NAME_NEXT_KEY)
|| iconName.equals(KeyboardIconsSet.NAME_PREVIOUS_KEY) || iconName.equals(KeyboardIconsSet.NAME_PREVIOUS_KEY)
@ -927,7 +927,7 @@ public class Key implements Comparable<Key> {
|| iconName.equals(KeyboardIconsSet.NAME_EMOJI_ACTION_KEY); || iconName.equals(KeyboardIconsSet.NAME_EMOJI_ACTION_KEY);
} }
public boolean isFunctional() { public boolean hasFunctionalBackground() {
return mBackgroundType == BACKGROUND_TYPE_FUNCTIONAL return mBackgroundType == BACKGROUND_TYPE_FUNCTIONAL
|| mBackgroundType == BACKGROUND_TYPE_STICKY_OFF || mBackgroundType == BACKGROUND_TYPE_STICKY_OFF
|| mBackgroundType == BACKGROUND_TYPE_STICKY_ON; || mBackgroundType == BACKGROUND_TYPE_STICKY_ON;
@ -954,12 +954,12 @@ public class Key implements Comparable<Key> {
// params for building // params for building
public boolean isSpacer; public boolean isSpacer;
private final KeyboardParams mKeyboardParams; // for reading gaps and keyboard width / height private final KeyboardParams mKeyboardParams; // for reading gaps and keyboard width / height
public float mRelativeWidth; public float mWidth;
public float mRelativeHeight; // also should allow negative values, indicating absolute height is defined public float mHeight; // also should allow negative values, indicating absolute height is defined
// params that may change // params that may change
public float mFullWidth; public float mAbsoluteWidth;
public float mFullHeight; public float mAbsoluteHeight;
public float xPos; public float xPos;
public float yPos; public float yPos;
@ -977,10 +977,10 @@ public class Key implements Comparable<Key> {
@Nullable public final OptionalAttributes mOptionalAttributes; @Nullable public final OptionalAttributes mOptionalAttributes;
public final boolean mEnabled; 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); final KeyParams spacer = new KeyParams(params);
spacer.mRelativeWidth = relativeWidth; spacer.mWidth = width;
spacer.mRelativeHeight = params.mDefaultRelativeRowHeight; spacer.mHeight = params.mDefaultRowHeight;
return spacer; return spacer;
} }
@ -989,18 +989,18 @@ public class Key implements Comparable<Key> {
return new Key(this); return new Key(this);
} }
public void setDimensionsFromRelativeSize(final float newX, final float newY) { public void setAbsoluteDimensions(final float newX, final float newY) {
if (mRelativeHeight == 0) if (mHeight == 0)
mRelativeHeight = mKeyboardParams.mDefaultRelativeRowHeight; mHeight = mKeyboardParams.mDefaultRowHeight;
if (!isSpacer && mRelativeWidth == 0) if (!isSpacer && mWidth == 0)
mRelativeWidth = mKeyboardParams.mDefaultRelativeKeyWidth; throw new IllegalStateException("width = 0 should have been evaluated already");
if (mRelativeHeight < 0) if (mHeight < 0)
// todo (later): deal with it properly when it needs to be adjusted, i.e. when changing popupKeys or moreSuggestions // 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"); throw new IllegalStateException("can't (yet) deal with absolute height");
xPos = newX; xPos = newX;
yPos = newY; yPos = newY;
mFullWidth = mRelativeWidth * mKeyboardParams.mBaseWidth; mAbsoluteWidth = mWidth * mKeyboardParams.mBaseWidth;
mFullHeight = mRelativeHeight * mKeyboardParams.mBaseHeight; mAbsoluteHeight = mHeight * mKeyboardParams.mBaseHeight;
} }
private static int getPopupKeysColumnAndFlagsAndSetNullInArray(final KeyboardParams params, final String[] popupKeys) { 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) @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, final int code,
@NonNull final KeyboardParams params, @NonNull final KeyboardParams params,
final float relativeWidth, final float width,
final int labelFlags, final int labelFlags,
final int backgroundType, final int backgroundType,
@Nullable final PopupSet<?> popupSet @Nullable final PopupSet<?> popupSet
@ -1060,8 +1060,8 @@ public class Key implements Comparable<Key> {
mKeyboardParams = params; mKeyboardParams = params;
mBackgroundType = backgroundType; mBackgroundType = backgroundType;
mLabelFlags = labelFlags; mLabelFlags = labelFlags;
mRelativeWidth = relativeWidth; mWidth = width;
mRelativeHeight = params.mDefaultRelativeRowHeight; mHeight = params.mDefaultRowHeight;
mIconId = KeySpecParser.getIconId(keySpec); mIconId = KeySpecParser.getIconId(keySpec);
final boolean needsToUpcase = needsToUpcase(mLabelFlags, params.mId.mElementId); 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 // 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.LANGUAGE_SWITCH
|| (mCode == KeyCode.ALPHA_SYMBOL && !params.mId.isAlphabetKeyboard()) || (mCode == KeyCode.SYMBOL_ALPHA && !params.mId.isAlphabetKeyboard())
) )
actionFlags |= ACTION_FLAGS_ENABLE_LONG_PRESS; actionFlags |= ACTION_FLAGS_ENABLE_LONG_PRESS;
if (mCode <= Constants.CODE_SPACE && mCode != KeyCode.MULTIPLE_CODE_POINTS) 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) { public KeyParams(final KeyParams keyParams) {
xPos = keyParams.xPos; xPos = keyParams.xPos;
yPos = keyParams.yPos; yPos = keyParams.yPos;
mRelativeWidth = keyParams.mRelativeWidth; mWidth = keyParams.mWidth;
mRelativeHeight = keyParams.mRelativeHeight; mHeight = keyParams.mHeight;
isSpacer = keyParams.isSpacer; isSpacer = keyParams.isSpacer;
mKeyboardParams = keyParams.mKeyboardParams; mKeyboardParams = keyParams.mKeyboardParams;
mEnabled = keyParams.mEnabled; mEnabled = keyParams.mEnabled;
@ -1248,8 +1248,8 @@ public class Key implements Comparable<Key> {
mHintLabel = keyParams.mHintLabel; mHintLabel = keyParams.mHintLabel;
mLabelFlags = keyParams.mLabelFlags; mLabelFlags = keyParams.mLabelFlags;
mIconId = keyParams.mIconId; mIconId = keyParams.mIconId;
mFullWidth = keyParams.mFullWidth; mAbsoluteWidth = keyParams.mAbsoluteWidth;
mFullHeight = keyParams.mFullHeight; mAbsoluteHeight = keyParams.mAbsoluteHeight;
mPopupKeys = keyParams.mPopupKeys; mPopupKeys = keyParams.mPopupKeys;
mPopupKeysColumnAndFlags = keyParams.mPopupKeysColumnAndFlags; mPopupKeysColumnAndFlags = keyParams.mPopupKeysColumnAndFlags;
mBackgroundType = keyParams.mBackgroundType; mBackgroundType = keyParams.mBackgroundType;

View file

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

View file

@ -472,7 +472,7 @@ public class KeyboardView extends View {
blendAlpha(paint, params.mAnimAlpha); blendAlpha(paint, params.mAnimAlpha);
final float labelCharHeight = TypefaceUtils.getReferenceCharHeight(paint); final float labelCharHeight = TypefaceUtils.getReferenceCharHeight(paint);
final float labelCharWidth = TypefaceUtils.getReferenceCharWidth(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; final float hintX, hintBaseline;
if (key.hasHintLabel()) { if (key.hasHintLabel()) {
// The hint label is placed just right of the key label. Used mainly on // 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) if (key.getBackgroundType() == Key.BACKGROUND_TYPE_SPACEBAR)
hintX = keyWidth + hintBaseline + labelCharWidth * 0.1f; hintX = keyWidth + hintBaseline + labelCharWidth * 0.1f;
else 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 { } else {
hintX = keyWidth - mKeyHintLetterPadding - TypefaceUtils.getReferenceCharWidth(paint) / 2.0f; 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); mPopupKeysKeyboardCache.put(key, popupKeysKeyboard);
} }
final View container = key.isActionKey() ? mPopupKeysKeyboardForActionContainer final View container = key.hasActionKeyBackground() ? mPopupKeysKeyboardForActionContainer
: mPopupKeysKeyboardContainer; : mPopupKeysKeyboardContainer;
final PopupKeysKeyboardView popupKeysKeyboardView = final PopupKeysKeyboardView popupKeysKeyboardView =
container.findViewById(R.id.popup_keys_keyboard_view); container.findViewById(R.id.popup_keys_keyboard_view);

View file

@ -1107,7 +1107,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element,
return; 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); sListener.onCodeInput(KeyCode.NUMPAD, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false);
return; return;
} }

View file

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

View file

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

View file

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

View file

@ -422,7 +422,7 @@ public final class KeyboardState {
onPressShift(); onPressShift();
} else if (code == KeyCode.CAPS_LOCK) { } else if (code == KeyCode.CAPS_LOCK) {
// Nothing to do here. See {@link #onReleaseKey(int,boolean)}. // Nothing to do here. See {@link #onReleaseKey(int,boolean)}.
} else if (code == KeyCode.ALPHA_SYMBOL) { } else if (code == KeyCode.SYMBOL_ALPHA) {
onPressAlphaSymbol(autoCapsFlags, recapitalizeMode); onPressAlphaSymbol(autoCapsFlags, recapitalizeMode);
} else if (code == KeyCode.SYMBOL) { } else if (code == KeyCode.SYMBOL) {
// don't start sliding, causes issues with fully customizable layouts // don't start sliding, causes issues with fully customizable layouts
@ -463,7 +463,7 @@ public final class KeyboardState {
onReleaseShift(withSliding, autoCapsFlags, recapitalizeMode); onReleaseShift(withSliding, autoCapsFlags, recapitalizeMode);
} else if (code == KeyCode.CAPS_LOCK) { } else if (code == KeyCode.CAPS_LOCK) {
setShiftLocked(!mAlphabetShiftState.isShiftLocked()); setShiftLocked(!mAlphabetShiftState.isShiftLocked());
} else if (code == KeyCode.ALPHA_SYMBOL) { } else if (code == KeyCode.SYMBOL_ALPHA) {
onReleaseAlphaSymbol(withSliding, autoCapsFlags, recapitalizeMode); onReleaseAlphaSymbol(withSliding, autoCapsFlags, recapitalizeMode);
} else if (code == KeyCode.SYMBOL) { } else if (code == KeyCode.SYMBOL) {
onReleaseSymbol(withSliding, autoCapsFlags, recapitalizeMode); onReleaseSymbol(withSliding, autoCapsFlags, recapitalizeMode);
@ -700,7 +700,7 @@ public final class KeyboardState {
switch (mSwitchState) { switch (mSwitchState) {
case SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL: 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. // Detected only the mode change key has been pressed, and then released.
if (mMode == MODE_ALPHABET) { if (mMode == MODE_ALPHABET) {
mSwitchState = SWITCH_STATE_ALPHA; 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, public Key buildKey(final int x, final int y, final int labelFlags,
@NonNull final KeyboardParams params) { @NonNull final KeyboardParams params) {
return new Key(mLabel, mIconId, mCode, mOutputText, null /* hintLabel */, labelFlags, 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); 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.KeyboardParams
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode
import helium314.keyboard.latin.R import helium314.keyboard.latin.R
import helium314.keyboard.latin.common.Constants
import helium314.keyboard.latin.common.StringUtils import helium314.keyboard.latin.common.StringUtils
import helium314.keyboard.latin.settings.Settings import helium314.keyboard.latin.settings.Settings
import helium314.keyboard.latin.utils.ResourceUtils 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) // 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 // 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 keyWidth = defaultKeyWidth * sqrt(Settings.getInstance().current.mKeyboardHeightScale)
val defaultKeyboardHeight = ResourceUtils.getDefaultKeyboardHeight(context.resources, false) val defaultKeyboardHeight = ResourceUtils.getDefaultKeyboardHeight(context.resources, false)
val defaultBottomPadding = context.resources.getFraction(R.fraction.config_keyboard_bottom_padding_holo, defaultKeyboardHeight, defaultKeyboardHeight) 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 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 -> emojiArray.forEachIndexed { i, codeArraySpec ->
val keyParams = parseEmojiKey(codeArraySpec, popupEmojisArray?.get(i)?.takeIf { it.isNotEmpty() }) ?: return@forEachIndexed val keyParams = parseEmojiKey(codeArraySpec, popupEmojisArray?.get(i)?.takeIf { it.isNotEmpty() }) ?: return@forEachIndexed
keyParams.xPos = currentX keyParams.xPos = currentX
keyParams.yPos = currentY keyParams.yPos = currentY
keyParams.mFullWidth = keyWidth keyParams.mAbsoluteWidth = keyWidth
keyParams.mFullHeight = keyHeight keyParams.mAbsoluteHeight = keyHeight
currentX += keyParams.mFullWidth currentX += keyParams.mAbsoluteWidth
row.add(keyParams) row.add(keyParams)
} }
return arrayListOf(row) return arrayListOf(row)

View file

@ -9,18 +9,17 @@ import androidx.annotation.StringRes
import helium314.keyboard.keyboard.Key import helium314.keyboard.keyboard.Key
import helium314.keyboard.keyboard.Key.KeyParams import helium314.keyboard.keyboard.Key.KeyParams
import helium314.keyboard.keyboard.KeyboardId import helium314.keyboard.keyboard.KeyboardId
import helium314.keyboard.keyboard.KeyboardTheme
import helium314.keyboard.keyboard.internal.KeyboardIconsSet import helium314.keyboard.keyboard.internal.KeyboardIconsSet
import helium314.keyboard.keyboard.internal.KeyboardParams 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.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.KeyType
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.SimplePopups 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.R
import helium314.keyboard.latin.common.Constants import helium314.keyboard.latin.common.Constants
import helium314.keyboard.latin.common.LocaleUtils.constructLocale import helium314.keyboard.latin.common.LocaleUtils.constructLocale
import helium314.keyboard.latin.common.isEmoji import helium314.keyboard.latin.common.isEmoji
import helium314.keyboard.latin.common.splitOnWhitespace
import helium314.keyboard.latin.define.DebugFlags import helium314.keyboard.latin.define.DebugFlags
import helium314.keyboard.latin.settings.Settings import helium314.keyboard.latin.settings.Settings
import helium314.keyboard.latin.spellcheck.AndroidSpellCheckerService 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
import helium314.keyboard.latin.utils.ScriptUtils.script import helium314.keyboard.latin.utils.ScriptUtils.script
import helium314.keyboard.latin.utils.getLayoutFile 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.runInLocale
import helium314.keyboard.latin.utils.splitAt
import helium314.keyboard.latin.utils.sumOf import helium314.keyboard.latin.utils.sumOf
import java.io.File import java.io.File
@ -65,101 +67,258 @@ abstract class KeyboardParser(private val params: KeyboardParams, private val co
params.mTouchPositionCorrection.load(context.resources.getStringArray(infos.touchPositionCorrectionData)) params.mTouchPositionCorrection.load(context.resources.getStringArray(infos.touchPositionCorrectionData))
val baseKeys: MutableList<List<KeyData>> = parseCoreLayout(layoutContent) val baseKeys: MutableList<List<KeyData>> = parseCoreLayout(layoutContent)
val keysInRows: ArrayList<ArrayList<KeyParams>> val keysInRows = if (params.mId.isAlphaOrSymbolKeyboard) {
if (params.mId.mElementId <= KeyboardId.ELEMENT_SYMBOLS_SHIFTED) { createAlphaSymbolRows(baseKeys)
keysInRows = createAlphaSymbolRows(baseKeys)
} else if (params.mId.isNumberLayout) { } else if (params.mId.isNumberLayout) {
keysInRows = createNumericRows(baseKeys) createNumericRows(baseKeys)
} else { } else {
throw(UnsupportedOperationException("creating KeyboardId ${params.mId.mElementId} not supported")) throw(UnsupportedOperationException("creating KeyboardId ${params.mId.mElementId} not supported"))
} }
// rescale height if we have more than 4 rows // rescale height if we have more than 4 rows
val heightRescale = if (keysInRows.size > 4) 4f / keysInRows.size else 1f val heightRescale = if (keysInRows.size > 4) 4f / keysInRows.size else 1f
if (heightRescale != 1f) { if (heightRescale != 1f) {
keysInRows.forEach { row -> row.forEach { it.mRelativeHeight *= heightRescale } } keysInRows.forEach { row -> row.forEach { it.mHeight *= heightRescale } }
} }
return keysInRows 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>> { private fun createAlphaSymbolRows(baseKeys: MutableList<List<KeyData>>): ArrayList<ArrayList<KeyParams>> {
addNumberRowOrPopupKeys(baseKeys) addNumberRowOrPopupKeys(baseKeys)
if (params.mId.isAlphabetKeyboard) if (params.mId.isAlphabetKeyboard)
addSymbolPopupKeys(baseKeys) 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 allFunctionalKeys = JsonKeyboardParser(params, context).parseCoreLayout(getFunctionalKeyLayoutText())
val functionalKeys = parseFunctionalKeys(R.string.key_def_functional) adjustBottomFunctionalRowAndBaseKeys(allFunctionalKeys, baseKeys)
val functionalKeysTop = parseFunctionalKeys(R.string.key_def_functional_top_row)
// todo: this loop could use some performance improvements if (allFunctionalKeys.none { it.singleOrNull()?.isKeyPlaceholder() == true })
baseKeys.forEachIndexed { i, it -> // add a placeholder so splitAt does what we really want
val row: List<KeyData> = if (i == baseKeys.lastIndex && isTablet()) { 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 // 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) val tabletExtraKeys = params.mLocaleKeyboardInfos.getTabletExtraKeys(params.mId.mElementId)
tabletExtraKeys.first + it + tabletExtraKeys.second tabletExtraKeys.first + it + tabletExtraKeys.second
} else { } else {
it 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 // build list of functional keys of same size as baseKeys
val usedKeyWidth = params.mDefaultRelativeKeyWidth * row.size val functionalKeysFromTop = functionalKeysTop.getOrNull(i) ?: emptyList()
val functionalKeyWidth = (functionalKeysLeft.sumOf { it.mRelativeWidth }) + (functionalKeysRight.sumOf { it.mRelativeWidth }) val functionalKeysFromBottom = functionalKeysBottom.getOrNull(i - bottomIndexOffset) ?: emptyList()
val availableWidth = 1f - functionalKeyWidth functionalKeys.add(getFunctionalKeysBySide(functionalKeysFromTop, functionalKeysFromBottom))
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 }
}
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)) val extraFlags = if (key.label.length > 2 && key.label.codePointCount(0, key.label.length) > 2 && !isEmoji(key.label))
Key.LABEL_FLAGS_AUTO_X_SCALE Key.LABEL_FLAGS_AUTO_X_SCALE
else 0 else 0
val keyData = key.compute(params)
if (DebugFlags.DEBUG_ENABLED) if (DebugFlags.DEBUG_ENABLED)
Log.d(TAG, "adding key ${keyData.label}, ${keyData.code}") Log.d(TAG, "adding key ${key.label}, ${key.code}")
val keyParams = keyData.toKeyParams(params, keyWidth, defaultLabelFlags or extraFlags) key.toKeyParams(params, defaultLabelFlags or extraFlags)
paramsRow.add(keyParams)
} }
if (spacerWidth != 0f) {
paramsRow.add(KeyParams.newSpacer(params, spacerWidth))
} }
functionalKeysRight.forEach { paramsRow.add(it) } 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) 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 }
} }
resizeLastRowIfNecessaryForAlignment(keysInRows) keysInRows.add(ArrayList(allKeys))
keysInRows.add(bottomRow) }
if (params.mId.mNumberRowEnabled) }
keysInRows.add(0, getNumberRow())
// 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 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>>) { private fun addNumberRowOrPopupKeys(baseKeys: MutableList<List<KeyData>>) {
@ -194,7 +353,7 @@ abstract class KeyboardParser(private val params: KeyboardParams, private val co
else SimpleKeyboardParser(params, context, false) else SimpleKeyboardParser(params, context, false)
parser.parseCoreLayout(getLayoutFile(layoutName, context).readText()) parser.parseCoreLayout(getLayoutFile(layoutName, context).readText())
} else { } else {
SimpleKeyboardParser(params, context, false).parseCoreLayout(context.readAssetsFile("layouts/$layoutName.txt")) SimpleKeyboardParser(params, context, false).parseCoreLayout(context.readAssetsLayoutFile("$layoutName.txt"))
} }
layout.forEachIndexed { i, row -> layout.forEachIndexed { i, row ->
val baseRow = baseKeys.getOrNull(i) ?: return@forEachIndexed 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>> { private fun createNumericRows(baseKeys: MutableList<List<KeyData>>): ArrayList<ArrayList<KeyParams>> {
val keysInRows = ArrayList<ArrayList<KeyParams>>() val keysInRows = ArrayList<ArrayList<KeyParams>>()
if (context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE && params.mId.mElementId != KeyboardId.ELEMENT_NUMPAD) { 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 -> row.forEach { key ->
var keyParams: KeyParams? = null var keyParams: KeyParams? = null
// try parsing a functional key // try parsing a functional key
// todo: note that this is ignoring code on those keys, if any
val functionalKeyName = when (key.label) { val functionalKeyName = when (key.label) {
// todo (later): maybe add special popupKeys for phone and number layouts? // 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) KeyLabel.PERIOD else "."
"," -> if (params.mId.mElementId == KeyboardId.ELEMENT_NUMPAD) "comma" else "," "," -> if (params.mId.mElementId == KeyboardId.ELEMENT_NUMPAD) KeyLabel.COMMA else ","
else -> key.label 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 { try {
keyParams = getFunctionalKeyParams(functionalKeyName) keyParams = key.copy(newLabel = functionalKeyName).processFunctionalKeys()!!.toKeyParams(params)
} catch (_: Throwable) {} // just use normal label } catch (_: Throwable) {} // just use normal label
} }
if (keyParams == null) { if (keyParams == null) {
@ -264,11 +394,11 @@ abstract class KeyboardParser(private val params: KeyboardParams, private val co
KeyboardId.ELEMENT_PHONE_SYMBOLS -> 0 KeyboardId.ELEMENT_PHONE_SYMBOLS -> 0
else -> Key.LABEL_FLAGS_FOLLOW_KEY_LARGE_LETTER_RATIO 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)) } 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 else
key.compute(params).toKeyParams(params, additionalLabelFlags = defaultLabelFlags) key.toKeyParams(params, additionalLabelFlags = defaultLabelFlags)
} }
if (key.type != KeyType.NUMERIC && keyParams.mBackgroundType != Key.BACKGROUND_TYPE_ACTION) if (key.type != KeyType.NUMERIC && keyParams.mBackgroundType != Key.BACKGROUND_TYPE_ACTION)
keyParams.mBackgroundType = Key.BACKGROUND_TYPE_FUNCTIONAL 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 // 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 // but determine from row size instead of from elementId, in case user wants to adjust numpad layout
if (row.size == baseKeys[0].size) { if (row.size == baseKeys[0].size) {
paramsRow.getOrNull(n - 1)?.mRelativeWidth = paramsRow[n].mRelativeWidth paramsRow.getOrNull(n - 1)?.mWidth = paramsRow[n].mWidth
paramsRow.getOrNull(n + 1)?.mRelativeWidth = paramsRow[n].mRelativeWidth paramsRow.getOrNull(n + 1)?.mWidth = paramsRow[n].mWidth
} else if (row.size == baseKeys[0].size + 2) { } else if (row.size == baseKeys[0].size + 2) {
// numpad last row -> make sure the keys next to 0 fit nicely // 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 - 1)?.mWidth = paramsRow[n].mWidth * 0.55f
paramsRow.getOrNull(n - 2)?.mRelativeWidth = paramsRow[n].mRelativeWidth * 0.45f paramsRow.getOrNull(n - 2)?.mWidth = paramsRow[n].mWidth * 0.45f
paramsRow.getOrNull(n + 1)?.mRelativeWidth = paramsRow[n].mRelativeWidth * 0.55f paramsRow.getOrNull(n + 1)?.mWidth = paramsRow[n].mWidth * 0.55f
paramsRow.getOrNull(n + 2)?.mRelativeWidth = paramsRow[n].mRelativeWidth * 0.45f paramsRow.getOrNull(n + 2)?.mWidth = paramsRow[n].mWidth * 0.45f
} }
} }
} }
val widthSum = paramsRow.sumOf { it.mRelativeWidth } val widthSum = paramsRow.sumOf { it.mWidth }
paramsRow.forEach { it.mRelativeWidth /= widthSum } paramsRow.forEach { it.mWidth /= widthSum }
keysInRows.add(paramsRow) keysInRows.add(paramsRow)
} }
return keysInRows 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 { 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)) 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" 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) // remove emoji shortcut on enter in tablet mode (like original, because bottom row always has an emoji key)
// (probably not necessary, but whatever) // (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) } val i = popupKeys.indexOfFirst { it.startsWith(Key.POPUP_KEYS_FIXED_COLUMN_ORDER) }
if (i > -1) { if (i > -1) {
val n = popupKeys[i].substringAfter(Key.POPUP_KEYS_FIXED_COLUMN_ORDER).toIntOrNull() 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) } 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 { companion object {
private const val TAG = "KeyboardParser" 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")!! val layoutFileNames = context.assets.list("layouts")!!
if (layoutFileNames.contains("$layoutName.json")) { 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")) { 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}") 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 { private fun getLayoutFileName(params: KeyboardParams, context: Context, overrideElementId: Int? = null): String {
var checkForCustom = true 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 // 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_MIN = -9999
const val INTERNAL_FLORIS_MAX = -1 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 INTERNAL_HELI = -19999..-10000 // for keys exclusive to this app
val CURRENCY = CURRENCY_SLOT_6..CURRENCY_SLOT_1 val CURRENCY = CURRENCY_SLOT_6..CURRENCY_SLOT_1
} }
@ -122,7 +122,7 @@ object KeyCode {
const val CJK_SPACE = 12288 const val CJK_SPACE = 12288
// heliboard only codes // heliboard only codes
const val ALPHA_SYMBOL = -10001 const val SYMBOL_ALPHA = -10001
const val START_ONE_HANDED_MODE = -10002 const val START_ONE_HANDED_MODE = -10002
const val STOP_ONE_HANDED_MODE = -10003 const val STOP_ONE_HANDED_MODE = -10003
const val SWITCH_ONE_HANDED_MODE = -10004 const val SWITCH_ONE_HANDED_MODE = -10004
@ -145,8 +145,8 @@ object KeyCode {
SHIFT, CAPS_LOCK, MULTIPLE_CODE_POINTS, UNSPECIFIED, SHIFT, CAPS_LOCK, MULTIPLE_CODE_POINTS, UNSPECIFIED,
// heliboard only // heliboard only
ALPHA_SYMBOL, START_ONE_HANDED_MODE, STOP_ONE_HANDED_MODE, SWITCH_ONE_HANDED_MODE, SHIFT_ENTER, SYMBOL_ALPHA, START_ONE_HANDED_MODE, STOP_ONE_HANDED_MODE, SWITCH_ONE_HANDED_MODE, SHIFT_ENTER,
ACTION_NEXT, ACTION_PREVIOUS, NOT_SPECIFIED ACTION_NEXT, ACTION_PREVIOUS, NOT_SPECIFIED, CLIPBOARD_COPY_ALL, PAGE_UP, PAGE_DOWN
-> this -> this
// conversion // conversion
@ -156,26 +156,4 @@ object KeyCode {
else -> throw IllegalStateException("key code $this not yet supported") 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) @Serializable(with = KeyTypeSerializer::class)
enum class KeyType { enum class KeyType {
// todo: implement the effect on background
// also, how to get that specific space bar background?
CHARACTER, // default 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 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 MODIFIER, // alt, ctrl, shift, gets functional key background
NAVIGATION, // home, page up, page down, tab, arrows, geta default background 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), geta functional background SYSTEM_GUI, // esc, print, pause, meta, (keyboard layout switch), gets functional background
NUMERIC, // numpad keys, get larger letter and larger width NUMERIC, // numpad keys, get larger letter and larger width in number layouts, and default background
PLACEHOLDER, // other keys go here, e.g. in shift, placeholder, delete the placeholder gets (typically) replaced by the bottom keyboard row PLACEHOLDER, // spacer, or actual placeholder when used in functional key layouts
UNSPECIFIED; // treated like default UNSPECIFIED; // empty background
override fun toString(): String { override fun toString(): String {
return super.toString().lowercase() return super.toString().lowercase()
@ -40,7 +38,13 @@ enum class KeyType {
companion object { companion object {
fun fromString(string: String): KeyType { 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 package helium314.keyboard.keyboard.internal.keyboard_parser.floris
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import helium314.keyboard.keyboard.internal.KeySpecParser
import helium314.keyboard.keyboard.internal.KeyboardParams import helium314.keyboard.keyboard.internal.KeyboardParams
import helium314.keyboard.latin.utils.addCollections
// only the constructor and name remain from FlorisBoard // only the constructor and name remain from FlorisBoard
// we don't care about the difference between main and relevant (at least for now) // we don't care about the difference between main and relevant (at least for now)
@Serializable @Serializable
open class PopupSet<T : AbstractKeyData>( open class PopupSet<T : AbstractKeyData>(
open val main: T? = null, open val main: T? = null,
open val relevant: List<T>? = null open val relevant: Collection<T>? = null
) { ) {
// get labels of all popup keys // get labels of all popup keys
open fun getPopupKeyLabels(params: KeyboardParams): Collection<String>? { open fun getPopupKeyLabels(params: KeyboardParams): Collection<String>? {
if (main == null && relevant == null) return null if (main == null && relevant == null) return null
val popupKeys = mutableListOf<String>() val popupKeys = mutableListOf<String>()
main?.getPopupLabel(params)?.let { popupKeys.add(it) } main?.compute(params)?.getPopupLabel(params)?.let { popupKeys.add(it) }
relevant?.let { popupKeys.addAll(it.map { it.getPopupLabel(params) }) } relevant?.let { popupKeys.addAll(it.mapNotNull { it.compute(params)?.getPopupLabel(params) }) }
if (popupKeys.isEmpty()) return null if (popupKeys.isEmpty()) return null
return popupKeys return popupKeys
} }
open fun isEmpty(): Boolean = main == null && relevant.isNullOrEmpty()
var numberIndex: Int? = null var numberIndex: Int? = null
var symbol: String? = null // maybe list of keys? 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>() { class SimplePopups(val popupKeys: Collection<String>?) : PopupSet<AbstractKeyData>() {
override fun getPopupKeyLabels(params: KeyboardParams) = popupKeys 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.Serializable
import kotlinx.serialization.Transient import kotlinx.serialization.Transient
import helium314.keyboard.keyboard.Key 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.KeyboardParams
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode.checkAndConvertCode 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.keyboard.internal.keyboard_parser.rtlLabel
import helium314.keyboard.latin.common.Constants import helium314.keyboard.latin.common.Constants
import helium314.keyboard.latin.common.StringUtils import helium314.keyboard.latin.common.StringUtils
import helium314.keyboard.latin.settings.Settings
// taken from FlorisBoard, small modifications (see also KeyData) // taken from FlorisBoard, small modifications (see also KeyData)
// internal keys removed (currently no plan to support them) // 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. * @property popup The popups for ths key. Can also dynamically be provided via popup extensions.
*/ */
sealed interface KeyData : AbstractKeyData { sealed interface KeyData : AbstractKeyData {
val type: KeyType val type: KeyType?
val code: Int val code: Int
val label: String val label: String
val groupId: Int 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 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 // groups (currently) not supported
companion object { 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 * 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 * 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 * 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. * popups specified for "~kana" in the popup mapping.
*/ */
const val GROUP_KANA: Int = 97 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 // make it non-nullable for simplicity, and to reflect current implementations
override fun compute(params: KeyboardParams): KeyData { 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 newLabel = label.convertFlorisLabel()
val newCode = code.checkAndConvertCode() 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) if (newCode != code || newLabel != label)
return TextKeyData(type, newCode, newLabel, groupId, popup, labelFlags).compute(params) return copy(newCode = newCode, newLabel = newLabel)
return this return this
} }
fun isSpaceKey(): Boolean { fun isSpaceKey(): Boolean {
return type == KeyType.CHARACTER && (code == Constants.CODE_SPACE || code == KeyCode.CJK_SPACE return code == Constants.CODE_SPACE || code == KeyCode.CJK_SPACE || code == KeyCode.ZWNJ || code == KeyCode.KESHIDA
|| code == KeyCode.ZWNJ || code == KeyCode.KESHIDA)
} }
fun toKeyParams(params: KeyboardParams, width: Float = params.mDefaultRelativeKeyWidth, additionalLabelFlags: Int = 0): Key.KeyParams { fun isKeyPlaceholder() = type == KeyType.PLACEHOLDER && code == KeyCode.UNSPECIFIED && width == 0f
// 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" }
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) // 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 // but also longer labels should work without issues, also for MultiTextKeyData
if (this is MultiTextKeyData) { if (this is MultiTextKeyData) {
val outputText = String(codePoints, 0, codePoints.size) val outputText = String(codePoints, 0, codePoints.size)
Key.KeyParams( Key.KeyParams(
"$label|$outputText", "$newLabel|$outputText",
code, newCode,
params, params,
width, newWidth,
labelFlags or additionalLabelFlags, newLabelFlags,
Key.BACKGROUND_TYPE_NORMAL, // todo (when supported): determine type background,
popup, newPopupKeys,
) )
} else { } else {
Key.KeyParams( 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, params,
width, newWidth,
labelFlags or additionalLabelFlags, newLabelFlags,
Key.BACKGROUND_TYPE_NORMAL, // todo (when supported): determine type background,
popup, newPopupKeys,
) )
} }
} else { } else {
Key.KeyParams( Key.KeyParams(
label.ifEmpty { StringUtils.newSingleCodePointString(code) }, newLabel.ifEmpty { StringUtils.newSingleCodePointString(newCode) },
code, newCode,
params, params,
width, newWidth,
labelFlags or additionalLabelFlags, newLabelFlags,
Key.BACKGROUND_TYPE_NORMAL, background,
popup, 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. * 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 * @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 * @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. * 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 * @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 @Serializable
@SerialName("text_key") @SerialName("text_key")
class TextKeyData( class TextKeyData(
override val type: KeyType = KeyType.CHARACTER, override val type: KeyType? = null,
override val code: Int = KeyCode.UNSPECIFIED, override val code: Int = KeyCode.UNSPECIFIED,
override val label: String = "", override val label: String = "",
override val groupId: Int = KeyData.GROUP_DEFAULT, 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 override val labelFlags: Int = 0
) : KeyData { ) : KeyData {
override fun asString(isForDisplay: Boolean): String { override fun asString(isForDisplay: Boolean): String {
@ -197,6 +382,16 @@ class TextKeyData(
return "${TextKeyData::class.simpleName} { type=$type code=$code label=\"$label\" groupId=$groupId }" 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 // AutoTextKeyData is just for converting case with shift, which HeliBoard always does anyway
@ -204,11 +399,12 @@ class TextKeyData(
@Serializable @Serializable
@SerialName("auto_text_key") @SerialName("auto_text_key")
class AutoTextKeyData( class AutoTextKeyData(
override val type: KeyType = KeyType.CHARACTER, override val type: KeyType? = null,
override val code: Int = KeyCode.UNSPECIFIED, override val code: Int = KeyCode.UNSPECIFIED,
override val label: String = "", override val label: String = "",
override val groupId: Int = KeyData.GROUP_DEFAULT, 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 override val labelFlags: Int = 0
) : KeyData { ) : KeyData {
@ -228,16 +424,28 @@ class AutoTextKeyData(
override fun toString(): String { override fun toString(): String {
return "${AutoTextKeyData::class.simpleName} { type=$type code=$code label=\"$label\" groupId=$groupId }" 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 @Serializable
@SerialName("multi_text_key") @SerialName("multi_text_key")
class MultiTextKeyData( class MultiTextKeyData(
override val type: KeyType = KeyType.CHARACTER, override val type: KeyType? = null,
val codePoints: IntArray = intArrayOf(), val codePoints: IntArray = intArrayOf(),
override val label: String = "", override val label: String = "",
override val groupId: Int = KeyData.GROUP_DEFAULT, 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 override val labelFlags: Int = 0
) : KeyData { ) : KeyData {
@Transient override val code: Int = KeyCode.MULTIPLE_CODE_POINTS @Transient override val code: Int = KeyCode.MULTIPLE_CODE_POINTS
@ -263,6 +471,17 @@ class MultiTextKeyData(
override fun toString(): String { override fun toString(): String {
return "${MultiTextKeyData::class.simpleName} { type=$type code=$code label=\"$label\" groupId=$groupId }" 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 = fun String.toTextKey(popupKeys: Collection<String>? = null, labelFlags: Int = 0): TextKeyData =

View file

@ -201,7 +201,7 @@ public final class Constants {
switch (code) { switch (code) {
case KeyCode.SHIFT: return "shift"; case KeyCode.SHIFT: return "shift";
case KeyCode.CAPS_LOCK: return "capslock"; 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.ALPHA: return "alpha";
case KeyCode.SYMBOL: return "symbol"; case KeyCode.SYMBOL: return "symbol";
case KeyCode.MULTIPLE_CODE_POINTS: return "text"; case KeyCode.MULTIPLE_CODE_POINTS: return "text";

View file

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

View file

@ -706,4 +706,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
return wrapper; 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.keyboard.internal.keyboard_parser.floris.KeyCode;
import helium314.keyboard.latin.R; import helium314.keyboard.latin.R;
import helium314.keyboard.latin.SuggestedWords; import helium314.keyboard.latin.SuggestedWords;
import helium314.keyboard.latin.common.Constants;
import helium314.keyboard.latin.utils.TypefaceUtils; import helium314.keyboard.latin.utils.TypefaceUtils;
public final class MoreSuggestions extends Keyboard { public final class MoreSuggestions extends Keyboard {
@ -87,7 +86,7 @@ public final class MoreSuggestions extends Keyboard {
mNumRows = row + 1; mNumRows = row + 1;
mBaseWidth = mOccupiedWidth = Math.max( mBaseWidth = mOccupiedWidth = Math.max(
minWidth, calcurateMaxRowWidth(fromIndex, index)); minWidth, calcurateMaxRowWidth(fromIndex, index));
mBaseHeight = mOccupiedHeight = mNumRows * mDefaultRowHeight + mVerticalGap; mBaseHeight = mOccupiedHeight = mNumRows * mDefaultAbsoluteRowHeight + mVerticalGap;
return index - fromIndex; return index - fromIndex;
} }
@ -138,7 +137,7 @@ public final class MoreSuggestions extends Keyboard {
public int getY(final int index) { public int getY(final int index) {
final int row = mRowNumbers[index]; final int row = mRowNumbers[index];
return (mNumRows -1 - row) * mDefaultRowHeight + mTopPadding; return (mNumRows -1 - row) * mDefaultAbsoluteRowHeight + mTopPadding;
} }
public int getWidth(final int index) { public int getWidth(final int index) {
@ -185,7 +184,7 @@ public final class MoreSuggestions extends Keyboard {
mParams.mId = parentKeyboard.mId; mParams.mId = parentKeyboard.mId;
readAttributes(xmlId); readAttributes(xmlId);
mParams.mVerticalGap = mParams.mTopPadding = parentKeyboard.mVerticalGap / 2; 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, final int count = mParams.layout(suggestedWords, fromIndex, maxWidth, minWidth, maxRow,
mPaneView.newLabelPaint(null /* key */), mResources); mPaneView.newLabelPaint(null /* key */), mResources);
mFromIndex = fromIndex; mFromIndex = fromIndex;
@ -218,7 +217,7 @@ public final class MoreSuggestions extends Keyboard {
final int numColumnInRow = params.getNumColumnInRow(index); final int numColumnInRow = params.getNumColumnInRow(index);
if (columnNumber < numColumnInRow - 1) { if (columnNumber < numColumnInRow - 1) {
final Divider divider = new Divider(params, params.mDivider, x + width, y, final Divider divider = new Divider(params, params.mDivider, x + width, y,
params.mDividerWidth, params.mDefaultRowHeight); params.mDividerWidth, params.mDefaultAbsoluteRowHeight);
params.onAddKey(divider); params.onAddKey(divider);
} }
} }
@ -234,7 +233,7 @@ public final class MoreSuggestions extends Keyboard {
super(word /* label */, KeyboardIconsSet.ICON_UNDEFINED, KeyCode.MULTIPLE_CODE_POINTS, super(word /* label */, KeyboardIconsSet.ICON_UNDEFINED, KeyCode.MULTIPLE_CODE_POINTS,
word /* outputText */, info, 0 /* labelFlags */, Key.BACKGROUND_TYPE_NORMAL, word /* outputText */, info, 0 /* labelFlags */, Key.BACKGROUND_TYPE_NORMAL,
params.getX(index), params.getY(index), params.getWidth(index), params.getX(index), params.getY(index), params.getWidth(index),
params.mDefaultRowHeight, params.mHorizontalGap, params.mVerticalGap); params.mDefaultAbsoluteRowHeight, params.mHorizontalGap, params.mVerticalGap);
mSuggestedWordIndex = index; 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) val resId = context.resources.getIdentifier(prefix + this, "string", context.packageName)
return if (resId == 0) this else context.getString(resId) 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 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() { @Test fun simpleParser() {
val params = KeyboardParams() val params = KeyboardParams()
params.mId = KeyboardLayoutSet.getFakeKeyboardId(KeyboardId.ELEMENT_ALPHABET) 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.mId = KeyboardLayoutSet.getFakeKeyboardId(KeyboardId.ELEMENT_ALPHABET)
params.mPopupKeyTypes.add(POPUP_KEYS_LAYOUT) params.mPopupKeyTypes.add(POPUP_KEYS_LAYOUT)
addLocaleKeyTextsToParams(latinIME, params, POPUP_KEYS_NORMAL) 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( val expected = listOf(
Expected("a", null, 'a'.code, null), Expected("a", null, 'a'.code, null),
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("$", null, '¥'.code, listOf("£", "", "¢", "¥", "")), Expected("$", null, '¥'.code, listOf("£", "", "¢", "¥", "")),
Expected("i", null, 105, null), Expected("i", null, 105, null),
@ -137,11 +144,15 @@ f""", // no newline at the end
Expected(".", null, '.'.code, listOf(">")), Expected(".", null, '.'.code, listOf(">")),
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("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(null, null, -7, null), // todo: expect an icon
Expected("?123", null, -207, null), Expected("?123", "?123", -202, null),
Expected("", null, ' '.code, null), Expected(null, null, ' '.code, null),
Expected("(", null, '('.code, listOf("<", "[", "{")), Expected("(", null, '('.code, listOf("<", "[", "{")),
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), Expected("p", null, 'p'.code, null),
) )
val layoutString = """ val layoutString = """
@ -149,6 +160,7 @@ f""", // no newline at the end
[ [
{ "$": "auto_text_key" "label": "a" }, { "$": "auto_text_key" "label": "a" },
{ "$": "text_key" "label": "a" }, { "$": "text_key" "label": "a" },
{ "$": "text_key" "label": "a|b", "popup": { "main": { "label": "b|a" } } },
{ "label": "$$$" }, { "label": "$$$" },
{ "label": "$$$", code: -805 }, { "label": "$$$", code: -805 },
{ "$": "case_selector", { "$": "case_selector",
@ -228,6 +240,10 @@ f""", // no newline at the end
{ "code": -805, "label": "currency_slot_5" } { "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" } { "label": "p" }
], ],
[ [
@ -254,11 +270,9 @@ f""", // no newline at the end
""".trimIndent() """.trimIndent()
val keys = JsonKeyboardParser(params, latinIME).parseCoreLayout(layoutString) val keys = JsonKeyboardParser(params, latinIME).parseCoreLayout(layoutString)
keys.first().forEachIndexed { index, keyData -> keys.first().forEachIndexed { index, keyData ->
println("key ${keyData.label}: code ${keyData.code}, popups: ${keyData.popup.getPopupKeyLabels(params)}") println("data: 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)
val keyParams = keyData.toKeyParams(params) val keyParams = keyData.toKeyParams(params)
println("key ${keyParams.mLabel}: code ${keyParams.mCode}, popups: ${keyParams.mPopupKeys?.toList()}") println("params: 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
assertEquals(expected[index].label, keyParams.mLabel) assertEquals(expected[index].label, keyParams.mLabel)
assertEquals(expected[index].code, keyParams.mCode) assertEquals(expected[index].code, keyParams.mCode)
assertEquals(expected[index].popups?.sorted(), keyParams.mPopupKeys?.mapNotNull { it.mLabel }?.sorted()) // todo (later): what's wrong with order? 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. A compilation of information about the layout formats usable in this app.
There are two distinct formats: 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) * 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 ## 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. 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. 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. 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. 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`.
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)
## Simple format ## Simple format
* One key per line * 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 * 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) * 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 * 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. * 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 ## 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) * 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 * 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`. * 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) * 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