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

@ -0,0 +1,28 @@
q %
w \
e |
r =
t [
y ]
u <
i >
o {
p }
a @
s #
d $ €
f _
g &
h -
j +
k (
l )
z *
x "
c '
v :
b ;
n !
m ?

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
}

View file

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

View file

@ -0,0 +1,16 @@
<?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
-->
<string name="key_def_functional" translatable="false">"shift 15%; delete 15%"</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 15%, comma, space, period, action 15%"</string>
</resources>