Add keyboard parser for simple layout definitions (#270)

and one example layout
and some todos containing the further plan
This commit is contained in:
Helium314 2023-11-13 11:44:40 +01:00 committed by GitHub
parent 1e79e3e2f6
commit 9f67113216
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 971 additions and 118 deletions

View file

@ -51,38 +51,38 @@ public class Key implements Comparable<Key> {
private final String mHintLabel;
/** Flags of the label */
private final int mLabelFlags;
private static final int LABEL_FLAGS_ALIGN_HINT_LABEL_TO_BOTTOM = 0x02;
private static final int LABEL_FLAGS_ALIGN_ICON_TO_BOTTOM = 0x04;
private static final int LABEL_FLAGS_ALIGN_LABEL_OFF_CENTER = 0x08;
public static final int LABEL_FLAGS_ALIGN_HINT_LABEL_TO_BOTTOM = 0x02;
public static final int LABEL_FLAGS_ALIGN_ICON_TO_BOTTOM = 0x04;
public static final int LABEL_FLAGS_ALIGN_LABEL_OFF_CENTER = 0x08;
// Font typeface specification.
private static final int LABEL_FLAGS_FONT_MASK = 0x30;
private static final int LABEL_FLAGS_FONT_NORMAL = 0x10;
private static final int LABEL_FLAGS_FONT_MONO_SPACE = 0x20;
private static final int LABEL_FLAGS_FONT_DEFAULT = 0x30;
public static final int LABEL_FLAGS_FONT_NORMAL = 0x10;
public static final int LABEL_FLAGS_FONT_MONO_SPACE = 0x20;
public static final int LABEL_FLAGS_FONT_DEFAULT = 0x30;
// Start of key text ratio enum values
private static final int LABEL_FLAGS_FOLLOW_KEY_TEXT_RATIO_MASK = 0x1C0;
private static final int LABEL_FLAGS_FOLLOW_KEY_LARGE_LETTER_RATIO = 0x40;
private static final int LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO = 0x80;
private static final int LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO = 0xC0;
private static final int LABEL_FLAGS_FOLLOW_KEY_HINT_LABEL_RATIO = 0x140;
public static final int LABEL_FLAGS_FOLLOW_KEY_LARGE_LETTER_RATIO = 0x40;
public static final int LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO = 0x80;
public static final int LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO = 0xC0;
public static final int LABEL_FLAGS_FOLLOW_KEY_HINT_LABEL_RATIO = 0x140;
// End of key text ratio mask enum values
private static final int LABEL_FLAGS_HAS_POPUP_HINT = 0x200;
private static final int LABEL_FLAGS_HAS_SHIFTED_LETTER_HINT = 0x400;
private static final int LABEL_FLAGS_HAS_HINT_LABEL = 0x800;
public static final int LABEL_FLAGS_HAS_POPUP_HINT = 0x200;
public static final int LABEL_FLAGS_HAS_SHIFTED_LETTER_HINT = 0x400;
public static final int LABEL_FLAGS_HAS_HINT_LABEL = 0x800;
// The bit to calculate the ratio of key label width against key width. If autoXScale bit is on
// and autoYScale bit is off, the key label may be shrunk only for X-direction.
// If both autoXScale and autoYScale bits are on, the key label text size may be auto scaled.
private static final int LABEL_FLAGS_AUTO_X_SCALE = 0x4000;
private static final int LABEL_FLAGS_AUTO_Y_SCALE = 0x8000;
private static final int LABEL_FLAGS_AUTO_SCALE = LABEL_FLAGS_AUTO_X_SCALE
public static final int LABEL_FLAGS_AUTO_X_SCALE = 0x4000;
public static final int LABEL_FLAGS_AUTO_Y_SCALE = 0x8000;
public static final int LABEL_FLAGS_AUTO_SCALE = LABEL_FLAGS_AUTO_X_SCALE
| LABEL_FLAGS_AUTO_Y_SCALE;
private static final int LABEL_FLAGS_PRESERVE_CASE = 0x10000;
private static final int LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED = 0x20000;
private static final int LABEL_FLAGS_FROM_CUSTOM_ACTION_LABEL = 0x40000;
private static final int LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR = 0x80000;
private static final int LABEL_FLAGS_KEEP_BACKGROUND_ASPECT_RATIO = 0x100000;
private static final int LABEL_FLAGS_DISABLE_HINT_LABEL = 0x40000000;
private static final int LABEL_FLAGS_DISABLE_ADDITIONAL_MORE_KEYS = 0x80000000;
public static final int LABEL_FLAGS_PRESERVE_CASE = 0x10000;
public static final int LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED = 0x20000;
public static final int LABEL_FLAGS_FROM_CUSTOM_ACTION_LABEL = 0x40000;
public static final int LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR = 0x80000;
public static final int LABEL_FLAGS_KEEP_BACKGROUND_ASPECT_RATIO = 0x100000;
public static final int LABEL_FLAGS_DISABLE_HINT_LABEL = 0x40000000;
public static final int LABEL_FLAGS_DISABLE_ADDITIONAL_MORE_KEYS = 0x80000000;
/** Icon to display instead of a label. Icon takes precedence over a label */
private final int mIconId;
@ -282,9 +282,9 @@ public class Key implements Comparable<Key> {
mEnabled = keyParams.mEnabled;
// stuff to create
// interestingly it looks a little better when rounding horizontalGap to int immediately instead of using horizontalGapFloat
// but using float for determining mX and mWidth is more correct and keyboard ends up looking exactly like before introduction of KeyParams
final float horizontalGapFloat = isSpacer() ? 0 : keyParams.mKeyboardParams.mHorizontalGap;
// get the "correct" float gap: may shift keys by one pixel, but results in more uniform gaps between keys
final float horizontalGapFloat = isSpacer() ? 0 : (keyParams.mKeyboardParams.mRelativeHorizontalGap * keyParams.mKeyboardParams.mOccupiedWidth);
mHorizontalGap = Math.round(horizontalGapFloat);
mVerticalGap = Math.round(keyParams.mKeyboardParams.mVerticalGap);
mWidth = Math.round(keyParams.mFullWidth - horizontalGapFloat);
@ -435,16 +435,16 @@ public class Key implements Comparable<Key> {
}
private static String backgroundName(final int backgroundType) {
switch (backgroundType) {
case BACKGROUND_TYPE_EMPTY: return "empty";
case BACKGROUND_TYPE_NORMAL: return "normal";
case BACKGROUND_TYPE_FUNCTIONAL: return "functional";
case BACKGROUND_TYPE_STICKY_OFF: return "stickyOff";
case BACKGROUND_TYPE_STICKY_ON: return "stickyOn";
case BACKGROUND_TYPE_ACTION: return "action";
case BACKGROUND_TYPE_SPACEBAR: return "spacebar";
default: return null;
}
return switch (backgroundType) {
case BACKGROUND_TYPE_EMPTY -> "empty";
case BACKGROUND_TYPE_NORMAL -> "normal";
case BACKGROUND_TYPE_FUNCTIONAL -> "functional";
case BACKGROUND_TYPE_STICKY_OFF -> "stickyOff";
case BACKGROUND_TYPE_STICKY_ON -> "stickyOn";
case BACKGROUND_TYPE_ACTION -> "action";
case BACKGROUND_TYPE_SPACEBAR -> "spacebar";
default -> null;
};
}
public int getCode() {
@ -985,8 +985,10 @@ public class Key implements Comparable<Key> {
}
public void setDimensionsFromRelativeSize(final float newX, final float newY) {
if (mRelativeHeight == 0 || mRelativeWidth == 0)
throw new IllegalStateException("can't use setUsingRelativeHeight, not all fields are set");
if (mRelativeHeight == 0)
mRelativeHeight = mKeyboardParams.mDefaultRelativeRowHeight;
if (mRelativeWidth == 0)
mRelativeWidth = mKeyboardParams.mDefaultRelativeKeyWidth;
if (mRelativeHeight < 0)
// todo (later): deal with it properly when it needs to be adjusted, i.e. when changing moreKeys or moreSuggestions
throw new IllegalStateException("can't (yet) deal with absolute height");
@ -996,6 +998,30 @@ public class Key implements Comparable<Key> {
mFullHeight = mRelativeHeight * mKeyboardParams.mBaseHeight;
}
private static int getMoreKeysColumnAndFlags(final KeyboardParams params, final String[] moreKeys) {
// Get maximum column order number and set a relevant mode value.
int moreKeysColumnAndFlags = MORE_KEYS_MODE_MAX_COLUMN_WITH_AUTO_ORDER | params.mMaxMoreKeysKeyboardColumn;
int value;
if ((value = MoreKeySpec.getIntValue(moreKeys, MORE_KEYS_AUTO_COLUMN_ORDER, -1)) > 0) {
// Override with fixed column order number and set a relevant mode value.
moreKeysColumnAndFlags = MORE_KEYS_MODE_FIXED_COLUMN_WITH_AUTO_ORDER | (value & MORE_KEYS_COLUMN_NUMBER_MASK);
}
if ((value = MoreKeySpec.getIntValue(moreKeys, MORE_KEYS_FIXED_COLUMN_ORDER, -1)) > 0) {
// Override with fixed column order number and set a relevant mode value.
moreKeysColumnAndFlags = MORE_KEYS_MODE_FIXED_COLUMN_WITH_FIXED_ORDER | (value & MORE_KEYS_COLUMN_NUMBER_MASK);
}
if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_HAS_LABELS)) {
moreKeysColumnAndFlags |= MORE_KEYS_FLAGS_HAS_LABELS;
}
if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_NEEDS_DIVIDERS)) {
moreKeysColumnAndFlags |= MORE_KEYS_FLAGS_NEEDS_DIVIDERS;
}
if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_NO_PANEL_AUTO_MORE_KEY)) {
moreKeysColumnAndFlags |= MORE_KEYS_FLAGS_NO_PANEL_AUTO_MORE_KEY;
}
return moreKeysColumnAndFlags;
}
/**
* Create keyParams with the given top-left coordinate and extract its attributes from a key
* specification string, Key attribute array, key style, and etc.
@ -1040,30 +1066,7 @@ public class Key implements Comparable<Key> {
final Locale localeForUpcasing = params.mId.getLocale();
int actionFlags = style.getFlags(keyAttr, R.styleable.Keyboard_Key_keyActionFlags);
String[] moreKeys = style.getStringArray(keyAttr, R.styleable.Keyboard_Key_moreKeys);
// Get maximum column order number and set a relevant mode value.
int moreKeysColumnAndFlags = MORE_KEYS_MODE_MAX_COLUMN_WITH_AUTO_ORDER
| style.getInt(keyAttr, R.styleable.Keyboard_Key_maxMoreKeysColumn,
params.mMaxMoreKeysKeyboardColumn);
int value;
if ((value = MoreKeySpec.getIntValue(moreKeys, MORE_KEYS_AUTO_COLUMN_ORDER, -1)) > 0) {
// Override with fixed column order number and set a relevant mode value.
moreKeysColumnAndFlags = MORE_KEYS_MODE_FIXED_COLUMN_WITH_AUTO_ORDER | (value & MORE_KEYS_COLUMN_NUMBER_MASK);
}
if ((value = MoreKeySpec.getIntValue(moreKeys, MORE_KEYS_FIXED_COLUMN_ORDER, -1)) > 0) {
// Override with fixed column order number and set a relevant mode value.
moreKeysColumnAndFlags = MORE_KEYS_MODE_FIXED_COLUMN_WITH_FIXED_ORDER | (value & MORE_KEYS_COLUMN_NUMBER_MASK);
}
if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_HAS_LABELS)) {
moreKeysColumnAndFlags |= MORE_KEYS_FLAGS_HAS_LABELS;
}
if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_NEEDS_DIVIDERS)) {
moreKeysColumnAndFlags |= MORE_KEYS_FLAGS_NEEDS_DIVIDERS;
}
if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_NO_PANEL_AUTO_MORE_KEY)) {
moreKeysColumnAndFlags |= MORE_KEYS_FLAGS_NO_PANEL_AUTO_MORE_KEY;
}
mMoreKeysColumnAndFlags = moreKeysColumnAndFlags;
mMoreKeysColumnAndFlags = getMoreKeysColumnAndFlags(params, moreKeys);
final String[] additionalMoreKeys;
if ((mLabelFlags & LABEL_FLAGS_DISABLE_ADDITIONAL_MORE_KEYS) != 0) {
@ -1151,7 +1154,151 @@ public class Key implements Comparable<Key> {
mEnabled = true;
}
/** for <GridRows/> */
/**
* constructor that does not require attrs, style or absolute key dimension / position
* setDimensionsFromRelativeSize needs to be called before creating the key
*/
public KeyParams(
// todo (much later): replace keySpec? these encoded icons and codes are not really great
@NonNull final String keySpec, // key text or some special string for KeySpecParser, e.g. "!icon/shift_key|!code/key_shift" (avoid using !text, should be removed)
@NonNull final KeyboardParams params,
final float relativeWidth,
final int labelFlags,
final int backgroundType,
@Nullable final String[] layoutMoreKeys // same style as current moreKeys (relevant for the special keys)
) {
mKeyboardParams = params;
mBackgroundType = backgroundType;
mLabelFlags = labelFlags;
mRelativeWidth = relativeWidth;
mRelativeHeight = params.mDefaultRelativeRowHeight;
mMoreKeysColumnAndFlags = getMoreKeysColumnAndFlags(params, layoutMoreKeys);
mIconId = KeySpecParser.getIconId(keySpec);
final boolean needsToUpcase = needsToUpcase(mLabelFlags, params.mId.mElementId);
final Locale localeForUpcasing = params.mId.getLocale();
int actionFlags = 0;
final String[] languageMoreKeys;
if ((mLabelFlags & LABEL_FLAGS_DISABLE_ADDITIONAL_MORE_KEYS) != 0) {
languageMoreKeys = null;
} else {
// same style as additionalMoreKeys (i.e. moreKeys with the % placeholder(s))
// todo: read from assets or xml, and cache the results for quick reading again
languageMoreKeys = null; // todo: getLanguageMoreKeys(keySpec, mKeyboardParams.mId.getLocale());
}
final String[] finalMoreKeys = MoreKeySpec.insertAdditionalMoreKeys(languageMoreKeys, layoutMoreKeys);
if (finalMoreKeys != null) {
actionFlags |= ACTION_FLAGS_ENABLE_LONG_PRESS;
mMoreKeys = new MoreKeySpec[finalMoreKeys.length];
for (int i = 0; i < finalMoreKeys.length; i++) {
mMoreKeys[i] = new MoreKeySpec(finalMoreKeys[i], needsToUpcase, localeForUpcasing);
}
} else {
mMoreKeys = null;
}
final int code = KeySpecParser.getCode(keySpec);
if ((mLabelFlags & LABEL_FLAGS_FROM_CUSTOM_ACTION_LABEL) != 0) {
mLabel = params.mId.mCustomActionLabel;
} else if (code >= Character.MIN_SUPPLEMENTARY_CODE_POINT) {
// This is a workaround to have a key that has a supplementary code point in its label.
// Because we can put a string in resource neither as a XML entity of a supplementary
// code point nor as a surrogate pair.
mLabel = new StringBuilder().appendCodePoint(code).toString();
} else {
final String label = KeySpecParser.getLabel(keySpec);
mLabel = needsToUpcase
? StringUtils.toTitleCaseOfKeyLabel(label, localeForUpcasing)
: label;
}
if ((mLabelFlags & LABEL_FLAGS_DISABLE_HINT_LABEL) != 0) {
mHintLabel = null;
} else {
// maybe also always null for comma and period keys
final boolean hintLabelAlwaysFromFirstLongPressKey = false; // todo (later): add the setting, and use it (store in params?)
String hintLabel;
if (hintLabelAlwaysFromFirstLongPressKey) {
hintLabel = mMoreKeys == null ? null : mMoreKeys[0].mLabel;
} else {
hintLabel = layoutMoreKeys == null ? null : layoutMoreKeys[0];
if (hintLabel != null && hintLabel.length() > 1 && hintLabel.startsWith("!")) // this is not great, but other than removing com key label this is definitely ok
hintLabel = null;
if (hintLabel != null && hintLabel.length() == 2 && hintLabel.startsWith("\\"))
hintLabel = hintLabel.replace("\\", "");
}
mHintLabel = needsToUpcase
? StringUtils.toTitleCaseOfKeyLabel(hintLabel, localeForUpcasing)
: hintLabel;
}
String outputText = KeySpecParser.getOutputText(keySpec);
if (needsToUpcase) {
outputText = StringUtils.toTitleCaseOfKeyLabel(outputText, localeForUpcasing);
}
// Choose the first letter of the label as primary code if not specified.
if (code == CODE_UNSPECIFIED && TextUtils.isEmpty(outputText) && !TextUtils.isEmpty(mLabel)) {
if (StringUtils.codePointCount(mLabel) == 1) {
// Use the first letter of the hint label if shiftedLetterActivated flag is
// specified.
if ((mLabelFlags & LABEL_FLAGS_HAS_SHIFTED_LETTER_HINT) != 0 && (mLabelFlags & LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED) != 0
&& !TextUtils.isEmpty(mHintLabel)) {
mCode = mHintLabel.codePointAt(0);
} else {
mCode = mLabel.codePointAt(0);
}
} else {
// In some locale and case, the character might be represented by multiple code
// points, such as upper case Eszett of German alphabet.
outputText = mLabel;
mCode = CODE_OUTPUT_TEXT;
}
} else if (code == CODE_UNSPECIFIED && outputText != null) {
if (StringUtils.codePointCount(outputText) == 1) {
mCode = outputText.codePointAt(0);
outputText = null;
} else {
mCode = CODE_OUTPUT_TEXT;
}
} else {
mCode = needsToUpcase ? StringUtils.toTitleCaseOfKeyCode(code, localeForUpcasing) : code;
}
// action flags don't need to be specified, they can be deduced from the key
if (backgroundType == BACKGROUND_TYPE_SPACEBAR || mCode == Constants.CODE_LANGUAGE_SWITCH)
actionFlags |= ACTION_FLAGS_ENABLE_LONG_PRESS;
if (mCode <= Constants.CODE_SPACE && mCode != CODE_OUTPUT_TEXT)
actionFlags |= ACTION_FLAGS_NO_KEY_PREVIEW;
if (mCode == Constants.CODE_DELETE)
actionFlags |= ACTION_FLAGS_IS_REPEATABLE;
if (mCode == Constants.CODE_SETTINGS || mCode == Constants.CODE_LANGUAGE_SWITCH)
actionFlags |= ACTION_FLAGS_ALT_CODE_WHILE_TYPING;
mActionFlags = actionFlags;
// todo: for what it is actually used? maybe it could be removed?
final int altCodeInAttr; // settings and language switch keys have alt code space, all others nothing
if (mCode == Constants.CODE_SETTINGS || mCode == Constants.CODE_LANGUAGE_SWITCH)
altCodeInAttr = Constants.CODE_SPACE;
else
altCodeInAttr = CODE_UNSPECIFIED;
final int altCode = needsToUpcase
? StringUtils.toTitleCaseOfKeyCode(altCodeInAttr, localeForUpcasing)
: altCodeInAttr;
mOptionalAttributes = OptionalAttributes.newInstance(outputText, altCode,
// disabled icon only ever for old version of shortcut key, visual insets can be replaced with spacer
// todo (much later): can the 3 below be removed completely?
KeyboardIconsSet.ICON_UNDEFINED, 0, 0);
// KeyVisualAttributes for a key essentially are what the theme has, but on a per-key base
// could be used e.g. for having a color gradient on key color
// where is it used / which attribute?
// keyLetterSize in some keyboards
// keyShiftedLetterHintRatio same
// keyHintLabelVerticalAdjustment same
// todo (later): make sure these keys look ok when migrating the non-latin layouts (+pc qwerty)
mKeyVisualAttributes = null;
mEnabled = true;
}
/** constructor for <GridRows/> */
public KeyParams(@Nullable final String label, final int code, @Nullable final String outputText,
@Nullable final String hintLabel, @Nullable final String moreKeySpecs,
final int labelFlags, final int backgroundType, final int x, final int y,
@ -1167,28 +1314,7 @@ public class Key implements Comparable<Key> {
if (moreKeySpecs != null) {
String[] moreKeys = MoreKeySpec.splitKeySpecs(moreKeySpecs);
// Get maximum column order number and set a relevant mode value.
int moreKeysColumnAndFlags = MORE_KEYS_MODE_MAX_COLUMN_WITH_AUTO_ORDER | params.mMaxMoreKeysKeyboardColumn;
int value;
if ((value = MoreKeySpec.getIntValue(moreKeys, MORE_KEYS_AUTO_COLUMN_ORDER, -1)) > 0) {
// Override with fixed column order number and set a relevant mode value.
moreKeysColumnAndFlags = MORE_KEYS_MODE_FIXED_COLUMN_WITH_AUTO_ORDER
| (value & MORE_KEYS_COLUMN_NUMBER_MASK);
}
if ((value = MoreKeySpec.getIntValue(moreKeys, MORE_KEYS_FIXED_COLUMN_ORDER, -1)) > 0) {
// Override with fixed column order number and set a relevant mode value.
moreKeysColumnAndFlags = MORE_KEYS_MODE_FIXED_COLUMN_WITH_FIXED_ORDER | (value & MORE_KEYS_COLUMN_NUMBER_MASK);
}
if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_HAS_LABELS)) {
moreKeysColumnAndFlags |= MORE_KEYS_FLAGS_HAS_LABELS;
}
if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_NEEDS_DIVIDERS)) {
moreKeysColumnAndFlags |= MORE_KEYS_FLAGS_NEEDS_DIVIDERS;
}
if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_NO_PANEL_AUTO_MORE_KEY)) {
moreKeysColumnAndFlags |= MORE_KEYS_FLAGS_NO_PANEL_AUTO_MORE_KEY;
}
mMoreKeysColumnAndFlags = moreKeysColumnAndFlags;
mMoreKeysColumnAndFlags = getMoreKeysColumnAndFlags(params, moreKeys);
moreKeys = MoreKeySpec.insertAdditionalMoreKeys(moreKeys, null);
int actionFlags = 0;

View file

@ -16,7 +16,6 @@ import androidx.core.content.ContextCompat;
import org.dslul.openboard.inputmethod.latin.R;
import org.dslul.openboard.inputmethod.latin.common.Colors;
import org.dslul.openboard.inputmethod.latin.settings.Settings;
import org.dslul.openboard.inputmethod.latin.utils.ColorUtilKt;
import org.dslul.openboard.inputmethod.latin.utils.DeviceProtectedUtils;
import java.util.Arrays;
@ -137,29 +136,41 @@ public final class KeyboardTheme implements Comparable<KeyboardTheme> {
return KEYBOARD_THEMES[DEFAULT_THEME_ID];
}
public static int getThemeActionAndEmojiKeyLabelFlags(final int themeId) {
if (themeId == THEME_ID_LXX_BASE || themeId == THEME_ID_ROUNDED_BASE)
return Key.LABEL_FLAGS_KEEP_BACKGROUND_ASPECT_RATIO;
return 0;
}
public static Colors getThemeColors(final String themeColors, final String themeStyle, final Context context, final SharedPreferences prefs) {
final boolean hasBorders = prefs.getBoolean(Settings.PREF_THEME_KEY_BORDERS, false);
switch (themeColors) {
case THEME_USER:
final int accent = Settings.readUserColor(prefs, context, Settings.PREF_COLOR_ACCENT_SUFFIX, false);
final int keyBgColor = Settings.readUserColor(prefs, context, Settings.PREF_COLOR_KEYS_SUFFIX, false);
final int functionalKeyBgColor = Settings.readUserColor(prefs, context, Settings.PREF_COLOR_FUNCTIONAL_KEYS_SUFFIX, false);
final int spaceBarBgColor = Settings.readUserColor(prefs, context, Settings.PREF_COLOR_SPACEBAR_SUFFIX, false);
final int keyTextColor = Settings.readUserColor(prefs, context, Settings.PREF_COLOR_TEXT_SUFFIX, false);
final int hintTextColor = Settings.readUserColor(prefs, context, Settings.PREF_COLOR_HINT_TEXT_SUFFIX, false);
final int spaceBarTextColor = Settings.readUserColor(prefs, context, Settings.PREF_COLOR_SPACEBAR_TEXT_SUFFIX, false);
final int background = Settings.readUserColor(prefs, context, Settings.PREF_COLOR_BACKGROUND_SUFFIX, false);
return new Colors(themeStyle, hasBorders, accent, background, keyBgColor, functionalKeyBgColor, spaceBarBgColor, keyTextColor, hintTextColor, spaceBarTextColor);
return new Colors(
themeStyle,
hasBorders,
Settings.readUserColor(prefs, context, Settings.PREF_COLOR_ACCENT_SUFFIX, false),
Settings.readUserColor(prefs, context, Settings.PREF_COLOR_BACKGROUND_SUFFIX, false),
Settings.readUserColor(prefs, context, Settings.PREF_COLOR_KEYS_SUFFIX, false),
Settings.readUserColor(prefs, context, Settings.PREF_COLOR_FUNCTIONAL_KEYS_SUFFIX, false),
Settings.readUserColor(prefs, context, Settings.PREF_COLOR_SPACEBAR_SUFFIX, false),
Settings.readUserColor(prefs, context, Settings.PREF_COLOR_TEXT_SUFFIX, false),
Settings.readUserColor(prefs, context, Settings.PREF_COLOR_HINT_TEXT_SUFFIX, false),
Settings.readUserColor(prefs, context, Settings.PREF_COLOR_SPACEBAR_TEXT_SUFFIX, false)
);
case THEME_USER_NIGHT:
final int accent2 = Settings.readUserColor(prefs, context, Settings.PREF_COLOR_ACCENT_SUFFIX, true);
final int keyBgColor2 = Settings.readUserColor(prefs, context, Settings.PREF_COLOR_KEYS_SUFFIX, true);
final int functionalKeyBgColor2 = Settings.readUserColor(prefs, context, Settings.PREF_COLOR_FUNCTIONAL_KEYS_SUFFIX, true);
final int spaceBarBgColor2 = Settings.readUserColor(prefs, context, Settings.PREF_COLOR_SPACEBAR_SUFFIX, true);
final int keyTextColor2 = Settings.readUserColor(prefs, context, Settings.PREF_COLOR_TEXT_SUFFIX, true);
final int hintTextColor2 = Settings.readUserColor(prefs, context, Settings.PREF_COLOR_HINT_TEXT_SUFFIX, true);
final int spaceBarTextColor2 = Settings.readUserColor(prefs, context, Settings.PREF_COLOR_SPACEBAR_TEXT_SUFFIX, true);
final int background2 = Settings.readUserColor(prefs, context, Settings.PREF_COLOR_BACKGROUND_SUFFIX, true);
return new Colors(themeStyle, hasBorders, accent2, background2, keyBgColor2, functionalKeyBgColor2, spaceBarBgColor2, keyTextColor2, hintTextColor2, spaceBarTextColor2);
return new Colors(
themeStyle,
hasBorders,
Settings.readUserColor(prefs, context, Settings.PREF_COLOR_ACCENT_SUFFIX, true),
Settings.readUserColor(prefs, context, Settings.PREF_COLOR_BACKGROUND_SUFFIX, true),
Settings.readUserColor(prefs, context, Settings.PREF_COLOR_KEYS_SUFFIX, true),
Settings.readUserColor(prefs, context, Settings.PREF_COLOR_FUNCTIONAL_KEYS_SUFFIX, true),
Settings.readUserColor(prefs, context, Settings.PREF_COLOR_SPACEBAR_SUFFIX, true),
Settings.readUserColor(prefs, context, Settings.PREF_COLOR_TEXT_SUFFIX, true),
Settings.readUserColor(prefs, context, Settings.PREF_COLOR_HINT_TEXT_SUFFIX, true),
Settings.readUserColor(prefs, context, Settings.PREF_COLOR_SPACEBAR_TEXT_SUFFIX, true)
);
case THEME_DARK:
return new Colors(
themeStyle,

View file

@ -420,8 +420,7 @@ public class KeyboardView extends View {
paint.setTextAlign(Align.CENTER);
}
if (key.needsAutoXScale()) {
final float ratio = Math.min(1.0f, (keyWidth * MAX_LABEL_RATIO) /
TypefaceUtils.getStringWidth(label, paint));
final float ratio = Math.min(1.0f, (keyWidth * MAX_LABEL_RATIO) / TypefaceUtils.getStringWidth(label, paint));
if (key.needsAutoScale()) {
final float autoSize = paint.getTextSize() * ratio;
paint.setTextSize(autoSize);

View file

@ -13,10 +13,12 @@ import org.dslul.openboard.inputmethod.keyboard.Key
import org.dslul.openboard.inputmethod.keyboard.Key.KeyParams
import org.dslul.openboard.inputmethod.keyboard.Keyboard
import org.dslul.openboard.inputmethod.keyboard.KeyboardId
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.SimpleKeyboardParser
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.XmlKeyboardParser
import org.dslul.openboard.inputmethod.latin.R
import org.dslul.openboard.inputmethod.latin.common.Constants
import org.dslul.openboard.inputmethod.latin.settings.Settings
import org.dslul.openboard.inputmethod.latin.utils.sumOf
import org.xmlpull.v1.XmlPullParserException
import java.io.IOException
@ -42,6 +44,120 @@ open class KeyboardBuilder<KP : KeyboardParams>(protected val mContext: Context,
mParams.mAllowRedundantMoreKeys = enabled
}
fun loadSimpleKeyboard(id: KeyboardId): KeyboardBuilder<KP> {
mParams.mId = id
keysInRows = SimpleKeyboardParser(mParams, mContext).parseFromAssets("qwerty")
useRelative()
// todo: further plan to make is actually useful
// create languageMoreKeys list from stuff in keyboard-text tools
// probably use files in assets, and cache them in a weak hash map with localestring as key
// or better 2 letter code, and join codes when combining languageMoreKeys for multiple locales
// or maybe locale tag, but that's super annoying for api < 24(?)
// or no caching if loading and combining is fast anyway (need to test)
// the locale morekeys then should be a map label -> moreKeys
// the whole moreKeys map for the current keyboard could be in mParams to simplify access when creating keys
// file format? it's easy to switch, but still... text like above? json?
// or use resources? could look like donottranslate-more-keys files
// should be possible with configuration and contextThemeWrapper, but probably more complicated than simple files
// also would be a bit annoying as it would require to have empty base strings for all possible keys
// test first whether something like morekeys_&#x1002;, or morekeys_&#x00F8; or better morekeys_ø actually works
// if not, definitely don't use resources
// consider the % placeholder, this should still be used and documented
// though maybe has issues when merging languages?
// how to deal with unnecessary moreKeys?
// e.g. german should have ö as moreKey on o, but swiss german layout has ö as separate key
// still have ö on o (like now), or remove it? or make it optional?
// is this handled by KeyboardParams.removeRedundantMoreKeys?
// not only moreKeys, also currency key and some labels keys should be translated, though not necessarily in that map
// need some placeholder for currency key, like $$$
// have an explicit all-more-keys definition, which is created from a script merging all available moreKeys
// only letter forms and nothing else, right?
// maybe some most-but-not-all? e.g. only all that occur for more than one language
// migrate latin layouts to this style (need to make exception for pcqwerty!)
// finalize simple layout format
// keep like now: nice, because simple and allows defining any number of moreKeys
// rows of letters, separated with space: very straightforward, but moreKeys are annoying and only one possible
// consider the current layout maybe doesn't have the correct moreKeys
// where to actually get the current keyboard layout name, so it can be used to select the correct file?
// maybe KeyboardLayoutSet will need to be replaced
// need to solve the scaling issue with number row and 5 row keyboards
// allow users to switch to old style (keep it until all layouts are switched)
// really helps to find differences
// add a text that issues / unwanted differences should be reported, as the setting will be removed at some point
// label flags to do (top part is for latin!)
// allow users to define their own layouts
// write up how things work for users, also regarding language more keys
// readme, maybe also some "help" button in a dialog
// some sort of proper UI, or simply text input?
// better text import for the start because of much work
// ui follows later (consider that users need to be able to start from existing layouts!)
// some warning if more than 2 or 3 characters on a single label
// currently can't resize keys, but could set autoXScale (does only decrease size, never increase)
// careful about moreKeys: if moreKeys don't fit on screen, parser throws an exception!
// need to somehow test for this
// is that autoColumnOrder thing a workaround for that?
// still would crash for a single huge label
// popup and (single key) long press preview rescale the label on x only, which may deform emojis
// migrate symbol layouts to this style
// maybe allow users to define their own symbol and shift-symbol layouts
// migrate emoji layouts to this style
// emojis are defined in that string array, should be simple to handle
// parsing could be done into a single row, which is then split as needed
// this might help with split layout (no change in key size, but in number of rows!)
// write another parser, it should already consider split
// more dynamic / lazy way for loading the 10 emoji keyboards?
// use recyclerView instead of a keyboard?
// or recyclerView with one keyboardView per row?
// could be possible if creating the keyboards is fast enough... but also need to check whether it's ok for memory use and stuff
// migrate keypad layouts to this style
// will need more configurable layout definition -> another parser
// migrate moreKeys and moreSuggestions to this style?
// at least they should not make use of the KeyTextsSet/Table and of the XmlKeyboardParser
// migrate other languages to this style
// may be difficult in some cases, like additional row, or no shift key, or pc qwerty layout
// also the (integrated) number row might cause issues
// at least some of these layouts will need more complicated definition, not just a simple text file
// remove all the keyboard layout related xmls if possible
// rows_, rowkeys_, row_, kbd_ maybe keyboard_layout_set, keys_, keystyle_, key_
// and the texts_table and its source tools
// todo: label flags
// alignHintLabelToBottom -> what does it do?
// fontNormal -> check / compare turkish layout
// fontDefault -> check exclamation and question keys
// hasShiftedLetterHint, shiftedLetterActivated -> what is the effect on period key?
// labelFlags should be set correctly
// alignHintLabelToBottom: on lxx and rounded themes
// alignIconToBottom: space_key_for_number_layout
// alignLabelOffCenter: number keys in phone layout
// fontNormal: turkish (rows 1 and 2 only), .com, emojis, numModeKeyStyle, a bunch of non-latin languages
// fontMonoSpace: unused (not really: fontDefault is monospace + normal)
// fontDefault: keyExclamationQuestion, a bunch of "normal" keys in fontNormal layouts like thai
// followKeyLargeLetterRatio: number keys in number/phone/numpad layouts
// followKeyLetterRatio: mode keys in number layouts, some keys in some non-latin layouts
// followKeyLabelRatio: enter key, some keys in phone layout (same as followKeyLetterRatio + followKeyLargeLetterRatio)
// followKeyHintLabelRatio: unused directly (but includes some others)
// hasPopupHint: basically the long-pressable functional keys
// hasShiftedLetterHint: period key and some keys on pcqwerty
// hasHintLabel: number keys in number layouts
// autoXScale: com key, action keys, some on phone layout, some non-latin languages
// autoScale: only one single letter in khmer layout (includes autoXScale)
// preserveCase: action key + more keys, com key, shift keys
// shiftedLetterActivated: period and some keys on pcqwerty, tablet only
// fromCustomActionLabel: action key with customLabelActionKeyStyle -> check parser where to get this info
// followFunctionalTextColor: number mode keys, action key
// keepBackgroundAspectRatio: lxx and rounded action more keys, lxx no-border action and emoji, moreKeys keyboard view
// disableKeyHintLabel: keys in pcqwerty row 1 and number row
// disableAdditionalMoreKeys: keys in pcqwerty row 1
// -> probably can't define the related layouts in a simple way, better use some json or xml or anything more reasonable than the simple text format
// maybe remove some of the flags? or keep supporting them?
// for pcqwerty: hasShiftedLetterHint -> hasShiftedLetterHint|shiftedLetterActivated when shift is enabled, need to consider if the flag is used
// actually period key also has shifted letter hint
return this
}
fun loadFromXml(xmlId: Int, id: KeyboardId): KeyboardBuilder<KP> {
mParams.mId = id
// loading a keyboard should set default params like mParams.readAttributes(mContext, attrs);
@ -84,16 +200,19 @@ open class KeyboardBuilder<KP : KeyboardParams>(protected val mContext: Context,
// still should not be more than a pixel difference
// keep it around for a while, for testing
private fun useRelative() {
var currentY = mParams.mTopPadding.toFloat()
for (row in keysInRows) {
if (row.isEmpty()) continue
fillGapsWithSpacers(row)
val y = row.first().yPos
assert(row.all { it.yPos == y })
var currentX = 0f
row.forEach {
it.setDimensionsFromRelativeSize(currentX, y)
it.setDimensionsFromRelativeSize(currentX, currentY)
currentX += it.mFullWidth
}
// need to truncate to int here, otherwise it may end up one pixel lower than original
// though actually not truncating would be more correct... but that's already an y / height issue somewhere in Key
// todo (later): round, and do the change together with the some thing in Key(KeyParams keyParams)
currentY += row.first().mFullHeight.toInt()
}
}
@ -103,6 +222,7 @@ open class KeyboardBuilder<KP : KeyboardParams>(protected val mContext: Context,
private fun fillGapsWithSpacers(row: MutableList<KeyParams>) {
if (mParams.mId.mElementId !in KeyboardId.ELEMENT_ALPHABET..KeyboardId.ELEMENT_SYMBOLS_SHIFTED) return
if (row.isEmpty()) return
if (row.all { it.xPos == 0f }) return // need existing xPos to determine gaps
var currentX = 0f + mParams.mLeftPadding
var i = 0
while (i < row.size) {
@ -258,6 +378,9 @@ open class KeyboardBuilder<KP : KeyboardParams>(protected val mContext: Context,
startRow()
for (keyParams in row) {
endKey(keyParams.createKey())
// todo (later): markAsBottomKey if in bottom row?
// this is not done in original parsing style, but why not?
// just test it (with different bottom paddings)
}
endRow()
}
@ -268,12 +391,3 @@ open class KeyboardBuilder<KP : KeyboardParams>(protected val mContext: Context,
private const val BUILDER_TAG = "Keyboard.Builder"
}
}
// adapted from Kotlin source: https://github.com/JetBrains/kotlin/blob/7a7d392b3470b38d42f80c896b7270678d0f95c3/libraries/stdlib/common/src/generated/_Collections.kt#L3004
private inline fun <T> Iterable<T>.sumOf(selector: (T) -> Float): Float {
var sum = 0f
for (element in this) {
sum += selector(element)
}
return sum
}

View file

@ -0,0 +1,540 @@
// SPDX-License-Identifier: GPL-3.0-only
package org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser
import android.content.Context
import android.view.inputmethod.EditorInfo
import org.dslul.openboard.inputmethod.keyboard.Key
import org.dslul.openboard.inputmethod.keyboard.Key.KeyParams
import org.dslul.openboard.inputmethod.keyboard.KeyboardId
import org.dslul.openboard.inputmethod.keyboard.KeyboardTheme
import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardIconsSet
import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardParams
import org.dslul.openboard.inputmethod.latin.R
import org.dslul.openboard.inputmethod.latin.utils.InputTypeUtils
import org.dslul.openboard.inputmethod.latin.utils.sumOf
/**
* Parser for simple layouts like qwerty or symbol, defined only as rows of (normal) keys with moreKeys.
* Functional keys are pre-defined and can't be changed, with exception of comma, period and similar
* keys in symbol layouts.
* There may be a short "extra row" for the configurable keys in the bottom row. This is two keys
* for alphabet, 3 keys for symbols and 4 keys for shift symbols. MoreKeys on period and comma get
* merged with defaults.
* All normal keys have the same width and flags, which likely makes the simple layout definitions
* incompatible with the requirements of certain (non-latin) languages. These languages need to use
* a different (more configurable) layout definition style, and therefore a different parser.
* Also number, phone and numpad layouts are not compatible with this parser.
*/
class SimpleKeyboardParser(private val params: KeyboardParams, private val context: Context) {
fun parseFromAssets(layoutName: String) =
parse(context.assets.open("layouts/$layoutName.txt").reader().readText())
fun parse(layoutContent: String): ArrayList<ArrayList<KeyParams>> {
params.readAttributes(context, null)
val keysInRows = ArrayList<ArrayList<KeyParams>>()
val baseKeys: MutableList<List<BaseKey>> = parseAdjustablePartOfLayout(layoutContent)
if (!params.mId.mNumberRowEnabled) {
// todo (later): not all layouts have numbers on first row, so maybe have some layout flag to switch it off (or an option)
// but for latin it's fine, so don't care now
val newFirstRow = baseKeys.first().mapIndexed { index, baseKey ->
if (index < numbers.size)
BaseKey(baseKey.label, baseKey.moreKeys?.let { arrayOf(numbers[index], *it) })
else baseKey
}
baseKeys[0] = newFirstRow
}
val functionalKeysReversed = parseFunctionalKeys().reversed()
// keyboard parsed bottom-up because the number of rows is not fixed, but the functional keys
// are always added to the rows near the bottom
keysInRows.add(getBottomRowAndAdjustBaseKeys(baseKeys))
baseKeys.reversed().forEachIndexed { i, row ->
// parse functional keys for this row (if any)
val functionalKeysDefs = if (i < functionalKeysReversed.size) functionalKeysReversed[i]
else emptyList<String>() to emptyList()
val functionalKeysLeft = functionalKeysDefs.first.map { getFunctionalKeyParams(it) }
val functionalKeysRight = functionalKeysDefs.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 availableWidth = 1f - (functionalKeysLeft.sumOf { it.mRelativeWidth }) - (functionalKeysRight.sumOf { it.mRelativeWidth })
val width: Float
val spacerWidth: Float
if (availableWidth - usedKeyWidth > 0.0001f) { // don't add spacers if only a tiny bit is empty
// width available, add spacer
width = params.mDefaultRelativeKeyWidth
spacerWidth = (availableWidth - usedKeyWidth) / 2
} else {
// need more width, re-scale
spacerWidth = 0f
width = availableWidth / row.size
}
if (spacerWidth != 0f) {
paramsRow.add(KeyParams.newSpacer(params).apply { mRelativeWidth = spacerWidth })
}
for (key in row) {
paramsRow.add(KeyParams(
key.label,
params,
width, // any reasonable way to scale width if there is a long text? might be allowed in user-defined layout
0, // todo: maybe autoScale / autoXScale if label has more than 2 characters (exception for emojis?)
Key.BACKGROUND_TYPE_NORMAL,
key.moreKeys
))
}
if (spacerWidth != 0f) {
paramsRow.add(KeyParams.newSpacer(params).apply { mRelativeWidth = spacerWidth })
}
functionalKeysRight.forEach { paramsRow.add(it) }
keysInRows.add(0, paramsRow) // we're doing it backwards, so add on top
}
// rescale height if we have more than 4 rows
val heightRescale = if (keysInRows.size > 4) 4f / keysInRows.size else 1f
if (params.mId.mNumberRowEnabled)
keysInRows.add(0, getNumberRow())
if (heightRescale != 1f)
// rescale all keys, so number row doesn't look weird (this is done like in current parsing)
// todo: in symbols view, number row is not rescaled
// so the symbols keyboard is higher than the normal one
// not a new issue, but should be solved in this migration
// how? possibly scale all keyboards to height of main alphabet? (consider suggestion strip)
keysInRows.forEach { it.forEach { it.mRelativeHeight *= heightRescale } }
return keysInRows
}
private fun parseAdjustablePartOfLayout(layoutContent: String) =
layoutContent.split("\n\n").mapTo(mutableListOf()) { row -> row.split("\n").mapNotNull {
if (it.isBlank()) return@mapNotNull null
val split = it.split(" ")
val moreKeys = if (split.size == 1) null else Array(split.size - 1) { split[it + 1] }
BaseKey(split.first(), moreKeys)
} }
private fun parseFunctionalKeys(): List<Pair<List<String>, List<String>>> =
context.getString(R.string.key_def_functional).split("\n").mapNotNull { line ->
if (line.isBlank()) return@mapNotNull null
val p = line.split(";")
p.first().let { if (it.isBlank()) emptyList() else it.split(",") } to
p.last().let { if (it.isBlank()) emptyList() else it.split(",") }
}
private fun getBottomRowAndAdjustBaseKeys(baseKeys: MutableList<List<BaseKey>>): 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().split(" ").first()
val adjustKey = when (key) {
KEY_COMMA -> adjustedKeys?.first()
KEY_PERIOD -> adjustedKeys?.last()
else -> null
}
val keyParams = getFunctionalKeyParams(it, adjustKey?.label, adjustKey?.moreKeys)
if (key == KEY_SPACE) { // add the extra keys around space
if (params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS) {
bottomRow.add(getFunctionalKeyParams(KEY_NUMPAD))
bottomRow.add(keyParams)
bottomRow.add(KeyParams(
adjustedKeys?.get(1)?.label ?: "/",
params,
params.mDefaultRelativeKeyWidth,
0,
Key.BACKGROUND_TYPE_FUNCTIONAL,
adjustedKeys?.get(1)?.moreKeys
))
} else if (params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS_SHIFTED) {
bottomRow.add(KeyParams(
adjustedKeys?.get(1)?.label ?: "<",
params,
params.mDefaultRelativeKeyWidth,
0,
Key.BACKGROUND_TYPE_FUNCTIONAL,
adjustedKeys?.get(1)?.moreKeys
))
bottomRow.add(keyParams)
bottomRow.add(KeyParams(
adjustedKeys?.get(2)?.label ?: ">",
params,
params.mDefaultRelativeKeyWidth,
0,
Key.BACKGROUND_TYPE_FUNCTIONAL,
adjustedKeys?.get(2)?.moreKeys
))
} else { // alphabet
if (params.mId.mLanguageSwitchKeyEnabled)
bottomRow.add(getFunctionalKeyParams(KEY_LANGUAGE_SWITCH))
if (params.mId.mEmojiKeyEnabled)
bottomRow.add(getFunctionalKeyParams(KEY_EMOJI))
bottomRow.add(keyParams)
// todo (later): add zwnj if necessary (where to get that info? layout file? then likely will not happen in this parser)
}
} 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
}
// todo: everything below here likely can and should be shared with the planned parser for more complicated layouts
// abstract class?
// interface?
// utils file?
private fun getNumberRow(): ArrayList<KeyParams> {
val row = ArrayList<KeyParams>()
numbers.forEachIndexed { i, n ->
row.add(KeyParams(
n,
params,
params.mDefaultRelativeKeyWidth,
Key.LABEL_FLAGS_DISABLE_HINT_LABEL, // todo (later): maybe optional or enable (but then all numbers should have hints)
Key.BACKGROUND_TYPE_NORMAL,
numbersMoreKeys[i] // todo (later, non-latin): language may add some (either alt numbers, or latin numbers if they are replaced above, see number todo)
))
}
return row
}
// for comma and period: label will override default, moreKeys will be appended
private fun getFunctionalKeyParams(def: String, label: String? = null, moreKeys: Array<String>? = null): KeyParams {
val split = def.trim().split(" ")
val key = split[0]
val width = if (split.size == 2) split[1].substringBefore("%").toFloat() / 100f
else params.mDefaultRelativeKeyWidth
return when (key) {
KEY_SYMBOL -> KeyParams(
"${getSymbolLabel()}|!code/key_switch_alpha_symbol", // todo (later): in numpad the code is key_symbolNumpad
params,
width,
Key.LABEL_FLAGS_PRESERVE_CASE or Key.LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR,
Key.BACKGROUND_TYPE_FUNCTIONAL,
null
)
KEY_COMMA -> KeyParams(
label ?: getDefaultCommaLabel(),
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 else Key.BACKGROUND_TYPE_FUNCTIONAL,
moreKeys?.let { getCommaMoreKeys() + it } ?: getCommaMoreKeys()
)
KEY_SPACE -> KeyParams(
"!icon/space_key|!code/key_space", // !icon/space_key_for_number_layout in number layout, but not on tablet
params,
width, // will not be used for normal space (only in number layouts)
0, // todo (later): alignIconToBottom for non-tablet number layout
Key.BACKGROUND_TYPE_SPACEBAR,
null
)
KEY_PERIOD -> KeyParams(
label ?: ".",
params,
width,
Key.LABEL_FLAGS_HAS_POPUP_HINT or Key.LABEL_FLAGS_HAS_SHIFTED_LETTER_HINT, // todo (later): check what LABEL_FLAGS_HAS_SHIFTED_LETTER_HINT does, maybe remove the flag here
if (label?.first()?.isLetter() == true) Key.BACKGROUND_TYPE_NORMAL else Key.BACKGROUND_TYPE_FUNCTIONAL,
moreKeys?.let { getPeriodMoreKeys() + it } ?: getPeriodMoreKeys()
)
KEY_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 KeyboardTheme.getThemeActionAndEmojiKeyLabelFlags(params.mThemeId),
Key.BACKGROUND_TYPE_ACTION,
getActionKeyMoreKeys()
)
KEY_DELETE -> KeyParams(
"!icon/delete_key|!code/key_delete",
params,
width,
0,
Key.BACKGROUND_TYPE_FUNCTIONAL,
null
)
KEY_SHIFT -> KeyParams(
"${getShiftLabel()}|!code/key_shift",
params,
width,
Key.LABEL_FLAGS_PRESERVE_CASE,
// todo (later): possibly the whole stickOn/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,
arrayOf("!noPanelAutoMoreKey!", " |!code/key_capslock")
)
KEY_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
KEY_EMOJI_COM -> if (params.mId.mMode == KeyboardId.MODE_URL || params.mId.mMode == KeyboardId.MODE_EMAIL)
getFunctionalKeyParams(KEY_COM)
else getFunctionalKeyParams(KEY_EMOJI)
KEY_COM -> KeyParams(
".com", // todo: should depend on language
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,
arrayOf("!hasLabels!", ".net", ".org", ".gov", ".edu") // todo: maybe should be in languageMoreKeys
)
KEY_LANGUAGE_SWITCH -> KeyParams(
"!icon/language_switch_key|!code/key_language_switch",
params,
width,
0,
Key.BACKGROUND_TYPE_FUNCTIONAL,
null
)
KEY_ALPHA -> KeyParams(
"${getAlphabetLabel()}|!code/key_switch_alpha_symbol", // todo (later): in numpad the code is key_alphaNumpad
params,
width,
Key.LABEL_FLAGS_PRESERVE_CASE or Key.LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR,
Key.BACKGROUND_TYPE_FUNCTIONAL,
null
)
KEY_NUMPAD -> KeyParams(
"!icon/numpad_key|!code/key_numpad",
params,
width,
0,
Key.BACKGROUND_TYPE_FUNCTIONAL,
null
)
KEY_EXCLAMATION -> KeyParams(
"!",
params,
width,
Key.LABEL_FLAGS_FONT_DEFAULT,
Key.BACKGROUND_TYPE_NORMAL,
arrayOf("¡") // todo (later) may depend on language
)
KEY_QUESTION -> KeyParams(
"\\?",
params,
width,
Key.LABEL_FLAGS_FONT_DEFAULT,
Key.BACKGROUND_TYPE_NORMAL,
arrayOf("¿") // todo (later) may depend on language
)
else -> throw IllegalArgumentException("unknown key definition \"$key\"")
}
}
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"
val iconName = when (params.mId.imeAction()) {
EditorInfo.IME_ACTION_GO -> KeyboardIconsSet.NAME_GO_KEY
EditorInfo.IME_ACTION_SEARCH -> KeyboardIconsSet.NAME_SEARCH_KEY
EditorInfo.IME_ACTION_SEND -> KeyboardIconsSet.NAME_SEND_KEY
EditorInfo.IME_ACTION_NEXT -> KeyboardIconsSet.NAME_NEXT_KEY
EditorInfo.IME_ACTION_DONE -> KeyboardIconsSet.NAME_DONE_KEY
EditorInfo.IME_ACTION_PREVIOUS -> KeyboardIconsSet.NAME_PREVIOUS_KEY
InputTypeUtils.IME_ACTION_CUSTOM_LABEL -> return params.mId.mCustomActionLabel
else -> return "!icon/enter_key"
}
val replacement = iconName.replaceIconWithLabelIfNoDrawable()
return if (iconName == replacement) // i.e. icon exists
"!icon/$iconName"
else
replacement
}
private fun getActionKeyCode() =
if (params.mId.isMultiLine && (params.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED || params.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED))
"!code/key_shift_enter"
else "!code/key_enter"
private fun getActionKeyMoreKeys(): Array<String>? {
val action = params.mId.imeAction()
val navigatePrev = params.mId.navigatePrevious()
val navigateNext = params.mId.navigateNext()
return when {
params.mId.passwordInput() -> when {
navigatePrev && action == EditorInfo.IME_ACTION_NEXT -> createMoreKeysArray(MORE_KEYS_NAVIGATE_PREVIOUS)
action == EditorInfo.IME_ACTION_NEXT -> null
navigateNext && action == EditorInfo.IME_ACTION_PREVIOUS -> createMoreKeysArray(MORE_KEYS_NAVIGATE_NEXT)
action == EditorInfo.IME_ACTION_PREVIOUS -> null
navigateNext && navigatePrev -> createMoreKeysArray(MORE_KEYS_NAVIGATE_PREVIOUS_NEXT)
navigateNext -> createMoreKeysArray(MORE_KEYS_NAVIGATE_NEXT)
navigatePrev -> createMoreKeysArray(MORE_KEYS_NAVIGATE_PREVIOUS)
else -> null
}
// could change definition of numbers to query a range, or have a pre-defined list, but not that crucial
params.mId.mMode in listOf(KeyboardId.MODE_URL, KeyboardId.MODE_EMAIL, KeyboardId.ELEMENT_PHONE, KeyboardId.ELEMENT_NUMBER, KeyboardId.MODE_DATE, KeyboardId.MODE_TIME, KeyboardId.MODE_DATETIME) -> when {
action == EditorInfo.IME_ACTION_NEXT && navigatePrev -> createMoreKeysArray(MORE_KEYS_NAVIGATE_PREVIOUS)
action == EditorInfo.IME_ACTION_NEXT -> null
action == EditorInfo.IME_ACTION_PREVIOUS && navigateNext -> createMoreKeysArray(MORE_KEYS_NAVIGATE_NEXT)
action == EditorInfo.IME_ACTION_PREVIOUS -> null
navigateNext && navigatePrev -> createMoreKeysArray(MORE_KEYS_NAVIGATE_PREVIOUS_NEXT)
navigateNext -> createMoreKeysArray(MORE_KEYS_NAVIGATE_NEXT)
navigatePrev -> createMoreKeysArray(MORE_KEYS_NAVIGATE_PREVIOUS)
else -> null
}
action == EditorInfo.IME_ACTION_NEXT && navigatePrev -> createMoreKeysArray(MORE_KEYS_NAVIGATE_EMOJI_PREVIOUS)
action == EditorInfo.IME_ACTION_NEXT -> createMoreKeysArray(MORE_KEYS_NAVIGATE_EMOJI)
action == EditorInfo.IME_ACTION_PREVIOUS && navigateNext -> createMoreKeysArray(MORE_KEYS_NAVIGATE_EMOJI_NEXT)
action == EditorInfo.IME_ACTION_PREVIOUS -> createMoreKeysArray(MORE_KEYS_NAVIGATE_EMOJI)
navigateNext && navigatePrev -> createMoreKeysArray(MORE_KEYS_NAVIGATE_EMOJI_PREVIOUS_NEXT)
navigateNext -> createMoreKeysArray(MORE_KEYS_NAVIGATE_EMOJI_NEXT)
navigatePrev -> createMoreKeysArray(MORE_KEYS_NAVIGATE_EMOJI_PREVIOUS)
else -> createMoreKeysArray(MORE_KEYS_NAVIGATE_EMOJI)
}
}
private fun createMoreKeysArray(moreKeysDef: String): Array<String> {
val moreKeys = mutableListOf<String>()
for (moreKey in moreKeysDef.split(",")) {
val iconPrefixRemoved = moreKey.substringAfter("!icon/")
if (iconPrefixRemoved == moreKey) { // i.e. there is no !icon/
moreKeys.add(moreKey)
continue
}
val iconName = iconPrefixRemoved.substringBefore("|")
val replacementText = iconName.replaceIconWithLabelIfNoDrawable()
if (replacementText == iconName) { // i.e. we have the drawable
moreKeys.add(moreKey)
} else {
moreKeys.add("!hasLabels!") // test what it actually does, but it's probably necessary
moreKeys.add(replacementText)
}
}
return moreKeys.toTypedArray()
}
private fun String.replaceIconWithLabelIfNoDrawable(): String {
if (params.mIconsSet.getIconDrawable(KeyboardIconsSet.getIconId(this)) != null) return this
val id = context.resources.getIdentifier("label_$this", "string", context.packageName)
return context.getString(id)
}
// todo: may depend on language
private fun getAlphabetLabel(): String {
return "ABC"
}
// todo: may depend on language
private fun getSymbolLabel(): String {
return "\\?123"
}
private fun getShiftLabel(): String {
val elementId = params.mId.mElementId
if (elementId == KeyboardId.ELEMENT_SYMBOLS_SHIFTED)
return "=\\<" // todo: may depend on language
if (elementId == KeyboardId.ELEMENT_SYMBOLS)
return getSymbolLabel()
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 getDefaultCommaLabel(): String {
if (params.mId.mMode == KeyboardId.MODE_URL)
return "/"
if (params.mId.mMode == KeyboardId.MODE_EMAIL)
return "\\@"
return ","
}
private fun getCommaMoreKeys(): Array<String> {
val keys = mutableListOf("!icon/clipboard_normal_key|!code/key_clipboard")
if (!params.mId.mEmojiKeyEnabled)
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")
keys.add("!icon/settings_key|!code/key_settings")
return keys.toTypedArray()
}
private fun getPeriodMoreKeys(): Array<String> {
if (params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS || params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS_SHIFTED)
return arrayOf("")
// todo: language-dependent, also influences the number after autoColumnOrder
// there is a weird messup with morekeys_punctuation and morekeys_period
// by default, morekeys_period is taken from morekeys_punctuation, but some languages override this
// morekeys_period is also changed by some languages
// period key always uses morekeys_period, except for dvorak layout which is the only user of morekeys_punctuation
// -> clean it up when implementing the language-dependent moreKeys
return arrayOf("!autoColumnOrder!8", "\\,", "?", "!", "#", ")", "(", "/", ";", "'", "@", ":", "-", "\"", "+", "\\%", "&")
}
}
// class for holding a parsed key of the simple layout
private class BaseKey(
val label: String,
val moreKeys: Array<String>? = null,
)
// todo (later): may depend on language for non-latin layouts... or should the number row always be latin?
private val numbers = (1..9).map { it.toString() } + "0"
// moreKeys for numbers, order is 1-9 and then 0
// todo (later): like numbers, for non-latin layouts this depends on language and therefore should not be in the parser
private val numbersMoreKeys = arrayOf(
arrayOf("¹", "½", "","¼", ""),
arrayOf("²", ""),
arrayOf("³", "¾", ""),
arrayOf(""),
arrayOf(""),
null,
arrayOf(""),
null,
null,
arrayOf("", ""),
)
// could use 1 string per key, and make arrays right away
private const val MORE_KEYS_NAVIGATE_PREVIOUS = "!icon/previous_key|!code/key_action_previous,!icon/clipboard_action_key|!code/key_clipboard"
private const val MORE_KEYS_NAVIGATE_NEXT = "!icon/clipboard_action_key|!code/key_clipboard,!icon/next_key|!code/key_action_next"
private const val MORE_KEYS_NAVIGATE_PREVIOUS_NEXT = "!fixedColumnOrder!3,!needsDividers!,!icon/previous_key|!code/key_action_previous,!icon/clipboard_action_key|!code/key_clipboard,!icon/next_key|!code/key_action_next"
private const val MORE_KEYS_NAVIGATE_EMOJI_PREVIOUS = "!fixedColumnOrder!3,!needsDividers!,!icon/previous_key|!code/key_action_previous,!icon/clipboard_action_key|!code/key_clipboard,!icon/emoji_action_key|!code/key_emoji"
private const val MORE_KEYS_NAVIGATE_EMOJI = "!icon/clipboard_action_key|!code/key_clipboard,!icon/emoji_action_key|!code/key_emoji"
private const val MORE_KEYS_NAVIGATE_EMOJI_NEXT = "!fixedColumnOrder!3,!needsDividers!,!icon/clipboard_action_key|!code/key_clipboard,!icon/emoji_action_key|!code/key_emoji,!icon/next_key|!code/key_action_next"
private const val MORE_KEYS_NAVIGATE_EMOJI_PREVIOUS_NEXT = "!fixedColumnOrder!4,!needsDividers!,!icon/previous_key|!code/key_action_previous,!icon/clipboard_action_key|!code/key_clipboard,!icon/emoji_action_key|!code/key_emoji,!icon/next_key|!code/key_action_next"
private const val KEY_EMOJI = "emoji"
private const val KEY_LANGUAGE_SWITCH = "language"
private const val KEY_COM = "com"
private const val KEY_EMOJI_COM = "emoji_com"
private const val KEY_DELETE = "delete"
private const val KEY_ACTION = "action"
private const val KEY_PERIOD = "period"
private const val KEY_COMMA = "comma"
private const val KEY_SPACE = "space"
private const val KEY_SHIFT = "shift"
private const val KEY_NUMPAD = "numpad"
private const val KEY_SYMBOL = "symbol"
private const val KEY_ALPHA = "alphabet"
private const val KEY_QUESTION = "question"
private const val KEY_EXCLAMATION = "exclamation"

View file

@ -0,0 +1,12 @@
package org.dslul.openboard.inputmethod.latin.utils
// generic extension functions
// adapted from Kotlin source: https://github.com/JetBrains/kotlin/blob/7a7d392b3470b38d42f80c896b7270678d0f95c3/libraries/stdlib/common/src/generated/_Collections.kt#L3004
inline fun <T> Iterable<T>.sumOf(selector: (T) -> Float): Float {
var sum = 0f
for (element in this) {
sum += selector(element)
}
return sum
}