From 69b4fa494fb13ff45f4de36b22027df3aa94db2d Mon Sep 17 00:00:00 2001 From: Helium314 Date: Wed, 25 Oct 2023 12:21:38 +0200 Subject: [PATCH] split keyboard builder and (xml) keyboard parser another step towards #216 --- .../openboard/inputmethod/keyboard/Key.java | 6 +- .../keyboard/KeyboardLayoutSet.java | 2 +- .../keyboard/MoreKeysKeyboard.java | 2 +- .../keyboard/internal/KeyStylesSet.java | 7 +- .../keyboard/internal/KeyboardBuilder.java | 843 +---------------- .../keyboard_parser/XmlKeyboardParser.java | 854 ++++++++++++++++++ .../XmlKeyboardRow.java} | 9 +- .../latin/suggestions/MoreSuggestions.java | 2 +- app/src/main/res/values/attrs.xml | 2 +- 9 files changed, 882 insertions(+), 845 deletions(-) create mode 100644 app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/XmlKeyboardParser.java rename app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/{KeyboardRow.java => keyboard_parser/XmlKeyboardRow.java} (95%) diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/Key.java b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/Key.java index 811b728d6..e3e63f94d 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/Key.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/Key.java @@ -18,7 +18,7 @@ import org.dslul.openboard.inputmethod.keyboard.internal.KeyStyle; import org.dslul.openboard.inputmethod.keyboard.internal.KeyVisualAttributes; import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardIconsSet; import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardParams; -import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardRow; +import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.XmlKeyboardRow; import org.dslul.openboard.inputmethod.keyboard.internal.MoreKeySpec; import org.dslul.openboard.inputmethod.latin.R; import org.dslul.openboard.inputmethod.latin.common.Constants; @@ -973,7 +973,7 @@ public class Key implements Comparable { int yPos; public static KeyParams newSpacer(final TypedArray keyAttr, final KeyStyle keyStyle, - final KeyboardParams params, final KeyboardRow row) { + final KeyboardParams params, final XmlKeyboardRow row) { final KeyParams keyParams = new KeyParams(null, keyAttr, keyStyle, params, row); keyParams.isSpacer = true; return keyParams; @@ -1017,7 +1017,7 @@ public class Key implements Comparable { */ public KeyParams(@Nullable final String keySpec, @NonNull final TypedArray keyAttr, @NonNull final KeyStyle style, @NonNull final KeyboardParams params, - @NonNull final KeyboardRow row) { + @NonNull final XmlKeyboardRow row) { mHorizontalGap = params.mHorizontalGap; mVerticalGap = params.mVerticalGap; diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/KeyboardLayoutSet.java b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/KeyboardLayoutSet.java index 3f002a1a9..b857468ac 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/KeyboardLayoutSet.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/KeyboardLayoutSet.java @@ -218,7 +218,7 @@ public final class KeyboardLayoutSet { sUniqueKeysCache.setEnabled(id.isAlphabetKeyboard()); builder.setAllowRedundantMoreKeys(elementParams.mAllowRedundantMoreKeys); final int keyboardXmlId = elementParams.mKeyboardXmlId; - builder.load(keyboardXmlId, id); + builder.loadFromXml(keyboardXmlId, id); if (mParams.mDisableTouchPositionCorrectionDataForTest) { builder.disableTouchPositionCorrectionDataForTest(); } diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/MoreKeysKeyboard.java b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/MoreKeysKeyboard.java index 13ee86bd0..a6b9ecb41 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/MoreKeysKeyboard.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/MoreKeysKeyboard.java @@ -266,7 +266,7 @@ public final class MoreKeysKeyboard extends Keyboard { final boolean isSingleMoreKeyWithPreview, final int keyPreviewVisibleWidth, final int keyPreviewVisibleHeight, final Paint paintToMeasure) { super(context, new MoreKeysKeyboardParams()); - load(keyboard.mMoreKeysTemplate, keyboard.mId); + loadFromXml(keyboard.mMoreKeysTemplate, keyboard.mId); // TODO: More keys keyboard's vertical gap is currently calculated heuristically. // Should revise the algorithm. diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/KeyStylesSet.java b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/KeyStylesSet.java index 3b715e77c..5d5eb08fe 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/KeyStylesSet.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/KeyStylesSet.java @@ -13,6 +13,7 @@ import android.util.SparseArray; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.XmlKeyboardParser; import org.dslul.openboard.inputmethod.latin.R; import org.dslul.openboard.inputmethod.latin.utils.XmlParseUtils; import org.xmlpull.v1.XmlPullParser; @@ -179,13 +180,13 @@ public final class KeyStylesSet { final String styleName = keyStyleAttr.getString(R.styleable.Keyboard_KeyStyle_styleName); if (styleName == null) { throw new XmlParseUtils.ParseException( - KeyboardBuilder.TAG_KEY_STYLE + " has no styleName attribute", parser); + XmlKeyboardParser.TAG_KEY_STYLE + " has no styleName attribute", parser); } if (DEBUG) { Log.d(TAG, String.format("<%s styleName=%s />", - KeyboardBuilder.TAG_KEY_STYLE, styleName)); + XmlKeyboardParser.TAG_KEY_STYLE, styleName)); if (mStyles.containsKey(styleName)) { - Log.d(TAG, KeyboardBuilder.TAG_KEY_STYLE + " " + styleName + " is overridden at " + Log.d(TAG, XmlKeyboardParser.TAG_KEY_STYLE + " " + styleName + " is overridden at " + parser.getPositionDescription()); } } diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/KeyboardBuilder.java b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/KeyboardBuilder.java index c66ef1167..2b04e58c1 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/KeyboardBuilder.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/KeyboardBuilder.java @@ -8,14 +8,7 @@ package org.dslul.openboard.inputmethod.keyboard.internal; import android.content.Context; import android.content.res.Resources; -import android.content.res.TypedArray; -import android.content.res.XmlResourceParser; -import android.os.Build; -import android.text.TextUtils; -import android.util.AttributeSet; import android.util.Log; -import android.util.TypedValue; -import android.util.Xml; import androidx.annotation.NonNull; @@ -23,112 +16,16 @@ import org.dslul.openboard.inputmethod.annotations.UsedForTesting; import org.dslul.openboard.inputmethod.keyboard.Key; import org.dslul.openboard.inputmethod.keyboard.Keyboard; import org.dslul.openboard.inputmethod.keyboard.KeyboardId; -import org.dslul.openboard.inputmethod.keyboard.KeyboardTheme; +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.common.StringUtils; -import org.dslul.openboard.inputmethod.latin.settings.Settings; -import org.dslul.openboard.inputmethod.latin.utils.ResourceUtils; -import org.dslul.openboard.inputmethod.latin.utils.XmlParseUtils; -import org.dslul.openboard.inputmethod.latin.utils.XmlParseUtils.ParseException; -import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Locale; - -/** - * Keyboard Building helper. - * - * This class parses Keyboard XML file and eventually build a Keyboard. - * The Keyboard XML file looks like: - *
- *   <!-- xml/keyboard.xml -->
- *   <Keyboard keyboard_attributes*>
- *     <!-- Keyboard Content -->
- *     <Row row_attributes*>
- *       <!-- Row Content -->
- *       <Key key_attributes* />
- *       <Spacer horizontalGap="32.0dp" />
- *       <include keyboardLayout="@xml/other_keys">
- *       ...
- *     </Row>
- *     <include keyboardLayout="@xml/other_rows">
- *     ...
- *   </Keyboard>
- * 
- * The XML file which is included in other file must have <merge> as root element, - * such as: - *
- *   <!-- xml/other_keys.xml -->
- *   <merge>
- *     <Key key_attributes* />
- *     ...
- *   </merge>
- * 
- * and - *
- *   <!-- xml/other_rows.xml -->
- *   <merge>
- *     <Row row_attributes*>
- *       <Key key_attributes* />
- *     </Row>
- *     ...
- *   </merge>
- * 
- * You can also use switch-case-default tags to select Rows and Keys. - *
- *   <switch>
- *     <case case_attribute*>
- *       <!-- Any valid tags at switch position -->
- *     </case>
- *     ...
- *     <default>
- *       <!-- Any valid tags at switch position -->
- *     </default>
- *   </switch>
- * 
- * You can declare Key style and specify styles within Key tags. - *
- *     <switch>
- *       <case mode="email">
- *         <key-style styleName="f1-key" parentStyle="modifier-key"
- *           keyLabel=".com"
- *         />
- *       </case>
- *       <case mode="url">
- *         <key-style styleName="f1-key" parentStyle="modifier-key"
- *           keyLabel="http://"
- *         />
- *       </case>
- *     </switch>
- *     ...
- *     <Key keyStyle="shift-key" ... />
- * 
- */ // TODO: Write unit tests for this class. public class KeyboardBuilder { private static final String BUILDER_TAG = "Keyboard.Builder"; - private static final boolean DEBUG = false; - - // Keyboard XML Tags - private static final String TAG_KEYBOARD = "Keyboard"; - private static final String TAG_ROW = "Row"; - private static final String TAG_GRID_ROWS = "GridRows"; - private static final String TAG_KEY = "Key"; - private static final String TAG_SPACER = "Spacer"; - private static final String TAG_INCLUDE = "include"; - private static final String TAG_MERGE = "merge"; - private static final String TAG_SWITCH = "switch"; - private static final String TAG_CASE = "case"; - private static final String TAG_DEFAULT = "default"; - public static final String TAG_KEY_STYLE = "key-style"; - - private static final int DEFAULT_KEYBOARD_COLUMNS = 10; - private static final int DEFAULT_KEYBOARD_ROWS = 4; @NonNull protected final KP mParams; @@ -138,11 +35,10 @@ public class KeyboardBuilder { private int mCurrentY = 0; // currently not used, but will be relevant when resizing a row or inserting a new key private float mCurrentX = 0f; - private KeyboardRow mCurrentRow = null; private boolean mLeftEdge; private boolean mTopEdge; private Key mRightEdgeKey = null; - private final ArrayList> keysInRows = new ArrayList<>(); + private ArrayList> keysInRows; public KeyboardBuilder(final Context context, @NonNull final KP params) { mContext = context; @@ -159,15 +55,10 @@ public class KeyboardBuilder { mParams.mAllowRedundantMoreKeys = enabled; } - // todo: - // split the parser from the builder - // parser should just setup the params (parseKeyboardAttributes) - // and return something like keysInRows - // then builder can nicely adjust the keyboard, maybe dimensions, maybe insert keys/spacers - public KeyboardBuilder load(final int xmlId, final KeyboardId id) { + public KeyboardBuilder loadFromXml(final int xmlId, final KeyboardId id) { mParams.mId = id; - try (XmlResourceParser parser = mResources.getXml(xmlId)) { - parseKeyboard(parser); + try (XmlKeyboardParser keyboardParser = new XmlKeyboardParser(xmlId, mParams, mContext)) { + keysInRows = keyboardParser.parseKeyboard(); } catch (XmlPullParserException e) { Log.w(BUILDER_TAG, "keyboard XML parse error", e); throw new IllegalArgumentException(e.getMessage(), e); @@ -193,720 +84,25 @@ public class KeyboardBuilder { return new Keyboard(mParams); } - private int mIndent; - private static final String SPACES = " "; - - private static String spaces(final int count) { - return (count < SPACES.length()) ? SPACES.substring(0, count) : SPACES; - } - - private void startTag(final String format, final Object ... args) { - Log.d(BUILDER_TAG, String.format(spaces(++mIndent * 2) + format, args)); - } - - private void endTag(final String format, final Object ... args) { - Log.d(BUILDER_TAG, String.format(spaces(mIndent-- * 2) + format, args)); - } - - private void startEndTag(final String format, final Object ... args) { - Log.d(BUILDER_TAG, String.format(spaces(++mIndent * 2) + format, args)); - mIndent--; - } - - private void parseKeyboard(final XmlPullParser parser) - throws XmlPullParserException, IOException { - if (DEBUG) startTag("<%s> %s", TAG_KEYBOARD, mParams.mId); - while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { - final int event = parser.next(); - if (event == XmlPullParser.START_TAG) { - final String tag = parser.getName(); - if (TAG_KEYBOARD.equals(tag)) { - parseKeyboardAttributes(parser); - startKeyboard(); - parseKeyboardContent(parser, false); - return; - } - throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD); - } - } - } - - private void parseKeyboardAttributes(final XmlPullParser parser) { - final AttributeSet attr = Xml.asAttributeSet(parser); - final TypedArray keyboardAttr = mContext.obtainStyledAttributes( - attr, R.styleable.Keyboard, R.attr.keyboardStyle, R.style.Keyboard); - final TypedArray keyAttr = mResources.obtainAttributes(attr, R.styleable.Keyboard_Key); - try { - final KeyboardParams params = mParams; - final int height = params.mId.mHeight; - final int width = params.mId.mWidth; - params.mOccupiedHeight = height; - params.mOccupiedWidth = width; - params.mTopPadding = (int)keyboardAttr.getFraction( - R.styleable.Keyboard_keyboardTopPadding, height, height, 0); - params.mBottomPadding = (int)keyboardAttr.getFraction( - R.styleable.Keyboard_keyboardBottomPadding, height, height, 0); - params.mLeftPadding = (int)keyboardAttr.getFraction( - R.styleable.Keyboard_keyboardLeftPadding, width, width, 0); - params.mRightPadding = (int)keyboardAttr.getFraction( - R.styleable.Keyboard_keyboardRightPadding, width, width, 0); - - final int baseWidth = - params.mOccupiedWidth - params.mLeftPadding - params.mRightPadding; - params.mBaseWidth = baseWidth; - params.mDefaultKeyWidth = (int)keyAttr.getFraction(R.styleable.Keyboard_Key_keyWidth, - baseWidth, baseWidth, baseWidth / DEFAULT_KEYBOARD_COLUMNS); - - if (Settings.getInstance().getCurrent().mNarrowKeyGaps) { - params.mHorizontalGap = (int) keyboardAttr.getFraction( - R.styleable.Keyboard_horizontalGapNarrow, baseWidth, baseWidth, 0); - params.mVerticalGap = (int) keyboardAttr.getFraction( - R.styleable.Keyboard_verticalGapNarrow, height, height, 0); - } else { - params.mHorizontalGap = (int) keyboardAttr.getFraction( - R.styleable.Keyboard_horizontalGap, baseWidth, baseWidth, 0); - // TODO: Fix keyboard geometry calculation clearer. Historically vertical gap between - // rows are determined based on the entire keyboard height including top and bottom - // paddings. - params.mVerticalGap = (int) keyboardAttr.getFraction( - R.styleable.Keyboard_verticalGap, height, height, 0); - } - - final int baseHeight = params.mOccupiedHeight - params.mTopPadding - - params.mBottomPadding + params.mVerticalGap; - params.mBaseHeight = baseHeight; - params.mDefaultRowHeight = (int)ResourceUtils.getDimensionOrFraction(keyboardAttr, - R.styleable.Keyboard_rowHeight, baseHeight, baseHeight / DEFAULT_KEYBOARD_ROWS); - - params.mKeyVisualAttributes = KeyVisualAttributes.newInstance(keyAttr); - - params.mMoreKeysTemplate = keyboardAttr.getResourceId( - R.styleable.Keyboard_moreKeysTemplate, 0); - params.mMaxMoreKeysKeyboardColumn = keyAttr.getInt( - R.styleable.Keyboard_Key_maxMoreKeysColumn, 5); - - params.mThemeId = keyboardAttr.getInt(R.styleable.Keyboard_themeId, 0); - params.mIconsSet.loadIcons(keyboardAttr); - params.mTextsSet.setLocale(params.mId.getLocale(), mContext); - - final int resourceId = keyboardAttr.getResourceId( - R.styleable.Keyboard_touchPositionCorrectionData, 0); - if (resourceId != 0) { - final String[] data = mResources.getStringArray(resourceId); - params.mTouchPositionCorrection.load(data); - } - } finally { - keyAttr.recycle(); - keyboardAttr.recycle(); - } - } - - private void parseKeyboardContent(final XmlPullParser parser, final boolean skip) - throws XmlPullParserException, IOException { - while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { - final int event = parser.next(); - if (event == XmlPullParser.START_TAG) { - final String tag = parser.getName(); - if (TAG_ROW.equals(tag)) { - final KeyboardRow row = parseRowAttributes(parser); - if (DEBUG) startTag("<%s>%s", TAG_ROW, skip ? " skipped" : ""); - if (!skip) { - startRow(row); - } - parseRowContent(parser, row, skip); - } else if (TAG_GRID_ROWS.equals(tag)) { - if (DEBUG) startTag("<%s>%s", TAG_GRID_ROWS, skip ? " skipped" : ""); - parseGridRows(parser, skip); - } else if (TAG_INCLUDE.equals(tag)) { - parseIncludeKeyboardContent(parser, skip); - } else if (TAG_SWITCH.equals(tag)) { - parseSwitchKeyboardContent(parser, skip); - } else if (TAG_KEY_STYLE.equals(tag)) { - parseKeyStyle(parser, skip); - } else { - throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_ROW); - } - } else if (event == XmlPullParser.END_TAG) { - final String tag = parser.getName(); - if (DEBUG) endTag("", tag); - if (TAG_KEYBOARD.equals(tag)) { - endKeyboard(); - return; - } - if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag) || TAG_MERGE.equals(tag)) { - return; - } - throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_ROW); - } - } - } - - private KeyboardRow parseRowAttributes(final XmlPullParser parser) - throws XmlPullParserException { - final AttributeSet attr = Xml.asAttributeSet(parser); - final TypedArray keyboardAttr = mResources.obtainAttributes(attr, R.styleable.Keyboard); - try { - if (keyboardAttr.hasValue(R.styleable.Keyboard_horizontalGap)) { - throw new XmlParseUtils.IllegalAttribute(parser, TAG_ROW, "horizontalGap"); - } - if (keyboardAttr.hasValue(R.styleable.Keyboard_verticalGap)) { - throw new XmlParseUtils.IllegalAttribute(parser, TAG_ROW, "verticalGap"); - } - return new KeyboardRow(mResources, mParams, parser, mCurrentY); - } finally { - keyboardAttr.recycle(); - } - } - - private void parseRowContent(final XmlPullParser parser, final KeyboardRow row, - final boolean skip) throws XmlPullParserException, IOException { - while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { - final int event = parser.next(); - if (event == XmlPullParser.START_TAG) { - final String tag = parser.getName(); - if (TAG_KEY.equals(tag)) { - parseKey(parser, row, skip); - } else if (TAG_SPACER.equals(tag)) { - parseSpacer(parser, row, skip); - } else if (TAG_INCLUDE.equals(tag)) { - parseIncludeRowContent(parser, row, skip); - } else if (TAG_SWITCH.equals(tag)) { - parseSwitchRowContent(parser, row, skip); - } else if (TAG_KEY_STYLE.equals(tag)) { - parseKeyStyle(parser, skip); - } else { - throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_ROW); - } - } else if (event == XmlPullParser.END_TAG) { - final String tag = parser.getName(); - if (DEBUG) endTag("", tag); - if (TAG_ROW.equals(tag)) { - if (!skip) { - endRow(row); - } - return; - } - if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag) || TAG_MERGE.equals(tag)) { - return; - } - throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_ROW); - } - } - } - - private void parseGridRows(final XmlPullParser parser, final boolean skip) - throws XmlPullParserException, IOException { - if (skip) { - XmlParseUtils.checkEndTag(TAG_GRID_ROWS, parser); - if (DEBUG) { - startEndTag("<%s /> skipped", TAG_GRID_ROWS); - } - return; - } - final KeyboardRow gridRows = new KeyboardRow(mResources, mParams, parser, mCurrentY); - final TypedArray gridRowAttr = mResources.obtainAttributes( - Xml.asAttributeSet(parser), R.styleable.Keyboard_GridRows); - final int codesArrayId = gridRowAttr.getResourceId( - R.styleable.Keyboard_GridRows_codesArray, 0); - final int textsArrayId = gridRowAttr.getResourceId( - R.styleable.Keyboard_GridRows_textsArray, 0); - final int moreCodesArrayId = gridRowAttr.getResourceId( - R.styleable.Keyboard_GridRows_moreCodesArray, 0); - // todo: read relative key width, key / row height and gaps (but they might also be absolute, see getDimensionOrFraction) - gridRowAttr.recycle(); - if (codesArrayId == 0 && textsArrayId == 0) { - throw new XmlParseUtils.ParseException( - "Missing codesArray or textsArray attributes", parser); - } - if (codesArrayId != 0 && textsArrayId != 0) { - throw new XmlParseUtils.ParseException( - "Both codesArray and textsArray attributes specifed", parser); - } - if (textsArrayId != 0 && moreCodesArrayId != 0) { - throw new XmlParseUtils.ParseException( - "moreCodesArray is not compatible with textsArray", parser); - } - final String[] array = mResources.getStringArray( - codesArrayId != 0 ? codesArrayId : textsArrayId); - final String[] arrayMore = moreCodesArrayId != 0 ? - mResources.getStringArray(moreCodesArrayId) : null; - final int counts = array.length; - if (arrayMore != null && counts != arrayMore.length) { - throw new XmlParseUtils.ParseException( - "Inconsistent array size between codesArray and moreKeysArray", parser); - } - final float keyWidth = gridRows.getKeyWidth(null, 0.0f); - final int numColumns = (int)(mParams.mOccupiedWidth / keyWidth); - for (int index = 0; index < counts; index += numColumns) { - final KeyboardRow row = new KeyboardRow(mResources, mParams, parser, mCurrentY); - startRow(row); - final ArrayList keyParamsRow = keysInRows.get(keysInRows.size() - 1); - for (int c = 0; c < numColumns; c++) { - final int i = index + c; - if (i >= counts) { - break; - } - final String label; - final int code; - final String outputText; - final int supportedMinSdkVersion; - final String moreKeySpecs; - if (codesArrayId != 0) { - final String codeArraySpec = array[i]; - label = CodesArrayParser.parseLabel(codeArraySpec); - code = CodesArrayParser.parseCode(codeArraySpec); - outputText = CodesArrayParser.parseOutputText(codeArraySpec); - supportedMinSdkVersion = - CodesArrayParser.getMinSupportSdkVersion(codeArraySpec); - moreKeySpecs = MoreCodesArrayParser.parseKeySpecs( - arrayMore != null ? arrayMore[i] : null); - } else { - final String textArraySpec = array[i]; - // TODO: Utilize KeySpecParser or write more generic TextsArrayParser. - label = textArraySpec; - code = Constants.CODE_OUTPUT_TEXT; - outputText = textArraySpec + (char)Constants.CODE_SPACE; - supportedMinSdkVersion = 0; - moreKeySpecs = null; - } - if (Build.VERSION.SDK_INT < supportedMinSdkVersion) { - continue; - } - final int labelFlags = row.getDefaultKeyLabelFlags(); - // TODO: Should be able to assign default keyActionFlags as well. - final int backgroundType = row.getDefaultBackgroundType(); - final int x = (int)row.getKeyX(null); - final int y = row.getKeyY(); - final int width = (int)keyWidth; - final int height = row.getRowHeight(); - final String hintLabel = moreKeySpecs != null ? "\u25E5" : null; - final Key.KeyParams key = new Key.KeyParams(label, code, outputText, hintLabel, moreKeySpecs, - labelFlags, backgroundType, x, y, width, height, mParams); - // todo: add relative width and others - keyParamsRow.add(key); - row.advanceXPos(keyWidth); - } - endRow(row); - } - - XmlParseUtils.checkEndTag(TAG_GRID_ROWS, parser); - } - - private void parseKey(final XmlPullParser parser, final KeyboardRow row, final boolean skip) - throws XmlPullParserException, IOException { - if (skip) { - XmlParseUtils.checkEndTag(TAG_KEY, parser); - if (DEBUG) startEndTag("<%s /> skipped", TAG_KEY); - return; - } - final TypedArray keyAttr = mResources.obtainAttributes( - Xml.asAttributeSet(parser), R.styleable.Keyboard_Key); - final KeyStyle keyStyle = mParams.mKeyStyles.getKeyStyle(keyAttr, parser); - final String keySpec = keyStyle.getString(keyAttr, R.styleable.Keyboard_Key_keySpec); - if (TextUtils.isEmpty(keySpec)) { - throw new ParseException("Empty keySpec", parser); - } - final Key.KeyParams key = new Key.KeyParams(keySpec, keyAttr, keyStyle, mParams, row); - keyAttr.recycle(); - // todo: add relative width and others - if (DEBUG) { - startEndTag("<%s%s %s moreKeys=%s />", TAG_KEY, (key.mEnabled ? "" : " disabled"), - key, Arrays.toString(key.mMoreKeys)); - } - XmlParseUtils.checkEndTag(TAG_KEY, parser); - keysInRows.get(keysInRows.size() - 1).add(key); - } - - private void parseSpacer(final XmlPullParser parser, final KeyboardRow row, final boolean skip) - throws XmlPullParserException, IOException { - if (skip) { - XmlParseUtils.checkEndTag(TAG_SPACER, parser); - if (DEBUG) startEndTag("<%s /> skipped", TAG_SPACER); - return; - } - final TypedArray keyAttr = mResources.obtainAttributes( - Xml.asAttributeSet(parser), R.styleable.Keyboard_Key); - final KeyStyle keyStyle = mParams.mKeyStyles.getKeyStyle(keyAttr, parser); - final Key.KeyParams spacer = Key.KeyParams.newSpacer(keyAttr, keyStyle, mParams, row); - keyAttr.recycle(); - // todo: add relative width and others - keysInRows.get(keysInRows.size() - 1).add(spacer); - if (DEBUG) startEndTag("<%s />", TAG_SPACER); - XmlParseUtils.checkEndTag(TAG_SPACER, parser); - } - - private void parseIncludeKeyboardContent(final XmlPullParser parser, final boolean skip) - throws XmlPullParserException, IOException { - parseIncludeInternal(parser, null, skip); - } - - private void parseIncludeRowContent(final XmlPullParser parser, final KeyboardRow row, - final boolean skip) throws XmlPullParserException, IOException { - parseIncludeInternal(parser, row, skip); - } - - private void parseIncludeInternal(final XmlPullParser parser, final KeyboardRow row, - final boolean skip) throws XmlPullParserException, IOException { - if (skip) { - XmlParseUtils.checkEndTag(TAG_INCLUDE, parser); - if (DEBUG) startEndTag(" skipped", TAG_INCLUDE); - return; - } - final AttributeSet attr = Xml.asAttributeSet(parser); - final TypedArray keyboardAttr = mResources.obtainAttributes( - attr, R.styleable.Keyboard_Include); - final TypedArray keyAttr = mResources.obtainAttributes(attr, R.styleable.Keyboard_Key); - final int keyboardLayout; - try { - XmlParseUtils.checkAttributeExists( - keyboardAttr, R.styleable.Keyboard_Include_keyboardLayout, "keyboardLayout", - TAG_INCLUDE, parser); - keyboardLayout = keyboardAttr.getResourceId( - R.styleable.Keyboard_Include_keyboardLayout, 0); - if (row != null) { - // Override current x coordinate. - row.setXPos(row.getKeyX(keyAttr)); - // Push current Row attributes and update with new attributes. - row.pushRowAttributes(keyAttr); - } - } finally { - keyboardAttr.recycle(); - keyAttr.recycle(); - } - - XmlParseUtils.checkEndTag(TAG_INCLUDE, parser); - if (DEBUG) { - startEndTag("<%s keyboardLayout=%s />",TAG_INCLUDE, - mResources.getResourceEntryName(keyboardLayout)); - } - try (XmlResourceParser parserForInclude = mResources.getXml(keyboardLayout)) { - parseMerge(parserForInclude, row, skip); - } finally { - if (row != null) { - // Restore Row attributes. - row.popRowAttributes(); - } - } - } - - private void parseMerge(final XmlPullParser parser, final KeyboardRow row, final boolean skip) - throws XmlPullParserException, IOException { - if (DEBUG) startTag("<%s>", TAG_MERGE); - while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { - final int event = parser.next(); - if (event == XmlPullParser.START_TAG) { - final String tag = parser.getName(); - if (TAG_MERGE.equals(tag)) { - if (row == null) { - parseKeyboardContent(parser, skip); - } else { - parseRowContent(parser, row, skip); - } - return; - } - throw new XmlParseUtils.ParseException( - "Included keyboard layout must have root element", parser); - } - } - } - - private void parseSwitchKeyboardContent(final XmlPullParser parser, final boolean skip) - throws XmlPullParserException, IOException { - parseSwitchInternal(parser, null, skip); - } - - private void parseSwitchRowContent(final XmlPullParser parser, final KeyboardRow row, - final boolean skip) throws XmlPullParserException, IOException { - parseSwitchInternal(parser, row, skip); - } - - private void parseSwitchInternal(final XmlPullParser parser, final KeyboardRow row, - final boolean skip) throws XmlPullParserException, IOException { - if (DEBUG) startTag("<%s> %s", TAG_SWITCH, mParams.mId); - boolean selected = false; - while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { - final int event = parser.next(); - if (event == XmlPullParser.START_TAG) { - final String tag = parser.getName(); - if (TAG_CASE.equals(tag)) { - selected |= parseCase(parser, row, selected || skip); - } else if (TAG_DEFAULT.equals(tag)) { - selected |= parseDefault(parser, row, selected || skip); - } else { - throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_SWITCH); - } - } else if (event == XmlPullParser.END_TAG) { - final String tag = parser.getName(); - if (TAG_SWITCH.equals(tag)) { - if (DEBUG) endTag("", TAG_SWITCH); - return; - } - throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_SWITCH); - } - } - } - - private boolean parseCase(final XmlPullParser parser, final KeyboardRow row, final boolean skip) - throws XmlPullParserException, IOException { - final boolean selected = parseCaseCondition(parser); - if (row == null) { - // Processing Rows. - parseKeyboardContent(parser, !selected || skip); - } else { - // Processing Keys. - parseRowContent(parser, row, !selected || skip); - } - return selected; - } - - private boolean parseCaseCondition(final XmlPullParser parser) { - final KeyboardId id = mParams.mId; - if (id == null) { - return true; - } - final AttributeSet attr = Xml.asAttributeSet(parser); - final TypedArray caseAttr = mResources.obtainAttributes(attr, R.styleable.Keyboard_Case); - try { - final boolean keyboardLayoutSetMatched = matchString(caseAttr, - R.styleable.Keyboard_Case_keyboardLayoutSet, - id.mSubtype.getKeyboardLayoutSetName()); - final boolean keyboardLayoutSetElementMatched = matchTypedValue(caseAttr, - R.styleable.Keyboard_Case_keyboardLayoutSetElement, id.mElementId, - KeyboardId.elementIdToName(id.mElementId)); - final boolean keyboardThemeMacthed = matchTypedValue(caseAttr, - R.styleable.Keyboard_Case_keyboardTheme, mParams.mThemeId, - KeyboardTheme.getKeyboardThemeName(mParams.mThemeId)); - final boolean modeMatched = matchTypedValue(caseAttr, - R.styleable.Keyboard_Case_mode, id.mMode, KeyboardId.modeName(id.mMode)); - final boolean navigateNextMatched = matchBoolean(caseAttr, - R.styleable.Keyboard_Case_navigateNext, id.navigateNext()); - final boolean navigatePreviousMatched = matchBoolean(caseAttr, - R.styleable.Keyboard_Case_navigatePrevious, id.navigatePrevious()); - final boolean passwordInputMatched = matchBoolean(caseAttr, - R.styleable.Keyboard_Case_passwordInput, id.passwordInput()); - final boolean clobberSettingsKeyMatched = matchBoolean(caseAttr, - R.styleable.Keyboard_Case_clobberSettingsKey, id.mClobberSettingsKey); - final boolean hasShortcutKeyMatched = matchBoolean(caseAttr, - R.styleable.Keyboard_Case_hasShortcutKey, id.mHasShortcutKey); - final boolean numberRowEnabledMatched = matchBoolean(caseAttr, - R.styleable.Keyboard_Case_numberRowEnabled, - id.mNumberRowEnabled); - final boolean languageSwitchKeyEnabledMatched = matchBoolean(caseAttr, - R.styleable.Keyboard_Case_languageSwitchKeyEnabled, - id.mLanguageSwitchKeyEnabled); - final boolean emojiKeyEnabledMatched = matchBoolean(caseAttr, - R.styleable.Keyboard_Case_emojiKeyEnabled, - id.mEmojiKeyEnabled); - final boolean isMultiLineMatched = matchBoolean(caseAttr, - R.styleable.Keyboard_Case_isMultiLine, id.isMultiLine()); - final boolean imeActionMatched = matchInteger(caseAttr, - R.styleable.Keyboard_Case_imeAction, id.imeAction()); - final boolean isIconDefinedMatched = isIconDefined(caseAttr, - R.styleable.Keyboard_Case_isIconDefined, mParams.mIconsSet); - final Locale locale = id.getLocale(); - final boolean localeCodeMatched = matchLocaleCodes(caseAttr, locale); - final boolean languageCodeMatched = matchLanguageCodes(caseAttr, locale); - final boolean countryCodeMatched = matchCountryCodes(caseAttr, locale); - final boolean splitLayoutMatched = matchBoolean(caseAttr, - R.styleable.Keyboard_Case_isSplitLayout, id.mIsSplitLayout); - final boolean oneHandedModeEnabledMatched = matchBoolean(caseAttr, - R.styleable.Keyboard_Case_oneHandedModeEnabled, - id.mOneHandedModeEnabled); - final boolean selected = keyboardLayoutSetMatched && keyboardLayoutSetElementMatched - && keyboardThemeMacthed && modeMatched && navigateNextMatched - && navigatePreviousMatched && passwordInputMatched && clobberSettingsKeyMatched - && hasShortcutKeyMatched && numberRowEnabledMatched && languageSwitchKeyEnabledMatched - && emojiKeyEnabledMatched && isMultiLineMatched && imeActionMatched && isIconDefinedMatched - && localeCodeMatched && languageCodeMatched && countryCodeMatched - && splitLayoutMatched && oneHandedModeEnabledMatched; - - if (DEBUG) { - startTag("<%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s>%s", TAG_CASE, - textAttr(caseAttr.getString( - R.styleable.Keyboard_Case_keyboardLayoutSet), "keyboardLayoutSet"), - textAttr(caseAttr.getString( - R.styleable.Keyboard_Case_keyboardLayoutSetElement), - "keyboardLayoutSetElement"), - textAttr(caseAttr.getString( - R.styleable.Keyboard_Case_keyboardTheme), "keyboardTheme"), - textAttr(caseAttr.getString(R.styleable.Keyboard_Case_mode), "mode"), - textAttr(caseAttr.getString(R.styleable.Keyboard_Case_imeAction), - "imeAction"), - booleanAttr(caseAttr, R.styleable.Keyboard_Case_navigateNext, - "navigateNext"), - booleanAttr(caseAttr, R.styleable.Keyboard_Case_navigatePrevious, - "navigatePrevious"), - booleanAttr(caseAttr, R.styleable.Keyboard_Case_clobberSettingsKey, - "clobberSettingsKey"), - booleanAttr(caseAttr, R.styleable.Keyboard_Case_passwordInput, - "passwordInput"), - booleanAttr(caseAttr, R.styleable.Keyboard_Case_hasShortcutKey, - "hasShortcutKey"), - booleanAttr(caseAttr, R.styleable.Keyboard_Case_numberRowEnabled, - "numberRowEnabled"), - booleanAttr(caseAttr, R.styleable.Keyboard_Case_languageSwitchKeyEnabled, - "languageSwitchKeyEnabled"), - booleanAttr(caseAttr, R.styleable.Keyboard_Case_emojiKeyEnabled, - "emojiKeyEnabled"), - booleanAttr(caseAttr, R.styleable.Keyboard_Case_isMultiLine, - "isMultiLine"), - booleanAttr(caseAttr, R.styleable.Keyboard_Case_isSplitLayout, - "splitLayout"), - textAttr(caseAttr.getString(R.styleable.Keyboard_Case_isIconDefined), - "isIconDefined"), - textAttr(caseAttr.getString(R.styleable.Keyboard_Case_localeCode), - "localeCode"), - textAttr(caseAttr.getString(R.styleable.Keyboard_Case_languageCode), - "languageCode"), - textAttr(caseAttr.getString(R.styleable.Keyboard_Case_countryCode), - "countryCode"), - booleanAttr(caseAttr, R.styleable.Keyboard_Case_oneHandedModeEnabled, - "oneHandedModeEnabled"), - selected ? "" : " skipped"); - } - - return selected; - } finally { - caseAttr.recycle(); - } - } - - private static boolean matchLocaleCodes(TypedArray caseAttr, final Locale locale) { - return matchString(caseAttr, R.styleable.Keyboard_Case_localeCode, locale.toString()); - } - - private static boolean matchLanguageCodes(TypedArray caseAttr, Locale locale) { - return matchString(caseAttr, R.styleable.Keyboard_Case_languageCode, locale.getLanguage()); - } - - private static boolean matchCountryCodes(TypedArray caseAttr, Locale locale) { - return matchString(caseAttr, R.styleable.Keyboard_Case_countryCode, locale.getCountry()); - } - - private static boolean matchInteger(final TypedArray a, final int index, final int value) { - // If does not have "index" attribute, that means this is wild-card for - // the attribute. - return !a.hasValue(index) || a.getInt(index, 0) == value; - } - - private static boolean matchBoolean(final TypedArray a, final int index, final boolean value) { - // If does not have "index" attribute, that means this is wild-card for - // the attribute. - return !a.hasValue(index) || a.getBoolean(index, false) == value; - } - - private static boolean matchString(final TypedArray a, final int index, final String value) { - // If does not have "index" attribute, that means this is wild-card for - // the attribute. - return !a.hasValue(index) - || StringUtils.containsInArray(value, a.getString(index).split("\\|")); - } - - private static boolean matchTypedValue(final TypedArray a, final int index, final int intValue, - final String strValue) { - // If does not have "index" attribute, that means this is wild-card for - // the attribute. - final TypedValue v = a.peekValue(index); - if (v == null) { - return true; - } - if (ResourceUtils.isIntegerValue(v)) { - return intValue == a.getInt(index, 0); - } - if (ResourceUtils.isStringValue(v)) { - return StringUtils.containsInArray(strValue, a.getString(index).split("\\|")); - } - return false; - } - - private static boolean isIconDefined(final TypedArray a, final int index, - final KeyboardIconsSet iconsSet) { - if (!a.hasValue(index)) { - return true; - } - final String iconName = a.getString(index); - final int iconId = KeyboardIconsSet.getIconId(iconName); - return iconsSet.getIconDrawable(iconId) != null; - } - - private boolean parseDefault(final XmlPullParser parser, final KeyboardRow row, - final boolean skip) throws XmlPullParserException, IOException { - if (DEBUG) startTag("<%s>", TAG_DEFAULT); - if (row == null) { - parseKeyboardContent(parser, skip); - } else { - parseRowContent(parser, row, skip); - } - return true; - } - - private void parseKeyStyle(final XmlPullParser parser, final boolean skip) - throws XmlPullParserException, IOException { - final AttributeSet attr = Xml.asAttributeSet(parser); - final TypedArray keyStyleAttr = mResources.obtainAttributes( - attr, R.styleable.Keyboard_KeyStyle); - final TypedArray keyAttrs = mResources.obtainAttributes(attr, R.styleable.Keyboard_Key); - try { - if (!keyStyleAttr.hasValue(R.styleable.Keyboard_KeyStyle_styleName)) { - throw new XmlParseUtils.ParseException("<" + TAG_KEY_STYLE - + "/> needs styleName attribute", parser); - } - if (DEBUG) { - startEndTag("<%s styleName=%s />%s", TAG_KEY_STYLE, - keyStyleAttr.getString(R.styleable.Keyboard_KeyStyle_styleName), - skip ? " skipped" : ""); - } - if (!skip) { - mParams.mKeyStyles.parseKeyStyleAttributes(keyStyleAttr, keyAttrs, parser); - } - } finally { - keyStyleAttr.recycle(); - keyAttrs.recycle(); - } - XmlParseUtils.checkEndTag(TAG_KEY_STYLE, parser); - } - private void startKeyboard() { mCurrentY += mParams.mTopPadding; mTopEdge = true; } - private void startRow(final KeyboardRow row) { - addEdgeSpace(mParams.mLeftPadding, row); - mCurrentRow = row; - mLeftEdge = true; - mRightEdgeKey = null; - keysInRows.add(new ArrayList<>()); - } - - private void startRowNew() { - addEdgeSpaceNew(mParams.mLeftPadding); + private void startRow() { + addEdgeSpace(mParams.mLeftPadding); mLeftEdge = true; mRightEdgeKey = null; } - private void endRow(final KeyboardRow row) { - if (mCurrentRow == null) { - throw new RuntimeException("orphan end row tag"); - } - if (mRightEdgeKey != null) { - mRightEdgeKey.markAsRightEdge(mParams); - mRightEdgeKey = null; - } - addEdgeSpace(mParams.mRightPadding, row); - mCurrentY += row.getRowHeight(); - mCurrentRow = null; - mTopEdge = false; - } - - private void endRowNew() { + private void endRow() { int lastKeyHeight = 0; if (mRightEdgeKey != null) { mRightEdgeKey.markAsRightEdge(mParams); lastKeyHeight = mRightEdgeKey.getHeight() + mRightEdgeKey.getVerticalGap(); mRightEdgeKey = null; } - addEdgeSpaceNew(mParams.mRightPadding); + addEdgeSpace(mParams.mRightPadding); mCurrentY += lastKeyHeight; mTopEdge = false; } @@ -937,33 +133,18 @@ public class KeyboardBuilder { mCurrentY = 0; startKeyboard(); for (ArrayList row : keysInRows) { - startRowNew(); + startRow(); for (Key.KeyParams keyParams : row) { endKey(keyParams.createKey()); } - endRowNew(); + endRow(); } endKeyboard(); } - private void addEdgeSpace(final float width, final KeyboardRow row) { - row.advanceXPos(width); - mLeftEdge = false; - mRightEdgeKey = null; - } - - private void addEdgeSpaceNew(final float width) { + private void addEdgeSpace(final float width) { mCurrentX += width; mLeftEdge = false; mRightEdgeKey = null; } - - private static String textAttr(final String value, final String name) { - return value != null ? String.format(" %s=%s", name, value) : ""; - } - - private static String booleanAttr(final TypedArray a, final int index, final String name) { - return a.hasValue(index) - ? String.format(" %s=%s", name, a.getBoolean(index, false)) : ""; - } } diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/XmlKeyboardParser.java b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/XmlKeyboardParser.java new file mode 100644 index 000000000..73c56558e --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/XmlKeyboardParser.java @@ -0,0 +1,854 @@ +package org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.os.Build; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Log; +import android.util.TypedValue; +import android.util.Xml; + +import org.dslul.openboard.inputmethod.keyboard.Key; +import org.dslul.openboard.inputmethod.keyboard.KeyboardId; +import org.dslul.openboard.inputmethod.keyboard.KeyboardTheme; +import org.dslul.openboard.inputmethod.keyboard.internal.CodesArrayParser; +import org.dslul.openboard.inputmethod.keyboard.internal.KeyStyle; +import org.dslul.openboard.inputmethod.keyboard.internal.KeyVisualAttributes; +import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardIconsSet; +import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardParams; +import org.dslul.openboard.inputmethod.keyboard.internal.MoreCodesArrayParser; +import org.dslul.openboard.inputmethod.latin.R; +import org.dslul.openboard.inputmethod.latin.common.Constants; +import org.dslul.openboard.inputmethod.latin.common.StringUtils; +import org.dslul.openboard.inputmethod.latin.settings.Settings; +import org.dslul.openboard.inputmethod.latin.utils.ResourceUtils; +import org.dslul.openboard.inputmethod.latin.utils.XmlParseUtils; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Locale; + +/** + * Keyboard Building helper. + * + * This class parses Keyboard XML file and returns the KeysParams to eventually build a Keyboard. + * The Keyboard XML file looks like: + *
+ *   <!-- xml/keyboard.xml -->
+ *   <Keyboard keyboard_attributes*>
+ *     <!-- Keyboard Content -->
+ *     <Row row_attributes*>
+ *       <!-- Row Content -->
+ *       <Key key_attributes* />
+ *       <Spacer horizontalGap="32.0dp" />
+ *       <include keyboardLayout="@xml/other_keys">
+ *       ...
+ *     </Row>
+ *     <include keyboardLayout="@xml/other_rows">
+ *     ...
+ *   </Keyboard>
+ * 
+ * The XML file which is included in other file must have <merge> as root element, + * such as: + *
+ *   <!-- xml/other_keys.xml -->
+ *   <merge>
+ *     <Key key_attributes* />
+ *     ...
+ *   </merge>
+ * 
+ * and + *
+ *   <!-- xml/other_rows.xml -->
+ *   <merge>
+ *     <Row row_attributes*>
+ *       <Key key_attributes* />
+ *     </Row>
+ *     ...
+ *   </merge>
+ * 
+ * You can also use switch-case-default tags to select Rows and Keys. + *
+ *   <switch>
+ *     <case case_attribute*>
+ *       <!-- Any valid tags at switch position -->
+ *     </case>
+ *     ...
+ *     <default>
+ *       <!-- Any valid tags at switch position -->
+ *     </default>
+ *   </switch>
+ * 
+ * You can declare Key style and specify styles within Key tags. + *
+ *     <switch>
+ *       <case mode="email">
+ *         <key-style styleName="f1-key" parentStyle="modifier-key"
+ *           keyLabel=".com"
+ *         />
+ *       </case>
+ *       <case mode="url">
+ *         <key-style styleName="f1-key" parentStyle="modifier-key"
+ *           keyLabel="http://"
+ *         />
+ *       </case>
+ *     </switch>
+ *     ...
+ *     <Key keyStyle="shift-key" ... />
+ * 
+ */ +// TODO: Write unit tests for this class. +public class XmlKeyboardParser implements AutoCloseable { + private static final String PARSER_TAG = "XmlKeyboardParser"; + private static final boolean DEBUG = false; + + // Keyboard XML Tags + private static final String TAG_KEYBOARD = "Keyboard"; + private static final String TAG_ROW = "Row"; + private static final String TAG_GRID_ROWS = "GridRows"; + private static final String TAG_KEY = "Key"; + private static final String TAG_SPACER = "Spacer"; + private static final String TAG_INCLUDE = "include"; + private static final String TAG_MERGE = "merge"; + private static final String TAG_SWITCH = "switch"; + private static final String TAG_CASE = "case"; + private static final String TAG_DEFAULT = "default"; + public static final String TAG_KEY_STYLE = "key-style"; + + private static final int DEFAULT_KEYBOARD_COLUMNS = 10; + private static final int DEFAULT_KEYBOARD_ROWS = 4; + + protected final Context mContext; + protected final Resources mResources; + private final XmlResourceParser mParser; + + private int mCurrentY = 0; + private XmlKeyboardRow mCurrentRow = null; + private final KeyboardParams mParams; + private final ArrayList> keysInRows = new ArrayList<>(); + + public XmlKeyboardParser(final int xmlId, final KeyboardParams params, final Context context) { + mParams = params; + mContext = context; + mResources = context.getResources(); + mParser = mResources.getXml(xmlId); + } + + @Override + public void close() { + mParser.close(); + } + + private int mIndent; + private static final String SPACES = " "; + + private static String spaces(final int count) { + return (count < SPACES.length()) ? SPACES.substring(0, count) : SPACES; + } + + private void startTag(final String format, final Object ... args) { + Log.d(PARSER_TAG, String.format(spaces(++mIndent * 2) + format, args)); + } + + private void endTag(final String format, final Object ... args) { + Log.d(PARSER_TAG, String.format(spaces(mIndent-- * 2) + format, args)); + } + + private void startEndTag(final String format, final Object ... args) { + Log.d(PARSER_TAG, String.format(spaces(++mIndent * 2) + format, args)); + mIndent--; + } + + public ArrayList> parseKeyboard() throws XmlPullParserException, IOException { + final XmlPullParser parser = mParser; + if (DEBUG) startTag("<%s> %s", TAG_KEYBOARD, mParams.mId); + while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { + final int event = parser.next(); + if (event == XmlPullParser.START_TAG) { + final String tag = parser.getName(); + if (TAG_KEYBOARD.equals(tag)) { + // can attribute parsing moved outside / public, so that params can be adjusted before parsing the content? + // will be a problem with multiple keyboards in one xml... if that exists + parseKeyboardAttributes(parser); + startKeyboard(); + parseKeyboardContent(parser, false); + return keysInRows; + } + throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD); + } + } + throw new XmlParseUtils.ParseException("no end tag", parser); + } + + /** this and parseKeyStyle are the only place where anything is written to params */ + private void parseKeyboardAttributes(final XmlPullParser parser) { + final AttributeSet attr = Xml.asAttributeSet(parser); + final TypedArray keyboardAttr = mContext.obtainStyledAttributes( + attr, R.styleable.Keyboard, R.attr.keyboardStyle, R.style.Keyboard); + final TypedArray keyAttr = mResources.obtainAttributes(attr, R.styleable.Keyboard_Key); + final KeyboardParams params = mParams; + try { + final int height = params.mId.mHeight; + final int width = params.mId.mWidth; + params.mOccupiedHeight = height; + params.mOccupiedWidth = width; + params.mTopPadding = (int)keyboardAttr.getFraction( + R.styleable.Keyboard_keyboardTopPadding, height, height, 0); + params.mBottomPadding = (int)keyboardAttr.getFraction( + R.styleable.Keyboard_keyboardBottomPadding, height, height, 0); + params.mLeftPadding = (int)keyboardAttr.getFraction( + R.styleable.Keyboard_keyboardLeftPadding, width, width, 0); + params.mRightPadding = (int)keyboardAttr.getFraction( + R.styleable.Keyboard_keyboardRightPadding, width, width, 0); + + final int baseWidth = + params.mOccupiedWidth - params.mLeftPadding - params.mRightPadding; + params.mBaseWidth = baseWidth; + params.mDefaultKeyWidth = (int)keyAttr.getFraction(R.styleable.Keyboard_Key_keyWidth, + baseWidth, baseWidth, baseWidth / DEFAULT_KEYBOARD_COLUMNS); + + // todo: actually settings should not be accessed from here + // maybe parse normal key gaps and adjust them later (using the relative params!) + if (Settings.getInstance().getCurrent().mNarrowKeyGaps) { + params.mHorizontalGap = (int) keyboardAttr.getFraction( + R.styleable.Keyboard_horizontalGapNarrow, baseWidth, baseWidth, 0); + params.mVerticalGap = (int) keyboardAttr.getFraction( + R.styleable.Keyboard_verticalGapNarrow, height, height, 0); + } else { + params.mHorizontalGap = (int) keyboardAttr.getFraction( + R.styleable.Keyboard_horizontalGap, baseWidth, baseWidth, 0); + // TODO: Fix keyboard geometry calculation clearer. Historically vertical gap between + // rows are determined based on the entire keyboard height including top and bottom + // paddings. + params.mVerticalGap = (int) keyboardAttr.getFraction( + R.styleable.Keyboard_verticalGap, height, height, 0); + } + + final int baseHeight = params.mOccupiedHeight - params.mTopPadding + - params.mBottomPadding + params.mVerticalGap; + params.mBaseHeight = baseHeight; + params.mDefaultRowHeight = (int) ResourceUtils.getDimensionOrFraction(keyboardAttr, + R.styleable.Keyboard_rowHeight, baseHeight, baseHeight / DEFAULT_KEYBOARD_ROWS); + + params.mKeyVisualAttributes = KeyVisualAttributes.newInstance(keyAttr); + + params.mMoreKeysTemplate = keyboardAttr.getResourceId( + R.styleable.Keyboard_moreKeysTemplate, 0); + params.mMaxMoreKeysKeyboardColumn = keyAttr.getInt( + R.styleable.Keyboard_Key_maxMoreKeysColumn, 5); + + params.mThemeId = keyboardAttr.getInt(R.styleable.Keyboard_themeId, 0); + params.mIconsSet.loadIcons(keyboardAttr); + params.mTextsSet.setLocale(params.mId.getLocale(), mContext); + + final int resourceId = keyboardAttr.getResourceId( + R.styleable.Keyboard_touchPositionCorrectionData, 0); + if (resourceId != 0) { + final String[] data = mResources.getStringArray(resourceId); + params.mTouchPositionCorrection.load(data); + } + } finally { + keyAttr.recycle(); + keyboardAttr.recycle(); + } + } + + private void parseKeyboardContent(final XmlPullParser parser, final boolean skip) + throws XmlPullParserException, IOException { + while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { + final int event = parser.next(); + if (event == XmlPullParser.START_TAG) { + final String tag = parser.getName(); + if (TAG_ROW.equals(tag)) { + final XmlKeyboardRow row = parseRowAttributes(parser); + if (DEBUG) startTag("<%s>%s", TAG_ROW, skip ? " skipped" : ""); + if (!skip) { + startRow(row); + } + parseRowContent(parser, row, skip); + } else if (TAG_GRID_ROWS.equals(tag)) { + if (DEBUG) startTag("<%s>%s", TAG_GRID_ROWS, skip ? " skipped" : ""); + parseGridRows(parser, skip); + } else if (TAG_INCLUDE.equals(tag)) { + parseIncludeKeyboardContent(parser, skip); + } else if (TAG_SWITCH.equals(tag)) { + parseSwitchKeyboardContent(parser, skip); + } else if (TAG_KEY_STYLE.equals(tag)) { + parseKeyStyle(parser, skip); + } else { + throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_ROW); + } + } else if (event == XmlPullParser.END_TAG) { + final String tag = parser.getName(); + if (DEBUG) endTag("", tag); + if (TAG_KEYBOARD.equals(tag) || TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag) || TAG_MERGE.equals(tag)) { + return; + } + throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_ROW); + } + } + } + + private XmlKeyboardRow parseRowAttributes(final XmlPullParser parser) + throws XmlPullParserException { + final AttributeSet attr = Xml.asAttributeSet(parser); + final TypedArray keyboardAttr = mResources.obtainAttributes(attr, R.styleable.Keyboard); + try { + if (keyboardAttr.hasValue(R.styleable.Keyboard_horizontalGap)) { + throw new XmlParseUtils.IllegalAttribute(parser, TAG_ROW, "horizontalGap"); + } + if (keyboardAttr.hasValue(R.styleable.Keyboard_verticalGap)) { + throw new XmlParseUtils.IllegalAttribute(parser, TAG_ROW, "verticalGap"); + } + return new XmlKeyboardRow(mResources, mParams, parser, mCurrentY); + } finally { + keyboardAttr.recycle(); + } + } + + private void parseRowContent(final XmlPullParser parser, final XmlKeyboardRow row, + final boolean skip) throws XmlPullParserException, IOException { + while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { + final int event = parser.next(); + if (event == XmlPullParser.START_TAG) { + final String tag = parser.getName(); + if (TAG_KEY.equals(tag)) { + parseKey(parser, row, skip); + } else if (TAG_SPACER.equals(tag)) { + parseSpacer(parser, row, skip); + } else if (TAG_INCLUDE.equals(tag)) { + parseIncludeRowContent(parser, row, skip); + } else if (TAG_SWITCH.equals(tag)) { + parseSwitchRowContent(parser, row, skip); + } else if (TAG_KEY_STYLE.equals(tag)) { + parseKeyStyle(parser, skip); + } else { + throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_ROW); + } + } else if (event == XmlPullParser.END_TAG) { + final String tag = parser.getName(); + if (DEBUG) endTag("", tag); + if (TAG_ROW.equals(tag)) { + if (!skip) { + endRow(row); + } + return; + } + if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag) || TAG_MERGE.equals(tag)) { + return; + } + throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_ROW); + } + } + } + + private void parseGridRows(final XmlPullParser parser, final boolean skip) + throws XmlPullParserException, IOException { + if (skip) { + XmlParseUtils.checkEndTag(TAG_GRID_ROWS, parser); + if (DEBUG) { + startEndTag("<%s /> skipped", TAG_GRID_ROWS); + } + return; + } + final XmlKeyboardRow gridRows = new XmlKeyboardRow(mResources, mParams, parser, mCurrentY); + final TypedArray gridRowAttr = mResources.obtainAttributes( + Xml.asAttributeSet(parser), R.styleable.Keyboard_GridRows); + final int codesArrayId = gridRowAttr.getResourceId( + R.styleable.Keyboard_GridRows_codesArray, 0); + final int textsArrayId = gridRowAttr.getResourceId( + R.styleable.Keyboard_GridRows_textsArray, 0); + final int moreCodesArrayId = gridRowAttr.getResourceId( + R.styleable.Keyboard_GridRows_moreCodesArray, 0); + // todo: read relative key width, key / row height and gaps (but they might also be absolute, see getDimensionOrFraction) + gridRowAttr.recycle(); + if (codesArrayId == 0 && textsArrayId == 0) { + throw new XmlParseUtils.ParseException( + "Missing codesArray or textsArray attributes", parser); + } + if (codesArrayId != 0 && textsArrayId != 0) { + throw new XmlParseUtils.ParseException( + "Both codesArray and textsArray attributes specifed", parser); + } + if (textsArrayId != 0 && moreCodesArrayId != 0) { + throw new XmlParseUtils.ParseException( + "moreCodesArray is not compatible with textsArray", parser); + } + final String[] array = mResources.getStringArray( + codesArrayId != 0 ? codesArrayId : textsArrayId); + final String[] arrayMore = moreCodesArrayId != 0 ? + mResources.getStringArray(moreCodesArrayId) : null; + final int counts = array.length; + if (arrayMore != null && counts != arrayMore.length) { + throw new XmlParseUtils.ParseException( + "Inconsistent array size between codesArray and moreKeysArray", parser); + } + final float keyWidth = gridRows.getKeyWidth(null, 0.0f); + final int numColumns = (int)(mParams.mOccupiedWidth / keyWidth); + for (int index = 0; index < counts; index += numColumns) { + final XmlKeyboardRow row = new XmlKeyboardRow(mResources, mParams, parser, mCurrentY); + startRow(row); + final ArrayList keyParamsRow = keysInRows.get(keysInRows.size() - 1); + for (int c = 0; c < numColumns; c++) { + final int i = index + c; + if (i >= counts) { + break; + } + final String label; + final int code; + final String outputText; + final int supportedMinSdkVersion; + final String moreKeySpecs; + if (codesArrayId != 0) { + final String codeArraySpec = array[i]; + label = CodesArrayParser.parseLabel(codeArraySpec); + code = CodesArrayParser.parseCode(codeArraySpec); + outputText = CodesArrayParser.parseOutputText(codeArraySpec); + supportedMinSdkVersion = + CodesArrayParser.getMinSupportSdkVersion(codeArraySpec); + moreKeySpecs = MoreCodesArrayParser.parseKeySpecs( + arrayMore != null ? arrayMore[i] : null); + } else { + final String textArraySpec = array[i]; + // TODO: Utilize KeySpecParser or write more generic TextsArrayParser. + label = textArraySpec; + code = Constants.CODE_OUTPUT_TEXT; + outputText = textArraySpec + (char)Constants.CODE_SPACE; + supportedMinSdkVersion = 0; + moreKeySpecs = null; + } + if (Build.VERSION.SDK_INT < supportedMinSdkVersion) { + continue; + } + final int labelFlags = row.getDefaultKeyLabelFlags(); + // TODO: Should be able to assign default keyActionFlags as well. + final int backgroundType = row.getDefaultBackgroundType(); + final int x = (int)row.getKeyX(null); + final int y = row.getKeyY(); + final int width = (int)keyWidth; + final int height = row.getRowHeight(); + final String hintLabel = moreKeySpecs != null ? "\u25E5" : null; + final Key.KeyParams key = new Key.KeyParams(label, code, outputText, hintLabel, moreKeySpecs, + labelFlags, backgroundType, x, y, width, height, mParams); + // todo: add relative width and others + keyParamsRow.add(key); + row.advanceXPos(keyWidth); + } + endRow(row); + } + + XmlParseUtils.checkEndTag(TAG_GRID_ROWS, parser); + } + + private void parseKey(final XmlPullParser parser, final XmlKeyboardRow row, final boolean skip) + throws XmlPullParserException, IOException { + if (skip) { + XmlParseUtils.checkEndTag(TAG_KEY, parser); + if (DEBUG) startEndTag("<%s /> skipped", TAG_KEY); + return; + } + final TypedArray keyAttr = mResources.obtainAttributes( + Xml.asAttributeSet(parser), R.styleable.Keyboard_Key); + final KeyStyle keyStyle = mParams.mKeyStyles.getKeyStyle(keyAttr, parser); + final String keySpec = keyStyle.getString(keyAttr, R.styleable.Keyboard_Key_keySpec); + if (TextUtils.isEmpty(keySpec)) { + throw new XmlParseUtils.ParseException("Empty keySpec", parser); + } + final Key.KeyParams key = new Key.KeyParams(keySpec, keyAttr, keyStyle, mParams, row); + keyAttr.recycle(); + // todo: add relative width and others + if (DEBUG) { + startEndTag("<%s%s %s moreKeys=%s />", TAG_KEY, (key.mEnabled ? "" : " disabled"), + key, Arrays.toString(key.mMoreKeys)); + } + XmlParseUtils.checkEndTag(TAG_KEY, parser); + keysInRows.get(keysInRows.size() - 1).add(key); + } + + private void parseSpacer(final XmlPullParser parser, final XmlKeyboardRow row, final boolean skip) + throws XmlPullParserException, IOException { + if (skip) { + XmlParseUtils.checkEndTag(TAG_SPACER, parser); + if (DEBUG) startEndTag("<%s /> skipped", TAG_SPACER); + return; + } + final TypedArray keyAttr = mResources.obtainAttributes( + Xml.asAttributeSet(parser), R.styleable.Keyboard_Key); + final KeyStyle keyStyle = mParams.mKeyStyles.getKeyStyle(keyAttr, parser); + final Key.KeyParams spacer = Key.KeyParams.newSpacer(keyAttr, keyStyle, mParams, row); + keyAttr.recycle(); + // todo: add relative width and others + keysInRows.get(keysInRows.size() - 1).add(spacer); + if (DEBUG) startEndTag("<%s />", TAG_SPACER); + XmlParseUtils.checkEndTag(TAG_SPACER, parser); + } + + private void parseIncludeKeyboardContent(final XmlPullParser parser, final boolean skip) + throws XmlPullParserException, IOException { + parseIncludeInternal(parser, null, skip); + } + + private void parseIncludeRowContent(final XmlPullParser parser, final XmlKeyboardRow row, + final boolean skip) throws XmlPullParserException, IOException { + parseIncludeInternal(parser, row, skip); + } + + private void parseIncludeInternal(final XmlPullParser parser, final XmlKeyboardRow row, + final boolean skip) throws XmlPullParserException, IOException { + if (skip) { + XmlParseUtils.checkEndTag(TAG_INCLUDE, parser); + if (DEBUG) startEndTag(" skipped", TAG_INCLUDE); + return; + } + final AttributeSet attr = Xml.asAttributeSet(parser); + final TypedArray keyboardAttr = mResources.obtainAttributes( + attr, R.styleable.Keyboard_Include); + final TypedArray keyAttr = mResources.obtainAttributes(attr, R.styleable.Keyboard_Key); + final int keyboardLayout; + try { + XmlParseUtils.checkAttributeExists( + keyboardAttr, R.styleable.Keyboard_Include_keyboardLayout, "keyboardLayout", + TAG_INCLUDE, parser); + keyboardLayout = keyboardAttr.getResourceId( + R.styleable.Keyboard_Include_keyboardLayout, 0); + if (row != null) { + // Override current x coordinate. + row.setXPos(row.getKeyX(keyAttr)); + // Push current Row attributes and update with new attributes. + row.pushRowAttributes(keyAttr); + } + } finally { + keyboardAttr.recycle(); + keyAttr.recycle(); + } + + XmlParseUtils.checkEndTag(TAG_INCLUDE, parser); + if (DEBUG) { + startEndTag("<%s keyboardLayout=%s />",TAG_INCLUDE, + mResources.getResourceEntryName(keyboardLayout)); + } + try (XmlResourceParser parserForInclude = mResources.getXml(keyboardLayout)) { + parseMerge(parserForInclude, row, skip); + } finally { + if (row != null) { + // Restore Row attributes. + row.popRowAttributes(); + } + } + } + + private void parseMerge(final XmlPullParser parser, final XmlKeyboardRow row, final boolean skip) + throws XmlPullParserException, IOException { + if (DEBUG) startTag("<%s>", TAG_MERGE); + while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { + final int event = parser.next(); + if (event == XmlPullParser.START_TAG) { + final String tag = parser.getName(); + if (TAG_MERGE.equals(tag)) { + if (row == null) { + parseKeyboardContent(parser, skip); + } else { + parseRowContent(parser, row, skip); + } + return; + } + throw new XmlParseUtils.ParseException( + "Included keyboard layout must have root element", parser); + } + } + } + + private void parseSwitchKeyboardContent(final XmlPullParser parser, final boolean skip) + throws XmlPullParserException, IOException { + parseSwitchInternal(parser, null, skip); + } + + private void parseSwitchRowContent(final XmlPullParser parser, final XmlKeyboardRow row, + final boolean skip) throws XmlPullParserException, IOException { + parseSwitchInternal(parser, row, skip); + } + + private void parseSwitchInternal(final XmlPullParser parser, final XmlKeyboardRow row, + final boolean skip) throws XmlPullParserException, IOException { + if (DEBUG) startTag("<%s> %s", TAG_SWITCH, mParams.mId); + boolean selected = false; + while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { + final int event = parser.next(); + if (event == XmlPullParser.START_TAG) { + final String tag = parser.getName(); + if (TAG_CASE.equals(tag)) { + selected |= parseCase(parser, row, selected || skip); + } else if (TAG_DEFAULT.equals(tag)) { + selected |= parseDefault(parser, row, selected || skip); + } else { + throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_SWITCH); + } + } else if (event == XmlPullParser.END_TAG) { + final String tag = parser.getName(); + if (TAG_SWITCH.equals(tag)) { + if (DEBUG) endTag("", TAG_SWITCH); + return; + } + throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_SWITCH); + } + } + } + + private boolean parseCase(final XmlPullParser parser, final XmlKeyboardRow row, final boolean skip) + throws XmlPullParserException, IOException { + final boolean selected = parseCaseCondition(parser); + if (row == null) { + // Processing Rows. + parseKeyboardContent(parser, !selected || skip); + } else { + // Processing Keys. + parseRowContent(parser, row, !selected || skip); + } + return selected; + } + + private boolean parseCaseCondition(final XmlPullParser parser) { + final KeyboardId id = mParams.mId; + if (id == null) { + return true; + } + final AttributeSet attr = Xml.asAttributeSet(parser); + final TypedArray caseAttr = mResources.obtainAttributes(attr, R.styleable.Keyboard_Case); + try { + final boolean keyboardLayoutSetMatched = matchString(caseAttr, + R.styleable.Keyboard_Case_keyboardLayoutSet, + id.mSubtype.getKeyboardLayoutSetName()); + final boolean keyboardLayoutSetElementMatched = matchTypedValue(caseAttr, + R.styleable.Keyboard_Case_keyboardLayoutSetElement, id.mElementId, + KeyboardId.elementIdToName(id.mElementId)); + final boolean keyboardThemeMacthed = matchTypedValue(caseAttr, + R.styleable.Keyboard_Case_keyboardTheme, mParams.mThemeId, + KeyboardTheme.getKeyboardThemeName(mParams.mThemeId)); + final boolean modeMatched = matchTypedValue(caseAttr, + R.styleable.Keyboard_Case_mode, id.mMode, KeyboardId.modeName(id.mMode)); + final boolean navigateNextMatched = matchBoolean(caseAttr, + R.styleable.Keyboard_Case_navigateNext, id.navigateNext()); + final boolean navigatePreviousMatched = matchBoolean(caseAttr, + R.styleable.Keyboard_Case_navigatePrevious, id.navigatePrevious()); + final boolean passwordInputMatched = matchBoolean(caseAttr, + R.styleable.Keyboard_Case_passwordInput, id.passwordInput()); + final boolean clobberSettingsKeyMatched = matchBoolean(caseAttr, + R.styleable.Keyboard_Case_clobberSettingsKey, id.mClobberSettingsKey); + final boolean hasShortcutKeyMatched = matchBoolean(caseAttr, + R.styleable.Keyboard_Case_hasShortcutKey, id.mHasShortcutKey); + final boolean numberRowEnabledMatched = matchBoolean(caseAttr, + R.styleable.Keyboard_Case_numberRowEnabled, + id.mNumberRowEnabled); + final boolean languageSwitchKeyEnabledMatched = matchBoolean(caseAttr, + R.styleable.Keyboard_Case_languageSwitchKeyEnabled, + id.mLanguageSwitchKeyEnabled); + final boolean emojiKeyEnabledMatched = matchBoolean(caseAttr, + R.styleable.Keyboard_Case_emojiKeyEnabled, + id.mEmojiKeyEnabled); + final boolean isMultiLineMatched = matchBoolean(caseAttr, + R.styleable.Keyboard_Case_isMultiLine, id.isMultiLine()); + final boolean imeActionMatched = matchInteger(caseAttr, + R.styleable.Keyboard_Case_imeAction, id.imeAction()); + final boolean isIconDefinedMatched = isIconDefined(caseAttr, + R.styleable.Keyboard_Case_isIconDefined, mParams.mIconsSet); + final Locale locale = id.getLocale(); + final boolean localeCodeMatched = matchLocaleCodes(caseAttr, locale); + final boolean languageCodeMatched = matchLanguageCodes(caseAttr, locale); + final boolean countryCodeMatched = matchCountryCodes(caseAttr, locale); + final boolean splitLayoutMatched = matchBoolean(caseAttr, + R.styleable.Keyboard_Case_isSplitLayout, id.mIsSplitLayout); + final boolean oneHandedModeEnabledMatched = matchBoolean(caseAttr, + R.styleable.Keyboard_Case_oneHandedModeEnabled, + id.mOneHandedModeEnabled); + final boolean selected = keyboardLayoutSetMatched && keyboardLayoutSetElementMatched + && keyboardThemeMacthed && modeMatched && navigateNextMatched + && navigatePreviousMatched && passwordInputMatched && clobberSettingsKeyMatched + && hasShortcutKeyMatched && numberRowEnabledMatched && languageSwitchKeyEnabledMatched + && emojiKeyEnabledMatched && isMultiLineMatched && imeActionMatched && isIconDefinedMatched + && localeCodeMatched && languageCodeMatched && countryCodeMatched + && splitLayoutMatched && oneHandedModeEnabledMatched; + + if (DEBUG) { + startTag("<%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s>%s", TAG_CASE, + textAttr(caseAttr.getString( + R.styleable.Keyboard_Case_keyboardLayoutSet), "keyboardLayoutSet"), + textAttr(caseAttr.getString( + R.styleable.Keyboard_Case_keyboardLayoutSetElement), + "keyboardLayoutSetElement"), + textAttr(caseAttr.getString( + R.styleable.Keyboard_Case_keyboardTheme), "keyboardTheme"), + textAttr(caseAttr.getString(R.styleable.Keyboard_Case_mode), "mode"), + textAttr(caseAttr.getString(R.styleable.Keyboard_Case_imeAction), + "imeAction"), + booleanAttr(caseAttr, R.styleable.Keyboard_Case_navigateNext, + "navigateNext"), + booleanAttr(caseAttr, R.styleable.Keyboard_Case_navigatePrevious, + "navigatePrevious"), + booleanAttr(caseAttr, R.styleable.Keyboard_Case_clobberSettingsKey, + "clobberSettingsKey"), + booleanAttr(caseAttr, R.styleable.Keyboard_Case_passwordInput, + "passwordInput"), + booleanAttr(caseAttr, R.styleable.Keyboard_Case_hasShortcutKey, + "hasShortcutKey"), + booleanAttr(caseAttr, R.styleable.Keyboard_Case_numberRowEnabled, + "numberRowEnabled"), + booleanAttr(caseAttr, R.styleable.Keyboard_Case_languageSwitchKeyEnabled, + "languageSwitchKeyEnabled"), + booleanAttr(caseAttr, R.styleable.Keyboard_Case_emojiKeyEnabled, + "emojiKeyEnabled"), + booleanAttr(caseAttr, R.styleable.Keyboard_Case_isMultiLine, + "isMultiLine"), + booleanAttr(caseAttr, R.styleable.Keyboard_Case_isSplitLayout, + "splitLayout"), + textAttr(caseAttr.getString(R.styleable.Keyboard_Case_isIconDefined), + "isIconDefined"), + textAttr(caseAttr.getString(R.styleable.Keyboard_Case_localeCode), + "localeCode"), + textAttr(caseAttr.getString(R.styleable.Keyboard_Case_languageCode), + "languageCode"), + textAttr(caseAttr.getString(R.styleable.Keyboard_Case_countryCode), + "countryCode"), + booleanAttr(caseAttr, R.styleable.Keyboard_Case_oneHandedModeEnabled, + "oneHandedModeEnabled"), + selected ? "" : " skipped"); + } + + return selected; + } finally { + caseAttr.recycle(); + } + } + + private static boolean matchLocaleCodes(TypedArray caseAttr, final Locale locale) { + return matchString(caseAttr, R.styleable.Keyboard_Case_localeCode, locale.toString()); + } + + private static boolean matchLanguageCodes(TypedArray caseAttr, Locale locale) { + return matchString(caseAttr, R.styleable.Keyboard_Case_languageCode, locale.getLanguage()); + } + + private static boolean matchCountryCodes(TypedArray caseAttr, Locale locale) { + return matchString(caseAttr, R.styleable.Keyboard_Case_countryCode, locale.getCountry()); + } + + private static boolean matchInteger(final TypedArray a, final int index, final int value) { + // If does not have "index" attribute, that means this is wild-card for + // the attribute. + return !a.hasValue(index) || a.getInt(index, 0) == value; + } + + private static boolean matchBoolean(final TypedArray a, final int index, final boolean value) { + // If does not have "index" attribute, that means this is wild-card for + // the attribute. + return !a.hasValue(index) || a.getBoolean(index, false) == value; + } + + private static boolean matchString(final TypedArray a, final int index, final String value) { + // If does not have "index" attribute, that means this is wild-card for + // the attribute. + return !a.hasValue(index) + || StringUtils.containsInArray(value, a.getString(index).split("\\|")); + } + + private static boolean matchTypedValue(final TypedArray a, final int index, final int intValue, + final String strValue) { + // If does not have "index" attribute, that means this is wild-card for + // the attribute. + final TypedValue v = a.peekValue(index); + if (v == null) { + return true; + } + if (ResourceUtils.isIntegerValue(v)) { + return intValue == a.getInt(index, 0); + } + if (ResourceUtils.isStringValue(v)) { + return StringUtils.containsInArray(strValue, a.getString(index).split("\\|")); + } + return false; + } + + private static boolean isIconDefined(final TypedArray a, final int index, + final KeyboardIconsSet iconsSet) { + if (!a.hasValue(index)) { + return true; + } + final String iconName = a.getString(index); + final int iconId = KeyboardIconsSet.getIconId(iconName); + return iconsSet.getIconDrawable(iconId) != null; + } + + private boolean parseDefault(final XmlPullParser parser, final XmlKeyboardRow row, + final boolean skip) throws XmlPullParserException, IOException { + if (DEBUG) startTag("<%s>", TAG_DEFAULT); + if (row == null) { + parseKeyboardContent(parser, skip); + } else { + parseRowContent(parser, row, skip); + } + return true; + } + + private void parseKeyStyle(final XmlPullParser parser, final boolean skip) + throws XmlPullParserException, IOException { + final AttributeSet attr = Xml.asAttributeSet(parser); + final TypedArray keyStyleAttr = mResources.obtainAttributes( + attr, R.styleable.Keyboard_KeyStyle); + final TypedArray keyAttrs = mResources.obtainAttributes(attr, R.styleable.Keyboard_Key); + try { + if (!keyStyleAttr.hasValue(R.styleable.Keyboard_KeyStyle_styleName)) { + throw new XmlParseUtils.ParseException("<" + TAG_KEY_STYLE + + "/> needs styleName attribute", parser); + } + if (DEBUG) { + startEndTag("<%s styleName=%s />%s", TAG_KEY_STYLE, + keyStyleAttr.getString(R.styleable.Keyboard_KeyStyle_styleName), + skip ? " skipped" : ""); + } + if (!skip) { + mParams.mKeyStyles.parseKeyStyleAttributes(keyStyleAttr, keyAttrs, parser); + } + } finally { + keyStyleAttr.recycle(); + keyAttrs.recycle(); + } + XmlParseUtils.checkEndTag(TAG_KEY_STYLE, parser); + } + + private void startKeyboard() { + mCurrentY += mParams.mTopPadding; + } + + private void startRow(final XmlKeyboardRow row) { + addEdgeSpace(mParams.mLeftPadding, row); + mCurrentRow = row; + keysInRows.add(new ArrayList<>()); + } + + private void endRow(final XmlKeyboardRow row) { + if (mCurrentRow == null) { + throw new RuntimeException("orphan end row tag"); + } + addEdgeSpace(mParams.mRightPadding, row); + mCurrentY += row.getRowHeight(); + mCurrentRow = null; + } + + private void addEdgeSpace(final float width, final XmlKeyboardRow row) { + row.advanceXPos(width); + } + + private static String textAttr(final String value, final String name) { + return value != null ? String.format(" %s=%s", name, value) : ""; + } + + private static String booleanAttr(final TypedArray a, final int index, final String name) { + return a.hasValue(index) + ? String.format(" %s=%s", name, a.getBoolean(index, false)) : ""; + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/KeyboardRow.java b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/XmlKeyboardRow.java similarity index 95% rename from app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/KeyboardRow.java rename to app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/XmlKeyboardRow.java index de7e5a84d..bd9b5a741 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/KeyboardRow.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/keyboard_parser/XmlKeyboardRow.java @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only */ -package org.dslul.openboard.inputmethod.keyboard.internal; +package org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser; import android.content.res.Resources; import android.content.res.TypedArray; @@ -12,6 +12,7 @@ import android.util.Xml; import org.dslul.openboard.inputmethod.keyboard.Key; import org.dslul.openboard.inputmethod.keyboard.Keyboard; +import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardParams; import org.dslul.openboard.inputmethod.latin.R; import org.dslul.openboard.inputmethod.latin.utils.ResourceUtils; import org.xmlpull.v1.XmlPullParser; @@ -23,7 +24,7 @@ import java.util.ArrayDeque; * Some of the key size defaults can be overridden per row from what the {@link Keyboard} * defines. */ -public final class KeyboardRow { +public final class XmlKeyboardRow { // keyWidth enum constants private static final int KEYWIDTH_NOT_ENUM = 0; private static final int KEYWIDTH_FILL_RIGHT = -1; @@ -82,8 +83,8 @@ public final class KeyboardRow { // Will be updated by {@link Key}'s constructor. private float mCurrentX; - public KeyboardRow(final Resources res, final KeyboardParams params, - final XmlPullParser parser, final int y) { + public XmlKeyboardRow(final Resources res, final KeyboardParams params, + final XmlPullParser parser, final int y) { mParams = params; final TypedArray keyboardAttr = res.obtainAttributes(Xml.asAttributeSet(parser), R.styleable.Keyboard); diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/latin/suggestions/MoreSuggestions.java b/app/src/main/java/org/dslul/openboard/inputmethod/latin/suggestions/MoreSuggestions.java index e08ef48f4..1117a4a8c 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/latin/suggestions/MoreSuggestions.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/latin/suggestions/MoreSuggestions.java @@ -181,7 +181,7 @@ public final class MoreSuggestions extends Keyboard { final int maxWidth, final int minWidth, final int maxRow, final Keyboard parentKeyboard) { final int xmlId = R.xml.kbd_suggestions_pane_template; - load(xmlId, parentKeyboard.mId); + loadFromXml(xmlId, parentKeyboard.mId); mParams.mVerticalGap = mParams.mTopPadding = parentKeyboard.mVerticalGap / 2; mPaneView.updateKeyboardGeometry(mParams.mDefaultRowHeight); final int count = mParams.layout(suggestedWords, fromIndex, maxWidth, minWidth, maxRow, diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 0c6e23c43..65d82d055 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -367,7 +367,7 @@ If the value is fillRight, the actual key width will be determined to fill out the area up to the right edge of the keyboard. --> + {@link org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.XmlKeyboardRow#KEYWIDTH_FILL_RIGHT}. -->