X-Git-Url: http://gitweb.fperrin.net/?a=blobdiff_plain;f=src%2Fcom%2Fhughes%2Fandroid%2Fdictionary%2FDictionaryActivity.java;h=6bfb379dd3bc6b54dccd2ee104bf7f5ac4a39a07;hb=cbcff0e7ca442adc1064f60b56ff2e551243576f;hp=a1521fa2fecae8db0c4d9367576dc1812e97249d;hpb=7570dd30c59e1fa56758af1e3b97b531df47fe9e;p=Dictionary.git diff --git a/src/com/hughes/android/dictionary/DictionaryActivity.java b/src/com/hughes/android/dictionary/DictionaryActivity.java index a1521fa..6bfb379 100644 --- a/src/com/hughes/android/dictionary/DictionaryActivity.java +++ b/src/com/hughes/android/dictionary/DictionaryActivity.java @@ -1,3 +1,17 @@ +// Copyright 2011 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package com.hughes.android.dictionary; import java.io.File; @@ -6,162 +20,257 @@ import java.io.IOException; import java.io.PrintWriter; import java.io.RandomAccessFile; import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Collections; import java.util.Date; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; import java.util.concurrent.Executor; import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import android.app.Dialog; import android.app.ListActivity; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.graphics.Typeface; +import android.net.Uri; import android.os.Bundle; import android.os.Handler; -import android.os.Vibrator; import android.preference.PreferenceManager; import android.text.ClipboardManager; import android.text.Editable; +import android.text.Selection; import android.text.Spannable; import android.text.TextWatcher; +import android.text.method.LinkMovementMethod; import android.text.style.StyleSpan; import android.util.Log; +import android.util.TypedValue; import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.view.ContextMenu.ContextMenuInfo; import android.view.MenuItem.OnMenuItemClickListener; +import android.view.MotionEvent; +import android.view.View; import android.view.View.OnClickListener; +import android.view.View.OnLongClickListener; +import android.view.ViewGroup; +import android.view.WindowManager; import android.view.inputmethod.InputMethodManager; import android.widget.AdapterView; +import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.BaseAdapter; import android.widget.Button; import android.widget.EditText; +import android.widget.ImageView; +import android.widget.LinearLayout; import android.widget.ListAdapter; import android.widget.ListView; import android.widget.TableLayout; import android.widget.TableRow; import android.widget.TextView; import android.widget.Toast; -import android.widget.AdapterView.AdapterContextMenuInfo; +import com.hughes.android.dictionary.DictionaryInfo.IndexInfo; import com.hughes.android.dictionary.engine.Dictionary; +import com.hughes.android.dictionary.engine.EntrySource; import com.hughes.android.dictionary.engine.Index; +import com.hughes.android.dictionary.engine.Index.IndexEntry; import com.hughes.android.dictionary.engine.PairEntry; +import com.hughes.android.dictionary.engine.PairEntry.Pair; import com.hughes.android.dictionary.engine.RowBase; import com.hughes.android.dictionary.engine.TokenRow; -import com.hughes.android.util.PersistentObjectCache; +import com.hughes.android.dictionary.engine.TransliteratorManager; +import com.hughes.android.util.IntentLauncher; +import com.hughes.android.util.NonLinkClickableSpan; public class DictionaryActivity extends ListActivity { static final String LOG = "QuickDic"; - - static final int VIBRATE_MILLIS = 100; - int dictIndex = 0; + private String initialSearchText; + + DictionaryApplication application; + File dictFile = null; RandomAccessFile dictRaf = null; Dictionary dictionary = null; int indexIndex = 0; Index index = null; + List rowsToShow = null; // if not null, just show these rows. // package for test. final Handler uiHandler = new Handler(); - private final Executor searchExecutor = Executors.newSingleThreadExecutor(); + private final Executor searchExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + return new Thread(r, "searchExecutor"); + } + }); private SearchOperation currentSearchOperation = null; + C.Theme theme = C.Theme.LIGHT; + int fontSizeSp; EditText searchText; Button langButton; // Never null. private File wordList = null; private boolean saveOnlyFirstSubentry = false; + private boolean clickOpensContextMenu = false; // Visible for testing. ListAdapter indexAdapter = null; - - private Vibrator vibrator = null; + final SearchTextWatcher searchTextWatcher = new SearchTextWatcher(); + + /** + * For some languages, loading the transliterators used in this search takes + * a long time, so we fire it up on a different thread, and don't invoke it + * from the main thread until it's already finished once. + */ + private volatile boolean indexPrepFinished = false; + + + public DictionaryActivity() { } - public static Intent getIntent(final Context context, final int dictIndex, final int indexIndex, final String searchToken) { - setDictionaryPrefs(context, dictIndex, indexIndex, searchToken); - + public static Intent getLaunchIntent(final File dictFile, final int indexIndex, final String searchToken) { final Intent intent = new Intent(); intent.setClassName(DictionaryActivity.class.getPackage().getName(), DictionaryActivity.class.getName()); + intent.putExtra(C.DICT_FILE, dictFile.getPath()); + intent.putExtra(C.INDEX_INDEX, indexIndex); + intent.putExtra(C.SEARCH_TOKEN, searchToken); return intent; } - - public static void setDictionaryPrefs(final Context context, - final int dictIndex, final int indexIndex, final String searchToken) { - final SharedPreferences.Editor prefs = PreferenceManager.getDefaultSharedPreferences(context).edit(); - prefs.putInt(C.DICT_INDEX, dictIndex); - prefs.putInt(C.INDEX_INDEX, indexIndex); - prefs.putString(C.SEARCH_TOKEN, searchToken); - prefs.commit(); + + @Override + protected void onSaveInstanceState(final Bundle outState) { + super.onSaveInstanceState(outState); + Log.d(LOG, "onSaveInstanceState: " + searchText.getText().toString()); + outState.putInt(C.INDEX_INDEX, indexIndex); + outState.putString(C.SEARCH_TOKEN, searchText.getText().toString()); } - public static void clearDictionaryPrefs(final Context context) { - final SharedPreferences.Editor prefs = PreferenceManager.getDefaultSharedPreferences(context).edit(); - prefs.remove(C.DICT_INDEX); - prefs.remove(C.INDEX_INDEX); - prefs.remove(C.SEARCH_TOKEN); - prefs.commit(); + @Override + protected void onRestoreInstanceState(final Bundle outState) { + super.onRestoreInstanceState(outState); + Log.d(LOG, "onRestoreInstanceState: " + outState.getString(C.SEARCH_TOKEN)); + onCreate(outState); } @Override - public void onCreate(Bundle savedInstanceState) { + public void onCreate(Bundle savedInstanceState) { + setTheme(((DictionaryApplication)getApplication()).getSelectedTheme().themeId); + + Log.d(LOG, "onCreate:" + this); super.onCreate(savedInstanceState); + + application = (DictionaryApplication) getApplication(); + theme = application.getSelectedTheme(); + + // Clear them so that if something goes wrong, we won't relaunch. + clearDictionaryPrefs(this); - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + final Intent intent = getIntent(); + dictFile = new File(intent.getStringExtra(C.DICT_FILE)); try { - PersistentObjectCache.init(this); - QuickDicConfig quickDicConfig = PersistentObjectCache.init( - this).read(C.DICTIONARY_CONFIGS, QuickDicConfig.class); - dictIndex = prefs.getInt(C.DICT_INDEX, 0) ; - final DictionaryConfig dictionaryConfig = quickDicConfig.dictionaryConfigs.get(dictIndex); - dictRaf = new RandomAccessFile(dictionaryConfig.localFile, "r"); + final String name = application.getDictionaryName(dictFile.getName()); + this.setTitle("QuickDic: " + name); + dictRaf = new RandomAccessFile(dictFile, "r"); dictionary = new Dictionary(dictRaf); } catch (Exception e) { Log.e(LOG, "Unable to load dictionary.", e); - Toast.makeText(this, getString(R.string.invalidDictionary, "", e.getMessage()), Toast.LENGTH_LONG); - startActivity(DictionaryEditActivity.getIntent(dictIndex)); + if (dictRaf != null) { + try { + dictRaf.close(); + } catch (IOException e1) { + Log.e(LOG, "Unable to close dictRaf.", e1); + } + dictRaf = null; + } + Toast.makeText(this, getString(R.string.invalidDictionary, "", e.getMessage()), Toast.LENGTH_LONG).show(); + startActivity(DictionaryManagerActivity.getLaunchIntent()); finish(); return; } - + indexIndex = intent.getIntExtra(C.INDEX_INDEX, 0); + if (savedInstanceState != null) { + indexIndex = savedInstanceState.getInt(C.INDEX_INDEX, indexIndex); + } + indexIndex %= dictionary.indices.size(); + Log.d(LOG, "Loading index " + indexIndex); + index = dictionary.indices.get(indexIndex); + setListAdapter(new IndexAdapter(index)); + // Pre-load the collators. - searchExecutor.execute(new Runnable() { + new Thread(new Runnable() { public void run() { final long startMillis = System.currentTimeMillis(); - for (final Index index : dictionary.indices) { - final com.ibm.icu.text.Collator c = index.sortLanguage.getCollator(); - if (c.compare("pre-print", "preppy") >= 0) { - Log.e(LOG, c.getClass() - + " is buggy, lookups may not work properly."); + try { + TransliteratorManager.init(new TransliteratorManager.Callback() { + @Override + public void onTransliteratorReady() { + uiHandler.post(new Runnable() { + @Override + public void run() { + onSearchTextChange(searchText.getText().toString()); + } + }); + } + }); + + for (final Index index : dictionary.indices) { + final String searchToken = index.sortedIndexEntries.get(0).token; + final IndexEntry entry = index.findExact(searchToken); + if (!searchToken.equals(entry.token)) { + Log.e(LOG, "Couldn't find token: " + searchToken + ", " + entry.token); + } } + indexPrepFinished = true; + } catch (Exception e) { + Log.w(LOG, "Exception while prepping. This can happen if dictionary is closed while search is happening."); } - Log.d(LOG, "Loading collators took:" - + (System.currentTimeMillis() - startMillis)); + Log.d(LOG, "Prepping indices took:" + + (System.currentTimeMillis() - startMillis)); } - }); + }).start(); - indexIndex = prefs.getInt(C.INDEX_INDEX, 0) % dictionary.indices.size(); - index = dictionary.indices.get(indexIndex); - setListAdapter(new IndexAdapter(index)); + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + final String fontSize = prefs.getString(getString(R.string.fontSizeKey), "14"); + try { + fontSizeSp = Integer.parseInt(fontSize.trim()); + } catch (NumberFormatException e) { + fontSizeSp = 12; + } + setContentView(R.layout.dictionary_activity); searchText = (EditText) findViewById(R.id.SearchText); + searchText.setTextSize(TypedValue.COMPLEX_UNIT_SP, fontSizeSp); + langButton = (Button) findViewById(R.id.LangButton); searchText.requestFocus(); - searchText.addTextChangedListener(new SearchTextWatcher()); - searchText.setText(prefs.getString(C.SEARCH_TOKEN, "")); - Log.d(LOG, "Trying to restore searchText=" + searchText.getText()); + searchText.addTextChangedListener(searchTextWatcher); + String text = ""; + if (savedInstanceState != null) { + text = savedInstanceState.getString(C.SEARCH_TOKEN); + if (text == null) { + text = ""; + } + } + setSearchText(text, true); + Log.d(LOG, "Trying to restore searchText=" + text); final Button clearSearchTextButton = (Button) findViewById(R.id.ClearSearchTextButton); clearSearchTextButton.setOnClickListener(new OnClickListener() { @@ -179,6 +288,13 @@ public class DictionaryActivity extends ListActivity { onLanguageButton(); } }); + langButton.setOnLongClickListener(new OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + onLanguageButtonLongClick(v.getContext()); + return true; + } + }); updateLangButton(); final Button upButton = (Button) findViewById(R.id.UpButton); @@ -199,10 +315,12 @@ public class DictionaryActivity extends ListActivity { public void onItemSelected(AdapterView adapterView, View arg1, final int position, long id) { if (!searchText.isFocused()) { - // TODO: don't do this if multi words are entered. - final RowBase row = (RowBase) getListAdapter().getItem(position); - final TokenRow tokenRow = row.getTokenRow(true); - searchText.setText(tokenRow.getToken()); + if (!isFiltered()) { + final RowBase row = (RowBase) getListAdapter().getItem(position); + Log.d(LOG, "onItemSelected: " + row.index()); + final TokenRow tokenRow = row.getTokenRow(true); + searchText.setText(tokenRow.getToken()); + } } } @@ -218,32 +336,82 @@ public class DictionaryActivity extends ListActivity { wordList = new File(prefs.getString(getString(R.string.wordListFileKey), getString(R.string.wordListFileDefault))); saveOnlyFirstSubentry = prefs.getBoolean(getString(R.string.saveOnlyFirstSubentryKey), false); - if (prefs.getBoolean(getString(R.string.vibrateOnFailedSearchKey), true)) { - vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); - } + clickOpensContextMenu = prefs.getBoolean(getString(R.string.clickOpensContextMenuKey), false); + //if (prefs.getBoolean(getString(R.string.vibrateOnFailedSearchKey), true)) { + // vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); + //} Log.d(LOG, "wordList=" + wordList + ", saveOnlyFirstSubentry=" + saveOnlyFirstSubentry); + + setDictionaryPrefs(this, dictFile, indexIndex, searchText.getText().toString()); } - @Override protected void onResume() { super.onResume(); + if (PreferenceActivity.prefsMightHaveChanged) { + PreferenceActivity.prefsMightHaveChanged = false; + finish(); + startActivity(getIntent()); + } + if (initialSearchText != null) { + setSearchText(initialSearchText, true); + } } - + @Override protected void onPause() { super.onPause(); } + + private static void setDictionaryPrefs(final Context context, + final File dictFile, final int indexIndex, final String searchToken) { + final SharedPreferences.Editor prefs = PreferenceManager.getDefaultSharedPreferences(context).edit(); + prefs.putString(C.DICT_FILE, dictFile.getPath()); + prefs.putInt(C.INDEX_INDEX, indexIndex); + prefs.putString(C.SEARCH_TOKEN, searchToken); + prefs.commit(); + } + + private static void clearDictionaryPrefs(final Context context) { + final SharedPreferences.Editor prefs = PreferenceManager.getDefaultSharedPreferences(context).edit(); + prefs.remove(C.DICT_FILE); + prefs.remove(C.INDEX_INDEX); + prefs.remove(C.SEARCH_TOKEN); + prefs.commit(); + } + @Override protected void onDestroy() { super.onDestroy(); - setDictionaryPrefs(this, dictIndex, indexIndex, searchText.getText().toString()); + if (dictRaf == null) { + return; + } + + // Before we close the RAF, we have to wind the current search down. + if (currentSearchOperation != null) { + Log.d(LOG, "Interrupting search to shut down."); + final SearchOperation searchOperation = currentSearchOperation; + currentSearchOperation = null; + searchOperation.interrupted.set(true); + synchronized (searchOperation) { + while (!searchOperation.done) { + try { + searchOperation.wait(); + } catch (InterruptedException e) { + Log.d(LOG, "Interrupted.", e); + } + } + } + } + try { + Log.d(LOG, "Closing RAF."); dictRaf.close(); } catch (IOException e) { Log.e(LOG, "Failed to close dictionary", e); } + dictRaf = null; } // -------------------------------------------------------------------------- @@ -260,7 +428,13 @@ public class DictionaryActivity extends ListActivity { } void updateLangButton() { - langButton.setText(index.shortName.toUpperCase()); +// final LanguageResources languageResources = Language.isoCodeToResources.get(index.shortName); +// if (languageResources != null && languageResources.flagId != 0) { +// langButton.setCompoundDrawablesWithIntrinsicBounds(0, 0, languageResources.flagId, 0); +// } else { +// langButton.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); + langButton.setText(index.shortName); +// } } void onLanguageButton() { @@ -268,17 +442,78 @@ public class DictionaryActivity extends ListActivity { currentSearchOperation.interrupted.set(true); currentSearchOperation = null; } + changeIndexGetFocusAndResearch((indexIndex + 1)% dictionary.indices.size()); + } + + void onLanguageButtonLongClick(final Context context) { + final Dialog dialog = new Dialog(context); + dialog.setContentView(R.layout.select_dictionary_dialog); + dialog.setTitle(R.string.selectDictionary); + + final List installedDicts = ((DictionaryApplication)getApplication()).getUsableDicts(); + ListView listView = (ListView) dialog.findViewById(android.R.id.list); + listView.setAdapter(new BaseAdapter() { + @Override + public View getView(int position, View convertView, ViewGroup parent) { + final LinearLayout result = new LinearLayout(parent.getContext()); + final DictionaryInfo dictionaryInfo = getItem(position); + final Button button = new Button(parent.getContext()); + final String name = application.getDictionaryName(dictionaryInfo.uncompressedFilename); + button.setText(name); + final IntentLauncher intentLauncher = new IntentLauncher(parent.getContext(), getLaunchIntent(application.getPath(dictionaryInfo.uncompressedFilename), 0, "")) { + @Override + protected void onGo() { + dialog.dismiss(); + DictionaryActivity.this.finish(); + }; + }; + button.setOnClickListener(intentLauncher); + + final LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + layoutParams.width = 0; + layoutParams.weight = 1.0f; + button.setLayoutParams(layoutParams); + + result.addView(button); + return result; + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public DictionaryInfo getItem(int position) { + return installedDicts.get(position); + } + + @Override + public int getCount() { + return installedDicts.size(); + } + }); - indexIndex = (indexIndex + 1) % dictionary.indices.size(); + dialog.show(); + } + + + private void changeIndexGetFocusAndResearch(final int newIndex) { + indexIndex = newIndex; index = dictionary.indices.get(indexIndex); indexAdapter = new IndexAdapter(index); - Log.d(LOG, "onLanguageButton, newLang=" + index.longName); + Log.d(LOG, "changingIndex, newLang=" + index.longName); setListAdapter(indexAdapter); updateLangButton(); + searchText.requestFocus(); // Otherwise, nothing may happen. onSearchTextChange(searchText.getText().toString()); + setDictionaryPrefs(this, dictFile, indexIndex, searchText.getText().toString()); } void onUpDownButton(final boolean up) { + if (isFiltered()) { + return; + } final int firstVisibleRow = getListView().getFirstVisiblePosition(); final RowBase row = index.rows.get(firstVisibleRow); final TokenRow tokenRow = row.getTokenRow(true); @@ -293,11 +528,13 @@ public class DictionaryActivity extends ListActivity { // Down destIndexEntry = Math.min(tokenRow.referenceIndex + 1, index.sortedIndexEntries.size()); } - final Index.IndexEntry dest = index.sortedIndexEntries.get(destIndexEntry); - searchText.setText(dest.token); Log.d(LOG, "onUpDownButton, destIndexEntry=" + dest.token); - //jumpToRow(index.sortedIndexEntries.get(destIndexEntry).startRow); + searchText.removeTextChangedListener(searchTextWatcher); + searchText.setText(dest.token); + Selection.moveToRightEdge(searchText.getText(), searchText.getLayout()); + jumpToRow(index.sortedIndexEntries.get(destIndexEntry).startRow); + searchText.addTextChangedListener(searchTextWatcher); } // -------------------------------------------------------------------------- @@ -306,39 +543,64 @@ public class DictionaryActivity extends ListActivity { @Override public boolean onCreateOptionsMenu(final Menu menu) { - - { - final MenuItem preferences = menu.add(getString(R.string.preferences)); - preferences.setOnMenuItemClickListener(new OnMenuItemClickListener() { - public boolean onMenuItemClick(final MenuItem menuItem) { - startActivity(new Intent(DictionaryActivity.this, - PreferenceActivity.class)); - return false; - } - }); - } + application.onCreateGlobalOptionsMenu(this, menu); { - final MenuItem dictionaryList = menu.add(getString(R.string.dictionaryList)); - dictionaryList.setOnMenuItemClickListener(new OnMenuItemClickListener() { - public boolean onMenuItemClick(final MenuItem menuItem) { - startActivity(DictionaryListActivity.getIntent(DictionaryActivity.this)); - startActivity(DictionaryListActivity.getIntent(DictionaryActivity.this)); - return false; - } - }); + final MenuItem dictionaryList = menu.add(getString(R.string.dictionaryManager)); + dictionaryList.setOnMenuItemClickListener(new OnMenuItemClickListener() { + public boolean onMenuItemClick(final MenuItem menuItem) { + startActivity(DictionaryManagerActivity.getLaunchIntent()); + finish(); + return false; + } + }); } { - final MenuItem dictionaryList = menu.add(getString(R.string.editDictionary)); - dictionaryList.setOnMenuItemClickListener(new OnMenuItemClickListener() { + final MenuItem aboutDictionary = menu.add(getString(R.string.aboutDictionary)); + aboutDictionary.setOnMenuItemClickListener(new OnMenuItemClickListener() { public boolean onMenuItemClick(final MenuItem menuItem) { - final Intent intent = DictionaryEditActivity.getIntent(dictIndex); - startActivity(intent); + final Context context = getListView().getContext(); + final Dialog dialog = new Dialog(context); + dialog.setContentView(R.layout.about_dictionary_dialog); + final TextView textView = (TextView) dialog.findViewById(R.id.text); + + final String name = application.getDictionaryName(dictFile.getName()); + dialog.setTitle(name); + + final StringBuilder builder = new StringBuilder(); + final DictionaryInfo dictionaryInfo = dictionary.getDictionaryInfo(); + dictionaryInfo.uncompressedBytes = dictFile.length(); + if (dictionaryInfo != null) { + builder.append(dictionaryInfo.dictInfo).append("\n\n"); + builder.append(getString(R.string.dictionaryPath, dictFile.getPath())).append("\n"); + builder.append(getString(R.string.dictionarySize, dictionaryInfo.uncompressedBytes)).append("\n"); + builder.append(getString(R.string.dictionaryCreationTime, dictionaryInfo.creationMillis)).append("\n"); + for (final IndexInfo indexInfo : dictionaryInfo.indexInfos) { + builder.append("\n"); + builder.append(getString(R.string.indexName, indexInfo.shortName)).append("\n"); + builder.append(getString(R.string.mainTokenCount, indexInfo.mainTokenCount)).append("\n"); + } + builder.append("\n"); + builder.append(getString(R.string.sources)).append("\n"); + for (final EntrySource source : dictionary.sources) { + builder.append(getString(R.string.sourceInfo, source.getName(), source.getNumEntries())).append("\n"); + } + } +// } else { +// builder.append(getString(R.string.invalidDictionary)); +// } + textView.setText(builder.toString()); + + dialog.show(); + final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(); + layoutParams.width = WindowManager.LayoutParams.FILL_PARENT; + layoutParams.height = WindowManager.LayoutParams.FILL_PARENT; + dialog.getWindow().setAttributes(layoutParams); return false; } }); - } + } return true; } @@ -369,15 +631,58 @@ public class DictionaryActivity extends ListActivity { return false; } }); + + if (selectedSpannableText != null) { + final String selectedText = selectedSpannableText; + final MenuItem searchForSelection = menu.add(getString(R.string.searchForSelection, selectedSpannableText)); + searchForSelection.setOnMenuItemClickListener(new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + int indexToUse = -1; + for (int i = 0; i < dictionary.indices.size(); ++i) { + final Index index = dictionary.indices.get(i); + if (indexPrepFinished) { + System.out.println("Doing index lookup: on " + selectedText); + final IndexEntry indexEntry = index.findExact(selectedText); + if (indexEntry != null) { + final TokenRow tokenRow = index.rows.get(indexEntry.startRow).getTokenRow(false); + if (tokenRow != null && tokenRow.hasMainEntry) { + indexToUse = i; + break; + } + } + } else { + Log.w(LOG, "Skipping findExact on index " + index.shortName); + } + } + if (indexToUse == -1) { + indexToUse = selectedSpannableIndex; + } + final boolean changeIndex = indexIndex != indexToUse; + setSearchText(selectedText, !changeIndex); // If we're not changing index, we have to triggerSearch. + if (changeIndex) { + changeIndexGetFocusAndResearch(indexToUse); + } + // Give focus back to list view because typing is done. + getListView().requestFocus(); + return false; + } + }); + } + } @Override protected void onListItemClick(ListView l, View v, int row, long id) { - openContextMenu(v); + defocusSearchText(); + if (clickOpensContextMenu && dictRaf != null) { + openContextMenu(v); + } } void onAppendToWordList(final RowBase row) { + defocusSearchText(); + final StringBuilder rawText = new StringBuilder(); rawText.append( new SimpleDateFormat("yyyy.MM.dd HH:mm:ss").format(new Date())) @@ -386,6 +691,7 @@ public class DictionaryActivity extends ListActivity { rawText.append(row.getTokenRow(true).getToken()).append("\t"); rawText.append(row.getRawText(saveOnlyFirstSubentry)); Log.d(LOG, "Writing : " + rawText); + try { wordList.getParentFile().mkdirs(); final PrintWriter out = new PrintWriter( @@ -398,8 +704,23 @@ public class DictionaryActivity extends ListActivity { } return; } + + /** + * Called when user clicks outside of search text, so that they can start + * typing again immediately. + */ + void defocusSearchText() { + //Log.d(LOG, "defocusSearchText"); + // Request focus so that if we start typing again, it clears the text input. + getListView().requestFocus(); + + // Visual indication that a new keystroke will clear the search text. + searchText.selectAll(); + } void onCopy(final RowBase row) { + defocusSearchText(); + Log.d(LOG, "Copy, row=" + row); final StringBuilder result = new StringBuilder(); result.append(row.getRawText(false)); @@ -412,15 +733,38 @@ public class DictionaryActivity extends ListActivity { public boolean onKeyDown(final int keyCode, final KeyEvent event) { if (event.getUnicodeChar() != 0) { if (!searchText.hasFocus()) { - searchText.setText("" + (char) event.getUnicodeChar()); - onSearchTextChange(searchText.getText().toString()); - searchText.requestFocus(); + setSearchText("" + (char) event.getUnicodeChar(), true); } return true; } + if (keyCode == KeyEvent.KEYCODE_BACK) { + Log.d(LOG, "Clearing dictionary prefs."); + DictionaryActivity.clearDictionaryPrefs(this); + } + if (keyCode == KeyEvent.KEYCODE_ENTER) { + Log.d(LOG, "Trying to hide soft keyboard."); + final InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + inputManager.hideSoftInputFromWindow(this.getCurrentFocus().getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); + return true; + } return super.onKeyDown(keyCode, event); } + private void setSearchText(final String text, final boolean triggerSearch) { + if (!triggerSearch) { + getListView().requestFocus(); + } + searchText.setText(text); + searchText.requestFocus(); + if (searchText.getLayout() != null) { + // Surprising, but this can crash when you rotate... + Selection.moveToRightEdge(searchText.getText(), searchText.getLayout()); + } + if (triggerSearch) { + onSearchTextChange(text); + } + } + // -------------------------------------------------------------------------- // SearchOperation @@ -439,16 +783,28 @@ public class DictionaryActivity extends ListActivity { final Index.IndexEntry searchResult = searchOperation.searchResult; Log.d(LOG, "searchFinished: " + searchOperation + ", searchResult=" + searchResult); - jumpToRow(searchResult.longestPrefix.startRow); + currentSearchOperation = null; + uiHandler.postDelayed(new Runnable() { + @Override + public void run() { + if (currentSearchOperation == null) { + if (searchResult != null) { + if (isFiltered()) { + clearFiltered(); + } + jumpToRow(searchResult.startRow); + } else if (searchOperation.multiWordSearchResult != null) { + // Multi-row search.... + setFiltered(searchOperation); + } else { + throw new IllegalStateException("This should never happen."); + } + } else { + Log.d(LOG, "More coming, waiting for currentSearchOperation."); + } + } + }, 20); -// if (!searchResult.success) { -// if (vibrator != null) { -// vibrator.vibrate(VIBRATE_MILLIS); -// } -// searchText.setText(searchResult.longestPrefixString); -// searchText.setSelection(searchResult.longestPrefixString.length()); -// return; -// } } private final void jumpToRow(final int row) { @@ -456,15 +812,20 @@ public class DictionaryActivity extends ListActivity { getListView().setSelected(true); } + static final Pattern WHITESPACE = Pattern.compile("\\s+"); final class SearchOperation implements Runnable { final AtomicBoolean interrupted = new AtomicBoolean(false); final String searchText; + List searchTokens; // filled in for multiWord. final Index index; long searchStartMillis; Index.IndexEntry searchResult; + List multiWordSearchResult; + + boolean done = false; SearchOperation(final String searchText, final Index index) { this.searchText = searchText.trim(); @@ -477,18 +838,31 @@ public class DictionaryActivity extends ListActivity { @Override public void run() { - searchStartMillis = System.currentTimeMillis(); - searchResult = index.findInsertionPoint(searchText, interrupted); - Log.d(LOG, "searchText=" + searchText + ", searchDuration=" - + (System.currentTimeMillis() - searchStartMillis) + ", interrupted=" - + interrupted.get()); - if (!interrupted.get()) { - uiHandler.post(new Runnable() { - @Override - public void run() { - searchFinished(SearchOperation.this); - } - }); + try { + searchStartMillis = System.currentTimeMillis(); + final String[] searchTokenArray = WHITESPACE.split(searchText); + if (searchTokenArray.length == 1) { + searchResult = index.findInsertionPoint(searchText, interrupted); + } else { + searchTokens = Arrays.asList(searchTokenArray); + multiWordSearchResult = index.multiWordSearch(searchTokens, interrupted); + } + Log.d(LOG, "searchText=" + searchText + ", searchDuration=" + + (System.currentTimeMillis() - searchStartMillis) + ", interrupted=" + + interrupted.get()); + if (!interrupted.get()) { + uiHandler.post(new Runnable() { + @Override + public void run() { + searchFinished(SearchOperation.this); + } + }); + } + } finally { + synchronized (this) { + done = true; + this.notifyAll(); + } } } } @@ -498,22 +872,32 @@ public class DictionaryActivity extends ListActivity { // IndexAdapter // -------------------------------------------------------------------------- - static final class IndexAdapter extends BaseAdapter { + final class IndexAdapter extends BaseAdapter { final Index index; + final List rows; + final Set toHighlight; IndexAdapter(final Index index) { this.index = index; + rows = index.rows; + this.toHighlight = null; + } + + IndexAdapter(final Index index, final List rows, final List toHighlight) { + this.index = index; + this.rows = rows; + this.toHighlight = new LinkedHashSet(toHighlight); } @Override public int getCount() { - return index.rows.size(); + return rows.size(); } @Override public RowBase getItem(int position) { - return index.rows.get(position); + return rows.get(position); } @Override @@ -522,82 +906,202 @@ public class DictionaryActivity extends ListActivity { } @Override - public View getView(int position, View convertView, ViewGroup parent) { - final RowBase row = index.rows.get(position); + public TableLayout getView(int position, View convertView, ViewGroup parent) { + final TableLayout result; + if (convertView instanceof TableLayout) { + result = (TableLayout) convertView; + result.removeAllViews(); + } else { + result = new TableLayout(parent.getContext()); + } + final RowBase row = getItem(position); if (row instanceof PairEntry.Row) { - return getView((PairEntry.Row) row, parent); + return getView(position, (PairEntry.Row) row, parent, result); } else if (row instanceof TokenRow) { - return getView((TokenRow) row, parent); + return getView((TokenRow) row, parent, result); } else { throw new IllegalArgumentException("Unsupported Row type: " + row.getClass()); } } - private View getView(PairEntry.Row row, ViewGroup parent) { - final TableLayout result = new TableLayout(parent.getContext()); + private TableLayout getView(final int position, PairEntry.Row row, ViewGroup parent, final TableLayout result) { final PairEntry entry = row.getEntry(); - final int rowCount = entry.pairs.length; + final int rowCount = entry.pairs.size(); + + final TableRow.LayoutParams layoutParams = new TableRow.LayoutParams(); + layoutParams.weight = 0.5f; + for (int r = 0; r < rowCount; ++r) { final TableRow tableRow = new TableRow(result.getContext()); - TextView column1 = new TextView(tableRow.getContext()); - TextView column2 = new TextView(tableRow.getContext()); - final TableRow.LayoutParams layoutParams = new TableRow.LayoutParams(); - layoutParams.weight = 0.5f; + final TextView col1 = new TextView(tableRow.getContext()); + final TextView col2 = new TextView(tableRow.getContext()); + // Set the columns in the table. if (r > 0) { - final TextView spacer = new TextView(tableRow.getContext()); - spacer.setText(" • "); - tableRow.addView(spacer); + final TextView bullet = new TextView(tableRow.getContext()); + bullet.setText(" • "); + tableRow.addView(bullet); } - tableRow.addView(column1, layoutParams); + tableRow.addView(col1, layoutParams); + final TextView margin = new TextView(tableRow.getContext()); + margin.setText(" "); + tableRow.addView(margin); if (r > 0) { - final TextView spacer = new TextView(tableRow.getContext()); - spacer.setText(" • "); - tableRow.addView(spacer); + final TextView bullet = new TextView(tableRow.getContext()); + bullet.setText(" • "); + tableRow.addView(bullet); } - tableRow.addView(column2, layoutParams); - - column1.setWidth(1); - column2.setWidth(1); + tableRow.addView(col2, layoutParams); + col1.setWidth(1); + col2.setWidth(1); + + // Set what's in the columns. - // TODO: color words by gender - final String col1Text = index.swapPairEntries ? entry.pairs[r].lang2 : entry.pairs[r].lang1; - column1.setText(col1Text, TextView.BufferType.SPANNABLE); - final Spannable col1Spannable = (Spannable) column1.getText(); + final Pair pair = entry.pairs.get(r); + final String col1Text = index.swapPairEntries ? pair.lang2 : pair.lang1; + final String col2Text = index.swapPairEntries ? pair.lang1 : pair.lang2; - int startPos = 0; - final String token = row.getTokenRow(true).getToken(); - while ((startPos = col1Text.indexOf(token, startPos)) != -1) { - col1Spannable.setSpan(new StyleSpan(Typeface.BOLD), startPos, - startPos + token.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE); - startPos += token.length(); + col1.setText(col1Text, TextView.BufferType.SPANNABLE); + col2.setText(col2Text, TextView.BufferType.SPANNABLE); + + // Bold the token instances in col1. + final Set toBold = toHighlight != null ? this.toHighlight : Collections.singleton(row.getTokenRow(true).getToken()); + final Spannable col1Spannable = (Spannable) col1.getText(); + for (final String token : toBold) { + int startPos = 0; + while ((startPos = col1Text.indexOf(token, startPos)) != -1) { + col1Spannable.setSpan(new StyleSpan(Typeface.BOLD), startPos, + startPos + token.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE); + startPos += token.length(); + } } - - final String col2Text = index.swapPairEntries ? entry.pairs[r].lang1 : entry.pairs[r].lang2; - column2.setText(col2Text, TextView.BufferType.NORMAL); - + + createTokenLinkSpans(col1, col1Spannable, col1Text); + createTokenLinkSpans(col2, (Spannable) col2.getText(), col2Text); + + col1.setTextSize(TypedValue.COMPLEX_UNIT_SP, fontSizeSp); + col2.setTextSize(TypedValue.COMPLEX_UNIT_SP, fontSizeSp); + // col2.setBackgroundResource(theme.otherLangBg); + + if (index.swapPairEntries) { + col2.setOnLongClickListener(textViewLongClickListenerIndex0); + col1.setOnLongClickListener(textViewLongClickListenerIndex1); + } else { + col1.setOnLongClickListener(textViewLongClickListenerIndex0); + col2.setOnLongClickListener(textViewLongClickListenerIndex1); + } + result.addView(tableRow); } + // Because we have a Button inside a ListView row: + // http://groups.google.com/group/android-developers/browse_thread/thread/3d96af1530a7d62a?pli=1 + result.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); + result.setClickable(true); + result.setFocusable(true); + result.setLongClickable(true); + result.setBackgroundResource(android.R.drawable.menuitem_background); + result.setOnClickListener(new TextView.OnClickListener() { + @Override + public void onClick(View v) { + DictionaryActivity.this.onListItemClick(getListView(), v, position, position); + } + }); + return result; } - private View getView(TokenRow row, ViewGroup parent) { - final TextView textView = new TextView(parent.getContext()); + private TableLayout getView(TokenRow row, ViewGroup parent, final TableLayout result) { + final Context context = parent.getContext(); + final TextView textView = new TextView(context); textView.setText(row.getToken()); - textView.setTextSize(20); - return textView; + // Doesn't work: + //textView.setTextColor(android.R.color.secondary_text_light); + textView.setTextAppearance(context, theme.tokenRowFg); + textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 5 * fontSizeSp / 4); + + final TableRow tableRow = new TableRow(result.getContext()); + tableRow.addView(textView); + tableRow.setBackgroundResource(row.hasMainEntry ? theme.tokenRowMainBg : theme.tokenRowOtherBg); + result.addView(tableRow); + return result; } } + static final Pattern CHAR_DASH = Pattern.compile("['\\p{L}0-9]+"); + + private void createTokenLinkSpans(final TextView textView, final Spannable spannable, final String text) { + // Saw from the source code that LinkMovementMethod sets the selection! + // http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/2.3.1_r1/android/text/method/LinkMovementMethod.java#LinkMovementMethod + textView.setMovementMethod(LinkMovementMethod.getInstance()); + final Matcher matcher = CHAR_DASH.matcher(text); + while (matcher.find()) { + spannable.setSpan(new NonLinkClickableSpan(), matcher.start(), matcher.end(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE); + } + } + + + String selectedSpannableText = null; + int selectedSpannableIndex = -1; + + @Override + public boolean onTouchEvent(MotionEvent event) { + selectedSpannableText = null; + selectedSpannableIndex = -1; + return super.onTouchEvent(event); + } + + private class TextViewLongClickListener implements OnLongClickListener { + final int index; + + private TextViewLongClickListener(final int index) { + this.index = index; + } + + @Override + public boolean onLongClick(final View v) { + final TextView textView = (TextView) v; + final int start = textView.getSelectionStart(); + final int end = textView.getSelectionEnd(); + if (start >= 0 && end >= 0) { + selectedSpannableText = textView.getText().subSequence(start, end).toString(); + selectedSpannableIndex = index; + } + return false; + } + } + final TextViewLongClickListener textViewLongClickListenerIndex0 = new TextViewLongClickListener(0); + final TextViewLongClickListener textViewLongClickListenerIndex1 = new TextViewLongClickListener(1); + + // -------------------------------------------------------------------------- // SearchText // -------------------------------------------------------------------------- void onSearchTextChange(final String text) { + if ("thadolina".equals(text)) { + final Dialog dialog = new Dialog(getListView().getContext()); + dialog.setContentView(R.layout.thadolina_dialog); + dialog.setTitle("Ti amo, amore mio!"); + final ImageView imageView = (ImageView) dialog.findViewById(R.id.thadolina_image); + imageView.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + final Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse("https://sites.google.com/site/cfoxroxvday/vday2012")); + startActivity(intent); + } + }); + dialog.show(); + } + if (dictRaf == null) { + Log.d(LOG, "searchText changed during shutdown, doing nothing."); + return; + } if (!searchText.isFocused()) { + Log.d(LOG, "searchText changed without focus, doing nothing."); return; } Log.d(LOG, "onSearchTextChange: " + text); @@ -626,4 +1130,26 @@ public class DictionaryActivity extends ListActivity { } } + // -------------------------------------------------------------------------- + // Filtered results. + // -------------------------------------------------------------------------- + + boolean isFiltered() { + return rowsToShow != null; + } + + void setFiltered(final SearchOperation searchOperation) { + ((Button) findViewById(R.id.UpButton)).setEnabled(false); + ((Button) findViewById(R.id.DownButton)).setEnabled(false); + rowsToShow = searchOperation.multiWordSearchResult; + setListAdapter(new IndexAdapter(index, rowsToShow, searchOperation.searchTokens)); + } + + void clearFiltered() { + ((Button) findViewById(R.id.UpButton)).setEnabled(true); + ((Button) findViewById(R.id.DownButton)).setEnabled(true); + setListAdapter(new IndexAdapter(index)); + rowsToShow = null; + } + }