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;
21import android.graphics.drawable.Drawable;
22
23import com.android.inputmethod.annotations.UsedForTesting;
24import com.android.inputmethod.keyboard.internal.KeyPreviewDrawParams;
25import com.android.inputmethod.keyboard.internal.KeyboardBuilder;
26import com.android.inputmethod.keyboard.internal.KeyboardIconsSet;
27import com.android.inputmethod.keyboard.internal.KeyboardParams;
28import com.android.inputmethod.keyboard.internal.MoreKeySpec;
29import com.android.inputmethod.latin.R;
30import com.android.inputmethod.latin.utils.StringUtils;
31import com.android.inputmethod.latin.utils.TypefaceUtils;
32
33public final class MoreKeysKeyboard extends Keyboard {
34    private final int mDefaultKeyCoordX;
35
36    MoreKeysKeyboard(final MoreKeysKeyboardParams params) {
37        super(params);
38        mDefaultKeyCoordX = params.getDefaultKeyCoordX() + params.mDefaultKeyWidth / 2;
39    }
40
41    public int getDefaultCoordX() {
42        return mDefaultKeyCoordX;
43    }
44
45    @UsedForTesting
46    static class MoreKeysKeyboardParams extends KeyboardParams {
47        public boolean mIsFixedOrder;
48        /* package */int mTopRowAdjustment;
49        public int mNumRows;
50        public int mNumColumns;
51        public int mTopKeys;
52        public int mLeftKeys;
53        public int mRightKeys; // includes default key.
54        public int mDividerWidth;
55        public int mColumnWidth;
56
57        public MoreKeysKeyboardParams() {
58            super();
59        }
60
61        /**
62         * Set keyboard parameters of more keys keyboard.
63         *
64         * @param numKeys number of keys in this more keys keyboard.
65         * @param maxColumns number of maximum columns of this more keys keyboard.
66         * @param keyWidth more keys keyboard key width in pixel, including horizontal gap.
67         * @param rowHeight more keys keyboard row height in pixel, including vertical gap.
68         * @param coordXInParent coordinate x of the key preview in parent keyboard.
69         * @param parentKeyboardWidth parent keyboard width in pixel.
70         * @param isFixedColumnOrder if true, more keys should be laid out in fixed order.
71         * @param dividerWidth width of divider, zero for no dividers.
72         */
73        public void setParameters(final int numKeys, final int maxColumns, final int keyWidth,
74                final int rowHeight, final int coordXInParent, final int parentKeyboardWidth,
75                final boolean isFixedColumnOrder, final int dividerWidth) {
76            mIsFixedOrder = isFixedColumnOrder;
77            if (parentKeyboardWidth / keyWidth < Math.min(numKeys, maxColumns)) {
78                throw new IllegalArgumentException("Keyboard is too small to hold more keys: "
79                        + parentKeyboardWidth + " " + keyWidth + " " + numKeys + " " + maxColumns);
80            }
81            mDefaultKeyWidth = keyWidth;
82            mDefaultRowHeight = rowHeight;
83
84            final int numRows = (numKeys + maxColumns - 1) / maxColumns;
85            mNumRows = numRows;
86            final int numColumns = mIsFixedOrder ? Math.min(numKeys, maxColumns)
87                    : getOptimizedColumns(numKeys, maxColumns);
88            mNumColumns = numColumns;
89            final int topKeys = numKeys % numColumns;
90            mTopKeys = topKeys == 0 ? numColumns : topKeys;
91
92            final int numLeftKeys = (numColumns - 1) / 2;
93            final int numRightKeys = numColumns - numLeftKeys; // including default key.
94            // Maximum number of keys we can layout both side of the parent key
95            final int maxLeftKeys = coordXInParent / keyWidth;
96            final int maxRightKeys = (parentKeyboardWidth - coordXInParent) / keyWidth;
97            int leftKeys, rightKeys;
98            if (numLeftKeys > maxLeftKeys) {
99                leftKeys = maxLeftKeys;
100                rightKeys = numColumns - leftKeys;
101            } else if (numRightKeys > maxRightKeys + 1) {
102                rightKeys = maxRightKeys + 1; // include default key
103                leftKeys = numColumns - rightKeys;
104            } else {
105                leftKeys = numLeftKeys;
106                rightKeys = numRightKeys;
107            }
108            // If the left keys fill the left side of the parent key, entire more keys keyboard
109            // should be shifted to the right unless the parent key is on the left edge.
110            if (maxLeftKeys == leftKeys && leftKeys > 0) {
111                leftKeys--;
112                rightKeys++;
113            }
114            // If the right keys fill the right side of the parent key, entire more keys
115            // should be shifted to the left unless the parent key is on the right edge.
116            if (maxRightKeys == rightKeys - 1 && rightKeys > 1) {
117                leftKeys++;
118                rightKeys--;
119            }
120            mLeftKeys = leftKeys;
121            mRightKeys = rightKeys;
122
123            // Adjustment of the top row.
124            mTopRowAdjustment = mIsFixedOrder ? getFixedOrderTopRowAdjustment()
125                    : getAutoOrderTopRowAdjustment();
126            mDividerWidth = dividerWidth;
127            mColumnWidth = mDefaultKeyWidth + mDividerWidth;
128            mBaseWidth = mOccupiedWidth = mNumColumns * mColumnWidth - mDividerWidth;
129            // Need to subtract the bottom row's gutter only.
130            mBaseHeight = mOccupiedHeight = mNumRows * mDefaultRowHeight - mVerticalGap
131                    + mTopPadding + mBottomPadding;
132        }
133
134        private int getFixedOrderTopRowAdjustment() {
135            if (mNumRows == 1 || mTopKeys % 2 == 1 || mTopKeys == mNumColumns
136                    || mLeftKeys == 0  || mRightKeys == 1) {
137                return 0;
138            }
139            return -1;
140        }
141
142        private int getAutoOrderTopRowAdjustment() {
143            if (mNumRows == 1 || mTopKeys == 1 || mNumColumns % 2 == mTopKeys % 2
144                    || mLeftKeys == 0 || mRightKeys == 1) {
145                return 0;
146            }
147            return -1;
148        }
149
150        // Return key position according to column count (0 is default).
151        /* package */int getColumnPos(final int n) {
152            return mIsFixedOrder ? getFixedOrderColumnPos(n) : getAutomaticColumnPos(n);
153        }
154
155        private int getFixedOrderColumnPos(final int n) {
156            final int col = n % mNumColumns;
157            final int row = n / mNumColumns;
158            if (!isTopRow(row)) {
159                return col - mLeftKeys;
160            }
161            final int rightSideKeys = mTopKeys / 2;
162            final int leftSideKeys = mTopKeys - (rightSideKeys + 1);
163            final int pos = col - leftSideKeys;
164            final int numLeftKeys = mLeftKeys + mTopRowAdjustment;
165            final int numRightKeys = mRightKeys - 1;
166            if (numRightKeys >= rightSideKeys && numLeftKeys >= leftSideKeys) {
167                return pos;
168            } else if (numRightKeys < rightSideKeys) {
169                return pos - (rightSideKeys - numRightKeys);
170            } else { // numLeftKeys < leftSideKeys
171                return pos + (leftSideKeys - numLeftKeys);
172            }
173        }
174
175        private int getAutomaticColumnPos(final int n) {
176            final int col = n % mNumColumns;
177            final int row = n / mNumColumns;
178            int leftKeys = mLeftKeys;
179            if (isTopRow(row)) {
180                leftKeys += mTopRowAdjustment;
181            }
182            if (col == 0) {
183                // default position.
184                return 0;
185            }
186
187            int pos = 0;
188            int right = 1; // include default position key.
189            int left = 0;
190            int i = 0;
191            while (true) {
192                // Assign right key if available.
193                if (right < mRightKeys) {
194                    pos = right;
195                    right++;
196                    i++;
197                }
198                if (i >= col)
199                    break;
200                // Assign left key if available.
201                if (left < leftKeys) {
202                    left++;
203                    pos = -left;
204                    i++;
205                }
206                if (i >= col)
207                    break;
208            }
209            return pos;
210        }
211
212        private static int getTopRowEmptySlots(final int numKeys, final int numColumns) {
213            final int remainings = numKeys % numColumns;
214            return remainings == 0 ? 0 : numColumns - remainings;
215        }
216
217        private int getOptimizedColumns(final int numKeys, final int maxColumns) {
218            int numColumns = Math.min(numKeys, maxColumns);
219            while (getTopRowEmptySlots(numKeys, numColumns) >= mNumRows) {
220                numColumns--;
221            }
222            return numColumns;
223        }
224
225        public int getDefaultKeyCoordX() {
226            return mLeftKeys * mColumnWidth;
227        }
228
229        public int getX(final int n, final int row) {
230            final int x = getColumnPos(n) * mColumnWidth + getDefaultKeyCoordX();
231            if (isTopRow(row)) {
232                return x + mTopRowAdjustment * (mColumnWidth / 2);
233            }
234            return x;
235        }
236
237        public int getY(final int row) {
238            return (mNumRows - 1 - row) * mDefaultRowHeight + mTopPadding;
239        }
240
241        public void markAsEdgeKey(final Key key, final int row) {
242            if (row == 0)
243                key.markAsTopEdge(this);
244            if (isTopRow(row))
245                key.markAsBottomEdge(this);
246        }
247
248        private boolean isTopRow(final int rowCount) {
249            return mNumRows > 1 && rowCount == mNumRows - 1;
250        }
251    }
252
253    public static class Builder extends KeyboardBuilder<MoreKeysKeyboardParams> {
254        private final Key mParentKey;
255        private final Drawable mDivider;
256
257        private static final float LABEL_PADDING_RATIO = 0.2f;
258        private static final float DIVIDER_RATIO = 0.2f;
259
260        /**
261         * The builder of MoreKeysKeyboard.
262         * @param context the context of {@link MoreKeysKeyboardView}.
263         * @param parentKey the {@link Key} that invokes more keys keyboard.
264         * @param parentKeyboardView the {@link KeyboardView} that contains the parentKey.
265         * @param keyPreviewDrawParams the parameter to place key preview.
266         */
267        public Builder(final Context context, final Key parentKey,
268                final MainKeyboardView parentKeyboardView,
269                final KeyPreviewDrawParams keyPreviewDrawParams) {
270            super(context, new MoreKeysKeyboardParams());
271            final Keyboard parentKeyboard = parentKeyboardView.getKeyboard();
272            load(parentKeyboard.mMoreKeysTemplate, parentKeyboard.mId);
273
274            // TODO: More keys keyboard's vertical gap is currently calculated heuristically.
275            // Should revise the algorithm.
276            mParams.mVerticalGap = parentKeyboard.mVerticalGap / 2;
277            mParentKey = parentKey;
278
279            final MoreKeySpec[] moreKeys = parentKey.getMoreKeys();
280            final int width, height;
281            // {@link KeyPreviewDrawParams#mPreviewVisibleWidth} should have been set at
282            // {@link MainKeyboardView#showKeyPreview(PointerTracker}, though there may be
283            // some chances that the value is zero. <code>width == 0</code> will cause
284            // zero-division error at
285            // {@link MoreKeysKeyboardParams#setParameters(int,int,int,int,int,int,boolean,int)}.
286            final boolean singleMoreKeyWithPreview = parentKeyboardView.isKeyPreviewPopupEnabled()
287                    && !parentKey.noKeyPreview() && moreKeys.length == 1
288                    && keyPreviewDrawParams.mPreviewVisibleWidth > 0;
289            if (singleMoreKeyWithPreview) {
290                // Use pre-computed width and height if this more keys keyboard has only one key to
291                // mitigate visual flicker between key preview and more keys keyboard.
292                // Caveats for the visual assets: To achieve this effect, both the key preview
293                // backgrounds and the more keys keyboard panel background have the exact same
294                // left/right/top paddings. The bottom paddings of both backgrounds don't need to
295                // be considered because the vertical positions of both backgrounds were already
296                // adjusted with their bottom paddings deducted.
297                width = keyPreviewDrawParams.mPreviewVisibleWidth;
298                height = keyPreviewDrawParams.mPreviewVisibleHeight + mParams.mVerticalGap;
299            } else {
300                final float padding = context.getResources().getDimension(
301                        R.dimen.more_keys_keyboard_key_horizontal_padding)
302                        + (parentKey.hasLabelsInMoreKeys()
303                                ? mParams.mDefaultKeyWidth * LABEL_PADDING_RATIO : 0.0f);
304                width = getMaxKeyWidth(parentKey, mParams.mDefaultKeyWidth, padding,
305                        parentKeyboardView.newLabelPaint(parentKey));
306                height = parentKeyboard.mMostCommonKeyHeight;
307            }
308            final int dividerWidth;
309            if (parentKey.needsDividersInMoreKeys()) {
310                mDivider = mResources.getDrawable(R.drawable.more_keys_divider);
311                dividerWidth = (int)(width * DIVIDER_RATIO);
312            } else {
313                mDivider = null;
314                dividerWidth = 0;
315            }
316            mParams.setParameters(moreKeys.length, parentKey.getMoreKeysColumn(),
317                    width, height, parentKey.getX() + parentKey.getWidth() / 2,
318                    parentKeyboard.mId.mWidth, parentKey.isFixedColumnOrderMoreKeys(),
319                    dividerWidth);
320        }
321
322        private static int getMaxKeyWidth(final Key parentKey, final int minKeyWidth,
323                final float padding, final Paint paint) {
324            int maxWidth = minKeyWidth;
325            for (final MoreKeySpec spec : parentKey.getMoreKeys()) {
326                final String label = spec.mLabel;
327                // If the label is single letter, minKeyWidth is enough to hold the label.
328                if (label != null && StringUtils.codePointCount(label) > 1) {
329                    maxWidth = Math.max(maxWidth,
330                            (int)(TypefaceUtils.getLabelWidth(label, paint) + padding));
331                }
332            }
333            return maxWidth;
334        }
335
336        @Override
337        public MoreKeysKeyboard build() {
338            final MoreKeysKeyboardParams params = mParams;
339            final int moreKeyFlags = mParentKey.getMoreKeyLabelFlags();
340            final MoreKeySpec[] moreKeys = mParentKey.getMoreKeys();
341            for (int n = 0; n < moreKeys.length; n++) {
342                final MoreKeySpec moreKeySpec = moreKeys[n];
343                final int row = n / params.mNumColumns;
344                final int x = params.getX(n, row);
345                final int y = params.getY(row);
346                final Key key = new Key(params, moreKeySpec, x, y,
347                        params.mDefaultKeyWidth, params.mDefaultRowHeight, moreKeyFlags);
348                params.markAsEdgeKey(key, row);
349                params.onAddKey(key);
350
351                final int pos = params.getColumnPos(n);
352                // The "pos" value represents the offset from the default position. Negative means
353                // left of the default position.
354                if (params.mDividerWidth > 0 && pos != 0) {
355                    final int dividerX = (pos > 0) ? x - params.mDividerWidth
356                            : x + params.mDefaultKeyWidth;
357                    final Key divider = new MoreKeyDivider(params, mDivider, dividerX, y);
358                    params.onAddKey(divider);
359                }
360            }
361            return new MoreKeysKeyboard(params);
362        }
363    }
364
365    private static class MoreKeyDivider extends Key.Spacer {
366        private final Drawable mIcon;
367
368        public MoreKeyDivider(final MoreKeysKeyboardParams params, final Drawable icon,
369                final int x, final int y) {
370            super(params, x, y, params.mDividerWidth, params.mDefaultRowHeight);
371            mIcon = icon;
372        }
373
374        @Override
375        public Drawable getIcon(final KeyboardIconsSet iconSet, final int alpha) {
376            // KeyboardIconsSet and alpha are unused. Use the icon that has been passed to the
377            // constructor.
378            // TODO: Drawable itself should have an alpha value.
379            mIcon.setAlpha(128);
380            return mIcon;
381        }
382    }
383}
384