mirror of
https://github.com/Helium314/HeliBoard.git
synced 2025-04-16 04:12:02 +00:00
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:
parent
691ae017bc
commit
34d8bd16f0
30 changed files with 931 additions and 712 deletions
21
app/src/main/assets/layouts/functional_keys.json
Normal file
21
app/src/main/assets/layouts/functional_keys.json
Normal 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 }
|
||||
]
|
||||
]
|
36
app/src/main/assets/layouts/functional_keys_tablet.json
Normal file
36
app/src/main/assets/layouts/functional_keys_tablet.json
Normal 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" }
|
||||
}
|
||||
]
|
||||
]
|
|
@ -29,7 +29,7 @@ internal class KeyCodeDescriptionMapper private constructor() {
|
|||
put(KeyCode.SETTINGS, R.string.spoken_description_settings)
|
||||
put(KeyCode.SHIFT, R.string.spoken_description_shift)
|
||||
put(KeyCode.VOICE_INPUT, R.string.spoken_description_mic)
|
||||
put(KeyCode.ALPHA_SYMBOL, R.string.spoken_description_to_symbol)
|
||||
put(KeyCode.SYMBOL_ALPHA, R.string.spoken_description_to_symbol)
|
||||
put(Constants.CODE_TAB, R.string.spoken_description_tab)
|
||||
put(KeyCode.LANGUAGE_SWITCH, R.string.spoken_description_language_switch)
|
||||
put(KeyCode.ACTION_NEXT, R.string.spoken_description_action_next)
|
||||
|
@ -58,7 +58,7 @@ internal class KeyCodeDescriptionMapper private constructor() {
|
|||
*/
|
||||
fun getDescriptionForKey(context: Context, keyboard: Keyboard?, key: Key, shouldObscure: Boolean): String? {
|
||||
val code = key.code
|
||||
if (code == KeyCode.ALPHA_SYMBOL || code == KeyCode.SYMBOL || code == KeyCode.ALPHA) {
|
||||
if (code == KeyCode.SYMBOL_ALPHA || code == KeyCode.SYMBOL || code == KeyCode.ALPHA) {
|
||||
val description = getDescriptionForSwitchAlphaSymbol(context, keyboard)
|
||||
if (description != null) {
|
||||
return description
|
||||
|
|
|
@ -308,17 +308,17 @@ public class Key implements Comparable<Key> {
|
|||
final float horizontalGapFloat = isSpacer() ? 0 : (keyParams.mKeyboardParams.mRelativeHorizontalGap * keyParams.mKeyboardParams.mOccupiedWidth);
|
||||
mHorizontalGap = Math.round(horizontalGapFloat);
|
||||
mVerticalGap = Math.round(keyParams.mKeyboardParams.mRelativeVerticalGap * keyParams.mKeyboardParams.mOccupiedHeight);
|
||||
mWidth = Math.round(keyParams.mFullWidth - horizontalGapFloat);
|
||||
mWidth = Math.round(keyParams.mAbsoluteWidth - horizontalGapFloat);
|
||||
// height is always rounded down, because rounding up may make the keyboard too high to fit, leading to issues
|
||||
mHeight = (int) (keyParams.mFullHeight - keyParams.mKeyboardParams.mVerticalGap);
|
||||
mHeight = (int) (keyParams.mAbsoluteHeight - keyParams.mKeyboardParams.mVerticalGap);
|
||||
if (!isSpacer() && (mWidth == 0 || mHeight == 0)) {
|
||||
throw new IllegalStateException("key needs positive width and height");
|
||||
}
|
||||
// Horizontal gap is divided equally to both sides of the key.
|
||||
mX = Math.round(keyParams.xPos + horizontalGapFloat / 2);
|
||||
mY = Math.round(keyParams.yPos);
|
||||
mHitBox.set(Math.round(keyParams.xPos), Math.round(keyParams.yPos), Math.round(keyParams.xPos + keyParams.mFullWidth) + 1,
|
||||
Math.round(keyParams.yPos + keyParams.mFullHeight));
|
||||
mHitBox.set(Math.round(keyParams.xPos), Math.round(keyParams.yPos), Math.round(keyParams.xPos + keyParams.mAbsoluteWidth) + 1,
|
||||
Math.round(keyParams.yPos + keyParams.mAbsoluteHeight));
|
||||
mHashCode = computeHashCode(this);
|
||||
}
|
||||
|
||||
|
@ -504,7 +504,7 @@ public class Key implements Comparable<Key> {
|
|||
return this instanceof Spacer;
|
||||
}
|
||||
|
||||
public final boolean isActionKey() {
|
||||
public final boolean hasActionKeyBackground() {
|
||||
return mBackgroundType == BACKGROUND_TYPE_ACTION;
|
||||
}
|
||||
|
||||
|
@ -513,7 +513,7 @@ public class Key implements Comparable<Key> {
|
|||
}
|
||||
|
||||
public final boolean isModifier() {
|
||||
return mCode == KeyCode.SHIFT || mCode == KeyCode.ALPHA_SYMBOL || mCode == KeyCode.ALPHA || mCode == KeyCode.SYMBOL;
|
||||
return mCode == KeyCode.SHIFT || mCode == KeyCode.SYMBOL_ALPHA || mCode == KeyCode.ALPHA || mCode == KeyCode.SYMBOL;
|
||||
}
|
||||
|
||||
public final boolean isRepeatable() {
|
||||
|
@ -906,7 +906,7 @@ public class Key implements Comparable<Key> {
|
|||
final Drawable background;
|
||||
if (isAccentColored()) {
|
||||
background = actionKeyBackground;
|
||||
} else if (isFunctional()) {
|
||||
} else if (hasFunctionalBackground()) {
|
||||
background = functionalKeyBackground;
|
||||
} else if (mBackgroundType == BACKGROUND_TYPE_SPACEBAR) {
|
||||
background = spacebarBackground;
|
||||
|
@ -919,7 +919,7 @@ public class Key implements Comparable<Key> {
|
|||
}
|
||||
|
||||
public final boolean isAccentColored() {
|
||||
if (isActionKey()) return true;
|
||||
if (hasActionKeyBackground()) return true;
|
||||
final String iconName = KeyboardIconsSet.getIconName(getIconId());
|
||||
return iconName.equals(KeyboardIconsSet.NAME_NEXT_KEY)
|
||||
|| iconName.equals(KeyboardIconsSet.NAME_PREVIOUS_KEY)
|
||||
|
@ -927,7 +927,7 @@ public class Key implements Comparable<Key> {
|
|||
|| iconName.equals(KeyboardIconsSet.NAME_EMOJI_ACTION_KEY);
|
||||
}
|
||||
|
||||
public boolean isFunctional() {
|
||||
public boolean hasFunctionalBackground() {
|
||||
return mBackgroundType == BACKGROUND_TYPE_FUNCTIONAL
|
||||
|| mBackgroundType == BACKGROUND_TYPE_STICKY_OFF
|
||||
|| mBackgroundType == BACKGROUND_TYPE_STICKY_ON;
|
||||
|
@ -954,12 +954,12 @@ public class Key implements Comparable<Key> {
|
|||
// params for building
|
||||
public boolean isSpacer;
|
||||
private final KeyboardParams mKeyboardParams; // for reading gaps and keyboard width / height
|
||||
public float mRelativeWidth;
|
||||
public float mRelativeHeight; // also should allow negative values, indicating absolute height is defined
|
||||
public float mWidth;
|
||||
public float mHeight; // also should allow negative values, indicating absolute height is defined
|
||||
|
||||
// params that may change
|
||||
public float mFullWidth;
|
||||
public float mFullHeight;
|
||||
public float mAbsoluteWidth;
|
||||
public float mAbsoluteHeight;
|
||||
public float xPos;
|
||||
public float yPos;
|
||||
|
||||
|
@ -977,10 +977,10 @@ public class Key implements Comparable<Key> {
|
|||
@Nullable public final OptionalAttributes mOptionalAttributes;
|
||||
public final boolean mEnabled;
|
||||
|
||||
public static KeyParams newSpacer(final KeyboardParams params, final float relativeWidth) {
|
||||
public static KeyParams newSpacer(final KeyboardParams params, final float width) {
|
||||
final KeyParams spacer = new KeyParams(params);
|
||||
spacer.mRelativeWidth = relativeWidth;
|
||||
spacer.mRelativeHeight = params.mDefaultRelativeRowHeight;
|
||||
spacer.mWidth = width;
|
||||
spacer.mHeight = params.mDefaultRowHeight;
|
||||
return spacer;
|
||||
}
|
||||
|
||||
|
@ -989,18 +989,18 @@ public class Key implements Comparable<Key> {
|
|||
return new Key(this);
|
||||
}
|
||||
|
||||
public void setDimensionsFromRelativeSize(final float newX, final float newY) {
|
||||
if (mRelativeHeight == 0)
|
||||
mRelativeHeight = mKeyboardParams.mDefaultRelativeRowHeight;
|
||||
if (!isSpacer && mRelativeWidth == 0)
|
||||
mRelativeWidth = mKeyboardParams.mDefaultRelativeKeyWidth;
|
||||
if (mRelativeHeight < 0)
|
||||
public void setAbsoluteDimensions(final float newX, final float newY) {
|
||||
if (mHeight == 0)
|
||||
mHeight = mKeyboardParams.mDefaultRowHeight;
|
||||
if (!isSpacer && mWidth == 0)
|
||||
throw new IllegalStateException("width = 0 should have been evaluated already");
|
||||
if (mHeight < 0)
|
||||
// todo (later): deal with it properly when it needs to be adjusted, i.e. when changing popupKeys or moreSuggestions
|
||||
throw new IllegalStateException("can't (yet) deal with absolute height");
|
||||
xPos = newX;
|
||||
yPos = newY;
|
||||
mFullWidth = mRelativeWidth * mKeyboardParams.mBaseWidth;
|
||||
mFullHeight = mRelativeHeight * mKeyboardParams.mBaseHeight;
|
||||
mAbsoluteWidth = mWidth * mKeyboardParams.mBaseWidth;
|
||||
mAbsoluteHeight = mHeight * mKeyboardParams.mBaseHeight;
|
||||
}
|
||||
|
||||
private static int getPopupKeysColumnAndFlagsAndSetNullInArray(final KeyboardParams params, final String[] popupKeys) {
|
||||
|
@ -1052,7 +1052,7 @@ public class Key implements Comparable<Key> {
|
|||
@NonNull final String keySpec, // key text or some special string for KeySpecParser, e.g. "!icon/shift_key|!code/key_shift" (avoid using !text, should be removed)
|
||||
final int code,
|
||||
@NonNull final KeyboardParams params,
|
||||
final float relativeWidth,
|
||||
final float width,
|
||||
final int labelFlags,
|
||||
final int backgroundType,
|
||||
@Nullable final PopupSet<?> popupSet
|
||||
|
@ -1060,8 +1060,8 @@ public class Key implements Comparable<Key> {
|
|||
mKeyboardParams = params;
|
||||
mBackgroundType = backgroundType;
|
||||
mLabelFlags = labelFlags;
|
||||
mRelativeWidth = relativeWidth;
|
||||
mRelativeHeight = params.mDefaultRelativeRowHeight;
|
||||
mWidth = width;
|
||||
mHeight = params.mDefaultRowHeight;
|
||||
mIconId = KeySpecParser.getIconId(keySpec);
|
||||
|
||||
final boolean needsToUpcase = needsToUpcase(mLabelFlags, params.mId.mElementId);
|
||||
|
@ -1144,9 +1144,9 @@ public class Key implements Comparable<Key> {
|
|||
}
|
||||
|
||||
// action flags don't need to be specified, they can be deduced from the key
|
||||
if (backgroundType == BACKGROUND_TYPE_SPACEBAR
|
||||
if (mCode == Constants.CODE_SPACE
|
||||
|| mCode == KeyCode.LANGUAGE_SWITCH
|
||||
|| (mCode == KeyCode.ALPHA_SYMBOL && !params.mId.isAlphabetKeyboard())
|
||||
|| (mCode == KeyCode.SYMBOL_ALPHA && !params.mId.isAlphabetKeyboard())
|
||||
)
|
||||
actionFlags |= ACTION_FLAGS_ENABLE_LONG_PRESS;
|
||||
if (mCode <= Constants.CODE_SPACE && mCode != KeyCode.MULTIPLE_CODE_POINTS)
|
||||
|
@ -1237,8 +1237,8 @@ public class Key implements Comparable<Key> {
|
|||
public KeyParams(final KeyParams keyParams) {
|
||||
xPos = keyParams.xPos;
|
||||
yPos = keyParams.yPos;
|
||||
mRelativeWidth = keyParams.mRelativeWidth;
|
||||
mRelativeHeight = keyParams.mRelativeHeight;
|
||||
mWidth = keyParams.mWidth;
|
||||
mHeight = keyParams.mHeight;
|
||||
isSpacer = keyParams.isSpacer;
|
||||
mKeyboardParams = keyParams.mKeyboardParams;
|
||||
mEnabled = keyParams.mEnabled;
|
||||
|
@ -1248,8 +1248,8 @@ public class Key implements Comparable<Key> {
|
|||
mHintLabel = keyParams.mHintLabel;
|
||||
mLabelFlags = keyParams.mLabelFlags;
|
||||
mIconId = keyParams.mIconId;
|
||||
mFullWidth = keyParams.mFullWidth;
|
||||
mFullHeight = keyParams.mFullHeight;
|
||||
mAbsoluteWidth = keyParams.mAbsoluteWidth;
|
||||
mAbsoluteHeight = keyParams.mAbsoluteHeight;
|
||||
mPopupKeys = keyParams.mPopupKeys;
|
||||
mPopupKeysColumnAndFlags = keyParams.mPopupKeysColumnAndFlags;
|
||||
mBackgroundType = keyParams.mBackgroundType;
|
||||
|
|
|
@ -149,6 +149,10 @@ public final class KeyboardId {
|
|||
return elementId < ELEMENT_SYMBOLS;
|
||||
}
|
||||
|
||||
public boolean isAlphaOrSymbolKeyboard() {
|
||||
return mElementId <= ELEMENT_SYMBOLS_SHIFTED;
|
||||
}
|
||||
|
||||
public boolean isAlphabetKeyboard() {
|
||||
return isAlphabetKeyboard(mElementId);
|
||||
}
|
||||
|
|
|
@ -472,7 +472,7 @@ public class KeyboardView extends View {
|
|||
blendAlpha(paint, params.mAnimAlpha);
|
||||
final float labelCharHeight = TypefaceUtils.getReferenceCharHeight(paint);
|
||||
final float labelCharWidth = TypefaceUtils.getReferenceCharWidth(paint);
|
||||
final boolean isFunctionalKeyAndRoundedStyle = mColors.getThemeStyle().equals(STYLE_ROUNDED) && key.isFunctional();
|
||||
final boolean isFunctionalKeyAndRoundedStyle = mColors.getThemeStyle().equals(STYLE_ROUNDED) && key.hasFunctionalBackground();
|
||||
final float hintX, hintBaseline;
|
||||
if (key.hasHintLabel()) {
|
||||
// The hint label is placed just right of the key label. Used mainly on
|
||||
|
@ -555,7 +555,7 @@ public class KeyboardView extends View {
|
|||
if (key.getBackgroundType() == Key.BACKGROUND_TYPE_SPACEBAR)
|
||||
hintX = keyWidth + hintBaseline + labelCharWidth * 0.1f;
|
||||
else
|
||||
hintX = key.isFunctional() || key.isActionKey() ? keyWidth / 2.0f : keyWidth - mKeyHintLetterPadding - labelCharWidth / 2.0f;
|
||||
hintX = key.hasFunctionalBackground() || key.hasActionKeyBackground() ? keyWidth / 2.0f : keyWidth - mKeyHintLetterPadding - labelCharWidth / 2.0f;
|
||||
} else {
|
||||
hintX = keyWidth - mKeyHintLetterPadding - TypefaceUtils.getReferenceCharWidth(paint) / 2.0f;
|
||||
}
|
||||
|
|
|
@ -540,7 +540,7 @@ public final class MainKeyboardView extends KeyboardView implements DrawingProxy
|
|||
mPopupKeysKeyboardCache.put(key, popupKeysKeyboard);
|
||||
}
|
||||
|
||||
final View container = key.isActionKey() ? mPopupKeysKeyboardForActionContainer
|
||||
final View container = key.hasActionKeyBackground() ? mPopupKeysKeyboardForActionContainer
|
||||
: mPopupKeysKeyboardContainer;
|
||||
final PopupKeysKeyboardView popupKeysKeyboardView =
|
||||
container.findViewById(R.id.popup_keys_keyboard_view);
|
||||
|
|
|
@ -1107,7 +1107,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element,
|
|||
return;
|
||||
}
|
||||
}
|
||||
if (code == KeyCode.ALPHA_SYMBOL && Settings.getInstance().getCurrent().mLongPressSymbolsForNumpad) {
|
||||
if (code == KeyCode.SYMBOL_ALPHA && Settings.getInstance().getCurrent().mLongPressSymbolsForNumpad) {
|
||||
sListener.onCodeInput(KeyCode.NUMPAD, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false);
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ public final class PopupKeysKeyboard extends Keyboard {
|
|||
|
||||
PopupKeysKeyboard(final PopupKeysKeyboardParams params) {
|
||||
super(params);
|
||||
mDefaultKeyCoordX = params.getDefaultKeyCoordX() + params.mDefaultKeyWidth / 2;
|
||||
mDefaultKeyCoordX = params.getDefaultKeyCoordX() + params.mDefaultAbsoluteKeyWidth / 2;
|
||||
}
|
||||
|
||||
public int getDefaultCoordX() {
|
||||
|
@ -71,8 +71,8 @@ public final class PopupKeysKeyboard extends Keyboard {
|
|||
throw new IllegalArgumentException("Keyboard is too small to hold popup keys: "
|
||||
+ parentKeyboardWidth + " " + keyWidth + " " + numKeys + " " + numColumn);
|
||||
}
|
||||
mDefaultKeyWidth = keyWidth;
|
||||
mDefaultRowHeight = rowHeight;
|
||||
mDefaultAbsoluteKeyWidth = keyWidth;
|
||||
mDefaultAbsoluteRowHeight = rowHeight;
|
||||
|
||||
mNumRows = (numKeys + numColumn - 1) / numColumn;
|
||||
final int numColumns = isPopupKeysFixedColumn ? Math.min(numKeys, numColumn)
|
||||
|
@ -116,10 +116,10 @@ public final class PopupKeysKeyboard extends Keyboard {
|
|||
mTopRowAdjustment = isPopupKeysFixedOrder ? getFixedOrderTopRowAdjustment()
|
||||
: getAutoOrderTopRowAdjustment();
|
||||
mDividerWidth = dividerWidth;
|
||||
mColumnWidth = mDefaultKeyWidth + mDividerWidth;
|
||||
mColumnWidth = mDefaultAbsoluteKeyWidth + mDividerWidth;
|
||||
mBaseWidth = mOccupiedWidth = mNumColumns * mColumnWidth - mDividerWidth;
|
||||
// Need to subtract the bottom row's gutter only.
|
||||
mBaseHeight = mOccupiedHeight = mNumRows * mDefaultRowHeight - mVerticalGap
|
||||
mBaseHeight = mOccupiedHeight = mNumRows * mDefaultAbsoluteRowHeight - mVerticalGap
|
||||
+ mTopPadding + mBottomPadding;
|
||||
}
|
||||
|
||||
|
@ -227,7 +227,7 @@ public final class PopupKeysKeyboard extends Keyboard {
|
|||
}
|
||||
|
||||
public int getY(final int row) {
|
||||
return (mNumRows - 1 - row) * mDefaultRowHeight + mTopPadding;
|
||||
return (mNumRows - 1 - row) * mDefaultAbsoluteRowHeight + mTopPadding;
|
||||
}
|
||||
|
||||
public void markAsEdgeKey(final Key key, final int row) {
|
||||
|
@ -287,8 +287,8 @@ public final class PopupKeysKeyboard extends Keyboard {
|
|||
final float padding = context.getResources().getDimension(
|
||||
R.dimen.config_popup_keys_keyboard_key_horizontal_padding)
|
||||
+ (key.hasLabelsInPopupKeys()
|
||||
? mParams.mDefaultKeyWidth * LABEL_PADDING_RATIO : 0.0f);
|
||||
keyWidth = getMaxKeyWidth(key, mParams.mDefaultKeyWidth, padding, paintToMeasure);
|
||||
? mParams.mDefaultAbsoluteKeyWidth * LABEL_PADDING_RATIO : 0.0f);
|
||||
keyWidth = getMaxKeyWidth(key, mParams.mDefaultAbsoluteKeyWidth, padding, paintToMeasure);
|
||||
rowHeight = keyboard.mMostCommonKeyHeight;
|
||||
}
|
||||
final int dividerWidth;
|
||||
|
@ -342,9 +342,9 @@ public final class PopupKeysKeyboard extends Keyboard {
|
|||
// left of the default position.
|
||||
if (params.mDividerWidth > 0 && pos != 0) {
|
||||
final int dividerX = (pos > 0) ? x - params.mDividerWidth
|
||||
: x + params.mDefaultKeyWidth;
|
||||
: x + params.mDefaultAbsoluteKeyWidth;
|
||||
final Key divider = new PopupKeyDivider(
|
||||
params, dividerX, y, params.mDividerWidth, params.mDefaultRowHeight);
|
||||
params, dividerX, y, params.mDividerWidth, params.mDefaultAbsoluteRowHeight);
|
||||
params.onAddKey(divider);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -111,12 +111,12 @@ open class KeyboardBuilder<KP : KeyboardParams>(protected val mContext: Context,
|
|||
fillGapsWithSpacers(row)
|
||||
var currentX = mParams.mLeftPadding.toFloat()
|
||||
row.forEach {
|
||||
it.setDimensionsFromRelativeSize(currentX, currentY)
|
||||
it.setAbsoluteDimensions(currentX, currentY)
|
||||
if (DebugFlags.DEBUG_ENABLED)
|
||||
Log.d(TAG, "setting size and position for ${it.mLabel}, ${it.mCode}: x ${currentX.toInt()}, w ${it.mFullWidth.toInt()}")
|
||||
currentX += it.mFullWidth
|
||||
Log.d(TAG, "setting size and position for ${it.mLabel}, ${it.mCode}: x ${currentX.toInt()}, w ${it.mAbsoluteWidth.toInt()}")
|
||||
currentX += it.mAbsoluteWidth
|
||||
}
|
||||
currentY += row.first().mFullHeight
|
||||
currentY += row.first().mAbsoluteHeight
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -140,7 +140,7 @@ open class KeyboardBuilder<KP : KeyboardParams>(protected val mContext: Context,
|
|||
i++
|
||||
currentX += currentKeyXPos - currentX
|
||||
}
|
||||
currentX += row[i].mFullWidth
|
||||
currentX += row[i].mAbsoluteWidth
|
||||
i++
|
||||
}
|
||||
if (currentX < mParams.mOccupiedWidth) {
|
||||
|
@ -161,48 +161,48 @@ open class KeyboardBuilder<KP : KeyboardParams>(protected val mContext: Context,
|
|||
for (row in keysInRows) {
|
||||
fillGapsWithSpacers(row)
|
||||
val y = row.first().yPos // all have the same y, so this is fine
|
||||
val relativeWidthSum = row.sumOf { it.mRelativeWidth } // sum up relative widths
|
||||
val relativeWidthSum = row.sumOf { it.mWidth } // sum up relative widths
|
||||
val spacer = KeyParams.newSpacer(mParams, spacerRelativeWidth)
|
||||
// insert spacer before first key that starts right of the center (also consider gap)
|
||||
var insertIndex = row.indexOfFirst { it.xPos + it.mFullWidth / 3 > mParams.mOccupiedWidth / 2 }
|
||||
var insertIndex = row.indexOfFirst { it.xPos + it.mAbsoluteWidth / 3 > mParams.mOccupiedWidth / 2 }
|
||||
.takeIf { it > -1 } ?: (row.size / 2) // fallback should never be needed, but better than having an error
|
||||
if (row.any { it.mCode == Constants.CODE_SPACE }) {
|
||||
val spaceLeft = row.single { it.mCode == Constants.CODE_SPACE }
|
||||
reduceSymbolAndActionKeyWidth(row)
|
||||
insertIndex = row.indexOf(spaceLeft) + 1
|
||||
val widthBeforeSpace = row.subList(0, insertIndex - 1).sumOf { it.mRelativeWidth }
|
||||
val widthAfterSpace = row.subList(insertIndex, row.size).sumOf { it.mRelativeWidth }
|
||||
val spaceLeftWidth = (maxWidthBeforeSpacer - widthBeforeSpace).coerceAtLeast(mParams.mDefaultRelativeKeyWidth)
|
||||
val spaceRightWidth = (maxWidthAfterSpacer - widthAfterSpace).coerceAtLeast(mParams.mDefaultRelativeKeyWidth)
|
||||
val spacerWidth = spaceLeft.mRelativeWidth + spacerRelativeWidth - spaceLeftWidth - spaceRightWidth
|
||||
val widthBeforeSpace = row.subList(0, insertIndex - 1).sumOf { it.mWidth }
|
||||
val widthAfterSpace = row.subList(insertIndex, row.size).sumOf { it.mWidth }
|
||||
val spaceLeftWidth = (maxWidthBeforeSpacer - widthBeforeSpace).coerceAtLeast(mParams.mDefaultKeyWidth)
|
||||
val spaceRightWidth = (maxWidthAfterSpacer - widthAfterSpace).coerceAtLeast(mParams.mDefaultKeyWidth)
|
||||
val spacerWidth = spaceLeft.mWidth + spacerRelativeWidth - spaceLeftWidth - spaceRightWidth
|
||||
if (spacerWidth > 0.05f) {
|
||||
// only insert if the spacer has a reasonable width
|
||||
val spaceRight = KeyParams(spaceLeft)
|
||||
spaceLeft.mRelativeWidth = spaceLeftWidth
|
||||
spaceRight.mRelativeWidth = spaceRightWidth
|
||||
spacer.mRelativeWidth = spacerWidth
|
||||
spaceLeft.mWidth = spaceLeftWidth
|
||||
spaceRight.mWidth = spaceRightWidth
|
||||
spacer.mWidth = spacerWidth
|
||||
row.add(insertIndex, spaceRight)
|
||||
row.add(insertIndex, spacer)
|
||||
} else {
|
||||
// otherwise increase space width, so other keys are resized properly
|
||||
spaceLeft.mRelativeWidth += spacerWidth
|
||||
spaceLeft.mWidth += spacerWidth
|
||||
}
|
||||
} else {
|
||||
val widthBeforeSpacer = row.subList(0, insertIndex).sumOf { it.mRelativeWidth }
|
||||
val widthAfterSpacer = row.subList(insertIndex, row.size).sumOf { it.mRelativeWidth }
|
||||
val widthBeforeSpacer = row.subList(0, insertIndex).sumOf { it.mWidth }
|
||||
val widthAfterSpacer = row.subList(insertIndex, row.size).sumOf { it.mWidth }
|
||||
maxWidthBeforeSpacer = maxWidthBeforeSpacer.coerceAtLeast(widthBeforeSpacer)
|
||||
maxWidthAfterSpacer = maxWidthAfterSpacer.coerceAtLeast(widthAfterSpacer)
|
||||
row.add(insertIndex, spacer)
|
||||
}
|
||||
// re-calculate relative widths
|
||||
val relativeWidthSumNew = row.sumOf { it.mRelativeWidth }
|
||||
val relativeWidthSumNew = row.sumOf { it.mWidth }
|
||||
val widthFactor = relativeWidthSum / relativeWidthSumNew
|
||||
// re-calculate absolute sizes and positions
|
||||
var currentX = 0f
|
||||
row.forEach {
|
||||
it.mRelativeWidth *= widthFactor
|
||||
it.setDimensionsFromRelativeSize(currentX, y)
|
||||
currentX += it.mFullWidth
|
||||
it.mWidth *= widthFactor
|
||||
it.setAbsoluteDimensions(currentX, y)
|
||||
currentX += it.mAbsoluteWidth
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -211,19 +211,19 @@ open class KeyboardBuilder<KP : KeyboardParams>(protected val mContext: Context,
|
|||
// todo: this assumes fixed layout for symbols keys, which will change soon!
|
||||
private fun reduceSymbolAndActionKeyWidth(row: ArrayList<KeyParams>) {
|
||||
val spaceKey = row.first { it.mCode == Constants.CODE_SPACE }
|
||||
val symbolKey = row.firstOrNull { it.mCode == KeyCode.ALPHA_SYMBOL }
|
||||
val symbolKeyWidth = symbolKey?.mRelativeWidth ?: 0f
|
||||
if (symbolKeyWidth > mParams.mDefaultRelativeKeyWidth) {
|
||||
val widthToChange = symbolKey!!.mRelativeWidth - mParams.mDefaultRelativeKeyWidth
|
||||
symbolKey.mRelativeWidth -= widthToChange
|
||||
spaceKey.mRelativeWidth += widthToChange
|
||||
val symbolKey = row.firstOrNull { it.mCode == KeyCode.SYMBOL_ALPHA }
|
||||
val symbolKeyWidth = symbolKey?.mWidth ?: 0f
|
||||
if (symbolKeyWidth > mParams.mDefaultKeyWidth) {
|
||||
val widthToChange = symbolKey!!.mWidth - mParams.mDefaultKeyWidth
|
||||
symbolKey.mWidth -= widthToChange
|
||||
spaceKey.mWidth += widthToChange
|
||||
}
|
||||
val actionKey = row.firstOrNull { it.mBackgroundType == Key.BACKGROUND_TYPE_ACTION }
|
||||
val actionKeyWidth = actionKey?.mRelativeWidth ?: 0f
|
||||
if (actionKeyWidth > mParams.mDefaultRelativeKeyWidth * 1.1f) { // allow it to stay a little wider
|
||||
val widthToChange = actionKey!!.mRelativeWidth - mParams.mDefaultRelativeKeyWidth * 1.1f
|
||||
actionKey.mRelativeWidth -= widthToChange
|
||||
spaceKey.mRelativeWidth += widthToChange
|
||||
val actionKeyWidth = actionKey?.mWidth ?: 0f
|
||||
if (actionKeyWidth > mParams.mDefaultKeyWidth * 1.1f) { // allow it to stay a little wider
|
||||
val widthToChange = actionKey!!.mWidth - mParams.mDefaultKeyWidth * 1.1f
|
||||
actionKey.mWidth -= widthToChange
|
||||
spaceKey.mWidth += widthToChange
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -58,7 +58,7 @@ public final class KeyboardCodesSet {
|
|||
Constants.CODE_SPACE,
|
||||
KeyCode.SHIFT,
|
||||
KeyCode.CAPS_LOCK,
|
||||
KeyCode.ALPHA_SYMBOL,
|
||||
KeyCode.SYMBOL_ALPHA,
|
||||
KeyCode.ALPHA,
|
||||
KeyCode.SYMBOL,
|
||||
KeyCode.MULTIPLE_CODE_POINTS,
|
||||
|
|
|
@ -19,7 +19,6 @@ import helium314.keyboard.keyboard.KeyboardId;
|
|||
import helium314.keyboard.keyboard.internal.keyboard_parser.LocaleKeyboardInfos;
|
||||
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode;
|
||||
import helium314.keyboard.latin.R;
|
||||
import helium314.keyboard.latin.common.Constants;
|
||||
import helium314.keyboard.latin.settings.Settings;
|
||||
import helium314.keyboard.latin.utils.ResourceUtils;
|
||||
|
||||
|
@ -55,13 +54,13 @@ public class KeyboardParams {
|
|||
@Nullable
|
||||
public KeyVisualAttributes mKeyVisualAttributes;
|
||||
|
||||
public float mDefaultRelativeRowHeight;
|
||||
public float mDefaultRelativeKeyWidth;
|
||||
public float mDefaultRowHeight;
|
||||
public float mDefaultKeyWidth;
|
||||
public float mRelativeHorizontalGap;
|
||||
public float mRelativeVerticalGap;
|
||||
// relative values multiplied with baseHeight / baseWidth
|
||||
public int mDefaultRowHeight;
|
||||
public int mDefaultKeyWidth;
|
||||
public int mDefaultAbsoluteRowHeight;
|
||||
public int mDefaultAbsoluteKeyWidth;
|
||||
public int mHorizontalGap;
|
||||
public int mVerticalGap;
|
||||
|
||||
|
@ -227,9 +226,9 @@ public class KeyboardParams {
|
|||
mBaseWidth = mOccupiedWidth - mLeftPadding - mRightPadding;
|
||||
final float defaultKeyWidthFactor = context.getResources().getInteger(R.integer.config_screen_metrics) > 2
|
||||
? 0.9f : 1f;
|
||||
mDefaultRelativeKeyWidth = keyAttr.getFraction(R.styleable.Keyboard_Key_keyWidth,
|
||||
mDefaultKeyWidth = keyAttr.getFraction(R.styleable.Keyboard_Key_keyWidth,
|
||||
1, 1, defaultKeyWidthFactor / DEFAULT_KEYBOARD_COLUMNS);
|
||||
mDefaultKeyWidth = (int) (mDefaultRelativeKeyWidth * mBaseWidth);
|
||||
mDefaultAbsoluteKeyWidth = (int) (mDefaultKeyWidth * mBaseWidth);
|
||||
|
||||
// todo: maybe settings should not be accessed from here?
|
||||
if (Settings.getInstance().getCurrent().mNarrowKeyGaps) {
|
||||
|
@ -250,13 +249,13 @@ public class KeyboardParams {
|
|||
mVerticalGap = (int) (mRelativeVerticalGap * height);
|
||||
|
||||
mBaseHeight = mOccupiedHeight - mTopPadding - mBottomPadding + mVerticalGap;
|
||||
mDefaultRelativeRowHeight = ResourceUtils.getDimensionOrFraction(keyboardAttr,
|
||||
mDefaultRowHeight = ResourceUtils.getDimensionOrFraction(keyboardAttr,
|
||||
R.styleable.Keyboard_rowHeight, 1, 1f / DEFAULT_KEYBOARD_ROWS);
|
||||
if (mDefaultRelativeRowHeight > 1) { // can be absolute size, in that case will be > 1
|
||||
mDefaultRowHeight = (int) mDefaultRelativeRowHeight;
|
||||
mDefaultRelativeRowHeight *= -1; // make it negative when it's absolute
|
||||
if (mDefaultRowHeight > 1) { // can be absolute size, in that case will be > 1
|
||||
mDefaultAbsoluteRowHeight = (int) mDefaultRowHeight;
|
||||
mDefaultRowHeight *= -1; // make it negative when it's absolute
|
||||
} else {
|
||||
mDefaultRowHeight = (int) (mDefaultRelativeRowHeight * mBaseHeight);
|
||||
mDefaultAbsoluteRowHeight = (int) (mDefaultRowHeight * mBaseHeight);
|
||||
}
|
||||
|
||||
mKeyVisualAttributes = KeyVisualAttributes.newInstance(keyAttr);
|
||||
|
|
|
@ -422,7 +422,7 @@ public final class KeyboardState {
|
|||
onPressShift();
|
||||
} else if (code == KeyCode.CAPS_LOCK) {
|
||||
// Nothing to do here. See {@link #onReleaseKey(int,boolean)}.
|
||||
} else if (code == KeyCode.ALPHA_SYMBOL) {
|
||||
} else if (code == KeyCode.SYMBOL_ALPHA) {
|
||||
onPressAlphaSymbol(autoCapsFlags, recapitalizeMode);
|
||||
} else if (code == KeyCode.SYMBOL) {
|
||||
// don't start sliding, causes issues with fully customizable layouts
|
||||
|
@ -463,7 +463,7 @@ public final class KeyboardState {
|
|||
onReleaseShift(withSliding, autoCapsFlags, recapitalizeMode);
|
||||
} else if (code == KeyCode.CAPS_LOCK) {
|
||||
setShiftLocked(!mAlphabetShiftState.isShiftLocked());
|
||||
} else if (code == KeyCode.ALPHA_SYMBOL) {
|
||||
} else if (code == KeyCode.SYMBOL_ALPHA) {
|
||||
onReleaseAlphaSymbol(withSliding, autoCapsFlags, recapitalizeMode);
|
||||
} else if (code == KeyCode.SYMBOL) {
|
||||
onReleaseSymbol(withSliding, autoCapsFlags, recapitalizeMode);
|
||||
|
@ -700,7 +700,7 @@ public final class KeyboardState {
|
|||
|
||||
switch (mSwitchState) {
|
||||
case SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL:
|
||||
if (code == KeyCode.ALPHA_SYMBOL) {
|
||||
if (code == KeyCode.SYMBOL_ALPHA) {
|
||||
// Detected only the mode change key has been pressed, and then released.
|
||||
if (mMode == MODE_ALPHABET) {
|
||||
mSwitchState = SWITCH_STATE_ALPHA;
|
||||
|
|
|
@ -70,7 +70,7 @@ public final class PopupKeySpec {
|
|||
public Key buildKey(final int x, final int y, final int labelFlags,
|
||||
@NonNull final KeyboardParams params) {
|
||||
return new Key(mLabel, mIconId, mCode, mOutputText, null /* hintLabel */, labelFlags,
|
||||
Key.BACKGROUND_TYPE_NORMAL, x, y, params.mDefaultKeyWidth, params.mDefaultRowHeight,
|
||||
Key.BACKGROUND_TYPE_NORMAL, x, y, params.mDefaultAbsoluteKeyWidth, params.mDefaultAbsoluteRowHeight,
|
||||
params.mHorizontalGap, params.mVerticalGap);
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,6 @@ import helium314.keyboard.keyboard.KeyboardId
|
|||
import helium314.keyboard.keyboard.internal.KeyboardParams
|
||||
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode
|
||||
import helium314.keyboard.latin.R
|
||||
import helium314.keyboard.latin.common.Constants
|
||||
import helium314.keyboard.latin.common.StringUtils
|
||||
import helium314.keyboard.latin.settings.Settings
|
||||
import helium314.keyboard.latin.utils.ResourceUtils
|
||||
|
@ -44,20 +43,20 @@ class EmojiParser(private val params: KeyboardParams, private val context: Conte
|
|||
|
||||
// determine key width for default settings (no number row, no one-handed mode, 100% height and bottom padding scale)
|
||||
// this is a bit long, but ensures that emoji size stays the same, independent of these settings
|
||||
val defaultKeyWidth = (ResourceUtils.getDefaultKeyboardWidth(context.resources) - params.mLeftPadding - params.mRightPadding) * params.mDefaultRelativeKeyWidth
|
||||
val defaultKeyWidth = (ResourceUtils.getDefaultKeyboardWidth(context.resources) - params.mLeftPadding - params.mRightPadding) * params.mDefaultKeyWidth
|
||||
val keyWidth = defaultKeyWidth * sqrt(Settings.getInstance().current.mKeyboardHeightScale)
|
||||
val defaultKeyboardHeight = ResourceUtils.getDefaultKeyboardHeight(context.resources, false)
|
||||
val defaultBottomPadding = context.resources.getFraction(R.fraction.config_keyboard_bottom_padding_holo, defaultKeyboardHeight, defaultKeyboardHeight)
|
||||
val emojiKeyboardHeight = ResourceUtils.getDefaultKeyboardHeight(context.resources, false) * 0.75f + params.mVerticalGap - defaultBottomPadding - context.resources.getDimensionPixelSize(R.dimen.config_emoji_category_page_id_height)
|
||||
val keyHeight = emojiKeyboardHeight * params.mDefaultRelativeRowHeight * Settings.getInstance().current.mKeyboardHeightScale // still apply height scale to key
|
||||
val keyHeight = emojiKeyboardHeight * params.mDefaultRowHeight * Settings.getInstance().current.mKeyboardHeightScale // still apply height scale to key
|
||||
|
||||
emojiArray.forEachIndexed { i, codeArraySpec ->
|
||||
val keyParams = parseEmojiKey(codeArraySpec, popupEmojisArray?.get(i)?.takeIf { it.isNotEmpty() }) ?: return@forEachIndexed
|
||||
keyParams.xPos = currentX
|
||||
keyParams.yPos = currentY
|
||||
keyParams.mFullWidth = keyWidth
|
||||
keyParams.mFullHeight = keyHeight
|
||||
currentX += keyParams.mFullWidth
|
||||
keyParams.mAbsoluteWidth = keyWidth
|
||||
keyParams.mAbsoluteHeight = keyHeight
|
||||
currentX += keyParams.mAbsoluteWidth
|
||||
row.add(keyParams)
|
||||
}
|
||||
return arrayListOf(row)
|
||||
|
|
|
@ -9,18 +9,17 @@ import androidx.annotation.StringRes
|
|||
import helium314.keyboard.keyboard.Key
|
||||
import helium314.keyboard.keyboard.Key.KeyParams
|
||||
import helium314.keyboard.keyboard.KeyboardId
|
||||
import helium314.keyboard.keyboard.KeyboardTheme
|
||||
import helium314.keyboard.keyboard.internal.KeyboardIconsSet
|
||||
import helium314.keyboard.keyboard.internal.KeyboardParams
|
||||
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode
|
||||
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyData
|
||||
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyLabel
|
||||
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyType
|
||||
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.SimplePopups
|
||||
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.TextKeyData
|
||||
import helium314.keyboard.latin.R
|
||||
import helium314.keyboard.latin.common.Constants
|
||||
import helium314.keyboard.latin.common.LocaleUtils.constructLocale
|
||||
import helium314.keyboard.latin.common.isEmoji
|
||||
import helium314.keyboard.latin.common.splitOnWhitespace
|
||||
import helium314.keyboard.latin.define.DebugFlags
|
||||
import helium314.keyboard.latin.settings.Settings
|
||||
import helium314.keyboard.latin.spellcheck.AndroidSpellCheckerService
|
||||
|
@ -31,7 +30,10 @@ import helium314.keyboard.latin.utils.POPUP_KEYS_NUMBER
|
|||
import helium314.keyboard.latin.utils.ScriptUtils
|
||||
import helium314.keyboard.latin.utils.ScriptUtils.script
|
||||
import helium314.keyboard.latin.utils.getLayoutFile
|
||||
import helium314.keyboard.latin.utils.removeFirst
|
||||
import helium314.keyboard.latin.utils.replaceFirst
|
||||
import helium314.keyboard.latin.utils.runInLocale
|
||||
import helium314.keyboard.latin.utils.splitAt
|
||||
import helium314.keyboard.latin.utils.sumOf
|
||||
import java.io.File
|
||||
|
||||
|
@ -65,103 +67,260 @@ abstract class KeyboardParser(private val params: KeyboardParams, private val co
|
|||
params.mTouchPositionCorrection.load(context.resources.getStringArray(infos.touchPositionCorrectionData))
|
||||
|
||||
val baseKeys: MutableList<List<KeyData>> = parseCoreLayout(layoutContent)
|
||||
val keysInRows: ArrayList<ArrayList<KeyParams>>
|
||||
if (params.mId.mElementId <= KeyboardId.ELEMENT_SYMBOLS_SHIFTED) {
|
||||
keysInRows = createAlphaSymbolRows(baseKeys)
|
||||
val keysInRows = if (params.mId.isAlphaOrSymbolKeyboard) {
|
||||
createAlphaSymbolRows(baseKeys)
|
||||
} else if (params.mId.isNumberLayout) {
|
||||
keysInRows = createNumericRows(baseKeys)
|
||||
createNumericRows(baseKeys)
|
||||
} else {
|
||||
throw(UnsupportedOperationException("creating KeyboardId ${params.mId.mElementId} not supported"))
|
||||
}
|
||||
// rescale height if we have more than 4 rows
|
||||
val heightRescale = if (keysInRows.size > 4) 4f / keysInRows.size else 1f
|
||||
if (heightRescale != 1f) {
|
||||
keysInRows.forEach { row -> row.forEach { it.mRelativeHeight *= heightRescale } }
|
||||
keysInRows.forEach { row -> row.forEach { it.mHeight *= heightRescale } }
|
||||
}
|
||||
|
||||
return keysInRows
|
||||
}
|
||||
|
||||
// this should be ready for customizable functional layouts, but needs cleanup
|
||||
// todo (later): remove this as part of adding a cache for parsed layouts
|
||||
private fun getFunctionalKeyLayoutText(): String {
|
||||
if (!params.mId.isAlphaOrSymbolKeyboard) throw IllegalStateException("functional key layout only for aloha and symbol layouts")
|
||||
val layouts = Settings.getLayoutsDir(context).list() ?: emptyArray()
|
||||
if (params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS_SHIFTED) {
|
||||
if ("functional_keys_symbols_shifted.json" in layouts)
|
||||
return getLayoutFile("functional_keys_symbols_shifted.json", context).readText()
|
||||
}
|
||||
if (!params.mId.isAlphabetKeyboard) {
|
||||
if ("functional_keys_symbols.json" in layouts)
|
||||
return getLayoutFile("functional_keys_symbols.json", context).readText()
|
||||
}
|
||||
if ("functional_keys.json" in layouts)
|
||||
return getLayoutFile("functional_keys.json", context).readText()
|
||||
val fileName = if (Settings.getInstance().isTablet) "functional_keys_tablet.json" else "functional_keys.json"
|
||||
return context.readAssetsLayoutFile(fileName)
|
||||
}
|
||||
|
||||
private fun createAlphaSymbolRows(baseKeys: MutableList<List<KeyData>>): ArrayList<ArrayList<KeyParams>> {
|
||||
addNumberRowOrPopupKeys(baseKeys)
|
||||
if (params.mId.isAlphabetKeyboard)
|
||||
addSymbolPopupKeys(baseKeys)
|
||||
val bottomRow = getBottomRowAndAdjustBaseKeys(baseKeys) // not really fast, but irrelevant compared to the loop
|
||||
if (params.mId.mNumberRowEnabled)
|
||||
baseKeys.add(
|
||||
0,
|
||||
params.mLocaleKeyboardInfos.getNumberRow()
|
||||
.map { it.copy(newLabelFlags = Key.LABEL_FLAGS_DISABLE_HINT_LABEL or defaultLabelFlags) })
|
||||
|
||||
val keysInRows = ArrayList<ArrayList<KeyParams>>()
|
||||
val functionalKeys = parseFunctionalKeys(R.string.key_def_functional)
|
||||
val functionalKeysTop = parseFunctionalKeys(R.string.key_def_functional_top_row)
|
||||
val allFunctionalKeys = JsonKeyboardParser(params, context).parseCoreLayout(getFunctionalKeyLayoutText())
|
||||
adjustBottomFunctionalRowAndBaseKeys(allFunctionalKeys, baseKeys)
|
||||
|
||||
// todo: this loop could use some performance improvements
|
||||
baseKeys.forEachIndexed { i, it ->
|
||||
val row: List<KeyData> = if (i == baseKeys.lastIndex && isTablet()) {
|
||||
if (allFunctionalKeys.none { it.singleOrNull()?.isKeyPlaceholder() == true })
|
||||
// add a placeholder so splitAt does what we really want
|
||||
allFunctionalKeys.add(0, listOf(TextKeyData(type = KeyType.PLACEHOLDER)))
|
||||
|
||||
val (functionalKeysTop, functionalKeysBottom) = allFunctionalKeys.splitAt { it.singleOrNull()?.isKeyPlaceholder() == true }
|
||||
|
||||
// offset for bottom, relevant for getting correct functional key rows
|
||||
val bottomIndexOffset = baseKeys.size - functionalKeysBottom.size
|
||||
|
||||
val functionalKeys = mutableListOf<Pair<List<KeyParams>, List<KeyParams>>>()
|
||||
val baseKeyParams = baseKeys.mapIndexed { i, it ->
|
||||
val row: List<KeyData> = if (i == baseKeys.lastIndex - 1 && Settings.getInstance().isTablet) {
|
||||
// add bottom row extra keys
|
||||
// todo (later): this can make very customized layouts look awkward
|
||||
// decide when to (not) add it
|
||||
// when not adding, consider that punctuation popup keys should not remove those keys!
|
||||
val tabletExtraKeys = params.mLocaleKeyboardInfos.getTabletExtraKeys(params.mId.mElementId)
|
||||
tabletExtraKeys.first + it + tabletExtraKeys.second
|
||||
} else {
|
||||
it
|
||||
}
|
||||
// parse functional keys for this row (if any)
|
||||
val offset = baseKeys.size - functionalKeys.size
|
||||
val functionalKeysDefs = if (i >= offset) functionalKeys[i - offset] // functional keys are aligned to bottom
|
||||
else emptyList<String>() to emptyList()
|
||||
val outerFunctionalKeyDefs = if (i == 0 && functionalKeysTop.isNotEmpty()) functionalKeysTop.first() // top row
|
||||
else emptyList<String>() to emptyList()
|
||||
// if we have a top row and top row entries from normal functional key defs, use top row as outer keys
|
||||
val functionalKeysLeft = outerFunctionalKeyDefs.first.map { getFunctionalKeyParams(it) } + functionalKeysDefs.first.map { getFunctionalKeyParams(it) }
|
||||
val functionalKeysRight = functionalKeysDefs.second.map { getFunctionalKeyParams(it) } + outerFunctionalKeyDefs.second.map { getFunctionalKeyParams(it) }
|
||||
val paramsRow = ArrayList<KeyParams>(functionalKeysLeft)
|
||||
|
||||
// determine key width, maybe scale factor for keys, and spacers to add
|
||||
val usedKeyWidth = params.mDefaultRelativeKeyWidth * row.size
|
||||
val functionalKeyWidth = (functionalKeysLeft.sumOf { it.mRelativeWidth }) + (functionalKeysRight.sumOf { it.mRelativeWidth })
|
||||
val availableWidth = 1f - functionalKeyWidth
|
||||
var keyWidth: Float
|
||||
val spacerWidth: Float
|
||||
if (availableWidth - usedKeyWidth > 0.0001f) { // don't add spacers if only a tiny bit is empty
|
||||
// width available, add spacer
|
||||
keyWidth = params.mDefaultRelativeKeyWidth
|
||||
spacerWidth = (availableWidth - usedKeyWidth) / 2
|
||||
} else {
|
||||
// need more width, re-scale
|
||||
spacerWidth = 0f
|
||||
keyWidth = availableWidth / row.size
|
||||
}
|
||||
if (spacerWidth != 0f) {
|
||||
paramsRow.add(KeyParams.newSpacer(params, spacerWidth))
|
||||
}
|
||||
if (keyWidth < params.mDefaultRelativeKeyWidth * 0.82 && spacerWidth == 0f) {
|
||||
// keys are very narrow, also rescale the functional keys to make keys a little wider
|
||||
// 0.82 is just some guess for "too narrow"
|
||||
val allKeyScale = 1f / (functionalKeyWidth + row.size * params.mDefaultRelativeKeyWidth)
|
||||
keyWidth = params.mDefaultRelativeKeyWidth * allKeyScale
|
||||
functionalKeysLeft.forEach { it.mRelativeWidth *= allKeyScale }
|
||||
functionalKeysRight.forEach { it.mRelativeWidth *= allKeyScale }
|
||||
}
|
||||
// build list of functional keys of same size as baseKeys
|
||||
val functionalKeysFromTop = functionalKeysTop.getOrNull(i) ?: emptyList()
|
||||
val functionalKeysFromBottom = functionalKeysBottom.getOrNull(i - bottomIndexOffset) ?: emptyList()
|
||||
functionalKeys.add(getFunctionalKeysBySide(functionalKeysFromTop, functionalKeysFromBottom))
|
||||
|
||||
for (key in row) {
|
||||
row.map { key ->
|
||||
val extraFlags = if (key.label.length > 2 && key.label.codePointCount(0, key.label.length) > 2 && !isEmoji(key.label))
|
||||
Key.LABEL_FLAGS_AUTO_X_SCALE
|
||||
else 0
|
||||
val keyData = key.compute(params)
|
||||
if (DebugFlags.DEBUG_ENABLED)
|
||||
Log.d(TAG, "adding key ${keyData.label}, ${keyData.code}")
|
||||
val keyParams = keyData.toKeyParams(params, keyWidth, defaultLabelFlags or extraFlags)
|
||||
paramsRow.add(keyParams)
|
||||
Log.d(TAG, "adding key ${key.label}, ${key.code}")
|
||||
key.toKeyParams(params, defaultLabelFlags or extraFlags)
|
||||
}
|
||||
if (spacerWidth != 0f) {
|
||||
paramsRow.add(KeyParams.newSpacer(params, spacerWidth))
|
||||
}
|
||||
functionalKeysRight.forEach { paramsRow.add(it) }
|
||||
keysInRows.add(paramsRow)
|
||||
}
|
||||
resizeLastRowIfNecessaryForAlignment(keysInRows)
|
||||
keysInRows.add(bottomRow)
|
||||
if (params.mId.mNumberRowEnabled)
|
||||
keysInRows.add(0, getNumberRow())
|
||||
return setReasonableWidths(baseKeyParams, functionalKeys)
|
||||
}
|
||||
|
||||
/** interprets key width -1, adjusts row size to nicely fit on screen, adds spacers if necessary */
|
||||
private fun setReasonableWidths(bassKeyParams: List<List<KeyParams>>, functionalKeys: List<Pair<List<KeyParams>, List<KeyParams>>>): ArrayList<ArrayList<KeyParams>> {
|
||||
val keysInRows = ArrayList<ArrayList<KeyParams>>()
|
||||
// expand width = -1 keys and make sure rows fit on screen, insert spacers if necessary
|
||||
bassKeyParams.forEachIndexed { i, keys ->
|
||||
val (functionalKeysLeft, functionalKeysRight) = functionalKeys[i]
|
||||
// sum up width, excluding -1 elements (put those in a separate list)
|
||||
val varWidthKeys = mutableListOf<KeyParams>()
|
||||
var totalWidth = 0f
|
||||
val allKeys = (functionalKeysLeft + keys + functionalKeysRight)
|
||||
allKeys.forEach {
|
||||
if (it.mWidth == -1f) varWidthKeys.add(it)
|
||||
else totalWidth += it.mWidth
|
||||
}
|
||||
|
||||
// set width for varWidthKeys
|
||||
if (varWidthKeys.isNotEmpty()) {
|
||||
val width = if (totalWidth + varWidthKeys.size * params.mDefaultKeyWidth > 1)
|
||||
params.mDefaultKeyWidth // never go below default width
|
||||
else (1f - totalWidth) / varWidthKeys.size // split remaining space evenly
|
||||
varWidthKeys.forEach { it.mWidth = width }
|
||||
|
||||
// re-calculate total width
|
||||
totalWidth = allKeys.sumOf { it.mWidth }
|
||||
}
|
||||
|
||||
// re-scale total width, or add spacers (or do nothing if totalWidth is near 1)
|
||||
if (totalWidth < 0.9999f) { // add spacers
|
||||
val spacerWidth = (1f - totalWidth) / 2
|
||||
val paramsRow = ArrayList<KeyParams>(functionalKeysLeft + KeyParams.newSpacer(params, spacerWidth) + keys +
|
||||
KeyParams.newSpacer(params, spacerWidth) + functionalKeysRight)
|
||||
keysInRows.add(paramsRow)
|
||||
} else {
|
||||
if (totalWidth > 1.0001f) { // re-scale total width
|
||||
val normalKeysWith = keys.sumOf { it.mWidth }
|
||||
val functionalKeysWidth = totalWidth - normalKeysWith
|
||||
val scaleFactor = (1f - functionalKeysWidth) / normalKeysWith
|
||||
// re-scale normal keys if factor is > 0.82, otherwise re-scale all keys
|
||||
if (scaleFactor > 0.82f) keys.forEach { it.mWidth *= scaleFactor }
|
||||
else allKeys.forEach { it.mWidth /= totalWidth }
|
||||
}
|
||||
keysInRows.add(ArrayList(allKeys))
|
||||
}
|
||||
}
|
||||
|
||||
// adjust last normal row key widths to be aligned with row above, assuming a reasonably close-to-default alpha / symbol layout
|
||||
// like in original layouts, e.g. for nordic and swiss layouts
|
||||
if (!params.mId.isAlphaOrSymbolKeyboard || bassKeyParams.size < 3 || bassKeyParams.last().isNotEmpty())
|
||||
return keysInRows
|
||||
val lastNormalRow = bassKeyParams[bassKeyParams.lastIndex - 1]
|
||||
val rowAboveLast = bassKeyParams[bassKeyParams.lastIndex - 2]
|
||||
val lastNormalRowKeyWidth = lastNormalRow.first().mWidth
|
||||
val rowAboveLastNormalRowKeyWidth = rowAboveLast.first().mWidth
|
||||
if (lastNormalRowKeyWidth <= rowAboveLastNormalRowKeyWidth + 0.0001f // no need
|
||||
|| lastNormalRowKeyWidth / rowAboveLastNormalRowKeyWidth > 1.1f // don't resize on large size difference
|
||||
|| lastNormalRow.any { it.isSpacer } || rowAboveLast.any { it.isSpacer } // annoying to deal with, and probably no resize wanted anyway
|
||||
|| lastNormalRow.any { it.mWidth != lastNormalRowKeyWidth } || rowAboveLast.any { it.mWidth != rowAboveLastNormalRowKeyWidth })
|
||||
return keysInRows
|
||||
val numberOfKeysInLast = lastNormalRow.count { it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL }
|
||||
val widthBefore = numberOfKeysInLast * lastNormalRowKeyWidth
|
||||
val widthAfter = numberOfKeysInLast * rowAboveLastNormalRowKeyWidth
|
||||
val spacerWidth = (widthBefore - widthAfter) / 2
|
||||
// resize keys
|
||||
lastNormalRow.forEach { if (it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL) it.mWidth = rowAboveLastNormalRowKeyWidth }
|
||||
// add spacers
|
||||
val lastNormalFullRow = keysInRows[keysInRows.lastIndex - 1]
|
||||
lastNormalFullRow.add(lastNormalFullRow.indexOfFirst { it == lastNormalRow.first() }, KeyParams.newSpacer(params, spacerWidth))
|
||||
lastNormalFullRow.add(lastNormalFullRow.indexOfLast { it == lastNormalRow.last() } + 1, KeyParams.newSpacer(params, spacerWidth))
|
||||
|
||||
return keysInRows
|
||||
}
|
||||
|
||||
/**
|
||||
* adds / removes keys to the bottom row
|
||||
* assumes a close-to-default bottom row consisting only of functional keys
|
||||
* does nothing if not isAlphaOrSymbolKeyboard or assumptions not met
|
||||
* adds an empty row to baseKeys, to have a baseKey row for the bottom functional row
|
||||
*/
|
||||
private fun adjustBottomFunctionalRowAndBaseKeys(allFunctionalKeys: MutableList<List<KeyData>>, baseKeys: MutableList<List<KeyData>>) {
|
||||
val functionalKeysBottom = allFunctionalKeys.lastOrNull()?.toMutableList() ?: return
|
||||
if (!params.mId.isAlphaOrSymbolKeyboard || functionalKeysBottom.isEmpty() || functionalKeysBottom.any { it.isKeyPlaceholder() })
|
||||
return
|
||||
if (true /* Settings.getInstance().current.mSingleFunctionalLayout */) { // todo with the customizable functional layout
|
||||
// remove unwanted keys (emoji, numpad, language switch)
|
||||
if (!Settings.getInstance().current.mShowsEmojiKey || !params.mId.isAlphabetKeyboard)
|
||||
functionalKeysBottom.removeFirst { it.label == KeyLabel.EMOJI }
|
||||
if (!Settings.getInstance().current.isLanguageSwitchKeyEnabled || !params.mId.isAlphabetKeyboard)
|
||||
functionalKeysBottom.removeFirst { it.label == KeyLabel.LANGUAGE_SWITCH }
|
||||
if (params.mId.mElementId != KeyboardId.ELEMENT_SYMBOLS)
|
||||
functionalKeysBottom.removeFirst { it.label == KeyLabel.NUMPAD }
|
||||
}
|
||||
// replace comma / period if 2 keys in normal bottom row
|
||||
if (baseKeys.last().size == 2) {
|
||||
Log.i("test", "$functionalKeysBottom")
|
||||
functionalKeysBottom.replaceFirst(
|
||||
{ it.label == KeyLabel.COMMA || it.groupId == KeyData.GROUP_COMMA},
|
||||
{ baseKeys.last()[0].copy(newGroupId = 1, newType = baseKeys.last()[0].type ?: it.type) }
|
||||
)
|
||||
functionalKeysBottom.replaceFirst(
|
||||
{ it.label == KeyLabel.PERIOD || it.groupId == KeyData.GROUP_PERIOD},
|
||||
{ baseKeys.last()[1].copy(newGroupId = 2, newType = baseKeys.last()[1].type ?: it.type) }
|
||||
)
|
||||
Log.i("test", "$functionalKeysBottom")
|
||||
baseKeys.removeLast()
|
||||
}
|
||||
// add those extra keys depending on layout (remove later)
|
||||
val spaceIndex = functionalKeysBottom.indexOfFirst { it.label == KeyLabel.SPACE && it.width <= 0 } // 0 or -1
|
||||
if (spaceIndex >= 0) {
|
||||
if (params.mLocaleKeyboardInfos.hasZwnjKey && params.mId.isAlphabetKeyboard) {
|
||||
// add zwnj key next to space
|
||||
functionalKeysBottom.add(spaceIndex + 1, TextKeyData(label = KeyLabel.ZWNJ))
|
||||
} else if (params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS) {
|
||||
// add / key next to space, todo (later): not any more, but keep it so this PR can be released without too many people complaining
|
||||
functionalKeysBottom.add(spaceIndex + 1, TextKeyData(label = "/", type = KeyType.FUNCTION))
|
||||
} else if (params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS_SHIFTED) {
|
||||
// add < and > keys next to space, todo (later): not any more, but keep it so this PR can be released without too many people complaining
|
||||
val key1 = TextKeyData(
|
||||
label = "<",
|
||||
popup = SimplePopups(listOf("!fixedColumnOrder!3", "‹", "≤", "«")),
|
||||
labelFlags = Key.LABEL_FLAGS_HAS_POPUP_HINT,
|
||||
type = KeyType.FUNCTION
|
||||
)
|
||||
val key2 = TextKeyData(
|
||||
label = ">",
|
||||
popup = SimplePopups(listOf("!fixedColumnOrder!3", "›", "≥", "»")),
|
||||
labelFlags = Key.LABEL_FLAGS_HAS_POPUP_HINT,
|
||||
type = KeyType.FUNCTION
|
||||
)
|
||||
functionalKeysBottom.add(spaceIndex + 1, key2)
|
||||
functionalKeysBottom.add(spaceIndex, key1)
|
||||
}
|
||||
}
|
||||
allFunctionalKeys[allFunctionalKeys.lastIndex] = functionalKeysBottom
|
||||
baseKeys.add(emptyList())
|
||||
}
|
||||
|
||||
// ideally we would get all functional keys in a nice list of pairs from the start, but at least it works...
|
||||
private fun getFunctionalKeysBySide(functionalKeysFromTop: List<KeyData>, functionalKeysFromBottom: List<KeyData>): Pair<List<KeyParams>, List<KeyParams>> {
|
||||
val (functionalKeysFromTopLeft, functionalKeysFromTopRight) = functionalKeysFromTop.splitAt { it.isKeyPlaceholder() }
|
||||
val (functionalKeysFromBottomLeft, functionalKeysFromBottomRight) = functionalKeysFromBottom.splitAt { it.isKeyPlaceholder() }
|
||||
// functional keys from top rows are the outermost, if there are some in the same row
|
||||
functionalKeysFromTopLeft.addAll(functionalKeysFromBottomLeft)
|
||||
functionalKeysFromBottomRight.addAll(functionalKeysFromTopRight)
|
||||
val functionalKeysLeft = functionalKeysFromTopLeft.mapNotNull { it.processFunctionalKeys()?.toKeyParams(params) }
|
||||
val functionalKeysRight = functionalKeysFromBottomRight.mapNotNull { it.processFunctionalKeys()?.toKeyParams(params) }
|
||||
return functionalKeysLeft to functionalKeysRight
|
||||
}
|
||||
|
||||
// this is not nice in here, but otherwise we'd need context, and defaultLabelFlags and infos for toKeyParams
|
||||
// improve it later, but currently this messy way is still ok
|
||||
private fun KeyData.processFunctionalKeys(): KeyData? {
|
||||
if (label == KeyLabel.PERIOD) {
|
||||
// todo: why defaultLabelFlags exactly here? is this for armenian or bengali period labels? try removing also check in holo theme
|
||||
return copy(newLabelFlags = labelFlags or defaultLabelFlags)
|
||||
}
|
||||
if (label == KeyLabel.SHIFT && !infos.hasShiftKey) return null
|
||||
if (label != KeyLabel.ACTION) return this
|
||||
return copy(
|
||||
// todo: evaluating the label should actually only happen in toKeyParams
|
||||
// this label change already makes it necessary to provide the background in here too, because toKeyParams can't use action as label
|
||||
newLabel = "${getActionKeyLabel()}|${getActionKeyCode()}",
|
||||
newPopup = popup.merge(getActionKeyPopupKeys()?.let { SimplePopups(it) }),
|
||||
// the label change is messing with toKeyParams, so we need to supply the appropriate BG type here
|
||||
newType = type ?: KeyType.ENTER_EDITING
|
||||
)
|
||||
}
|
||||
|
||||
private fun addNumberRowOrPopupKeys(baseKeys: MutableList<List<KeyData>>) {
|
||||
if (!params.mId.mNumberRowEnabled && params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS) {
|
||||
// replace first symbols row with number row, but use the labels as popupKeys
|
||||
|
@ -194,7 +353,7 @@ abstract class KeyboardParser(private val params: KeyboardParams, private val co
|
|||
else SimpleKeyboardParser(params, context, false)
|
||||
parser.parseCoreLayout(getLayoutFile(layoutName, context).readText())
|
||||
} else {
|
||||
SimpleKeyboardParser(params, context, false).parseCoreLayout(context.readAssetsFile("layouts/$layoutName.txt"))
|
||||
SimpleKeyboardParser(params, context, false).parseCoreLayout(context.readAssetsLayoutFile("$layoutName.txt"))
|
||||
}
|
||||
layout.forEachIndexed { i, row ->
|
||||
val baseRow = baseKeys.getOrNull(i) ?: return@forEachIndexed
|
||||
|
@ -204,34 +363,6 @@ abstract class KeyboardParser(private val params: KeyboardParams, private val co
|
|||
}
|
||||
}
|
||||
|
||||
// resize keys in last row if they are wider than keys in the row above
|
||||
// this is done so the keys align with the keys above, like in original layouts
|
||||
// e.g. for nordic and swiss layouts
|
||||
private fun resizeLastRowIfNecessaryForAlignment(keysInRows: ArrayList<ArrayList<KeyParams>>) {
|
||||
if (keysInRows.size < 3)
|
||||
return
|
||||
val lastRow = keysInRows.last()
|
||||
val rowAboveLast = keysInRows[keysInRows.lastIndex - 1]
|
||||
if (lastRow.any { it.isSpacer } || rowAboveLast.any { it.isSpacer })
|
||||
return // annoying to deal with, and probably no resize needed anyway
|
||||
val lastNormalRowKeyWidth = lastRow.first { it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL }.mRelativeWidth
|
||||
val rowAboveLastNormalRowKeyWidth = rowAboveLast.first { it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL }.mRelativeWidth
|
||||
if (lastNormalRowKeyWidth <= rowAboveLastNormalRowKeyWidth + 0.0001f)
|
||||
return // no need
|
||||
if (lastNormalRowKeyWidth / rowAboveLastNormalRowKeyWidth > 1.1f)
|
||||
return // don't resize on large size difference
|
||||
if (lastRow.any { it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL && it.mRelativeWidth != lastNormalRowKeyWidth })
|
||||
return // normal keys have different width, don't deal with this
|
||||
val numberOfNormalKeys = lastRow.count { it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL }
|
||||
val widthBefore = numberOfNormalKeys * lastNormalRowKeyWidth
|
||||
val widthAfter = numberOfNormalKeys * rowAboveLastNormalRowKeyWidth
|
||||
val spacerWidth = (widthBefore - widthAfter) / 2
|
||||
// resize keys and add spacers
|
||||
lastRow.forEach { if (it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL) it.mRelativeWidth = rowAboveLastNormalRowKeyWidth }
|
||||
lastRow.add(lastRow.indexOfFirst { it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL }, KeyParams.newSpacer(params, spacerWidth))
|
||||
lastRow.add(lastRow.indexOfLast { it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL } + 1, KeyParams.newSpacer(params, spacerWidth))
|
||||
}
|
||||
|
||||
private fun createNumericRows(baseKeys: MutableList<List<KeyData>>): ArrayList<ArrayList<KeyParams>> {
|
||||
val keysInRows = ArrayList<ArrayList<KeyParams>>()
|
||||
if (context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE && params.mId.mElementId != KeyboardId.ELEMENT_NUMPAD) {
|
||||
|
@ -245,16 +376,15 @@ abstract class KeyboardParser(private val params: KeyboardParams, private val co
|
|||
row.forEach { key ->
|
||||
var keyParams: KeyParams? = null
|
||||
// try parsing a functional key
|
||||
// todo: note that this is ignoring code on those keys, if any
|
||||
val functionalKeyName = when (key.label) {
|
||||
// todo (later): maybe add special popupKeys for phone and number layouts?
|
||||
"." -> if (params.mId.mElementId == KeyboardId.ELEMENT_NUMPAD) "period" else "."
|
||||
"," -> if (params.mId.mElementId == KeyboardId.ELEMENT_NUMPAD) "comma" else ","
|
||||
"." -> if (params.mId.mElementId == KeyboardId.ELEMENT_NUMPAD) KeyLabel.PERIOD else "."
|
||||
"," -> if (params.mId.mElementId == KeyboardId.ELEMENT_NUMPAD) KeyLabel.COMMA else ","
|
||||
else -> key.label
|
||||
}
|
||||
if (functionalKeyName.length > 1 && key.type != KeyType.NUMERIC) { // todo: why exception for numeric?
|
||||
if (functionalKeyName.length > 1 && key.type != KeyType.NUMERIC) {
|
||||
try {
|
||||
keyParams = getFunctionalKeyParams(functionalKeyName)
|
||||
keyParams = key.copy(newLabel = functionalKeyName).processFunctionalKeys()!!.toKeyParams(params)
|
||||
} catch (_: Throwable) {} // just use normal label
|
||||
}
|
||||
if (keyParams == null) {
|
||||
|
@ -264,11 +394,11 @@ abstract class KeyboardParser(private val params: KeyboardParams, private val co
|
|||
KeyboardId.ELEMENT_PHONE_SYMBOLS -> 0
|
||||
else -> Key.LABEL_FLAGS_FOLLOW_KEY_LARGE_LETTER_RATIO
|
||||
}
|
||||
key.compute(params).toKeyParams(params, 0.17f, labelFlags or defaultLabelFlags)
|
||||
key.toKeyParams(params, labelFlags or defaultLabelFlags)
|
||||
} else if (key.label.length == 1 && (params.mId.mElementId == KeyboardId.ELEMENT_PHONE || params.mId.mElementId == KeyboardId.ELEMENT_NUMBER))
|
||||
key.compute(params).toKeyParams(params, additionalLabelFlags = Key.LABEL_FLAGS_FOLLOW_KEY_LARGE_LETTER_RATIO or defaultLabelFlags)
|
||||
key.toKeyParams(params, additionalLabelFlags = Key.LABEL_FLAGS_FOLLOW_KEY_LARGE_LETTER_RATIO or defaultLabelFlags)
|
||||
else
|
||||
key.compute(params).toKeyParams(params, additionalLabelFlags = defaultLabelFlags)
|
||||
key.toKeyParams(params, additionalLabelFlags = defaultLabelFlags)
|
||||
}
|
||||
if (key.type != KeyType.NUMERIC && keyParams.mBackgroundType != Key.BACKGROUND_TYPE_ACTION)
|
||||
keyParams.mBackgroundType = Key.BACKGROUND_TYPE_FUNCTIONAL
|
||||
|
@ -296,256 +426,24 @@ abstract class KeyboardParser(private val params: KeyboardParams, private val co
|
|||
// make those keys same width as numeric keys except in numpad layout
|
||||
// but determine from row size instead of from elementId, in case user wants to adjust numpad layout
|
||||
if (row.size == baseKeys[0].size) {
|
||||
paramsRow.getOrNull(n - 1)?.mRelativeWidth = paramsRow[n].mRelativeWidth
|
||||
paramsRow.getOrNull(n + 1)?.mRelativeWidth = paramsRow[n].mRelativeWidth
|
||||
paramsRow.getOrNull(n - 1)?.mWidth = paramsRow[n].mWidth
|
||||
paramsRow.getOrNull(n + 1)?.mWidth = paramsRow[n].mWidth
|
||||
} else if (row.size == baseKeys[0].size + 2) {
|
||||
// numpad last row -> make sure the keys next to 0 fit nicely
|
||||
paramsRow.getOrNull(n - 1)?.mRelativeWidth = paramsRow[n].mRelativeWidth * 0.55f
|
||||
paramsRow.getOrNull(n - 2)?.mRelativeWidth = paramsRow[n].mRelativeWidth * 0.45f
|
||||
paramsRow.getOrNull(n + 1)?.mRelativeWidth = paramsRow[n].mRelativeWidth * 0.55f
|
||||
paramsRow.getOrNull(n + 2)?.mRelativeWidth = paramsRow[n].mRelativeWidth * 0.45f
|
||||
paramsRow.getOrNull(n - 1)?.mWidth = paramsRow[n].mWidth * 0.55f
|
||||
paramsRow.getOrNull(n - 2)?.mWidth = paramsRow[n].mWidth * 0.45f
|
||||
paramsRow.getOrNull(n + 1)?.mWidth = paramsRow[n].mWidth * 0.55f
|
||||
paramsRow.getOrNull(n + 2)?.mWidth = paramsRow[n].mWidth * 0.45f
|
||||
}
|
||||
}
|
||||
}
|
||||
val widthSum = paramsRow.sumOf { it.mRelativeWidth }
|
||||
paramsRow.forEach { it.mRelativeWidth /= widthSum }
|
||||
val widthSum = paramsRow.sumOf { it.mWidth }
|
||||
paramsRow.forEach { it.mWidth /= widthSum }
|
||||
keysInRows.add(paramsRow)
|
||||
}
|
||||
return keysInRows
|
||||
}
|
||||
|
||||
private fun parseFunctionalKeys(@StringRes id: Int): List<Pair<List<String>, List<String>>> =
|
||||
context.getString(id).split("\n").mapNotNull { line ->
|
||||
if (line.isBlank()) return@mapNotNull null
|
||||
val p = line.split(";")
|
||||
splitFunctionalKeyDefs(p.first()) to splitFunctionalKeyDefs(p.last())
|
||||
}
|
||||
|
||||
private fun splitFunctionalKeyDefs(def: String): List<String> {
|
||||
if (def.isBlank()) return emptyList()
|
||||
return def.split(",").filter { infos.hasShiftKey || !it.trim().startsWith("shift") }
|
||||
}
|
||||
|
||||
private fun getBottomRowAndAdjustBaseKeys(baseKeys: MutableList<List<KeyData>>): ArrayList<KeyParams> {
|
||||
val adjustableKeyCount = when (params.mId.mElementId) {
|
||||
KeyboardId.ELEMENT_SYMBOLS -> 3
|
||||
KeyboardId.ELEMENT_SYMBOLS_SHIFTED -> 4
|
||||
else -> 2 // must be alphabet, parser doesn't work for other elementIds
|
||||
}
|
||||
val adjustedKeys = if (baseKeys.last().size == adjustableKeyCount) baseKeys.last()
|
||||
else null
|
||||
if (adjustedKeys != null)
|
||||
baseKeys.removeLast()
|
||||
val bottomRow = ArrayList<KeyParams>()
|
||||
context.getString(R.string.key_def_bottom_row).split(",").forEach {
|
||||
val key = it.trim().splitOnWhitespace().first()
|
||||
val adjustKey = when (key) {
|
||||
"comma" -> adjustedKeys?.first()
|
||||
"period" -> adjustedKeys?.last()
|
||||
else -> null
|
||||
}
|
||||
val keyParams = getFunctionalKeyParams(it, adjustKey?.label, adjustKey?.popup?.getPopupKeyLabels(params))
|
||||
if (key == "space") { // add the extra keys around space
|
||||
if (params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS) {
|
||||
bottomRow.add(getFunctionalKeyParams(FunctionalKey.NUMPAD))
|
||||
bottomRow.add(keyParams)
|
||||
bottomRow.add(KeyParams(
|
||||
adjustedKeys?.get(1)?.label ?: "/",
|
||||
params,
|
||||
params.mDefaultRelativeKeyWidth,
|
||||
defaultLabelFlags,
|
||||
Key.BACKGROUND_TYPE_FUNCTIONAL,
|
||||
adjustedKeys?.get(1)?.popup
|
||||
))
|
||||
} else if (params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS_SHIFTED) {
|
||||
bottomRow.add(KeyParams(
|
||||
(adjustedKeys?.get(1)?.label ?: "<").rtlLabel(params),
|
||||
params,
|
||||
params.mDefaultRelativeKeyWidth,
|
||||
defaultLabelFlags or Key.LABEL_FLAGS_HAS_POPUP_HINT,
|
||||
Key.BACKGROUND_TYPE_FUNCTIONAL,
|
||||
adjustedKeys?.get(1)?.popup ?: SimplePopups(listOf("!fixedColumnOrder!3", "‹", "≤", "«"))
|
||||
))
|
||||
bottomRow.add(keyParams)
|
||||
bottomRow.add(KeyParams(
|
||||
(adjustedKeys?.get(2)?.label ?: ">").rtlLabel(params),
|
||||
params,
|
||||
params.mDefaultRelativeKeyWidth,
|
||||
defaultLabelFlags or Key.LABEL_FLAGS_HAS_POPUP_HINT,
|
||||
Key.BACKGROUND_TYPE_FUNCTIONAL,
|
||||
adjustedKeys?.get(2)?.popup ?: SimplePopups(listOf("!fixedColumnOrder!3", "›", "≥", "»"))
|
||||
))
|
||||
} else { // alphabet
|
||||
if (params.mId.mLanguageSwitchKeyEnabled)
|
||||
bottomRow.add(getFunctionalKeyParams(FunctionalKey.LANGUAGE_SWITCH))
|
||||
if (params.mId.mEmojiKeyEnabled)
|
||||
bottomRow.add(getFunctionalKeyParams(FunctionalKey.EMOJI))
|
||||
bottomRow.add(keyParams)
|
||||
if (params.mLocaleKeyboardInfos.hasZwnjKey)
|
||||
bottomRow.add(getFunctionalKeyParams(FunctionalKey.ZWNJ))
|
||||
}
|
||||
} else {
|
||||
bottomRow.add(keyParams)
|
||||
}
|
||||
}
|
||||
// set space width
|
||||
val space = bottomRow.first { it.mBackgroundType == Key.BACKGROUND_TYPE_SPACEBAR }
|
||||
space.mRelativeWidth = 1f - bottomRow.filter { it != space }.sumOf { it.mRelativeWidth }
|
||||
return bottomRow
|
||||
}
|
||||
|
||||
private fun getNumberRow(): ArrayList<KeyParams> =
|
||||
params.mLocaleKeyboardInfos.getNumberRow().mapTo(ArrayList()) {
|
||||
it.toKeyParams(params, additionalLabelFlags = Key.LABEL_FLAGS_DISABLE_HINT_LABEL or defaultLabelFlags)
|
||||
}
|
||||
|
||||
private fun getFunctionalKeyParams(def: String, label: String? = null, popupKeys: Collection<String>? = null): KeyParams {
|
||||
val split = def.trim().splitOnWhitespace()
|
||||
val key = FunctionalKey.valueOf(split[0].uppercase())
|
||||
val width = if (split.size == 2) split[1].substringBefore("%").toFloat() / 100f
|
||||
else params.mDefaultRelativeKeyWidth
|
||||
return getFunctionalKeyParams(key, width, label, popupKeys)
|
||||
}
|
||||
|
||||
private fun getFunctionalKeyParams(key: FunctionalKey, relativeWidth: Float? = null, label: String? = null, popupKeys: Collection<String>? = null): KeyParams {
|
||||
// for comma and period: label will override default, popupKeys will be appended
|
||||
val width = relativeWidth ?: params.mDefaultRelativeKeyWidth
|
||||
return when (key) {
|
||||
FunctionalKey.SYMBOL_ALPHA -> KeyParams(
|
||||
if (params.mId.isAlphabetKeyboard) getToSymbolLabel() else params.mLocaleKeyboardInfos.labelAlphabet,
|
||||
KeyCode.ALPHA_SYMBOL,
|
||||
params,
|
||||
width,
|
||||
Key.LABEL_FLAGS_PRESERVE_CASE or Key.LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR,
|
||||
Key.BACKGROUND_TYPE_FUNCTIONAL,
|
||||
null
|
||||
)
|
||||
FunctionalKey.SYMBOL -> KeyParams(
|
||||
getToSymbolLabel(),
|
||||
KeyCode.SYMBOL,
|
||||
params,
|
||||
width,
|
||||
Key.LABEL_FLAGS_PRESERVE_CASE or Key.LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR,
|
||||
Key.BACKGROUND_TYPE_FUNCTIONAL,
|
||||
null
|
||||
)
|
||||
FunctionalKey.ALPHA -> KeyParams(
|
||||
params.mLocaleKeyboardInfos.labelAlphabet,
|
||||
KeyCode.ALPHA,
|
||||
params,
|
||||
width,
|
||||
Key.LABEL_FLAGS_PRESERVE_CASE or Key.LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR,
|
||||
Key.BACKGROUND_TYPE_FUNCTIONAL,
|
||||
null
|
||||
)
|
||||
FunctionalKey.COMMA -> KeyParams(
|
||||
label ?: getCommaLabel(),
|
||||
params,
|
||||
width,
|
||||
Key.LABEL_FLAGS_HAS_POPUP_HINT, // previously only if normal comma, but always is more correct
|
||||
if (label?.first()?.isLetter() == true) Key.BACKGROUND_TYPE_NORMAL // mimic behavior of old dvorak and halmak layouts
|
||||
else Key.BACKGROUND_TYPE_FUNCTIONAL,
|
||||
SimplePopups(popupKeys?.let { getCommaPopupKeys() + it } ?: getCommaPopupKeys())
|
||||
)
|
||||
FunctionalKey.PERIOD -> KeyParams(
|
||||
label ?: getPeriodLabel(),
|
||||
params,
|
||||
width,
|
||||
Key.LABEL_FLAGS_HAS_POPUP_HINT or defaultLabelFlags,
|
||||
if (label?.first()?.isLetter() == true) Key.BACKGROUND_TYPE_NORMAL
|
||||
else Key.BACKGROUND_TYPE_FUNCTIONAL,
|
||||
SimplePopups(popupKeys?.let { getPunctuationPopupKeys() + it } ?: getPunctuationPopupKeys())
|
||||
)
|
||||
FunctionalKey.SPACE -> KeyParams(
|
||||
getSpaceLabel(),
|
||||
params,
|
||||
width, // will not be used for normal space (only in number layouts)
|
||||
if (params.mId.isNumberLayout) Key.LABEL_FLAGS_ALIGN_ICON_TO_BOTTOM else 0,
|
||||
Key.BACKGROUND_TYPE_SPACEBAR,
|
||||
null
|
||||
)
|
||||
FunctionalKey.ACTION -> KeyParams(
|
||||
"${getActionKeyLabel()}|${getActionKeyCode()}",
|
||||
params,
|
||||
width,
|
||||
Key.LABEL_FLAGS_PRESERVE_CASE
|
||||
or Key.LABEL_FLAGS_AUTO_X_SCALE
|
||||
or Key.LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO
|
||||
or Key.LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR
|
||||
or Key.LABEL_FLAGS_HAS_POPUP_HINT
|
||||
or KeyboardTheme.getThemeActionAndEmojiKeyLabelFlags(params.mThemeId),
|
||||
Key.BACKGROUND_TYPE_ACTION,
|
||||
getActionKeyPopupKeys()?.let { SimplePopups(it) }
|
||||
)
|
||||
FunctionalKey.DELETE -> KeyParams(
|
||||
"!icon/delete_key|!code/key_delete",
|
||||
params,
|
||||
width,
|
||||
0,
|
||||
Key.BACKGROUND_TYPE_FUNCTIONAL,
|
||||
null
|
||||
)
|
||||
FunctionalKey.SHIFT -> KeyParams(
|
||||
"${getShiftLabel()}|!code/key_shift",
|
||||
params,
|
||||
width,
|
||||
Key.LABEL_FLAGS_PRESERVE_CASE or if (!params.mId.isAlphabetKeyboard) Key.LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR else 0,
|
||||
// todo (later): possibly the whole stickyOn/Off stuff can be removed, currently it should only have a very slight effect in holo
|
||||
if (params.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED || params.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED)
|
||||
Key.BACKGROUND_TYPE_STICKY_ON
|
||||
else Key.BACKGROUND_TYPE_STICKY_OFF,
|
||||
if (params.mId.isAlphabetKeyboard) SimplePopups(listOf("!noPanelAutoPopupKey!", " |!code/key_capslock")) else null // why the alphabet popup keys actually?
|
||||
)
|
||||
FunctionalKey.EMOJI -> KeyParams(
|
||||
"!icon/emoji_normal_key|!code/key_emoji",
|
||||
params,
|
||||
width,
|
||||
KeyboardTheme.getThemeActionAndEmojiKeyLabelFlags(params.mThemeId),
|
||||
Key.BACKGROUND_TYPE_FUNCTIONAL,
|
||||
null
|
||||
)
|
||||
// tablet layout has an emoji key that changes to com key in url / mail
|
||||
FunctionalKey.EMOJI_COM -> if (params.mId.mMode == KeyboardId.MODE_URL || params.mId.mMode == KeyboardId.MODE_EMAIL)
|
||||
getFunctionalKeyParams(FunctionalKey.COM, width)
|
||||
else getFunctionalKeyParams(FunctionalKey.EMOJI, width)
|
||||
FunctionalKey.COM -> KeyParams(
|
||||
// todo (later): label and popupKeys could be in localeKeyTexts, handled similar to currency key
|
||||
// better not in the text files, because it should be handled per country
|
||||
".com",
|
||||
params,
|
||||
width,
|
||||
Key.LABEL_FLAGS_AUTO_X_SCALE or Key.LABEL_FLAGS_FONT_NORMAL or Key.LABEL_FLAGS_HAS_POPUP_HINT or Key.LABEL_FLAGS_PRESERVE_CASE,
|
||||
Key.BACKGROUND_TYPE_FUNCTIONAL,
|
||||
SimplePopups(listOf(Key.POPUP_KEYS_HAS_LABELS, ".net", ".org", ".gov", ".edu"))
|
||||
)
|
||||
FunctionalKey.LANGUAGE_SWITCH -> KeyParams(
|
||||
"!icon/language_switch_key|!code/key_language_switch",
|
||||
params,
|
||||
width,
|
||||
0,
|
||||
Key.BACKGROUND_TYPE_FUNCTIONAL,
|
||||
null
|
||||
)
|
||||
FunctionalKey.NUMPAD -> KeyParams(
|
||||
"!icon/numpad_key|!code/key_numpad",
|
||||
params,
|
||||
width,
|
||||
0,
|
||||
Key.BACKGROUND_TYPE_FUNCTIONAL,
|
||||
null
|
||||
)
|
||||
FunctionalKey.ZWNJ -> KeyParams(
|
||||
"!icon/zwnj_key|\u200C",
|
||||
params,
|
||||
width,
|
||||
Key.LABEL_FLAGS_HAS_POPUP_HINT,
|
||||
// this may not be a good place to make this choice, but probably it's fine (though reading from settings here is not good)
|
||||
if (Settings.getInstance().current.mColors.hasKeyBorders) Key.BACKGROUND_TYPE_SPACEBAR else Key.BACKGROUND_TYPE_NORMAL,
|
||||
SimplePopups(listOf("!icon/zwj_key|\u200D"))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getActionKeyLabel(): String {
|
||||
if (params.mId.isMultiLine && (params.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED || params.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED))
|
||||
return "!icon/enter_key"
|
||||
|
@ -627,7 +525,7 @@ abstract class KeyboardParser(private val params: KeyboardParams, private val co
|
|||
}
|
||||
// remove emoji shortcut on enter in tablet mode (like original, because bottom row always has an emoji key)
|
||||
// (probably not necessary, but whatever)
|
||||
if (isTablet() && popupKeys.remove("!icon/emoji_action_key|!code/key_emoji")) {
|
||||
if (Settings.getInstance().isTablet && popupKeys.remove("!icon/emoji_action_key|!code/key_emoji")) {
|
||||
val i = popupKeys.indexOfFirst { it.startsWith(Key.POPUP_KEYS_FIXED_COLUMN_ORDER) }
|
||||
if (i > -1) {
|
||||
val n = popupKeys[i].substringAfter(Key.POPUP_KEYS_FIXED_COLUMN_ORDER).toIntOrNull()
|
||||
|
@ -662,84 +560,6 @@ abstract class KeyboardParser(private val params: KeyboardParams, private val co
|
|||
return runInLocale(context, locale) { it.getString(id) }
|
||||
}
|
||||
|
||||
private fun getToSymbolLabel() =
|
||||
if (params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS || params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS_SHIFTED)
|
||||
params.mLocaleKeyboardInfos.labelAlphabet
|
||||
else params.mLocaleKeyboardInfos.labelSymbol
|
||||
|
||||
private fun getShiftLabel(): String {
|
||||
val elementId = params.mId.mElementId
|
||||
if (elementId == KeyboardId.ELEMENT_SYMBOLS_SHIFTED)
|
||||
return params.mLocaleKeyboardInfos.labelSymbol
|
||||
if (elementId == KeyboardId.ELEMENT_SYMBOLS)
|
||||
return params.mLocaleKeyboardInfos.getShiftSymbolLabel(isTablet())
|
||||
if (elementId == KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED || elementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED
|
||||
|| elementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED || elementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED)
|
||||
return "!icon/shift_key_shifted"
|
||||
return "!icon/shift_key"
|
||||
}
|
||||
|
||||
private fun getPeriodLabel(): String {
|
||||
if (params.mId.isNumberLayout) return "."
|
||||
if (params.mId.isAlphabetKeyboard || params.mId.locale.language in listOf("ar", "fa")) // todo: this exception is not so great...
|
||||
return params.mLocaleKeyboardInfos.labelPeriod
|
||||
return "."
|
||||
}
|
||||
|
||||
private fun getCommaLabel(): String {
|
||||
if (params.mId.mMode == KeyboardId.MODE_URL && params.mId.isAlphabetKeyboard)
|
||||
return "/"
|
||||
if (params.mId.mMode == KeyboardId.MODE_EMAIL && params.mId.isAlphabetKeyboard)
|
||||
return "\\@"
|
||||
if (params.mId.isNumberLayout)
|
||||
return ","
|
||||
return params.mLocaleKeyboardInfos.labelComma
|
||||
}
|
||||
|
||||
private fun getCommaPopupKeys(): List<String> {
|
||||
val keys = mutableListOf<String>()
|
||||
if (!params.mId.mDeviceLocked)
|
||||
keys.add("!icon/clipboard_normal_key|!code/key_clipboard")
|
||||
if (!params.mId.mEmojiKeyEnabled && !params.mId.isNumberLayout)
|
||||
keys.add("!icon/emoji_normal_key|!code/key_emoji")
|
||||
if (!params.mId.mLanguageSwitchKeyEnabled)
|
||||
keys.add("!icon/language_switch_key|!code/key_language_switch")
|
||||
if (!params.mId.mOneHandedModeEnabled)
|
||||
keys.add("!icon/start_onehanded_mode_key|!code/key_start_onehanded")
|
||||
if (!params.mId.mDeviceLocked)
|
||||
keys.add("!icon/settings_key|!code/key_settings")
|
||||
return keys
|
||||
}
|
||||
|
||||
private fun getPunctuationPopupKeys(): List<String> {
|
||||
if (params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS || params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS_SHIFTED)
|
||||
return listOf("…")
|
||||
if (params.mId.isNumberLayout)
|
||||
return listOf(":", "…", ";", "∞", "π", "√", "°", "^")
|
||||
val popupKeys = params.mLocaleKeyboardInfos.getPopupKeys("punctuation")!!.toMutableList()
|
||||
if (params.mId.mSubtype.isRtlSubtype) {
|
||||
for (i in popupKeys.indices)
|
||||
popupKeys[i] = popupKeys[i].rtlLabel(params) // for parentheses
|
||||
}
|
||||
if (isTablet() && popupKeys.contains("!") && popupKeys.contains("?")) {
|
||||
// remove ! and ? keys and reduce number in autoColumnOrder
|
||||
// this makes use of removal of empty popupKeys in PopupKeySpec.insertAdditionalPopupKeys
|
||||
popupKeys[popupKeys.indexOf("!")] = ""
|
||||
popupKeys[popupKeys.indexOf("?")] = ""
|
||||
val columns = popupKeys[0].substringAfter(Key.POPUP_KEYS_AUTO_COLUMN_ORDER).toIntOrNull()
|
||||
if (columns != null)
|
||||
popupKeys[0] = "${Key.POPUP_KEYS_AUTO_COLUMN_ORDER}${columns - 1}"
|
||||
}
|
||||
return popupKeys
|
||||
}
|
||||
|
||||
private fun getSpaceLabel(): String =
|
||||
if (params.mId.mElementId <= KeyboardId.ELEMENT_SYMBOLS_SHIFTED)
|
||||
"!icon/space_key|!code/key_space"
|
||||
else "!icon/space_key_for_number_layout|!code/key_space"
|
||||
|
||||
private fun isTablet() = context.resources.getInteger(R.integer.config_screen_metrics) >= 3
|
||||
|
||||
companion object {
|
||||
private const val TAG = "KeyboardParser"
|
||||
|
||||
|
@ -755,15 +575,15 @@ abstract class KeyboardParser(private val params: KeyboardParams, private val co
|
|||
}
|
||||
val layoutFileNames = context.assets.list("layouts")!!
|
||||
if (layoutFileNames.contains("$layoutName.json")) {
|
||||
return JsonKeyboardParser(params, context).parseLayoutString(context.readAssetsFile("layouts${File.separator}$layoutName.json"))
|
||||
return JsonKeyboardParser(params, context).parseLayoutString(context.readAssetsLayoutFile("$layoutName.json"))
|
||||
}
|
||||
if (layoutFileNames.contains("$layoutName.txt")) {
|
||||
return SimpleKeyboardParser(params, context).parseLayoutString(context.readAssetsFile("layouts${File.separator}$layoutName.txt"))
|
||||
return SimpleKeyboardParser(params, context).parseLayoutString(context.readAssetsLayoutFile("$layoutName.txt"))
|
||||
}
|
||||
throw IllegalStateException("can't parse layout $layoutName with id ${params.mId} and elementId ${params.mId.mElementId}")
|
||||
}
|
||||
|
||||
private fun Context.readAssetsFile(name: String) = assets.open(name).reader().readText()
|
||||
private fun Context.readAssetsLayoutFile(name: String) = assets.open("layouts${File.separator}$name").reader().readText()
|
||||
|
||||
private fun getLayoutFileName(params: KeyboardParams, context: Context, overrideElementId: Int? = null): String {
|
||||
var checkForCustom = true
|
||||
|
@ -815,10 +635,6 @@ abstract class KeyboardParser(private val params: KeyboardParams, private val co
|
|||
}
|
||||
}
|
||||
|
||||
protected enum class FunctionalKey {
|
||||
EMOJI, LANGUAGE_SWITCH, COM, EMOJI_COM, ACTION, DELETE, PERIOD, COMMA, SPACE, SHIFT, NUMPAD, SYMBOL, ALPHA, SYMBOL_ALPHA, ZWNJ
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// todo: actually this should be in some separate file
|
||||
|
|
|
@ -14,7 +14,7 @@ object KeyCode {
|
|||
|
||||
const val INTERNAL_FLORIS_MIN = -9999
|
||||
const val INTERNAL_FLORIS_MAX = -1
|
||||
val INTERNAL_FLORIS = INTERNAL_FLORIS_MIN..INTERNAL_FLORIS_MAX
|
||||
val INTERNAL_FLORIS = INTERNAL_FLORIS_MIN..INTERNAL_FLORIS_MAX // do NOT add key codes in this range
|
||||
val INTERNAL_HELI = -19999..-10000 // for keys exclusive to this app
|
||||
val CURRENCY = CURRENCY_SLOT_6..CURRENCY_SLOT_1
|
||||
}
|
||||
|
@ -122,7 +122,7 @@ object KeyCode {
|
|||
const val CJK_SPACE = 12288
|
||||
|
||||
// heliboard only codes
|
||||
const val ALPHA_SYMBOL = -10001
|
||||
const val SYMBOL_ALPHA = -10001
|
||||
const val START_ONE_HANDED_MODE = -10002
|
||||
const val STOP_ONE_HANDED_MODE = -10003
|
||||
const val SWITCH_ONE_HANDED_MODE = -10004
|
||||
|
@ -132,8 +132,8 @@ object KeyCode {
|
|||
// Code value representing the code is not specified.
|
||||
const val NOT_SPECIFIED = -10008 // todo: not sure if there is need to have the "old" unspecified keyCode different, just test it and maybe merge
|
||||
const val CLIPBOARD_COPY_ALL = -10009
|
||||
const val PAGE_UP = -10010
|
||||
const val PAGE_DOWN = -10011
|
||||
const val PAGE_UP = -10010
|
||||
const val PAGE_DOWN = -10011
|
||||
|
||||
/** to make sure a FlorisBoard code works when reading a JSON layout */
|
||||
fun Int.checkAndConvertCode(): Int = if (this > 0) this else when (this) {
|
||||
|
@ -145,8 +145,8 @@ object KeyCode {
|
|||
SHIFT, CAPS_LOCK, MULTIPLE_CODE_POINTS, UNSPECIFIED,
|
||||
|
||||
// heliboard only
|
||||
ALPHA_SYMBOL, START_ONE_HANDED_MODE, STOP_ONE_HANDED_MODE, SWITCH_ONE_HANDED_MODE, SHIFT_ENTER,
|
||||
ACTION_NEXT, ACTION_PREVIOUS, NOT_SPECIFIED
|
||||
SYMBOL_ALPHA, START_ONE_HANDED_MODE, STOP_ONE_HANDED_MODE, SWITCH_ONE_HANDED_MODE, SHIFT_ENTER,
|
||||
ACTION_NEXT, ACTION_PREVIOUS, NOT_SPECIFIED, CLIPBOARD_COPY_ALL, PAGE_UP, PAGE_DOWN
|
||||
-> this
|
||||
|
||||
// conversion
|
||||
|
@ -156,26 +156,4 @@ object KeyCode {
|
|||
|
||||
else -> throw IllegalStateException("key code $this not yet supported")
|
||||
}
|
||||
|
||||
/** to make sure a FlorisBoard label works when reading a JSON layout */
|
||||
// resulting special labels should be names of FunctionalKey enum, case insensitive
|
||||
fun String.convertFlorisLabel(): String = when (this) {
|
||||
"view_characters" -> "alpha"
|
||||
"view_symbols" -> "symbol"
|
||||
"view_numeric_advanced" -> "numpad"
|
||||
"view_phone" -> "alpha" // phone keyboard is treated like alphabet, just with different layout
|
||||
"view_phone2" -> "symbols" // phone symbols
|
||||
"ime_ui_mode_media" -> "emoji"
|
||||
"ime_ui_mode_clipboard" -> "clipboard" // todo: is this supported? when yes -> add to readme, and add a test
|
||||
"ime_ui_mode_text" -> "alpha"
|
||||
"currency_slot_1" -> "$$$"
|
||||
"currency_slot_2" -> "$$$1"
|
||||
"currency_slot_3" -> "$$$2"
|
||||
"currency_slot_4" -> "$$$3"
|
||||
"currency_slot_5" -> "$$$4"
|
||||
"currency_slot_6" -> "$$$5"
|
||||
"enter" -> "action"
|
||||
"half_space" -> "zwnj"
|
||||
else -> this
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -21,18 +21,16 @@ import kotlinx.serialization.encoding.Encoder
|
|||
*/
|
||||
@Serializable(with = KeyTypeSerializer::class)
|
||||
enum class KeyType {
|
||||
// todo: implement the effect on background
|
||||
// also, how to get that specific space bar background?
|
||||
CHARACTER, // default
|
||||
ENTER_EDITING, // enter/insert/delete, gets functional key background (if not action key)
|
||||
ENTER_EDITING, // should be enter/insert/delete, but always gets action key background
|
||||
FUNCTION, // f1..., gets functional key background
|
||||
LOCK, // scroll lock, num lock, caps lock, gets functional key background
|
||||
LOCK, // scroll lock, num lock, caps lock, gets sticky on/off background, which currently is the same as functional background
|
||||
MODIFIER, // alt, ctrl, shift, gets functional key background
|
||||
NAVIGATION, // home, page up, page down, tab, arrows, geta default background
|
||||
SYSTEM_GUI, // esc, print, pause, meta, (keyboard layout switch), geta functional background
|
||||
NUMERIC, // numpad keys, get larger letter and larger width
|
||||
PLACEHOLDER, // other keys go here, e.g. in shift, placeholder, delete the placeholder gets (typically) replaced by the bottom keyboard row
|
||||
UNSPECIFIED; // treated like default
|
||||
NAVIGATION, // home, page up, page down, tab, arrows, gets space background because it'S still the most suitable type
|
||||
SYSTEM_GUI, // esc, print, pause, meta, (keyboard layout switch), gets functional background
|
||||
NUMERIC, // numpad keys, get larger letter and larger width in number layouts, and default background
|
||||
PLACEHOLDER, // spacer, or actual placeholder when used in functional key layouts
|
||||
UNSPECIFIED; // empty background
|
||||
|
||||
override fun toString(): String {
|
||||
return super.toString().lowercase()
|
||||
|
@ -40,7 +38,13 @@ enum class KeyType {
|
|||
|
||||
companion object {
|
||||
fun fromString(string: String): KeyType {
|
||||
return valueOf(string.uppercase())
|
||||
// resolve alternative names
|
||||
return when (string) {
|
||||
"space" -> NAVIGATION
|
||||
"action" -> ENTER_EDITING
|
||||
"shift" -> LOCK
|
||||
else -> valueOf(string.uppercase())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,30 +6,49 @@
|
|||
package helium314.keyboard.keyboard.internal.keyboard_parser.floris
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import helium314.keyboard.keyboard.internal.KeySpecParser
|
||||
import helium314.keyboard.keyboard.internal.KeyboardParams
|
||||
import helium314.keyboard.latin.utils.addCollections
|
||||
|
||||
// only the constructor and name remain from FlorisBoard
|
||||
// we don't care about the difference between main and relevant (at least for now)
|
||||
@Serializable
|
||||
open class PopupSet<T : AbstractKeyData>(
|
||||
open val main: T? = null,
|
||||
open val relevant: List<T>? = null
|
||||
open val relevant: Collection<T>? = null
|
||||
) {
|
||||
// get labels of all popup keys
|
||||
open fun getPopupKeyLabels(params: KeyboardParams): Collection<String>? {
|
||||
if (main == null && relevant == null) return null
|
||||
val popupKeys = mutableListOf<String>()
|
||||
main?.getPopupLabel(params)?.let { popupKeys.add(it) }
|
||||
relevant?.let { popupKeys.addAll(it.map { it.getPopupLabel(params) }) }
|
||||
main?.compute(params)?.getPopupLabel(params)?.let { popupKeys.add(it) }
|
||||
relevant?.let { popupKeys.addAll(it.mapNotNull { it.compute(params)?.getPopupLabel(params) }) }
|
||||
if (popupKeys.isEmpty()) return null
|
||||
return popupKeys
|
||||
}
|
||||
open fun isEmpty(): Boolean = main == null && relevant.isNullOrEmpty()
|
||||
|
||||
var numberIndex: Int? = null
|
||||
var symbol: String? = null // maybe list of keys?
|
||||
|
||||
fun <U : AbstractKeyData> merge(other: PopupSet<U>?): PopupSet<out AbstractKeyData> {
|
||||
if (other == null || other.isEmpty()) return this
|
||||
if (this.isEmpty()) return other
|
||||
if (this is SimplePopups) {
|
||||
if (other is SimplePopups)
|
||||
return SimplePopups(addCollections(popupKeys, other.popupKeys))
|
||||
return PopupSet(other.main, addCollections(popupKeys?.map { it.toTextKey() }, other.relevant))
|
||||
} else if (other is SimplePopups) {
|
||||
return PopupSet(main, addCollections(relevant, other.popupKeys?.map { it.toTextKey() }))
|
||||
}
|
||||
val newMain = if (main == null) other.main else main
|
||||
val newRelevant = addCollections(relevant, other.relevant)
|
||||
if (main != null && other.main != null)
|
||||
return PopupSet(newMain, addCollections(listOf(other.main!!), newRelevant))
|
||||
return PopupSet(newMain, newRelevant)
|
||||
}
|
||||
}
|
||||
|
||||
class SimplePopups(val popupKeys: Collection<String>?) : PopupSet<AbstractKeyData>() {
|
||||
override fun getPopupKeyLabels(params: KeyboardParams) = popupKeys
|
||||
override fun isEmpty(): Boolean = popupKeys.isNullOrEmpty()
|
||||
}
|
||||
|
|
|
@ -9,12 +9,16 @@ import kotlinx.serialization.SerialName
|
|||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
import helium314.keyboard.keyboard.Key
|
||||
import helium314.keyboard.keyboard.KeyboardId
|
||||
import helium314.keyboard.keyboard.KeyboardTheme
|
||||
import helium314.keyboard.keyboard.internal.KeyboardIconsSet
|
||||
import helium314.keyboard.keyboard.internal.KeyboardParams
|
||||
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode.checkAndConvertCode
|
||||
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode.convertFlorisLabel
|
||||
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyLabel.convertFlorisLabel
|
||||
import helium314.keyboard.keyboard.internal.keyboard_parser.rtlLabel
|
||||
import helium314.keyboard.latin.common.Constants
|
||||
import helium314.keyboard.latin.common.StringUtils
|
||||
import helium314.keyboard.latin.settings.Settings
|
||||
|
||||
// taken from FlorisBoard, small modifications (see also KeyData)
|
||||
// internal keys removed (currently no plan to support them)
|
||||
|
@ -32,13 +36,17 @@ import helium314.keyboard.latin.common.StringUtils
|
|||
* @property popup The popups for ths key. Can also dynamically be provided via popup extensions.
|
||||
*/
|
||||
sealed interface KeyData : AbstractKeyData {
|
||||
val type: KeyType
|
||||
val type: KeyType?
|
||||
val code: Int
|
||||
val label: String
|
||||
val groupId: Int
|
||||
val popup: PopupSet<AbstractKeyData> // not nullable because can't add number otherwise
|
||||
val popup: PopupSet<out AbstractKeyData> // not nullable because can't add number otherwise
|
||||
val width: Float // in percent of keyboard width, 0 is default (depends on key), -1 is fill (like space bar)
|
||||
val labelFlags: Int
|
||||
|
||||
fun copy(newType: KeyType? = type, newCode: Int = code, newLabel: String = label, newGroupId: Int = groupId,
|
||||
newPopup: PopupSet<out AbstractKeyData> = popup, newWidth: Float = width, newLabelFlags: Int = labelFlags): KeyData
|
||||
|
||||
// groups (currently) not supported
|
||||
companion object {
|
||||
/**
|
||||
|
@ -49,15 +57,15 @@ sealed interface KeyData : AbstractKeyData {
|
|||
|
||||
/**
|
||||
* Constant for the Left modifier key group. Any key belonging to this group will get the
|
||||
* popups specified for "~left" in the popup mapping.
|
||||
* popups specified for the comma key.
|
||||
*/
|
||||
const val GROUP_LEFT: Int = 1
|
||||
const val GROUP_COMMA: Int = 1
|
||||
|
||||
/**
|
||||
* Constant for the right modifier key group. Any key belonging to this group will get the
|
||||
* popups specified for "~right" in the popup mapping.
|
||||
* popups specified for the period key.
|
||||
*/
|
||||
const val GROUP_RIGHT: Int = 2
|
||||
const val GROUP_PERIOD: Int = 2
|
||||
|
||||
/**
|
||||
* Constant for the enter modifier key group. Any key belonging to this group will get the
|
||||
|
@ -70,100 +78,276 @@ sealed interface KeyData : AbstractKeyData {
|
|||
* popups specified for "~kana" in the popup mapping.
|
||||
*/
|
||||
const val GROUP_KANA: Int = 97
|
||||
|
||||
private fun getShiftLabel(params: KeyboardParams) = when (params.mId.mElementId) {
|
||||
KeyboardId.ELEMENT_SYMBOLS_SHIFTED -> params.mLocaleKeyboardInfos.labelSymbol
|
||||
KeyboardId.ELEMENT_SYMBOLS -> params.mLocaleKeyboardInfos.getShiftSymbolLabel(Settings.getInstance().isTablet)
|
||||
KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED, KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED,
|
||||
KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED, KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED -> "!icon/${KeyboardIconsSet.NAME_SHIFT_KEY_SHIFTED}"
|
||||
else -> "!icon/${KeyboardIconsSet.NAME_SHIFT_KEY}"
|
||||
}
|
||||
|
||||
// todo (later): try avoiding this weirdness
|
||||
// maybe just remove it and if users want it they can use custom functional layouts?
|
||||
// but it has been like this "forever" and actually seems to make sense
|
||||
private fun getPeriodLabel(params: KeyboardParams): String {
|
||||
if (params.mId.isNumberLayout) return "."
|
||||
if (params.mId.isAlphabetKeyboard || params.mId.locale.language in listOf("ar", "fa"))
|
||||
return params.mLocaleKeyboardInfos.labelPeriod
|
||||
return "."
|
||||
}
|
||||
|
||||
private fun getSpaceLabel(params: KeyboardParams): String =
|
||||
if (params.mId.mElementId <= KeyboardId.ELEMENT_SYMBOLS_SHIFTED)
|
||||
"!icon/space_key|!code/key_space"
|
||||
else "!icon/space_key_for_number_layout|!code/key_space"
|
||||
|
||||
private fun getCommaPopupKeys(params: KeyboardParams): List<String> {
|
||||
val keys = mutableListOf<String>()
|
||||
if (!params.mId.mDeviceLocked)
|
||||
keys.add("!icon/clipboard_normal_key|!code/key_clipboard")
|
||||
if (!params.mId.mEmojiKeyEnabled && !params.mId.isNumberLayout)
|
||||
keys.add("!icon/emoji_normal_key|!code/key_emoji")
|
||||
if (!params.mId.mLanguageSwitchKeyEnabled)
|
||||
keys.add("!icon/language_switch_key|!code/key_language_switch")
|
||||
if (!params.mId.mOneHandedModeEnabled)
|
||||
keys.add("!icon/start_onehanded_mode_key|!code/key_start_onehanded")
|
||||
if (!params.mId.mDeviceLocked)
|
||||
keys.add("!icon/settings_key|!code/key_settings")
|
||||
return keys
|
||||
}
|
||||
|
||||
private fun getPunctuationPopupKeys(params: KeyboardParams): List<String> {
|
||||
if (params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS || params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS_SHIFTED)
|
||||
return listOf("…")
|
||||
if (params.mId.isNumberLayout)
|
||||
return listOf(":", "…", ";", "∞", "π", "√", "°", "^")
|
||||
val popupKeys = params.mLocaleKeyboardInfos.getPopupKeys("punctuation")!!.toMutableList()
|
||||
if (params.mId.mSubtype.isRtlSubtype) {
|
||||
for (i in popupKeys.indices)
|
||||
popupKeys[i] = popupKeys[i].rtlLabel(params) // for parentheses
|
||||
}
|
||||
if (Settings.getInstance().isTablet && popupKeys.contains("!") && popupKeys.contains("?")) {
|
||||
// remove ! and ? keys and reduce number in autoColumnOrder
|
||||
// this makes use of removal of empty popupKeys in PopupKeySpec.insertAdditionalPopupKeys
|
||||
popupKeys[popupKeys.indexOf("!")] = ""
|
||||
popupKeys[popupKeys.indexOf("?")] = ""
|
||||
val columns = popupKeys[0].substringAfter(Key.POPUP_KEYS_AUTO_COLUMN_ORDER).toIntOrNull()
|
||||
if (columns != null)
|
||||
popupKeys[0] = "${Key.POPUP_KEYS_AUTO_COLUMN_ORDER}${columns - 1}"
|
||||
}
|
||||
return popupKeys
|
||||
}
|
||||
}
|
||||
|
||||
// make it non-nullable for simplicity, and to reflect current implementations
|
||||
override fun compute(params: KeyboardParams): KeyData {
|
||||
require(groupId <= GROUP_ENTER) { "only groups up to GROUP_ENTER are supported" }
|
||||
require(label.isNotEmpty() || type == KeyType.PLACEHOLDER || code != KeyCode.UNSPECIFIED) { "non-placeholder key has no code and no label" }
|
||||
val newLabel = label.convertFlorisLabel()
|
||||
val newCode = code.checkAndConvertCode()
|
||||
|
||||
// resolve currency keys
|
||||
if (newLabel.startsWith("$$$") || newCode in KeyCode.Spec.CURRENCY) {
|
||||
val currencyKey = params.mLocaleKeyboardInfos.currencyKey
|
||||
val currencyCodeAsString = if (newCode in KeyCode.Spec.CURRENCY) {
|
||||
when (newCode) {
|
||||
KeyCode.CURRENCY_SLOT_1 -> "|" + currencyKey.first
|
||||
KeyCode.CURRENCY_SLOT_2 -> "|" + currencyKey.second[0]
|
||||
KeyCode.CURRENCY_SLOT_3 -> "|" + currencyKey.second[1]
|
||||
KeyCode.CURRENCY_SLOT_4 -> "|" + currencyKey.second[2]
|
||||
KeyCode.CURRENCY_SLOT_5 -> "|" + currencyKey.second[3]
|
||||
KeyCode.CURRENCY_SLOT_6 -> "|" + currencyKey.second[4]
|
||||
else -> ""
|
||||
}
|
||||
} else ""
|
||||
if (newLabel == "$$$") {
|
||||
val finalLabel = currencyKey.first + currencyCodeAsString
|
||||
// the flag is to match old parser, but why is it there for main currency key and not for others?
|
||||
return TextKeyData(type, KeyCode.UNSPECIFIED, finalLabel, groupId, SimplePopups(currencyKey.second), labelFlags or Key.LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO)
|
||||
}
|
||||
val n = newLabel.substringAfter("$$$").toIntOrNull()
|
||||
if (n != null && n <= 5 && n > 0) {
|
||||
val finalLabel = currencyKey.second[n - 1] + currencyCodeAsString
|
||||
return TextKeyData(type, KeyCode.UNSPECIFIED, finalLabel, groupId, popup, labelFlags)
|
||||
}
|
||||
}
|
||||
if (newCode != code || newLabel != label)
|
||||
return TextKeyData(type, newCode, newLabel, groupId, popup, labelFlags).compute(params)
|
||||
return copy(newCode = newCode, newLabel = newLabel)
|
||||
return this
|
||||
}
|
||||
|
||||
|
||||
fun isSpaceKey(): Boolean {
|
||||
return type == KeyType.CHARACTER && (code == Constants.CODE_SPACE || code == KeyCode.CJK_SPACE
|
||||
|| code == KeyCode.ZWNJ || code == KeyCode.KESHIDA)
|
||||
return code == Constants.CODE_SPACE || code == KeyCode.CJK_SPACE || code == KeyCode.ZWNJ || code == KeyCode.KESHIDA
|
||||
}
|
||||
|
||||
fun toKeyParams(params: KeyboardParams, width: Float = params.mDefaultRelativeKeyWidth, additionalLabelFlags: Int = 0): Key.KeyParams {
|
||||
// todo: remove checks here, do only when reading json layouts
|
||||
// numeric keys are assigned a higher width in number layouts
|
||||
require(type == KeyType.CHARACTER || type == KeyType.NUMERIC) { "only KeyType CHARACTER or NUMERIC is supported" }
|
||||
// allow GROUP_ENTER negative codes so original florisboard number layouts can be used, bu actually it's ignored
|
||||
require(groupId == GROUP_DEFAULT || groupId == GROUP_ENTER) { "currently only GROUP_DEFAULT or GROUP_ENTER is supported" }
|
||||
require(code != KeyCode.UNSPECIFIED || label.isNotEmpty()) { "key has no code and no label" }
|
||||
fun isKeyPlaceholder() = type == KeyType.PLACEHOLDER && code == KeyCode.UNSPECIFIED && width == 0f
|
||||
|
||||
return if (code == KeyCode.UNSPECIFIED || code == KeyCode.MULTIPLE_CODE_POINTS) {
|
||||
/** this expects that codes and labels are already converted from FlorisBoard values, usually through compute */
|
||||
fun toKeyParams(params: KeyboardParams, additionalLabelFlags: Int = 0): Key.KeyParams {
|
||||
if (type == KeyType.PLACEHOLDER) return Key.KeyParams.newSpacer(params, width)
|
||||
|
||||
val newWidth = if (width == 0f) getDefaultWidth(params) else width
|
||||
val newCode: Int
|
||||
val newLabel: String
|
||||
if (code in KeyCode.Spec.CURRENCY) {
|
||||
// special treatment necessary, because we may need to encode it in the label
|
||||
// (currency is a string, so might have more than 1 codepoint)
|
||||
newCode = 0
|
||||
val l = processLabel(params)
|
||||
newLabel = when (code) {
|
||||
// consider currency codes for label
|
||||
KeyCode.CURRENCY_SLOT_1 -> "$l|${params.mLocaleKeyboardInfos.currencyKey.first}"
|
||||
KeyCode.CURRENCY_SLOT_2 -> "$l|${params.mLocaleKeyboardInfos.currencyKey.second[0]}"
|
||||
KeyCode.CURRENCY_SLOT_3 -> "$l|${params.mLocaleKeyboardInfos.currencyKey.second[1]}"
|
||||
KeyCode.CURRENCY_SLOT_4 -> "$l|${params.mLocaleKeyboardInfos.currencyKey.second[2]}"
|
||||
KeyCode.CURRENCY_SLOT_5 -> "$l|${params.mLocaleKeyboardInfos.currencyKey.second[3]}"
|
||||
KeyCode.CURRENCY_SLOT_6 -> "$l|${params.mLocaleKeyboardInfos.currencyKey.second[4]}"
|
||||
else -> throw IllegalStateException("code in currency range, but not in currency range?")
|
||||
}
|
||||
} else {
|
||||
newCode = processCode()
|
||||
newLabel = processLabel(params)
|
||||
}
|
||||
val newLabelFlags = labelFlags or additionalLabelFlags or getAdditionalLabelFlags(params)
|
||||
val newPopupKeys = popup.merge(getAdditionalPopupKeys(params))
|
||||
|
||||
val background = when (type) {
|
||||
KeyType.CHARACTER, KeyType.NUMERIC -> Key.BACKGROUND_TYPE_NORMAL
|
||||
KeyType.FUNCTION, KeyType.MODIFIER, KeyType.SYSTEM_GUI -> Key.BACKGROUND_TYPE_FUNCTIONAL
|
||||
KeyType.PLACEHOLDER, KeyType.UNSPECIFIED -> Key.BACKGROUND_TYPE_EMPTY
|
||||
KeyType.NAVIGATION -> Key.BACKGROUND_TYPE_SPACEBAR
|
||||
KeyType.ENTER_EDITING -> Key.BACKGROUND_TYPE_ACTION
|
||||
KeyType.LOCK -> getShiftBackground(params)
|
||||
null -> getDefaultBackground(params)
|
||||
}
|
||||
|
||||
return if (newCode == KeyCode.UNSPECIFIED || newCode == KeyCode.MULTIPLE_CODE_POINTS) {
|
||||
// code will be determined from label if possible (i.e. label is single code point)
|
||||
// but also longer labels should work without issues, also for MultiTextKeyData
|
||||
if (this is MultiTextKeyData) {
|
||||
val outputText = String(codePoints, 0, codePoints.size)
|
||||
Key.KeyParams(
|
||||
"$label|$outputText",
|
||||
code,
|
||||
"$newLabel|$outputText",
|
||||
newCode,
|
||||
params,
|
||||
width,
|
||||
labelFlags or additionalLabelFlags,
|
||||
Key.BACKGROUND_TYPE_NORMAL, // todo (when supported): determine type
|
||||
popup,
|
||||
newWidth,
|
||||
newLabelFlags,
|
||||
background,
|
||||
newPopupKeys,
|
||||
)
|
||||
} else {
|
||||
Key.KeyParams(
|
||||
label.rtlLabel(params), // todo (when supported): convert special labels to keySpec
|
||||
newLabel.rtlLabel(params), // todo (when supported): convert special labels to keySpec
|
||||
params,
|
||||
width,
|
||||
labelFlags or additionalLabelFlags,
|
||||
Key.BACKGROUND_TYPE_NORMAL, // todo (when supported): determine type
|
||||
popup,
|
||||
newWidth,
|
||||
newLabelFlags,
|
||||
background,
|
||||
newPopupKeys,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Key.KeyParams(
|
||||
label.ifEmpty { StringUtils.newSingleCodePointString(code) },
|
||||
code,
|
||||
newLabel.ifEmpty { StringUtils.newSingleCodePointString(newCode) },
|
||||
newCode,
|
||||
params,
|
||||
width,
|
||||
labelFlags or additionalLabelFlags,
|
||||
Key.BACKGROUND_TYPE_NORMAL,
|
||||
popup,
|
||||
newWidth,
|
||||
newLabelFlags,
|
||||
background,
|
||||
newPopupKeys,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDefaultBackground(params: KeyboardParams): Int {
|
||||
// functional keys
|
||||
when (label) { // or use code?
|
||||
KeyLabel.SYMBOL_ALPHA, KeyLabel.SYMBOL, KeyLabel.ALPHA, KeyLabel.COMMA, KeyLabel.PERIOD, KeyLabel.DELETE,
|
||||
KeyLabel.EMOJI, KeyLabel.COM, KeyLabel.LANGUAGE_SWITCH, KeyLabel.NUMPAD -> return Key.BACKGROUND_TYPE_FUNCTIONAL
|
||||
KeyLabel.SPACE, KeyLabel.ZWNJ -> return Key.BACKGROUND_TYPE_SPACEBAR
|
||||
KeyLabel.ACTION -> return Key.BACKGROUND_TYPE_ACTION
|
||||
KeyLabel.SHIFT -> return getShiftBackground(params)
|
||||
}
|
||||
if (type == KeyType.PLACEHOLDER) return Key.BACKGROUND_TYPE_EMPTY
|
||||
return Key.BACKGROUND_TYPE_NORMAL
|
||||
}
|
||||
|
||||
// todo (later): possibly the whole stickyOn/Off stuff can be removed, currently it should only have a very slight effect in holo
|
||||
// but iirc there is some attempt in reviving the sticky thing, right?
|
||||
private fun getShiftBackground(params: KeyboardParams): Int {
|
||||
return if (params.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED
|
||||
|| params.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED) Key.BACKGROUND_TYPE_STICKY_ON
|
||||
else Key.BACKGROUND_TYPE_STICKY_OFF
|
||||
}
|
||||
|
||||
private fun getDefaultWidth(params: KeyboardParams): Float {
|
||||
return if (label == KeyLabel.SPACE && params.mId.isAlphaOrSymbolKeyboard) -1f
|
||||
else if (type == KeyType.NUMERIC && params.mId.isNumberLayout) 0.17f // todo (later) consider making this -1?
|
||||
else params.mDefaultKeyWidth
|
||||
}
|
||||
|
||||
// todo (later): encoding the code in the label should be avoided, because we know it already
|
||||
private fun processLabel(params: KeyboardParams): String = when (label) {
|
||||
KeyLabel.SYMBOL_ALPHA -> if (params.mId.isAlphabetKeyboard) params.mLocaleKeyboardInfos.labelSymbol else params.mLocaleKeyboardInfos.labelAlphabet
|
||||
KeyLabel.SYMBOL -> params.mLocaleKeyboardInfos.labelSymbol
|
||||
KeyLabel.ALPHA -> params.mLocaleKeyboardInfos.labelAlphabet
|
||||
KeyLabel.COMMA -> params.mLocaleKeyboardInfos.labelComma
|
||||
KeyLabel.PERIOD -> getPeriodLabel(params)
|
||||
KeyLabel.SPACE -> getSpaceLabel(params)
|
||||
// KeyLabel.ACTION -> "${getActionKeyLabel(params)}|${getActionKeyCode(params)}" would need context
|
||||
KeyLabel.DELETE -> "!icon/delete_key|!code/key_delete"
|
||||
KeyLabel.SHIFT -> "${getShiftLabel(params)}|!code/key_shift"
|
||||
KeyLabel.EMOJI -> "!icon/emoji_normal_key|!code/key_emoji"
|
||||
// todo (later): label and popupKeys for .com should be in localeKeyTexts, handled similar to currency key
|
||||
KeyLabel.COM -> ".com"
|
||||
KeyLabel.LANGUAGE_SWITCH -> "!icon/language_switch_key|!code/key_language_switch"
|
||||
KeyLabel.NUMPAD -> "!icon/numpad_key|!code/key_numpad"
|
||||
KeyLabel.ZWNJ -> "!icon/zwnj_key|\u200C"
|
||||
KeyLabel.CURRENCY -> params.mLocaleKeyboardInfos.currencyKey.first
|
||||
KeyLabel.CURRENCY1 -> params.mLocaleKeyboardInfos.currencyKey.second[0]
|
||||
KeyLabel.CURRENCY2 -> params.mLocaleKeyboardInfos.currencyKey.second[1]
|
||||
KeyLabel.CURRENCY3 -> params.mLocaleKeyboardInfos.currencyKey.second[2]
|
||||
KeyLabel.CURRENCY4 -> params.mLocaleKeyboardInfos.currencyKey.second[3]
|
||||
KeyLabel.CURRENCY5 -> params.mLocaleKeyboardInfos.currencyKey.second[4]
|
||||
else -> label
|
||||
}
|
||||
|
||||
private fun processCode(): Int {
|
||||
if (code != KeyCode.UNSPECIFIED) return code
|
||||
return when (label) {
|
||||
KeyLabel.SYMBOL_ALPHA -> KeyCode.SYMBOL_ALPHA
|
||||
KeyLabel.SYMBOL -> KeyCode.SYMBOL
|
||||
KeyLabel.ALPHA -> KeyCode.ALPHA
|
||||
else -> code
|
||||
}
|
||||
}
|
||||
|
||||
// todo (later): add explanations / reasoning, often this is just taken from conversion from AOSP layouts
|
||||
private fun getAdditionalLabelFlags(params: KeyboardParams): Int {
|
||||
return when (label) {
|
||||
KeyLabel.ALPHA, KeyLabel.SYMBOL_ALPHA, KeyLabel.SYMBOL -> Key.LABEL_FLAGS_PRESERVE_CASE or Key.LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR
|
||||
KeyLabel.PERIOD, KeyLabel.COMMA -> Key.LABEL_FLAGS_HAS_POPUP_HINT // todo: period also has defaultLabelFlags -> when is this relevant?
|
||||
KeyLabel.ACTION -> {
|
||||
Key.LABEL_FLAGS_PRESERVE_CASE or Key.LABEL_FLAGS_AUTO_X_SCALE or
|
||||
Key.LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO or Key.LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR or
|
||||
Key.LABEL_FLAGS_HAS_POPUP_HINT or KeyboardTheme.getThemeActionAndEmojiKeyLabelFlags(params.mThemeId)
|
||||
}
|
||||
KeyLabel.SPACE -> if (params.mId.isNumberLayout) Key.LABEL_FLAGS_ALIGN_ICON_TO_BOTTOM else 0
|
||||
KeyLabel.SHIFT -> Key.LABEL_FLAGS_PRESERVE_CASE or if (!params.mId.isAlphabetKeyboard) Key.LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR else 0
|
||||
KeyLabel.EMOJI -> KeyboardTheme.getThemeActionAndEmojiKeyLabelFlags(params.mThemeId)
|
||||
KeyLabel.COM -> Key.LABEL_FLAGS_AUTO_X_SCALE or Key.LABEL_FLAGS_FONT_NORMAL or Key.LABEL_FLAGS_HAS_POPUP_HINT or Key.LABEL_FLAGS_PRESERVE_CASE
|
||||
KeyLabel.ZWNJ -> Key.LABEL_FLAGS_HAS_POPUP_HINT
|
||||
KeyLabel.CURRENCY -> Key.LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAdditionalPopupKeys(params: KeyboardParams): PopupSet<AbstractKeyData>? {
|
||||
if (groupId == GROUP_COMMA) return SimplePopups(getCommaPopupKeys(params))
|
||||
if (groupId == GROUP_PERIOD) return SimplePopups(getPunctuationPopupKeys(params))
|
||||
// if (groupId == GROUP_ENTER) return getActionKeyPopupKeys(params)?.let { SimplePopups(it) }
|
||||
return when (label) {
|
||||
KeyLabel.COMMA -> SimplePopups(getCommaPopupKeys(params))
|
||||
KeyLabel.PERIOD -> SimplePopups(getPunctuationPopupKeys(params))
|
||||
// KeyLabel.ACTION -> getActionKeyPopupKeys(params)?.let { SimplePopups(it) }
|
||||
KeyLabel.SHIFT -> {
|
||||
if (params.mId.isAlphabetKeyboard) SimplePopups(
|
||||
listOf(
|
||||
"!noPanelAutoPopupKey!",
|
||||
" |!code/key_capslock"
|
||||
)
|
||||
) else null // why the alphabet popup keys actually?
|
||||
}
|
||||
KeyLabel.COM -> SimplePopups(listOf(Key.POPUP_KEYS_HAS_LABELS, ".net", ".org", ".gov", ".edu"))
|
||||
KeyLabel.ZWNJ -> SimplePopups(listOf("!icon/zwj_key|\u200D"))
|
||||
// only add currency popups if there are none defined on the key
|
||||
KeyLabel.CURRENCY -> if (popup.isEmpty()) SimplePopups(params.mLocaleKeyboardInfos.currencyKey.second) else null
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data class which describes a single key and its attributes.
|
||||
*
|
||||
* @property type The type of the key. Some actions require both [code] and [type] to match in order
|
||||
* to be successfully executed. Defaults to [KeyType.CHARACTER].
|
||||
* to be successfully executed. Defaults to null.
|
||||
* @property code The UTF-8 encoded code of the character. The code defined here is used as the
|
||||
* data passed to the system. Defaults to 0.
|
||||
* @property label The string used to display the key in the UI. Is not used for the actual data
|
||||
|
@ -173,11 +357,12 @@ sealed interface KeyData : AbstractKeyData {
|
|||
@Serializable
|
||||
@SerialName("text_key")
|
||||
class TextKeyData(
|
||||
override val type: KeyType = KeyType.CHARACTER,
|
||||
override val type: KeyType? = null,
|
||||
override val code: Int = KeyCode.UNSPECIFIED,
|
||||
override val label: String = "",
|
||||
override val groupId: Int = KeyData.GROUP_DEFAULT,
|
||||
override val popup: PopupSet<AbstractKeyData> = PopupSet(),
|
||||
override val popup: PopupSet<out AbstractKeyData> = SimplePopups(null),
|
||||
override val width: Float = 0f,
|
||||
override val labelFlags: Int = 0
|
||||
) : KeyData {
|
||||
override fun asString(isForDisplay: Boolean): String {
|
||||
|
@ -197,6 +382,16 @@ class TextKeyData(
|
|||
return "${TextKeyData::class.simpleName} { type=$type code=$code label=\"$label\" groupId=$groupId }"
|
||||
}
|
||||
|
||||
override fun copy(
|
||||
newType: KeyType?,
|
||||
newCode: Int,
|
||||
newLabel: String,
|
||||
newGroupId: Int,
|
||||
newPopup: PopupSet<out AbstractKeyData>,
|
||||
newWidth: Float,
|
||||
newLabelFlags: Int
|
||||
) = TextKeyData(newType, newCode, newLabel, newGroupId, newPopup, newWidth, newLabelFlags)
|
||||
|
||||
}
|
||||
|
||||
// AutoTextKeyData is just for converting case with shift, which HeliBoard always does anyway
|
||||
|
@ -204,11 +399,12 @@ class TextKeyData(
|
|||
@Serializable
|
||||
@SerialName("auto_text_key")
|
||||
class AutoTextKeyData(
|
||||
override val type: KeyType = KeyType.CHARACTER,
|
||||
override val type: KeyType? = null,
|
||||
override val code: Int = KeyCode.UNSPECIFIED,
|
||||
override val label: String = "",
|
||||
override val groupId: Int = KeyData.GROUP_DEFAULT,
|
||||
override val popup: PopupSet<AbstractKeyData> = PopupSet(),
|
||||
override val popup: PopupSet<out AbstractKeyData> = SimplePopups(null),
|
||||
override val width: Float = 0f,
|
||||
override val labelFlags: Int = 0
|
||||
) : KeyData {
|
||||
|
||||
|
@ -228,16 +424,28 @@ class AutoTextKeyData(
|
|||
override fun toString(): String {
|
||||
return "${AutoTextKeyData::class.simpleName} { type=$type code=$code label=\"$label\" groupId=$groupId }"
|
||||
}
|
||||
|
||||
override fun copy(
|
||||
newType: KeyType?,
|
||||
newCode: Int,
|
||||
newLabel: String,
|
||||
newGroupId: Int,
|
||||
newPopup: PopupSet<out AbstractKeyData>,
|
||||
newWidth: Float,
|
||||
newLabelFlags: Int
|
||||
) = AutoTextKeyData(newType, newCode, newLabel, newGroupId, newPopup, newWidth, newLabelFlags)
|
||||
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@SerialName("multi_text_key")
|
||||
class MultiTextKeyData(
|
||||
override val type: KeyType = KeyType.CHARACTER,
|
||||
override val type: KeyType? = null,
|
||||
val codePoints: IntArray = intArrayOf(),
|
||||
override val label: String = "",
|
||||
override val groupId: Int = KeyData.GROUP_DEFAULT,
|
||||
override val popup: PopupSet<AbstractKeyData> = PopupSet(),
|
||||
override val popup: PopupSet<out AbstractKeyData> = SimplePopups(null),
|
||||
override val width: Float = 0f,
|
||||
override val labelFlags: Int = 0
|
||||
) : KeyData {
|
||||
@Transient override val code: Int = KeyCode.MULTIPLE_CODE_POINTS
|
||||
|
@ -263,6 +471,17 @@ class MultiTextKeyData(
|
|||
override fun toString(): String {
|
||||
return "${MultiTextKeyData::class.simpleName} { type=$type code=$code label=\"$label\" groupId=$groupId }"
|
||||
}
|
||||
|
||||
override fun copy(
|
||||
newType: KeyType?,
|
||||
newCode: Int,
|
||||
newLabel: String,
|
||||
newGroupId: Int,
|
||||
newPopup: PopupSet<out AbstractKeyData>,
|
||||
newWidth: Float,
|
||||
newLabelFlags: Int
|
||||
) = MultiTextKeyData(newType, codePoints, newLabel, newGroupId, newPopup, newWidth, newLabelFlags)
|
||||
|
||||
}
|
||||
|
||||
fun String.toTextKey(popupKeys: Collection<String>? = null, labelFlags: Int = 0): TextKeyData =
|
||||
|
|
|
@ -201,7 +201,7 @@ public final class Constants {
|
|||
switch (code) {
|
||||
case KeyCode.SHIFT: return "shift";
|
||||
case KeyCode.CAPS_LOCK: return "capslock";
|
||||
case KeyCode.ALPHA_SYMBOL: return "alpha_symbol";
|
||||
case KeyCode.SYMBOL_ALPHA: return "symbol_alpha";
|
||||
case KeyCode.ALPHA: return "alpha";
|
||||
case KeyCode.SYMBOL: return "symbol";
|
||||
case KeyCode.MULTIPLE_CODE_POINTS: return "text";
|
||||
|
|
|
@ -490,7 +490,7 @@ public final class InputLogic {
|
|||
}
|
||||
if (!inputTransaction.didAutoCorrect() && processedEvent.getMKeyCode() != KeyCode.SHIFT
|
||||
&& processedEvent.getMKeyCode() != KeyCode.CAPS_LOCK
|
||||
&& processedEvent.getMKeyCode() != KeyCode.ALPHA_SYMBOL
|
||||
&& processedEvent.getMKeyCode() != KeyCode.SYMBOL_ALPHA
|
||||
&& processedEvent.getMKeyCode() != KeyCode.ALPHA
|
||||
&& processedEvent.getMKeyCode() != KeyCode.SYMBOL)
|
||||
mLastComposedWord.deactivate();
|
||||
|
@ -775,7 +775,7 @@ public final class InputLogic {
|
|||
// We need to switch to the shortcut IME. This is handled by LatinIME since the
|
||||
// input logic has no business with IME switching.
|
||||
case KeyCode.CAPS_LOCK:
|
||||
case KeyCode.ALPHA_SYMBOL:
|
||||
case KeyCode.SYMBOL_ALPHA:
|
||||
case KeyCode.ALPHA:
|
||||
case KeyCode.SYMBOL:
|
||||
case KeyCode.NUMPAD:
|
||||
|
|
|
@ -706,4 +706,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
|
|||
return wrapper;
|
||||
}
|
||||
|
||||
public boolean isTablet() {
|
||||
return mContext.getResources().getInteger(R.integer.config_screen_metrics) >= 3;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,6 @@ import helium314.keyboard.keyboard.internal.KeyboardParams;
|
|||
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode;
|
||||
import helium314.keyboard.latin.R;
|
||||
import helium314.keyboard.latin.SuggestedWords;
|
||||
import helium314.keyboard.latin.common.Constants;
|
||||
import helium314.keyboard.latin.utils.TypefaceUtils;
|
||||
|
||||
public final class MoreSuggestions extends Keyboard {
|
||||
|
@ -87,7 +86,7 @@ public final class MoreSuggestions extends Keyboard {
|
|||
mNumRows = row + 1;
|
||||
mBaseWidth = mOccupiedWidth = Math.max(
|
||||
minWidth, calcurateMaxRowWidth(fromIndex, index));
|
||||
mBaseHeight = mOccupiedHeight = mNumRows * mDefaultRowHeight + mVerticalGap;
|
||||
mBaseHeight = mOccupiedHeight = mNumRows * mDefaultAbsoluteRowHeight + mVerticalGap;
|
||||
return index - fromIndex;
|
||||
}
|
||||
|
||||
|
@ -138,7 +137,7 @@ public final class MoreSuggestions extends Keyboard {
|
|||
|
||||
public int getY(final int index) {
|
||||
final int row = mRowNumbers[index];
|
||||
return (mNumRows -1 - row) * mDefaultRowHeight + mTopPadding;
|
||||
return (mNumRows -1 - row) * mDefaultAbsoluteRowHeight + mTopPadding;
|
||||
}
|
||||
|
||||
public int getWidth(final int index) {
|
||||
|
@ -185,7 +184,7 @@ public final class MoreSuggestions extends Keyboard {
|
|||
mParams.mId = parentKeyboard.mId;
|
||||
readAttributes(xmlId);
|
||||
mParams.mVerticalGap = mParams.mTopPadding = parentKeyboard.mVerticalGap / 2;
|
||||
mPaneView.updateKeyboardGeometry(mParams.mDefaultRowHeight);
|
||||
mPaneView.updateKeyboardGeometry(mParams.mDefaultAbsoluteRowHeight);
|
||||
final int count = mParams.layout(suggestedWords, fromIndex, maxWidth, minWidth, maxRow,
|
||||
mPaneView.newLabelPaint(null /* key */), mResources);
|
||||
mFromIndex = fromIndex;
|
||||
|
@ -218,7 +217,7 @@ public final class MoreSuggestions extends Keyboard {
|
|||
final int numColumnInRow = params.getNumColumnInRow(index);
|
||||
if (columnNumber < numColumnInRow - 1) {
|
||||
final Divider divider = new Divider(params, params.mDivider, x + width, y,
|
||||
params.mDividerWidth, params.mDefaultRowHeight);
|
||||
params.mDividerWidth, params.mDefaultAbsoluteRowHeight);
|
||||
params.onAddKey(divider);
|
||||
}
|
||||
}
|
||||
|
@ -234,7 +233,7 @@ public final class MoreSuggestions extends Keyboard {
|
|||
super(word /* label */, KeyboardIconsSet.ICON_UNDEFINED, KeyCode.MULTIPLE_CODE_POINTS,
|
||||
word /* outputText */, info, 0 /* labelFlags */, Key.BACKGROUND_TYPE_NORMAL,
|
||||
params.getX(index), params.getY(index), params.getWidth(index),
|
||||
params.mDefaultRowHeight, params.mHorizontalGap, params.mVerticalGap);
|
||||
params.mDefaultAbsoluteRowHeight, params.mHorizontalGap, params.mVerticalGap);
|
||||
mSuggestedWordIndex = index;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,3 +17,40 @@ fun CharSequence.getStringResourceOrName(prefix: String, context: Context): Char
|
|||
val resId = context.resources.getIdentifier(prefix + this, "string", context.packageName)
|
||||
return if (resId == 0) this else context.getString(resId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits the collection into a pair of lists on the first match of [condition], discarding the element first matching the condition.
|
||||
* If [condition] is not met, all elements are in the first list.
|
||||
*/
|
||||
fun <T> Collection<T>.splitAt(condition: (T) -> Boolean): Pair<MutableList<T>, MutableList<T>> {
|
||||
var conditionMet = false
|
||||
val first = mutableListOf<T>()
|
||||
val second = mutableListOf<T>()
|
||||
forEach {
|
||||
if (conditionMet) {
|
||||
second.add(it)
|
||||
} else {
|
||||
conditionMet = condition(it)
|
||||
if (!conditionMet)
|
||||
first.add(it)
|
||||
}
|
||||
}
|
||||
return first to second
|
||||
}
|
||||
|
||||
// like plus, but for nullable collections
|
||||
fun <T> addCollections(a: Collection<T>?, b: Collection<T>?): Collection<T>? {
|
||||
if (a.isNullOrEmpty()) return b
|
||||
if (b.isNullOrEmpty()) return a
|
||||
return a + b
|
||||
}
|
||||
|
||||
fun <T> MutableList<T>.removeFirst(predicate: (T) -> Boolean) {
|
||||
val i = indexOfFirst(predicate)
|
||||
if (i >= 0) removeAt(i)
|
||||
}
|
||||
|
||||
fun <T> MutableList<T>.replaceFirst(predicate: (T) -> Boolean, with: (T) -> T) {
|
||||
val i = indexOfFirst(predicate)
|
||||
if (i >= 0) this[i] = with(this[i])
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -50,6 +50,12 @@ class ParserTest {
|
|||
ShadowLog.stream = System.out
|
||||
}
|
||||
|
||||
// todo: add more tests
|
||||
// (popup) keys with label and code
|
||||
// (popup) keys with icon
|
||||
// (popup) keys with that are essentially toolbar keys (yes, this should work at some point!)
|
||||
// correct background type, depending on key type and maybe sth else
|
||||
|
||||
@Test fun simpleParser() {
|
||||
val params = KeyboardParams()
|
||||
params.mId = KeyboardLayoutSet.getFakeKeyboardId(KeyboardId.ELEMENT_ALPHABET)
|
||||
|
@ -124,10 +130,11 @@ f""", // no newline at the end
|
|||
params.mId = KeyboardLayoutSet.getFakeKeyboardId(KeyboardId.ELEMENT_ALPHABET)
|
||||
params.mPopupKeyTypes.add(POPUP_KEYS_LAYOUT)
|
||||
addLocaleKeyTextsToParams(latinIME, params, POPUP_KEYS_NORMAL)
|
||||
data class Expected(val label: String, val text: String?, val code: Int, val popups: List<String>?)
|
||||
data class Expected(val label: String?, val text: String?, val code: Int, val popups: List<String>? = null)
|
||||
val expected = listOf(
|
||||
Expected("a", null, 'a'.code, null),
|
||||
Expected("a", null, 'a'.code, null),
|
||||
Expected("a", null, 'b'.code, listOf("b")), // todo: should also check whether code is "a"
|
||||
Expected("$", null, '$'.code, listOf("£", "€", "¢", "¥", "₱")),
|
||||
Expected("$", null, '¥'.code, listOf("£", "€", "¢", "¥", "₱")),
|
||||
Expected("i", null, 105, null),
|
||||
|
@ -137,11 +144,15 @@ f""", // no newline at the end
|
|||
Expected(".", null, '.'.code, listOf(">")),
|
||||
Expected("'", null, '\''.code, listOf("!", "\"")),
|
||||
Expected("9", null, '9'.code, null), // todo (later): also should have different background or whatever is related to type
|
||||
Expected("", null, -7, null), // todo: expect an icon
|
||||
Expected("?123", null, -207, null),
|
||||
Expected("", null, ' '.code, null),
|
||||
Expected(null, null, -7, null), // todo: expect an icon
|
||||
Expected("?123", "?123", -202, null),
|
||||
Expected(null, null, ' '.code, null),
|
||||
Expected("(", null, '('.code, listOf("<", "[", "{")),
|
||||
Expected("$", null, '$'.code, listOf("£", "₱", "€", "¢", "¥")),
|
||||
Expected("a", null, ' '.code, null),
|
||||
Expected("a", null, ' '.code, null),
|
||||
Expected(null, null, KeyCode.CLIPBOARD, null), // todo: expect an icon
|
||||
Expected(null, null, KeyCode.MULTIPLE_CODE_POINTS, null), // todo: this works here, but crashes on phone
|
||||
Expected("p", null, 'p'.code, null),
|
||||
)
|
||||
val layoutString = """
|
||||
|
@ -149,6 +160,7 @@ f""", // no newline at the end
|
|||
[
|
||||
{ "$": "auto_text_key" "label": "a" },
|
||||
{ "$": "text_key" "label": "a" },
|
||||
{ "$": "text_key" "label": "a|b", "popup": { "main": { "label": "b|a" } } },
|
||||
{ "label": "$$$" },
|
||||
{ "label": "$$$", code: -805 },
|
||||
{ "$": "case_selector",
|
||||
|
@ -228,6 +240,10 @@ f""", // no newline at the end
|
|||
{ "code": -805, "label": "currency_slot_5" }
|
||||
]
|
||||
} },
|
||||
{ "code": 32, "label": "a|!code/key_delete" },
|
||||
{ "code": 32, "label": "a|b" },
|
||||
{ "label": "!icon/clipboard_action_key|!code/key_clipboard" },
|
||||
{ "label": "!icon/clipboard_action_key" },
|
||||
{ "label": "p" }
|
||||
],
|
||||
[
|
||||
|
@ -254,11 +270,9 @@ f""", // no newline at the end
|
|||
""".trimIndent()
|
||||
val keys = JsonKeyboardParser(params, latinIME).parseCoreLayout(layoutString)
|
||||
keys.first().forEachIndexed { index, keyData ->
|
||||
println("key ${keyData.label}: code ${keyData.code}, popups: ${keyData.popup.getPopupKeyLabels(params)}")
|
||||
if (keyData.type == KeyType.ENTER_EDITING || keyData.type == KeyType.SYSTEM_GUI) return@forEachIndexed // todo: currently not accepted, but should be (see below)
|
||||
println("data: key ${keyData.label}: code ${keyData.code}, popups: ${keyData.popup.getPopupKeyLabels(params)}")
|
||||
val keyParams = keyData.toKeyParams(params)
|
||||
println("key ${keyParams.mLabel}: code ${keyParams.mCode}, popups: ${keyParams.mPopupKeys?.toList()}")
|
||||
if (keyParams.outputText == "space") return@forEachIndexed // todo: only works for numeric layouts... idea: parse space anywhere, and otherwise only if special type
|
||||
println("params: key ${keyParams.mLabel}: code ${keyParams.mCode}, popups: ${keyParams.mPopupKeys?.toList()}")
|
||||
assertEquals(expected[index].label, keyParams.mLabel)
|
||||
assertEquals(expected[index].code, keyParams.mCode)
|
||||
assertEquals(expected[index].popups?.sorted(), keyParams.mPopupKeys?.mapNotNull { it.mLabel }?.sorted()) // todo (later): what's wrong with order?
|
||||
|
|
99
layouts.md
99
layouts.md
|
@ -1,36 +1,17 @@
|
|||
A compilation of information about the layout formats usable in this app.
|
||||
|
||||
There are two distinct formats:
|
||||
* the _simple_ format is a text file with one key per line, and two consecutive line breaks indicating a switch to the next row, [example](app/src/main/assets/layouts/qwerty.txt)
|
||||
* the _simple_ format is a text file with one key label per line, and two consecutive line breaks indicating a switch to the next row, [example](app/src/main/assets/layouts/qwerty.txt)
|
||||
* the _json_ format taken from [FlorisBoard](https://github.com/florisboard/florisboard/blob/master/CONTRIBUTING.md#adding-the-layout), but only "normal" keys are supported (i.e. no action keys and similar), [example](app/src/main/assets/layouts/azerty.json)
|
||||
|
||||
## General notes
|
||||
Adding too many keys or too long texts will make the keyboard look awkward or broken, and even crash the app under some specific conditions.
|
||||
Adding too many keys or too long texts will make the keyboard look awkward or broken, and even crash the app under some specific conditions (popup keys are especially prone for this).
|
||||
There are some sanity checks when adding a layout to avoid such issues, but they do not cover all possible cases.
|
||||
Further there is no check whether the layout actually contains characters of the selected language.
|
||||
|
||||
If you use an external glide typing library, you likely will have issues if your layout contains duplicate keys, or keys with text longer than a single letter.
|
||||
|
||||
There are special key labels that are intended for internal use only, but can (currently) be set on custom layouts too. An example is `!icon/previous_key|!code/key_action_previous`, so it's unlikely you will stumble upon issues here when not intentionally provoking it.
|
||||
One special label that might be wanted though is `$$$`, which will be replaced by the local currency. `$$$1` - `$$$5` will be replaced by currencies available on long-pressing the currency key.
|
||||
If you want different key label and use text, set the label to [label]|[text], e.g. `aa|bb` will show `aa`, but pressing the key will input `bb`.
|
||||
|
||||
Some special key labels will be implemented, most are already working in the (currently experimental) customization of number layouts (numpad and similar). Some keys have two names for compatibility to FlorisBoard layouts.
|
||||
* _alpha_ / _view_characters_: switch to alphabet keyboard (or main phone keyboard in case of phone layout)
|
||||
* _symbol_ / _view_symbols_: switch to symbol keyboard (or phone symbols keyboard in case of phone layout)
|
||||
* _symbol_alpha_: toggle alpha / symbol keyboard
|
||||
* _numpad_ / _view_numeric_advanced_: switch to numpad layout
|
||||
* _emoji_: switch to emoji view
|
||||
* _com_: display common TLDs (.com and similar)
|
||||
* _emoji_com_: emoji key, but in URL and email fields it's a com key
|
||||
* _language_switch_: language switch key
|
||||
* _action_ / _enter_: the action (enter) key
|
||||
* _delete_: delete key
|
||||
* _shift_: shift key, will change label when in symbols layout
|
||||
* _period_: `.` key with punctuation popups, will adapt to language-specific period
|
||||
* _comma_: `,` key with special popups, will adapt to language-specific comma, or display `/` in URL fields and `@` in email fields
|
||||
* _space_: space key, with icon when using a number layout
|
||||
* _zwnj_: Zero-width non-joiner (automatically added next to space in alphabet layout for some languages)
|
||||
If the layout has exactly 2 keys in the bottom row, these keys will replace comma and period keys. More exactly: the first key will replace the first functional key with `"groupId": 1` in the bottom row, and the second key with replace the first key with `"groupId": 2`.
|
||||
|
||||
## Simple format
|
||||
* One key per line
|
||||
|
@ -44,8 +25,68 @@ Some special key labels will be implemented, most are already working in the (cu
|
|||
* There is no need for specifying a `code`, it will be determined from the label automatically
|
||||
* You can still specify it, but it's only necessary if you want key label and code to be different (please avoid contributing layout with unnecessary codes to HeliBoard)
|
||||
* Note that not all _special codes_ (negative numbers) from FlorisBoard are supported
|
||||
* You can add the numeric value of a _labelFlag_ to a key for some specific effects, see [here](app/src/main/res/values/attrs.xml) in the section _keyLabelFlags_ for names and numeric values.
|
||||
* More details on the formal will be provided. For now you can check other layouts, often you just need to copy lines and change the labels.
|
||||
* Key classes: specified with `$`, usually you can omit them in HeliBoard
|
||||
* `text_key`: normal key, default
|
||||
* `auto_text_key`: used in FlorisBoard for a key that changes text case when shift is enabled, HeliBoard does that anyway unless disabled with a _labelFlag_
|
||||
* `multi_text_key`: key with an array of code points, e.g. `{ "$": "multi_text_key", "codePoints": [2509, 2480], "label": "্র" }`
|
||||
* there are also selector classes, which allow to change keys conditionally, see the [dvorak layout](https://github.com/Helium314/HeliBoard/blob/main/app/src/main/assets/layouts/dvorak.json) for an example:
|
||||
* `case_selector`: keys for `lower` and `upper` (both mandatory), similar to `shift_state_selector`
|
||||
* `shift_state_selector`: keys for `unshifted`, `shifted`, `shiftedManual`, `shiftedAutomatic`, `capsLock`, `manualOrLocked`, `default` (all opttional)
|
||||
* `variation_selector`: keys for `datetime`, `time`, `date`, `password`, `normal`, `uri`, `email`, `default` (all opttional)
|
||||
* `layout_direction_selector`: keys for `ltr` and `rtl` (both mandatory)
|
||||
### Properties
|
||||
* A (non-selector) key can have the following properties:
|
||||
* `type`: only specific values, HeliBoard mostly uses this to determine background color and type, determined automatically by default
|
||||
* `character`: normal key color
|
||||
* `function`: functional key color
|
||||
* `space`: space bar color
|
||||
* `action`: action key color
|
||||
* `unspecified`: no background color
|
||||
* `placeholder`: no background color, no label, and pressing the key does nothing
|
||||
* There are some more values, but they do nothing
|
||||
* `code`: code point that is entered when the key is pressed, determined from the label by default, not available for `multi_text_key`
|
||||
* There are special negative values available, e.g. the ones used by functional keys, see [KeyCode.kt](/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/floris/KeyCode.kt). There are several not yet supported key codes in there, you can see in the function `checkAndConvertCode` which ones are working.
|
||||
* `codePoints`: when multiple code points should be entered, only available for `multi_text_key`
|
||||
* `label`: text to display on the key, determined from code if empty
|
||||
* There are some special values, see the [label section](#labels)
|
||||
* `groupId`: which additional popup keys to show, `0` is default and does not add anything, `1` adds the comma popup keys, and `2` adds the period popup keys
|
||||
* `popup`: list of keys to add in the popup, e.g. `"label": ")", "popup": {"relevant": [{ "label": "." }]}` is a `)` key with a `.` popup
|
||||
* Note that in popup keys, properties are ignored with the exception of `$`, `code`, `codePoints`, and `label`
|
||||
* When specifying a _selector_ key class in a popup key, it will be evaluated correctly (e.g. for changing popups dependent on shift state)
|
||||
* `width`: width of the key in units of screen width, e.g. a key with `"width": 0.1` has a width of 10% of the screen, defaults to `0`
|
||||
* A special value is `-1`, which means the key expands to the available space not already used by other keys (e.g. the space bar)
|
||||
* `0` is interpreted as follows
|
||||
* `-1` on the `space` key in alphabet or symbols layouts
|
||||
* `0.17` for keys with `"type": numeric` in number layouts
|
||||
* Otherwise the default width is used, which is `0.1` for phones and `0.09` for tablets
|
||||
* If the sum of widths in a row is greater than 1, keys are rescaled to fit on the screen
|
||||
* `labelFlags`: allows specific effects, see [here](app/src/main/res/values/attrs.xml) in the section _keyLabelFlags_ for names and numeric values
|
||||
|
||||
## Labels
|
||||
In the simple format you only specify labels, in json layouts you do it explicitly via the `label` property.
|
||||
Usually the label is what is displayed on the key. However, there are some special labels:
|
||||
* Currency keys
|
||||
* `$$$` will be replaced by the local currency, depending on your current layout language. If you define a key with `$$$` without defining popup keys, it will get the first 4 additional currencies (see below) as popup
|
||||
* `$$$1` - `$$$5` will be replaced by currencies available on long-pressing the currency key
|
||||
* Functional keys (incomplete list)
|
||||
* _alpha_: switch to alphabet keyboard (or main phone keyboard in case of phone layout)
|
||||
* _symbol_: switch to symbol keyboard (or phone symbols keyboard in case of phone layout)
|
||||
* _symbol_alpha_: toggle alpha / symbol keyboard
|
||||
* _numpad_: switch to numpad layout
|
||||
* _emoji_: switch to emoji view
|
||||
* _com_: display common TLDs (.com and similar, currently not localized)
|
||||
* _language_switch_: language switch key
|
||||
* _action_: the action (enter) key
|
||||
* _delete_: delete key
|
||||
* _shift_: shift key, will change label when in symbols layout
|
||||
* _period_: `.` key with punctuation popups, will adapt to language-specific period
|
||||
* _comma_: `,` key with special popups, will adapt to language-specific comma, or display `/` in URL fields and `@` in email fields
|
||||
* _space_: space key, with icon when using a number layout
|
||||
* _zwnj_: Zero-width non-joiner (automatically added next to space in alphabet layout for some languages)
|
||||
* If you want different key label and use text, set the label to [label]|[text], e.g. `aa|bb` will show `aa`, but pressing the key will input `bb`.
|
||||
You can also specify special key codes like `a|!code/key_action_previous`, but it's cleaner to use a json layout and specify the code explicitly. Note that when specifying a code in the label, and a code in a json layout, the code in the label will be ignored.
|
||||
It's also possible to specify an icon together with a code `!icon/previous_key|!code/key_action_previous`, but this is not fully supported yet.
|
||||
|
||||
## Adding new layouts / languages
|
||||
* You need a layout file in one of the formats above, and add it to [layouts](app/src/main/assets/layouts)
|
||||
|
@ -66,3 +107,15 @@ Some special key labels will be implemented, most are already working in the (cu
|
|||
* If you add a new language for which Android does not have a display name, it will be displayed using the language tag
|
||||
* Avoiding this currently is more complicated than necessary: add the language tag to [LocaleUtils.getLocaleDisplayNameInSystemLocale](/app/src/main/java/helium314/keyboard/latin/common/LocaleUtils.kt#L181) to have an exception, and add a string named `subtype_<langage tag, but with _ instead of ->` to [`strings.xml`](/app/src/main/res/values/strings.xml). Further you may need to add a `subtype_in_root_locale_<language tag>` to [donottranslate.xml](/app/src/main/res/values/donottranslate.xml), and add the language tag to `subtype_locale_exception_keys` and `subtype_locale_displayed_in_root_locale`.
|
||||
* If a newly added language does not use latin script, please update the default scripts method `Locale.script` in [ScriptUtils](app/src/main/java/helium314/keyboard/latin/utils/ScriptUtils.kt)
|
||||
|
||||
## Functional key layouts
|
||||
This is not yet customizable, but will be soon!
|
||||
Mostly customizing functional keys works like other layouts, with some specific adjustments:
|
||||
* you can either have a single layout for functional keys (default), or separate layouts for symbols and shift symbols
|
||||
* when using a single layout
|
||||
* emoji and language switch keys will only show in alphabet layout and when the option is enabled
|
||||
* numpad key will only show in symbols layout
|
||||
* otherwise the layout will be shown as it is in the layout file
|
||||
* use keys with `"type": "placeholder"` for
|
||||
* separating left and right functional keys (e.g. shift and delete in default layout)
|
||||
* separating top and bottom rows in case you want to have functional key rows aligned to the top of the keyboard
|
||||
|
|
Loading…
Add table
Reference in a new issue