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