package juloo.keyboard2; import android.content.res.Resources; import android.content.res.XmlResourceParser; import java.util.ArrayList; import java.util.function.Function; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; class KeyboardData { public final List rows; /** Total width of the keyboard. */ public final float keysWidth; /** Total height of the keyboard. */ public final float keysHeight; /** Whether to add extra keys. */ public final boolean extra_keys; public KeyboardData mapKeys(MapKey f) { ArrayList rows_ = new ArrayList(); for (Row r : rows) rows_.add(r.mapKeys(f)); return new KeyboardData(rows_, keysWidth, extra_keys); } /** Add keys from the given iterator into the keyboard. Extra keys are added * on the empty key4 corner of the second row, from right to left. If there's * not enough room, key3 of the second row is tried then key2 and key1 of the * third row. */ public KeyboardData addExtraKeys(Iterator k) { if (!extra_keys) return this; ArrayList rows = new ArrayList(this.rows); addExtraKeys_to_row(rows, k, 1, 4); addExtraKeys_to_row(rows, k, 1, 3); addExtraKeys_to_row(rows, k, 2, 2); addExtraKeys_to_row(rows, k, 2, 1); if (k.hasNext()) { for (int r = 0; r < rows.size(); r++) for (int c = 1; c <= 4; c++) addExtraKeys_to_row(rows, k, r, c); } return new KeyboardData(rows, keysWidth, extra_keys); } public Key findKeyWithValue(KeyValue kv) { for (Row r : rows) { Key k = r.findKeyWithValue(kv); if (k != null) return k; } return null; } private static void addExtraKeys_to_row(ArrayList rows, final Iterator extra_keys, int row_i, final int d) { if (!extra_keys.hasNext()) return; rows.set(row_i, rows.get(row_i).mapKeys(new MapKey(){ public Key apply(Key k) { if (k.getKeyValue(d) == null && extra_keys.hasNext()) return k.withKeyValue(d, extra_keys.next()); else return k; } })); } private static Row _bottomRow = null; private static Map _layoutCache = new HashMap(); public static KeyboardData load(Resources res, int id) { KeyboardData l = _layoutCache.get(id); if (l == null) { try { if (_bottomRow == null) _bottomRow = parse_bottom_row(res.getXml(R.xml.bottom_row)); l = parse_keyboard(res.getXml(id)); _layoutCache.put(id, l); } catch (Exception e) { e.printStackTrace(); } } return l; } private static KeyboardData parse_keyboard(XmlResourceParser parser) throws Exception { if (!expect_tag(parser, "keyboard")) throw new Exception("Empty layout file"); boolean bottom_row = parser.getAttributeBooleanValue(null, "bottom_row", true); boolean extra_keys = parser.getAttributeBooleanValue(null, "extra_keys", true); ArrayList rows = new ArrayList(); while (expect_tag(parser, "row")) rows.add(Row.parse(parser)); float kw = compute_max_width(rows); if (bottom_row) rows.add(_bottomRow.updateWidth(kw)); return new KeyboardData(rows, kw, extra_keys); } private static float compute_max_width(List rows) { float w = 0.f; for (Row r : rows) w = Math.max(w, r.keysWidth); return w; } private static Row parse_bottom_row(XmlResourceParser parser) throws Exception { if (!expect_tag(parser, "row")) throw new Exception("Failed to parse bottom row"); return Row.parse(parser); } protected KeyboardData(List rows_, float kw, boolean xk) { float kh = 0.f; for (Row r : rows_) kh += r.height + r.shift; rows = rows_; keysWidth = kw; keysHeight = kh; extra_keys = xk; } public static class Row { public final List keys; /** Height of the row, without 'shift'. */ public final float height; /** Extra empty space on the top. */ public final float shift; /** Total width of the row. */ private final float keysWidth; protected Row(List keys_, float h, float s) { float kw = 0.f; for (Key k : keys_) kw += k.width + k.shift; keys = keys_; height = h; shift = s; keysWidth = kw; } public static Row parse(XmlResourceParser parser) throws Exception { ArrayList keys = new ArrayList(); int status; float h = parser.getAttributeFloatValue(null, "height", 1f); float shift = parser.getAttributeFloatValue(null, "shift", 0f); while (expect_tag(parser, "key")) keys.add(Key.parse(parser)); return new Row(keys, h, shift); } public Row mapKeys(MapKey f) { ArrayList keys_ = new ArrayList(); for (Key k : keys) keys_.add(f.apply(k)); return new Row(keys_, height, shift); } /** Change the width of every keys so that the row is 's' units wide. */ public Row updateWidth(float newWidth) { final float s = newWidth / keysWidth; return mapKeys(new MapKey(){ public Key apply(Key k) { return k.scaleWidth(s); } }); } public Key findKeyWithValue(KeyValue kv) { for (Key k : keys) if (k.hasValue(kv)) return k; return null; } } public static class Key { /* ** 1 2 ** 0 ** 3 4 */ public final Corner key0; public final Corner key1; public final Corner key2; public final Corner key3; public final Corner key4; /** Key width in relative unit. */ public final float width; /** Extra empty space on the left of the key. */ public final float shift; /** Put keys 1 to 4 on the edges instead of the corners. */ public final boolean edgekeys; protected Key(Corner k0, Corner k1, Corner k2, Corner k3, Corner k4, float w, float s, boolean e) { key0 = k0; key1 = k1; key2 = k2; key3 = k3; key4 = k4; width = w; shift = s; edgekeys = e; } public static Key parse(XmlResourceParser parser) throws Exception { Corner k0 = Corner.parse_of_attr(parser, "key0"); Corner k1 = Corner.parse_of_attr(parser, "key1"); Corner k2 = Corner.parse_of_attr(parser, "key2"); Corner k3 = Corner.parse_of_attr(parser, "key3"); Corner k4 = Corner.parse_of_attr(parser, "key4"); float width = parser.getAttributeFloatValue(null, "width", 1f); float shift = parser.getAttributeFloatValue(null, "shift", 0.f); boolean edgekeys = parser.getAttributeBooleanValue(null, "edgekeys", false); while (parser.next() != XmlResourceParser.END_TAG) continue ; return new Key(k0, k1, k2, k3, k4, width, shift, edgekeys); } /** New key with the width multiplied by 's'. */ public Key scaleWidth(float s) { return new Key(key0, key1, key2, key3, key4, width * s, shift, edgekeys); } public KeyValue getKeyValue(int i) { Corner c; switch (i) { case 0: c = key0; break; case 1: c = key1; break; case 2: c = key2; break; case 3: c = key3; break; case 4: c = key4; break; default: c = null; break; } return (c == null) ? null : c.kv; } public Key withKeyValue(int i, KeyValue kv) { Corner k0 = key0, k1 = key1, k2 = key2, k3 = key3, k4 = key4; Corner k = Corner.of_kv(kv); switch (i) { case 0: k0 = k; break; case 1: k1 = k; break; case 2: k2 = k; break; case 3: k3 = k; break; case 4: k4 = k; break; } return new Key(k0, k1, k2, k3, k4, width, shift, edgekeys); } /** * See Pointers.onTouchMove() for the represented direction. */ public KeyValue getAtDirection(int direction) { Corner c = null; if (edgekeys) { // \ 1 / // \ / // 3 0 2 // / \ // / 4 \ switch (direction) { case 2: case 3: c = key1; break; case 4: case 5: c = key2; break; case 6: case 7: c = key4; break; case 8: case 1: c = key3; break; } } else { // 1 | 2 // | // --0-- // | // 3 | 4 switch (direction) { case 1: case 2: c = key1; break; case 3: case 4: c = key2; break; case 5: case 6: c = key4; break; case 7: case 8: c = key3; break; } } return (c == null) ? null : c.kv; } public boolean hasValue(KeyValue kv) { return (hasValue(key0, kv) || hasValue(key1, kv) || hasValue(key2, kv) || hasValue(key3, kv) || hasValue(key4, kv)); } private static boolean hasValue(Corner c, KeyValue kv) { return (c != null && c.kv.equals(kv)); } } public static final class Corner { public final KeyValue kv; /** Whether the kv is marked with the "loc " prefix. To be removed if not specified in the [extra_keys]. */ public final boolean localized; protected Corner(KeyValue k, boolean l) { kv = k; localized = l; } public static Corner parse_of_attr(XmlResourceParser parser, String attr) throws Exception { String name = parser.getAttributeValue(null, attr); boolean localized = false; if (name == null) return null; String name_loc = stripPrefix(name, "loc "); if (name_loc != null) { localized = true; name = name_loc; } return new Corner(KeyValue.getKeyByName(name), localized); } public static Corner of_kv(KeyValue kv) { return new Corner(kv, false); } private static String stripPrefix(String s, String prefix) { if (s.startsWith(prefix)) return s.substring(prefix.length()); else return null; } } // Not using Function to keep compatibility with Android 6. public static abstract interface MapKey { public Key apply(Key k); } public static abstract class MapKeyValues implements MapKey { abstract public KeyValue apply(KeyValue c, boolean localized); public Key apply(Key k) { return new Key(apply(k.key0), apply(k.key1), apply(k.key2), apply(k.key3), apply(k.key4), k.width, k.shift, k.edgekeys); } private Corner apply(Corner c) { if (c == null) return null; KeyValue kv = apply(c.kv, c.localized); if (kv == null) return null; return Corner.of_kv(kv); } } /** Parsing utils */ /** Returns [false] on [END_DOCUMENT] or [END_TAG], [true] otherwise. */ private static boolean expect_tag(XmlResourceParser parser, String name) throws Exception { int status; do { status = parser.next(); if (status == XmlResourceParser.END_DOCUMENT || status == XmlResourceParser.END_TAG) return false; } while (status != XmlResourceParser.START_TAG); if (!parser.getName().equals(name)) throw new Exception("Unknow tag: " + parser.getName()); return true; } }