1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.inputmethod.keyboard;
18
19import android.content.Context;
20import android.graphics.Paint;
21
22import com.android.inputmethod.annotations.UsedForTesting;
23import com.android.inputmethod.keyboard.internal.KeyboardBuilder;
24import com.android.inputmethod.keyboard.internal.KeyboardParams;
25import com.android.inputmethod.keyboard.internal.MoreKeySpec;
26import com.android.inputmethod.latin.R;
27import com.android.inputmethod.latin.common.StringUtils;
28import com.android.inputmethod.latin.utils.TypefaceUtils;
29
30import javax.annotation.Nonnull;
31
32public final class MoreKeysKeyboard extends Keyboard {
33    private final int mDefaultKeyCoordX;
34
35    MoreKeysKeyboard(final MoreKeysKeyboardParams params) {
36        super(params);
37        mDefaultKeyCoordX = params.getDefaultKeyCoordX() + params.mDefaultKeyWidth / 2;
38    }
39
40    public int getDefaultCoordX() {
41        return mDefaultKeyCoordX;
42    }
43
44    @UsedForTesting
45    static class MoreKeysKeyboardParams extends KeyboardParams {
46        public boolean mIsMoreKeysFixedOrder;
47        /* package */int mTopRowAdjustment;
48        public int mNumRows;
49        public int mNumColumns;
50        public int mTopKeys;
51        public int mLeftKeys;
52        public int mRightKeys; // includes default key.
53        public int mDividerWidth;
54        public int mColumnWidth;
55
56        public MoreKeysKeyboardParams() {
57            super();
58        }
59
60        /**
61         * Set keyboard parameters of more keys keyboard.
62         *
63         * @param numKeys number of keys in this more keys keyboard.
64         * @param numColumn number of columns of this more keys keyboard.
65         * @param keyWidth more keys keyboard key width in pixel, including horizontal gap.
66         * @param rowHeight more keys keyboard row height in pixel, including vertical gap.
67         * @param coordXInParent coordinate x of the key preview in parent keyboard.
68         * @param parentKeyboardWidth parent keyboard width in pixel.
69         * @param isMoreKeysFixedColumn true if more keys keyboard should have
70         *   <code>numColumn</code> columns. Otherwise more keys keyboard should have
71         *   <code>numColumn</code> columns at most.
72         * @param isMoreKeysFixedOrder true if the order of more keys is determined by the order in
73         *   the more keys' specification. Otherwise the order of more keys is automatically
74         *   determined.
75         * @param dividerWidth width of divider, zero for no dividers.
76         */
77        public void setParameters(final int numKeys, final int numColumn, final int keyWidth,
78                final int rowHeight, final int coordXInParent, final int parentKeyboardWidth,
79                final boolean isMoreKeysFixedColumn, final boolean isMoreKeysFixedOrder,
80                final int dividerWidth) {
81            mIsMoreKeysFixedOrder = isMoreKeysFixedOrder;
82            if (parentKeyboardWidth / keyWidth < Math.min(numKeys, numColumn)) {
83                throw new IllegalArgumentException("Keyboard is too small to hold more keys: "
84                        + parentKeyboardWidth + " " + keyWidth + " " + numKeys + " " + numColumn);
85            }
86            mDefaultKeyWidth = keyWidth;
87            mDefaultRowHeight = rowHeight;
88
89            final int numRows = (numKeys + numColumn - 1) / numColumn;
90            mNumRows = numRows;
91            final int numColumns = isMoreKeysFixedColumn ? Math.min(numKeys, numColumn)
92                    : getOptimizedColumns(numKeys, numColumn);
93            mNumColumns = numColumns;
94            final int topKeys = numKeys % numColumns;
95            mTopKeys = topKeys == 0 ? numColumns : topKeys;
96
97            final int numLeftKeys = (numColumns - 1) / 2;
98            final int numRightKeys = numColumns - numLeftKeys; // including default key.
99            // Maximum number of keys we can layout both side of the parent key
100            final int maxLeftKeys = coordXInParent / keyWidth;
101            final int maxRightKeys = (parentKeyboardWidth - coordXInParent) / keyWidth;
102            int leftKeys, rightKeys;
103            if (numLeftKeys > maxLeftKeys) {
104                leftKeys = maxLeftKeys;
105                rightKeys = numColumns - leftKeys;
106            } else if (numRightKeys > maxRightKeys + 1) {
107                rightKeys = maxRightKeys + 1; // include default key
108                leftKeys = numColumns - rightKeys;
109            } else {
110                leftKeys = numLeftKeys;
111                rightKeys = numRightKeys;
112            }
113            // If the left keys fill the left side of the parent key, entire more keys keyboard
114            // should be shifted to the right unless the parent key is on the left edge.
115            if (maxLeftKeys == leftKeys && leftKeys > 0) {
116                leftKeys--;
117                rightKeys++;
118            }
119            // If the right keys fill the right side of the parent key, entire more keys
120            // should be shifted to the left unless the parent key is on the right edge.
121            if (maxRightKeys == rightKeys - 1 && rightKeys > 1) {
122                leftKeys++;
123                rightKeys--;
124            }
125            mLeftKeys = leftKeys;
126            mRightKeys = rightKeys;
127
128            // Adjustment of the top row.
129            mTopRowAdjustment = isMoreKeysFixedOrder ? getFixedOrderTopRowAdjustment()
130                    : getAutoOrderTopRowAdjustment();
131            mDividerWidth = dividerWidth;
132            mColumnWidth = mDefaultKeyWidth + mDividerWidth;
133            mBaseWidth = mOccupiedWidth = mNumColumns * mColumnWidth - mDividerWidth;
134            // Need to subtract the bottom row's gutter only.
135            mBaseHeight = mOccupiedHeight = mNumRows * mDefaultRowHeight - mVerticalGap
136                    + mTopPadding + mBottomPadding;
137        }
138
139        private int getFixedOrderTopRowAdjustment() {
140            if (mNumRows == 1 || mTopKeys % 2 == 1 || mTopKeys == mNumColumns
141                    || mLeftKeys == 0  || mRightKeys == 1) {
142                return 0;
143            }
144            return -1;
145        }
146
147        private int getAutoOrderTopRowAdjustment() {
148            if (mNumRows == 1 || mTopKeys == 1 || mNumColumns % 2 == mTopKeys % 2
149                    || mLeftKeys == 0 || mRightKeys == 1) {
150                return 0;
151            }
152            return -1;
153        }
154
155        // Return key position according to column count (0 is default).
156        /* package */int getColumnPos(final int n) {
157            return mIsMoreKeysFixedOrder ? getFixedOrderColumnPos(n) : getAutomaticColumnPos(n);
158        }
159
160        private int getFixedOrderColumnPos(final int n) {
161            final int col = n % mNumColumns;
162            final int row = n / mNumColumns;
163            if (!isTopRow(row)) {
164                return col - mLeftKeys;
165            }
166            final int rightSideKeys = mTopKeys / 2;
167            final int leftSideKeys = mTopKeys - (rightSideKeys + 1);
168            final int pos = col - leftSideKeys;
169            final int numLeftKeys = mLeftKeys + mTopRowAdjustment;
170            final int numRightKeys = mRightKeys - 1;
171            if (numRightKeys >= rightSideKeys && numLeftKeys >= leftSideKeys) {
172                return pos;
173            } else if (numRightKeys < rightSideKeys) {
174                return pos - (rightSideKeys - numRightKeys);
175            } else { // numLeftKeys < leftSideKeys
176                return pos + (leftSideKeys - numLeftKeys);
177            }
178        }
179
180        private int getAutomaticColumnPos(final int n) {
181            final int col = n % mNumColumns;
182            final int row = n / mNumColumns;
183            int leftKeys = mLeftKeys;
184            if (isTopRow(row)) {
185                leftKeys += mTopRowAdjustment;
186            }
187            if (col == 0) {
188                // default position.
189                return 0;
190            }
191
192            int pos = 0;
193            int right = 1; // include default position key.
194            int left = 0;
195            int i = 0;
196            while (true) {
197                // Assign right key if available.
198                if (right < mRightKeys) {
199                    pos = right;
200                    right++;
201                    i++;
202                }
203                if (i >= col)
204                    break;
205                // Assign left key if available.
206                if (left < leftKeys) {
207                    left++;
208                    pos = -left;
209                    i++;
210                }
211                if (i >= col)
212                    break;
213            }
214            return pos;
215        }
216
217        private static int getTopRowEmptySlots(final int numKeys, final int numColumns) {
218            final int remainings = numKeys % numColumns;
219            return remainings == 0 ? 0 : numColumns - remainings;
220        }
221
222        private int getOptimizedColumns(final int numKeys, final int maxColumns) {
223            int numColumns = Math.min(numKeys, maxColumns);
224            while (getTopRowEmptySlots(numKeys, numColumns) >= mNumRows) {
225                numColumns--;
226            }
227            return numColumns;
228        }
229
230        public int getDefaultKeyCoordX() {
231            return mLeftKeys * mColumnWidth + mLeftPadding;
232        }
233
234        public int getX(final int n, final int row) {
235            final int x = getColumnPos(n) * mColumnWidth + getDefaultKeyCoordX();
236            if (isTopRow(row)) {
237                return x + mTopRowAdjustment * (mColumnWidth / 2);
238            }
239            return x;
240        }
241
242        public int getY(final int row) {
243            return (mNumRows - 1 - row) * mDefaultRowHeight + mTopPadding;
244        }
245
246        public void markAsEdgeKey(final Key key, final int row) {
247            if (row == 0)
248                key.markAsTopEdge(this);
249            if (isTopRow(row))
250                key.markAsBottomEdge(this);
251        }
252
253        private boolean isTopRow(final int rowCount) {
254            return mNumRows > 1 && rowCount == mNumRows - 1;
255        }
256    }
257
258    public static class Builder extends KeyboardBuilder<MoreKeysKeyboardParams> {
259        private final Key mParentKey;
260
261        private static final float LABEL_PADDING_RATIO = 0.2f;
262        private static final float DIVIDER_RATIO = 0.2f;
263
264        /**
265         * The builder of MoreKeysKeyboard.
266         * @param context the context of {@link MoreKeysKeyboardView}.
267         * @param key the {@link Key} that invokes more keys keyboard.
268         * @param keyboard the {@link Keyboard} that contains the parentKey.
269         * @param isSingleMoreKeyWithPreview true if the <code>key</code> has just a single
270         *        "more key" and its key popup preview is enabled.
271         * @param keyPreviewVisibleWidth the width of visible part of key popup preview.
272         * @param keyPreviewVisibleHeight the height of visible part of key popup preview
273         * @param paintToMeasure the {@link Paint} object to measure a "more key" width
274         */
275        public Builder(final Context context, final Key key, final Keyboard keyboard,
276                final boolean isSingleMoreKeyWithPreview, final int keyPreviewVisibleWidth,
277                final int keyPreviewVisibleHeight, final Paint paintToMeasure) {
278            super(context, new MoreKeysKeyboardParams());
279            load(keyboard.mMoreKeysTemplate, keyboard.mId);
280
281            // TODO: More keys keyboard's vertical gap is currently calculated heuristically.
282            // Should revise the algorithm.
283            mParams.mVerticalGap = keyboard.mVerticalGap / 2;
284            // This {@link MoreKeysKeyboard} is invoked from the <code>key</code>.
285            mParentKey = key;
286
287            final int keyWidth, rowHeight;
288            if (isSingleMoreKeyWithPreview) {
289                // Use pre-computed width and height if this more keys keyboard has only one key to
290                // mitigate visual flicker between key preview and more keys keyboard.
291                // Caveats for the visual assets: To achieve this effect, both the key preview
292                // backgrounds and the more keys keyboard panel background have the exact same
293                // left/right/top paddings. The bottom paddings of both backgrounds don't need to
294                // be considered because the vertical positions of both backgrounds were already
295                // adjusted with their bottom paddings deducted.
296                keyWidth = keyPreviewVisibleWidth;
297                rowHeight = keyPreviewVisibleHeight + mParams.mVerticalGap;
298            } else {
299                final float padding = context.getResources().getDimension(
300                        R.dimen.config_more_keys_keyboard_key_horizontal_padding)
301                        + (key.hasLabelsInMoreKeys()
302                                ? mParams.mDefaultKeyWidth * LABEL_PADDING_RATIO : 0.0f);
303                keyWidth = getMaxKeyWidth(key, mParams.mDefaultKeyWidth, padding, paintToMeasure);
304                rowHeight = keyboard.mMostCommonKeyHeight;
305            }
306            final int dividerWidth;
307            if (key.needsDividersInMoreKeys()) {
308                dividerWidth = (int)(keyWidth * DIVIDER_RATIO);
309            } else {
310                dividerWidth = 0;
311            }
312            final MoreKeySpec[] moreKeys = key.getMoreKeys();
313            mParams.setParameters(moreKeys.length, key.getMoreKeysColumnNumber(), keyWidth,
314                    rowHeight, key.getX() + key.getWidth() / 2, keyboard.mId.mWidth,
315                    key.isMoreKeysFixedColumn(), key.isMoreKeysFixedOrder(), dividerWidth);
316        }
317
318        private static int getMaxKeyWidth(final Key parentKey, final int minKeyWidth,
319                final float padding, final Paint paint) {
320            int maxWidth = minKeyWidth;
321            for (final MoreKeySpec spec : parentKey.getMoreKeys()) {
322                final String label = spec.mLabel;
323                // If the label is single letter, minKeyWidth is enough to hold the label.
324                if (label != null && StringUtils.codePointCount(label) > 1) {
325                    maxWidth = Math.max(maxWidth,
326                            (int)(TypefaceUtils.getStringWidth(label, paint) + padding));
327                }
328            }
329            return maxWidth;
330        }
331
332        @Override
333        @Nonnull
334        public MoreKeysKeyboard build() {
335            final MoreKeysKeyboardParams params = mParams;
336            final int moreKeyFlags = mParentKey.getMoreKeyLabelFlags();
337            final MoreKeySpec[] moreKeys = mParentKey.getMoreKeys();
338            for (int n = 0; n < moreKeys.length; n++) {
339                final MoreKeySpec moreKeySpec = moreKeys[n];
340                final int row = n / params.mNumColumns;
341                final int x = params.getX(n, row);
342                final int y = params.getY(row);
343                final Key key = moreKeySpec.buildKey(x, y, moreKeyFlags, params);
344                params.markAsEdgeKey(key, row);
345                params.onAddKey(key);
346
347                final int pos = params.getColumnPos(n);
348                // The "pos" value represents the offset from the default position. Negative means
349                // left of the default position.
350                if (params.mDividerWidth > 0 && pos != 0) {
351                    final int dividerX = (pos > 0) ? x - params.mDividerWidth
352                            : x + params.mDefaultKeyWidth;
353                    final Key divider = new MoreKeyDivider(
354                            params, dividerX, y, params.mDividerWidth, params.mDefaultRowHeight);
355                    params.onAddKey(divider);
356                }
357            }
358            return new MoreKeysKeyboard(params);
359        }
360    }
361
362    // Used as a divider maker. A divider is drawn by {@link MoreKeysKeyboardView}.
363    public static class MoreKeyDivider extends Key.Spacer {
364        public MoreKeyDivider(final KeyboardParams params, final int x, final int y,
365                final int width, final int height) {
366            super(params, x, y, width, height);
367        }
368    }
369}
370