]> gitweb.fperrin.net Git - Dictionary.git/blob - src/com/hughes/android/dictionary/DictionaryActivity.java
Theme, different color rows, dictionary_info, long click begin.
[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.Date;\r
24 import java.util.concurrent.Executor;\r
25 import java.util.concurrent.Executors;\r
26 import java.util.concurrent.ThreadFactory;\r
27 import java.util.concurrent.atomic.AtomicBoolean;\r
28 \r
29 import android.app.ListActivity;\r
30 import android.content.Context;\r
31 import android.content.Intent;\r
32 import android.content.SharedPreferences;\r
33 import android.graphics.Typeface;\r
34 import android.os.Bundle;\r
35 import android.os.Handler;\r
36 import android.preference.PreferenceManager;\r
37 import android.text.ClipboardManager;\r
38 import android.text.Editable;\r
39 import android.text.Selection;\r
40 import android.text.Spannable;\r
41 import android.text.TextWatcher;\r
42 import android.text.style.StyleSpan;\r
43 import android.util.Log;\r
44 import android.util.TypedValue;\r
45 import android.view.ContextMenu;\r
46 import android.view.ContextMenu.ContextMenuInfo;\r
47 import android.view.KeyEvent;\r
48 import android.view.Menu;\r
49 import android.view.MenuItem;\r
50 import android.view.MenuItem.OnMenuItemClickListener;\r
51 import android.view.View;\r
52 import android.view.View.OnClickListener;\r
53 import android.view.ViewGroup;\r
54 import android.view.inputmethod.InputMethodManager;\r
55 import android.widget.AdapterView;\r
56 import android.widget.AdapterView.AdapterContextMenuInfo;\r
57 import android.widget.BaseAdapter;\r
58 import android.widget.Button;\r
59 import android.widget.EditText;\r
60 import android.widget.ListAdapter;\r
61 import android.widget.ListView;\r
62 import android.widget.TableLayout;\r
63 import android.widget.TableRow;\r
64 import android.widget.TextView;\r
65 import android.widget.Toast;\r
66 \r
67 import com.hughes.android.dictionary.engine.Dictionary;\r
68 import com.hughes.android.dictionary.engine.Index;\r
69 import com.hughes.android.dictionary.engine.Language;\r
70 import com.hughes.android.dictionary.engine.PairEntry;\r
71 import com.hughes.android.dictionary.engine.PairEntry.Pair;\r
72 import com.hughes.android.dictionary.engine.RowBase;\r
73 import com.hughes.android.dictionary.engine.TokenRow;\r
74 import com.hughes.android.dictionary.engine.TransliteratorManager;\r
75 import com.hughes.android.util.PersistentObjectCache;\r
76 \r
77 public class DictionaryActivity extends ListActivity {\r
78 \r
79   static final String LOG = "QuickDic";\r
80   \r
81   int dictIndex = 0;\r
82   RandomAccessFile dictRaf = null;\r
83   Dictionary dictionary = null;\r
84   int indexIndex = 0;\r
85   Index index = null;\r
86   \r
87   // package for test.\r
88   final Handler uiHandler = new Handler();\r
89   private final Executor searchExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() {\r
90     @Override\r
91     public Thread newThread(Runnable r) {\r
92       return new Thread(r, "searchExecutor");\r
93     }\r
94   });\r
95   private SearchOperation currentSearchOperation = null;\r
96 \r
97   C.Theme theme = C.Theme.LIGHT;\r
98   int fontSizeSp;\r
99   EditText searchText;\r
100   Button langButton;\r
101 \r
102   // Never null.\r
103   private File wordList = null;\r
104   private boolean saveOnlyFirstSubentry = false;\r
105   private boolean clickOpensContextMenu = false;\r
106 \r
107   // Visible for testing.\r
108   ListAdapter indexAdapter = null;\r
109   \r
110   final SearchTextWatcher searchTextWatcher = new SearchTextWatcher();\r
111 \r
112   //private Vibrator vibrator = null;\r
113   \r
114   public DictionaryActivity() {\r
115   }\r
116   \r
117   public static Intent getIntent(final Context context, final int dictIndex, final int indexIndex, final String searchToken) {\r
118     setDictionaryPrefs(context, dictIndex, indexIndex, searchToken);\r
119     final Intent intent = new Intent();\r
120     intent.setClassName(DictionaryActivity.class.getPackage().getName(), DictionaryActivity.class.getName());\r
121     return intent;\r
122   }\r
123   \r
124   // TODO: Can we trigger an App restart when the preferences activity gets opened?\r
125   // TODO: fix these...\r
126 \r
127   @Override\r
128   protected void onSaveInstanceState(final Bundle outState) {\r
129     super.onSaveInstanceState(outState);\r
130     setDictionaryPrefs(this, dictIndex, indexIndex, searchText.getText().toString());\r
131   }\r
132 \r
133   public static void setDictionaryPrefs(final Context context,\r
134       final int dictIndex, final int indexIndex, final String searchToken) {\r
135     final SharedPreferences.Editor prefs = PreferenceManager.getDefaultSharedPreferences(context).edit();\r
136     prefs.putInt(C.DICT_INDEX, dictIndex);\r
137     prefs.putInt(C.INDEX_INDEX, indexIndex);\r
138     prefs.putString(C.SEARCH_TOKEN, searchToken);\r
139     prefs.commit();\r
140   }\r
141   \r
142   public static void clearDictionaryPrefs(final Context context) {\r
143     final SharedPreferences.Editor prefs = PreferenceManager\r
144         .getDefaultSharedPreferences(context).edit();\r
145     prefs.remove(C.DICT_INDEX);\r
146     prefs.remove(C.INDEX_INDEX);\r
147     prefs.remove(C.SEARCH_TOKEN);\r
148     prefs.commit();\r
149     Log.d(LOG, "Removed default dictionary prefs.");\r
150   }\r
151 \r
152   @Override\r
153   public void onCreate(Bundle savedInstanceState) {\r
154     Log.d(LOG, "onCreate:" + this);\r
155     theme = ((DictionaryApplication)getApplication()).getSelectedTheme();\r
156     super.onCreate(savedInstanceState);\r
157     \r
158     final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);\r
159     \r
160     try {\r
161       PersistentObjectCache.init(this);\r
162       QuickDicConfig quickDicConfig = PersistentObjectCache.init(\r
163           this).read(C.DICTIONARY_CONFIGS, QuickDicConfig.class);\r
164       dictIndex = prefs.getInt(C.DICT_INDEX, 0) ;\r
165       final DictionaryInfo dictionaryConfig = quickDicConfig.dictionaryInfos.get(dictIndex);\r
166       this.setTitle("QuickDic: " + dictionaryConfig.name);\r
167       dictRaf = new RandomAccessFile(dictionaryConfig.localFile, "r");\r
168       dictionary = new Dictionary(dictRaf); \r
169     } catch (Exception e) {\r
170       Log.e(LOG, "Unable to load dictionary.", e);\r
171       if (dictRaf != null) {\r
172         try {\r
173           dictRaf.close();\r
174         } catch (IOException e1) {\r
175           Log.e(LOG, "Unable to close dictRaf.", e1);\r
176         }\r
177         dictRaf = null;\r
178       }\r
179       Toast.makeText(this, getString(R.string.invalidDictionary, "", e.getMessage()), Toast.LENGTH_LONG);\r
180       startActivity(DictionaryManagerActivity.getIntent(this));\r
181       finish();\r
182       return;\r
183     }\r
184 \r
185     indexIndex = prefs.getInt(C.INDEX_INDEX, 0) % dictionary.indices.size();\r
186     Log.d(LOG, "Loading index.");\r
187     index = dictionary.indices.get(indexIndex);\r
188     setListAdapter(new IndexAdapter(index));\r
189 \r
190     // Pre-load the collators.\r
191     searchExecutor.execute(new Runnable() {\r
192       public void run() {\r
193         final long startMillis = System.currentTimeMillis();\r
194         \r
195         TransliteratorManager.init(new TransliteratorManager.Callback() {\r
196           @Override\r
197           public void onTransliteratorReady() {\r
198             uiHandler.post(new Runnable() {\r
199               @Override\r
200               public void run() {\r
201                 onSearchTextChange(searchText.getText().toString());\r
202               }\r
203             });\r
204           }\r
205         });\r
206         \r
207         for (final Index index : dictionary.indices) {\r
208           Log.d(LOG, "Starting collator load for lang=" + index.sortLanguage.getIsoCode());\r
209           \r
210           final com.ibm.icu.text.Collator c = index.sortLanguage.getCollator();          \r
211           if (c.compare("pre-print", "preppy") >= 0) {\r
212             Log.e(LOG, c.getClass()\r
213                 + " is buggy, lookups may not work properly.");\r
214           }\r
215         }\r
216         Log.d(LOG, "Loading collators took:"\r
217             + (System.currentTimeMillis() - startMillis));\r
218       }\r
219     });\r
220     \r
221     final String fontSize = prefs.getString(getString(R.string.fontSizeKey), "12");\r
222     try {\r
223       fontSizeSp = Integer.parseInt(fontSize);\r
224     } catch (NumberFormatException e) {\r
225       fontSizeSp = 12;\r
226     }\r
227 \r
228     setContentView(R.layout.dictionary_activity);\r
229     searchText = (EditText) findViewById(R.id.SearchText);\r
230     searchText.setTextSize(TypedValue.COMPLEX_UNIT_SP, fontSizeSp);\r
231     \r
232     langButton = (Button) findViewById(R.id.LangButton);\r
233     \r
234     searchText.requestFocus();\r
235     searchText.addTextChangedListener(searchTextWatcher);\r
236     final String search = prefs.getString(C.SEARCH_TOKEN, "");\r
237     searchText.setText(search);\r
238     searchText.setSelection(0, search.length());\r
239     Log.d(LOG, "Trying to restore searchText=" + search);\r
240     \r
241     final Button clearSearchTextButton = (Button) findViewById(R.id.ClearSearchTextButton);\r
242     clearSearchTextButton.setOnClickListener(new OnClickListener() {\r
243       public void onClick(View v) {\r
244         onClearSearchTextButton(clearSearchTextButton);\r
245       }\r
246     });\r
247     clearSearchTextButton.setVisibility(PreferenceManager.getDefaultSharedPreferences(this).getBoolean(\r
248         getString(R.string.showClearSearchTextButtonKey), true) ? View.VISIBLE\r
249         : View.GONE);\r
250     \r
251     final Button langButton = (Button) findViewById(R.id.LangButton);\r
252     langButton.setOnClickListener(new OnClickListener() {\r
253       public void onClick(View v) {\r
254         onLanguageButton();\r
255       }\r
256     });\r
257     updateLangButton();\r
258     \r
259     final Button upButton = (Button) findViewById(R.id.UpButton);\r
260     upButton.setOnClickListener(new OnClickListener() {\r
261       public void onClick(View v) {\r
262         onUpDownButton(true);\r
263       }\r
264     });\r
265     final Button downButton = (Button) findViewById(R.id.DownButton);\r
266     downButton.setOnClickListener(new OnClickListener() {\r
267       public void onClick(View v) {\r
268         onUpDownButton(false);\r
269       }\r
270     });\r
271 \r
272    getListView().setOnItemSelectedListener(new ListView.OnItemSelectedListener() {\r
273       @Override\r
274       public void onItemSelected(AdapterView<?> adapterView, View arg1, final int position,\r
275           long id) {\r
276         if (!searchText.isFocused()) {\r
277           // TODO: don't do this if multi words are entered.\r
278           final RowBase row = (RowBase) getListAdapter().getItem(position);\r
279           Log.d(LOG, "onItemSelected: " + row.index());\r
280           final TokenRow tokenRow = row.getTokenRow(true);\r
281           searchText.setText(tokenRow.getToken());\r
282         }\r
283       }\r
284 \r
285       @Override\r
286       public void onNothingSelected(AdapterView<?> arg0) {\r
287       }\r
288     });\r
289 \r
290     // ContextMenu.\r
291     registerForContextMenu(getListView());\r
292 \r
293     // Prefs.\r
294     wordList = new File(prefs.getString(getString(R.string.wordListFileKey),\r
295         getString(R.string.wordListFileDefault)));\r
296     saveOnlyFirstSubentry = prefs.getBoolean(getString(R.string.saveOnlyFirstSubentryKey), false);\r
297     clickOpensContextMenu = prefs.getBoolean(getString(R.string.clickOpensContextMenuKey), false);\r
298     //if (prefs.getBoolean(getString(R.string.vibrateOnFailedSearchKey), true)) {\r
299       // vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);\r
300     //}\r
301     Log.d(LOG, "wordList=" + wordList + ", saveOnlyFirstSubentry=" + saveOnlyFirstSubentry);\r
302   }\r
303   \r
304   @Override\r
305   protected void onResume() {\r
306     super.onResume();\r
307     if (PreferenceActivity.prefsMightHaveChanged) {\r
308       PreferenceActivity.prefsMightHaveChanged = false;\r
309       finish();\r
310       startActivity(getIntent());\r
311     }\r
312   }\r
313   \r
314   @Override\r
315   protected void onPause() {\r
316     super.onPause();\r
317   }\r
318 \r
319   @Override\r
320   protected void onDestroy() {\r
321     super.onDestroy();\r
322     if (dictRaf == null) {\r
323       return;\r
324     }\r
325     setDictionaryPrefs(this, dictIndex, indexIndex, searchText.getText().toString());\r
326     \r
327     // Before we close the RAF, we have to wind the current search down.\r
328     if (currentSearchOperation != null) {\r
329       Log.d(LOG, "Interrupting search to shut down.");\r
330       final SearchOperation searchOperation = currentSearchOperation;\r
331       currentSearchOperation = null;\r
332       searchOperation.interrupted.set(true);\r
333       synchronized (searchOperation) {\r
334         while (!searchOperation.done) {\r
335           try {\r
336             searchOperation.wait();\r
337           } catch (InterruptedException e) {\r
338             Log.d(LOG, "Interrupted.", e);\r
339           }\r
340         }\r
341       }\r
342     }\r
343     \r
344     try {\r
345       Log.d(LOG, "Closing RAF.");\r
346       dictRaf.close();\r
347     } catch (IOException e) {\r
348       Log.e(LOG, "Failed to close dictionary", e);\r
349     }\r
350     dictRaf = null;\r
351   }\r
352 \r
353   // --------------------------------------------------------------------------\r
354   // Buttons\r
355   // --------------------------------------------------------------------------\r
356 \r
357   private void onClearSearchTextButton(final Button clearSearchTextButton) {\r
358     clearSearchTextButton.requestFocus();\r
359     searchText.setText("");\r
360     searchText.requestFocus();\r
361     Log.d(LOG, "Trying to show soft keyboard.");\r
362     final InputMethodManager manager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);\r
363     manager.showSoftInput(searchText, InputMethodManager.SHOW_FORCED);\r
364   }\r
365   \r
366   void updateLangButton() {\r
367     langButton.setText(index.shortName.toUpperCase());\r
368   }\r
369 \r
370   void onLanguageButton() {\r
371     if (currentSearchOperation != null) {\r
372       currentSearchOperation.interrupted.set(true);\r
373       currentSearchOperation = null;\r
374     }\r
375     \r
376     indexIndex = (indexIndex + 1) % dictionary.indices.size();\r
377     index = dictionary.indices.get(indexIndex);\r
378     indexAdapter = new IndexAdapter(index);\r
379     Log.d(LOG, "onLanguageButton, newLang=" + index.longName);\r
380     setListAdapter(indexAdapter);\r
381     updateLangButton();\r
382     onSearchTextChange(searchText.getText().toString());\r
383   }\r
384   \r
385   void onUpDownButton(final boolean up) {\r
386     final int firstVisibleRow = getListView().getFirstVisiblePosition();\r
387     final RowBase row = index.rows.get(firstVisibleRow);\r
388     final TokenRow tokenRow = row.getTokenRow(true);\r
389     final int destIndexEntry;\r
390     if (up) {\r
391       if (row != tokenRow) {\r
392         destIndexEntry = tokenRow.referenceIndex;\r
393       } else {\r
394         destIndexEntry = Math.max(tokenRow.referenceIndex - 1, 0);\r
395       }\r
396     } else {\r
397       // Down\r
398       destIndexEntry = Math.min(tokenRow.referenceIndex + 1, index.sortedIndexEntries.size());\r
399     }\r
400     final Index.IndexEntry dest = index.sortedIndexEntries.get(destIndexEntry);\r
401     Log.d(LOG, "onUpDownButton, destIndexEntry=" + dest.token);\r
402     searchText.removeTextChangedListener(searchTextWatcher);\r
403     searchText.setText(dest.token);\r
404     jumpToRow(index.sortedIndexEntries.get(destIndexEntry).startRow);\r
405     searchText.addTextChangedListener(searchTextWatcher);\r
406   }\r
407 \r
408   // --------------------------------------------------------------------------\r
409   // Options Menu\r
410   // --------------------------------------------------------------------------\r
411   \r
412   @Override\r
413   public boolean onCreateOptionsMenu(final Menu menu) {\r
414     \r
415     {\r
416       final MenuItem preferences = menu.add(getString(R.string.preferences));\r
417       preferences.setOnMenuItemClickListener(new OnMenuItemClickListener() {\r
418         public boolean onMenuItemClick(final MenuItem menuItem) {\r
419           PreferenceActivity.prefsMightHaveChanged = true;\r
420           startActivity(new Intent(DictionaryActivity.this,\r
421               PreferenceActivity.class));\r
422           return false;\r
423         }\r
424       });\r
425     }\r
426 \r
427     {\r
428       final MenuItem dictionaryList = menu.add(getString(R.string.dictionaryList));\r
429       dictionaryList.setOnMenuItemClickListener(new OnMenuItemClickListener() {\r
430         public boolean onMenuItemClick(final MenuItem menuItem) {\r
431           startActivity(DictionaryManagerActivity.getIntent(DictionaryActivity.this));\r
432           finish();\r
433           return false;\r
434         }\r
435       });\r
436     }\r
437 \r
438     {\r
439       final MenuItem dictionaryEdit = menu.add(getString(R.string.editDictionary));\r
440       dictionaryEdit.setOnMenuItemClickListener(new OnMenuItemClickListener() {\r
441         public boolean onMenuItemClick(final MenuItem menuItem) {\r
442           final Intent intent = DictionaryEditActivity.getIntent(dictIndex);\r
443           startActivity(intent);\r
444           return false;\r
445         }\r
446       });\r
447     }\r
448 \r
449     {\r
450       final MenuItem about = menu.add(getString(R.string.about));\r
451       about.setOnMenuItemClickListener(new OnMenuItemClickListener() {\r
452         public boolean onMenuItemClick(final MenuItem menuItem) {\r
453           final Intent intent = new Intent().setClassName(AboutActivity.class\r
454               .getPackage().getName(), AboutActivity.class.getCanonicalName());\r
455           startActivity(intent);\r
456           return false;\r
457         }\r
458       });\r
459     }\r
460 \r
461     return true;\r
462   }\r
463 \r
464 \r
465   // --------------------------------------------------------------------------\r
466   // Context Menu + clicks\r
467   // --------------------------------------------------------------------------\r
468 \r
469   @Override\r
470   public void onCreateContextMenu(ContextMenu menu, View v,\r
471       ContextMenuInfo menuInfo) {\r
472     AdapterContextMenuInfo adapterContextMenuInfo = (AdapterContextMenuInfo) menuInfo;\r
473     final RowBase row = (RowBase) getListAdapter().getItem(adapterContextMenuInfo.position);\r
474 \r
475     final MenuItem addToWordlist = menu.add(getString(R.string.addToWordList, wordList.getName()));\r
476     addToWordlist.setOnMenuItemClickListener(new OnMenuItemClickListener() {\r
477       public boolean onMenuItemClick(MenuItem item) {\r
478         onAppendToWordList(row);\r
479         return false;\r
480       }\r
481     });\r
482 \r
483     final MenuItem copy = menu.add(android.R.string.copy);\r
484     copy.setOnMenuItemClickListener(new OnMenuItemClickListener() {\r
485       public boolean onMenuItemClick(MenuItem item) {\r
486         onCopy(row);\r
487         return false;\r
488       }\r
489     });\r
490 \r
491   }\r
492   \r
493   @Override\r
494   protected void onListItemClick(ListView l, View v, int row, long id) {\r
495     defocusSearchText();\r
496     \r
497     if (clickOpensContextMenu && dictRaf != null) {\r
498       openContextMenu(v);\r
499     }\r
500   }\r
501   \r
502   void onAppendToWordList(final RowBase row) {\r
503     defocusSearchText();\r
504     \r
505     final StringBuilder rawText = new StringBuilder();\r
506     rawText.append(\r
507         new SimpleDateFormat("yyyy.MM.dd HH:mm:ss").format(new Date()))\r
508         .append("\t");\r
509     rawText.append(index.longName).append("\t");\r
510     rawText.append(row.getTokenRow(true).getToken()).append("\t");\r
511     rawText.append(row.getRawText(saveOnlyFirstSubentry));\r
512     Log.d(LOG, "Writing : " + rawText);\r
513 \r
514     try {\r
515       wordList.getParentFile().mkdirs();\r
516       final PrintWriter out = new PrintWriter(\r
517           new FileWriter(wordList, true));\r
518       out.println(rawText.toString());\r
519       out.close();\r
520     } catch (IOException e) {\r
521       Log.e(LOG, "Unable to append to " + wordList.getAbsolutePath(), e);\r
522       Toast.makeText(this, getString(R.string.failedAddingToWordList, wordList.getAbsolutePath()), Toast.LENGTH_LONG);\r
523     }\r
524     return;\r
525   }\r
526   \r
527   /**\r
528    * Called when user clicks outside of search text, so that they can start\r
529    * typing again immediately.\r
530    */\r
531   void defocusSearchText() {\r
532     //Log.d(LOG, "defocusSearchText");\r
533     // Request focus so that if we start typing again, it clears the text input.\r
534     getListView().requestFocus();\r
535     \r
536     // Visual indication that a new keystroke will clear the search text.\r
537     searchText.selectAll();\r
538   }\r
539 \r
540   void onCopy(final RowBase row) {\r
541     defocusSearchText();\r
542 \r
543     Log.d(LOG, "Copy, row=" + row);\r
544     final StringBuilder result = new StringBuilder();\r
545     result.append(row.getRawText(false));\r
546     final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);\r
547     clipboardManager.setText(result.toString());\r
548     Log.d(LOG, "Copied: " + result);\r
549   }\r
550 \r
551   @Override\r
552   public boolean onKeyDown(final int keyCode, final KeyEvent event) {\r
553     if (event.getUnicodeChar() != 0) {\r
554       if (!searchText.hasFocus()) {\r
555         searchText.setText("" + (char) event.getUnicodeChar());\r
556         onSearchTextChange(searchText.getText().toString());\r
557         searchText.requestFocus();\r
558         Selection.moveToRightEdge(searchText.getText(), searchText.getLayout());\r
559       }\r
560       return true;\r
561     }\r
562     if (keyCode == KeyEvent.KEYCODE_BACK) {\r
563     }\r
564     if (keyCode == KeyEvent.KEYCODE_ENTER) {\r
565       Log.d(LOG, "Trying to hide soft keyboard.");\r
566       final InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);\r
567       inputManager.hideSoftInputFromWindow(this.getCurrentFocus().getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);\r
568       return true;\r
569     }\r
570     return super.onKeyDown(keyCode, event);\r
571   }\r
572 \r
573 \r
574   // --------------------------------------------------------------------------\r
575   // SearchOperation\r
576   // --------------------------------------------------------------------------\r
577 \r
578   private void searchFinished(final SearchOperation searchOperation) {\r
579     if (searchOperation.interrupted.get()) {\r
580       Log.d(LOG, "Search operation was interrupted: " + searchOperation);\r
581       return;\r
582     }\r
583     if (searchOperation != this.currentSearchOperation) {\r
584       Log.d(LOG, "Stale searchOperation finished: " + searchOperation);\r
585       return;\r
586     }\r
587     \r
588     final Index.IndexEntry searchResult = searchOperation.searchResult;\r
589     Log.d(LOG, "searchFinished: " + searchOperation + ", searchResult=" + searchResult);\r
590 \r
591     currentSearchOperation = null;\r
592 \r
593     uiHandler.postDelayed(new Runnable() {\r
594       @Override\r
595       public void run() {\r
596         if (currentSearchOperation == null) {\r
597           jumpToRow(searchResult.startRow);\r
598         } else {\r
599           Log.d(LOG, "More coming, waiting for currentSearchOperation.");\r
600         }\r
601       }\r
602     }, 50);\r
603     \r
604 //    if (!searchResult.success) {\r
605 //      if (vibrator != null) {\r
606 //        vibrator.vibrate(VIBRATE_MILLIS);\r
607 //      }\r
608 //      searchText.setText(searchResult.longestPrefixString);\r
609 //      searchText.setSelection(searchResult.longestPrefixString.length());\r
610 //      return;\r
611 //    }\r
612     \r
613   }\r
614   \r
615   private final void jumpToRow(final int row) {\r
616     setSelection(row);\r
617     getListView().setSelected(true);\r
618   }\r
619 \r
620   final class SearchOperation implements Runnable {\r
621     \r
622     final AtomicBoolean interrupted = new AtomicBoolean(false);\r
623     final String searchText;\r
624     final Index index;\r
625     \r
626     long searchStartMillis;\r
627 \r
628     Index.IndexEntry searchResult;\r
629     \r
630     boolean done = false;\r
631     \r
632     SearchOperation(final String searchText, final Index index) {\r
633       this.searchText = searchText.trim();\r
634       this.index = index;\r
635     }\r
636     \r
637     public String toString() {\r
638       return String.format("SearchOperation(%s,%s)", searchText, interrupted.toString());\r
639     }\r
640 \r
641     @Override\r
642     public void run() {\r
643       try {\r
644         searchStartMillis = System.currentTimeMillis();\r
645         searchResult = index.findInsertionPoint(searchText, interrupted);\r
646         Log.d(LOG, "searchText=" + searchText + ", searchDuration="\r
647             + (System.currentTimeMillis() - searchStartMillis) + ", interrupted="\r
648             + interrupted.get());\r
649         if (!interrupted.get()) {\r
650           uiHandler.post(new Runnable() {\r
651             @Override\r
652             public void run() {            \r
653               searchFinished(SearchOperation.this);\r
654             }\r
655           });\r
656         }\r
657       } finally {\r
658         synchronized (this) {\r
659           done = true;\r
660           this.notifyAll();\r
661         }\r
662       }\r
663     }\r
664   }\r
665 \r
666   \r
667   // --------------------------------------------------------------------------\r
668   // IndexAdapter\r
669   // --------------------------------------------------------------------------\r
670 \r
671   final class IndexAdapter extends BaseAdapter {\r
672     \r
673     final Index index;\r
674 \r
675     IndexAdapter(final Index index) {\r
676       this.index = index;\r
677     }\r
678 \r
679     @Override\r
680     public int getCount() {\r
681       return index.rows.size();\r
682     }\r
683 \r
684     @Override\r
685     public RowBase getItem(int position) {\r
686       return index.rows.get(position);\r
687     }\r
688 \r
689     @Override\r
690     public long getItemId(int position) {\r
691       return getItem(position).index();\r
692     }\r
693 \r
694     @Override\r
695     public View getView(int position, View convertView, ViewGroup parent) {\r
696       final RowBase row = index.rows.get(position);\r
697       if (row instanceof PairEntry.Row) {\r
698         return getView((PairEntry.Row) row, parent);\r
699       } else if (row instanceof TokenRow) {\r
700         return getView((TokenRow) row, parent);\r
701       } else {\r
702         throw new IllegalArgumentException("Unsupported Row type: " + row.getClass());\r
703       }\r
704     }\r
705 \r
706     private View getView(PairEntry.Row row, ViewGroup parent) {\r
707       final TableLayout result = new TableLayout(parent.getContext());\r
708       final PairEntry entry = row.getEntry();\r
709       final int rowCount = entry.pairs.size();\r
710       for (int r = 0; r < rowCount; ++r) {\r
711         final TableRow tableRow = new TableRow(result.getContext());\r
712 \r
713         final EditText column1 = new EditText(tableRow.getContext());\r
714         final EditText column2 = new EditText(tableRow.getContext());\r
715         final TableRow.LayoutParams layoutParams = new TableRow.LayoutParams();\r
716         layoutParams.weight = 0.5f;\r
717 \r
718         if (r > 0) {\r
719           final TextView spacer = new TextView(tableRow.getContext());\r
720           spacer.setText(" • ");\r
721           tableRow.addView(spacer);\r
722         }\r
723         tableRow.addView(column1, layoutParams);\r
724         if (r > 0) {\r
725           final TextView spacer = new TextView(tableRow.getContext());\r
726           spacer.setText(" • ");\r
727           tableRow.addView(spacer);\r
728         }\r
729         tableRow.addView(column2, layoutParams);\r
730 \r
731         column1.setWidth(1);\r
732         column2.setWidth(1);\r
733 \r
734         // TODO: color words by gender\r
735         final Pair pair = entry.pairs.get(r);\r
736         final String col1Text = Language.fixBidiText(index.swapPairEntries ? pair.lang2 : pair.lang1);\r
737         column1.setText(col1Text, TextView.BufferType.SPANNABLE);\r
738         final Spannable col1Spannable = (Spannable) column1.getText();\r
739         \r
740         int startPos = 0;\r
741         final String token = row.getTokenRow(true).getToken();\r
742         while ((startPos = col1Text.indexOf(token, startPos)) != -1) {\r
743           col1Spannable.setSpan(new StyleSpan(Typeface.BOLD), startPos,\r
744               startPos + token.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);\r
745           startPos += token.length();\r
746         }\r
747 \r
748         String col2Text = index.swapPairEntries ? pair.lang1 : pair.lang2;\r
749         col2Text = Language.fixBidiText(col2Text);\r
750         column2.setText(col2Text, TextView.BufferType.NORMAL);\r
751         \r
752         column1.setTextSize(TypedValue.COMPLEX_UNIT_SP, fontSizeSp);\r
753         column2.setTextSize(TypedValue.COMPLEX_UNIT_SP, fontSizeSp);\r
754         column2.setBackgroundResource(theme.otherLangBg);\r
755         \r
756         column2.setOnLongClickListener(new EditText.OnLongClickListener() {\r
757           @Override\r
758           public boolean onLongClick(View v) {\r
759             final int start = column2.getSelectionStart();\r
760             final int end = column2.getSelectionStart();\r
761             Log.i(LOG, "Long click on: " + column2.getText().toString().substring(start, end));\r
762             return false;\r
763           }\r
764         });\r
765 \r
766         result.addView(tableRow);\r
767       }\r
768 \r
769       return result;\r
770     }\r
771 \r
772     private View getView(TokenRow row, ViewGroup parent) {\r
773       final Context context = parent.getContext();\r
774       final TextView textView = new TextView(context);\r
775       textView.setText(row.getToken());\r
776       textView.setBackgroundResource(row.hasMainEntry ? theme.tokenRowMainBg : theme.tokenRowOtherBg);\r
777       // Doesn't work:\r
778       //textView.setTextColor(android.R.color.secondary_text_light);\r
779       textView.setTextAppearance(context, theme.tokenRowFg);\r
780       textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 5 * fontSizeSp / 4);\r
781       return textView;\r
782     }\r
783     \r
784   }\r
785 \r
786   // --------------------------------------------------------------------------\r
787   // SearchText\r
788   // --------------------------------------------------------------------------\r
789 \r
790   void onSearchTextChange(final String text) {\r
791     if (dictRaf == null) {\r
792       Log.d(LOG, "searchText changed during shutdown, doing nothing.");\r
793       return;\r
794     }\r
795     if (!searchText.isFocused()) {\r
796       Log.d(LOG, "searchText changed without focus, doing nothing.");\r
797       return;\r
798     }\r
799     Log.d(LOG, "onSearchTextChange: " + text);    \r
800     if (currentSearchOperation != null) {\r
801       Log.d(LOG, "Interrupting currentSearchOperation.");\r
802       currentSearchOperation.interrupted.set(true);\r
803     }\r
804     currentSearchOperation = new SearchOperation(text, index);\r
805     searchExecutor.execute(currentSearchOperation);\r
806   }\r
807   \r
808   private class SearchTextWatcher implements TextWatcher {\r
809     public void afterTextChanged(final Editable searchTextEditable) {\r
810       if (searchText.hasFocus()) {\r
811         Log.d(LOG, "Search text changed with focus: " + searchText.getText());\r
812         // If they were typing to cause the change, update the UI.\r
813         onSearchTextChange(searchText.getText().toString());\r
814       }\r
815     }\r
816 \r
817     public void beforeTextChanged(CharSequence arg0, int arg1, int arg2,\r
818         int arg3) {\r
819     }\r
820 \r
821     public void onTextChanged(CharSequence arg0, int arg1, int arg2, int arg3) {\r
822     }\r
823   }\r
824 \r
825 }\r