From: Thad Hughes Date: Thu, 26 Jan 2012 00:01:36 +0000 (-0800) Subject: Added multiword search to dictionary. X-Git-Url: http://gitweb.fperrin.net/?p=Dictionary.git;a=commitdiff_plain;h=590af2616dcdacdd2c4d56e71e3a502a4b3b81da Added multiword search to dictionary. --- diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 5496c76..3e5b8b7 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -55,6 +55,7 @@ + diff --git a/res/raw/help.html b/res/raw/help.html index 681978f..3813f6d 100644 --- a/res/raw/help.html +++ b/res/raw/help.html @@ -4,21 +4,25 @@ -

Dictionary manager

+

Dictionary manager

This screen lists all the available and installed dictionaries.
  • Click a dictionary to download or open it.
  • Long-click on a dictionary to move it to the top of the list or delete it.
-

Dictionary

+

Dictionary

This screen shows dictionary entries. This is a massive list that you can scroll all the way through, or you can type in the search text to jump there in the list. Entries in the list are filed in multiple places, under all relevant words. -

What to do: +

Searching

  • Type a single word to search for it (multi-word searches not yet supported). +
  • QuickDic tries to sort words using a romanized transliteration, so you can try searching for non-Latin words using the Latin alphabet. +
+

Other

+
  • If the search text box isn't focused, you can start typing and its contents will be replaced.
  • Click the button to the right of the search box to switch dictionary directions: EN->DE to DE->EN.
  • Long-click the button to the right of the search box to pick a new dictionary. diff --git a/res/values/strings.xml b/res/values/strings.xml index 99f5cd0..557988d 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -32,8 +32,6 @@ Search Text Select dictionary… Switch to %s - Preferences… - About QuickDic… Add to word list: %s Find: %s Failure adding to word list: %s @@ -57,8 +55,14 @@ Downloading: %1$,d of %2$,d bytes. Unzipping: %1$,d of %2$,d bytes. Finished: %,d bytes. - "Error downloading file: \n%s" + Error downloading file: \n%s + + About QuickDic… + Preferences… + Help + + wordListFile Word list file diff --git a/src/com/hughes/android/dictionary/DictionaryActivity.java b/src/com/hughes/android/dictionary/DictionaryActivity.java index 66aee42..d9ef0cd 100644 --- a/src/com/hughes/android/dictionary/DictionaryActivity.java +++ b/src/com/hughes/android/dictionary/DictionaryActivity.java @@ -179,8 +179,8 @@ public class DictionaryActivity extends ListActivity { return; } - Log.d(LOG, "Loading index."); indexIndex = intent.getIntExtra(C.INDEX_INDEX, 0) % dictionary.indices.size(); + Log.d(LOG, "Loading index " + indexIndex); index = dictionary.indices.get(indexIndex); setListAdapter(new IndexAdapter(index)); @@ -400,27 +400,6 @@ public class DictionaryActivity extends ListActivity { changeIndex((indexIndex + 1)% dictionary.indices.size()); } - static class OpenIndexButton extends Button implements OnClickListener { - - final Activity activity; - final Intent intent; - - public OpenIndexButton(final Context context, final Activity activity, final String text, final Intent intent) { - super(context); - this.activity = activity; - this.intent = intent; - setOnClickListener(this); - setText(text, BufferType.NORMAL); - } - - @Override - public void onClick(View v) { - activity.finish(); - getContext().startActivity(intent); - } - - } - void onLanguageButtonLongClick(final Context context) { final Dialog dialog = new Dialog(context); dialog.setContentView(R.layout.select_dictionary_dialog); @@ -489,6 +468,7 @@ public class DictionaryActivity extends ListActivity { updateLangButton(); searchText.requestFocus(); // Otherwise, nothing may happen. onSearchTextChange(searchText.getText().toString()); + setDictionaryPrefs(this, dictFile, indexIndex, searchText.getText().toString()); } void onUpDownButton(final boolean up) { @@ -583,7 +563,6 @@ public class DictionaryActivity extends ListActivity { @Override protected void onListItemClick(ListView l, View v, int row, long id) { defocusSearchText(); - if (clickOpensContextMenu && dictRaf != null) { openContextMenu(v); } @@ -791,7 +770,7 @@ public class DictionaryActivity extends ListActivity { public View getView(int position, final View convertView, ViewGroup parent) { final RowBase row = index.rows.get(position); if (row instanceof PairEntry.Row) { - return getView((PairEntry.Row) row, parent, convertView); + return getView(position, (PairEntry.Row) row, parent, convertView); } else if (row instanceof TokenRow) { return getView((TokenRow) row, parent, convertView); } else { @@ -799,7 +778,7 @@ public class DictionaryActivity extends ListActivity { } } - private View getView(PairEntry.Row row, ViewGroup parent, final View convertView) { + private View getView(final int position, PairEntry.Row row, ViewGroup parent, final View convertView) { final TableLayout result = new TableLayout(parent.getContext()); final PairEntry entry = row.getEntry(); final int rowCount = entry.pairs.size(); @@ -865,6 +844,20 @@ public class DictionaryActivity extends ListActivity { col2.setOnLongClickListener(textViewLongClickListenerIndex1); } + // 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(null, v, position, position); + } + }); + result.addView(tableRow); } diff --git a/src/com/hughes/android/dictionary/DictionaryApplication.java b/src/com/hughes/android/dictionary/DictionaryApplication.java index 77db795..c2379ee 100644 --- a/src/com/hughes/android/dictionary/DictionaryApplication.java +++ b/src/com/hughes/android/dictionary/DictionaryApplication.java @@ -281,20 +281,20 @@ public class DictionaryApplication extends Application { public void onCreateGlobalOptionsMenu( final Context context, final Menu menu) { - final MenuItem help = menu.add(getString(R.string.about)); - help.setOnMenuItemClickListener(new OnMenuItemClickListener() { + final MenuItem about = menu.add(getString(R.string.about)); + about.setOnMenuItemClickListener(new OnMenuItemClickListener() { public boolean onMenuItemClick(final MenuItem menuItem) { - startActivity(HelpActivity.getLaunchIntent()); + final Intent intent = new Intent().setClassName(AboutActivity.class + .getPackage().getName(), AboutActivity.class.getCanonicalName()); + context.startActivity(intent); return false; } }); - final MenuItem about = menu.add(getString(R.string.about)); - about.setOnMenuItemClickListener(new OnMenuItemClickListener() { + final MenuItem help = menu.add(getString(R.string.help)); + help.setOnMenuItemClickListener(new OnMenuItemClickListener() { public boolean onMenuItemClick(final MenuItem menuItem) { - final Intent intent = new Intent().setClassName(AboutActivity.class - .getPackage().getName(), AboutActivity.class.getCanonicalName()); - startActivity(intent); + context.startActivity(HelpActivity.getLaunchIntent()); return false; } }); @@ -305,7 +305,7 @@ public class DictionaryApplication extends Application { PreferenceActivity.prefsMightHaveChanged = true; final Intent intent = new Intent().setClassName(PreferenceActivity.class .getPackage().getName(), PreferenceActivity.class.getCanonicalName()); - startActivity(intent); + context.startActivity(intent); return false; } }); diff --git a/src/com/hughes/android/dictionary/DictionaryManagerActivity.java b/src/com/hughes/android/dictionary/DictionaryManagerActivity.java index 3638aac..1ba06fc 100644 --- a/src/com/hughes/android/dictionary/DictionaryManagerActivity.java +++ b/src/com/hughes/android/dictionary/DictionaryManagerActivity.java @@ -31,7 +31,6 @@ import android.view.ContextMenu.ContextMenuInfo; import android.view.Menu; import android.view.MenuItem; import android.view.MenuItem.OnMenuItemClickListener; -import android.view.ViewGroup.LayoutParams; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; @@ -41,6 +40,7 @@ import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.AdapterView.OnItemClickListener; import android.widget.BaseAdapter; import android.widget.Button; +import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; @@ -240,6 +240,10 @@ public class DictionaryManagerActivity extends ListActivity { layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT; downloadButton.setLayoutParams(layoutParams); row.addView(downloadButton); + } else { + final ImageView checkMark = new ImageView(parent.getContext()); + checkMark.setImageResource(android.R.drawable.checkbox_on_background); + row.addView(checkMark); } final TextView textView = new TextView(parent.getContext()); diff --git a/src/com/hughes/android/dictionary/HelpActivity.java b/src/com/hughes/android/dictionary/HelpActivity.java index 2d5c141..0f424c9 100644 --- a/src/com/hughes/android/dictionary/HelpActivity.java +++ b/src/com/hughes/android/dictionary/HelpActivity.java @@ -35,7 +35,7 @@ public final class HelpActivity extends Activity { super.onCreate(savedInstanceState); setContentView(R.layout.help_activity); final String html = StringUtil.readToString(getResources().openRawResource(R.raw.help)); - final WebView webView = (WebView) findViewById(R.layout.help_activity); + final WebView webView = (WebView) findViewById(R.id.helpWebView); webView.loadData(html, "text/html", "utf-8"); } diff --git a/src/com/hughes/android/dictionary/engine/Index.java b/src/com/hughes/android/dictionary/engine/Index.java index 19e0ecc..6107b8b 100644 --- a/src/com/hughes/android/dictionary/engine/Index.java +++ b/src/com/hughes/android/dictionary/engine/Index.java @@ -22,7 +22,11 @@ import java.io.PrintStream; import java.io.RandomAccessFile; import java.util.ArrayList; import java.util.Collection; +import java.util.EnumMap; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import com.hughes.android.dictionary.DictionaryInfo; @@ -178,13 +182,12 @@ public final class Index implements RAFSerializable { } public IndexEntry findInsertionPoint(String token, final AtomicBoolean interrupted) { - if (TransliteratorManager.init(null)) { - final Transliterator normalizer = normalizer(); - token = normalizer.transliterate(token); - } else { - // Do our best since the Transliterators aren't up yet. - token = token.toLowerCase(); - } + final int index = findInsertionPointIndex(token, interrupted); + return index != -1 ? sortedIndexEntries.get(index) : null; + } + + public int findInsertionPointIndex(String token, final AtomicBoolean interrupted) { + token = normalizeToken(token); int start = 0; int end = sortedIndexEntries.size(); @@ -193,14 +196,14 @@ public final class Index implements RAFSerializable { while (start < end) { final int mid = (start + end) / 2; if (interrupted.get()) { - return null; + return -1; } final IndexEntry midEntry = sortedIndexEntries.get(mid); final int comp = sortCollator.compare(token, midEntry.normalizedToken()); if (comp == 0) { final int result = windBackCase(token, mid, interrupted); - return sortedIndexEntries.get(result); + return result; } else if (comp < 0) { //System.out.println("Upper bound: " + midEntry + ", norm=" + midEntry.normalizedToken() + ", mid=" + mid); end = mid; @@ -213,7 +216,7 @@ public final class Index implements RAFSerializable { // If we search for a substring of a string that's in there, return that. int result = Math.min(start, sortedIndexEntries.size() - 1); result = windBackCase(sortedIndexEntries.get(result).normalizedToken(), result, interrupted); - return sortedIndexEntries.get(result); + return result; } private final int windBackCase(final String token, int result, final AtomicBoolean interrupted) { @@ -229,5 +232,66 @@ public final class Index implements RAFSerializable { public IndexInfo getIndexInfo() { return new DictionaryInfo.IndexInfo(shortName, sortedIndexEntries.size(), mainTokenCount); } + + final List multiWordSearch(final List searchTokens, final AtomicBoolean interrupted) { + final List result = new ArrayList(); + + // Heuristic: use the longest searchToken as the base. + String searchToken = null; + for (int i = 0; i < searchTokens.size(); ++i) { + if (interrupted.get()) { return null; } + final String normalized = normalizeToken(searchTokens.get(i)); + // Normalize them all. + searchTokens.set(i, normalized); + if (searchToken == null || normalized.length() > searchToken.length()) { + searchToken = normalized; + } + } + + final int insertionPointIndex = findInsertionPointIndex(searchToken, interrupted); + if (insertionPointIndex == -1 || interrupted.get()) { + return null; + } + + // The things that match. + // TODO: use a key + final Map> matches = new EnumMap>(RowMatchType.class); + for (final RowMatchType rowMatchType : RowMatchType.values()) { + matches.put(rowMatchType, new LinkedHashSet()); + } + + for (int index = insertionPointIndex; index < sortedIndexEntries.size(); ++index) { + if (interrupted.get()) { return null; } + final IndexEntry indexEntry = sortedIndexEntries.get(index); + if (!indexEntry.normalizedToken.equals(searchToken)) { + break; + } + + for (int rowIndex = indexEntry.startRow; rowIndex < indexEntry.startRow + indexEntry.numRows; ++rowIndex) { + if (interrupted.get()) { return null; } + final RowBase row = rows.get(rowIndex); + final RowMatchType matchType = row.matches(searchTokens, normalizer, swapPairEntries); + if (matchType != RowMatchType.NO_MATCH) { + matches.get(matchType).add(row); + } + } + } + + for (final Set rows : matches.values()) { + result.addAll(rows); + } + + return result; + } + + private String normalizeToken(final String searchToken) { + if (TransliteratorManager.init(null)) { + final Transliterator normalizer = normalizer(); + return normalizer.transliterate(searchToken); + } else { + // Do our best since the Transliterators aren't up yet. + return searchToken.toLowerCase(); + } + } } \ No newline at end of file diff --git a/src/com/hughes/android/dictionary/engine/PairEntry.java b/src/com/hughes/android/dictionary/engine/PairEntry.java index ebfd84a..e836a5c 100644 --- a/src/com/hughes/android/dictionary/engine/PairEntry.java +++ b/src/com/hughes/android/dictionary/engine/PairEntry.java @@ -19,9 +19,11 @@ import java.io.PrintStream; import java.io.RandomAccessFile; import java.util.ArrayList; import java.util.List; +import java.util.regex.Pattern; import com.hughes.util.raf.RAFSerializable; import com.hughes.util.raf.RAFSerializer; +import com.ibm.icu.text.Transliterator; public class PairEntry extends AbstractEntry implements RAFSerializable, Comparable { @@ -97,6 +99,11 @@ public class PairEntry extends AbstractEntry implements RAFSerializable searchTokens, final Transliterator normalizer, final boolean swapPairEntries) { + final int side = swapPairEntries ? 1 : 0; + final List pairs = getEntry().pairs; + final String[] pairSides = new String[pairs.size()]; + for (int i = 0; i < pairs.size(); ++i) { + pairSides[i] = normalizer.transform(pairs.get(i).get(side)); + } + for (int i = searchTokens.size() - 1; i >= 0; --i) { + final String searchToken = searchTokens.get(i); + boolean found = false; + for (final String pairSide : pairSides) { + found |= pairSide.contains(searchToken); + } + if (!found) { + return RowMatchType.NO_MATCH; + } + } + final StringBuilder regex = new StringBuilder(); + for (final String searchToken : searchTokens) { + if (regex.length() > 0) { + regex.append("[\\s]*"); + } + regex.append(Pattern.quote(searchToken)); + } + final Pattern pattern = Pattern.compile(regex.toString()); + for (final String pairSide : pairSides) { + if (pattern.matcher(pairSide).matches()) { + return RowMatchType.ORDERED_MATCH; + } + } + return RowMatchType.BAG_OF_WORDS_MATCH; + } } diff --git a/src/com/hughes/android/dictionary/engine/RowBase.java b/src/com/hughes/android/dictionary/engine/RowBase.java index 569fc00..e84eb73 100644 --- a/src/com/hughes/android/dictionary/engine/RowBase.java +++ b/src/com/hughes/android/dictionary/engine/RowBase.java @@ -17,9 +17,11 @@ package com.hughes.android.dictionary.engine; import java.io.IOException; import java.io.PrintStream; import java.io.RandomAccessFile; +import java.util.List; import com.hughes.util.IndexedObject; import com.hughes.util.raf.RAFListSerializer; +import com.ibm.icu.text.Transliterator; public abstract class RowBase extends IndexedObject { /** @@ -97,6 +99,8 @@ public abstract class RowBase extends IndexedObject { public abstract void print(PrintStream out); public abstract String getRawText(final boolean compact); + + public abstract RowMatchType matches(final List searchTokens, final Transliterator normalizer, boolean swapPairEntries); // RowBase must manage "disk-based" polymorphism. All other polymorphism is // dealt with in the normal manner. @@ -134,5 +138,5 @@ public abstract class RowBase extends IndexedObject { raf.writeInt(t.referenceIndex); } } - + } diff --git a/src/com/hughes/android/dictionary/engine/RowMatchType.java b/src/com/hughes/android/dictionary/engine/RowMatchType.java new file mode 100644 index 0000000..96ac4ad --- /dev/null +++ b/src/com/hughes/android/dictionary/engine/RowMatchType.java @@ -0,0 +1,23 @@ +// Copyright 2012 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.engine; + +public enum RowMatchType { + + ORDERED_MATCH, + BAG_OF_WORDS_MATCH, + NO_MATCH + +} diff --git a/src/com/hughes/android/dictionary/engine/TextEntry.java b/src/com/hughes/android/dictionary/engine/TextEntry.java index 8bef294..ec5bc39 100644 --- a/src/com/hughes/android/dictionary/engine/TextEntry.java +++ b/src/com/hughes/android/dictionary/engine/TextEntry.java @@ -17,9 +17,11 @@ package com.hughes.android.dictionary.engine; import java.io.IOException; import java.io.PrintStream; import java.io.RandomAccessFile; +import java.util.List; import com.hughes.util.raf.RAFSerializable; import com.hughes.util.raf.RAFSerializer; +import com.ibm.icu.text.Transliterator; public class TextEntry extends AbstractEntry implements RAFSerializable { @@ -28,6 +30,7 @@ public class TextEntry extends AbstractEntry implements RAFSerializable searchTokens, Transliterator normalizer, boolean swapPairEntries) { + return null; + } } diff --git a/src/com/hughes/android/dictionary/engine/TokenRow.java b/src/com/hughes/android/dictionary/engine/TokenRow.java index adb4c58..7b7736c 100644 --- a/src/com/hughes/android/dictionary/engine/TokenRow.java +++ b/src/com/hughes/android/dictionary/engine/TokenRow.java @@ -17,6 +17,9 @@ package com.hughes.android.dictionary.engine; import java.io.IOException; import java.io.PrintStream; import java.io.RandomAccessFile; +import java.util.List; + +import com.ibm.icu.text.Transliterator; public class TokenRow extends RowBase { @@ -61,5 +64,10 @@ public class TokenRow extends RowBase { return getToken(); } + @Override + public RowMatchType matches(List searchTokens, Transliterator normalizer, boolean swapPairEntries) { + return RowMatchType.NO_MATCH; + } + }