1/*
2 * Copyright (C) 2010 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.calculator2;
18
19import android.content.ClipData;
20import android.content.ClipboardManager;
21import android.content.Context;
22import android.content.res.Resources;
23import android.text.Editable;
24import android.text.InputType;
25import android.text.TextUtils;
26import android.util.AttributeSet;
27import android.util.Log;
28import android.view.ActionMode;
29import android.view.ContextMenu;
30import android.view.Menu;
31import android.view.MenuItem;
32import android.view.MotionEvent;
33import android.view.accessibility.AccessibilityEvent;
34import android.view.accessibility.AccessibilityNodeInfo;
35import android.widget.EditText;
36import android.widget.Toast;
37
38import com.google.common.collect.ImmutableMap;
39
40public class CalculatorEditText extends EditText {
41
42    private static final String LOG_TAG = "Calculator2";
43    private static final int CUT = 0;
44    private static final int COPY = 1;
45    private static final int PASTE = 2;
46    private String[] mMenuItemsStrings;
47    private ImmutableMap<String, String> sReplacementTable;
48    private String[] sOperators;
49
50    public CalculatorEditText(Context context, AttributeSet attrs) {
51        super(context, attrs);
52        setCustomSelectionActionModeCallback(new NoTextSelectionMode());
53        setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
54    }
55
56    @Override
57    public boolean onTouchEvent(MotionEvent event) {
58       if (event.getActionMasked() == MotionEvent.ACTION_UP) {
59            // Hack to prevent keyboard and insertion handle from showing.
60           cancelLongPress();
61        }
62        return super.onTouchEvent(event);
63    }
64
65    @Override
66    public boolean performLongClick() {
67        showContextMenu();
68        return true;
69    }
70
71    @Override
72    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
73        super.onInitializeAccessibilityEvent(event);
74        String mathText = mathParse(getText().toString());
75        // Parse the string into something more "mathematical" sounding.
76        if (!TextUtils.isEmpty(mathText)) {
77            event.getText().clear();
78            event.getText().add(mathText);
79            setContentDescription(mathText);
80        }
81    }
82
83    @Override
84    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
85        super.onInitializeAccessibilityNodeInfo(info);
86        info.setText(mathParse(getText().toString()));
87    }
88
89    @Override
90    public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
91        // Do nothing.
92    }
93
94    private String mathParse(String plainText) {
95        String parsedText = plainText;
96        if (!TextUtils.isEmpty(parsedText)) {
97            // Initialize replacement table.
98            initializeReplacementTable();
99            for (String operator : sOperators) {
100                if (sReplacementTable.containsKey(operator)) {
101                    parsedText = parsedText.replace(operator, sReplacementTable.get(operator));
102                }
103            }
104        }
105        return parsedText;
106    }
107
108    private synchronized void initializeReplacementTable() {
109        if (sReplacementTable == null) {
110            ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
111            Resources res = getContext().getResources();
112            sOperators = res.getStringArray(R.array.operators);
113            String[] descs = res.getStringArray(R.array.operatorDescs);
114            int pos = 0;
115            for (String key : sOperators) {
116                builder.put(key, descs[pos]);
117                pos++;
118            }
119            sReplacementTable = builder.build();
120        }
121    }
122
123    private class MenuHandler implements MenuItem.OnMenuItemClickListener {
124        public boolean onMenuItemClick(MenuItem item) {
125            return onTextContextMenuItem(item.getTitle());
126        }
127    }
128
129    public boolean onTextContextMenuItem(CharSequence title) {
130        boolean handled = false;
131        if (TextUtils.equals(title, mMenuItemsStrings[CUT])) {
132            cutContent();
133            handled = true;
134        } else if (TextUtils.equals(title,  mMenuItemsStrings[COPY])) {
135            copyContent();
136            handled = true;
137        } else if (TextUtils.equals(title,  mMenuItemsStrings[PASTE])) {
138            pasteContent();
139            handled = true;
140        }
141        return handled;
142    }
143
144    @Override
145    public void onCreateContextMenu(ContextMenu menu) {
146        MenuHandler handler = new MenuHandler();
147        if (mMenuItemsStrings == null) {
148            Resources resources = getResources();
149            mMenuItemsStrings = new String[3];
150            mMenuItemsStrings[CUT] = resources.getString(android.R.string.cut);
151            mMenuItemsStrings[COPY] = resources.getString(android.R.string.copy);
152            mMenuItemsStrings[PASTE] = resources.getString(android.R.string.paste);
153        }
154        for (int i = 0; i < mMenuItemsStrings.length; i++) {
155            menu.add(Menu.NONE, i, i, mMenuItemsStrings[i]).setOnMenuItemClickListener(handler);
156        }
157        if (getText().length() == 0) {
158            menu.getItem(CUT).setVisible(false);
159            menu.getItem(COPY).setVisible(false);
160        }
161        ClipData primaryClip = getPrimaryClip();
162        if (primaryClip == null || primaryClip.getItemCount() == 0
163                || !canPaste(primaryClip.getItemAt(0).coerceToText(getContext()))) {
164            menu.getItem(PASTE).setVisible(false);
165        }
166    }
167
168    private void setPrimaryClip(ClipData clip) {
169        ClipboardManager clipboard = (ClipboardManager) getContext().
170                getSystemService(Context.CLIPBOARD_SERVICE);
171        clipboard.setPrimaryClip(clip);
172    }
173
174    private void copyContent() {
175        final Editable text = getText();
176        int textLength = text.length();
177        setSelection(0, textLength);
178        ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(
179                Context.CLIPBOARD_SERVICE);
180        clipboard.setPrimaryClip(ClipData.newPlainText(null, text));
181        Toast.makeText(getContext(), R.string.text_copied_toast, Toast.LENGTH_SHORT).show();
182        setSelection(textLength);
183    }
184
185    private void cutContent() {
186        final Editable text = getText();
187        int textLength = text.length();
188        setSelection(0, textLength);
189        setPrimaryClip(ClipData.newPlainText(null, text));
190        ((Editable) getText()).delete(0, textLength);
191        setSelection(0);
192    }
193
194    private ClipData getPrimaryClip() {
195        ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(
196                Context.CLIPBOARD_SERVICE);
197        return clipboard.getPrimaryClip();
198    }
199
200    private void pasteContent() {
201        ClipData clip = getPrimaryClip();
202        if (clip != null) {
203            for (int i = 0; i < clip.getItemCount(); i++) {
204                CharSequence paste = clip.getItemAt(i).coerceToText(getContext());
205                if (canPaste(paste)) {
206                    ((Editable) getText()).insert(getSelectionEnd(), paste);
207                }
208            }
209        }
210    }
211
212    private boolean canPaste(CharSequence paste) {
213        boolean canPaste = true;
214        try {
215            Float.parseFloat(paste.toString());
216        } catch (NumberFormatException e) {
217            Log.e(LOG_TAG, "Error turning string to integer. Ignoring paste.", e);
218            canPaste = false;
219        }
220        return canPaste;
221    }
222
223    class NoTextSelectionMode implements ActionMode.Callback {
224        @Override
225        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
226            return false;
227        }
228
229        @Override
230        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
231            copyContent();
232            // Prevents the selection action mode on double tap.
233            return false;
234        }
235
236        @Override
237        public void onDestroyActionMode(ActionMode mode) {}
238
239        @Override
240        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
241            return false;
242        }
243    }
244}
245