]> gitweb.fperrin.net Git - Dictionary.git/blob - src/com/hughes/android/dictionary/DictionaryActivity.java
About dialog, added pictures, multi word search.
[Dictionary.git] / src / com / hughes / android / dictionary / DictionaryActivity.java
1 // Copyright 2011 Google Inc. All Rights Reserved.\r
2 //\r
3 // Licensed under the Apache License, Version 2.0 (the "License");\r
4 // you may not use this file except in compliance with the License.\r
5 // You may obtain a copy of the License at\r
6 //\r
7 //     http://www.apache.org/licenses/LICENSE-2.0\r
8 //\r
9 // Unless required by applicable law or agreed to in writing, software\r
10 // distributed under the License is distributed on an "AS IS" BASIS,\r
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
12 // See the License for the specific language governing permissions and\r
13 // limitations under the License.\r
14 \r
15 package com.hughes.android.dictionary;\r
16 \r
17 import java.io.File;\r
18 import java.io.FileWriter;\r
19 import java.io.IOException;\r
20 import java.io.PrintWriter;\r
21 import java.io.RandomAccessFile;\r
22 import java.text.SimpleDateFormat;\r
23 import java.util.Arrays;\r
24 import java.util.Collections;\r
25 import java.util.Date;\r
26 import java.util.LinkedHashSet;\r
27 import java.util.List;\r
28 import java.util.Set;\r
29 import java.util.concurrent.Executor;\r
30 import java.util.concurrent.Executors;\r
31 import java.util.concurrent.ThreadFactory;\r
32 import java.util.concurrent.atomic.AtomicBoolean;\r
33 import java.util.regex.Matcher;\r
34 import java.util.regex.Pattern;\r
35 \r
36 import android.app.Dialog;\r
37 import android.app.ListActivity;\r
38 import android.content.Context;\r
39 import android.content.Intent;\r
40 import android.content.SharedPreferences;\r
41 import android.graphics.Typeface;\r
42 import android.os.Bundle;\r
43 import android.os.Handler;\r
44 import android.preference.PreferenceManager;\r
45 import android.text.ClipboardManager;\r
46 import android.text.Editable;\r
47 import android.text.Selection;\r
48 import android.text.Spannable;\r
49 import android.text.TextWatcher;\r
50 import android.text.method.LinkMovementMethod;\r
51 import android.text.style.StyleSpan;\r
52 import android.util.Log;\r
53 import android.util.TypedValue;\r
54 import android.view.ContextMenu;\r
55 import android.view.ContextMenu.ContextMenuInfo;\r
56 import android.view.KeyEvent;\r
57 import android.view.Menu;\r
58 import android.view.MenuItem;\r
59 import android.view.WindowManager;\r
60 import android.view.MenuItem.OnMenuItemClickListener;\r
61 import android.view.MotionEvent;\r
62 import android.view.View;\r
63 import android.view.View.OnClickListener;\r
64 import android.view.View.OnLongClickListener;\r
65 import android.view.ViewGroup;\r
66 import android.view.inputmethod.InputMethodManager;\r
67 import android.widget.AdapterView;\r
68 import android.widget.AdapterView.AdapterContextMenuInfo;\r
69 import android.widget.BaseAdapter;\r
70 import android.widget.Button;\r
71 import android.widget.EditText;\r
72 import android.widget.LinearLayout;\r
73 import android.widget.ListAdapter;\r
74 import android.widget.ListView;\r
75 import android.widget.TableLayout;\r
76 import android.widget.TableRow;\r
77 import android.widget.TextView;\r
78 import android.widget.Toast;\r
79 \r
80 import com.hughes.android.dictionary.DictionaryInfo.IndexInfo;\r
81 import com.hughes.android.dictionary.engine.Dictionary;\r
82 import com.hughes.android.dictionary.engine.Index;\r
83 import com.hughes.android.dictionary.engine.PairEntry;\r
84 import com.hughes.android.dictionary.engine.PairEntry.Pair;\r
85 import com.hughes.android.dictionary.engine.RowBase;\r
86 import com.hughes.android.dictionary.engine.TokenRow;\r
87 import com.hughes.android.dictionary.engine.TransliteratorManager;\r
88 import com.hughes.android.util.IntentLauncher;\r
89 import com.hughes.android.util.NonLinkClickableSpan;\r
90 \r
91 public class DictionaryActivity extends ListActivity {\r
92 \r
93   static final String LOG = "QuickDic";\r
94   \r
95   DictionaryApplication application;\r
96   File dictFile = null;\r
97   RandomAccessFile dictRaf = null;\r
98   Dictionary dictionary = null;\r
99   int indexIndex = 0;\r
100   Index index = null;\r
101   List<RowBase> rowsToShow = null;  // if not null, just show these rows.\r
102   \r
103   // package for test.\r
104   final Handler uiHandler = new Handler();\r
105   private final Executor searchExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() {\r
106     @Override\r
107     public Thread newThread(Runnable r) {\r
108       return new Thread(r, "searchExecutor");\r
109     }\r
110   });\r
111   private SearchOperation currentSearchOperation = null;\r
112 \r
113   C.Theme theme = C.Theme.LIGHT;\r
114   int fontSizeSp;\r
115   EditText searchText;\r
116   Button langButton;\r
117 \r
118   // Never null.\r
119   private File wordList = null;\r
120   private boolean saveOnlyFirstSubentry = false;\r
121   private boolean clickOpensContextMenu = false;\r
122 \r
123   // Visible for testing.\r
124   ListAdapter indexAdapter = null;\r
125   \r
126   final SearchTextWatcher searchTextWatcher = new SearchTextWatcher();\r
127 \r
128   public DictionaryActivity() {\r
129   }\r
130   \r
131   public static Intent getLaunchIntent(final File dictFile, final int indexIndex, final String searchToken) {\r
132     final Intent intent = new Intent();\r
133     intent.setClassName(DictionaryActivity.class.getPackage().getName(), DictionaryActivity.class.getName());\r
134     intent.putExtra(C.DICT_FILE, dictFile.getPath());\r
135     intent.putExtra(C.INDEX_INDEX, indexIndex);\r
136     intent.putExtra(C.SEARCH_TOKEN, searchToken);\r
137     return intent;\r
138   }\r
139   \r
140   @Override\r
141   protected void onSaveInstanceState(final Bundle outState) {\r
142     super.onSaveInstanceState(outState);\r
143     outState.putString(C.SEARCH_TOKEN, searchText.getText().toString());\r
144   }\r
145 \r
146   @Override\r
147   protected void onRestoreInstanceState(final Bundle outState) {\r
148     super.onRestoreInstanceState(outState);\r
149     setSearchText(outState.getString(C.SEARCH_TOKEN));\r
150   }\r
151 \r
152   @Override\r
153   public void onCreate(Bundle savedInstanceState) {    \r
154     Log.d(LOG, "onCreate:" + this);\r
155     super.onCreate(savedInstanceState);\r
156 \r
157     application = (DictionaryApplication) getApplication();\r
158     theme = application.getSelectedTheme();\r
159 \r
160     // Clear them so that if something goes wrong, we won't relaunch.\r
161     clearDictionaryPrefs(this);\r
162     \r
163     final Intent intent = getIntent();\r
164     dictFile = new File(intent.getStringExtra(C.DICT_FILE));\r
165     \r
166     try {\r
167       final String name = application.getDictionaryName(dictFile.getName());\r
168       this.setTitle("QuickDic: " + name);\r
169       dictRaf = new RandomAccessFile(dictFile, "r");\r
170       dictionary = new Dictionary(dictRaf); \r
171     } catch (Exception e) {\r
172       Log.e(LOG, "Unable to load dictionary.", e);\r
173       if (dictRaf != null) {\r
174         try {\r
175           dictRaf.close();\r
176         } catch (IOException e1) {\r
177           Log.e(LOG, "Unable to close dictRaf.", e1);\r
178         }\r
179         dictRaf = null;\r
180       }\r
181       Toast.makeText(this, getString(R.string.invalidDictionary, "", e.getMessage()), Toast.LENGTH_LONG);\r
182       startActivity(DictionaryManagerActivity.getLaunchIntent());\r
183       finish();\r
184       return;\r
185     }\r
186 \r
187     indexIndex = intent.getIntExtra(C.INDEX_INDEX, 0) % dictionary.indices.size();\r
188     Log.d(LOG, "Loading index " + indexIndex);\r
189     index = dictionary.indices.get(indexIndex);\r
190     setListAdapter(new IndexAdapter(index));\r
191     \r
192     // Pre-load the collators.\r
193     searchExecutor.execute(new Runnable() {\r
194       public void run() {\r
195         final long startMillis = System.currentTimeMillis();\r
196         \r
197         TransliteratorManager.init(new TransliteratorManager.Callback() {\r
198           @Override\r
199           public void onTransliteratorReady() {\r
200             uiHandler.post(new Runnable() {\r
201               @Override\r
202               public void run() {\r
203                 onSearchTextChange(searchText.getText().toString());\r
204               }\r
205             });\r
206           }\r
207         });\r
208         \r
209         for (final Index index : dictionary.indices) {\r
210           Log.d(LOG, "Starting collator load for lang=" + index.sortLanguage.getIsoCode());\r
211           \r
212           final com.ibm.icu.text.Collator c = index.sortLanguage.getCollator();          \r
213           if (c.compare("pre-print", "preppy") >= 0) {\r
214             Log.e(LOG, c.getClass()\r
215                 + " is buggy, lookups may not work properly.");\r
216           }\r
217         }\r
218         Log.d(LOG, "Loading collators took:"\r
219             + (System.currentTimeMillis() - startMillis));\r
220       }\r
221     });\r
222     \r
223     final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);\r
224     \r
225     final String fontSize = prefs.getString(getString(R.string.fontSizeKey), "14");\r
226     try {\r
227       fontSizeSp = Integer.parseInt(fontSize.trim());\r
228     } catch (NumberFormatException e) {\r
229       fontSizeSp = 12;\r
230     }\r
231 \r
232     setContentView(R.layout.dictionary_activity);\r
233     searchText = (EditText) findViewById(R.id.SearchText);\r
234     searchText.setTextSize(TypedValue.COMPLEX_UNIT_SP, fontSizeSp);\r
235     \r
236     langButton = (Button) findViewById(R.id.LangButton);\r
237     \r
238     searchText.requestFocus();\r
239     searchText.addTextChangedListener(searchTextWatcher);\r
240     final String search = prefs.getString(C.SEARCH_TOKEN, "");\r
241     searchText.setText(search);\r
242     searchText.setSelection(0, search.length());\r
243     Log.d(LOG, "Trying to restore searchText=" + search);\r
244     \r
245     final Button clearSearchTextButton = (Button) findViewById(R.id.ClearSearchTextButton);\r
246     clearSearchTextButton.setOnClickListener(new OnClickListener() {\r
247       public void onClick(View v) {\r
248         onClearSearchTextButton(clearSearchTextButton);\r
249       }\r
250     });\r
251     clearSearchTextButton.setVisibility(PreferenceManager.getDefaultSharedPreferences(this).getBoolean(\r
252         getString(R.string.showClearSearchTextButtonKey), true) ? View.VISIBLE\r
253         : View.GONE);\r
254     \r
255     final Button langButton = (Button) findViewById(R.id.LangButton);\r
256     langButton.setOnClickListener(new OnClickListener() {\r
257       public void onClick(View v) {\r
258         onLanguageButton();\r
259       }\r
260     });\r
261     langButton.setOnLongClickListener(new OnLongClickListener() {\r
262       @Override\r
263       public boolean onLongClick(View v) {\r
264         onLanguageButtonLongClick(v.getContext());\r
265         return true;\r
266       }\r
267     });\r
268     updateLangButton();\r
269     \r
270     final Button upButton = (Button) findViewById(R.id.UpButton);\r
271     upButton.setOnClickListener(new OnClickListener() {\r
272       public void onClick(View v) {\r
273         onUpDownButton(true);\r
274       }\r
275     });\r
276     final Button downButton = (Button) findViewById(R.id.DownButton);\r
277     downButton.setOnClickListener(new OnClickListener() {\r
278       public void onClick(View v) {\r
279         onUpDownButton(false);\r
280       }\r
281     });\r
282 \r
283    getListView().setOnItemSelectedListener(new ListView.OnItemSelectedListener() {\r
284       @Override\r
285       public void onItemSelected(AdapterView<?> adapterView, View arg1, final int position,\r
286           long id) {\r
287         if (!searchText.isFocused()) {\r
288           if (!isFiltered()) {\r
289             final RowBase row = (RowBase) getListAdapter().getItem(position);\r
290             Log.d(LOG, "onItemSelected: " + row.index());\r
291             final TokenRow tokenRow = row.getTokenRow(true);\r
292             searchText.setText(tokenRow.getToken());\r
293           }\r
294         }\r
295       }\r
296 \r
297       @Override\r
298       public void onNothingSelected(AdapterView<?> arg0) {\r
299       }\r
300     });\r
301 \r
302     // ContextMenu.\r
303     registerForContextMenu(getListView());\r
304 \r
305     // Prefs.\r
306     wordList = new File(prefs.getString(getString(R.string.wordListFileKey),\r
307         getString(R.string.wordListFileDefault)));\r
308     saveOnlyFirstSubentry = prefs.getBoolean(getString(R.string.saveOnlyFirstSubentryKey), false);\r
309     clickOpensContextMenu = prefs.getBoolean(getString(R.string.clickOpensContextMenuKey), false);\r
310     //if (prefs.getBoolean(getString(R.string.vibrateOnFailedSearchKey), true)) {\r
311       // vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);\r
312     //}\r
313     Log.d(LOG, "wordList=" + wordList + ", saveOnlyFirstSubentry=" + saveOnlyFirstSubentry);\r
314     \r
315     setDictionaryPrefs(this, dictFile, indexIndex, searchText.getText().toString());\r
316   }\r
317   \r
318   @Override\r
319   protected void onResume() {\r
320     super.onResume();\r
321     if (PreferenceActivity.prefsMightHaveChanged) {\r
322       PreferenceActivity.prefsMightHaveChanged = false;\r
323       finish();\r
324       startActivity(getIntent());\r
325     }\r
326   }\r
327   \r
328   @Override\r
329   protected void onPause() {\r
330     super.onPause();\r
331   }\r
332   \r
333   private static void setDictionaryPrefs(final Context context,\r
334       final File dictFile, final int indexIndex, final String searchToken) {\r
335     final SharedPreferences.Editor prefs = PreferenceManager.getDefaultSharedPreferences(context).edit();\r
336     prefs.putString(C.DICT_FILE, dictFile.getPath());\r
337     prefs.putInt(C.INDEX_INDEX, indexIndex);\r
338     prefs.putString(C.SEARCH_TOKEN, searchToken);\r
339     prefs.commit();\r
340   }\r
341 \r
342   private static void clearDictionaryPrefs(final Context context) {\r
343     final SharedPreferences.Editor prefs = PreferenceManager.getDefaultSharedPreferences(context).edit();\r
344     prefs.remove(C.DICT_FILE);\r
345     prefs.remove(C.INDEX_INDEX);\r
346     prefs.remove(C.SEARCH_TOKEN);\r
347     prefs.commit();\r
348   }\r
349 \r
350 \r
351   @Override\r
352   protected void onDestroy() {\r
353     super.onDestroy();\r
354     if (dictRaf == null) {\r
355       return;\r
356     }\r
357     \r
358     // Before we close the RAF, we have to wind the current search down.\r
359     if (currentSearchOperation != null) {\r
360       Log.d(LOG, "Interrupting search to shut down.");\r
361       final SearchOperation searchOperation = currentSearchOperation;\r
362       currentSearchOperation = null;\r
363       searchOperation.interrupted.set(true);\r
364       synchronized (searchOperation) {\r
365         while (!searchOperation.done) {\r
366           try {\r
367             searchOperation.wait();\r
368           } catch (InterruptedException e) {\r
369             Log.d(LOG, "Interrupted.", e);\r
370           }\r
371         }\r
372       }\r
373     }\r
374     \r
375     try {\r
376       Log.d(LOG, "Closing RAF.");\r
377       dictRaf.close();\r
378     } catch (IOException e) {\r
379       Log.e(LOG, "Failed to close dictionary", e);\r
380     }\r
381     dictRaf = null;\r
382   }\r
383 \r
384   // --------------------------------------------------------------------------\r
385   // Buttons\r
386   // --------------------------------------------------------------------------\r
387 \r
388   private void onClearSearchTextButton(final Button clearSearchTextButton) {\r
389     clearSearchTextButton.requestFocus();\r
390     searchText.setText("");\r
391     searchText.requestFocus();\r
392     Log.d(LOG, "Trying to show soft keyboard.");\r
393     final InputMethodManager manager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);\r
394     manager.showSoftInput(searchText, InputMethodManager.SHOW_IMPLICIT);\r
395   }\r
396   \r
397   void updateLangButton() {\r
398     langButton.setText(index.shortName);\r
399   }\r
400 \r
401   void onLanguageButton() {\r
402     if (currentSearchOperation != null) {\r
403       currentSearchOperation.interrupted.set(true);\r
404       currentSearchOperation = null;\r
405     }\r
406     changeIndex((indexIndex + 1)% dictionary.indices.size());\r
407   }\r
408   \r
409   void onLanguageButtonLongClick(final Context context) {\r
410     final Dialog dialog = new Dialog(context);\r
411     dialog.setContentView(R.layout.select_dictionary_dialog);\r
412     dialog.setTitle(R.string.selectDictionary);\r
413 \r
414     final List<DictionaryInfo> installedDicts = ((DictionaryApplication)getApplication()).getUsableDicts();\r
415     ListView listView = (ListView) dialog.findViewById(android.R.id.list);\r
416     listView.setAdapter(new BaseAdapter() {\r
417       @Override\r
418       public View getView(int position, View convertView, ViewGroup parent) {\r
419         final LinearLayout result = new LinearLayout(parent.getContext());\r
420         final DictionaryInfo dictionaryInfo = getItem(position);\r
421         for (int i = 0; i < dictionaryInfo.indexInfos.size(); ++i) {\r
422           final IndexInfo indexInfo = dictionaryInfo.indexInfos.get(i);\r
423           final Button button = new Button(parent.getContext());\r
424           String name = application.getLanguageName(indexInfo.shortName);\r
425           if (name == null) {\r
426             name = indexInfo.shortName;\r
427           }\r
428           button.setText(name);\r
429           final IntentLauncher intentLauncher = new IntentLauncher(parent.getContext(), getLaunchIntent(application.getPath(dictionaryInfo.uncompressedFilename), i, "")) {\r
430             @Override\r
431             protected void onGo() {\r
432               dialog.dismiss();\r
433               DictionaryActivity.this.finish();\r
434             };\r
435           };\r
436           button.setOnClickListener(intentLauncher);\r
437           \r
438           final LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);\r
439           layoutParams.width = 0;\r
440           layoutParams.weight = 1.0f;\r
441           button.setLayoutParams(layoutParams);\r
442 \r
443           result.addView(button);\r
444         }\r
445         return result;\r
446       }\r
447       \r
448       @Override\r
449       public long getItemId(int position) {\r
450         return position;\r
451       }\r
452       \r
453       @Override\r
454       public DictionaryInfo getItem(int position) {\r
455         return installedDicts.get(position);\r
456       }\r
457       \r
458       @Override\r
459       public int getCount() {\r
460         return installedDicts.size();\r
461       }\r
462     });\r
463     \r
464     dialog.show();\r
465   }\r
466 \r
467 \r
468   private void changeIndex(final int newIndex) {\r
469     indexIndex = newIndex;\r
470     index = dictionary.indices.get(indexIndex);\r
471     indexAdapter = new IndexAdapter(index);\r
472     Log.d(LOG, "changingIndex, newLang=" + index.longName);\r
473     setListAdapter(indexAdapter);\r
474     updateLangButton();\r
475     searchText.requestFocus();  // Otherwise, nothing may happen.\r
476     onSearchTextChange(searchText.getText().toString());\r
477     setDictionaryPrefs(this, dictFile, indexIndex, searchText.getText().toString());\r
478   }\r
479   \r
480   void onUpDownButton(final boolean up) {\r
481     if (isFiltered()) {\r
482       return;\r
483     }\r
484     final int firstVisibleRow = getListView().getFirstVisiblePosition();\r
485     final RowBase row = index.rows.get(firstVisibleRow);\r
486     final TokenRow tokenRow = row.getTokenRow(true);\r
487     final int destIndexEntry;\r
488     if (up) {\r
489       if (row != tokenRow) {\r
490         destIndexEntry = tokenRow.referenceIndex;\r
491       } else {\r
492         destIndexEntry = Math.max(tokenRow.referenceIndex - 1, 0);\r
493       }\r
494     } else {\r
495       // Down\r
496       destIndexEntry = Math.min(tokenRow.referenceIndex + 1, index.sortedIndexEntries.size());\r
497     }\r
498     final Index.IndexEntry dest = index.sortedIndexEntries.get(destIndexEntry);\r
499     Log.d(LOG, "onUpDownButton, destIndexEntry=" + dest.token);\r
500     searchText.removeTextChangedListener(searchTextWatcher);\r
501     searchText.setText(dest.token);\r
502     Selection.moveToRightEdge(searchText.getText(), searchText.getLayout());\r
503     jumpToRow(index.sortedIndexEntries.get(destIndexEntry).startRow);\r
504     searchText.addTextChangedListener(searchTextWatcher);\r
505   }\r
506 \r
507   // --------------------------------------------------------------------------\r
508   // Options Menu\r
509   // --------------------------------------------------------------------------\r
510   \r
511   @Override\r
512   public boolean onCreateOptionsMenu(final Menu menu) {\r
513     application.onCreateGlobalOptionsMenu(this, menu);\r
514 \r
515     {\r
516       final MenuItem dictionaryList = menu.add(getString(R.string.dictionaryManager));\r
517       dictionaryList.setOnMenuItemClickListener(new OnMenuItemClickListener() {\r
518         public boolean onMenuItemClick(final MenuItem menuItem) {\r
519           startActivity(DictionaryManagerActivity.getLaunchIntent());\r
520           finish();\r
521           return false;\r
522         }\r
523       });\r
524     }\r
525 \r
526     {\r
527       final MenuItem aboutDictionary = menu.add(getString(R.string.aboutDictionary));\r
528       aboutDictionary.setOnMenuItemClickListener(new OnMenuItemClickListener() {\r
529         public boolean onMenuItemClick(final MenuItem menuItem) {\r
530           final Context context = getListView().getContext();\r
531           final Dialog dialog = new Dialog(context);\r
532           dialog.setContentView(R.layout.about_dictionary_dialog);\r
533           final TextView textView = (TextView) dialog.findViewById(R.id.text);\r
534 \r
535           final String name = application.getDictionaryName(dictFile.getName());\r
536           dialog.setTitle(name);\r
537           \r
538           final StringBuilder builder = new StringBuilder();\r
539           final DictionaryInfo dictionaryInfo = Dictionary.getDictionaryInfo(dictFile);\r
540           if (dictionaryInfo != null) {\r
541             builder.append(dictionaryInfo.dictInfo).append("\n\n");\r
542             builder.append(getString(R.string.dictionaryPath, dictFile.getPath())).append("\n");\r
543             builder.append(getString(R.string.dictionarySize, dictionaryInfo.uncompressedBytes)).append("\n");\r
544             builder.append(getString(R.string.dictionaryCreationTime, dictionaryInfo.creationMillis)).append("\n");\r
545             for (final IndexInfo indexInfo : dictionaryInfo.indexInfos) {\r
546               builder.append("\n");\r
547               builder.append(getString(R.string.indexName, indexInfo.shortName)).append("\n");\r
548               builder.append(getString(R.string.mainTokenCount, indexInfo.mainTokenCount)).append("\n");\r
549             }\r
550           } else {\r
551             builder.append(getString(R.string.invalidDictionary));\r
552           }\r
553           textView.setText(builder.toString());\r
554           \r
555           dialog.show();\r
556           final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();\r
557           layoutParams.width = WindowManager.LayoutParams.FILL_PARENT;\r
558           layoutParams.height = WindowManager.LayoutParams.FILL_PARENT;\r
559           dialog.getWindow().setAttributes(layoutParams);\r
560           return false;\r
561         }\r
562       });\r
563     }\r
564 \r
565     return true;\r
566   }\r
567 \r
568 \r
569   // --------------------------------------------------------------------------\r
570   // Context Menu + clicks\r
571   // --------------------------------------------------------------------------\r
572 \r
573   @Override\r
574   public void onCreateContextMenu(ContextMenu menu, View v,\r
575       ContextMenuInfo menuInfo) {\r
576     AdapterContextMenuInfo adapterContextMenuInfo = (AdapterContextMenuInfo) menuInfo;\r
577     final RowBase row = (RowBase) getListAdapter().getItem(adapterContextMenuInfo.position);\r
578 \r
579     final MenuItem addToWordlist = menu.add(getString(R.string.addToWordList, wordList.getName()));\r
580     addToWordlist.setOnMenuItemClickListener(new OnMenuItemClickListener() {\r
581       public boolean onMenuItemClick(MenuItem item) {\r
582         onAppendToWordList(row);\r
583         return false;\r
584       }\r
585     });\r
586 \r
587     final MenuItem copy = menu.add(android.R.string.copy);\r
588     copy.setOnMenuItemClickListener(new OnMenuItemClickListener() {\r
589       public boolean onMenuItemClick(MenuItem item) {\r
590         onCopy(row);\r
591         return false;\r
592       }\r
593     });\r
594     \r
595     if (selectedSpannableText != null) {\r
596       final String selectedText = selectedSpannableText;\r
597       final MenuItem searchForSelection = menu.add(getString(R.string.searchForSelection, selectedSpannableText));\r
598       searchForSelection.setOnMenuItemClickListener(new OnMenuItemClickListener() {\r
599         public boolean onMenuItemClick(MenuItem item) {\r
600           if (indexIndex != selectedSpannableIndex) {\r
601             changeIndex(selectedSpannableIndex);\r
602           }\r
603           setSearchText(selectedText);\r
604           return false;\r
605         }\r
606       });\r
607     }\r
608     \r
609 \r
610   }\r
611   \r
612   @Override\r
613   protected void onListItemClick(ListView l, View v, int row, long id) {\r
614     defocusSearchText();\r
615     if (clickOpensContextMenu && dictRaf != null) {\r
616       openContextMenu(v);\r
617     }\r
618   }\r
619   \r
620   void onAppendToWordList(final RowBase row) {\r
621     defocusSearchText();\r
622     \r
623     final StringBuilder rawText = new StringBuilder();\r
624     rawText.append(\r
625         new SimpleDateFormat("yyyy.MM.dd HH:mm:ss").format(new Date()))\r
626         .append("\t");\r
627     rawText.append(index.longName).append("\t");\r
628     rawText.append(row.getTokenRow(true).getToken()).append("\t");\r
629     rawText.append(row.getRawText(saveOnlyFirstSubentry));\r
630     Log.d(LOG, "Writing : " + rawText);\r
631 \r
632     try {\r
633       wordList.getParentFile().mkdirs();\r
634       final PrintWriter out = new PrintWriter(\r
635           new FileWriter(wordList, true));\r
636       out.println(rawText.toString());\r
637       out.close();\r
638     } catch (IOException e) {\r
639       Log.e(LOG, "Unable to append to " + wordList.getAbsolutePath(), e);\r
640       Toast.makeText(this, getString(R.string.failedAddingToWordList, wordList.getAbsolutePath()), Toast.LENGTH_LONG);\r
641     }\r
642     return;\r
643   }\r
644   \r
645   /**\r
646    * Called when user clicks outside of search text, so that they can start\r
647    * typing again immediately.\r
648    */\r
649   void defocusSearchText() {\r
650     //Log.d(LOG, "defocusSearchText");\r
651     // Request focus so that if we start typing again, it clears the text input.\r
652     getListView().requestFocus();\r
653     \r
654     // Visual indication that a new keystroke will clear the search text.\r
655     searchText.selectAll();\r
656   }\r
657 \r
658   void onCopy(final RowBase row) {\r
659     defocusSearchText();\r
660 \r
661     Log.d(LOG, "Copy, row=" + row);\r
662     final StringBuilder result = new StringBuilder();\r
663     result.append(row.getRawText(false));\r
664     final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);\r
665     clipboardManager.setText(result.toString());\r
666     Log.d(LOG, "Copied: " + result);\r
667   }\r
668 \r
669   @Override\r
670   public boolean onKeyDown(final int keyCode, final KeyEvent event) {\r
671     if (event.getUnicodeChar() != 0) {\r
672       if (!searchText.hasFocus()) {\r
673         setSearchText("" + (char) event.getUnicodeChar());\r
674       }\r
675       return true;\r
676     }\r
677     if (keyCode == KeyEvent.KEYCODE_BACK) {\r
678       Log.d(LOG, "Clearing dictionary prefs.");\r
679       DictionaryActivity.clearDictionaryPrefs(this);\r
680     }\r
681     if (keyCode == KeyEvent.KEYCODE_ENTER) {\r
682       Log.d(LOG, "Trying to hide soft keyboard.");\r
683       final InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);\r
684       inputManager.hideSoftInputFromWindow(this.getCurrentFocus().getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);\r
685       return true;\r
686     }\r
687     return super.onKeyDown(keyCode, event);\r
688   }\r
689 \r
690   private void setSearchText(final String text) {\r
691     searchText.setText(text);\r
692     searchText.requestFocus();\r
693     onSearchTextChange(searchText.getText().toString());\r
694     Selection.moveToRightEdge(searchText.getText(), searchText.getLayout());\r
695   }\r
696 \r
697 \r
698   // --------------------------------------------------------------------------\r
699   // SearchOperation\r
700   // --------------------------------------------------------------------------\r
701 \r
702   private void searchFinished(final SearchOperation searchOperation) {\r
703     if (searchOperation.interrupted.get()) {\r
704       Log.d(LOG, "Search operation was interrupted: " + searchOperation);\r
705       return;\r
706     }\r
707     if (searchOperation != this.currentSearchOperation) {\r
708       Log.d(LOG, "Stale searchOperation finished: " + searchOperation);\r
709       return;\r
710     }\r
711     \r
712     final Index.IndexEntry searchResult = searchOperation.searchResult;\r
713     Log.d(LOG, "searchFinished: " + searchOperation + ", searchResult=" + searchResult);\r
714 \r
715     currentSearchOperation = null;\r
716     uiHandler.postDelayed(new Runnable() {\r
717       @Override\r
718       public void run() {\r
719         if (currentSearchOperation == null) {\r
720           if (searchResult != null) {\r
721             if (isFiltered()) {\r
722               clearFiltered();\r
723             }\r
724             jumpToRow(searchResult.startRow);\r
725           } else if (searchOperation.multiWordSearchResult != null) {\r
726             // Multi-row search....\r
727             setFiltered(searchOperation);\r
728           } else {\r
729             throw new IllegalStateException("This should never happen.");\r
730           }\r
731         } else {\r
732           Log.d(LOG, "More coming, waiting for currentSearchOperation.");\r
733         }\r
734       }\r
735     }, 20);\r
736     \r
737   }\r
738   \r
739   private final void jumpToRow(final int row) {\r
740     setSelection(row);\r
741     getListView().setSelected(true);\r
742   }\r
743 \r
744   static final Pattern WHITESPACE = Pattern.compile("\\s+");\r
745   final class SearchOperation implements Runnable {\r
746     \r
747     final AtomicBoolean interrupted = new AtomicBoolean(false);\r
748     final String searchText;\r
749     List<String> searchTokens;  // filled in for multiWord.\r
750     final Index index;\r
751     \r
752     long searchStartMillis;\r
753 \r
754     Index.IndexEntry searchResult;\r
755     List<RowBase> multiWordSearchResult;\r
756     \r
757     boolean done = false;\r
758     \r
759     SearchOperation(final String searchText, final Index index) {\r
760       this.searchText = searchText.trim();\r
761       this.index = index;\r
762     }\r
763     \r
764     public String toString() {\r
765       return String.format("SearchOperation(%s,%s)", searchText, interrupted.toString());\r
766     }\r
767 \r
768     @Override\r
769     public void run() {\r
770       try {\r
771         searchStartMillis = System.currentTimeMillis();\r
772         final String[] searchTokenArray = WHITESPACE.split(searchText);\r
773         if (searchTokenArray.length == 1) {\r
774           searchResult = index.findInsertionPoint(searchText, interrupted);\r
775         } else {\r
776           searchTokens = Arrays.asList(searchTokenArray);\r
777           multiWordSearchResult = index.multiWordSearch(searchTokens, interrupted);\r
778         }\r
779         Log.d(LOG, "searchText=" + searchText + ", searchDuration="\r
780             + (System.currentTimeMillis() - searchStartMillis) + ", interrupted="\r
781             + interrupted.get());\r
782         if (!interrupted.get()) {\r
783           uiHandler.post(new Runnable() {\r
784             @Override\r
785             public void run() {            \r
786               searchFinished(SearchOperation.this);\r
787             }\r
788           });\r
789         }\r
790       } finally {\r
791         synchronized (this) {\r
792           done = true;\r
793           this.notifyAll();\r
794         }\r
795       }\r
796     }\r
797   }\r
798 \r
799   \r
800   // --------------------------------------------------------------------------\r
801   // IndexAdapter\r
802   // --------------------------------------------------------------------------\r
803 \r
804   final class IndexAdapter extends BaseAdapter {\r
805     \r
806     final Index index;\r
807     final List<RowBase> rows;\r
808     final Set<String> toHighlight;\r
809 \r
810     IndexAdapter(final Index index) {\r
811       this.index = index;\r
812       rows = index.rows;\r
813       this.toHighlight = null;\r
814     }\r
815 \r
816     IndexAdapter(final Index index, final List<RowBase> rows, final List<String> toHighlight) {\r
817       this.index = index;\r
818       this.rows = rows;\r
819       this.toHighlight = new LinkedHashSet<String>(toHighlight);\r
820     }\r
821 \r
822     @Override\r
823     public int getCount() {\r
824       return rows.size();\r
825     }\r
826 \r
827     @Override\r
828     public RowBase getItem(int position) {\r
829       return rows.get(position);\r
830     }\r
831 \r
832     @Override\r
833     public long getItemId(int position) {\r
834       return getItem(position).index();\r
835     }\r
836 \r
837     @Override\r
838     public View getView(int position, final View convertView, ViewGroup parent) {\r
839       final RowBase row = getItem(position);\r
840       if (row instanceof PairEntry.Row) {\r
841         return getView(position, (PairEntry.Row) row, parent, convertView);\r
842       } else if (row instanceof TokenRow) {\r
843         return getView((TokenRow) row, parent, convertView);\r
844       } else {\r
845         throw new IllegalArgumentException("Unsupported Row type: " + row.getClass());\r
846       }\r
847     }\r
848 \r
849     private View getView(final int position, PairEntry.Row row, ViewGroup parent, final View convertView) {\r
850       final TableLayout result = new TableLayout(parent.getContext());\r
851       final PairEntry entry = row.getEntry();\r
852       final int rowCount = entry.pairs.size();\r
853       for (int r = 0; r < rowCount; ++r) {\r
854         final TableRow tableRow = new TableRow(result.getContext());\r
855 \r
856         final TextView col1 = new TextView(tableRow.getContext());\r
857         final TextView col2 = new TextView(tableRow.getContext());\r
858         final TableRow.LayoutParams layoutParams = new TableRow.LayoutParams();\r
859         layoutParams.weight = 0.5f;\r
860 \r
861         // Set the columns in the table.\r
862         if (r > 0) {\r
863           final TextView bullet = new TextView(tableRow.getContext());\r
864           bullet.setText(" â€¢ ");\r
865           tableRow.addView(bullet);\r
866         }\r
867         tableRow.addView(col1, layoutParams);\r
868         final TextView margin = new TextView(tableRow.getContext());\r
869         margin.setText(" ");\r
870         tableRow.addView(margin);\r
871         if (r > 0) {\r
872           final TextView bullet = new TextView(tableRow.getContext());\r
873           bullet.setText(" â€¢ ");\r
874           tableRow.addView(bullet);\r
875         }\r
876         tableRow.addView(col2, layoutParams);\r
877         col1.setWidth(1);\r
878         col2.setWidth(1);\r
879         \r
880         // Set what's in the columns.\r
881 \r
882         // TODO: color words by gender\r
883         final Pair pair = entry.pairs.get(r);\r
884         final String col1Text = index.swapPairEntries ? pair.lang2 : pair.lang1;\r
885         final String col2Text = index.swapPairEntries ? pair.lang1 : pair.lang2;\r
886         \r
887         col1.setText(col1Text, TextView.BufferType.SPANNABLE);\r
888         col2.setText(col2Text, TextView.BufferType.SPANNABLE);\r
889         \r
890         // Bold the token instances in col1.\r
891         final Set<String> toBold = toHighlight != null ? this.toHighlight : Collections.singleton(row.getTokenRow(true).getToken());\r
892         final Spannable col1Spannable = (Spannable) col1.getText();\r
893         for (final String token : toBold) {\r
894           int startPos = 0;\r
895           while ((startPos = col1Text.indexOf(token, startPos)) != -1) {\r
896             col1Spannable.setSpan(new StyleSpan(Typeface.BOLD), startPos,\r
897                 startPos + token.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);\r
898             startPos += token.length();\r
899           }\r
900         }\r
901         \r
902         createTokenLinkSpans(col1, col1Spannable, col1Text);\r
903         createTokenLinkSpans(col2, (Spannable) col2.getText(), col2Text);\r
904         \r
905         col1.setTextSize(TypedValue.COMPLEX_UNIT_SP, fontSizeSp);\r
906         col2.setTextSize(TypedValue.COMPLEX_UNIT_SP, fontSizeSp);\r
907         // col2.setBackgroundResource(theme.otherLangBg);\r
908         \r
909         if (index.swapPairEntries) {\r
910           col2.setOnLongClickListener(textViewLongClickListenerIndex0);\r
911           col1.setOnLongClickListener(textViewLongClickListenerIndex1);\r
912         } else {\r
913           col1.setOnLongClickListener(textViewLongClickListenerIndex0);\r
914           col2.setOnLongClickListener(textViewLongClickListenerIndex1);\r
915         }\r
916         \r
917         // Because we have a Button inside a ListView row:\r
918         // http://groups.google.com/group/android-developers/browse_thread/thread/3d96af1530a7d62a?pli=1\r
919         result.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);\r
920         result.setClickable(true);\r
921         result.setFocusable(true);\r
922         result.setLongClickable(true);\r
923         result.setBackgroundResource(android.R.drawable.menuitem_background);\r
924         result.setOnClickListener(new TextView.OnClickListener() {\r
925           @Override\r
926           public void onClick(View v) {\r
927             DictionaryActivity.this.onListItemClick(null, v, position, position);\r
928           }\r
929         });\r
930         \r
931         result.addView(tableRow);\r
932       }\r
933 \r
934       return result;\r
935 \r
936       \r
937 //      final WebView result = (WebView) (convertView instanceof WebView ? convertView : new WebView(parent.getContext()));\r
938 //        \r
939 //      final PairEntry entry = row.getEntry();\r
940 //      final int rowCount = entry.pairs.size();\r
941 //      final StringBuilder html = new StringBuilder();\r
942 //      html.append("<html><body><table width=\"100%\">");\r
943 //      for (int r = 0; r < rowCount; ++r) {\r
944 //        html.append("<tr>");\r
945 //\r
946 //        final Pair pair = entry.pairs.get(r);\r
947 //        // TODO: escape both the token and the text.\r
948 //        final String token = row.getTokenRow(true).getToken();\r
949 //        final String col1Text = index.swapPairEntries ? pair.lang2 : pair.lang1;\r
950 //        final String col2Text = index.swapPairEntries ? pair.lang1 : pair.lang2;\r
951 //        \r
952 //        col1Text.replaceAll(token, String.format("<b>%s</b>", token));\r
953 //\r
954 //        // Column1\r
955 //        html.append("<td width=\"50%\">");\r
956 //        if (r > 0) {\r
957 //          html.append("<li>");\r
958 //        }\r
959 //        html.append(col1Text);\r
960 //        html.append("</td>");\r
961 //\r
962 //        // Column2\r
963 //        html.append("<td width=\"50%\">");\r
964 //        if (r > 0) {\r
965 //          html.append("<li>");\r
966 //        }\r
967 //        html.append(col2Text);\r
968 //        html.append("</td>");\r
969 //\r
970 ////        column1.setTextSize(TypedValue.COMPLEX_UNIT_SP, fontSizeSp);\r
971 ////        column2.setTextSize(TypedValue.COMPLEX_UNIT_SP, fontSizeSp);\r
972 //\r
973 //        html.append("</tr>");\r
974 //      }\r
975 //      html.append("</table></body></html>");\r
976 //      \r
977 //      Log.i(LOG, html.toString());\r
978 //      \r
979 //      result.getSettings().setRenderPriority(RenderPriority.HIGH);\r
980 //      result.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE);\r
981 //      \r
982 //      result.loadData("<html><body><table><tr><td>line (connected series of public conveyances, and hence, an established arrangement for forwarding merchandise, etc.) (noun)</td><td>verbinding</td></tr></table></body></html>", "text/html", "utf-8");\r
983 //\r
984 //      return result;\r
985     }\r
986 \r
987     private View getView(TokenRow row, ViewGroup parent, final View convertView) {\r
988       final Context context = parent.getContext();\r
989       final TextView textView = new TextView(context);\r
990       textView.setText(row.getToken());\r
991       textView.setBackgroundResource(row.hasMainEntry ? theme.tokenRowMainBg : theme.tokenRowOtherBg);\r
992       // Doesn't work:\r
993       //textView.setTextColor(android.R.color.secondary_text_light);\r
994       textView.setTextAppearance(context, theme.tokenRowFg);\r
995       textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 5 * fontSizeSp / 4);\r
996       return textView;\r
997     }\r
998     \r
999   }\r
1000 \r
1001   static final Pattern CHAR_DASH = Pattern.compile("['\\p{L}0-9]+");\r
1002 \r
1003   private void createTokenLinkSpans(final TextView textView, final Spannable spannable, final String text) {\r
1004     // Saw from the source code that LinkMovementMethod sets the selection!\r
1005     // http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/2.3.1_r1/android/text/method/LinkMovementMethod.java#LinkMovementMethod\r
1006     textView.setMovementMethod(LinkMovementMethod.getInstance());\r
1007     final Matcher matcher = CHAR_DASH.matcher(text);\r
1008     while (matcher.find()) {\r
1009       spannable.setSpan(new NonLinkClickableSpan(), matcher.start(), matcher.end(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);\r
1010     }\r
1011   }\r
1012   \r
1013 \r
1014   String selectedSpannableText = null;\r
1015   int selectedSpannableIndex = -1;\r
1016 \r
1017   @Override\r
1018   public boolean onTouchEvent(MotionEvent event) {\r
1019     selectedSpannableText = null;\r
1020     selectedSpannableIndex = -1;\r
1021     return super.onTouchEvent(event);\r
1022   }\r
1023 \r
1024   private class TextViewLongClickListener implements OnLongClickListener {\r
1025     final int index;\r
1026     \r
1027     private TextViewLongClickListener(final int index) {\r
1028       this.index = index;\r
1029     }\r
1030 \r
1031     @Override\r
1032     public boolean onLongClick(final View v) {\r
1033       final TextView textView = (TextView) v;\r
1034       final int start = textView.getSelectionStart();\r
1035       final int end = textView.getSelectionEnd();\r
1036       if (start >= 0 &&  end >= 0) {\r
1037         selectedSpannableText = textView.getText().subSequence(start, end).toString();\r
1038         selectedSpannableIndex = index;\r
1039       }\r
1040       return false;\r
1041     }\r
1042   }\r
1043   final TextViewLongClickListener textViewLongClickListenerIndex0 = new TextViewLongClickListener(0);\r
1044   final TextViewLongClickListener textViewLongClickListenerIndex1 = new TextViewLongClickListener(1);\r
1045   \r
1046 \r
1047   // --------------------------------------------------------------------------\r
1048   // SearchText\r
1049   // --------------------------------------------------------------------------\r
1050 \r
1051   void onSearchTextChange(final String text) {\r
1052     if (dictRaf == null) {\r
1053       Log.d(LOG, "searchText changed during shutdown, doing nothing.");\r
1054       return;\r
1055     }\r
1056     if (!searchText.isFocused()) {\r
1057       Log.d(LOG, "searchText changed without focus, doing nothing.");\r
1058       return;\r
1059     }\r
1060     Log.d(LOG, "onSearchTextChange: " + text);    \r
1061     if (currentSearchOperation != null) {\r
1062       Log.d(LOG, "Interrupting currentSearchOperation.");\r
1063       currentSearchOperation.interrupted.set(true);\r
1064     }\r
1065     currentSearchOperation = new SearchOperation(text, index);\r
1066     searchExecutor.execute(currentSearchOperation);\r
1067   }\r
1068   \r
1069   private class SearchTextWatcher implements TextWatcher {\r
1070     public void afterTextChanged(final Editable searchTextEditable) {\r
1071       if (searchText.hasFocus()) {\r
1072         Log.d(LOG, "Search text changed with focus: " + searchText.getText());\r
1073         // If they were typing to cause the change, update the UI.\r
1074         onSearchTextChange(searchText.getText().toString());\r
1075       }\r
1076     }\r
1077 \r
1078     public void beforeTextChanged(CharSequence arg0, int arg1, int arg2,\r
1079         int arg3) {\r
1080     }\r
1081 \r
1082     public void onTextChanged(CharSequence arg0, int arg1, int arg2, int arg3) {\r
1083     }\r
1084   }\r
1085 \r
1086   // --------------------------------------------------------------------------\r
1087   // Filtered results.\r
1088   // --------------------------------------------------------------------------\r
1089 \r
1090   boolean isFiltered() {\r
1091     return rowsToShow != null;\r
1092   }\r
1093 \r
1094   void setFiltered(final SearchOperation searchOperation) {\r
1095     ((Button) findViewById(R.id.UpButton)).setEnabled(false);\r
1096     ((Button) findViewById(R.id.DownButton)).setEnabled(false);\r
1097     rowsToShow = searchOperation.multiWordSearchResult;\r
1098     setListAdapter(new IndexAdapter(index, rowsToShow, searchOperation.searchTokens));\r
1099   }\r
1100 \r
1101   void clearFiltered() {\r
1102     ((Button) findViewById(R.id.UpButton)).setEnabled(true);\r
1103     ((Button) findViewById(R.id.DownButton)).setEnabled(true);\r
1104     setListAdapter(new IndexAdapter(index));\r
1105     rowsToShow = null;\r
1106   }\r
1107 \r
1108 }\r